github.com/metasources/buildx@v0.0.0-20230418141019-7aa1459cedea/test/cli/utils_test.go (about)

     1  package cli
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"os"
    11  	"os/exec"
    12  	"path"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"syscall"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/stretchr/testify/require"
    21  
    22  	"github.com/anchore/stereoscope/pkg/imagetest"
    23  )
    24  
    25  var showOutput = flag.Bool("show-output", false, "show stdout and stderr for failing tests")
    26  
    27  func logOutputOnFailure(t testing.TB, cmd *exec.Cmd, stdout, stderr string) {
    28  	if t.Failed() && showOutput != nil && *showOutput {
    29  		t.Log("STDOUT:\n", stdout)
    30  		t.Log("STDERR:\n", stderr)
    31  		t.Log("COMMAND:", strings.Join(cmd.Args, " "))
    32  	}
    33  }
    34  
    35  func runAndShow(t *testing.T, cmd *exec.Cmd) {
    36  	t.Helper()
    37  
    38  	stderr, err := cmd.StderrPipe()
    39  	require.NoErrorf(t, err, "could not get stderr: +v", err)
    40  
    41  	stdout, err := cmd.StdoutPipe()
    42  	require.NoErrorf(t, err, "could not get stdout: +v", err)
    43  
    44  	err = cmd.Start()
    45  	require.NoErrorf(t, err, "failed to start cmd: %+v", err)
    46  
    47  	show := func(label string, reader io.ReadCloser) {
    48  		scanner := bufio.NewScanner(reader)
    49  		scanner.Split(bufio.ScanLines)
    50  		for scanner.Scan() {
    51  			t.Logf("%s: %s", label, scanner.Text())
    52  		}
    53  	}
    54  
    55  	show("out", stdout)
    56  	show("err", stderr)
    57  }
    58  
    59  func setupPKI(t *testing.T, pw string) func() {
    60  	err := os.Setenv("COSIGN_PASSWORD", pw)
    61  	if err != nil {
    62  		t.Fatal(err)
    63  	}
    64  
    65  	cosignPath := filepath.Join(repoRoot(t), ".tmp/cosign")
    66  	cmd := exec.Command(cosignPath, "generate-key-pair")
    67  	stdout, stderr, _ := runCommand(cmd, nil)
    68  	if cmd.ProcessState.ExitCode() != 0 {
    69  		t.Log("STDOUT", stdout)
    70  		t.Log("STDERR", stderr)
    71  		t.Fatalf("could not generate keypair")
    72  	}
    73  
    74  	return func() {
    75  		err := os.Unsetenv("COSIGN_PASSWORD")
    76  		if err != nil {
    77  			t.Fatal(err)
    78  		}
    79  
    80  		err = os.Remove("cosign.key")
    81  		if err != nil {
    82  			t.Fatalf("could not cleanup cosign.key")
    83  		}
    84  
    85  		err = os.Remove("cosign.pub")
    86  		if err != nil {
    87  			t.Fatalf("could not cleanup cosign.key")
    88  		}
    89  	}
    90  }
    91  
    92  func getFixtureImage(t testing.TB, fixtureImageName string) string {
    93  	t.Logf("obtaining fixture image for %s", fixtureImageName)
    94  	imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
    95  	return imagetest.GetFixtureImageTarPath(t, fixtureImageName)
    96  }
    97  
    98  func pullDockerImage(t testing.TB, image string) {
    99  	cmd := exec.Command("docker", "pull", image)
   100  	stdout, stderr, _ := runCommand(cmd, nil)
   101  	if cmd.ProcessState.ExitCode() != 0 {
   102  		t.Log("STDOUT", stdout)
   103  		t.Log("STDERR", stderr)
   104  		t.Fatalf("could not pull docker image")
   105  	}
   106  }
   107  
   108  // docker run -v $(pwd)/sbom:/sbom cyclonedx/cyclonedx-cli:latest validate --input-format json --input-version v1_4 --input-file /sbom
   109  func runCycloneDXInDocker(t testing.TB, env map[string]string, image string, f *os.File, args ...string) (*exec.Cmd, string, string) {
   110  	allArgs := append(
   111  		[]string{
   112  			"run",
   113  			"-t",
   114  			"-v",
   115  			fmt.Sprintf("%s:/sbom", f.Name()),
   116  			image,
   117  		},
   118  		args...,
   119  	)
   120  	cmd := exec.Command("docker", allArgs...)
   121  	stdout, stderr, _ := runCommand(cmd, env)
   122  	return cmd, stdout, stderr
   123  }
   124  
   125  func runBuildxInDocker(t testing.TB, env map[string]string, image string, args ...string) (*exec.Cmd, string, string) {
   126  	allArgs := append(
   127  		[]string{
   128  			"run",
   129  			"-t",
   130  			"-e",
   131  			"BUILDX_CHECK_FOR_APP_UPDATE=false",
   132  			"-v",
   133  			fmt.Sprintf("%s:/buildx", getBuildxBinaryLocationByOS(t, "linux")),
   134  			image,
   135  			"/buildx",
   136  		},
   137  		args...,
   138  	)
   139  	cmd := exec.Command("docker", allArgs...)
   140  	stdout, stderr, _ := runCommand(cmd, env)
   141  	return cmd, stdout, stderr
   142  }
   143  
   144  func runBuildx(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
   145  	return runBuildxCommand(t, env, true, args...)
   146  }
   147  
   148  func runBuildxSafe(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
   149  	return runBuildxCommand(t, env, false, args...)
   150  }
   151  
   152  func runBuildxCommand(t testing.TB, env map[string]string, expectError bool, args ...string) (*exec.Cmd, string, string) {
   153  	cancel := make(chan bool, 1)
   154  	defer func() {
   155  		cancel <- true
   156  	}()
   157  
   158  	cmd := getBuildxCommand(t, args...)
   159  	if env == nil {
   160  		env = make(map[string]string)
   161  	}
   162  
   163  	// we should not have tests reaching out for app update checks
   164  	env["BUILDX_CHECK_FOR_APP_UPDATE"] = "false"
   165  
   166  	timeout := func() {
   167  		select {
   168  		case <-cancel:
   169  			return
   170  		case <-time.After(60 * time.Second):
   171  		}
   172  
   173  		if cmd != nil && cmd.Process != nil {
   174  			// get a stack trace printed
   175  			err := cmd.Process.Signal(syscall.SIGABRT)
   176  			if err != nil {
   177  				t.Errorf("error aborting: %+v", err)
   178  			}
   179  		}
   180  	}
   181  
   182  	go timeout()
   183  
   184  	stdout, stderr, err := runCommand(cmd, env)
   185  
   186  	if !expectError && err != nil && stdout == "" {
   187  		t.Errorf("error running buildx: %+v", err)
   188  		t.Errorf("STDOUT: %s", stdout)
   189  		t.Errorf("STDERR: %s", stderr)
   190  
   191  		// this probably indicates a timeout... lets run it again with more verbosity to help debug issues
   192  		args = append(args, "-vv")
   193  		cmd = getBuildxCommand(t, args...)
   194  
   195  		go timeout()
   196  		stdout, stderr, err = runCommand(cmd, env)
   197  
   198  		if err != nil {
   199  			t.Errorf("error rerunning buildx: %+v", err)
   200  			t.Errorf("STDOUT: %s", stdout)
   201  			t.Errorf("STDERR: %s", stderr)
   202  		}
   203  	}
   204  
   205  	return cmd, stdout, stderr
   206  }
   207  
   208  func runCommandObj(t testing.TB, cmd *exec.Cmd, env map[string]string, expectError bool) (string, string) {
   209  	cancel := make(chan bool, 1)
   210  	defer func() {
   211  		cancel <- true
   212  	}()
   213  
   214  	if env == nil {
   215  		env = make(map[string]string)
   216  	}
   217  
   218  	// we should not have tests reaching out for app update checks
   219  	env["BUILDX_CHECK_FOR_APP_UPDATE"] = "false"
   220  
   221  	timeout := func() {
   222  		select {
   223  		case <-cancel:
   224  			return
   225  		case <-time.After(60 * time.Second):
   226  		}
   227  
   228  		if cmd != nil && cmd.Process != nil {
   229  			// get a stack trace printed
   230  			err := cmd.Process.Signal(syscall.SIGABRT)
   231  			if err != nil {
   232  				t.Errorf("error aborting: %+v", err)
   233  			}
   234  		}
   235  	}
   236  
   237  	go timeout()
   238  
   239  	stdout, stderr, err := runCommand(cmd, env)
   240  
   241  	if !expectError && err != nil && stdout == "" {
   242  		t.Errorf("error running buildx: %+v", err)
   243  		t.Errorf("STDOUT: %s", stdout)
   244  		t.Errorf("STDERR: %s", stderr)
   245  	}
   246  
   247  	return stdout, stderr
   248  }
   249  
   250  func runCosign(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
   251  	cmd := getCommand(t, ".tmp/cosign", args...)
   252  	if env == nil {
   253  		env = make(map[string]string)
   254  	}
   255  
   256  	stdout, stderr, err := runCommand(cmd, env)
   257  
   258  	if err != nil {
   259  		t.Errorf("error running cosign: %+v", err)
   260  	}
   261  
   262  	return cmd, stdout, stderr
   263  }
   264  
   265  func getCommand(t testing.TB, location string, args ...string) *exec.Cmd {
   266  	return exec.Command(filepath.Join(repoRoot(t), location), args...)
   267  }
   268  
   269  func runCommand(cmd *exec.Cmd, env map[string]string) (string, string, error) {
   270  	if env != nil {
   271  		cmd.Env = append(os.Environ(), envMapToSlice(env)...)
   272  	}
   273  	var stdout, stderr bytes.Buffer
   274  	cmd.Stdout = &stdout
   275  	cmd.Stderr = &stderr
   276  
   277  	// ignore errors since this may be what the test expects
   278  	err := cmd.Run()
   279  
   280  	return stdout.String(), stderr.String(), err
   281  }
   282  
   283  func envMapToSlice(env map[string]string) (envList []string) {
   284  	for key, val := range env {
   285  		if key == "" {
   286  			continue
   287  		}
   288  		envList = append(envList, fmt.Sprintf("%s=%s", key, val))
   289  	}
   290  	return
   291  }
   292  
   293  func getBuildxCommand(t testing.TB, args ...string) *exec.Cmd {
   294  	return exec.Command(getBuildxBinaryLocation(t), args...)
   295  }
   296  
   297  func getBuildxBinaryLocation(t testing.TB) string {
   298  	if os.Getenv("BUILDX_BINARY_LOCATION") != "" {
   299  		// BUILDX_BINARY_LOCATION is the absolute path to the snapshot binary
   300  		return os.Getenv("BUILDX_BINARY_LOCATION")
   301  	}
   302  	return getBuildxBinaryLocationByOS(t, runtime.GOOS)
   303  }
   304  
   305  func getBuildxBinaryLocationByOS(t testing.TB, goOS string) string {
   306  	// note: for amd64 we need to update the snapshot location with the v1 suffix
   307  	// see : https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-builds
   308  	archPath := runtime.GOARCH
   309  	if runtime.GOARCH == "amd64" {
   310  		archPath = fmt.Sprintf("%s_v1", archPath)
   311  	}
   312  	// note: there is a subtle - vs _ difference between these versions
   313  	switch goOS {
   314  	case "darwin", "linux":
   315  		return path.Join(repoRoot(t), fmt.Sprintf("snapshot/%s-build_%s_%s/buildx", goOS, goOS, archPath))
   316  	default:
   317  		t.Fatalf("unsupported OS: %s", runtime.GOOS)
   318  	}
   319  	return ""
   320  }
   321  
   322  func repoRoot(t testing.TB) string {
   323  	t.Helper()
   324  	root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
   325  	if err != nil {
   326  		t.Fatalf("unable to find repo root dir: %+v", err)
   327  	}
   328  	absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
   329  	if err != nil {
   330  		t.Fatal("unable to get abs path to repo root:", err)
   331  	}
   332  	return absRepoRoot
   333  }
   334  
   335  func testRetryIntervals(done <-chan struct{}) <-chan time.Duration {
   336  	return exponentialBackoffDurations(250*time.Millisecond, 4*time.Second, 2, done)
   337  }
   338  
   339  func exponentialBackoffDurations(minDuration, maxDuration time.Duration, step float64, done <-chan struct{}) <-chan time.Duration {
   340  	sleepDurations := make(chan time.Duration)
   341  	go func() {
   342  		defer close(sleepDurations)
   343  	retryLoop:
   344  		for attempt := 0; ; attempt++ {
   345  			duration := exponentialBackoffDuration(minDuration, maxDuration, step, attempt)
   346  
   347  			select {
   348  			case sleepDurations <- duration:
   349  				break
   350  			case <-done:
   351  				break retryLoop
   352  			}
   353  
   354  			if duration == maxDuration {
   355  				break
   356  			}
   357  		}
   358  	}()
   359  	return sleepDurations
   360  }
   361  
   362  func exponentialBackoffDuration(minDuration, maxDuration time.Duration, step float64, attempt int) time.Duration {
   363  	duration := time.Duration(float64(minDuration) * math.Pow(step, float64(attempt)))
   364  	if duration < minDuration {
   365  		return minDuration
   366  	} else if duration > maxDuration {
   367  		return maxDuration
   368  	}
   369  	return duration
   370  }