mesh-build-go: run in the integration level only the tests its tags add, read from the source, so the unit tests are not run twice (issue 1921, try speed S7)

This commit is contained in:
2026-10-25 01:13:21 +02:00
parent 8536021f82
commit 391095ccb9
5 changed files with 271 additions and 6 deletions
@@ -10,6 +10,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -68,6 +69,26 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
if err != nil {
return nil, err
}
// The tests the tags add, not the unit tests again (issue 1921, try speed S7): only the packages holding one compile
// and run, with a -run pattern naming them. A try that names its own pattern runs it as asked.
var runByDir map[string]string
if pattern == "" {
tagged, err := taggedTests(tree, dirs, packages, integrationTags)
if err != nil {
return nil, err
}
if len(tagged) == 0 {
res := &TestResult{Level: LevelIntegration, Packages: []PackageResult{}}
res.judge(m.integrationBounds())
return res, nil // no test the tags add: nothing to start or run
}
byDir, runByDir, packages = map[string][]string{}, map[string]string{}, nil
for d, tr := range tagged {
byDir[d], runByDir[d] = tr.packages, tr.pattern
packages = append(packages, tr.packages...)
}
sort.Strings(packages)
}
// The store, the bus and the compile start together (issue 1921, try speed S5); the setup is the longest of them.
// The compile runs no test binary (-exec true), so it needs neither server: whether the tests compile is for the run
// to say, and its failure is not read here; the run below finds them built.
@@ -127,7 +148,7 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
setup := m.clock().Sub(began)
return m.runLevel(ctx, tree, levelOpts{
workspace: workspace, run: pattern,
workspace: workspace, run: pattern, runByDir: runByDir,
level: LevelIntegration, tags: integrationTags, packages: packages, bounds: m.integrationBounds(),
env: env,
setup: setup, wall: true,
@@ -128,9 +128,32 @@ func integModule(t *testing.T, f *integFake) (*Module, *keptStates) {
if err := os.MkdirAll(filepath.Join(m.Work, "ws1", "tree"), 0o755); err != nil {
t.Fatal(err)
}
// A test the integration tag adds, so the level has a package to run (issue 1921, try speed S7).
writeTaggedTest(t, filepath.Join(m.Work, "ws1", "tree"), "fixture/slow")
return m, states
}
// taggedTestFile is the file writeTaggedTest writes: one test the integration tag adds.
const taggedTestFile = "level_integration_test.go"
// writeTaggedTest gives the go.mod directory dir the module path mp and one test the integration tag adds, in its root
// package and in its package x.
func writeTaggedTest(t *testing.T, dir, mp string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+mp+"\n\ngo 1.25.0\n"), 0o644); err != nil {
t.Fatal(err)
}
body := "//go:build integration\n\npackage x\n\nimport \"testing\"\n\nfunc TestTakesAWhile(t *testing.T) {}\n"
for _, d := range []string{dir, filepath.Join(dir, "x")} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(d, taggedTestFile), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
}
func runLevelTool(t *testing.T, m *Module, states *keptStates, level string) (TestResult, delivery.Step) {
t.Helper()
for _, tool := range m.Tools(m.StepTools()) {
@@ -406,7 +429,7 @@ func TestTheSetupCompilesTheDirectoriesAtOnceBesideTheStoreAndTheBus(t *testing.
tree := filepath.Join(m.Work, "ws1", "tree")
for _, d := range []string{"a", "b", "c", "d", "e"} {
os.MkdirAll(filepath.Join(tree, d), 0o755)
os.WriteFile(filepath.Join(tree, d, "go.mod"), []byte("module x/"+d+"\n"), 0o644)
writeTaggedTest(t, filepath.Join(tree, d), "x/"+d)
}
inner := m.Run
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
@@ -447,3 +470,60 @@ func TestTheSetupCompilesTheDirectoriesAtOnceBesideTheStoreAndTheBus(t *testing.
t.Errorf("setup = %v", res.Setup)
}
}
// 1921/S7: the integration level runs the tests its tags add, not the unit tests again: only the packages holding a
// tagged test compile and run, with a -run pattern naming those tests; a tree with no tagged test starts no store.
func TestTheIntegrationLevelRunsOnlyTheTestsItsTagsAdd(t *testing.T) {
f := newIntegFake(t, "slow")
m, states := integModule(t, f)
tree := filepath.Join(m.Work, "ws1", "tree")
os.RemoveAll(filepath.Join(tree, "x"))
os.Remove(filepath.Join(tree, taggedTestFile))
for name, body := range map[string]string{
"go.mod": "module fixture/slow\n",
"unit/u_test.go": "package unit\n\nimport \"testing\"\n\nfunc TestUnit(t *testing.T) {}\n",
"store/s_test.go": "package store\n\nimport \"testing\"\n\nfunc TestPlain(t *testing.T) {}\n",
"store/s_integration_test.go": "//go:build integration\n\npackage store\n\nimport \"testing\"\n\nfunc TestMain(m *testing.M) {}\nfunc TestReal(t *testing.T) {}\nfunc Testable() {}\nfunc helper() {}\n",
"store/fake_test.go": "//go:build !integration\n\npackage store\n\nimport \"testing\"\n\nfunc TestFake(t *testing.T) {}\n",
"iso/iso_test.go": "//go:build integration && isolation && linux\n\npackage iso\n\nimport \"testing\"\n\nfunc TestShutOff(t *testing.T) {}\n",
"plat/p_linux_test.go": "//go:build linux\n\npackage plat\n\nimport \"testing\"\n\nfunc TestLinux(t *testing.T) {}\n",
} {
os.MkdirAll(filepath.Dir(filepath.Join(tree, name)), 0o755)
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
if _, st := runLevelTool(t, m, states, "integration"); st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
var compiled, ran []string
for _, c := range f.calls {
switch {
case strings.HasPrefix(c.line, "go ") && strings.Contains(c.line, "-exec true"):
compiled = append(compiled, c.line)
case strings.HasPrefix(c.line, "go ") && strings.Contains(c.line, " -json"):
ran = append(ran, c.line)
}
}
if len(ran) != 1 || !strings.Contains(ran[0], "-run ^(TestReal|TestShutOff)$") ||
!strings.HasSuffix(ran[0], " fixture/slow/iso fixture/slow/store") {
t.Errorf("the run = %q, want the tagged tests of iso and store alone", ran)
}
if len(compiled) != 1 || !strings.HasSuffix(compiled[0], " fixture/slow/iso fixture/slow/store") {
t.Errorf("the compile = %q, want iso and store alone", compiled)
}
g := newIntegFake(t, "slow")
m, states = integModule(t, g)
tree = filepath.Join(m.Work, "ws1", "tree")
os.RemoveAll(filepath.Join(tree, "x"))
os.Remove(filepath.Join(tree, taggedTestFile))
os.WriteFile(filepath.Join(tree, "u_test.go"), []byte("package x\n\nimport \"testing\"\n\nfunc TestUnit(t *testing.T) {}\n"), 0o644)
res, st := runLevelTool(t, m, states, "integration")
if st.State != delivery.StepPassed || !res.Passed || !res.NoTests {
t.Fatalf("a tree with no tagged test: %s %+v", st.State, res)
}
if strings.Contains(g.lines(), "pg_ctl") || strings.Contains(g.lines(), "go -C") {
t.Errorf("a tree with no tagged test started the store or ran go:\n%s", g.lines())
}
}
@@ -24,9 +24,7 @@ func threeModules(t *testing.T, f *integFake) (*Module, *keptStates) {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module fixture/"+d+"\n\ngo 1.25.0\n"), 0o644); err != nil {
t.Fatal(err)
}
writeTaggedTest(t, dir, "fixture/"+d)
}
return m, states
}
@@ -0,0 +1,161 @@
package main
// The integration level runs the tests its tags add, not the unit tests again (issue 1921, try speed S7): `go test
// -tags integration` without -run would run every untagged test too, and the unit level ran those already. Which tests a
// tag adds is read from the source: a test function in a _test.go file whose //go:build line holds with the level's
// tags and does not hold without them.
import (
"go/ast"
"go/build/constraint"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// taggedRun is what the integration level runs in one go.mod directory: the packages holding a test the tags add, by
// import path, and the -run pattern naming those tests.
type taggedRun struct {
packages []string
pattern string
}
// taggedTests reads, for each go.mod directory, the tests that tags add. selected are the packages the level was asked
// to run (import paths, as build_go_digest keys them); none, or a relative pattern, is every package. A directory with no
// test the tags add is left out of the answer.
func taggedTests(tree string, dirs, selected, tags []string) (map[string]taggedRun, error) {
want := map[string]bool{}
for _, p := range selected {
if strings.HasPrefix(p, "./") || p == "." {
want = nil
break
}
want[p] = true
}
if len(selected) == 0 {
want = nil
}
with := map[string]bool{}
for _, t := range tags {
with[t] = true
}
out := map[string]taggedRun{}
for _, d := range dirs {
raw, _ := os.ReadFile(filepath.Join(tree, d, "go.mod"))
mp := modulePath(string(raw))
base := filepath.Join(tree, d)
pkgs := map[string]bool{}
names := map[string]bool{}
err := lintable(base, func(path string) error {
if !strings.HasSuffix(path, "_test.go") {
return nil
}
src, err := os.ReadFile(path)
if err != nil {
return err
}
expr := buildLine(src)
if expr == nil || !expr.Eval(func(tag string) bool { return with[tag] || platformTag(tag) }) || expr.Eval(platformTag) {
return nil // no constraint, or one the tags do not change: the unit level's
}
rel, _ := filepath.Rel(base, filepath.Dir(path))
pkg := mp
if rel != "." {
pkg = mp + "/" + filepath.ToSlash(rel)
}
if want != nil && !want[pkg] {
return nil
}
found := testNames(path, src)
for _, n := range found {
names[n] = true
}
if len(found) > 0 {
pkgs[pkg] = true
}
return nil
})
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
continue
}
run := taggedRun{packages: sortedKeys(pkgs)}
run.pattern = "^(" + strings.Join(sortedKeys(names), "|") + ")$"
out[d] = run
}
return out, nil
}
func sortedKeys(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
// buildLine is a file's //go:build constraint, nil when it has none.
func buildLine(src []byte) constraint.Expr {
for _, line := range strings.Split(string(src), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "package ") {
break
}
if constraint.IsGoBuild(line) {
if e, err := constraint.Parse(line); err == nil {
return e
}
}
}
return nil
}
// platformTag is how a tag other than the level's evaluates, the same with the level's tags and without them: the
// machine's platform, unix, cgo and every Go version hold; any other tag does not.
func platformTag(tag string) bool {
switch tag {
case runtime.GOOS, runtime.GOARCH, "unix", "cgo", "gc":
return true
}
return strings.HasPrefix(tag, "go1.")
}
// testNames are the top-level test functions of a test file: func TestXxx, as go test runs them, TestMain left out.
func testNames(path string, src []byte) []string {
f, err := parser.ParseFile(token.NewFileSet(), path, src, parser.SkipObjectResolution)
if err != nil {
return nil // a file that does not parse fails the compile, which the run reports
}
var out []string
for _, decl := range f.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Recv != nil || fn.Name.Name == "TestMain" || !isTestName(fn.Name.Name) {
continue
}
out = append(out, fn.Name.Name)
}
return out
}
// isTestName says whether go test runs a function of this name as a test: Test, then nothing or a rune that is not
// lower case.
func isTestName(name string) bool {
rest, ok := strings.CutPrefix(name, "Test")
if !ok {
return false
}
if rest == "" {
return true
}
r, _ := utf8.DecodeRuneInString(rest)
return !unicode.IsLower(r)
}
@@ -171,6 +171,9 @@ type levelOpts struct {
// parallelism, where the unit level's is the slowest package's.
setup time.Duration
wall bool
// runByDir is the -run pattern of each go.mod directory, over run: the tests the integration level's tags add there
// (issue 1921, try speed S7).
runByDir map[string]string
// compose is a state of the level to put in the result (the store and the bus it ran against).
store, bus string
}
@@ -251,7 +254,9 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
if len(o.tags) > 0 {
args = append(args, "-tags", strings.Join(o.tags, ","))
}
if o.run != "" {
if r := o.runByDir[d]; r != "" {
args = append(args, "-run", r)
} else if o.run != "" {
args = append(args, "-run", o.run)
}
args = append(args, byDir[d]...)