Add the Go build module's test step with its unit tests and the guard tests (issues 1290, 1483), part 11 of 20
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// keptStates stands in for the toolchains and the steps states.
|
||||
type keptStates struct {
|
||||
mu sync.Mutex
|
||||
toolchains map[string]delivery.Toolchain
|
||||
steps map[string][]delivery.Step
|
||||
ended chan string
|
||||
}
|
||||
|
||||
func newStates() *keptStates {
|
||||
return &keptStates{toolchains: map[string]delivery.Toolchain{}, steps: map[string][]delivery.Step{}, ended: make(chan string, 16)}
|
||||
}
|
||||
|
||||
func (s *keptStates) putToolchain(key string, value any) (uint64, error) {
|
||||
tc, err := delivery.DecodeToolchain(value.(json.RawMessage))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.toolchains[key] = tc
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (s *keptStates) putStep(key string, value any) (uint64, error) {
|
||||
st, err := delivery.DecodeStep(value.(json.RawMessage))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.steps[key] = append(s.steps[key], st)
|
||||
s.mu.Unlock()
|
||||
if st.State != delivery.StepRunning {
|
||||
s.ended <- key
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (s *keptStates) toolchain(key string) (delivery.Toolchain, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
tc, ok := s.toolchains[key]
|
||||
return tc, ok
|
||||
}
|
||||
|
||||
func (s *keptStates) waitEnd(t *testing.T) delivery.Step {
|
||||
t.Helper()
|
||||
select {
|
||||
case key := <-s.ended:
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
v := s.steps[key]
|
||||
return v[len(v)-1]
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("the step never ended on its key")
|
||||
return delivery.Step{}
|
||||
}
|
||||
}
|
||||
|
||||
func moduleWith(states *keptStates, installed string) *Module {
|
||||
return &Module{Node: "ace", Recipe: Recipe{Steps: "1", Go: "go1.27.1", PostgresMajor: 18, NatsServer: "2.11.17"},
|
||||
Probe: func(context.Context) (string, error) { return installed, nil },
|
||||
Toolchains: states.putToolchain, Steps: states.putStep, Ctx: context.Background()}
|
||||
}
|
||||
|
||||
// 1483/AC2: a probe answering go1.27.1: the bundle's start writes ace.go with that version and the recipe's, and
|
||||
// the health tool answers healthy.
|
||||
func TestStartWritesTheToolchainAndHealthIsHealthy(t *testing.T) {
|
||||
states := newStates()
|
||||
m := moduleWith(states, "go1.27.1")
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
defer stop()
|
||||
go m.Start(ctx)
|
||||
var tc delivery.Toolchain
|
||||
for deadline := time.Now().Add(3 * time.Second); time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) {
|
||||
if got, ok := states.toolchain("ace.go"); ok {
|
||||
tc = got
|
||||
break
|
||||
}
|
||||
}
|
||||
if tc.Version != "go1.27.1" || tc.Recipe != m.Recipe.Version() || tc.Language != "go" {
|
||||
t.Fatalf("ace.go = %+v, want go1.27.1 and recipe %s", tc, m.Recipe.Version())
|
||||
}
|
||||
if a := m.Health(ctx); a.State != health.StateHealthy || !a.Healthy {
|
||||
t.Errorf("health = %+v, want healthy", a)
|
||||
}
|
||||
}
|
||||
|
||||
// 1483/AC3: a probe answering go1.27.2: every step tool's step ends refused naming both versions, ace.go holds
|
||||
// go1.27.2, and the health tool answers unhealthy naming both.
|
||||
func TestAnotherGoRefusesEveryStepAndHealthIsUnhealthy(t *testing.T) {
|
||||
states := newStates()
|
||||
m := moduleWith(states, "go1.27.2")
|
||||
ran := false
|
||||
run := func(context.Context, map[string]any) (any, error) { ran = true; return "done", nil }
|
||||
steps := append([]StepTool{
|
||||
{Name: "long_probe", Long: true, Run: run},
|
||||
{Name: "short_probe", Run: run},
|
||||
}, m.StepTools()...)
|
||||
for _, tool := range m.Tools(steps) {
|
||||
if tool.Name == StatusTool || tool.Name == HealthTool {
|
||||
continue
|
||||
}
|
||||
var reason string
|
||||
out, err := tool.Run(map[string]any{})
|
||||
if isLong(steps, tool.Name) {
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tool.Name, err)
|
||||
}
|
||||
if out.(map[string]any)["step"] == "" {
|
||||
t.Fatalf("%s answered no step id: %v", tool.Name, out)
|
||||
}
|
||||
st := states.waitEnd(t)
|
||||
if st.State != delivery.StepRefused {
|
||||
t.Errorf("%s: the step ended %s, want refused", tool.Name, st.State)
|
||||
}
|
||||
reason = st.Reason
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("%s: answered %v, want a refusal", tool.Name, out)
|
||||
}
|
||||
reason = err.Error()
|
||||
}
|
||||
if !strings.Contains(reason, "go1.27.2") || !strings.Contains(reason, "go1.27.1") {
|
||||
t.Errorf("%s: the refusal %q does not name both versions", tool.Name, reason)
|
||||
}
|
||||
}
|
||||
if ran {
|
||||
t.Errorf("a step ran on a node that refuses")
|
||||
}
|
||||
if tc, _ := states.toolchain("ace.go"); tc.Version != "go1.27.2" {
|
||||
t.Errorf("ace.go holds %q, want the installed go1.27.2", tc.Version)
|
||||
}
|
||||
a := m.Health(context.Background())
|
||||
if a.State != health.StateUnhealthy || !strings.Contains(a.Why, "go1.27.2") || !strings.Contains(a.Why, "go1.27.1") {
|
||||
t.Errorf("health = %+v, want unhealthy naming both versions", a)
|
||||
}
|
||||
}
|
||||
|
||||
func isLong(steps []StepTool, name string) bool {
|
||||
for _, s := range steps {
|
||||
if s.Name == name {
|
||||
return s.Long
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 1483/AC4: recipes differing only in the postgres major or the nats-server version have different recipe versions;
|
||||
// equal recipes have the same one.
|
||||
func TestRecipeVersionMovesWithEachOfItsValues(t *testing.T) {
|
||||
base := Recipe{Steps: "1", Go: "go1.27.1", PostgresMajor: 18, NatsServer: "2.11.17"}
|
||||
if base.Version() != (Recipe{Steps: "1", Go: "go1.27.1", PostgresMajor: 18, NatsServer: "2.11.17"}).Version() {
|
||||
t.Errorf("one recipe has two versions")
|
||||
}
|
||||
seen := map[string]string{base.Version(): "base"}
|
||||
for name, r := range map[string]Recipe{
|
||||
"postgres major": {Steps: "1", Go: "go1.27.1", PostgresMajor: 19, NatsServer: "2.11.17"},
|
||||
"nats-server": {Steps: "1", Go: "go1.27.1", PostgresMajor: 18, NatsServer: "2.11.18"},
|
||||
"go": {Steps: "1", Go: "go1.27.2", PostgresMajor: 18, NatsServer: "2.11.17"},
|
||||
"steps": {Steps: "2", Go: "go1.27.1", PostgresMajor: 18, NatsServer: "2.11.17"},
|
||||
} {
|
||||
v := r.Version()
|
||||
if was, dup := seen[v]; dup {
|
||||
t.Errorf("the recipe differing in %s has the version of the %s recipe: %s", name, was, v)
|
||||
}
|
||||
seen[v] = name
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRecipeNamesGo1271AndPostgres18AndTheLibrarysNatsVersion(t *testing.T) {
|
||||
r := Current()
|
||||
if r.Go != "go1.27.1" || r.PostgresMajor != 18 {
|
||||
t.Errorf("recipe = %+v", r)
|
||||
}
|
||||
if r.NatsServer == "" || strings.HasPrefix(r.NatsServer, "v") {
|
||||
t.Errorf("nats-server version = %q", r.NatsServer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGoVersion(t *testing.T) {
|
||||
if v, err := parseGoVersion("go version go1.27.1 linux/amd64\n"); err != nil || v != "go1.27.1" {
|
||||
t.Errorf("= %q, %v", v, err)
|
||||
}
|
||||
if _, err := parseGoVersion("not go"); err == nil {
|
||||
t.Errorf("accepted a text that is not `go version`'s")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
// The test step (novox/hq ADR 0445; issues 1290 and 1486): a fetched workspace's tests by Go's convention, one level
|
||||
// at a time, each judged by its own bounds. The unit level is `go test` over every package without the `integration`
|
||||
// build tag.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// testTimeout is go test's own timeout for a level: a bound on a test that never ends, far above any level's bound.
|
||||
const testTimeout = "10m"
|
||||
|
||||
var packageOK = regexp.MustCompile(`^(\./|[A-Za-z0-9])[A-Za-z0-9._/@~-]*(/\.\.\.)?$`)
|
||||
|
||||
func (m *Module) testTool() StepTool {
|
||||
return StepTool{
|
||||
Name: "build_go_test",
|
||||
Description: "Run the tests of a fetched workspace by Go's convention, one level: the unit level is go test over every " +
|
||||
"package without the integration build tag, within five seconds. Answers a step id at once; the step ends on " +
|
||||
"its key with the level's result: its packages, its times, the tests that failed and the slowest tests. A " +
|
||||
"level that fails is a finished step whose result says passed false.",
|
||||
Input: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"workspace": map[string]any{"type": "string", "description": "the workspace id build_go_fetch answered"},
|
||||
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
||||
"packages": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||
"description": "package patterns of each module, ./... when none"},
|
||||
},
|
||||
"required": []string{"workspace", "level"},
|
||||
},
|
||||
Long: true,
|
||||
Run: m.test,
|
||||
}
|
||||
}
|
||||
|
||||
// unitBound is the unit level's bound: UnitBound unless a test lowered it.
|
||||
func (m *Module) unitBound() time.Duration {
|
||||
if m.UnitBound > 0 {
|
||||
return m.UnitBound
|
||||
}
|
||||
return UnitBound
|
||||
}
|
||||
|
||||
func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
||||
workspace, _ := args["workspace"].(string)
|
||||
level, _ := args["level"].(string)
|
||||
packages, err := packagesArg(args["packages"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !workspaceOK.MatchString(workspace) {
|
||||
return nil, fmt.Errorf("%q is not a workspace id", workspace)
|
||||
}
|
||||
tree := filepath.Join(m.Work, workspace, "tree")
|
||||
if _, err := os.Stat(tree); err != nil {
|
||||
return nil, fmt.Errorf("no workspace %s: it was never fetched here, or it is older than a day and was removed", workspace)
|
||||
}
|
||||
switch level {
|
||||
case LevelUnit:
|
||||
return m.runLevel(ctx, tree, levelOpts{level: LevelUnit, packages: packages, bounds: Bounds{Run: m.unitBound()}})
|
||||
case LevelIntegration:
|
||||
return m.integration(ctx, workspace, tree, packages)
|
||||
}
|
||||
return nil, fmt.Errorf("the level %q is neither unit nor integration", level)
|
||||
}
|
||||
|
||||
func packagesArg(v any) ([]string, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
list, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("packages is a list of package patterns")
|
||||
}
|
||||
out := []string{}
|
||||
for _, e := range list {
|
||||
s, _ := e.(string)
|
||||
if !packageOK.MatchString(s) || strings.Contains(strings.TrimSuffix(s, "/..."), "..") {
|
||||
return nil, fmt.Errorf("%q is not a package pattern", s)
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// levelOpts are what a level asks of runLevel.
|
||||
type levelOpts struct {
|
||||
level string
|
||||
tags []string
|
||||
packages []string
|
||||
bounds Bounds
|
||||
// env is what the level adds to a go command's environment (MESH_TEST_POSTGRES, MESH_TEST_NATS).
|
||||
env []string
|
||||
// setup is the time the level's setup took, which the caller measured; wall says the level's running time is the
|
||||
// wall time of its go test commands, as the integration level's packages run side by side by go test's own
|
||||
// parallelism, where the unit level's is the slowest package's.
|
||||
setup time.Duration
|
||||
wall bool
|
||||
// compose is a state of the level to put in the result (the store and the bus it ran against).
|
||||
store, bus string
|
||||
}
|
||||
|
||||
// runLevel runs `go test -json` in each go.mod directory of the tree and judges the whole against the bounds.
|
||||
func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestResult, error) {
|
||||
dirs, err := m.goModDirs(ctx, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &TestResult{Level: o.level, Packages: []PackageResult{}, Setup: o.setup.Seconds(), Store: o.store, Bus: o.bus}
|
||||
packages := o.packages
|
||||
if len(packages) == 0 {
|
||||
packages = []string{"./..."}
|
||||
}
|
||||
began := m.clock()
|
||||
for _, d := range dirs {
|
||||
args := []string{"-C", filepath.Join(tree, d), "test", "-json", "-count=1", "-timeout", testTimeout}
|
||||
if len(o.tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(o.tags, ","))
|
||||
}
|
||||
args = append(args, packages...)
|
||||
r := m.Run(ctx, append(m.goEnvFor(filepath.Join(tree, d)), o.env...), "go", args...)
|
||||
rd := newReader(res, d)
|
||||
noise := rd.read(r.Stdout)
|
||||
rd.finish()
|
||||
if (r.Status != 0 || r.Err != "") && len(rd.pkgs) == 0 {
|
||||
// go test said nothing a test said: it could not run (a go.mod it cannot read, a toolchain it cannot find).
|
||||
return nil, fmt.Errorf("go test in %s: %s", d, firstLine(r.Err+"\n"+strings.Join(noise, "\n")+"\n"+r.Stderr))
|
||||
}
|
||||
}
|
||||
if o.wall {
|
||||
res.Elapsed = m.clock().Sub(began).Seconds()
|
||||
}
|
||||
res.judge(o.bounds)
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// capturedRunner answers the commands of a test step from a fixture repository: ls-files with the go.mod, and every
|
||||
// `go test` with the output captured from the real go (testdata/captured, see its README).
|
||||
func capturedRunner(t *testing.T, captured string, calls *[]cmdRecord) forgekey.Runner {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", "captured", captured+".json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status := 0
|
||||
if strings.Contains(string(raw), `"Action":"fail"`) {
|
||||
status = 1 // go test exits 1 when a test or a build failed
|
||||
}
|
||||
return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
*calls = append(*calls, cmdRecord{env: env, line: name + " " + strings.Join(args, " ")})
|
||||
switch {
|
||||
case name == "git" && args[2] == "ls-files":
|
||||
return forgekey.Ran{Stdout: "go.mod\n"}
|
||||
case name == "go":
|
||||
return forgekey.Ran{Stdout: string(raw), Status: status}
|
||||
}
|
||||
return forgekey.Ran{}
|
||||
}
|
||||
}
|
||||
|
||||
// workspaceFor is a module whose work directory holds a fetched workspace, and the id of it.
|
||||
func workspaceFor(t *testing.T, captured string) (*Module, *keptStates, string, *[]cmdRecord) {
|
||||
t.Helper()
|
||||
calls := &[]cmdRecord{}
|
||||
states := newStates()
|
||||
m := moduleWith(states, "go1.27.1")
|
||||
m.Home, m.Work, m.Cache = t.TempDir(), t.TempDir(), t.TempDir()
|
||||
m.Run = capturedRunner(t, captured, calls)
|
||||
if err := os.MkdirAll(filepath.Join(m.Work, "ws1", "tree"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m, states, "ws1", calls
|
||||
}
|
||||
|
||||
func runUnit(t *testing.T, m *Module, states *keptStates, ws string) (TestResult, delivery.Step) {
|
||||
t.Helper()
|
||||
var run func(map[string]any) (any, error)
|
||||
for _, tool := range m.Tools(m.StepTools()) {
|
||||
if tool.Name == "build_go_test" {
|
||||
run = tool.Run
|
||||
}
|
||||
}
|
||||
if _, err := run(map[string]any{"workspace": ws, "level": "unit"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := states.waitEnd(t)
|
||||
var res TestResult
|
||||
if st.State == delivery.StepPassed {
|
||||
if err := json.Unmarshal(st.Result, &res); err != nil {
|
||||
t.Fatalf("result %s: %v", st.Result, err)
|
||||
}
|
||||
}
|
||||
return res, st
|
||||
}
|
||||
|
||||
// 1290/AC1: a fixture with one passing package and one failing test: the result names the failing test and its
|
||||
// package, and the level fails. The step itself finished, so it ends passed with the verdict in its result.
|
||||
func TestUnitLevelNamesTheFailingTestAndFailsTheLevel(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "failing")
|
||||
res, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
if res.Passed || res.Level != "unit" {
|
||||
t.Errorf("result = %+v, want the unit level failed", res)
|
||||
}
|
||||
if len(res.Failed) != 1 || res.Failed[0].Package != "fixture/failing/bad" || res.Failed[0].Test != "TestThreeIsFour" ||
|
||||
!strings.Contains(res.Failed[0].Output, "Three() = 3, want 4") {
|
||||
t.Errorf("failed = %+v", res.Failed)
|
||||
}
|
||||
if !strings.Contains(res.Why, "fixture/failing/bad TestThreeIsFour") {
|
||||
t.Errorf("why = %q", res.Why)
|
||||
}
|
||||
good := false
|
||||
for _, p := range res.Packages {
|
||||
if p.Package == "fixture/failing/good" && p.Passed && p.Tests == 1 {
|
||||
good = true
|
||||
}
|
||||
}
|
||||
if !good || res.Tests != 2 {
|
||||
t.Errorf("packages = %+v, tests = %d; want the good package passed with its test, 2 tests", res.Packages, res.Tests)
|
||||
}
|
||||
}
|
||||
|
||||
// 1290/AC2: a fixture whose tests take more than the bound (lowered by the module's option, not by waiting five
|
||||
// seconds): the level fails naming the bound and the slowest tests; under the real bound the same run passes.
|
||||
func TestUnitLevelOverItsBoundFailsNamingTheBoundAndTheSlowestTests(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "slow")
|
||||
m.UnitBound = 10 * time.Millisecond
|
||||
res, _ := runUnit(t, m, states, ws)
|
||||
if res.Passed {
|
||||
t.Fatalf("a level of %.3f s passed under a bound of %v", res.Elapsed, m.UnitBound)
|
||||
}
|
||||
for _, want := range []string{"over its bound of 0.01 s", "fixture/slow TestTakesAWhile"} {
|
||||
if !strings.Contains(res.Why, want) {
|
||||
t.Errorf("why = %q lacks %q", res.Why, want)
|
||||
}
|
||||
}
|
||||
if len(res.Slowest) == 0 || res.Slowest[0].Test != "TestTakesAWhile" || res.Bound != 0.01 {
|
||||
t.Errorf("slowest = %+v, bound = %v", res.Slowest, res.Bound)
|
||||
}
|
||||
|
||||
m2, states2, ws2, _ := workspaceFor(t, "slow")
|
||||
if res, _ := runUnit(t, m2, states2, ws2); !res.Passed || res.Bound != 5 {
|
||||
t.Errorf("under the bound of five seconds: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
// 1290/AC3: a fixture whose only test carries //go:build integration: go test says [no test files], and the unit
|
||||
// level reports no tests and passes. No command of the unit level asks for the tag.
|
||||
func TestUnitLevelOfAFixtureWithOnlyAnIntegrationTestHasNoTests(t *testing.T) {
|
||||
m, states, ws, calls := workspaceFor(t, "tagged")
|
||||
res, _ := runUnit(t, m, states, ws)
|
||||
if !res.Passed || !res.NoTests || res.Tests != 0 || !strings.Contains(res.Why, "no tests") {
|
||||
t.Errorf("result = %+v, want no tests, passed", res)
|
||||
}
|
||||
for _, c := range *calls {
|
||||
if strings.Contains(c.line, "-tags") {
|
||||
t.Errorf("the unit level asked for a tag: %s", c.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1290/AC4: a fixture with no tests at all: the result says the repository has no tests, neither a failure nor a warning.
|
||||
func TestUnitLevelOfAFixtureWithNoTestsSaysSo(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "empty")
|
||||
res, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepPassed || !res.Passed || !res.NoTests || res.Why != "the repository has no tests at the unit level" || len(res.Failed) != 0 {
|
||||
t.Errorf("result = %+v (%s)", res, st.State)
|
||||
}
|
||||
}
|
||||
|
||||
// A package that does not build fails the level with the compiler's words.
|
||||
func TestUnitLevelOfAPackageThatDoesNotBuildFailsWithTheCompilersLine(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "broken")
|
||||
res, _ := runUnit(t, m, states, ws)
|
||||
if res.Passed || len(res.Failed) != 1 || res.Failed[0].Package != "fixture/broken" ||
|
||||
!strings.Contains(res.Failed[0].Output, `cannot use "one" (untyped string constant) as int value`) {
|
||||
t.Errorf("result = %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
// The command is go test -json in the module's directory of the workspace, with the toolchain pinned to the installed
|
||||
// one and the module's caches.
|
||||
func TestUnitLevelRunsGoTestJsonInTheModulesDirectoryWithTheStepsEnvironment(t *testing.T) {
|
||||
m, states, ws, calls := workspaceFor(t, "empty")
|
||||
runUnit(t, m, states, ws)
|
||||
var line string
|
||||
var env string
|
||||
for _, c := range *calls {
|
||||
if strings.HasPrefix(c.line, "go ") {
|
||||
line, env = c.line, strings.Join(c.env, " ")
|
||||
}
|
||||
}
|
||||
want := "go -C " + filepath.Join(m.Work, ws, "tree", ".") + " test -json -count=1 -timeout 10m ./..."
|
||||
if line != want {
|
||||
t.Errorf("command = %q, want %q", line, want)
|
||||
}
|
||||
for _, w := range []string{"GOTOOLCHAIN=local", "GOFLAGS=-mod=mod", "GOMODCACHE=" + filepath.Join(m.Cache, "gomod")} {
|
||||
if !strings.Contains(env, w) {
|
||||
t.Errorf("environment %s lacks %s", env, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A go test that said nothing a test says (it could not run) ends the step failed, not the level.
|
||||
func TestAGoTestThatCouldNotRunEndsTheStepFailed(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "empty")
|
||||
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
if name == "go" {
|
||||
return forgekey.Ran{Status: 1, Stderr: "go: cannot find main module\n"}
|
||||
}
|
||||
return forgekey.Ran{Stdout: "go.mod\n"}
|
||||
}
|
||||
_, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "go: cannot find main module") {
|
||||
t.Errorf("the step ended %s: %q", st.State, st.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestATestOfAnUnknownWorkspaceOrAnotherLevelIsRefused(t *testing.T) {
|
||||
m, states, _, _ := workspaceFor(t, "empty")
|
||||
var run func(map[string]any) (any, error)
|
||||
for _, tool := range m.Tools(m.StepTools()) {
|
||||
if tool.Name == "build_go_test" {
|
||||
run = tool.Run
|
||||
}
|
||||
}
|
||||
for _, args := range []map[string]any{
|
||||
{"workspace": "nope", "level": "unit"},
|
||||
{"workspace": "../ws1", "level": "unit"},
|
||||
{"workspace": "ws1", "level": "stress"},
|
||||
{"workspace": "ws1", "level": "unit", "packages": []any{"-run=x"}},
|
||||
{"workspace": "ws1", "level": "unit", "packages": []any{"../x"}},
|
||||
} {
|
||||
run(args)
|
||||
if st := states.waitEnd(t); st.State != delivery.StepFailed {
|
||||
t.Errorf("%v: the step ended %s", args, st.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A directory that vendors is tested, and listed, from its vendor directory: fetch downloaded nothing for it, and the
|
||||
// last value of GOFLAGS is the one go uses.
|
||||
func TestADirectoryThatVendorsIsReadFromItsVendorDirectory(t *testing.T) {
|
||||
m, states, ws, calls := workspaceFor(t, "empty")
|
||||
vendor := filepath.Join(m.Work, ws, "tree", "vendor")
|
||||
os.MkdirAll(vendor, 0o755)
|
||||
os.WriteFile(filepath.Join(vendor, "modules.txt"), nil, 0o644)
|
||||
runUnit(t, m, states, ws)
|
||||
flags := ""
|
||||
for _, c := range *calls {
|
||||
if strings.HasPrefix(c.line, "go ") {
|
||||
for _, w := range c.env {
|
||||
if strings.HasPrefix(w, "GOFLAGS=") {
|
||||
flags = w
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if flags != "GOFLAGS=-mod=vendor" {
|
||||
t.Errorf("the last GOFLAGS of go test in a vendoring directory is %q", flags)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user