Skip to content

Commit 6960c38

Browse files
cmd/gosim: add -seeds flag
- Add a -seeds flag to the metatest binary. - Pass -seeds from cmd/gosim for both gosim test and gosim debug. - Modify testing output to print the test's seed. - Add a script test verifying this works. - Update the README to showcase determinism and the seeds flag.
1 parent 1fa7745 commit 6960c38

File tree

8 files changed

+141
-34
lines changed

8 files changed

+141
-34
lines changed

README.md

+36-10
Original file line numberDiff line numberDiff line change
@@ -36,41 +36,67 @@ started with `gosim`, you need to import it in your module:
3636
To test code with Gosim, write a small a test in a file `simple_test.go` and then run
3737
it using `go test`:
3838
```go
39-
package example_test
39+
package examples_test
4040

4141
import (
42+
"math/rand"
4243
"testing"
4344

4445
"github.com/jellevandenhooff/gosim"
4546
)
4647

4748
func TestGosim(t *testing.T) {
4849
t.Logf("Are we in the Matrix? %v", gosim.IsSim())
50+
t.Logf("Random: %d", rand.Int())
4951
}
5052
```
5153
```
52-
> go test -v -run TestGosim
54+
> go test -v -count=1 -run TestGosim
5355
=== RUN TestGosim
54-
simple_test.go:10: Are we in the Matrix? false
55-
--- PASS: TestGosim (0.00s simulated)
56+
simple_test.go:11: Are we in the Matrix? false
57+
simple_test.go:12: Random: 1225418128470362734
58+
--- PASS: TestGosim (0.00s)
5659
PASS
5760
ok example 0.216s
5861
```
62+
The test prints that Gosim is not enabled, and each time the test runs the random number is different.
63+
5964
To run this test with `gosim` instead, replace `go test` with
6065
`go run github.com/jellevandenhooff/gosim/cmd/gosim test`:
6166
```
6267
> go run github.com/jellevandenhooff/gosim/cmd/gosim test -v -run TestGosim
63-
=== RUN TestGosim
64-
1 main/4 14:10:03.000 INF example/simple_test.go:11 > Are we in the Matrix? true method=t.Logf
65-
--- PASS: TestGosim (0.00s)
68+
=== RUN TestGosim (seed 1)
69+
1 main/4 14:10:03.000 INF examples/simple_test.go:12 > Are we in the Matrix? true method=t.Logf
70+
2 main/4 14:10:03.000 INF examples/simple_test.go:13 > Random: 811966193383742320 method=t.Logf
71+
--- PASS: TestGosim (0.00s simulated)
6672
ok translated/example 0.204s
6773
```
6874
The `gosim test` command has flags similar to `go test`. The test output is
6975
more involved than a normal `go test`. Every log line includes the simulated
7076
machine and the goroutine that invoked the log to help debug tests running on
7177
multiple machines.
7278

73-
If running gosim fails with errors about missing `go.sum` entries, run
79+
The `=== RUN` line includes the test's seed. Each time this test runs with the
80+
same seed it will output the same random number. To test with different seeds,
81+
you can pass ranges of seeds to `gosim test`:
82+
```
83+
go run github.com/jellevandenhooff/gosim/cmd/gosim test -v -seeds=1-3 -run=TestGosim .
84+
=== RUN TestGosim (seed 1)
85+
1 main/4 14:10:03.000 INF examples/simple_test.go:12 > Are we in the Matrix? true method=t.Logf
86+
2 main/4 14:10:03.000 INF examples/simple_test.go:13 > Random: 811966193383742320 method=t.Logf
87+
--- PASS: TestGosim (0.00s simulated)
88+
=== RUN TestGosim (seed 2)
89+
1 main/4 14:10:03.000 INF examples/simple_test.go:12 > Are we in the Matrix? true method=t.Logf
90+
2 main/4 14:10:03.000 INF examples/simple_test.go:13 > Random: 5374891573232646577 method=t.Logf
91+
--- PASS: TestGosim (0.00s simulated)
92+
=== RUN TestGosim (seed 3)
93+
1 main/4 14:10:03.000 INF examples/simple_test.go:12 > Are we in the Matrix? true method=t.Logf
94+
2 main/4 14:10:03.000 INF examples/simple_test.go:13 > Random: 3226404213937589817 method=t.Logf
95+
--- PASS: TestGosim (0.00s simulated)
96+
ok translated/github.com/jellevandenhooff/gosim/examples 0.254s
97+
```
98+
99+
If running `gosim` fails with errors about missing `go.sum` entries, run
74100
`go mod tidy` to update your `go.sum` file.
75101

76102
## Simulation
@@ -162,7 +188,7 @@ what happens this test is run:
162188
<!-- TODO: two machines and a bug? -->
163189
```
164190
> go run github.com/jellevandenhooff/gosim/cmd/gosim test -v -run TestMachines
165-
=== RUN TestMachines
191+
=== RUN TestMachines (seed 1)
166192
1 server/5 14:10:03.000 INF example/machines_test.go:17 > starting server
167193
2 main/4 14:10:04.000 INF example/machines_test.go:27 > making a request
168194
3 server/8 14:10:04.000 INF example/machines_test.go:20 > got a request from 11.0.0.1:10000
@@ -217,7 +243,7 @@ realistic latency and timeouts without development time becoming slow.
217243
## Debugging
218244
Gosim's simulation is deterministic, which means that running a test twice
219245
will result in the exact same behavior, from randomly generated numbers
220-
to goroutine concurrency interleavings. That is cool because it means that
246+
to goroutine concurrency interleavings. That is useful because it means that
221247
if we see an interesting log after running the program but do not fully
222248
understand why that log printed a certain value, we can re-run the program
223249
and see exactly what happened at the moment of printing.

cmd/gosim/doc.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,25 @@ The -race flag translates all code with the race build tag set.
3232
3333
The 'test' command:
3434
35-
Usage: gosim test [-race] [-run=...] [-v] [packages]
35+
Usage: gosim test [-race] [-run=...] [-seeds=...] [-v] [packages]
3636
3737
The test command translates and runs tests for the specified packages. It first
3838
invokes translate, and then invokes 'go test' on the translated code, passing
3939
through the -run and -v flags.
4040
41+
The -seeds flag specifies what seeds to run the tests on as a comma-separated
42+
list of seeds and ranges, such as -seeds=1,2,5-10. The default is -seeds=1.
43+
4144
The 'debug' command:
4245
43-
Usage: gosim debug [-race] [-headless] -package=[package] -test=[test] -step=[step]
46+
Usage: gosim debug [-race] [-headless] -package=[package] -test=[test] [-seed=...] -step=[step]
4447
4548
The debug command translates and runs a specific test using the delve debugger.
4649
It first invokes translate, and then runs 'dlv test' on the specific test. The
4750
-step flag is the step to pause at as seen in the logs from running 'gosim test'.
4851
52+
The -seed flag is the seed to run. The default is -seed=1.
53+
4954
The -headless flag optionally runs delve in headless mode for use with an
5055
external interface like an IDE.
5156

cmd/gosim/main.go

+6
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func main() {
182182
logformat := testflags.String("logformat", "pretty", "gosim log formatting: raw|indented|pretty")
183183
simtrace := testflags.String("simtrace", "", "set of a comma-separated traces to enable")
184184
jsonlogout := testflags.String("jsonlogout", "", "path to a file to write json log to, for use with viewer")
185+
seeds := testflags.String("seeds", "1", "a comma separated list of seeds and ranges to run, such as 1,2,10-100,99")
185186
testflags.Parse(cmdArgs)
186187

187188
cfg.Race = *race
@@ -230,6 +231,9 @@ func main() {
230231
}
231232
args = append(args, "-jsonlogout", abs)
232233
}
234+
if *seeds != "" {
235+
args = append(args, "-seeds", *seeds)
236+
}
233237

234238
cmd := exec.Command(name, args...)
235239
cmd.Env = append(os.Environ(), "FORCE_COLOR=1")
@@ -368,6 +372,7 @@ func main() {
368372
race := debugflags.Bool("race", false, "build in -race mode")
369373
pkg := debugflags.String("package", "", "package path to debug")
370374
test := debugflags.String("test", "", "full test name to debug")
375+
seed := debugflags.Int("seed", 1, "seed to debug")
371376
step := debugflags.Int("step", 0, "step to break at")
372377
headless := debugflags.Bool("headless", false, "run headless for IDE debugging")
373378
debugflags.Parse(cmdArgs)
@@ -447,6 +452,7 @@ stepout`)
447452
"^"+*test+"$",
448453
"-test.v",
449454
"-step-breakpoint="+fmt.Sprint(*step),
455+
"-seeds="+fmt.Sprint(*seed),
450456
)
451457

452458
cmd := exec.Command(name, flags...)

examples/simple_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package examples_test
22

33
import (
4+
"math/rand"
45
"testing"
56

67
"github.com/jellevandenhooff/gosim"
78
)
89

9-
func TestHello(t *testing.T) {
10-
t.Log(gosim.IsSim())
10+
func TestGosim(t *testing.T) {
11+
t.Logf("Are we in the Matrix? %v", gosim.IsSim())
12+
t.Logf("Random: %d", rand.Int())
1113
}

gosimruntime/runtime.go

+7
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ func newMachine(label string) *Machine {
9090
}
9191

9292
type scheduler struct {
93+
seed int64
94+
9395
goroutines intrusiveList[*goroutine]
9496
runnable intrusiveList[*goroutine]
9597
nextGoroutineID int
@@ -124,6 +126,7 @@ func newScheduler(seed int64, logger logger, checksummer *checksummer, extraenv
124126
rand := mathrand.New(mathrand.NewSource(seed))
125127

126128
s := &scheduler{
129+
seed: seed,
127130
goroutines: make([]*goroutine, 0, 1024),
128131
runnable: make([]*goroutine, 0, 1024),
129132
nextGoroutineID: 1,
@@ -330,6 +333,10 @@ type internalRunResult struct {
330333
Err error
331334
}
332335

336+
func Seed() int64 {
337+
return gs.get().seed
338+
}
339+
333340
func run(f func(), seed int64, enableChecksum bool, captureLog bool, logLevelOverride string, simLogger io.Writer, extraenv []string) internalRunResult {
334341
if !runtimeInitialized {
335342
panic("not yet initialized")

gosimruntime/testmain.go

+57-19
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package gosimruntime
33
import (
44
"encoding/json"
55
"flag"
6+
"fmt"
67
"io"
78
"log"
89
"maps"
910
"os"
1011
"slices"
12+
"strconv"
1113
"strings"
1214
)
1315

@@ -68,8 +70,35 @@ var supportedFlags = map[string]bool{
6870
"test.v": true,
6971
}
7072

73+
type seedRange struct {
74+
start, end int64
75+
}
76+
77+
func parseSeeds(seedsStr string) ([]seedRange, error) {
78+
var seeds []seedRange
79+
for _, rangeStr := range strings.Split(seedsStr, ",") {
80+
startStr, endStr, ok := strings.Cut(rangeStr, "-")
81+
if !ok {
82+
endStr = startStr
83+
}
84+
85+
start, err := strconv.ParseInt(startStr, 10, 64)
86+
if err != nil {
87+
return nil, fmt.Errorf("bad start of seed range %q: %v", startStr, err)
88+
}
89+
end, err := strconv.ParseInt(endStr, 10, 64)
90+
if err != nil {
91+
return nil, fmt.Errorf("bad end of seed range %q: %v", endStr, err)
92+
}
93+
94+
seeds = append(seeds, seedRange{start: start, end: end})
95+
}
96+
return seeds, nil
97+
}
98+
7199
func TestMain(rt Runtime) {
72100
simtrace := flag.String("simtrace", "", "set of comma-separated traces to enable")
101+
seedsStr := flag.String("seeds", "1", "comma-separated list of seeds and ranges")
73102

74103
// TODO: make this flag beter; it won't work with multiple test runs?
75104
jsonlogout := flag.String("jsonlogout", "", "path to a file to write json log to, for use with viewer")
@@ -96,7 +125,11 @@ func TestMain(rt Runtime) {
96125
log.Fatal(err)
97126
}
98127

99-
seed := int64(1)
128+
seeds, err := parseSeeds(*seedsStr)
129+
if err != nil {
130+
log.Fatalf("parsing -seeds: %s", err)
131+
}
132+
100133
enableTracer := true
101134
captureLog := true
102135
logLevelOverride := "INFO"
@@ -114,25 +147,30 @@ func TestMain(rt Runtime) {
114147
}
115148
}
116149

117-
for _, test := range allTestsSlice {
118-
// log.Println("running", test.Name)
119-
result := run(func() {
120-
ok := rt.TestEntrypoint(match, skip, []Test{
121-
test,
122-
})
123-
if !ok {
124-
SetAbortError(ErrTestFailed)
150+
for _, seedRange := range seeds {
151+
for seed := seedRange.start; seed <= seedRange.end; seed++ {
152+
// TODO: filter list of tests outside since each run call has quite some overhead
153+
// for skipped tests.
154+
for _, test := range allTestsSlice {
155+
result := run(func() {
156+
ok := rt.TestEntrypoint(match, skip, []Test{
157+
test,
158+
})
159+
if !ok {
160+
SetAbortError(ErrTestFailed)
161+
}
162+
}, int64(seed), enableTracer, captureLog, logLevelOverride, makeConsoleLogger(os.Stderr), []string{})
163+
164+
if jsonout != nil {
165+
if _, err := jsonout.Write(result.LogOutput); err != nil {
166+
log.Fatalf("error writing jsonout: %s", err)
167+
}
168+
}
169+
170+
if result.Failed {
171+
outerOk = false
172+
}
125173
}
126-
}, seed, enableTracer, captureLog, logLevelOverride, makeConsoleLogger(os.Stderr), []string{})
127-
128-
if jsonout != nil {
129-
if _, err := jsonout.Write(result.LogOutput); err != nil {
130-
log.Fatalf("error writing jsonout: %s", err)
131-
}
132-
}
133-
134-
if result.Failed {
135-
outerOk = false
136174
}
137175
}
138176

internal/testing/testing.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ func (t *T) Run(name string, f func(t *T)) bool {
884884
// t.w = indenter{&t.common}
885885

886886
if t.chatty != nil {
887-
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
887+
t.chatty.Updatef(t.name, "=== RUN %s (seed %d)\n", t.name, gosimruntime.Seed())
888888
}
889889
running.Store(t.name, highPrecisionTimeNow())
890890

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# running gosim with seeds
2+
exec gosim test -run TestRand -v .
3+
stdout 'TestRand.*seed 1'
4+
! stdout 'TestRand.*seed 2'
5+
6+
exec gosim test -run TestRand -seeds 2-3,9 -v .
7+
! stdout 'TestRand.*seed 1$'
8+
stdout 'TestRand.*seed 2'
9+
stdout 'TestRand.*seed 3'
10+
! stdout 'TestRand.*seed 4'
11+
stdout 'TestRand.*seed 9'
12+
13+
-- seed_test.go --
14+
package behavior_test
15+
16+
import (
17+
"math/rand"
18+
"testing"
19+
)
20+
21+
func TestRand(t *testing.T) {
22+
t.Log(rand.Int())
23+
}

0 commit comments

Comments
 (0)