Repository navigation
Expand file tree
/
Copy pathfuzz_test.go
More file actions
154 lines (144 loc) · 4.34 KB
/
Copy pathfuzz_test.go
File metadata and controls
154 lines (144 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package cron
import (
"testing"
"time"
)
// fuzzZones are the timezones FuzzSpecScheduleNext exercises: UTC, a US DST
// zone, a fractional-offset DST zone (Lord Howe shifts 30 minutes), and a
// European DST zone.
var fuzzZones = func() []*time.Location {
zones := []*time.Location{time.UTC}
for _, name := range []string{"America/New_York", "Australia/Lord_Howe", "Europe/Berlin"} {
if loc, err := time.LoadLocation(name); err == nil {
zones = append(zones, loc)
}
}
return zones
}()
// linearNextMinute is the golden reference for SpecSchedule.Next on 5-field
// specs: walk forward one absolute minute at a time and return the first
// wall-clock match — the semantic definition itself, immune to DST math bugs.
func linearNextMinute(s *SpecSchedule, after time.Time, window time.Duration) time.Time {
loc := s.loc
t := after.In(loc)
cursor := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, loc).Add(time.Minute)
limit := after.Add(window)
for !cursor.After(limit) {
if s.second&1 != 0 &&
1<<uint(cursor.Minute())&s.minute != 0 &&
1<<uint(cursor.Hour())&s.hour != 0 &&
1<<uint(cursor.Month())&s.month != 0 &&
dayMatches(s, cursor, cursor.Day()) {
return cursor
}
cursor = cursor.Add(time.Minute)
}
return time.Time{}
}
// FuzzSpecScheduleNext checks Next's core properties on fuzzed specs, times,
// and DST-observing zones, and differentially compares it against a linear
// wall-clock scan over the following 48 hours.
func FuzzSpecScheduleNext(f *testing.F) {
type seed struct {
spec string
sec int64
zone uint8
}
springForward := time.Date(2026, 3, 8, 0, 30, 0, 0, time.UTC).Unix()
fallBack := time.Date(2026, 11, 1, 0, 0, 0, 0, time.UTC).Unix()
seeds := []seed{
{"0 5 * * *", springForward, 1},
{"30 2 * * *", springForward, 1},
{"30 1 * * *", fallBack, 1},
{"*/15 * * * *", springForward, 2},
{"0 9,18 * * 1-5", springForward, 3},
{"15,45 3 1,15 * *", time.Date(2026, 1, 31, 23, 59, 0, 0, time.UTC).Unix(), 0},
{"0 0 29 2 *", time.Date(2027, 12, 31, 0, 0, 0, 0, time.UTC).Unix(), 0},
{"@daily", springForward, 1},
}
for _, s := range seeds {
f.Add(s.spec, s.sec, s.zone)
}
f.Fuzz(func(t *testing.T, spec string, sec int64, zoneIdx uint8) {
loc := fuzzZones[int(zoneIdx)%len(fuzzZones)]
p := NewStandardParser(WithDefaultLocation(loc))
sched, err := p.Parse(spec)
if err != nil {
return
}
s, ok := sched.(*SpecSchedule)
if !ok {
return
}
// Clamp to 2020..2030 so the scan stays in tzdata-rich territory.
const lo, hi = 1577836800, 1893456000
sec = lo + ((sec%(hi-lo))+(hi-lo))%(hi-lo)
from := time.Unix(sec, int64(sec%1e9)).In(loc)
got := s.Next(from)
if got.IsZero() {
return // exhausted (e.g. Feb 30); nothing more to check
}
if !got.After(from) {
t.Fatalf("spec=%q from=%v: Next=%v not strictly after", spec, from, got)
}
if got.Nanosecond() != 0 {
t.Fatalf("spec=%q: Next=%v has sub-second component", spec, got)
}
if got.Location() != from.Location() {
t.Fatalf("spec=%q: Next changed location %v -> %v", spec, from.Location(), got.Location())
}
if again := s.Next(from); !again.Equal(got) {
t.Fatalf("spec=%q: Next not deterministic: %v vs %v", spec, got, again)
}
if n2 := s.Next(got); !n2.IsZero() && !n2.After(got) {
t.Fatalf("spec=%q: chain not monotonic: %v -> %v", spec, got, n2)
}
const window = 48 * time.Hour
ref := linearNextMinute(s, from, window)
switch {
case !ref.IsZero():
if !got.Equal(ref) {
t.Fatalf("spec=%q from=%v (%s): Next=%v, linear scan=%v",
spec, from, loc, got, ref)
}
case !got.After(from.Add(window)):
t.Fatalf("spec=%q from=%v (%s): Next=%v inside window but linear scan found nothing",
spec, from, loc, got)
}
})
}
func FuzzParser_StandardNeverPanics(f *testing.F) {
seeds := []string{
"* * * * *",
"@every 1s",
"@hourly",
"0 0 * * *",
"*/5 * * * *",
"15,45 * * * *",
"TZ=UTC 0 0 * * *",
"@every 90s",
"0-30/5 * * * *",
}
for _, s := range seeds {
f.Add(s)
}
p := NewStandardParser()
from := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
f.Fuzz(func(t *testing.T, spec string) {
s, err := p.Parse(spec)
if err != nil {
return
}
cur := from
for range 32 {
next := s.Next(cur)
if next.IsZero() {
return
}
if !next.After(cur) {
t.Fatalf("non-monotonic: spec=%q cur=%v next=%v", spec, cur, next)
}
cur = next
}
})
}