|
| 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 | + "context" |
| 19 | + "fmt" |
| 20 | + "net/http" |
| 21 | + "os" |
| 22 | + "os/exec" |
| 23 | + "path/filepath" |
| 24 | + "testing" |
| 25 | + "time" |
| 26 | + |
| 27 | + "github.com/google/go-github/v56/github" |
| 28 | + "github.com/rogpeppe/go-internal/testscript" |
| 29 | + "github.com/rogpeppe/retry" |
| 30 | +) |
| 31 | + |
| 32 | +func TestMain(m *testing.M) { |
| 33 | + cachedGobin := os.Getenv("CUE_CACHED_GOBIN") |
| 34 | + if cachedGobin == "" { |
| 35 | + // Install the cmd/cue version into a cached GOBIN so we can reuse it. |
| 36 | + // TODO: use "go tool cue" once we can rely on Go 1.22's tool dependency tracking in go.mod. |
| 37 | + // See: https://go.dev/issue/48429 |
| 38 | + cacheDir, err := os.UserCacheDir() |
| 39 | + if err != nil { |
| 40 | + panic(err) |
| 41 | + } |
| 42 | + cachedGobin = filepath.Join(cacheDir, "cue-e2e-gobin") |
| 43 | + cmd := exec.Command("go", "install", "cuelang.org/go/cmd/cue") |
| 44 | + cmd.Env = append(cmd.Environ(), "GOBIN="+cachedGobin) |
| 45 | + out, err := cmd.CombinedOutput() |
| 46 | + if err != nil { |
| 47 | + panic(fmt.Errorf("%v: %s", err, out)) |
| 48 | + } |
| 49 | + os.Setenv("CUE_CACHED_GOBIN", cachedGobin) |
| 50 | + } |
| 51 | + |
| 52 | + os.Exit(testscript.RunMain(m, map[string]func() int{ |
| 53 | + "cue": func() int { |
| 54 | + // Note that we could avoid this wrapper entirely by setting PATH, |
| 55 | + // since TestMain sets up a single cue binary in a GOBIN directory, |
| 56 | + // but that may change at any point, or we might just switch to "go tool cue". |
| 57 | + cmd := exec.Command(filepath.Join(cachedGobin, "cue"), os.Args[1:]...) |
| 58 | + cmd.Stdin = os.Stdin |
| 59 | + cmd.Stdout = os.Stdout |
| 60 | + cmd.Stderr = os.Stderr |
| 61 | + if err := cmd.Run(); err != nil { |
| 62 | + if err, ok := err.(*exec.ExitError); ok { |
| 63 | + return err.ExitCode() |
| 64 | + } |
| 65 | + fmt.Fprintln(os.Stderr, err) |
| 66 | + return 1 |
| 67 | + } |
| 68 | + return 0 |
| 69 | + }, |
| 70 | + })) |
| 71 | +} |
| 72 | + |
| 73 | +var ( |
| 74 | + // githubOrg is a GitHub organization where the "CUE Module Publisher" |
| 75 | + // GitHub App has been installed on all repositories. |
| 76 | + // This is necessary since we will create a new repository per test, |
| 77 | + // and there's no way to easily install the app on each repo via the API. |
| 78 | + githubOrg = envOr("GITHUB_ORG", "cue-labs-modules-testing") |
| 79 | + // githubToken should have read and write access to repository |
| 80 | + // administration and contents within githubOrg, |
| 81 | + // to be able to create repositories under the org and git push to them. |
| 82 | + githubToken = envMust("GITHUB_TOKEN") |
| 83 | + // githubKeep leaves the newly created repo around when set to true. |
| 84 | + githubKeep = envOr("GITHUB_KEEP", "false") |
| 85 | +) |
| 86 | + |
| 87 | +func TestScript(t *testing.T) { |
| 88 | + p := testscript.Params{ |
| 89 | + Dir: filepath.Join("testdata", "script"), |
| 90 | + RequireExplicitExec: true, |
| 91 | + Setup: func(env *testscript.Env) error { |
| 92 | + env.Setenv("CUE_EXPERIMENT", "modules") |
| 93 | + env.Setenv("CUE_REGISTRY", "registry.cue.works") |
| 94 | + env.Setenv("CUE_CACHED_GOBIN", os.Getenv("CUE_CACHED_GOBIN")) |
| 95 | + env.Setenv("GITHUB_TOKEN", githubToken) // needed for "git push" |
| 96 | + return nil |
| 97 | + }, |
| 98 | + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ |
| 99 | + // create-github-repo creates a unique repository under githubOrg |
| 100 | + // and sets $MODULE to its resulting module path. |
| 101 | + "create-github-repo": func(ts *testscript.TestScript, neg bool, args []string) { |
| 102 | + if neg || len(args) > 0 { |
| 103 | + ts.Fatalf("usage: create-github-repo") |
| 104 | + } |
| 105 | + // TODO: name the repo after ts.Name once the API lands |
| 106 | + // TODO: add a short random suffix to prevent time collisions |
| 107 | + repoName := time.Now().UTC().Format("2006-01-02.15-04-05") |
| 108 | + client := github.NewClient(nil).WithAuthToken(githubToken) |
| 109 | + ctx := context.TODO() |
| 110 | + |
| 111 | + repo := &github.Repository{ |
| 112 | + Name: github.String(repoName), |
| 113 | + } |
| 114 | + _, _, err := client.Repositories.Create(ctx, githubOrg, repo) |
| 115 | + ts.Check(err) |
| 116 | + |
| 117 | + // Unless GITHUB_KEEP=true is set, delete the repo when we finish. |
| 118 | + // |
| 119 | + // TODO: It might be useful to leave the repo around when the test fails. |
| 120 | + // We would need testscript.TestScript to expose T.Failed for this. |
| 121 | + ts.Defer(func() { |
| 122 | + if githubKeep == "true" { |
| 123 | + return |
| 124 | + } |
| 125 | + _, err := client.Repositories.Delete(ctx, githubOrg, repoName) |
| 126 | + ts.Check(err) |
| 127 | + }) |
| 128 | + |
| 129 | + ts.Setenv("MODULE", fmt.Sprintf("github.com/%s/%s", githubOrg, repoName)) |
| 130 | + }, |
| 131 | + // env-fill rewrites its argument files to replace any environment variable |
| 132 | + // references with their values, using the same algorithm as cmpenv. |
| 133 | + "env-fill": func(ts *testscript.TestScript, neg bool, args []string) { |
| 134 | + if neg || len(args) == 0 { |
| 135 | + ts.Fatalf("usage: env-fill args...") |
| 136 | + } |
| 137 | + for _, arg := range args { |
| 138 | + path := ts.MkAbs(arg) |
| 139 | + data := ts.ReadFile(path) |
| 140 | + data = tsExpand(ts, data) |
| 141 | + ts.Check(os.WriteFile(path, []byte(data), 0o666)) |
| 142 | + } |
| 143 | + }, |
| 144 | + // cue-mod-wait waits for a CUE module to exist in a registry for up to 20s. |
| 145 | + // Since this is easily done via an HTTP HEAD request, an OCI client isn't necessary. |
| 146 | + "cue-mod-wait": func(ts *testscript.TestScript, neg bool, args []string) { |
| 147 | + if neg || len(args) > 0 { |
| 148 | + ts.Fatalf("usage: cue-mod-wait") |
| 149 | + } |
| 150 | + manifest := tsExpand(ts, "https://${CUE_REGISTRY}/v2/${MODULE}/manifests/${VERSION}") |
| 151 | + retries := retry.Strategy{ |
| 152 | + Delay: 10 * time.Millisecond, |
| 153 | + MaxDelay: time.Second, |
| 154 | + MaxDuration: 20 * time.Second, |
| 155 | + } |
| 156 | + for it := retries.Start(); it.Next(nil); { |
| 157 | + resp, err := http.Head(manifest) |
| 158 | + ts.Check(err) |
| 159 | + if resp.StatusCode == http.StatusOK { |
| 160 | + return |
| 161 | + } |
| 162 | + } |
| 163 | + ts.Fatalf("timed out waiting for module") |
| 164 | + }, |
| 165 | + }, |
| 166 | + } |
| 167 | + testscript.Run(t, p) |
| 168 | +} |
| 169 | + |
| 170 | +func envOr(name, fallback string) string { |
| 171 | + if s := os.Getenv(name); s != "" { |
| 172 | + return s |
| 173 | + } |
| 174 | + return fallback |
| 175 | +} |
| 176 | + |
| 177 | +func envMust(name string) string { |
| 178 | + if s := os.Getenv(name); s != "" { |
| 179 | + return s |
| 180 | + } |
| 181 | + panic(fmt.Sprintf("%s must be set", name)) |
| 182 | +} |
| 183 | + |
| 184 | +func tsExpand(ts *testscript.TestScript, s string) string { |
| 185 | + return os.Expand(s, func(key string) string { |
| 186 | + return ts.Getenv(key) |
| 187 | + }) |
| 188 | +} |
0 commit comments