Skip to content

Commit b251cf8

Browse files
committed
adding tests
1 parent d7b2190 commit b251cf8

File tree

8 files changed

+367
-64
lines changed

8 files changed

+367
-64
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ jobs:
1717
- name: Run tests
1818
run: go test $(cat tests)
1919
```
20+
21+
## Why?
22+
23+
Strategies like `go test ./... -list .` compile your code in order to discover test cases.
24+
This can be especially slow in CI environments depending on the state of your build cache.
25+
26+
We can discover test cases significantly faster by essentially `grep`-ing for patterns like `Test..`, `Fuzz...`, etc.
27+
28+
## Caveats
29+
30+
* A test can potentially be executed more than once if another package shares a test with the same name.
31+
Renaming your tests to be globally unique is currently the best workaround if you want to guarantee a single execution per test function.
32+
You can discover test with name collisions by running `shard --total 1 --index 0`.
33+
* Benchmarks aren't currently collected so running with `-bench` will not have any effect.
2034

2135

2236

internal/collect.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/doc"
7+
"go/parser"
8+
"go/token"
9+
"math/rand"
10+
"os"
11+
"path/filepath"
12+
"slices"
13+
"strings"
14+
"unicode"
15+
"unicode/utf8"
16+
)
17+
18+
type testf struct {
19+
Path string
20+
Name string
21+
}
22+
23+
func Collect(root string) ([]testf, error) {
24+
tests := []testf{}
25+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
26+
if err != nil {
27+
return err
28+
}
29+
30+
// Don't collect testdata directories.
31+
if info.IsDir() && filepath.Base(info.Name()) == "testdata" {
32+
return filepath.SkipDir
33+
}
34+
35+
if info.IsDir() || !strings.HasSuffix(path, "_test.go") {
36+
return nil
37+
}
38+
39+
// Parse the file to find test functions
40+
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
41+
if err != nil {
42+
return err
43+
}
44+
for _, decl := range file.Decls {
45+
fn, ok := decl.(*ast.FuncDecl)
46+
if !ok {
47+
continue
48+
}
49+
if !isTestFunc(fn) {
50+
continue
51+
}
52+
tests = append(tests, testf{Path: filepath.Dir(path), Name: fn.Name.Name})
53+
}
54+
55+
examples := doc.Examples(file)
56+
for _, ex := range examples {
57+
if ex.Output == "" && !ex.EmptyOutput {
58+
// Don't run tests with empty output.
59+
continue
60+
}
61+
tests = append(tests, testf{Path: filepath.Dir(path), Name: "Example" + ex.Name})
62+
63+
}
64+
65+
return nil
66+
})
67+
return tests, err
68+
}
69+
70+
func Assign(tests []testf, index int, total int, seed int64) (names, paths []string) {
71+
// Shuffle the tests.
72+
if seed != 0 {
73+
random := rand.New(rand.NewSource(seed))
74+
for i := range tests {
75+
j := random.Intn(i + 1) //nolint:gosec // Not cryptographic.
76+
tests[i], tests[j] = tests[j], tests[i]
77+
}
78+
}
79+
80+
// Assign tests to our shard.
81+
for idx, test := range tests {
82+
if idx%total != index {
83+
continue
84+
}
85+
paths = append(paths, "./"+test.Path)
86+
names = append(names, test.Name)
87+
}
88+
89+
// De-dupe.
90+
slices.Sort(names)
91+
slices.Sort(paths)
92+
93+
names = slices.CompactFunc(names, func(l, r string) bool {
94+
if l == r {
95+
fmt.Fprintln(os.Stderr, fmt.Sprintf("warning: %q exists in multiple packages, consider renaming it", l))
96+
}
97+
return l == r
98+
})
99+
paths = slices.Compact(paths)
100+
101+
return names, paths
102+
}
103+
104+
// isTestFunc tells whether fn has the type of a testing function. arg
105+
// specifies the parameter type we look for: B, F, M or T.
106+
func isTestFunc(fn *ast.FuncDecl) bool {
107+
if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
108+
fn.Type.Params.List == nil ||
109+
len(fn.Type.Params.List) != 1 ||
110+
len(fn.Type.Params.List[0].Names) > 1 {
111+
return false
112+
}
113+
ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
114+
if !ok {
115+
return false
116+
}
117+
118+
fnName := fn.Name.Name
119+
120+
if !(strings.HasPrefix(fnName, "Test") || strings.HasPrefix(fnName, "Fuzz")) {
121+
return false
122+
}
123+
124+
if len(fnName) > 4 {
125+
rune, _ := utf8.DecodeRuneInString(fnName[4:])
126+
if unicode.IsLower(rune) {
127+
return false
128+
}
129+
}
130+
131+
for _, want := range []string{"T", "F"} {
132+
// We can't easily check that the type is *testing.M
133+
// because we don't know how testing has been imported,
134+
// but at least check that it's *M or *something.M.
135+
// Same applies for B, F and T.
136+
if name, ok := ptr.X.(*ast.Ident); ok && name.Name == want {
137+
return true
138+
}
139+
if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == want {
140+
return true
141+
}
142+
}
143+
return false
144+
}

internal/collect_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
func TestCollect(t *testing.T) {
10+
11+
t.Run("ignored", func(t *testing.T) {
12+
tests, err := Collect("testdata/ignored")
13+
if err != nil {
14+
t.Fatalf("unexpected error: %s", err)
15+
}
16+
if len(tests) == 0 {
17+
return
18+
}
19+
t.Fatalf("unexepected tests collected: %+v", tests)
20+
})
21+
22+
t.Run("collected", func(t *testing.T) {
23+
want := []testf{
24+
{Path: "testdata/collected", Name: "Test"},
25+
{Path: "testdata/collected", Name: "Test_It"},
26+
{Path: "testdata/collected", Name: "TestⱯUnicodeName"},
27+
{Path: "testdata/collected", Name: "TestAliasedT"},
28+
{Path: "testdata/collected", Name: "TestMain"},
29+
{Path: "testdata/collected", Name: "FuzzIt"},
30+
{Path: "testdata/collected", Name: "TestWithSubtests"},
31+
{Path: "testdata/collected", Name: "ExampleWithOutput"},
32+
}
33+
34+
tests, err := Collect("testdata/collected")
35+
if err != nil {
36+
t.Fatalf("unexpected error: %s", err)
37+
}
38+
if !reflect.DeepEqual(want, tests) {
39+
t.Fatalf("wanted\n\t%+v\nbut got\n\t%+v", want, tests)
40+
}
41+
})
42+
43+
}
44+
45+
func TestAssign(t *testing.T) {
46+
given := []testf{
47+
{Path: "package", Name: "Test"},
48+
{Path: "package", Name: "Test0"},
49+
{Path: "package", Name: "Test1"},
50+
{Path: "package", Name: "Test2"},
51+
{Path: "other/package", Name: "Test"},
52+
{Path: "other/package", Name: "Test3"},
53+
{Path: "other/package", Name: "Test4"},
54+
{Path: "other/package", Name: "Test5"},
55+
{Path: "other/package", Name: "Test6"},
56+
}
57+
58+
tests := []struct {
59+
name string
60+
index, total int
61+
seed int64
62+
wantPaths []string
63+
wantNames []string
64+
}{
65+
{
66+
name: "1/1",
67+
index: 0,
68+
total: 1,
69+
wantPaths: []string{"./other/package", "./package"},
70+
wantNames: []string{"Test", "Test0", "Test1", "Test2", "Test3", "Test4", "Test5", "Test6"},
71+
},
72+
{
73+
index: 0,
74+
total: 100,
75+
wantPaths: []string{"./package"},
76+
wantNames: []string{"Test"},
77+
},
78+
{
79+
index: 99,
80+
total: 100,
81+
wantPaths: nil,
82+
wantNames: nil,
83+
},
84+
{
85+
index: 0,
86+
total: 4,
87+
wantPaths: []string{"./other/package", "./package"},
88+
wantNames: []string{"Test", "Test6"},
89+
},
90+
{
91+
index: 1,
92+
total: 4,
93+
wantPaths: []string{"./other/package", "./package"},
94+
wantNames: []string{"Test0", "Test3"},
95+
},
96+
{
97+
index: 2,
98+
total: 4,
99+
wantPaths: []string{"./other/package", "./package"},
100+
wantNames: []string{"Test1", "Test4"},
101+
},
102+
{
103+
index: 3,
104+
total: 4,
105+
wantPaths: []string{"./other/package", "./package"},
106+
wantNames: []string{"Test2", "Test5"},
107+
},
108+
{
109+
index: 0,
110+
total: 3,
111+
seed: 1,
112+
wantPaths: []string{"./other/package", "./package"},
113+
wantNames: []string{"Test2", "Test3", "Test4"},
114+
},
115+
{
116+
index: 1,
117+
total: 3,
118+
seed: 1,
119+
wantPaths: []string{"./other/package"},
120+
wantNames: []string{"Test", "Test5", "Test6"},
121+
},
122+
{
123+
index: 2,
124+
total: 3,
125+
seed: 1,
126+
wantPaths: []string{"./other/package", "./package"},
127+
wantNames: []string{"Test", "Test1"},
128+
},
129+
}
130+
131+
for _, tt := range tests {
132+
t.Run(fmt.Sprintf("%d/%d", tt.index, tt.total), func(t *testing.T) {
133+
names, paths := Assign(given, tt.index, tt.total, tt.seed)
134+
if !reflect.DeepEqual(tt.wantPaths, paths) {
135+
t.Errorf("wanted\n\t%+v\nbut got\n\t%+v", tt.wantPaths, paths)
136+
}
137+
if !reflect.DeepEqual(tt.wantNames, names) {
138+
t.Errorf("wanted\n\t%+v\nbut got\n\t%+v", tt.wantNames, names)
139+
}
140+
})
141+
}
142+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package collected
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
type T = testing.T
9+
10+
func Test(t *testing.T) {}
11+
12+
func Test_It(t *testing.T) {}
13+
14+
func TestⱯUnicodeName(t *testing.T) {}
15+
16+
func TestAliasedT(t *T) {}
17+
18+
func TestMain(t *testing.T) {} // Only testing.M is ignored.
19+
20+
func FuzzIt(f *testing.F) {}
21+
22+
func ExampleWithOutput() {
23+
fmt.Println("foo")
24+
// Output: foo
25+
}
26+
27+
func TestWithSubtests(t *testing.T) {
28+
t.Run("subtest", func(t *testing.T) {})
29+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package ignore
2+
3+
import "testing"
4+
5+
func TestHelper(t *testing.T) {}
6+
7+
func ExampleNotTested() {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ignore
2+
3+
import "testing"
4+
5+
func TestMain(m *testing.M) {}
6+
7+
func Testament(t *testing.T) {}
8+
9+
func TeståMent(t *testing.T) {}
10+
11+
func testPrivate(t *testing.T) {}
12+
13+
func TestWithExtraArgs(t *testing.T, foo int) {}
14+
15+
func BenchmarkIgnored(b *testing.B) {}
16+
17+
func OtherTest(t *testing.T) {}
18+
19+
func ExampleWithNoOutput() {}
20+
21+
func TestWeirdArg(t *testing.Cover) {}
22+
func TestWeirdArg2(t testing.T) {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package testdata
2+
3+
import "testing"
4+
5+
func TestInsideTestdata(t *testing.T) {}

0 commit comments

Comments
 (0)