299 lines
8.7 KiB
Go
299 lines
8.7 KiB
Go
package validation
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
race bool
|
|
fullVet bool
|
|
}{
|
|
{name: "default"},
|
|
{name: "race", race: true},
|
|
{name: "full vet", fullVet: true},
|
|
{name: "race and full vet", race: true, fullVet: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
logPath := installGoShim(t, "", false)
|
|
err := Run(context.Background(), Request{
|
|
RepositoryDir: t.TempDir(),
|
|
Settings: Settings{Race: test.race, FullVet: test.fullVet},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
|
|
commands := readGoShimLog(t, logPath)
|
|
var tests, vets []string
|
|
var testPosition, vetPosition int
|
|
for i, command := range commands {
|
|
if strings.HasPrefix(command, "test ") {
|
|
tests = append(tests, command)
|
|
testPosition = i
|
|
}
|
|
if strings.HasPrefix(command, "vet ") {
|
|
vets = append(vets, command)
|
|
vetPosition = i
|
|
}
|
|
}
|
|
if len(tests) != 1 {
|
|
t.Fatalf("Go test invocations = %q, want exactly one", tests)
|
|
}
|
|
command := " " + tests[0] + " "
|
|
for _, argument := range []string{"test", "-mod=readonly", "-count=1", "./..."} {
|
|
if !strings.Contains(command, " "+argument+" ") {
|
|
t.Errorf("Go test command %q does not contain %q", tests[0], argument)
|
|
}
|
|
}
|
|
if got := strings.Contains(command, " -race "); got != test.race {
|
|
t.Errorf("Go test command %q has race=%t, want %t", tests[0], got, test.race)
|
|
}
|
|
if got := strings.Contains(command, " -vet=off "); got != test.fullVet {
|
|
t.Errorf("Go test command %q disables test-time vet=%t, want %t", tests[0], got, test.fullVet)
|
|
}
|
|
wantVets := 0
|
|
if test.fullVet {
|
|
wantVets = 1
|
|
}
|
|
if len(vets) != wantVets {
|
|
t.Fatalf("Go vet invocations = %q, want %d", vets, wantVets)
|
|
}
|
|
if test.fullVet {
|
|
vetCommand := " " + vets[0] + " "
|
|
for _, argument := range []string{"vet", "-mod=readonly", "./..."} {
|
|
if !strings.Contains(vetCommand, " "+argument+" ") {
|
|
t.Errorf("Go vet command %q does not contain %q", vets[0], argument)
|
|
}
|
|
}
|
|
if vetPosition <= testPosition {
|
|
t.Errorf("Go vet command %q did not follow the test suite %q", vets[0], tests[0])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) {
|
|
logPath := installGoShim(t, "race", false)
|
|
err := Run(context.Background(), Request{
|
|
RepositoryDir: t.TempDir(),
|
|
Settings: Settings{Race: true},
|
|
})
|
|
var failure *Failure
|
|
if !errors.As(err, &failure) || failure.Check != GoTest {
|
|
t.Fatalf("Run() error = %v, want a go-test validation failure", err)
|
|
}
|
|
var exitError *exec.ExitError
|
|
if !errors.As(err, &exitError) {
|
|
t.Fatalf("Run() error = %v, want the command failure preserved", err)
|
|
}
|
|
commands := readGoShimLog(t, logPath)
|
|
var tests []string
|
|
for _, command := range commands {
|
|
if strings.HasPrefix(command, "test ") {
|
|
tests = append(tests, command)
|
|
}
|
|
}
|
|
if len(tests) != 1 || !strings.Contains(" "+tests[0]+" ", " -race ") {
|
|
t.Fatalf("Go test invocations = %q, want one race-enabled invocation and no fallback", tests)
|
|
}
|
|
}
|
|
|
|
func TestRunStopsAfterGoTestFailure(t *testing.T) {
|
|
logPath := installGoShim(t, "test", false)
|
|
err := Run(context.Background(), Request{
|
|
RepositoryDir: t.TempDir(),
|
|
Settings: Settings{FullVet: true},
|
|
})
|
|
var failure *Failure
|
|
if !errors.As(err, &failure) || failure.Check != GoTest {
|
|
t.Fatalf("Run() error = %v, want a go-test validation failure", err)
|
|
}
|
|
for _, command := range readGoShimLog(t, logPath) {
|
|
if strings.HasPrefix(command, "vet ") {
|
|
t.Fatalf("Go vet ran after Go test failed: %q", command)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunAttributesFullVetFailure(t *testing.T) {
|
|
logPath := installGoShim(t, "vet", false)
|
|
err := Run(context.Background(), Request{
|
|
RepositoryDir: t.TempDir(),
|
|
Settings: Settings{FullVet: true},
|
|
})
|
|
var failure *Failure
|
|
if !errors.As(err, &failure) || failure.Check != GoVet {
|
|
t.Fatalf("Run() error = %v, want a go-vet validation failure", err)
|
|
}
|
|
var exitError *exec.ExitError
|
|
if !errors.As(err, &exitError) {
|
|
t.Fatalf("Run() error = %v, want the command failure preserved", err)
|
|
}
|
|
commands := readGoShimLog(t, logPath)
|
|
var validationCommands []string
|
|
for _, command := range commands {
|
|
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") {
|
|
validationCommands = append(validationCommands, command)
|
|
}
|
|
}
|
|
if len(validationCommands) != 2 || !strings.HasPrefix(validationCommands[0], "test ") || !strings.HasPrefix(validationCommands[1], "vet ") {
|
|
t.Fatalf("validation command sequence = %q, want test followed by vet", validationCommands)
|
|
}
|
|
}
|
|
|
|
func TestRunPreservesFullVetCancellation(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
|
}
|
|
installGoShim(t, "", true)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
err := Run(ctx, Request{
|
|
RepositoryDir: t.TempDir(),
|
|
Settings: Settings{FullVet: true},
|
|
})
|
|
var failure *Failure
|
|
if !errors.As(err, &failure) || failure.Check != GoVet {
|
|
t.Fatalf("Run() error = %v, want a go-vet validation failure", err)
|
|
}
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("Run() error = %v, want deadline cause preserved", err)
|
|
}
|
|
}
|
|
|
|
func TestRunExecutesRaceEnabledSuiteOnHost(t *testing.T) {
|
|
if !raceSupported(runtime.GOOS, runtime.GOARCH) {
|
|
t.Skipf("Go race detector is not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
|
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
|
t.Skip("Go race detector requires CGO_ENABLED=1")
|
|
}
|
|
cc, err := exec.Command("go", "env", "CC").Output()
|
|
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
|
t.Skip("Go race detector requires a C compiler")
|
|
}
|
|
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
|
t.Skip("Go race detector C compiler is unavailable")
|
|
}
|
|
module := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte("module example.com/racefixture\n\ngo 1.25.0\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(module, "race_test.go"), []byte(`package racefixture
|
|
|
|
import "testing"
|
|
|
|
func TestRaceFixture(t *testing.T) {}
|
|
`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Run(context.Background(), Request{RepositoryDir: module, Settings: Settings{Race: true}}); err != nil {
|
|
t.Fatalf("race-enabled Go suite failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func installGoShim(t *testing.T, failure string, delayVet bool) string {
|
|
t.Helper()
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
|
}
|
|
directory := t.TempDir()
|
|
logPath := filepath.Join(directory, "go-commands")
|
|
settings := map[string]string{
|
|
"GOHOSTOS": runtime.GOOS,
|
|
"GOHOSTARCH": runtime.GOARCH,
|
|
"CC": "", "CXX": "", "CGO_CFLAGS": "", "CGO_CPPFLAGS": "",
|
|
"CGO_CXXFLAGS": "", "CGO_ENABLED": "1", "CGO_LDFLAGS": "",
|
|
"GOCACHE": "", "GOCACHEPROG": "", "GOMODCACHE": "", "GOPATH": "",
|
|
"GOROOT": "", "GOTMPDIR": "", "PKG_CONFIG": "",
|
|
}
|
|
encodedSettings, err := json.Marshal(settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vetDelay := ""
|
|
if delayVet {
|
|
vetDelay = `
|
|
if [ "${1-}" = "vet" ]; then
|
|
exec sleep 5
|
|
fi
|
|
`
|
|
}
|
|
failBlock := fmt.Sprintf(`
|
|
if [ "${1-}" = "test" ]; then
|
|
for argument in "$@"; do
|
|
if [ "%s" = "race" ] && [ "$argument" = "-race" ]; then
|
|
echo "race detector prerequisite unavailable" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
if [ "%s" = "test" ]; then
|
|
echo "Go test failed" >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
if [ "${1-}" = "vet" ] && [ "%s" = "vet" ]; then
|
|
echo "Go vet failed" >&2
|
|
exit 2
|
|
fi
|
|
%s
|
|
`, failure, failure, failure, vetDelay)
|
|
script := fmt.Sprintf(`#!/bin/sh
|
|
set -eu
|
|
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
|
if [ "${1-}" = "env" ]; then
|
|
printf '%%s\n' '%s'
|
|
exit 0
|
|
fi
|
|
%s
|
|
exit 0
|
|
`, encodedSettings, failBlock)
|
|
shimPath := filepath.Join(directory, "go")
|
|
if err := os.WriteFile(shimPath, []byte(script), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := directory
|
|
if current := os.Getenv("PATH"); current != "" {
|
|
path += string(os.PathListSeparator) + current
|
|
}
|
|
t.Setenv("PATH", path)
|
|
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
|
return logPath
|
|
}
|
|
|
|
func readGoShimLog(t *testing.T, path string) []string {
|
|
t.Helper()
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
|
}
|
|
|
|
func raceSupported(goos, goarch string) bool {
|
|
switch goos {
|
|
case "darwin":
|
|
return goarch == "amd64" || goarch == "arm64"
|
|
case "freebsd", "linux":
|
|
return goarch == "amd64" || goarch == "arm64" || goarch == "ppc64le"
|
|
case "windows":
|
|
return goarch == "amd64"
|
|
default:
|
|
return false
|
|
}
|
|
}
|