tools/integration_tests/util/setup/setup.go (470 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package setup import ( "context" "flag" "fmt" "log" "math/rand" "os" "os/exec" "path" "path/filepath" "runtime/debug" "strconv" "strings" "testing" "time" "cloud.google.com/go/storage" "github.com/googlecloudplatform/gcsfuse/v2/tools/integration_tests/util/operations" "github.com/googlecloudplatform/gcsfuse/v2/tools/util" "google.golang.org/api/iterator" ) var isPresubmitRun = flag.Bool("presubmit", false, "Boolean flag to indicate if test-run is a presubmit run.") var isZonalBucketRun = flag.Bool("zonal", false, "Boolean flag to indicate if test-run should use a zonal bucket.") var testBucket = flag.String("testbucket", "", "The GCS bucket used for the test.") var mountedDirectory = flag.String("mountedDirectory", "", "The GCSFuse mounted directory used for the test.") var integrationTest = flag.Bool("integrationTest", false, "Run tests only when the flag value is true.") var testInstalledPackage = flag.Bool("testInstalledPackage", false, "[Optional] Run tests on the package pre-installed on the host machine. By default, integration tests build a new package to run the tests.") var testOnTPCEndPoint = flag.Bool("testOnTPCEndPoint", false, "Run tests on TPC endpoint only when the flag value is true.") const ( FilePermission_0600 = 0600 DirPermission_0755 = 0755 Charset = "abcdefghijklmnopqrstuvwxyz0123456789" PathEnvVariable = "PATH" GCSFuseLogFilePrefix = "gcsfuse-failed-integration-test-logs-" ProxyServerLogFilePrefix = "proxy-server-failed-integration-test-logs-" ) var ( binFile string logFile string testDir string mntDir string sbinFile string onlyDirMounted string dynamicBucketMounted string ) // Run the shell script to prepare the testData in the specified bucket. // First argument will be name of scipt script func RunScriptForTestData(args ...string) { cmd := exec.Command("/bin/bash", args...) out, err := cmd.CombinedOutput() if err != nil { log.Printf("Error: %s", out) panic(err) } } func IsPresubmitRun() bool { return *isPresubmitRun } func IsZonalBucketRun() bool { return *isZonalBucketRun } func IsIntegrationTest() bool { return *integrationTest } func TestBucket() string { return *testBucket } func TestInstalledPackage() bool { return *testInstalledPackage } func TestOnTPCEndPoint() bool { return *testOnTPCEndPoint } func MountedDirectory() string { return *mountedDirectory } func SetLogFile(logFileValue string) { logFile = logFileValue } func LogFile() string { return logFile } func BinFile() string { return binFile } func SbinFile() string { return sbinFile } func TestDir() string { return testDir } func SetMntDir(mntDirValue string) { mntDir = mntDirValue } func MntDir() string { return mntDir } // OnlyDirMounted returns the name of the directory mounted in case of only dir mount. func OnlyDirMounted() string { return onlyDirMounted } // SetOnlyDirMounted sets the name of the directory mounted in case of only dir mount. func SetOnlyDirMounted(onlyDirValue string) { onlyDirMounted = onlyDirValue } // DynamicBucketMounted returns the name of the bucket in case of dynamic mount. func DynamicBucketMounted() string { return dynamicBucketMounted } // SetDynamicBucketMounted sets the name of the bucket in case of dynamic mount. func SetDynamicBucketMounted(dynamicBucketValue string) { dynamicBucketMounted = dynamicBucketValue } func CompareFileContents(t *testing.T, fileName string, fileContent string) { content, err := os.ReadFile(fileName) if err != nil { t.Errorf("Read: %v", err) } if got := string(content); got != fileContent { t.Errorf("File content doesn't match. Expected: %q, Actual: %q", fileContent, got) } } func SetUpTestDir() error { var err error testDir, err = os.MkdirTemp("", "gcsfuse_readwrite_test_") if err != nil { return fmt.Errorf("TempDir: %w", err) } if !TestInstalledPackage() { err = util.BuildGcsfuse(testDir) if err != nil { return fmt.Errorf("BuildGcsfuse(%q): %w", TestDir(), err) } binFile = path.Join(TestDir(), "bin/gcsfuse") sbinFile = path.Join(TestDir(), "sbin/mount.gcsfuse") // mount.gcsfuse will find gcsfuse executable in mentioned locations. // https://github.com/GoogleCloudPlatform/gcsfuse/blob/master/tools/mount_gcsfuse/find.go#L59 // Setting PATH so that executable is found in test directory. err := os.Setenv(PathEnvVariable, path.Join(TestDir(), "bin")+string(filepath.ListSeparator)+os.Getenv(PathEnvVariable)) if err != nil { log.Printf("Error in setting PATH environment variable: %v", err.Error()) } } else { // when testInstalledPackage flag is set, gcsfuse is preinstalled on the // machine. Hence, here we are overwriting binFile to gcsfuse. binFile = "gcsfuse" sbinFile = "mount.gcsfuse" } logFile = path.Join(TestDir(), "gcsfuse.log") mntDir = path.Join(TestDir(), "mnt") err = os.Mkdir(mntDir, 0755) if err != nil { return fmt.Errorf("Mkdir(%q): %v", MntDir(), err) } return nil } func UnMount() error { fusermount, err := exec.LookPath("fusermount") if err != nil { return fmt.Errorf("cannot find fusermount: %w", err) } cmd := exec.Command(fusermount, "-uz", mntDir) if _, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("fusermount error: %w", err) } return nil } func ExecuteTest(m *testing.M) (successCode int) { successCode = m.Run() return successCode } func GenerateRandomString(length int) string { seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) b := make([]byte, length) for i := range b { b[i] = Charset[seededRand.Intn(len(Charset))] } return string(b) } func UnMountBucket() { err := UnMount() if err != nil { LogAndExit(fmt.Sprintf("Error in unmounting bucket: %v", err)) } } func SaveLogFileInCaseOfFailure(successCode int) { if successCode != 0 { SaveLogFileAsArtifact(LogFile(), GCSFuseLogFilePrefix+GenerateRandomString(5)) } } // Saves logFile as given artifactName in KOKORO or // TestDir based on where the test is ran. func SaveLogFileAsArtifact(logFile, artifactName string) { logDir := os.Getenv("KOKORO_ARTIFACTS_DIR") if logDir == "" { // Save log files in TestDir as this run is not on KOKORO. logDir = TestDir() } artifactPath := path.Join(logDir, artifactName) err := operations.CopyFile(logFile, artifactPath) if err != nil { log.Fatalf("Error in copying logfile to artifact path: %v", err) } log.Printf("Log file saved at %v", artifactPath) } // In case of test failure saves GCSFuse log file to // KOKORO artifacts directory if test ran on KOKORO // or saves to TestDir if test ran on local. func SaveGCSFuseLogFileInCaseOfFailure(tb testing.TB) { if !tb.Failed() || MountedDirectory() != "" { return } SaveLogFileAsArtifact(LogFile(), GCSFuseLogFilePrefix+strings.ReplaceAll(tb.Name(), "/", "_")+GenerateRandomString(5)) } // In case of test failure saves ProxyServerLogFile to // KOKORO artifacts directory if test ran on KOKORO // or saves to TestDir if test ran on local. func SaveProxyServerLogFileInCaseOfFailure(proxyServerLogFile string, tb testing.TB) { if !tb.Failed() { return } SaveLogFileAsArtifact(proxyServerLogFile, ProxyServerLogFilePrefix+strings.ReplaceAll(tb.Name(), "/", "_")+GenerateRandomString(5)) } func UnMountAndThrowErrorInFailure(flags []string, successCode int) { UnMountBucket() if successCode != 0 { // Print flag on which test fails f := strings.Join(flags, " ") log.Print("Test Fails on " + f) SaveLogFileInCaseOfFailure(successCode) } } func ExecuteTestForFlagsSet(flags []string, m *testing.M) (successCode int) { successCode = ExecuteTest(m) UnMountAndThrowErrorInFailure(flags, successCode) return } func ParseSetUpFlags() { flag.Parse() if !*integrationTest { log.Print("Pass --integrationTest flag to run the tests.") os.Exit(0) } } func IgnoreTestIfIntegrationTestFlagIsSet(t *testing.T) { flag.Parse() if *integrationTest { t.SkipNow() } } // IgnoreTestIfIntegrationTestFlagIsNotSet helps skip a test if --integrationTest flag is not set. // If the test uses TestMain, then one usually calls os.Exit() to skip the test, // but for non-TestMain tests, this helps skip integration tests if --integrationTest has not been passed. func IgnoreTestIfIntegrationTestFlagIsNotSet(t *testing.T) { flag.Parse() if !*integrationTest { t.SkipNow() } } func IgnoreTestIfPresubmitFlagIsSet(b *testing.B) { flag.Parse() if *isPresubmitRun { b.SkipNow() } } func ExitWithFailureIfBothTestBucketAndMountedDirectoryFlagsAreNotSet() { ParseSetUpFlags() if *testBucket == "" && *mountedDirectory == "" { log.Print("--testbucket or --mountedDirectory must be specified") os.Exit(1) } } func ExitWithFailureIfMountedDirectoryIsSetOrTestBucketIsNotSet() { ParseSetUpFlags() if *testBucket == "" { log.Print("Please pass the name of bucket to be mounted to --testBucket flag. It is required for this test.") os.Exit(1) } if *mountedDirectory != "" { log.Print("Please do not pass the mountedDirectory at test runtime. It is not supported for this test.") os.Exit(1) } } func RunTestsForMountedDirectoryFlag(m *testing.M) { // Execute tests for the mounted directory. if *mountedDirectory != "" { mntDir = *mountedDirectory successCode := ExecuteTest(m) os.Exit(successCode) } } func SetUpTestDirForTestBucketFlag() { if TestBucket() == "" { log.Fatal("Not running TestBucket tests as --testBucket flag is not set.") } if err := SetUpTestDir(); err != nil { log.Printf("setUpTestDir: %v\n", err) os.Exit(1) } } func SetUpLogDirForTestDirTests(logDirName string) (logDir string) { logDir = path.Join(TestDir(), logDirName) err := os.Mkdir(logDir, DirPermission_0755) if err != nil { log.Printf("os.Mkdir %s: %v\n", logDir, err) os.Exit(1) } return } func ValidateLogDirForMountedDirTests(logDirName string) (logDir string) { if *mountedDirectory == "" { return "" } logDir = path.Join(os.TempDir(), logDirName) _, err := os.Stat(logDir) if err != nil { log.Printf("validateLogDirForMountedDirTests %s: %v\n", logDir, err) os.Exit(1) } return } func LogAndExit(s string) { log.Print(s) log.Print(string(debug.Stack())) os.Exit(1) } // CleanUpDir cleans up the content in given directory. func CleanUpDir(directoryPath string) { dir, err := os.ReadDir(directoryPath) if err != nil { log.Printf("Error in reading directory: %v", err) } for _, d := range dir { err := os.RemoveAll(path.Join([]string{directoryPath, d.Name()}...)) if err != nil && !strings.Contains(err.Error(), "no such file or directory") { log.Printf("Error in removing directory: %v", err) } } } // SetupTestDirectory creates a test directory hierarchy in the mounted directory, // cleaning up any content present. It takes a testDirName which can include // slashes to create nested directories (e.g., "a/b/c"). func SetupTestDirectory(testDirName string) string { testDirPath := path.Join(MntDir(), testDirName) err := os.MkdirAll(testDirPath, DirPermission_0755) if err != nil && !strings.Contains(err.Error(), "file exists") { log.Printf("Error while setting up directory %s for testing: %v", testDirPath, err) } CleanUpDir(testDirPath) return testDirPath } // SetupTestDirectoryRecursive recursively creates a testDirectory in the mounted directory and cleans up // any content present in it. func SetupTestDirectoryRecursive(testDirName string) string { testDirPath := path.Join(MntDir(), testDirName) err := os.MkdirAll(testDirPath, DirPermission_0755) if err != nil && !strings.Contains(err.Error(), "file exists") { log.Printf("Error while setting up directory %s for testing: %v", testDirPath, err) } CleanUpDir(testDirPath) return testDirPath } // CleanupDirectoryOnGCS cleans up the object/directory path passed in parameter. func CleanupDirectoryOnGCS(ctx context.Context, client *storage.Client, directoryPathOnGCS string) { bucket, dirPath := GetBucketAndObjectBasedOnTypeOfMount(directoryPathOnGCS) bucketHandle := client.Bucket(bucket) it := bucketHandle.Objects(ctx, &storage.Query{Prefix: dirPath + "/"}) for { attrs, err := it.Next() if err == iterator.Done { break // No more objects found } if err != nil { log.Fatalf("Error iterating objects: %v", err) } if err := bucketHandle.Object(attrs.Name).Delete(ctx); err != nil { log.Printf("Error deleting object %s: %v", attrs.Name, err) } } } func AreBothMountedDirectoryAndTestBucketFlagsSet() bool { if MountedDirectory() != "" && TestBucket() != "" { return true } log.Print("Not running mounted directory tests as both --mountedDirectory and --testBucket flags are not set.") return false } func IsHierarchicalBucket(ctx context.Context, storageClient *storage.Client) bool { attrs, err := storageClient.Bucket(TestBucket()).Attrs(ctx) if err != nil { return false } if attrs.HierarchicalNamespace != nil && attrs.HierarchicalNamespace.Enabled { return true } return false } // Explicitly set the enable-hns config flag to true when running tests on the HNS bucket. func AddHNSFlagForHierarchicalBucket(ctx context.Context, storageClient *storage.Client) ([]string, error) { if !IsHierarchicalBucket(ctx, storageClient) { return nil, fmt.Errorf("bucket is not Hierarchical") } var flags []string mountConfig4 := map[string]interface{}{ "enable-hns": true, } filePath4 := YAMLConfigFile(mountConfig4, "config_hns.yaml") flags = append(flags, "--config-file="+filePath4) return flags, nil } func separateBucketAndObjectName(bucket, object string) (string, string) { bucketAndObjectPath := strings.SplitN(bucket, "/", 2) bucket = bucketAndObjectPath[0] object = path.Join(bucketAndObjectPath[1], object) return bucket, object } func GetBucketAndObjectBasedOnTypeOfMount(object string) (string, string) { bucket := TestBucket() if strings.Contains(TestBucket(), "/") { // This case arises when we run tests on mounted directory and pass // bucket/directory in testbucket flag. bucket, object = separateBucketAndObjectName(bucket, object) } if dynamicBucketMounted != "" { bucket = dynamicBucketMounted } if OnlyDirMounted() != "" { var suffix string if strings.HasSuffix(object, "/") { suffix = "/" } object = path.Join(OnlyDirMounted(), object) + suffix } return bucket, object } func MountGCSFuseWithGivenMountFunc(flags []string, mountFunc func([]string) error) { if *mountedDirectory == "" { // Mount GCSFuse only when tests are not running on mounted directory. if err := mountFunc(flags); err != nil { LogAndExit(fmt.Sprintf("Failed to mount GCSFuse: %v", err)) } } } func UnmountGCSFuseAndDeleteLogFile(rootDir string) { UnmountGCSFuse(rootDir) // delete log file created if *mountedDirectory == "" { err := os.Remove(LogFile()) if err != nil { LogAndExit(fmt.Sprintf("Error in deleting log file: %v", err)) } } } func UnmountGCSFuse(rootDir string) { SetMntDir(rootDir) if *mountedDirectory == "" { // Unmount GCSFuse only when tests are not running on mounted directory. err := UnMount() if err != nil { LogAndExit(fmt.Sprintf("Error in unmounting bucket: %v", err)) } } } func RunTestsOnlyForStaticMount(mountDir string, t *testing.T) { if strings.Contains(mountDir, *testBucket) || OnlyDirMounted() != "" { log.Println("This test will run only for static mounting...") t.SkipNow() } } // AppendFlagsToAllFlagsInTheFlagsSet appends each flag in newFlags to every flags present in the // flagsSet. // Input flagsSet: [][]string{{"--x", "--y"}, {"--x", "--z"}} // Input newFlags: {"--a", "--b", ""} // Output modified flagsSet: [][]string{{"--x", "--y", "--a"}, {"--x", "--z", "--a"},{"--x", "--y", "--b"},{"--x", "--z", "--b"},{"--x", "--y"}, {"--x", "--z"}} func AppendFlagsToAllFlagsInTheFlagsSet(flagsSet *[][]string, newFlags ...string) { var resultFlagsSet [][]string for _, flags := range *flagsSet { for _, newFlag := range newFlags { f := flags if strings.Compare(newFlag, "") != 0 { f = append(flags, newFlag) } resultFlagsSet = append(resultFlagsSet, f) } } *flagsSet = resultFlagsSet } // CreateFileAndCopyToMntDir creates a file of given size. // The same file will be copied to the mounted directory as well. func CreateFileAndCopyToMntDir(t *testing.T, fileSize int, dirName string) (string, string) { testDir := SetupTestDirectory(dirName) fileInLocalDisk := "test_file" + GenerateRandomString(5) + ".txt" filePathInLocalDisk := path.Join(os.TempDir(), fileInLocalDisk) filePathInMntDir := path.Join(testDir, fileInLocalDisk) CreateFileOnDiskAndCopyToMntDir(t, filePathInLocalDisk, filePathInMntDir, fileSize) return filePathInLocalDisk, filePathInMntDir } // CreateFileOnDiskAndCopyToMntDir creates a file of given size and copies to given path. func CreateFileOnDiskAndCopyToMntDir(t *testing.T, filePathInLocalDisk string, filePathInMntDir string, fileSize int) { RunScriptForTestData("../util/setup/testdata/write_content_of_fix_size_in_file.sh", filePathInLocalDisk, strconv.Itoa(fileSize)) err := operations.CopyFile(filePathInLocalDisk, filePathInMntDir) if err != nil { t.Errorf("Error in copying file:%v", err) } } func CreateProxyServerLogFile(t *testing.T) string { proxyServerLogFile := path.Join(TestDir(), "proxy-server-log-"+GenerateRandomString(5)) _, err := os.Create(proxyServerLogFile) if err != nil { t.Fatalf("Error in creating log file for proxy server: %v", err) } return proxyServerLogFile } func AppendProxyEndpointToFlagSet(flagSet *[]string, port int) { *flagSet = append(*flagSet, "--custom-endpoint="+fmt.Sprintf("http://localhost:%d/storage/v1/", port)) }