cuelang.org/go@v0.10.1/internal/_e2e/script_test.go (about) 1 // Copyright 2023 The CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package e2e_test 16 17 import ( 18 "bytes" 19 "cmp" 20 cryptorand "crypto/rand" 21 "fmt" 22 "os" 23 "os/exec" 24 "path" 25 "path/filepath" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/rogpeppe/go-internal/testscript" 31 ) 32 33 func TestMain(m *testing.M) { 34 cachedGobin := os.Getenv("CUE_CACHED_GOBIN") 35 if cachedGobin == "" { 36 // Install the cmd/cue version into a cached GOBIN so we can reuse it. 37 // TODO: use "go tool cue" once we can rely on Go's tool dependency tracking in go.mod. 38 // See: https://go.dev/issue/48429 39 cacheDir, err := os.UserCacheDir() 40 if err != nil { 41 panic(err) 42 } 43 cachedGobin = filepath.Join(cacheDir, "cue-e2e-gobin") 44 cmd := exec.Command("go", "install", "cuelang.org/go/cmd/cue") 45 cmd.Env = append(cmd.Environ(), "GOBIN="+cachedGobin) 46 out, err := cmd.CombinedOutput() 47 if err != nil { 48 panic(fmt.Errorf("%v: %s", err, out)) 49 } 50 os.Setenv("CUE_CACHED_GOBIN", cachedGobin) 51 } 52 53 os.Exit(testscript.RunMain(m, map[string]func() int{ 54 "cue": func() int { 55 // Note that we could avoid this wrapper entirely by setting PATH, 56 // since TestMain sets up a single cue binary in a GOBIN directory, 57 // but that may change at any point, or we might just switch to "go tool cue". 58 cmd := exec.Command(filepath.Join(cachedGobin, "cue"), os.Args[1:]...) 59 cmd.Stdin = os.Stdin 60 cmd.Stdout = os.Stdout 61 cmd.Stderr = os.Stderr 62 if err := cmd.Run(); err != nil { 63 if err, ok := err.(*exec.ExitError); ok { 64 return err.ExitCode() 65 } 66 fmt.Fprintln(os.Stderr, err) 67 return 1 68 } 69 return 0 70 }, 71 })) 72 } 73 74 var ( 75 // githubPublicRepo is a GitHub public repository 76 // with the "cue.works authz" GitHub App installed. 77 // The repository can be entirely empty, as it's only needed for authz. 78 githubPublicRepo = cmp.Or(os.Getenv("GITHUB_PUBLIC_REPO"), "github.com/cue-labs-modules-testing/e2e-public") 79 80 // githubPublicRepo is a GitHub private repository 81 // with the "cue.works authz" GitHub App installed. 82 // The repository can be entirely empty, as it's only needed for authz. 83 githubPrivateRepo = cmp.Or(os.Getenv("GITHUB_PRIVATE_REPO"), "github.com/cue-labs-modules-testing/e2e-private") 84 85 // gcloudRegistry is an existing Google Cloud Artifact Registry repository 86 // to publish module versions to via "cue mod publish", 87 // and authenticated via gcloud's configuration in the host environment. 88 gcloudRegistry = cmp.Or(os.Getenv("GCLOUD_REGISTRY"), "europe-west1-docker.pkg.dev/project-unity-377819/modules-e2e-registry") 89 ) 90 91 func TestScript(t *testing.T) { 92 p := testscript.Params{ 93 Dir: filepath.Join("testdata", "script"), 94 RequireExplicitExec: true, 95 RequireUniqueNames: true, 96 Setup: func(env *testscript.Env) error { 97 env.Setenv("CUE_EXPERIMENT", "modules") 98 env.Setenv("CUE_REGISTRY", "registry.cue.works") 99 env.Setenv("CUE_CACHED_GOBIN", os.Getenv("CUE_CACHED_GOBIN")) 100 env.Setenv("CUE_REGISTRY_TOKEN", os.Getenv("CUE_REGISTRY_TOKEN")) 101 102 // Just like cmd/cue/cmd.TestScript, set up separate cache and config dirs per test. 103 env.Setenv("CUE_CACHE_DIR", filepath.Join(env.WorkDir, "tmp/cachedir")) 104 configDir := filepath.Join(env.WorkDir, "tmp/configdir") 105 env.Setenv("CUE_CONFIG_DIR", configDir) 106 107 // CUE_TEST_LOGINS is a secret used by the scripts publishing to registry.cue.works. 108 // When unset, those tests would fail with an auth error. 109 if logins := os.Getenv("CUE_TEST_LOGINS"); logins != "" { 110 if err := os.MkdirAll(configDir, 0o777); err != nil { 111 return err 112 } 113 if err := os.WriteFile(filepath.Join(configDir, "logins.json"), []byte(logins), 0o666); err != nil { 114 return err 115 } 116 } 117 return nil 118 }, 119 Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ 120 // github-repo-module sets $MODULE to a unique nested module under the given repository path. 121 "github-repo-module": func(ts *testscript.TestScript, neg bool, args []string) { 122 if neg || len(args) != 1 { 123 ts.Fatalf("usage: with-github-repo <public|private>") 124 } 125 moduleName := testModuleName(ts) 126 var repo string 127 switch args[0] { 128 case "public": 129 repo = githubPublicRepo 130 case "private": 131 repo = githubPrivateRepo 132 default: 133 ts.Fatalf("usage: with-github-repo <public|private>") 134 } 135 module := path.Join(repo, moduleName) 136 ts.Setenv("MODULE", module) 137 ts.Logf("using module path %s", module) 138 }, 139 // env-fill rewrites its argument files to replace any environment variable 140 // references with their values, using the same algorithm as cmpenv. 141 "env-fill": func(ts *testscript.TestScript, neg bool, args []string) { 142 if neg || len(args) == 0 { 143 ts.Fatalf("usage: env-fill args...") 144 } 145 for _, arg := range args { 146 path := ts.MkAbs(arg) 147 data := ts.ReadFile(path) 148 data = tsExpand(ts, data) 149 ts.Check(os.WriteFile(path, []byte(data), 0o666)) 150 } 151 }, 152 // gcloud-auth-docker configures gcloud so that it uses the host's existing configuration, 153 // and sets CUE_REGISTRY and CUE_REGISTRY_HOST according to gcloudRegistry. 154 "gcloud-auth-docker": func(ts *testscript.TestScript, neg bool, args []string) { 155 if neg || len(args) > 0 { 156 ts.Fatalf("usage: gcloud-auth-docker") 157 } 158 // The test script needs to be able to run gcloud as a docker credential helper. 159 // gcloud will be accessible via $PATH without issue, but it needs to use its host config, 160 // so we pass it along as $CLOUDSDK_CONFIG to not share the host's entire $HOME. 161 // 162 // We assume that the host already has gcloud authorized to upload OCI artifacts, 163 // via either a user account (gcloud auth login) or a service account key (gcloud auth activate-service-account). 164 gcloudConfigPath, err := exec.Command("gcloud", "info", "--format=value(config.paths.global_config_dir)").Output() 165 ts.Check(err) 166 ts.Setenv("CLOUDSDK_CONFIG", string(bytes.TrimSpace(gcloudConfigPath))) 167 168 // The module path can be anything we want in this case, 169 // but we might as well make it unique and realistic. 170 ts.Setenv("MODULE", "domain.test/"+testModuleName(ts)) 171 172 ts.Setenv("CUE_REGISTRY", gcloudRegistry) 173 // TODO: reuse internal/mod/modresolve.parseRegistry, returning a Location with Host. 174 gcloudRegistryHost, _, _ := strings.Cut(gcloudRegistry, "/") 175 ts.Setenv("CUE_REGISTRY_HOST", gcloudRegistryHost) 176 }, 177 }, 178 } 179 testscript.Run(t, p) 180 } 181 182 func addr[T any](t T) *T { return &t } 183 184 func envMust(t *testing.T, name string) string { 185 if s := os.Getenv(name); s != "" { 186 return s 187 } 188 t.Fatalf("%s must be set", name) 189 return "" 190 } 191 192 func tsExpand(ts *testscript.TestScript, s string) string { 193 return os.Expand(s, func(key string) string { 194 return ts.Getenv(key) 195 }) 196 } 197 198 // testModuleName creates a unique string without any slashes 199 // which can be used as the base name for a module path to publish. 200 // 201 // It has three components: 202 // "e2e" with the test name as a prefix, to spot which test created it, 203 // a timestamp in seconds, to get an idea of when the test was run, 204 // and a short random suffix to avoid timing collisions between machines. 205 func testModuleName(ts *testscript.TestScript) string { 206 var randomTrailer [3]byte 207 if _, err := cryptorand.Read(randomTrailer[:]); err != nil { 208 panic(err) // should typically not happen 209 } 210 return fmt.Sprintf("%s-%s-%x", ts.Name(), 211 time.Now().UTC().Format("2006.01.02-15.04.05"), randomTrailer) 212 }