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
15package e2e_test
16
17import (
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
33func 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 testscript.Main(m, map[string]func(){
54 "cue": func() {
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 os.Exit(err.ExitCode())
65 }
66 fmt.Fprintln(os.Stderr, err)
67 os.Exit(1)
68 }
69 },
70 })
71}
72
73var (
74 // githubPublicRepo is a GitHub public repository
75 // with the "cue.works authz" GitHub App installed.
76 // The repository can be entirely empty, as it's only needed for authz.
77 githubPublicRepo = cmp.Or(os.Getenv("GITHUB_PUBLIC_REPO"), "github.com/cue-labs-modules-testing/e2e-public")
78
79 // githubPublicRepo is a GitHub private repository
80 // with the "cue.works authz" GitHub App installed.
81 // The repository can be entirely empty, as it's only needed for authz.
82 githubPrivateRepo = cmp.Or(os.Getenv("GITHUB_PRIVATE_REPO"), "github.com/cue-labs-modules-testing/e2e-private")
83
84 // gcloudRegistry is an existing Google Cloud Artifact Registry repository
85 // to publish module versions to via "cue mod publish",
86 // and authenticated via gcloud's configuration in the host environment.
87 gcloudRegistry = cmp.Or(os.Getenv("GCLOUD_REGISTRY"), "europe-west1-docker.pkg.dev/project-unity-377819/modules-e2e-registry")
88)
89
90func TestScript(t *testing.T) {
91 p := testscript.Params{
92 Dir: filepath.Join("testdata", "script"),
93 RequireExplicitExec: true,
94 RequireUniqueNames: true,
95 Setup: func(env *testscript.Env) error {
96 env.Setenv("CUE_CACHED_GOBIN", os.Getenv("CUE_CACHED_GOBIN"))
97
98 // Just like cmd/cue/cmd.TestScript, set up separate cache and config dirs per test.
99 env.Setenv("CUE_CACHE_DIR", filepath.Join(env.WorkDir, "tmp/cachedir"))
100 configDir := filepath.Join(env.WorkDir, "tmp/configdir")
101 env.Setenv("CUE_CONFIG_DIR", configDir)
102
103 // CUE_TEST_TOKEN is a secret used by the scripts publishing to registry.cue.works.
104 // When unset, those tests would fail with an auth error.
105 if token := os.Getenv("CUE_TEST_TOKEN"); token != "" {
106 cmd := exec.Command("cue", "login", "--token", token)
107 cmd.Env = env.Vars // store the token in the CUE_CONFIG_DIR we just set
108 if out, err := cmd.CombinedOutput(); err != nil {
109 return fmt.Errorf("%v: %s", err, out)
110 }
111 }
112 return nil
113 },
114 Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
115 // github-repo-module sets $MODULE to a unique nested module under the given repository path.
116 "github-repo-module": func(ts *testscript.TestScript, neg bool, args []string) {
117 if neg || len(args) != 1 {
118 ts.Fatalf("usage: with-github-repo <public|private>")
119 }
120 moduleName := testModuleName(ts)
121 var repo string
122 switch args[0] {
123 case "public":
124 repo = githubPublicRepo
125 case "private":
126 repo = githubPrivateRepo
127 default:
128 ts.Fatalf("usage: with-github-repo <public|private>")
129 }
130 module := path.Join(repo, moduleName)
131 ts.Setenv("MODULE", module)
132 ts.Logf("using module path %s", module)
133 },
134 // env-fill rewrites its argument files to replace any environment variable
135 // references with their values, using the same algorithm as cmpenv.
136 "env-fill": func(ts *testscript.TestScript, neg bool, args []string) {
137 if neg || len(args) == 0 {
138 ts.Fatalf("usage: env-fill args...")
139 }
140 for _, arg := range args {
141 path := ts.MkAbs(arg)
142 data := ts.ReadFile(path)
143 data = tsExpand(ts, data)
144 ts.Check(os.WriteFile(path, []byte(data), 0o666))
145 }
146 },
147 // gcloud-auth-docker configures gcloud so that it uses the host's existing configuration,
148 // and sets CUE_REGISTRY and CUE_REGISTRY_HOST according to gcloudRegistry.
149 "gcloud-auth-docker": func(ts *testscript.TestScript, neg bool, args []string) {
150 if neg || len(args) > 0 {
151 ts.Fatalf("usage: gcloud-auth-docker")
152 }
153 // The test script needs to be able to run gcloud as a docker credential helper.
154 // gcloud will be accessible via $PATH without issue, but it needs to use its host config,
155 // so we pass it along as $CLOUDSDK_CONFIG to not share the host's entire $HOME.
156 //
157 // We assume that the host already has gcloud authorized to upload OCI artifacts,
158 // via either a user account (gcloud auth login) or a service account key (gcloud auth activate-service-account).
159 gcloudConfigPath, err := exec.Command("gcloud", "info", "--format=value(config.paths.global_config_dir)").Output()
160 ts.Check(err)
161 ts.Setenv("CLOUDSDK_CONFIG", string(bytes.TrimSpace(gcloudConfigPath)))
162
163 // The module path can be anything we want in this case,
164 // but we might as well make it unique and realistic.
165 ts.Setenv("MODULE", "domain.test/"+testModuleName(ts))
166
167 ts.Setenv("CUE_REGISTRY", gcloudRegistry)
168 // TODO: reuse internal/mod/modresolve.parseRegistry, returning a Location with Host.
169 gcloudRegistryHost, _, _ := strings.Cut(gcloudRegistry, "/")
170 ts.Setenv("CUE_REGISTRY_HOST", gcloudRegistryHost)
171 },
172 },
173 }
174 testscript.Run(t, p)
175}
176
177func addr[T any](t T) *T { return &t }
178
179func envMust(t *testing.T, name string) string {
180 if s := os.Getenv(name); s != "" {
181 return s
182 }
183 t.Fatalf("%s must be set", name)
184 return ""
185}
186
187func tsExpand(ts *testscript.TestScript, s string) string {
188 return os.Expand(s, func(key string) string {
189 return ts.Getenv(key)
190 })
191}
192
193// testModuleName creates a unique string without any slashes
194// which can be used as the base name for a module path to publish.
195//
196// It has three components:
197// "e2e" with the test name as a prefix, to spot which test created it,
198// a timestamp in seconds, to get an idea of when the test was run,
199// and a short random suffix to avoid timing collisions between machines.
200func testModuleName(ts *testscript.TestScript) string {
201 var randomTrailer [3]byte
202 if _, err := cryptorand.Read(randomTrailer[:]); err != nil {
203 panic(err) // should typically not happen
204 }
205 return fmt.Sprintf("%s-%s-%x", ts.Name(),
206 time.Now().UTC().Format("2006.01.02-15.04.05"), randomTrailer)
207}