issue 975: a test that fails on a ticker and on a timer that names no bounded step it retries
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **A timer in this module names the bounded step it retries** (novox/hq ADR 0406 "How it is checked", issue 975): the
|
||||
// module acts on events, so a clock that fires on its own — a ticker, time.Tick — is a poll, and a one-shot timer
|
||||
// (time.AfterFunc, time.NewTimer) is allowed only for a bounded step, said in a comment on the line above:
|
||||
// `// retries: <step>`. This reads the module's Go source, not its tests.
|
||||
|
||||
var (
|
||||
pollingClock = regexp.MustCompile(`\btime\.(NewTicker|Tick)\(`)
|
||||
oneShot = regexp.MustCompile(`\btime\.(AfterFunc|NewTimer)\(`)
|
||||
retriesLine = regexp.MustCompile(`^\s*//\s*retries:\s*\S`)
|
||||
)
|
||||
|
||||
// timerFaults are the lines of a source file that make a clock of their own without naming the step it retries.
|
||||
func timerFaults(name, src string) []string {
|
||||
var faults []string
|
||||
lines := strings.Split(src, "\n")
|
||||
for i, line := range lines {
|
||||
code, _, _ := strings.Cut(line, "//")
|
||||
switch {
|
||||
case pollingClock.MatchString(code):
|
||||
faults = append(faults, name+":"+strconv.Itoa(i+1)+": a ticker polls; act on an event, or name a bounded step's one-shot timer: "+strings.TrimSpace(line))
|
||||
case oneShot.MatchString(code) && (i == 0 || !retriesLine.MatchString(lines[i-1])):
|
||||
faults = append(faults, name+":"+strconv.Itoa(i+1)+": a timer with no `// retries: <step>` comment on the line above: "+strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
return faults
|
||||
}
|
||||
|
||||
func TestNoTimerInTheModuleFiresWithoutNamingTheBoundedStepItRetries(t *testing.T) {
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil || len(files) == 0 {
|
||||
t.Fatalf("no source read: %v", err)
|
||||
}
|
||||
timers := 0
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, fault := range timerFaults(f, string(raw)) {
|
||||
t.Error(fault)
|
||||
}
|
||||
timers += len(oneShot.FindAllString(string(raw), -1))
|
||||
}
|
||||
if timers == 0 {
|
||||
t.Fatal("the test found no timer at all: it is not reading the source it should")
|
||||
}
|
||||
}
|
||||
|
||||
// The checker itself: the tick this module had before it acted on events, and a timer that names nothing, fail; a
|
||||
// named one passes.
|
||||
func TestTheTimerCheckFailsOnATickerAndOnAnUnnamedTimer(t *testing.T) {
|
||||
was := "\tgo func() {\n\t\ttick := time.NewTicker(10 * time.Second)\n\t\tdefer tick.Stop()\n\t}()\n"
|
||||
if got := timerFaults("main.go", was); len(got) != 1 || !strings.Contains(got[0], "main.go:2") {
|
||||
t.Fatalf("the old tick was not found: %v", got)
|
||||
}
|
||||
if got := timerFaults("x.go", "for range time.Tick(time.Second) {}\n"); len(got) != 1 {
|
||||
t.Fatalf("time.Tick was not found: %v", got)
|
||||
}
|
||||
if got := timerFaults("x.go", "\ttime.AfterFunc(d, f)\n"); len(got) != 1 {
|
||||
t.Fatalf("an unnamed timer was not found: %v", got)
|
||||
}
|
||||
if got := timerFaults("x.go", "\t// retries:\n\tt := time.NewTimer(d)\n"); len(got) != 1 {
|
||||
t.Fatalf("a comment that names no step passed: %v", got)
|
||||
}
|
||||
if got := timerFaults("x.go", "\t// retries: the refused merge, after a minute\n\ttime.AfterFunc(d, f)\n"); len(got) != 0 {
|
||||
t.Fatalf("a named timer failed: %v", got)
|
||||
}
|
||||
if got := timerFaults("x.go", "\t// time.NewTicker was here\n"); len(got) != 0 {
|
||||
t.Fatalf("a comment about a ticker failed: %v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user