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  }