github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/test/cli/utils_test.go (about)

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