cuelang.org/go@v0.13.0/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  	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  
    73  var (
    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  
    90  func 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  
   177  func addr[T any](t T) *T { return &t }
   178  
   179  func 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  
   187  func 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.
   200  func 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  }