├── .gitignore ├── internal └── util.go ├── simulation ├── internal │ ├── events │ │ ├── generator.go │ │ ├── event.go │ │ ├── generator_once.go │ │ ├── generator_periodic.go │ │ ├── queue.go │ │ ├── configs.go │ │ ├── event_test.go │ │ ├── generator_once_test.go │ │ ├── configs_test.go │ │ ├── generator_periodic_test.go │ │ └── queue_test.go │ ├── clock │ │ ├── clock.go │ │ └── clock_test.go │ ├── config │ │ ├── config.go │ │ └── config_test.go │ ├── waitgroups │ │ ├── waitgroup.go │ │ ├── generatorwaitgroups.go │ │ ├── waitgroup_test.go │ │ ├── generatorwaitgroups_test.go │ │ ├── eventwaitgroups.go │ │ └── eventwaitgroups_test.go │ └── data │ │ ├── bitmap.go │ │ ├── taggedstore.go │ │ ├── bitmap_test.go │ │ └── taggedstore_test.go ├── config │ └── config.go ├── scheduler.go └── scheduler_test.go ├── .mockery.yaml ├── go.mod ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── system ├── scheduler.go └── scheduler_test.go ├── examples ├── simple_test.go ├── processor_test.go └── repetitive_test.go ├── go.sum ├── model.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | mock_* 2 | .vscode 3 | .idea -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | func Ptr[T any](t T) *T { 4 | return &t 5 | } 6 | -------------------------------------------------------------------------------- /simulation/internal/events/generator.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ErrGeneratorFinished = errors.New("event generator is finished") 8 | 9 | type Generator interface { 10 | Pop() *Event 11 | Peek() Event 12 | 13 | Finished() bool 14 | } 15 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | #file: noinspection SpellCheckingInspection 2 | with-expecter: true 3 | dir: "{{.InterfaceDir}}" 4 | inpackage: true 5 | packages: 6 | github.com/metamogul/timestone/v2: 7 | interfaces: 8 | Action: 9 | github.com/metamogul/timestone/v2/simulation/internal/events: 10 | interfaces: 11 | Generator: -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/metamogul/timestone/v2 2 | 3 | go 1.23 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/kr/pretty v0.3.0 // indirect 10 | github.com/pmezard/go-difflib v1.0.0 // indirect 11 | github.com/stretchr/objx v0.5.2 // indirect 12 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /simulation/internal/clock/clock.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Clock struct { 8 | now time.Time 9 | } 10 | 11 | func NewClock(now time.Time) *Clock { 12 | return &Clock{ 13 | now: now, 14 | } 15 | } 16 | 17 | func (c *Clock) Now() time.Time { 18 | return c.now 19 | } 20 | 21 | func (c *Clock) Set(t time.Time) { 22 | if t.Before(c.now) { 23 | panic("time can't be in the past") 24 | } 25 | 26 | c.now = t 27 | } 28 | -------------------------------------------------------------------------------- /simulation/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/metamogul/timestone/v2/simulation/config" 5 | "time" 6 | ) 7 | 8 | type At struct { 9 | Tags []string 10 | Time time.Time 11 | } 12 | 13 | func (b At) GetTags() []string { 14 | return b.Tags 15 | } 16 | 17 | func Convert(before config.Before, eventTime time.Time) At { 18 | return At{ 19 | Tags: before.Tags, 20 | Time: eventTime.Add(before.Interval), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /simulation/internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/metamogul/timestone/v2/simulation/config" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestConvert(t *testing.T) { 11 | t.Parallel() 12 | 13 | before := config.Before{ 14 | Interval: -1, 15 | Tags: []string{"test"}, 16 | } 17 | 18 | beforeInternal := Convert(before, time.Time{}) 19 | require.Equal(t, before.Tags, beforeInternal.Tags) 20 | require.Equal(t, time.Time{}.Add(before.Interval), beforeInternal.Time) 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a problem you have found 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of the buggy behaviour. 12 | 13 | **To Reproduce** 14 | Code snippet or example that reproduces the bug. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Versions** 20 | - Go version 21 | - Timestone version 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /simulation/internal/waitgroups/waitgroup.go: -------------------------------------------------------------------------------- 1 | package waitgroups 2 | 3 | import "sync" 4 | 5 | type waitGroup struct { 6 | waitGroup sync.WaitGroup 7 | count int 8 | mu sync.Mutex 9 | } 10 | 11 | func (w *waitGroup) add(delta int) { 12 | if delta == 0 { 13 | return 14 | } 15 | 16 | w.mu.Lock() 17 | defer w.mu.Unlock() 18 | 19 | if w.count+delta < 0 { 20 | delta = -w.count 21 | } 22 | 23 | w.count += delta 24 | w.waitGroup.Add(delta) 25 | } 26 | 27 | func (w *waitGroup) done() { 28 | w.mu.Lock() 29 | defer w.mu.Unlock() 30 | 31 | if w.count == 0 { 32 | return 33 | } 34 | 35 | w.count-- 36 | w.waitGroup.Done() 37 | } 38 | 39 | func (w *waitGroup) wait() { 40 | w.waitGroup.Wait() 41 | } 42 | -------------------------------------------------------------------------------- /simulation/internal/events/event.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/vektra/mockery/v2@v2.43.2 2 | package events 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | "github.com/metamogul/timestone/v2" 9 | ) 10 | 11 | const DefaultTag = "" 12 | 13 | type Event struct { 14 | timestone.Action 15 | time.Time 16 | 17 | context.Context 18 | 19 | tags []string 20 | } 21 | 22 | func NewEvent(ctx context.Context, action timestone.Action, time time.Time, tags []string) *Event { 23 | if action == nil { 24 | panic("action can't be nil") 25 | } 26 | 27 | if len(tags) == 0 { 28 | tags = []string{DefaultTag} 29 | } 30 | 31 | return &Event{ 32 | Action: action, 33 | Time: time, 34 | Context: ctx, 35 | tags: tags, 36 | } 37 | } 38 | 39 | func (e *Event) Tags() []string { 40 | return e.tags 41 | } 42 | -------------------------------------------------------------------------------- /simulation/internal/events/generator_once.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/metamogul/timestone/v2" 8 | ) 9 | 10 | type OnceGenerator struct { 11 | event *Event 12 | ctx context.Context 13 | } 14 | 15 | func NewOnceGenerator(ctx context.Context, action timestone.Action, time time.Time, tags []string) *OnceGenerator { 16 | return &OnceGenerator{ 17 | event: NewEvent(ctx, action, time, tags), 18 | ctx: ctx, 19 | } 20 | } 21 | 22 | func (o *OnceGenerator) Pop() *Event { 23 | if o.Finished() { 24 | panic(ErrGeneratorFinished) 25 | } 26 | 27 | defer func() { o.event = nil }() 28 | 29 | return o.event 30 | } 31 | 32 | func (o *OnceGenerator) Peek() Event { 33 | if o.Finished() { 34 | panic(ErrGeneratorFinished) 35 | } 36 | 37 | return *o.event 38 | } 39 | 40 | func (o *OnceGenerator) Finished() bool { 41 | return o.event == nil || o.ctx.Err() != nil 42 | } 43 | -------------------------------------------------------------------------------- /simulation/internal/waitgroups/generatorwaitgroups.go: -------------------------------------------------------------------------------- 1 | package waitgroups 2 | 3 | import ( 4 | "fmt" 5 | "github.com/metamogul/timestone/v2/simulation/internal/data" 6 | "sync" 7 | ) 8 | 9 | type GeneratorWaitGroups struct { 10 | waitGroups *data.TaggedStore[*waitGroup] 11 | 12 | mu sync.RWMutex 13 | } 14 | 15 | func NewGeneratorWaitGroups() *GeneratorWaitGroups { 16 | return &GeneratorWaitGroups{ 17 | waitGroups: data.NewTaggedStore[*waitGroup](), 18 | } 19 | } 20 | 21 | func (w *GeneratorWaitGroups) Add(delta int, tags []string) { 22 | w.mu.Lock() 23 | defer w.mu.Unlock() 24 | 25 | matchingEntry := w.waitGroups.Matching(tags) 26 | if matchingEntry == nil { 27 | matchingEntry = &waitGroup{} 28 | w.waitGroups.Set(matchingEntry, tags) 29 | } 30 | 31 | matchingEntry.add(delta) 32 | } 33 | 34 | func (w *GeneratorWaitGroups) Done(tags []string) { 35 | w.mu.RLock() 36 | defer w.mu.RUnlock() 37 | 38 | matchingEntries := w.waitGroups.ContainedIn(tags) 39 | if len(matchingEntries) == 0 { 40 | return 41 | } 42 | 43 | for _, matchingEntry := range matchingEntries { 44 | matchingEntry.done() 45 | } 46 | } 47 | 48 | func (w *GeneratorWaitGroups) WaitFor(tags []string) { 49 | w.mu.RLock() 50 | defer w.mu.RUnlock() 51 | 52 | waitGroupForTags := w.waitGroups.Matching(tags) 53 | if waitGroupForTags == nil { 54 | panic(fmt.Sprintf("WaitGroup for %v does not exist", tags)) 55 | } 56 | 57 | waitGroupForTags.wait() 58 | } 59 | -------------------------------------------------------------------------------- /system/scheduler.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/metamogul/timestone/v2" 8 | ) 9 | 10 | type Clock struct{} 11 | 12 | func (c Clock) Now() time.Time { 13 | return time.Now() 14 | } 15 | 16 | type Scheduler struct { 17 | Clock 18 | } 19 | 20 | func (s *Scheduler) PerformNow(ctx context.Context, action timestone.Action, _ ...string) { 21 | go func() { 22 | select { 23 | case <-ctx.Done(): 24 | return 25 | default: 26 | action.Perform(context.WithValue(ctx, timestone.ActionContextClockKey, s.Clock)) 27 | } 28 | }() 29 | } 30 | 31 | func (s *Scheduler) PerformAfter(ctx context.Context, action timestone.Action, duration time.Duration, _ ...string) { 32 | go func() { 33 | select { 34 | case <-time.After(duration): 35 | action.Perform(context.WithValue(ctx, timestone.ActionContextClockKey, s.Clock)) 36 | case <-ctx.Done(): 37 | return 38 | } 39 | }() 40 | } 41 | 42 | func (s *Scheduler) PerformRepeatedly(ctx context.Context, action timestone.Action, until *time.Time, interval time.Duration, _ ...string) { 43 | ticker := time.NewTicker(interval) 44 | 45 | var timer *time.Timer 46 | if until != nil { 47 | timer = time.NewTimer(until.Sub(s.Now())) 48 | } else { 49 | timer = &time.Timer{} 50 | } 51 | 52 | go func() { 53 | for { 54 | select { 55 | case <-ticker.C: 56 | action.Perform(context.WithValue(ctx, timestone.ActionContextClockKey, s.Clock)) 57 | case <-timer.C: 58 | return 59 | case <-ctx.Done(): 60 | return 61 | } 62 | } 63 | }() 64 | } 65 | -------------------------------------------------------------------------------- /simulation/internal/clock/clock_test.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNewClock(t *testing.T) { 11 | t.Parallel() 12 | 13 | now := time.Now() 14 | 15 | clock := NewClock(now) 16 | 17 | require.NotNil(t, clock) 18 | require.Equal(t, now, clock.Now()) 19 | } 20 | 21 | func TestClock_Now(t *testing.T) { 22 | t.Parallel() 23 | 24 | now := time.Now() 25 | 26 | clock := Clock{now} 27 | require.Equal(t, now, clock.Now()) 28 | } 29 | 30 | func Test_clock_Set(t *testing.T) { 31 | t.Parallel() 32 | 33 | now := time.Now() 34 | 35 | tests := []struct { 36 | name string 37 | now time.Time 38 | newTime time.Time 39 | requirePanic bool 40 | }{ 41 | { 42 | name: "newMatching time in the past", 43 | now: now, 44 | newTime: now.Add(-time.Second), 45 | requirePanic: true, 46 | }, 47 | { 48 | name: "newMatching time equals current time", 49 | now: now, 50 | newTime: now, 51 | }, 52 | { 53 | name: "newMatching time after curent time", 54 | now: now, 55 | newTime: now.Add(time.Second), 56 | }, 57 | } 58 | 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | t.Parallel() 62 | 63 | c := &Clock{ 64 | now: tt.now, 65 | } 66 | 67 | if tt.requirePanic { 68 | require.Panics(t, func() { 69 | c.Set(tt.newTime) 70 | }) 71 | return 72 | } 73 | 74 | c.Set(tt.newTime) 75 | require.Equal(t, tt.newTime, c.now) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /simulation/internal/data/bitmap.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // bitmap implements a bit map. 4 | // IMPORTANT: Note that this implementation assumes that 5 | // bitmaps are always trimmed to not contain trailing 6 | // zero chunks. 7 | type bitmap []uint64 8 | 9 | func newBitmap(index int) bitmap { 10 | countSlices := index/64 + 1 11 | positionLastSlice := index % 64 12 | 13 | result := make(bitmap, countSlices) 14 | result[len(result)-1] = 1 << positionLastSlice 15 | 16 | return result 17 | } 18 | 19 | func (b *bitmap) or(bb bitmap) { 20 | if len(*b) < len(bb) { 21 | *b = append(*b, make([]uint64, len(bb)-len(*b))...) 22 | } 23 | 24 | for chunkIndex := range len(*b) { 25 | if chunkIndex >= len(bb) { 26 | break 27 | } 28 | 29 | (*b)[chunkIndex] = (*b)[chunkIndex] | bb[chunkIndex] 30 | } 31 | } 32 | 33 | func (b *bitmap) contains(target bitmap) bool { 34 | if len(target) > len(*b) { 35 | return false 36 | } 37 | 38 | for chunkIndex := range target { 39 | if (*b)[chunkIndex]&target[chunkIndex] != target[chunkIndex] { 40 | return false 41 | } 42 | } 43 | 44 | return true 45 | } 46 | 47 | func (b *bitmap) containedIn(target bitmap) bool { 48 | if len(target) < len(*b) { 49 | return false 50 | } 51 | 52 | for chunkIndex := range *b { 53 | if target[chunkIndex]&(*b)[chunkIndex] != (*b)[chunkIndex] { 54 | return false 55 | } 56 | } 57 | 58 | return true 59 | } 60 | 61 | func (b *bitmap) equal(target bitmap) bool { 62 | if len(target) != len(*b) { 63 | return false 64 | } 65 | 66 | for chunkIndex := range target { 67 | if (*b)[chunkIndex] != target[chunkIndex] { 68 | return false 69 | } 70 | } 71 | 72 | return true 73 | } 74 | -------------------------------------------------------------------------------- /simulation/internal/events/generator_periodic.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/metamogul/timestone/v2" 8 | ) 9 | 10 | type PeriodicGenerator struct { 11 | action timestone.Action 12 | from time.Time 13 | to *time.Time 14 | interval time.Duration 15 | 16 | tags []string 17 | 18 | nextEvent *Event 19 | 20 | ctx context.Context 21 | } 22 | 23 | func NewPeriodicGenerator( 24 | ctx context.Context, 25 | action timestone.Action, 26 | from time.Time, 27 | to *time.Time, 28 | interval time.Duration, 29 | tags []string, 30 | ) *PeriodicGenerator { 31 | if action == nil { 32 | panic("Action can't be nil") 33 | } 34 | 35 | if to != nil && !to.After(from) { 36 | panic("to must be after from") 37 | } 38 | 39 | if interval == 0 { 40 | panic("interval must be greater than zero") 41 | } 42 | 43 | if to != nil && interval >= to.Sub(from) { 44 | panic("interval must be shorter than timespan given by from and to") 45 | } 46 | 47 | firstEvent := NewEvent(ctx, action, from.Add(interval), tags) 48 | 49 | return &PeriodicGenerator{ 50 | action: action, 51 | from: from, 52 | to: to, 53 | interval: interval, 54 | 55 | tags: tags, 56 | 57 | nextEvent: firstEvent, 58 | 59 | ctx: ctx, 60 | } 61 | } 62 | 63 | func (p *PeriodicGenerator) Pop() *Event { 64 | if p.Finished() { 65 | panic(ErrGeneratorFinished) 66 | } 67 | 68 | defer func() { p.nextEvent = NewEvent(p.ctx, p.action, p.nextEvent.Time.Add(p.interval), p.tags) }() 69 | 70 | return p.nextEvent 71 | } 72 | 73 | func (p *PeriodicGenerator) Peek() Event { 74 | if p.Finished() { 75 | panic(ErrGeneratorFinished) 76 | } 77 | 78 | return *p.nextEvent 79 | } 80 | 81 | func (p *PeriodicGenerator) Finished() bool { 82 | if p.ctx.Err() != nil { 83 | return true 84 | } 85 | 86 | if p.to == nil { 87 | return false 88 | } 89 | 90 | return p.nextEvent.Add(p.interval).After(*p.to) 91 | } 92 | -------------------------------------------------------------------------------- /examples/simple_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/metamogul/timestone/v2" 10 | "github.com/metamogul/timestone/v2/simulation" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const simulateLoadMilliseconds = 10 15 | 16 | type register struct { 17 | counter int 18 | } 19 | 20 | type Action func(context.Context) 21 | 22 | func (a Action) Perform(ctx context.Context) { a(ctx) } 23 | 24 | func (a Action) Name() string { return "" } 25 | 26 | func (r *register) incrementAfterOneMinute(scheduler timestone.Scheduler) { 27 | scheduler.PerformAfter( 28 | context.Background(), 29 | Action(func(context.Context) { 30 | // Simulate execution time 31 | time.Sleep(time.Duration(simulateLoadMilliseconds) * time.Millisecond) 32 | 33 | r.counter++ 34 | }), 35 | time.Minute, 36 | ) 37 | } 38 | 39 | func (r *register) incrementEveryMinute(scheduler timestone.Scheduler) { 40 | mu := sync.Mutex{} 41 | 42 | scheduler.PerformRepeatedly( 43 | context.Background(), 44 | Action(func(context.Context) { 45 | mu.Lock() 46 | 47 | // Simulate execution time 48 | time.Sleep(time.Duration(simulateLoadMilliseconds) * time.Millisecond) 49 | 50 | r.counter++ 51 | 52 | mu.Unlock() 53 | }), 54 | nil, 55 | time.Minute, 56 | ) 57 | } 58 | 59 | func Test_incrementAfterOneMinute(t *testing.T) { 60 | t.Parallel() 61 | 62 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 63 | 64 | scheduler := simulation.NewScheduler(now) 65 | 66 | r := ®ister{} 67 | r.incrementAfterOneMinute(scheduler) 68 | 69 | scheduler.Forward(time.Minute * 60) 70 | require.Equal(t, 1, r.counter) 71 | } 72 | 73 | func Test_incrementEveryMinute(t *testing.T) { 74 | t.Parallel() 75 | 76 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 77 | 78 | scheduler := simulation.NewScheduler(now) 79 | 80 | r := ®ister{} 81 | r.incrementEveryMinute(scheduler) 82 | 83 | scheduler.Forward(time.Minute * 60) 84 | require.Equal(t, 60, r.counter) 85 | } 86 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 5 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 6 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 7 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 15 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 16 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 17 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 18 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 19 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 24 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /simulation/internal/waitgroups/waitgroup_test.go: -------------------------------------------------------------------------------- 1 | package waitgroups 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_waitGroup_add(t *testing.T) { 8 | t.Parallel() 9 | 10 | testcases := []struct { 11 | name string 12 | initialCount int 13 | addDelta int 14 | wantCount int 15 | }{ 16 | { 17 | name: "set zero", 18 | initialCount: 0, 19 | addDelta: 0, 20 | wantCount: 0, 21 | }, 22 | { 23 | name: "set one", 24 | initialCount: 0, 25 | addDelta: 1, 26 | wantCount: 1, 27 | }, 28 | { 29 | name: "set multiple", 30 | initialCount: 0, 31 | addDelta: 10, 32 | wantCount: 10, 33 | }, 34 | { 35 | name: "set negative, count+delta < 0", 36 | initialCount: 1, 37 | addDelta: -2, 38 | wantCount: 0, 39 | }, 40 | } 41 | 42 | for _, tt := range testcases { 43 | t.Run(tt.name, func(t *testing.T) { 44 | t.Parallel() 45 | 46 | w := waitGroup{} 47 | 48 | w.waitGroup.Add(tt.initialCount) 49 | w.count = tt.initialCount 50 | 51 | w.add(tt.addDelta) 52 | 53 | for range w.count { 54 | go func() { 55 | w.done() 56 | }() 57 | } 58 | w.wait() 59 | }) 60 | } 61 | } 62 | 63 | func Test_waitGroup_done(t *testing.T) { 64 | t.Parallel() 65 | 66 | testcases := []struct { 67 | name string 68 | initialCount int 69 | timesDone int 70 | wantCount int 71 | }{ 72 | { 73 | name: "count greater zero", 74 | initialCount: 1, 75 | timesDone: 1, 76 | wantCount: 0, 77 | }, 78 | { 79 | name: "count is zero", 80 | initialCount: 0, 81 | timesDone: 1, 82 | wantCount: 0, 83 | }, 84 | } 85 | 86 | for _, tt := range testcases { 87 | t.Run(tt.name, func(t *testing.T) { 88 | t.Parallel() 89 | 90 | w := waitGroup{} 91 | 92 | w.waitGroup.Add(tt.initialCount) 93 | w.count = tt.initialCount 94 | 95 | for range tt.timesDone { 96 | go func() { w.done() }() 97 | } 98 | w.wait() 99 | }) 100 | } 101 | } 102 | 103 | func Test_waitGroup_wait(t *testing.T) { 104 | t.Parallel() 105 | 106 | w := waitGroup{} 107 | 108 | const delta = 5 109 | 110 | w.add(delta) 111 | for range delta { 112 | go func() { w.done() }() 113 | } 114 | 115 | w.wait() 116 | } 117 | -------------------------------------------------------------------------------- /simulation/internal/waitgroups/generatorwaitgroups_test.go: -------------------------------------------------------------------------------- 1 | package waitgroups 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_NewGeneratorWaitGroups(t *testing.T) { 10 | t.Parallel() 11 | 12 | newWaitGroups := NewGeneratorWaitGroups() 13 | 14 | require.NotNil(t, newWaitGroups) 15 | require.Empty(t, newWaitGroups.waitGroups.All()) 16 | } 17 | 18 | func Test_GeneratorWaitGroups_Add(t *testing.T) { 19 | t.Parallel() 20 | 21 | w := NewGeneratorWaitGroups() 22 | 23 | w.Add(1, []string{"test1"}) 24 | go func() { w.Done([]string{"test1", "test2"}) }() 25 | w.WaitFor([]string{"test1"}) 26 | } 27 | 28 | func Test_GeneratorWaitGroups_Done(t *testing.T) { 29 | t.Parallel() 30 | 31 | w := NewGeneratorWaitGroups() 32 | 33 | t.Run("one exact matching call", func(t *testing.T) { 34 | w.Add(1, []string{"testGroup", "test1"}) 35 | go func() { 36 | w.Done([]string{"testGroup", "test1"}) 37 | }() 38 | w.WaitFor([]string{"testGroup", "test1"}) 39 | }) 40 | 41 | t.Run("sufficient done calls", func(t *testing.T) { 42 | w.Add(2, []string{"testGroup"}) 43 | go func() { 44 | w.Done([]string{"testGroup", "test1"}) 45 | w.Done([]string{"testGroup", "test2"}) 46 | }() 47 | w.WaitFor([]string{"testGroup"}) 48 | }) 49 | 50 | t.Run("more than sufficient done calls", func(t *testing.T) { 51 | w.Add(2, []string{"testGroup"}) 52 | go func() { 53 | w.Done([]string{"testGroup", "test1"}) 54 | w.Done([]string{"testGroup", "test2"}) 55 | w.Done([]string{"testGroup", "test3"}) 56 | }() 57 | w.WaitFor([]string{"testGroup"}) 58 | }) 59 | 60 | } 61 | 62 | func Test_GeneratorWaitGroups_WaitFor(t *testing.T) { 63 | t.Parallel() 64 | 65 | // TODO: add more cases 66 | // - cover multiple matching waitgroups for tagset 67 | // - test behavior that enables waiting for unavailable wgs 68 | 69 | t.Run("wait group for tags exists", func(t *testing.T) { 70 | t.Parallel() 71 | 72 | w := NewGeneratorWaitGroups() 73 | 74 | w.Add(1, []string{"test1", "test2"}) 75 | w.Add(1, []string{"test3", "test4"}) 76 | go func() { 77 | w.Done([]string{"test1", "test2"}) 78 | w.Done([]string{"test3", "test4"}) 79 | }() 80 | w.WaitFor([]string{"test1", "test2"}) 81 | w.WaitFor([]string{"test3", "test4"}) 82 | }) 83 | 84 | t.Run("wait group for tags doesn't exist", func(t *testing.T) { 85 | t.Parallel() 86 | 87 | w := NewGeneratorWaitGroups() 88 | require.Panics(t, func() { w.WaitFor([]string{"test5", "test2"}) }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /simulation/internal/events/queue.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/metamogul/timestone/v2/simulation/config" 5 | "github.com/metamogul/timestone/v2/simulation/internal/waitgroups" 6 | "slices" 7 | ) 8 | 9 | type Queue struct { 10 | configs *Configs 11 | activeGenerators []Generator 12 | finishedGenerators []Generator 13 | 14 | NewGeneratorsWaitGroups *waitgroups.GeneratorWaitGroups 15 | } 16 | 17 | func NewQueue(configs *Configs) *Queue { 18 | queue := &Queue{ 19 | configs: configs, 20 | activeGenerators: make([]Generator, 0), 21 | finishedGenerators: make([]Generator, 0), 22 | NewGeneratorsWaitGroups: waitgroups.NewGeneratorWaitGroups(), 23 | } 24 | 25 | return queue 26 | } 27 | 28 | func (q *Queue) Add(generator Generator) { 29 | if generator.Finished() { 30 | q.finishedGenerators = append(q.finishedGenerators, generator) 31 | return 32 | } 33 | 34 | q.activeGenerators = append(q.activeGenerators, generator) 35 | 36 | generatorEventTags := generator.Peek().tags 37 | q.NewGeneratorsWaitGroups.Done(generatorEventTags) 38 | 39 | q.sortActiveGenerators() 40 | } 41 | 42 | func (q *Queue) ExpectGenerators(expectedGenerators []*config.Generator) { 43 | for _, expectation := range expectedGenerators { 44 | q.NewGeneratorsWaitGroups.Add(expectation.Count, expectation.Tags) 45 | } 46 | } 47 | 48 | func (q *Queue) WaitForExpectedGenerators(expectedGenerators []*config.Generator) { 49 | for _, expectedGenerator := range expectedGenerators { 50 | q.NewGeneratorsWaitGroups.WaitFor(expectedGenerator.Tags) 51 | } 52 | } 53 | 54 | func (q *Queue) Pop() *Event { 55 | if q.Finished() { 56 | panic(ErrGeneratorFinished) 57 | } 58 | 59 | nextEvent := q.activeGenerators[0].Pop() 60 | 61 | if q.activeGenerators[0].Finished() { 62 | q.finishedGenerators = append(q.finishedGenerators, q.activeGenerators[0]) 63 | q.activeGenerators = q.activeGenerators[1:] 64 | } 65 | 66 | q.sortActiveGenerators() 67 | 68 | return nextEvent 69 | } 70 | 71 | func (q *Queue) Peek() Event { 72 | if q.Finished() { 73 | panic(ErrGeneratorFinished) 74 | } 75 | 76 | return q.activeGenerators[0].Peek() 77 | } 78 | 79 | func (q *Queue) Finished() bool { 80 | return len(q.activeGenerators) == 0 81 | } 82 | 83 | func (q *Queue) sortActiveGenerators() { 84 | slices.SortStableFunc(q.activeGenerators, func(a, b Generator) int { 85 | eventA, eventB := a.Peek(), b.Peek() 86 | 87 | if timeComparison := eventA.Time.Compare(eventB.Time); timeComparison != 0 { 88 | return timeComparison 89 | } 90 | 91 | priorityComparison := q.configs.Priority(&eventA) - q.configs.Priority(&eventB) 92 | 93 | return priorityComparison 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /simulation/internal/events/configs.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/metamogul/timestone/v2/simulation/config" 5 | configinternal "github.com/metamogul/timestone/v2/simulation/internal/config" 6 | "github.com/metamogul/timestone/v2/simulation/internal/data" 7 | "time" 8 | ) 9 | 10 | const ( 11 | // EventPriorityDefault represents the default priority for an event if 12 | // none has been Set. 13 | EventPriorityDefault = iota 14 | ) 15 | 16 | type Configs struct { 17 | configsByTags *data.TaggedStore[*config.Config] 18 | configsByTagsAndTime map[int64]*data.TaggedStore[*config.Config] 19 | } 20 | 21 | func NewConfigs() *Configs { 22 | return &Configs{ 23 | configsByTags: data.NewTaggedStore[*config.Config](), 24 | configsByTagsAndTime: make(map[int64]*data.TaggedStore[*config.Config]), 25 | } 26 | } 27 | 28 | func (c *Configs) Set(config config.Config) { 29 | if !config.Time.IsZero() { 30 | c.configsByTagsForTime(config.Time).Set(&config, config.Tags) 31 | return 32 | } 33 | 34 | c.configsByTags.Set(&config, config.Tags) 35 | } 36 | 37 | func (c *Configs) Priority(event *Event) int { 38 | if configuration := c.get(event); configuration != nil { 39 | return configuration.Priority 40 | } 41 | 42 | return EventPriorityDefault 43 | } 44 | 45 | func (c *Configs) BlockingEvents(event *Event) []config.Event { 46 | if configuration := c.get(event); configuration != nil { 47 | 48 | blockingEvents := configuration.WaitFor 49 | 50 | result := make([]config.Event, len(blockingEvents)) 51 | for i, blockingEvent := range blockingEvents { 52 | switch blockingEvent := blockingEvent.(type) { 53 | case config.Before: 54 | result[i] = configinternal.Convert(blockingEvent, event.Time) 55 | default: 56 | result[i] = blockingEvent 57 | } 58 | } 59 | 60 | return result 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (c *Configs) ExpectedGenerators(event *Event) []*config.Generator { 67 | if configuration := c.get(event); configuration != nil { 68 | return configuration.Adds 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (c *Configs) configsByTagsForTime(time time.Time) *data.TaggedStore[*config.Config] { 75 | result, exists := c.configsByTagsAndTime[time.UnixMilli()] 76 | 77 | if !exists { 78 | result = data.NewTaggedStore[*config.Config]() 79 | c.configsByTagsAndTime[time.UnixMilli()] = result 80 | } 81 | 82 | return result 83 | } 84 | 85 | func (c *Configs) get(event *Event) *config.Config { 86 | if configuration := c.configsByTagsForTime(event.Time).Matching(event.tags); configuration != nil { 87 | return configuration 88 | } 89 | 90 | if configuration := c.configsByTags.Matching(event.tags); configuration != nil { 91 | return configuration 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /simulation/internal/events/event_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/metamogul/timestone/v2" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_NewEvent(t *testing.T) { 13 | t.Parallel() 14 | 15 | type args struct { 16 | ctx context.Context 17 | action timestone.Action 18 | actionTime time.Time 19 | tags []string 20 | } 21 | 22 | tests := []struct { 23 | name string 24 | args args 25 | want *Event 26 | requirePanic bool 27 | }{ 28 | { 29 | name: "no Action", 30 | args: args{ 31 | action: nil, 32 | actionTime: time.Time{}, 33 | }, 34 | requirePanic: true, 35 | }, 36 | { 37 | name: "success, no tags provided", 38 | args: args{ 39 | ctx: context.Background(), 40 | action: timestone.NewMockAction(t), 41 | actionTime: time.Time{}, 42 | tags: nil, 43 | }, 44 | want: &Event{ 45 | Context: context.Background(), 46 | Action: timestone.NewMockAction(t), 47 | Time: time.Time{}, 48 | tags: []string{DefaultTag}, 49 | }, 50 | }, 51 | { 52 | name: "success, tags empty", 53 | args: args{ 54 | ctx: context.Background(), 55 | action: timestone.NewMockAction(t), 56 | actionTime: time.Time{}, 57 | tags: []string{}, 58 | }, 59 | want: &Event{ 60 | Context: context.Background(), 61 | Action: timestone.NewMockAction(t), 62 | Time: time.Time{}, 63 | tags: []string{DefaultTag}, 64 | }, 65 | }, 66 | { 67 | name: "success", 68 | args: args{ 69 | ctx: context.Background(), 70 | action: timestone.NewMockAction(t), 71 | actionTime: time.Time{}, 72 | tags: []string{"foo", "bar"}, 73 | }, 74 | want: &Event{ 75 | Context: context.Background(), 76 | Action: timestone.NewMockAction(t), 77 | Time: time.Time{}, 78 | tags: []string{"foo", "bar"}, 79 | }, 80 | }, 81 | } 82 | 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | t.Parallel() 86 | 87 | if tt.requirePanic { 88 | require.Panics(t, func() { 89 | _ = NewEvent(tt.args.ctx, tt.args.action, tt.args.actionTime, tt.args.tags) 90 | }) 91 | return 92 | } 93 | 94 | require.Equal(t, tt.want, NewEvent(tt.args.ctx, tt.args.action, tt.args.actionTime, tt.args.tags)) 95 | }) 96 | } 97 | } 98 | 99 | func Test_Event_Tags(t *testing.T) { 100 | t.Parallel() 101 | 102 | e := NewEvent(context.Background(), timestone.NewMockAction(t), time.Now(), []string{"test1", "test2"}) 103 | require.Equal(t, []string{"test1", "test2"}, e.Tags()) 104 | } 105 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package timestone 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Clock provides access to the current time and should be used inside 9 | // actions instead of calling time.Now(). It is available as value in 10 | // the context.Context inside an action under the key 11 | // ActionContextClockKey 12 | type Clock interface { 13 | // Now returns the current time. 14 | Now() time.Time 15 | } 16 | 17 | // ActionContextClockKey provides access to a Clock as value in the 18 | // context.Context inside an Action. 19 | const ActionContextClockKey = "timestone.ActionContextClock" 20 | 21 | // An Action is a function to be scheduled by a Scheduler instance. 22 | // It is identified by a name, e.g. for other Action s to wait for it. 23 | type Action interface { 24 | // Perform executes the action. A clock is passed inside ctx at the 25 | // ActionContextClockKey. 26 | Perform(ctx context.Context) 27 | } 28 | 29 | // SimpleAction provides a reference implementation for Action that 30 | // covers most use cases. 31 | type SimpleAction func(context.Context) 32 | 33 | // Perform implements Action and performs the func aliased by SimpleAction. 34 | func (s SimpleAction) Perform(ctx context.Context) { 35 | s(ctx) 36 | } 37 | 38 | // Scheduler encapsulates the scheduling of Action s and should replace 39 | // every use of goroutines to enable deterministic unit tests. 40 | // 41 | // The system.Scheduler implementation will use goroutines for scheduling 42 | // using well established concurrency patterns. It is intended to be 43 | // passed as the actual production dependency to all components that 44 | // need to perform asynchronous Action s. 45 | // 46 | // The simulation.Scheduler implementation uses a configurable run loop 47 | // instead. It is intended for use in unit tests, where you can use the 48 | // simulation.Scheduler.ConfigureEvents method to provide various options 49 | // that help the Scheduler to establish a deterministic and repeatable 50 | // execution order of actions. 51 | type Scheduler interface { 52 | // Clock embeds a clock that represents the current point in time 53 | // as events are being executed. 54 | Clock 55 | // PerformNow schedules action to be executed immediately, that is 56 | // at the current time of the Scheduler's clock. 57 | PerformNow(ctx context.Context, action Action, tags ...string) 58 | // PerformAfter schedules an action to be run once after a delay 59 | // of duration. 60 | PerformAfter(ctx context.Context, action Action, duration time.Duration, tags ...string) 61 | // PerformRepeatedly schedules an action to be run every interval 62 | // after an initial delay of interval. If until is provided, the last 63 | // event will be run before or at until. 64 | PerformRepeatedly(ctx context.Context, action Action, until *time.Time, interval time.Duration, tags ...string) 65 | } 66 | -------------------------------------------------------------------------------- /simulation/internal/data/taggedstore.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // To support very large sets, there's great potential 4 | // for optimization here using a prefix tree as an index, 5 | // as well as compressing sparse bitmaps. For small sets 6 | // of a few ten to a few thousand items, this is sufficiently 7 | // good. 8 | 9 | type taggedValue[T any] struct { 10 | bitmap 11 | value T 12 | } 13 | 14 | type TaggedStore[T any] struct { 15 | bitmapsByTags map[string]bitmap 16 | content []taggedValue[T] 17 | } 18 | 19 | func NewTaggedStore[T any]() *TaggedStore[T] { 20 | return &TaggedStore[T]{ 21 | bitmapsByTags: make(map[string]bitmap), 22 | content: make([]taggedValue[T], 0), 23 | } 24 | } 25 | 26 | func (t *TaggedStore[T]) Set(value T, tags []string) { 27 | if len(tags) == 0 { 28 | panic("tags must not be empty") 29 | } 30 | 31 | bitmapForTags := t.bitmapForTags(tags) 32 | 33 | // If entry exists, replace value 34 | for i, entry := range t.content { 35 | if entry.bitmap.equal(bitmapForTags) { 36 | t.content[i].value = value 37 | return 38 | } 39 | } 40 | 41 | // Create new entry 42 | t.content = append(t.content, taggedValue[T]{ 43 | bitmap: bitmapForTags, 44 | value: value, 45 | }) 46 | } 47 | 48 | func (t *TaggedStore[T]) Containing(tags []string) []T { 49 | bitmaskForTags := t.bitmapForTags(tags) 50 | 51 | result := make([]T, 0, len(t.content)) 52 | for _, entry := range t.content { 53 | if entry.contains(bitmaskForTags) { 54 | result = append(result, entry.value) 55 | } 56 | } 57 | 58 | return result 59 | } 60 | 61 | func (t *TaggedStore[T]) ContainedIn(tags []string) []T { 62 | bitmaskForTags := t.bitmapForTags(tags) 63 | 64 | result := make([]T, 0, len(t.content)) 65 | for _, entry := range t.content { 66 | if entry.containedIn(bitmaskForTags) { 67 | result = append(result, entry.value) 68 | } 69 | } 70 | 71 | return result 72 | } 73 | 74 | func (t *TaggedStore[T]) Matching(tags []string) T { 75 | bitmaskForTags := t.bitmapForTags(tags) 76 | 77 | for _, entry := range t.content { 78 | if entry.equal(bitmaskForTags) { 79 | return entry.value 80 | } 81 | } 82 | 83 | return *new(T) 84 | } 85 | 86 | func (t *TaggedStore[T]) All() []T { 87 | var result []T 88 | for _, entry := range t.content { 89 | result = append(result, entry.value) 90 | } 91 | 92 | return result 93 | } 94 | 95 | func (t *TaggedStore[T]) bitmapForTag(tag string) bitmap { 96 | bitmapForTag, ok := t.bitmapsByTags[tag] 97 | if !ok { 98 | bitmapForTag = newBitmap(len(t.bitmapsByTags)) 99 | t.bitmapsByTags[tag] = bitmapForTag 100 | } 101 | 102 | return bitmapForTag 103 | } 104 | 105 | func (t *TaggedStore[T]) bitmapForTags(tags []string) bitmap { 106 | tagsBitmask := make(bitmap, len(t.bitmapsByTags)/64+1) 107 | 108 | for _, tag := range tags { 109 | tagsBitmask.or(t.bitmapForTag(tag)) 110 | } 111 | 112 | return tagsBitmask 113 | } 114 | -------------------------------------------------------------------------------- /simulation/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | // Event is used to target one or multiple events. 6 | type Event interface { 7 | GetTags() []string 8 | } 9 | 10 | // All is a Event targeting all events with Tags. 11 | type All struct { 12 | // Tags to address events. An event will match if it has been at least 13 | // tagged with all entries in Tags. 14 | Tags []string 15 | } 16 | 17 | func (a All) GetTags() []string { return a.Tags } 18 | 19 | // At is a key that will target at Time by Tags. 20 | type At struct { 21 | // Time is used to match only actions at the specific time. If you want 22 | // to match all actions with the given Tags, pass nil for Time. 23 | // 24 | // If no matching event to wait for is found the scheduler will panic. 25 | Time time.Time 26 | // Tags to address events. An event will match if it has been at least 27 | // tagged with all entries in Tags. 28 | Tags []string 29 | } 30 | 31 | func (a At) GetTags() []string { return a.Tags } 32 | 33 | type Before struct { 34 | // Before will match an event relative to the event that is configured. 35 | // 36 | // Unlike At, where a missing match will result in a panic, a missing 37 | // match from the Before Event will be silently ignored 38 | Interval time.Duration 39 | // Tags to address events. An event will match if it has been at least 40 | // tagged with all entries in Tags. 41 | Tags []string 42 | } 43 | 44 | func (r Before) GetTags() []string { return r.Tags } 45 | 46 | // Generator represents an expectation for a number of 47 | // event generators to be added to a simulation.Scheduler. It will block 48 | // the simulation.Scheduler until the expectation has been fulfilled. 49 | type Generator struct { 50 | // Tags are used to identify the expected generators. 51 | Tags []string 52 | // How many generators are expected to be added. 53 | Count int 54 | } 55 | 56 | // Config is used to provide settings for events when 57 | // being scheduled and executed in the simulation.Scheduler. 58 | type Config struct { 59 | // Tags to address events to configure. An event will match if it has 60 | // been at least tagged with all entries in Tags. 61 | Tags []string 62 | // Time is optional. If set, the Config will match specifically events 63 | // at the given Time. 64 | Time time.Time 65 | // Assign a Priority to define scheduling order in case of simultaneous 66 | // actions. 67 | Priority int 68 | // Delay the start of the execution of an action until the execution of 69 | // all WaitFor has been completed. This doesn't change the 70 | // behaviour for actions scheduled in ExecModeSequential which always 71 | // waits for every other scheduled action to complete execution. You can 72 | // set multiple groups of tags to target the respective actions. 73 | WaitFor []Event 74 | // Signal the Scheduler that the configured event is supposed to set more 75 | // event generators to the schedulers queue. The key of the map is the 76 | // name of the events spawned by the generator, while the value is the 77 | // number of corresponding newMatching event generators the Scheduler will 78 | // expect to hold before continuing. 79 | Adds []*Generator 80 | } 81 | -------------------------------------------------------------------------------- /system/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "github.com/metamogul/timestone/v2/internal" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/metamogul/timestone/v2" 11 | ) 12 | 13 | func TestScheduler_PerformNow(t *testing.T) { 14 | t.Parallel() 15 | 16 | ctx := context.Background() 17 | clock := Clock{} 18 | 19 | wg := &sync.WaitGroup{} 20 | 21 | mockAction := timestone.NewMockAction(t) 22 | mockAction.EXPECT(). 23 | Perform(context.WithValue(ctx, timestone.ActionContextClockKey, clock)). 24 | Run(func(context.Context) { wg.Done() }). 25 | Once() 26 | 27 | s := &Scheduler{Clock: clock} 28 | wg.Add(1) 29 | s.PerformNow(ctx, mockAction) 30 | wg.Wait() 31 | } 32 | 33 | func TestScheduler_PerformNow_cancelled(t *testing.T) { 34 | t.Parallel() 35 | 36 | ctx, cancel := context.WithCancel(context.Background()) 37 | cancel() 38 | clock := Clock{} 39 | 40 | s := &Scheduler{Clock: clock} 41 | s.PerformNow(ctx, timestone.NewMockAction(t)) 42 | time.Sleep(2 * time.Millisecond) 43 | } 44 | 45 | func TestScheduler_PerformAfter(t *testing.T) { 46 | t.Parallel() 47 | 48 | ctx := context.Background() 49 | clock := Clock{} 50 | 51 | wg := &sync.WaitGroup{} 52 | 53 | mockAction := timestone.NewMockAction(t) 54 | mockAction.EXPECT(). 55 | Perform(context.WithValue(ctx, timestone.ActionContextClockKey, clock)). 56 | Run(func(context.Context) { wg.Done() }). 57 | Once() 58 | 59 | s := &Scheduler{Clock: clock} 60 | wg.Add(1) 61 | s.PerformAfter(ctx, mockAction, time.Millisecond) 62 | wg.Wait() 63 | } 64 | 65 | func TestScheduler_PerformAfter_cancelled(t *testing.T) { 66 | t.Parallel() 67 | 68 | ctx, cancel := context.WithCancel(context.Background()) 69 | cancel() 70 | clock := Clock{} 71 | 72 | s := &Scheduler{Clock: clock} 73 | s.PerformAfter(ctx, timestone.NewMockAction(t), time.Millisecond) 74 | time.Sleep(2 * time.Millisecond) 75 | } 76 | 77 | func TestScheduler_PerformRepeatedly_until(t *testing.T) { 78 | t.Parallel() 79 | 80 | ctx := context.Background() 81 | clock := Clock{} 82 | 83 | wg := &sync.WaitGroup{} 84 | 85 | mockAction := timestone.NewMockAction(t) 86 | mockAction.EXPECT(). 87 | Perform(context.WithValue(ctx, timestone.ActionContextClockKey, clock)). 88 | Run(func(context.Context) { wg.Done() }). 89 | Twice() 90 | 91 | s := &Scheduler{Clock: Clock{}} 92 | wg.Add(2) 93 | s.PerformRepeatedly(ctx, mockAction, internal.Ptr(clock.Now().Add(3*time.Millisecond)), time.Millisecond) 94 | wg.Wait() 95 | } 96 | 97 | func TestScheduler_PerformRepeatedly_indefinitely(t *testing.T) { 98 | t.Parallel() 99 | 100 | ctx := context.Background() 101 | clock := Clock{} 102 | 103 | mockAction := timestone.NewMockAction(t) 104 | mockAction.EXPECT(). 105 | Perform(context.WithValue(ctx, timestone.ActionContextClockKey, clock)). 106 | Twice() 107 | 108 | s := &Scheduler{Clock: Clock{}} 109 | s.PerformRepeatedly(ctx, mockAction, nil, time.Millisecond) 110 | time.Sleep(3 * time.Millisecond) 111 | } 112 | 113 | func TestScheduler_PerformRepeatedly_cancelled(t *testing.T) { 114 | t.Parallel() 115 | 116 | ctx, cancel := context.WithCancel(context.Background()) 117 | cancel() 118 | clock := Clock{} 119 | 120 | s := &Scheduler{Clock: Clock{}} 121 | s.PerformRepeatedly(ctx, timestone.NewMockAction(t), internal.Ptr(clock.Now().Add(3*time.Millisecond)), time.Millisecond) 122 | time.Sleep(2 * time.Millisecond) 123 | } 124 | -------------------------------------------------------------------------------- /simulation/internal/waitgroups/eventwaitgroups.go: -------------------------------------------------------------------------------- 1 | package waitgroups 2 | 3 | import ( 4 | "fmt" 5 | "github.com/metamogul/timestone/v2/simulation/config" 6 | configinternal "github.com/metamogul/timestone/v2/simulation/internal/config" 7 | "github.com/metamogul/timestone/v2/simulation/internal/data" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type EventWaitGroups struct { 13 | waitGroups *data.TaggedStore[map[int64]*sync.WaitGroup] 14 | 15 | mu sync.RWMutex 16 | } 17 | 18 | func NewEventWaitGroups() *EventWaitGroups { 19 | return &EventWaitGroups{ 20 | waitGroups: data.NewTaggedStore[map[int64]*sync.WaitGroup](), 21 | } 22 | } 23 | 24 | func (e *EventWaitGroups) New(time time.Time, tags []string) *sync.WaitGroup { 25 | e.mu.Lock() 26 | defer e.mu.Unlock() 27 | 28 | waitGroupsForTags := e.waitGroups.Matching(tags) 29 | if waitGroupsForTags == nil { 30 | waitGroupsForTags = make(map[int64]*sync.WaitGroup) 31 | e.waitGroups.Set(waitGroupsForTags, tags) 32 | } 33 | 34 | timeUnixMilli := time.UnixMilli() 35 | waitGroupForTagsAndTime, exists := waitGroupsForTags[timeUnixMilli] 36 | if !exists { 37 | waitGroupForTagsAndTime = new(sync.WaitGroup) 38 | waitGroupsForTags[timeUnixMilli] = waitGroupForTagsAndTime 39 | } 40 | 41 | waitGroupForTagsAndTime.Add(1) 42 | 43 | return waitGroupForTagsAndTime 44 | } 45 | 46 | func (e *EventWaitGroups) WaitFor(events []config.Event) { 47 | // To understand why this implementation has been chosen, 48 | // consider an action with tag "action2" adding more actions tagged 49 | // "action2.1", with an "action1" previously called that has been 50 | // configured to Wait for "action2" as well as all "action2.1" created 51 | // by it. 52 | // At the time of calling WaitFor, no "action2.1" exists yet, and 53 | // in consequence also no WaitGroup for this name. Therefore we first 54 | // Wait for "action2" (or all other actions that already have a 55 | // corresponding WaitGroup) to give it a chance to spawn 56 | // the missing GeneratorWaitGroups and avoid a panic. 57 | 58 | for len(events) > 0 { 59 | var remainingEvents []config.Event 60 | 61 | e.mu.RLock() 62 | for _, eventKey := range events { 63 | foundAllWaitGroups := e.waitFor(eventKey) 64 | if !foundAllWaitGroups { 65 | remainingEvents = append(remainingEvents, eventKey) 66 | } 67 | } 68 | e.mu.RUnlock() 69 | 70 | if len(remainingEvents) == len(events) { 71 | panic(fmt.Sprintf("Wait group(s) for %v do not exist", events)) 72 | } 73 | 74 | events = remainingEvents 75 | } 76 | } 77 | 78 | func (e *EventWaitGroups) waitFor(event config.Event) (success bool) { 79 | waitGroupSetsForTagsByTime := e.waitGroups.Containing(event.GetTags()) 80 | if len(waitGroupSetsForTagsByTime) == 0 { 81 | _, ignoreMissmatch := event.(configinternal.At) 82 | return ignoreMissmatch 83 | } 84 | 85 | switch event := event.(type) { 86 | 87 | case configinternal.At: 88 | // Wait for events at time, ignore missing match 89 | for _, waitGroupsForTagsByTime := range waitGroupSetsForTagsByTime { 90 | wg, exists := waitGroupsForTagsByTime[event.Time.UnixMilli()] 91 | if !exists { 92 | return true 93 | } 94 | 95 | e.mu.RUnlock() // Unlock before waiting to avoid deadlocks 96 | wg.Wait() 97 | e.mu.RLock() // Reacquire the lock after waiting 98 | } 99 | 100 | case config.At: 101 | // Wait for events at time, don't ignore missing match 102 | for _, waitGroupsForTagsByTime := range waitGroupSetsForTagsByTime { 103 | wg, exists := waitGroupsForTagsByTime[event.Time.UnixMilli()] 104 | if !exists { 105 | return false 106 | } 107 | 108 | e.mu.RUnlock() // Unlock before waiting to avoid deadlocks 109 | wg.Wait() 110 | e.mu.RLock() // Reacquire the lock after waiting 111 | } 112 | 113 | case config.All: 114 | // Wait for all events containing tags 115 | for _, waitGroupsForTagsByTime := range waitGroupSetsForTagsByTime { 116 | e.mu.RUnlock() // Unlock before waiting to avoid deadlocks 117 | for _, wg := range waitGroupsForTagsByTime { 118 | wg.Wait() 119 | } 120 | e.mu.RLock() // Reacquire the lock after waiting 121 | } 122 | } 123 | 124 | return true 125 | } 126 | 127 | func (e *EventWaitGroups) Wait() { 128 | e.mu.RLock() 129 | allWaitGroupsByTime := e.waitGroups.All() 130 | e.mu.RUnlock() 131 | 132 | for _, waitGroupsByTime := range allWaitGroupsByTime { 133 | for _, wg := range waitGroupsByTime { 134 | wg.Wait() 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /simulation/internal/waitgroups/eventwaitgroups_test.go: -------------------------------------------------------------------------------- 1 | package waitgroups 2 | 3 | import ( 4 | "fmt" 5 | "github.com/metamogul/timestone/v2/simulation/config" 6 | configinternal "github.com/metamogul/timestone/v2/simulation/internal/config" 7 | "github.com/stretchr/testify/require" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func Test_NewEventWaitGroups(t *testing.T) { 13 | t.Parallel() 14 | 15 | newEventWaitGroups := NewEventWaitGroups() 16 | require.NotNil(t, newEventWaitGroups.waitGroups) 17 | require.Empty(t, newEventWaitGroups.waitGroups.All()) 18 | } 19 | 20 | func TestEventWaitGroups_New(t *testing.T) { 21 | t.Parallel() 22 | 23 | testcases := []struct { 24 | name string 25 | time time.Time 26 | tags []string 27 | addCount int 28 | }{ 29 | { 30 | name: "wait groups for tags doesn't exist", 31 | time: time.Time{}, 32 | tags: []string{"test"}, 33 | addCount: -1, 34 | }, 35 | { 36 | name: "wait groups for tags exists", 37 | time: time.Time{}, 38 | tags: []string{"testExists"}, 39 | addCount: -1, 40 | }, 41 | { 42 | name: "wait group for tags exists and entry for time exists", 43 | time: time.Time{}.Add(time.Second), 44 | tags: []string{"testExists"}, 45 | addCount: -2, 46 | }, 47 | } 48 | 49 | for _, tt := range testcases { 50 | t.Run(tt.name, func(t *testing.T) { 51 | t.Parallel() 52 | 53 | e := NewEventWaitGroups() 54 | _ = e.New(time.Time{}.Add(time.Second), []string{"testExists"}) 55 | 56 | wg := e.New(tt.time, tt.tags) 57 | go func() { wg.Add(tt.addCount) }() 58 | wg.Wait() 59 | }) 60 | } 61 | 62 | } 63 | 64 | func TestEventWaitGroups_WaitFor(t *testing.T) { 65 | t.Parallel() 66 | 67 | presentTags := []string{"test", "testGroup", "foo"} 68 | presentTime := time.Time{} 69 | 70 | e := NewEventWaitGroups() 71 | 72 | wg := e.New(presentTime, presentTags) 73 | go func() { 74 | wg2 := e.New(presentTime.Add(time.Second), presentTags) 75 | wg2.Done() 76 | wg.Done() 77 | }() 78 | 79 | e.WaitFor( 80 | []config.Event{ 81 | config.At{ 82 | Time: presentTime.Add(time.Second), 83 | Tags: presentTags, 84 | }, 85 | config.At{ 86 | Time: presentTime, 87 | Tags: presentTags, 88 | }, 89 | }, 90 | ) 91 | 92 | } 93 | 94 | func TestEventWaitGroups_waitFor(t *testing.T) { 95 | t.Parallel() 96 | 97 | testcases := []struct { 98 | name string 99 | presentTags []string 100 | presentTime time.Time 101 | waitForEventKey config.Event 102 | wantSuccess bool 103 | }{ 104 | { 105 | name: "match relative time, no result for tags", 106 | presentTags: []string{"test", "testGroup", "foo"}, 107 | presentTime: time.Time{}, 108 | waitForEventKey: configinternal.At{Time: time.Time{}, Tags: []string{"baz"}}, 109 | wantSuccess: true, 110 | }, 111 | { 112 | name: "match time, no result for tags", 113 | presentTags: []string{"test", "testGroup", "foo"}, 114 | presentTime: time.Time{}, 115 | waitForEventKey: config.At{Time: time.Time{}, Tags: []string{"baz"}}, 116 | wantSuccess: false, 117 | }, 118 | { 119 | name: "match all times, no result for tags", 120 | presentTags: []string{"test", "testGroup", "foo"}, 121 | presentTime: time.Time{}, 122 | waitForEventKey: config.All{Tags: []string{"baz"}}, 123 | wantSuccess: false, 124 | }, 125 | { 126 | name: "match relative time, has no match", 127 | waitForEventKey: configinternal.At{Time: time.Time{}.Add(-1), Tags: []string{"test"}}, 128 | wantSuccess: true, 129 | }, 130 | { 131 | name: "match relative time, has match", 132 | presentTags: []string{"test", "testGroup", "foo"}, 133 | presentTime: time.Time{}, 134 | waitForEventKey: configinternal.At{Time: time.Time{}, Tags: []string{"test"}}, 135 | wantSuccess: true, 136 | }, 137 | { 138 | name: "match time, has no match", 139 | presentTags: []string{"test", "testGroup", "foo"}, 140 | presentTime: time.Time{}, 141 | waitForEventKey: config.At{Time: time.Now(), Tags: []string{"test"}}, 142 | wantSuccess: false, 143 | }, 144 | { 145 | name: "match time, has match", 146 | presentTags: []string{"test", "testGroup", "foo"}, 147 | presentTime: time.Time{}, 148 | waitForEventKey: config.At{Time: time.Time{}, Tags: []string{"test"}}, 149 | wantSuccess: true, 150 | }, 151 | { 152 | name: "match all", 153 | presentTags: []string{"test", "testGroup", "foo"}, 154 | presentTime: time.Time{}, 155 | waitForEventKey: config.All{Tags: []string{"test"}}, 156 | wantSuccess: true, 157 | }, 158 | } 159 | 160 | for _, tt := range testcases { 161 | t.Run(tt.name, func(t *testing.T) { 162 | t.Parallel() 163 | 164 | e := NewEventWaitGroups() 165 | 166 | if tt.presentTags != nil { 167 | wg := e.New(tt.presentTime, tt.presentTags) 168 | go func() { wg.Done() }() 169 | } 170 | 171 | e.mu.RLock() 172 | success := e.waitFor(tt.waitForEventKey) 173 | e.mu.RUnlock() 174 | 175 | require.Equal(t, tt.wantSuccess, success) 176 | }) 177 | } 178 | } 179 | 180 | func TestEvenWaitGroups_Wait(t *testing.T) { 181 | t.Parallel() 182 | 183 | e := NewEventWaitGroups() 184 | 185 | for i := range 5 { 186 | testTag := fmt.Sprintf("test%d", i) 187 | wg1 := e.New(time.Time{}, []string{testTag}) 188 | wg2 := e.New(time.Time{}.Add(time.Second), []string{testTag}) 189 | go func() { wg1.Done(); wg2.Done() }() 190 | } 191 | 192 | e.Wait() 193 | } 194 | -------------------------------------------------------------------------------- /simulation/internal/events/generator_once_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/metamogul/timestone/v2" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_NewOnceGenerator(t *testing.T) { 13 | t.Parallel() 14 | 15 | type args struct { 16 | ctx context.Context 17 | action timestone.Action 18 | actionTime time.Time 19 | tags []string 20 | } 21 | 22 | ctx := context.Background() 23 | 24 | tests := []struct { 25 | name string 26 | args args 27 | want *OnceGenerator 28 | requirePanic bool 29 | }{ 30 | { 31 | name: "no Action", 32 | args: args{ 33 | ctx: ctx, 34 | action: nil, 35 | actionTime: time.Time{}, 36 | tags: []string{"test"}, 37 | }, 38 | requirePanic: true, 39 | }, 40 | { 41 | name: "success", 42 | args: args{ 43 | ctx: ctx, 44 | action: timestone.NewMockAction(t), 45 | actionTime: time.Time{}, 46 | tags: []string{"test"}, 47 | }, 48 | want: &OnceGenerator{ 49 | event: &Event{ 50 | Context: ctx, 51 | Action: timestone.NewMockAction(t), 52 | Time: time.Time{}, 53 | tags: []string{"test"}, 54 | }, 55 | ctx: ctx, 56 | }, 57 | }, 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | t.Parallel() 63 | 64 | if tt.requirePanic { 65 | require.Panics(t, func() { 66 | _ = NewOnceGenerator(tt.args.ctx, tt.args.action, tt.args.actionTime, tt.args.tags) 67 | }) 68 | return 69 | } 70 | 71 | require.Equal(t, tt.want, NewOnceGenerator(tt.args.ctx, tt.args.action, tt.args.actionTime, tt.args.tags)) 72 | }) 73 | } 74 | } 75 | 76 | func Test_OnceGenerator_Pop(t *testing.T) { 77 | t.Parallel() 78 | 79 | type fields struct { 80 | event *Event 81 | ctx context.Context 82 | } 83 | 84 | ctx := context.Background() 85 | 86 | tests := []struct { 87 | name string 88 | fields fields 89 | want *Event 90 | requirePanic bool 91 | }{ 92 | { 93 | name: "already finished", 94 | fields: fields{ 95 | event: nil, 96 | ctx: ctx, 97 | }, 98 | requirePanic: true, 99 | }, 100 | { 101 | name: "success", 102 | fields: fields{ 103 | event: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{"test"}), 104 | ctx: ctx, 105 | }, 106 | want: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{"test"}), 107 | }, 108 | } 109 | 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | t.Parallel() 113 | 114 | o := &OnceGenerator{ 115 | event: tt.fields.event, 116 | ctx: tt.fields.ctx, 117 | } 118 | 119 | if tt.requirePanic { 120 | require.Panics(t, func() { 121 | _ = o.Pop() 122 | }) 123 | return 124 | } 125 | 126 | require.Equal(t, tt.want, o.Pop()) 127 | 128 | if tt.want != nil { 129 | require.True(t, o.Finished()) 130 | } else { 131 | require.False(t, o.Finished()) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func Test_OnceGenerator_Peek(t *testing.T) { 138 | t.Parallel() 139 | 140 | type fields struct { 141 | event *Event 142 | ctx context.Context 143 | } 144 | 145 | ctx := context.Background() 146 | 147 | tests := []struct { 148 | name string 149 | fields fields 150 | want Event 151 | requirePanic bool 152 | }{ 153 | { 154 | name: "already finished", 155 | fields: fields{ 156 | event: nil, 157 | ctx: ctx, 158 | }, 159 | requirePanic: true, 160 | }, 161 | { 162 | name: "success", 163 | fields: fields{ 164 | event: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{"test"}), 165 | ctx: ctx, 166 | }, 167 | want: *NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{"test"}), 168 | }, 169 | } 170 | 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | t.Parallel() 174 | 175 | o := &OnceGenerator{ 176 | event: tt.fields.event, 177 | ctx: tt.fields.ctx, 178 | } 179 | 180 | if tt.requirePanic { 181 | require.Panics(t, func() { 182 | _ = o.Peek() 183 | }) 184 | return 185 | } 186 | 187 | require.Equal(t, tt.want, o.Peek()) 188 | require.False(t, o.Finished()) 189 | }) 190 | } 191 | } 192 | 193 | func Test_OnceGenerator_Finished(t *testing.T) { 194 | t.Parallel() 195 | 196 | type fields struct { 197 | event *Event 198 | ctx context.Context 199 | } 200 | 201 | ctx, cancel := context.WithCancel(context.Background()) 202 | cancel() 203 | 204 | tests := []struct { 205 | name string 206 | fields fields 207 | want bool 208 | }{ 209 | { 210 | name: "no event", 211 | fields: fields{ 212 | event: nil, 213 | ctx: context.Background(), 214 | }, 215 | want: true, 216 | }, 217 | { 218 | name: "context is done", 219 | fields: fields{ 220 | event: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{"test"}), 221 | ctx: ctx, 222 | }, 223 | want: true, 224 | }, 225 | { 226 | name: "not finished", 227 | fields: fields{ 228 | event: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{"test"}), 229 | ctx: context.Background(), 230 | }, 231 | want: false, 232 | }, 233 | } 234 | 235 | for _, tt := range tests { 236 | t.Run(tt.name, func(t *testing.T) { 237 | t.Parallel() 238 | 239 | o := &OnceGenerator{ 240 | event: tt.fields.event, 241 | ctx: tt.fields.ctx, 242 | } 243 | 244 | require.Equal(t, tt.want, o.Finished()) 245 | }) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /simulation/internal/data/bitmap_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func Test_newBitmap(t *testing.T) { 9 | t.Parallel() 10 | 11 | tests := []struct { 12 | name string 13 | index int 14 | want bitmap 15 | }{ 16 | { 17 | name: "index 0", 18 | index: 0, 19 | want: bitmap{1 << 0}, 20 | }, 21 | { 22 | name: "index 63", 23 | index: 63, 24 | want: bitmap{1 << 63}, 25 | }, 26 | { 27 | name: "index 64", 28 | index: 64, 29 | want: bitmap{0, 1 << 0}, 30 | }, 31 | { 32 | name: "index 127", 33 | index: 127, 34 | want: bitmap{0, 1 << 63}, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | t.Parallel() 41 | 42 | got := newBitmap(tt.index) 43 | require.Equal(t, tt.want, got) 44 | }) 45 | } 46 | } 47 | 48 | func Test_bitmap_or(t *testing.T) { 49 | t.Parallel() 50 | 51 | tests := []struct { 52 | name string 53 | b bitmap 54 | bb bitmap 55 | want bitmap 56 | }{ 57 | { 58 | name: "only empty", 59 | b: bitmap{}, 60 | bb: bitmap{}, 61 | want: bitmap{}, 62 | }, 63 | { 64 | name: "identity with empty bitmap", 65 | b: bitmap{1 << 63}, 66 | bb: bitmap{}, 67 | want: bitmap{1 << 63}, 68 | }, 69 | { 70 | name: "identity with zero bitmap", 71 | b: bitmap{1 << 63}, 72 | bb: bitmap{}, 73 | want: bitmap{1 << 63}, 74 | }, 75 | { 76 | name: "b OR bb, equal length", 77 | b: bitmap{0, 1 << 1}, 78 | bb: bitmap{1 << 1, 1 << 2}, 79 | want: bitmap{0 | 1<<1, 1<<1 | 1<<2}, 80 | }, 81 | { 82 | name: "b OR bb, b longer", 83 | b: bitmap{0, 1 << 1, 0}, 84 | bb: bitmap{1 << 1, 1 << 2}, 85 | want: bitmap{0 | 1<<1, 1<<1 | 1<<2, 0}, 86 | }, 87 | { 88 | name: "b OR bb, bb longer", 89 | b: bitmap{0, 1 << 1}, 90 | bb: bitmap{1 << 1, 1 << 2, 0}, 91 | want: bitmap{0 | 1<<1, 1<<1 | 1<<2, 0}, 92 | }, 93 | } 94 | 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | t.Parallel() 98 | 99 | tt.b.or(tt.bb) 100 | require.Equal(t, tt.want, tt.b) 101 | }) 102 | } 103 | } 104 | 105 | func Test_bitmap_contains(t *testing.T) { 106 | t.Parallel() 107 | 108 | tests := []struct { 109 | name string 110 | b bitmap 111 | target bitmap 112 | want bool 113 | }{ 114 | { 115 | name: "zero target", 116 | b: bitmap{1 << 0}, 117 | target: bitmap{0}, 118 | want: true, 119 | }, 120 | { 121 | name: "contains target", 122 | b: bitmap{1<<1 | 1<<0, 1 << 0}, 123 | target: bitmap{1 << 0}, 124 | want: true, 125 | }, 126 | { 127 | name: "equals target", 128 | b: bitmap{1<<1 | 1<<0}, 129 | target: bitmap{1<<1 | 1<<0}, 130 | want: true, 131 | }, 132 | { 133 | name: "does not contain target", 134 | b: bitmap{1<<1 | 1<<0}, 135 | target: bitmap{1 << 2}, 136 | want: false, 137 | }, 138 | { 139 | name: "target too large", 140 | b: bitmap{1<<1 | 1<<0}, 141 | target: bitmap{1 << 2, 1 << 0}, 142 | want: false, 143 | }, 144 | } 145 | 146 | for _, tt := range tests { 147 | t.Run(tt.name, func(t *testing.T) { 148 | t.Parallel() 149 | 150 | got := tt.b.contains(tt.target) 151 | require.Equal(t, tt.want, got) 152 | }) 153 | } 154 | } 155 | 156 | func Test_bitmap_containedIn(t *testing.T) { 157 | t.Parallel() 158 | 159 | tests := []struct { 160 | name string 161 | b bitmap 162 | target bitmap 163 | want bool 164 | }{ 165 | { 166 | name: "zero bitmap", 167 | b: bitmap{0}, 168 | target: bitmap{1 << 0}, 169 | want: true, 170 | }, 171 | { 172 | name: "contained in target", 173 | b: bitmap{1 << 0}, 174 | target: bitmap{1<<1 | 1<<0, 1 << 0}, 175 | want: true, 176 | }, 177 | { 178 | name: "equals target", 179 | b: bitmap{1<<1 | 1<<0}, 180 | target: bitmap{1<<1 | 1<<0}, 181 | want: true, 182 | }, 183 | { 184 | name: "is not contained in target", 185 | b: bitmap{1 << 2}, 186 | target: bitmap{1<<1 | 1<<0}, 187 | want: false, 188 | }, 189 | { 190 | name: "bitmap too large", 191 | b: bitmap{1 << 2, 1 << 0}, 192 | target: bitmap{1<<1 | 1<<0}, 193 | want: false, 194 | }, 195 | } 196 | 197 | for _, tt := range tests { 198 | t.Run(tt.name, func(t *testing.T) { 199 | t.Parallel() 200 | 201 | got := tt.b.containedIn(tt.target) 202 | require.Equal(t, tt.want, got) 203 | }) 204 | } 205 | } 206 | 207 | func Test_bitmap_equal(t *testing.T) { 208 | t.Parallel() 209 | 210 | tests := []struct { 211 | name string 212 | b bitmap 213 | target bitmap 214 | want bool 215 | }{ 216 | { 217 | name: "empty bitmaps", 218 | b: bitmap{}, 219 | target: bitmap{}, 220 | want: true, 221 | }, 222 | { 223 | name: "zero bitmaps", 224 | b: bitmap{0}, 225 | target: bitmap{0}, 226 | want: true, 227 | }, 228 | { 229 | name: "equal bitmaps", 230 | b: bitmap{1 << 1}, 231 | target: bitmap{1 << 1}, 232 | want: true, 233 | }, 234 | { 235 | name: "equal length, unequal bitmaps", 236 | b: bitmap{1 << 1}, 237 | target: bitmap{1 << 2}, 238 | want: false, 239 | }, 240 | { 241 | name: "unequal lengths", 242 | b: bitmap{1 << 1}, 243 | target: bitmap{1<<0 | 1<<2}, 244 | want: false, 245 | }, 246 | } 247 | 248 | for _, tt := range tests { 249 | t.Run(tt.name, func(t *testing.T) { 250 | t.Parallel() 251 | 252 | got := tt.b.equal(tt.target) 253 | require.Equal(t, tt.want, got) 254 | }) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /examples/processor_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "context" 5 | "github.com/metamogul/timestone/v2/simulation" 6 | c "github.com/metamogul/timestone/v2/simulation/config" 7 | "github.com/stretchr/testify/require" 8 | "math/rand/v2" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/metamogul/timestone/v2" 14 | ) 15 | 16 | const simulateProcessorLoadMilliseconds = 100 17 | 18 | type processingCache struct { 19 | content map[string]string 20 | contentMu sync.RWMutex 21 | } 22 | 23 | func newProcessingCache() *processingCache { 24 | return &processingCache{ 25 | content: make(map[string]string), 26 | } 27 | } 28 | 29 | func (p *processingCache) set(key string, value string) { 30 | p.contentMu.Lock() 31 | defer p.contentMu.Unlock() 32 | 33 | p.content[key] = value 34 | } 35 | 36 | func (p *processingCache) get(key string) (value string) { 37 | p.contentMu.RLock() 38 | defer p.contentMu.RUnlock() 39 | 40 | return p.content[key] 41 | } 42 | 43 | func (p *processingCache) getContent() (content map[string]string) { 44 | p.contentMu.RLock() 45 | defer p.contentMu.RUnlock() 46 | 47 | return p.content 48 | } 49 | 50 | type fooProcessor struct { 51 | cache *processingCache 52 | scheduler timestone.Scheduler 53 | } 54 | 55 | func newFooProcessor(cache *processingCache, scheduler timestone.Scheduler) *fooProcessor { 56 | return &fooProcessor{ 57 | cache: cache, 58 | scheduler: scheduler, 59 | } 60 | } 61 | 62 | func (a *fooProcessor) invoke(context.Context) { 63 | for key, value := range a.cache.getContent() { 64 | time.Sleep(time.Duration(rand.Int64N(simulateProcessorLoadMilliseconds)) * time.Millisecond) 65 | a.cache.set(key, value+"foo") 66 | } 67 | } 68 | 69 | type barProcessor struct { 70 | cache *processingCache 71 | scheduler timestone.Scheduler 72 | } 73 | 74 | func newBarProcessor(cache *processingCache, scheduler timestone.Scheduler) *barProcessor { 75 | return &barProcessor{ 76 | cache: cache, 77 | scheduler: scheduler, 78 | } 79 | } 80 | 81 | func (m *barProcessor) invoke(ctx context.Context) { 82 | for key, value := range m.cache.getContent() { 83 | time.Sleep(time.Duration(rand.Int64N(simulateProcessorLoadMilliseconds)) * time.Millisecond) 84 | m.cache.set(key, value+"bar") 85 | 86 | m.scheduler.PerformNow(ctx, 87 | timestone.SimpleAction( 88 | func(context.Context) { 89 | time.Sleep(time.Duration(rand.Int64N(simulateProcessorLoadMilliseconds)) * time.Millisecond) 90 | 91 | value := m.cache.get(key) 92 | m.cache.set(key, value+"baz") 93 | }, 94 | ), 95 | "barPostprocessingBaz", 96 | ) 97 | } 98 | } 99 | 100 | type app struct { 101 | ctx context.Context 102 | 103 | scheduler timestone.Scheduler 104 | cache *processingCache 105 | fooProcessor *fooProcessor 106 | barProcessor *barProcessor 107 | } 108 | 109 | func newApp(scheduler timestone.Scheduler) *app { 110 | cache := newProcessingCache() 111 | fooProcessor := newFooProcessor(cache, scheduler) 112 | barProcessor := newBarProcessor(cache, scheduler) 113 | 114 | return &app{ 115 | ctx: context.Background(), 116 | 117 | scheduler: scheduler, 118 | cache: cache, 119 | fooProcessor: fooProcessor, 120 | barProcessor: barProcessor, 121 | } 122 | } 123 | 124 | func (a *app) seedCache() { 125 | a.cache.set("bort", "") 126 | a.cache.set("burf", "") 127 | a.cache.set("bell", "") 128 | a.cache.set("bick", "") 129 | a.cache.set("bams", "") 130 | } 131 | 132 | func (a *app) run() { 133 | a.scheduler.PerformRepeatedly(a.ctx, timestone.SimpleAction(a.fooProcessor.invoke), nil, time.Hour, "fooProcessing") 134 | a.scheduler.PerformRepeatedly(a.ctx, timestone.SimpleAction(a.barProcessor.invoke), nil, time.Hour, "barProcessing") 135 | } 136 | 137 | func TestApp(t *testing.T) { 138 | t.Parallel() 139 | 140 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 141 | 142 | testcases := []struct { 143 | name string 144 | configureScheduler func(s *simulation.Scheduler) 145 | result string 146 | }{ 147 | { 148 | name: "foo before bar", 149 | configureScheduler: func(s *simulation.Scheduler) { 150 | s.ConfigureEvents(c.Config{ 151 | Tags: []string{"barProcessing"}, 152 | WaitFor: []c.Event{ 153 | c.All{Tags: []string{"fooProcessing"}}, 154 | }, 155 | Adds: []*c.Generator{ 156 | {Tags: []string{"barPostprocessingBaz"}, Count: 5}, 157 | }, 158 | }) 159 | }, 160 | result: "foobarbaz", 161 | }, 162 | { 163 | name: "foo after bar", 164 | configureScheduler: func(s *simulation.Scheduler) { 165 | s.ConfigureEvents(c.Config{ 166 | Tags: []string{"fooProcessing"}, 167 | Priority: 3, 168 | WaitFor: []c.Event{ 169 | c.All{Tags: []string{"barProcessing"}}, 170 | c.All{Tags: []string{"barPostprocessingBaz"}}, 171 | }, 172 | }) 173 | s.ConfigureEvents(c.Config{ 174 | Tags: []string{"barProcessing"}, 175 | Priority: 1, 176 | Adds: []*c.Generator{ 177 | {Tags: []string{"barPostprocessingBaz"}, Count: 5}, 178 | }, 179 | }) 180 | s.ConfigureEvents(c.Config{ 181 | Tags: []string{"barPostprocessingBaz"}, 182 | Priority: 2, 183 | }) 184 | }, 185 | result: "barbazfoo", 186 | }, 187 | } 188 | 189 | for _, tt := range testcases { 190 | t.Run(tt.name, func(t *testing.T) { 191 | t.Parallel() 192 | 193 | simulationScheduler := simulation.NewScheduler(now) 194 | 195 | tt.configureScheduler(simulationScheduler) 196 | 197 | a := newApp(simulationScheduler) 198 | a.seedCache() 199 | a.run() 200 | 201 | simulationScheduler.Forward(1 * time.Hour) 202 | 203 | for _, value := range a.cache.content { 204 | require.Equal(t, tt.result, value) 205 | } 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /simulation/scheduler.go: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import ( 4 | "context" 5 | "github.com/metamogul/timestone/v2/simulation/config" 6 | 7 | "github.com/metamogul/timestone/v2/simulation/internal/clock" 8 | "github.com/metamogul/timestone/v2/simulation/internal/events" 9 | "github.com/metamogul/timestone/v2/simulation/internal/waitgroups" 10 | "sync" 11 | "time" 12 | 13 | "github.com/metamogul/timestone/v2" 14 | ) 15 | 16 | type Scheduler struct { 17 | clock *clock.Clock 18 | 19 | eventQueue *events.Queue 20 | eventGeneratorsMu sync.RWMutex 21 | 22 | eventConfigs *events.Configs 23 | 24 | eventWaitGroups *waitgroups.EventWaitGroups 25 | } 26 | 27 | // NewScheduler will return a newMatching Scheduler instance, with its 28 | // clock initialized to return now. 29 | func NewScheduler(now time.Time) *Scheduler { 30 | eventConfigs := events.NewConfigs() 31 | 32 | return &Scheduler{ 33 | clock: clock.NewClock(now), 34 | eventQueue: events.NewQueue(eventConfigs), 35 | eventConfigs: eventConfigs, 36 | eventWaitGroups: waitgroups.NewEventWaitGroups(), 37 | } 38 | } 39 | 40 | func (s *Scheduler) Now() time.Time { 41 | return s.clock.Now() 42 | } 43 | 44 | // ConfigureEvents provides an config.Config to configure one ore multiple events. 45 | func (s *Scheduler) ConfigureEvents(configs ...config.Config) { 46 | for _, configuration := range configs { 47 | s.eventConfigs.Set(configuration) 48 | } 49 | } 50 | 51 | // ForwardOne executes just the next event that is scheduled on the 52 | // event queue of the Scheduler, and sets the timestone.Clock of the Scheduler 53 | // to the time of the event. 54 | func (s *Scheduler) ForwardOne() { 55 | s.eventGeneratorsMu.RLock() 56 | 57 | if s.eventQueue.Finished() { 58 | s.eventGeneratorsMu.RUnlock() 59 | return 60 | } 61 | 62 | nextEvent := s.eventQueue.Pop() 63 | s.eventGeneratorsMu.RUnlock() 64 | 65 | s.execEvent(nextEvent) 66 | } 67 | 68 | // WaitFor is to be used after ForwardOne and blocks until all scheduled 69 | // events embedding actions with the specified actionNames have finished. 70 | func (s *Scheduler) WaitFor(events ...config.Event) { 71 | s.eventWaitGroups.WaitFor(events) 72 | } 73 | 74 | // Wait is to be used after ForwardOne and blocks until all scheduled 75 | // events have finished. 76 | func (s *Scheduler) Wait() { 77 | s.eventWaitGroups.Wait() 78 | } 79 | 80 | // Forward will forward the Scheduler.Clock while running all events to 81 | // occur until Scheduler.Clock.Now() + interval. Each action will receive in 82 | // its context.Context a timestone.Clock set to return the respective execution 83 | // time for Now(). 84 | // 85 | // Depending on their individual configuration, Event s will either be run 86 | // sequentially, waiting for all preciously started Event s to finish, or 87 | // asynchronously. 88 | // 89 | // Event s will be materialized and executed from the schedulers event queue in 90 | // temporal order. In case of simultaneousness, the exection order can be changed 91 | // with the Config.Priority passed through ConfigureEvents. 92 | // 93 | // Event s configured via their Config.WaitFor will only start 94 | // execution once the specified events have finished. 95 | // 96 | // Event s configured via Config.Adds will block the run 97 | // loop until the specified Generator instances have been passed to the 98 | // Scheduler, either via one of the Perform... methods or via AddEventGenerators. 99 | func (s *Scheduler) Forward(interval time.Duration) { 100 | targetTime := s.clock.Now().Add(interval) 101 | 102 | for s.execNextEvent(targetTime) { 103 | } 104 | 105 | s.eventWaitGroups.Wait() 106 | } 107 | 108 | func (s *Scheduler) execNextEvent(targetTime time.Time) (shouldContinue bool) { 109 | s.eventGeneratorsMu.RLock() 110 | 111 | if s.eventQueue.Finished() { 112 | s.clock.Set(targetTime) 113 | s.eventGeneratorsMu.RUnlock() 114 | return false 115 | } 116 | 117 | if s.eventQueue.Peek().After(targetTime) { 118 | s.clock.Set(targetTime) 119 | s.eventGeneratorsMu.RUnlock() 120 | return false 121 | } 122 | 123 | nextEvent := s.eventQueue.Pop() 124 | s.eventGeneratorsMu.RUnlock() 125 | 126 | s.execEvent(nextEvent) 127 | 128 | return true 129 | } 130 | 131 | func (s *Scheduler) execEvent(eventToExec *events.Event) { 132 | s.clock.Set(eventToExec.Time) 133 | 134 | blockingEvents := s.eventConfigs.BlockingEvents(eventToExec) 135 | expectedGenerators := s.eventConfigs.ExpectedGenerators(eventToExec) 136 | 137 | s.eventQueue.ExpectGenerators(expectedGenerators) 138 | 139 | eventWaitGroup := s.eventWaitGroups.New(eventToExec.Time, eventToExec.Tags()) 140 | go func() { 141 | s.eventWaitGroups.WaitFor(blockingEvents) 142 | eventToExec.Perform(context.WithValue(eventToExec.Context, timestone.ActionContextClockKey, clock.NewClock(eventToExec.Time))) 143 | eventWaitGroup.Done() 144 | }() 145 | 146 | s.eventQueue.WaitForExpectedGenerators(expectedGenerators) 147 | } 148 | 149 | // PerformNow schedules action to be executed immediately, that is 150 | // at the current time of the Scheduler's clock. It adds a newMatching Event 151 | // generator which materializes a corresponding event to the Scheduler's 152 | // event queue. 153 | func (s *Scheduler) PerformNow(ctx context.Context, action timestone.Action, tags ...string) { 154 | s.AddEventGenerators(events.NewOnceGenerator(ctx, action, s.clock.Now(), tags)) 155 | } 156 | 157 | // PerformAfter schedules an action to be run once after a delay 158 | // of duration. It adds a newMatching Event generator which materializes a 159 | // corresponding event to the Scheduler's event queue. 160 | func (s *Scheduler) PerformAfter(ctx context.Context, action timestone.Action, interval time.Duration, tags ...string) { 161 | s.AddEventGenerators(events.NewOnceGenerator(ctx, action, s.clock.Now().Add(interval), tags)) 162 | } 163 | 164 | // PerformRepeatedly schedules an action to be run every interval 165 | // after an initial delay of interval. If until is provided, the last 166 | // event will be run before or at until. It adds a newMatching Event 167 | // generator which materializes corresponding events to the Scheduler's 168 | // event queue. 169 | func (s *Scheduler) PerformRepeatedly(ctx context.Context, action timestone.Action, until *time.Time, interval time.Duration, tags ...string) { 170 | s.AddEventGenerators(events.NewPeriodicGenerator(ctx, action, s.clock.Now(), until, interval, tags)) 171 | } 172 | 173 | // AddEventGenerators is used by the Perform... methods of the Scheduler. 174 | // It can be used to pass a custom event generator if Timestone is used 175 | // to run event-based simulations. 176 | func (s *Scheduler) AddEventGenerators(generators ...events.Generator) { 177 | s.eventGeneratorsMu.Lock() 178 | defer s.eventGeneratorsMu.Unlock() 179 | 180 | for _, generator := range generators { 181 | if generator.Finished() { 182 | continue 183 | } 184 | 185 | s.eventQueue.Add(generator) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /examples/repetitive_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | c "github.com/metamogul/timestone/v2/simulation/config" 7 | "math/rand/v2" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/metamogul/timestone/v2" 13 | "github.com/metamogul/timestone/v2/simulation" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const simulateWriteLoadMilliseconds = 30 18 | 19 | type writer struct { 20 | result string 21 | scheduler timestone.Scheduler 22 | 23 | countWriteOne int 24 | countWriteTwo int 25 | 26 | mu sync.Mutex 27 | } 28 | 29 | func (w *writer) writeOne(context.Context) { 30 | w.mu.Lock() 31 | defer w.mu.Unlock() 32 | time.Sleep(time.Duration(rand.Int64N(simulateWriteLoadMilliseconds)) * time.Millisecond) 33 | 34 | w.result += fmt.Sprintf("one%d ", w.countWriteOne) 35 | w.countWriteOne++ 36 | } 37 | 38 | func (w *writer) writeTwo(context.Context) { 39 | w.mu.Lock() 40 | defer w.mu.Unlock() 41 | 42 | w.result += fmt.Sprintf("two%d ", w.countWriteTwo) 43 | w.countWriteTwo++ 44 | } 45 | 46 | func (w *writer) run(ctx context.Context, writeInterval time.Duration) { 47 | w.scheduler.PerformRepeatedly( 48 | ctx, timestone.SimpleAction(w.writeOne), nil, writeInterval, "writeOne", 49 | ) 50 | w.scheduler.PerformRepeatedly( 51 | ctx, timestone.SimpleAction(w.writeTwo), nil, writeInterval, "writeTwo", 52 | ) 53 | } 54 | 55 | func TestNoRaceWriting_AutomaticOrder(t *testing.T) { 56 | t.Parallel() 57 | 58 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 59 | writeInterval := time.Minute 60 | 61 | scheduler := simulation.NewScheduler(now) 62 | scheduler.ConfigureEvents( 63 | c.Config{ 64 | Tags: []string{"writeOne"}, 65 | Priority: 1, 66 | WaitFor: []c.Event{c.Before{ 67 | Interval: -writeInterval, 68 | Tags: []string{"writeTwo"}, 69 | }}, 70 | }, 71 | ) 72 | scheduler.ConfigureEvents( 73 | c.Config{ 74 | Tags: []string{"writeTwo"}, 75 | Priority: 2, 76 | WaitFor: []c.Event{c.Before{ 77 | Interval: 0, 78 | Tags: []string{"writeOne"}, 79 | }}, 80 | }, 81 | ) 82 | 83 | w := writer{scheduler: scheduler} 84 | w.run(context.Background(), writeInterval) 85 | 86 | scheduler.Forward(6 * writeInterval) 87 | 88 | require.Equal(t, "one0 two0 one1 two1 one2 two2 one3 two3 one4 two4 one5 two5 ", w.result) 89 | } 90 | 91 | func TestNoRaceWriting_ManualOrder(t *testing.T) { 92 | t.Parallel() 93 | 94 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 95 | writeInterval := time.Minute 96 | 97 | scheduler := simulation.NewScheduler(now) 98 | 99 | scheduler.ConfigureEvents(c.Config{ 100 | Tags: []string{"writeOne"}, 101 | Time: now.Add(writeInterval), 102 | Priority: 1, 103 | }) 104 | scheduler.ConfigureEvents(c.Config{ 105 | Tags: []string{"writeTwo"}, 106 | Time: now.Add(writeInterval), 107 | Priority: 2, 108 | WaitFor: []c.Event{c.At{ 109 | Tags: []string{"writeOne"}, 110 | Time: now.Add(writeInterval), 111 | }}, 112 | }) 113 | 114 | scheduler.ConfigureEvents(c.Config{ 115 | Tags: []string{"writeTwo"}, 116 | Time: now.Add(writeInterval * 2), 117 | Priority: 1, 118 | WaitFor: []c.Event{c.At{ 119 | Tags: []string{"writeTwo"}, 120 | Time: now.Add(writeInterval), 121 | }}, 122 | }) 123 | scheduler.ConfigureEvents(c.Config{ 124 | Tags: []string{"writeOne"}, 125 | Time: now.Add(writeInterval * 2), 126 | Priority: 2, 127 | WaitFor: []c.Event{c.At{ 128 | Tags: []string{"writeTwo"}, 129 | Time: now.Add(writeInterval * 2), 130 | }}, 131 | }) 132 | 133 | scheduler.ConfigureEvents(c.Config{ 134 | Tags: []string{"writeOne"}, 135 | Time: now.Add(writeInterval * 3), 136 | Priority: 1, 137 | WaitFor: []c.Event{c.At{ 138 | Tags: []string{"writeOne"}, 139 | Time: now.Add(writeInterval * 2), 140 | }}, 141 | }) 142 | scheduler.ConfigureEvents(c.Config{ 143 | Tags: []string{"writeTwo"}, 144 | Time: now.Add(writeInterval * 3), 145 | Priority: 2, 146 | WaitFor: []c.Event{c.At{ 147 | Tags: []string{"writeOne"}, 148 | Time: now.Add(writeInterval * 3), 149 | }}, 150 | }) 151 | 152 | scheduler.ConfigureEvents(c.Config{ 153 | Tags: []string{"writeOne"}, 154 | Time: now.Add(writeInterval * 4), 155 | Priority: 1, 156 | WaitFor: []c.Event{c.At{ 157 | Tags: []string{"writeTwo"}, 158 | Time: now.Add(writeInterval * 3), 159 | }}, 160 | }) 161 | scheduler.ConfigureEvents(c.Config{ 162 | Tags: []string{"writeTwo"}, 163 | Time: now.Add(writeInterval * 4), 164 | Priority: 2, 165 | WaitFor: []c.Event{c.At{ 166 | Tags: []string{"writeOne"}, 167 | Time: now.Add(writeInterval * 4), 168 | }}, 169 | }) 170 | 171 | scheduler.ConfigureEvents(c.Config{ 172 | Tags: []string{"writeOne"}, 173 | Time: now.Add(writeInterval * 5), 174 | Priority: 1, 175 | WaitFor: []c.Event{c.At{ 176 | Tags: []string{"writeTwo"}, 177 | Time: now.Add(writeInterval * 4), 178 | }}, 179 | }) 180 | scheduler.ConfigureEvents(c.Config{ 181 | Tags: []string{"writeTwo"}, 182 | Time: now.Add(writeInterval * 5), 183 | Priority: 2, 184 | WaitFor: []c.Event{c.At{ 185 | Tags: []string{"writeOne"}, 186 | Time: now.Add(writeInterval * 5), 187 | }}, 188 | }) 189 | 190 | scheduler.ConfigureEvents(c.Config{ 191 | Tags: []string{"writeOne"}, 192 | Time: now.Add(writeInterval * 6), 193 | Priority: 1, 194 | WaitFor: []c.Event{c.At{ 195 | Tags: []string{"writeTwo"}, 196 | Time: now.Add(writeInterval * 5), 197 | }}, 198 | }) 199 | scheduler.ConfigureEvents(c.Config{ 200 | Tags: []string{"writeTwo"}, 201 | Time: now.Add(writeInterval * 6), 202 | Priority: 2, 203 | WaitFor: []c.Event{c.At{ 204 | Tags: []string{"writeOne"}, 205 | Time: now.Add(writeInterval * 6), 206 | }}, 207 | }) 208 | 209 | w := writer{scheduler: scheduler} 210 | w.run(context.Background(), writeInterval) 211 | 212 | scheduler.Forward(6 * writeInterval) 213 | 214 | require.Equal(t, "one0 two0 two1 one1 one2 two2 one3 two3 one4 two4 one5 two5 ", w.result) 215 | } 216 | 217 | type timeWriter struct { 218 | scheduler timestone.Scheduler 219 | mu sync.Mutex 220 | } 221 | 222 | func (w *timeWriter) writeTime(ctx context.Context) { 223 | w.mu.Lock() 224 | defer w.mu.Unlock() 225 | 226 | now := (ctx.Value(timestone.ActionContextClockKey)).(timestone.Clock).Now() 227 | time.Sleep(time.Duration(rand.Int64N(simulateWriteLoadMilliseconds)) * time.Millisecond) 228 | 229 | fmt.Printf("%v\n", now) 230 | } 231 | 232 | func (w *timeWriter) run(ctx context.Context, writeInterval time.Duration) { 233 | w.scheduler.PerformRepeatedly( 234 | ctx, timestone.SimpleAction(w.writeTime), nil, writeInterval, "writeTime", 235 | ) 236 | } 237 | 238 | func ExampleNoRaceSelfWait() { 239 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 240 | writeInterval := time.Minute 241 | 242 | scheduler := simulation.NewScheduler(now) 243 | scheduler.ConfigureEvents( 244 | c.Config{ 245 | Tags: []string{"writeTime"}, 246 | WaitFor: []c.Event{c.Before{ 247 | Interval: -writeInterval, 248 | Tags: []string{"writeTime"}, 249 | }}, 250 | }, 251 | ) 252 | 253 | w := timeWriter{scheduler: scheduler} 254 | w.run(context.Background(), writeInterval) 255 | 256 | scheduler.Forward(6 * writeInterval) 257 | 258 | // Output: 259 | // 2024-01-01 12:01:00 +0000 UTC 260 | // 2024-01-01 12:02:00 +0000 UTC 261 | // 2024-01-01 12:03:00 +0000 UTC 262 | // 2024-01-01 12:04:00 +0000 UTC 263 | // 2024-01-01 12:05:00 +0000 UTC 264 | // 2024-01-01 12:06:00 +0000 UTC 265 | } 266 | -------------------------------------------------------------------------------- /simulation/internal/data/taggedstore_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func Benchmark_TaggedStoreBitmask_GetContaining(b *testing.B) { 9 | ts := NewTaggedStore[string]() 10 | 11 | // Add values with tags 12 | ts.Set("apple", []string{"fruit", "red", "round", "borra", "bazza", "bumma", "climb", "result", "president"}) 13 | ts.Set("banana", []string{"fruit", "yellow", "long", "borrb", "bazzb", "bummb"}) 14 | ts.Set("carrot", []string{"vegetable", "orange", "long", "borrc", "bazzc", "bummc"}) 15 | 16 | for n := 0; n < b.N; n++ { 17 | _ = ts.Containing([]string{"fruit", "red", "borra", "bazza", "bumma", "climb", "result", "president"}) 18 | _ = ts.Containing([]string{"fruit", "long"}) 19 | _ = ts.Containing([]string{"vegetable", "orange", "long"}) 20 | _ = ts.Containing([]string{"red", "fruit"}) 21 | } 22 | } 23 | 24 | func Test_NewTaggedStore(t *testing.T) { 25 | t.Parallel() 26 | 27 | ts := NewTaggedStore[string]() 28 | 29 | require.NotNil(t, ts) 30 | require.NotNil(t, ts.content) 31 | require.Empty(t, ts.content) 32 | require.NotNil(t, ts.bitmapsByTags) 33 | require.Empty(t, ts.bitmapsByTags) 34 | } 35 | 36 | func Test_TaggedStore_Set(t *testing.T) { 37 | t.Parallel() 38 | 39 | tests := []struct { 40 | name string 41 | value string 42 | tags []string 43 | wantPanic bool 44 | }{ 45 | { 46 | name: "no tags passed", 47 | value: "value", 48 | wantPanic: true, 49 | }, 50 | { 51 | name: "success", 52 | value: "value", 53 | tags: []string{"foo", "bar", "baz"}, 54 | wantPanic: false, 55 | }, 56 | } 57 | 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | t.Parallel() 61 | 62 | ts := NewTaggedStore[string]() 63 | 64 | if tt.wantPanic { 65 | require.Panics(t, func() { 66 | ts.Set(tt.value, tt.tags) 67 | }) 68 | return 69 | } 70 | 71 | ts.Set(tt.value, tt.tags) 72 | for _, tag := range tt.tags { 73 | result := ts.Containing([]string{tag}) 74 | require.NotNil(t, result) 75 | require.Equal(t, result[0], tt.value) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func Test_TaggedStore_Containing(t *testing.T) { 82 | t.Parallel() 83 | 84 | value1 := "value" 85 | tags1 := []string{"foo", "bar", "baz"} 86 | 87 | value2 := "apple" 88 | tags2 := []string{"fruit", "round", "red", "foo"} 89 | 90 | tests := []struct { 91 | name string 92 | getForTags []string 93 | want []string 94 | }{ 95 | { 96 | name: "no matches", 97 | getForTags: []string{"bum"}, 98 | want: []string{}, 99 | }, 100 | { 101 | name: "match value1", 102 | getForTags: []string{"foo", "bar"}, 103 | want: []string{value1}, 104 | }, 105 | { 106 | name: "match value1 and value2", 107 | getForTags: []string{"foo"}, 108 | want: []string{value1, value2}, 109 | }, 110 | { 111 | name: "match value2", 112 | getForTags: []string{"fruit"}, 113 | want: []string{value2}, 114 | }, 115 | } 116 | 117 | for _, tt := range tests { 118 | t.Run(tt.name, func(t *testing.T) { 119 | t.Parallel() 120 | 121 | ts := NewTaggedStore[string]() 122 | 123 | ts.Set(value1, tags1) 124 | ts.Set(value2, tags2) 125 | 126 | value := ts.Containing(tt.getForTags) 127 | require.Equal(t, tt.want, value) 128 | }) 129 | } 130 | } 131 | 132 | func Test_TaggedStore_ContainedIn(t *testing.T) { 133 | t.Parallel() 134 | 135 | value1 := "value" 136 | tags1 := []string{"foo", "bar"} 137 | 138 | value2 := "apple" 139 | tags2 := []string{"fruit", "round"} 140 | 141 | tests := []struct { 142 | name string 143 | getForTags []string 144 | want []string 145 | }{ 146 | { 147 | name: "no matches", 148 | getForTags: []string{"foo", "bum", "baz"}, 149 | want: []string{}, 150 | }, 151 | { 152 | name: "match value1", 153 | getForTags: []string{"foo", "bar", "baz"}, 154 | want: []string{value1}, 155 | }, 156 | { 157 | name: "match value1 and value2", 158 | getForTags: []string{"foo", "bar", "baz", "fruit", "round", "red"}, 159 | want: []string{value1, value2}, 160 | }, 161 | { 162 | name: "match value2", 163 | getForTags: []string{"fruit", "round", "red"}, 164 | want: []string{value2}, 165 | }, 166 | } 167 | 168 | for _, tt := range tests { 169 | t.Run(tt.name, func(t *testing.T) { 170 | t.Parallel() 171 | 172 | ts := NewTaggedStore[string]() 173 | 174 | ts.Set(value1, tags1) 175 | ts.Set(value2, tags2) 176 | 177 | value := ts.ContainedIn(tt.getForTags) 178 | require.Equal(t, tt.want, value) 179 | }) 180 | } 181 | } 182 | 183 | func Test_TaggedStore_Matching(t *testing.T) { 184 | t.Parallel() 185 | 186 | value1 := "value" 187 | tags1 := []string{"foo", "bar", "baz"} 188 | 189 | value2 := "apple" 190 | tags2 := []string{"fruit", "round", "red", "foo"} 191 | 192 | tests := []struct { 193 | name string 194 | getForTags []string 195 | want string 196 | }{ 197 | { 198 | name: "no matches", 199 | getForTags: []string{"bum"}, 200 | want: "", 201 | }, 202 | { 203 | name: "match value1", 204 | getForTags: []string{"foo", "bar", "baz"}, 205 | want: value1, 206 | }, 207 | { 208 | name: "don't match subsets", 209 | getForTags: []string{"foo", "bar"}, 210 | want: "", 211 | }, 212 | { 213 | name: "match value2", 214 | getForTags: []string{"fruit", "round", "red", "foo"}, 215 | want: value2, 216 | }, 217 | } 218 | 219 | for _, tt := range tests { 220 | t.Run(tt.name, func(t *testing.T) { 221 | t.Parallel() 222 | 223 | ts := NewTaggedStore[string]() 224 | 225 | ts.Set(value1, tags1) 226 | ts.Set(value2, tags2) 227 | 228 | value := ts.Matching(tt.getForTags) 229 | require.Equal(t, tt.want, value) 230 | }) 231 | } 232 | } 233 | 234 | func Test_TaggedStore_All(t *testing.T) { 235 | t.Parallel() 236 | 237 | value1 := "value" 238 | tags1 := []string{"foo", "bar", "baz"} 239 | 240 | value2 := "apple" 241 | tags2 := []string{"fruit", "round", "red", "foo"} 242 | 243 | ts := NewTaggedStore[string]() 244 | 245 | ts.Set(value1, tags1) 246 | ts.Set(value2, tags2) 247 | 248 | values := ts.All() 249 | require.Equal(t, []string{value1, value2}, values) 250 | } 251 | 252 | func Test_TaggedStore_bitmapForTag(t *testing.T) { 253 | t.Parallel() 254 | 255 | tests := []struct { 256 | name string 257 | tagsAlreadyAdded []string 258 | tag string 259 | want bitmap 260 | }{ 261 | { 262 | name: "first tag", 263 | tagsAlreadyAdded: []string{}, 264 | tag: "foo", 265 | want: bitmap{1 << 0}, 266 | }, 267 | { 268 | name: "second tag", 269 | tagsAlreadyAdded: []string{"foo"}, 270 | tag: "bar", 271 | want: bitmap{1 << 1}, 272 | }, 273 | { 274 | name: "tag was added before", 275 | tagsAlreadyAdded: []string{"foo", "bar"}, 276 | tag: "bar", 277 | want: bitmap{1 << 1}, 278 | }, 279 | } 280 | 281 | for _, tt := range tests { 282 | t.Run(tt.name, func(t *testing.T) { 283 | t.Parallel() 284 | 285 | ts := NewTaggedStore[string]() 286 | for _, tag := range tt.tagsAlreadyAdded { 287 | _ = ts.bitmapForTag(tag) 288 | } 289 | 290 | got := ts.bitmapForTag(tt.tag) 291 | require.Equal(t, tt.want, got) 292 | }) 293 | } 294 | } 295 | 296 | func Test_TaggedStore_bitmapForTags(t *testing.T) { 297 | t.Parallel() 298 | 299 | tagsAlreadyAdded := []string{"foo", "bar", "baz"} 300 | 301 | tests := []struct { 302 | name string 303 | 304 | tags []string 305 | want bitmap 306 | }{ 307 | { 308 | name: "one tag", 309 | tags: []string{"foo"}, 310 | want: bitmap{1 << 0}, 311 | }, 312 | { 313 | name: "multiple tags", 314 | tags: []string{"foo", "baz"}, 315 | want: bitmap{1<<0 | 1<<2}, 316 | }, 317 | } 318 | 319 | for _, tt := range tests { 320 | t.Run(tt.name, func(t *testing.T) { 321 | t.Parallel() 322 | 323 | ts := NewTaggedStore[string]() 324 | for _, tag := range tagsAlreadyAdded { 325 | _ = ts.bitmapForTag(tag) 326 | } 327 | 328 | got := ts.bitmapForTags(tt.tags) 329 | require.Equal(t, tt.want, got) 330 | }) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /simulation/internal/events/configs_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "github.com/metamogul/timestone/v2/simulation/config" 6 | configinternal "github.com/metamogul/timestone/v2/simulation/internal/config" 7 | "github.com/metamogul/timestone/v2/simulation/internal/data" 8 | "testing" 9 | "time" 10 | 11 | "github.com/metamogul/timestone/v2" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func Test_NewConfigs(t *testing.T) { 16 | t.Parallel() 17 | 18 | newEventConfigurations := NewConfigs() 19 | 20 | require.NotNil(t, newEventConfigurations) 21 | require.NotNil(t, newEventConfigurations.configsByTags) 22 | require.Empty(t, newEventConfigurations.configsByTags.All()) 23 | require.NotNil(t, newEventConfigurations.configsByTagsAndTime) 24 | require.Empty(t, newEventConfigurations.configsByTagsAndTime) 25 | } 26 | 27 | func Test_Configs_Add(t *testing.T) { 28 | t.Parallel() 29 | 30 | e := NewConfigs() 31 | 32 | require.Panics(t, func() { 33 | e.Set(config.Config{}) 34 | }) 35 | 36 | e.Set(config.Config{Tags: []string{"test1", "test2"}}) 37 | require.Len(t, e.configsByTags.All(), 1) 38 | require.Len(t, e.configsByTagsAndTime, 0) 39 | 40 | e.Set(config.Config{Tags: []string{"test1", "test2"}}) 41 | require.Len(t, e.configsByTags.All(), 1) 42 | require.Len(t, e.configsByTagsAndTime, 0) 43 | 44 | now := time.Now() 45 | 46 | e.Set(config.Config{Time: now, Tags: []string{"test1", "test2"}}) 47 | require.Len(t, e.configsByTags.All(), 1) 48 | require.Len(t, e.configsByTagsAndTime, 1) 49 | require.Len(t, e.configsByTagsAndTime[now.UnixMilli()].All(), 1) 50 | 51 | e.Set(config.Config{Time: now, Tags: []string{"test1", "test2"}}) 52 | require.Len(t, e.configsByTags.All(), 1) 53 | require.Len(t, e.configsByTagsAndTime, 1) 54 | require.Len(t, e.configsByTagsAndTime[now.UnixMilli()].All(), 1) 55 | } 56 | 57 | func Test_Configs_Priority(t *testing.T) { 58 | t.Parallel() 59 | 60 | testcases := []struct { 61 | name string 62 | insertConfigs []config.Config 63 | wantPriority int 64 | }{ 65 | { 66 | name: "valid config", 67 | insertConfigs: []config.Config{{Tags: []string{"test1", "test2"}, Priority: 1}}, 68 | wantPriority: 1, 69 | }, 70 | { 71 | name: "no config for event", 72 | insertConfigs: []config.Config{}, 73 | wantPriority: EventPriorityDefault, 74 | }, 75 | } 76 | 77 | for _, tt := range testcases { 78 | t.Run(tt.name, func(t *testing.T) { 79 | t.Parallel() 80 | 81 | e := NewConfigs() 82 | for _, configToInsert := range tt.insertConfigs { 83 | e.Set(configToInsert) 84 | } 85 | 86 | mockEvent := NewEvent( 87 | context.Background(), 88 | timestone.NewMockAction(t), 89 | time.Time{}, 90 | []string{"test1", "test2"}, 91 | ) 92 | 93 | priority := e.Priority(mockEvent) 94 | require.Equal(t, tt.wantPriority, priority) 95 | }) 96 | } 97 | } 98 | 99 | func Test_Configs_BlockingEvents(t *testing.T) { 100 | t.Parallel() 101 | 102 | testcases := []struct { 103 | name string 104 | insertConfigs []config.Config 105 | wantBlockingEvents []config.Event 106 | }{ 107 | { 108 | name: "transformed event keys", 109 | insertConfigs: []config.Config{ 110 | { 111 | Tags: []string{"test1", "test2"}, 112 | WaitFor: []config.Event{config.Before{Interval: -1, Tags: []string{"test1", "test2"}}}, 113 | }, 114 | }, 115 | wantBlockingEvents: []config.Event{configinternal.At{Time: time.Time{}.Add(-1), Tags: []string{"test1", "test2"}}}, 116 | }, 117 | { 118 | name: "valid config", 119 | insertConfigs: []config.Config{ 120 | { 121 | Tags: []string{"test1", "test2"}, 122 | WaitFor: []config.Event{config.All{Tags: []string{"test1", "test2"}}}, 123 | }, 124 | }, 125 | wantBlockingEvents: []config.Event{config.All{Tags: []string{"test1", "test2"}}}, 126 | }, 127 | { 128 | name: "no config for event", 129 | insertConfigs: []config.Config{}, 130 | wantBlockingEvents: nil, 131 | }, 132 | } 133 | 134 | for _, tt := range testcases { 135 | t.Run(tt.name, func(t *testing.T) { 136 | t.Parallel() 137 | 138 | e := NewConfigs() 139 | for _, configToInsert := range tt.insertConfigs { 140 | e.Set(configToInsert) 141 | } 142 | 143 | mockEvent := NewEvent( 144 | context.Background(), 145 | timestone.NewMockAction(t), 146 | time.Time{}, 147 | []string{"test1", "test2"}, 148 | ) 149 | 150 | blockingEvents := e.BlockingEvents(mockEvent) 151 | require.Equal(t, tt.wantBlockingEvents, blockingEvents) 152 | }) 153 | } 154 | } 155 | 156 | func Test_Configs_ExpectedGenerators(t *testing.T) { 157 | t.Parallel() 158 | testcases := []struct { 159 | name string 160 | insertConfigs []config.Config 161 | wantExpectedGenerators []*config.Generator 162 | }{ 163 | { 164 | name: "valid config", 165 | insertConfigs: []config.Config{ 166 | { 167 | Tags: []string{"test1", "test2"}, 168 | Adds: []*config.Generator{{[]string{"testWanted"}, 1}}, 169 | }, 170 | }, 171 | wantExpectedGenerators: []*config.Generator{{[]string{"testWanted"}, 1}}, 172 | }, 173 | { 174 | name: "no config for event", 175 | insertConfigs: []config.Config{}, 176 | wantExpectedGenerators: nil, 177 | }, 178 | } 179 | 180 | for _, tt := range testcases { 181 | t.Run(tt.name, func(t *testing.T) { 182 | t.Parallel() 183 | 184 | e := NewConfigs() 185 | for _, configToInsert := range tt.insertConfigs { 186 | e.Set(configToInsert) 187 | } 188 | 189 | mockEvent := NewEvent( 190 | context.Background(), 191 | timestone.NewMockAction(t), 192 | time.Time{}, 193 | []string{"test1", "test2"}, 194 | ) 195 | 196 | wantedNewGenerators := e.ExpectedGenerators(mockEvent) 197 | require.Equal(t, tt.wantExpectedGenerators, wantedNewGenerators) 198 | }) 199 | } 200 | } 201 | 202 | func Test_Configs_configsByTagsForTime(t *testing.T) { 203 | t.Parallel() 204 | 205 | testcases := []struct { 206 | name string 207 | configsByTagsAndTime map[int64]*data.TaggedStore[*config.Config] 208 | }{ 209 | { 210 | name: "entry exists", 211 | configsByTagsAndTime: map[int64]*data.TaggedStore[*config.Config]{ 212 | 0: data.NewTaggedStore[*config.Config](), 213 | }, 214 | }, 215 | { 216 | name: "entry does not exist", 217 | configsByTagsAndTime: make(map[int64]*data.TaggedStore[*config.Config]), 218 | }, 219 | } 220 | 221 | for _, tt := range testcases { 222 | t.Run(tt.name, func(t *testing.T) { 223 | t.Parallel() 224 | 225 | e := NewConfigs() 226 | e.configsByTagsAndTime = tt.configsByTagsAndTime 227 | 228 | result := e.configsByTagsForTime(time.Time{}) 229 | require.Equal(t, data.NewTaggedStore[*config.Config](), result) 230 | }) 231 | } 232 | } 233 | 234 | func Test_Configs_get(t *testing.T) { 235 | t.Parallel() 236 | 237 | testcases := []struct { 238 | name string 239 | insertConfigs []config.Config 240 | wantConfiguration *config.Config 241 | }{ 242 | { 243 | name: "both configs exist", 244 | insertConfigs: []config.Config{ 245 | {Tags: []string{"test1", "test2"}, Time: time.Time{}.Add(1), Priority: 20}, 246 | {Tags: []string{"test1", "test2"}, Priority: 10}, 247 | }, 248 | wantConfiguration: &config.Config{Tags: []string{"test1", "test2"}, Time: time.Time{}.Add(1), Priority: 20}, 249 | }, 250 | { 251 | name: "config for name exists", 252 | insertConfigs: []config.Config{ 253 | {Tags: []string{"test1", "test2"}, Priority: 10}, 254 | }, 255 | wantConfiguration: &config.Config{Tags: []string{"test1", "test2"}, Priority: 10}, 256 | }, 257 | { 258 | name: "config for name + time exists", 259 | insertConfigs: []config.Config{ 260 | {Tags: []string{"test1", "test2"}, Time: time.Time{}.Add(1), Priority: 20}, 261 | }, 262 | wantConfiguration: &config.Config{Tags: []string{"test1", "test2"}, Time: time.Time{}.Add(1), Priority: 20}, 263 | }, 264 | { 265 | name: "no config exists", 266 | insertConfigs: []config.Config{}, 267 | wantConfiguration: nil, 268 | }, 269 | } 270 | 271 | for _, tt := range testcases { 272 | t.Run(tt.name, func(t *testing.T) { 273 | t.Parallel() 274 | 275 | e := NewConfigs() 276 | for _, configToInsert := range tt.insertConfigs { 277 | e.Set(configToInsert) 278 | } 279 | 280 | mockEvent := NewEvent( 281 | context.Background(), 282 | timestone.NewMockAction(t), 283 | time.Time{}, 284 | []string{"test1", "test2"}, 285 | ) 286 | 287 | gotConfig := e.get(mockEvent) 288 | require.Equal(t, tt.wantConfiguration, gotConfig) 289 | }) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | **⚠️ Since the arrival of [`synctest`](https://go.dev/blog/synctest) in Go 1.24, there's a much better tool for testing 4 | concurrent Go code. Therefore this project is deprecated and will not be maintained any longer.** 5 | 6 | To be specific what "much better" means: 7 | 8 | - Synctest works with native Go routines, no changes at all to the tested implementation needed 9 | - Less complex implementation directly in the Go runtime 10 | - Much simpler API and usage 11 | - Support for context and cancellation that Timestone was missing 12 | 13 | The only real drawback of `synctest` compared to Timestone is the lacking ability to order tests. However since the benefits greatly 14 | outway the drawbacks, I have moved myself to using synctest and therefore have decided to deprecate this project. In case 15 | anybody has started to incorporate this into production code, I'll leave it in public archive. 16 | 17 | --- 18 | 19 | # Timestone 🗿 20 | 21 | Timestone is a library to create deterministic and easy-to-understand unit tests for time-dependent, concurrent go 22 | code. Existing libraries such as [Quartz](https://github.com/coder/quartz) or [Clock](https://github.com/benbjohnson/clock) 23 | show the need for such a tool, yet have various shortcomings, for example not being able to reliably prevent race-conditions 24 | in tests, or being difficult to read and understand when used. 25 | 26 | ### Goals 27 | 28 | This library is built around the following primary design goals: 29 | 30 | - 🤌 Eliminate flaky unit tests 31 | - 🧹Keep unit tests free of boilerplate syncing code and focussed on assumptions and expectations 32 | - 🐭 As little invasive as possible to the tested implementation 33 | 34 | Secondary goals are a good separation of concerns and extensive test coverage for the library itself. 35 | 36 | ### Design principles 37 | 38 | To offer a high-level API that keeps manual syncing code out of unit tests, Timestone takes an opinionated, 39 | use-case-oriented approach rather than attempting to substitute low-level runtime primitives like timers and tickers. 40 | However this approach can be limiting, and there may be a need to extend the library’s public interface to support 41 | additional use cases. For instance, Timestone’s public model already includes passing the commonly used 42 | `context.Context` but the `simulation` implementation doesn't always respect it. Another example is a cron syntax for 43 | scheduling recurring tasks which could be easily integrated but is not included yet. 44 | 45 | ### Limitations 46 | 47 | At its current stage, Timestone is fully functional and supports all possible use cases under the assumption that the 48 | computing time for scheduled go routines is not a concern. As a consequence, when testing the time inside actions is 49 | always fixed to an instant and won't pass or change for the duration of the action's execution. 50 | 51 | Another major limitation is the lack to fully support contexts: Contexts with a deadline from the `context` standard 52 | libraray package are currently not supported for technical reasons. 53 | 54 | Both issues are linked and will be considered in upcoming releases. 55 | 56 | ## Concepts 57 | 58 | To achieve its goals, Timestone aims to encapsulate concurrency. Instead of directly invoking goroutines, the library 59 | provides a `Scheduler` interface with methods for scheduling `Action`s, such as one-time or recurring tasks. There are 60 | two implementations of the `Scheduler`: `system.Scheduler` and `simulation.Scheduler`. While the former uses standard 61 | library runtime primitives to dispatch actions, the latter employs a run loop to control where actions are scheduled. 62 | Through various configuration options, the scheduling mode and order of actions can be controlled, and action 63 | dependencies can be setup. 64 | 65 | To see how this works in practice, take a look at the `examples` package, which contains functional test cases that 66 | serve as integration tests for the Timestone library, as well as demonstrative use cases. 67 | 68 | The following sections provide a more detailed explanation: 69 | 70 | ### Scheduler 71 | 72 | One of the main challenges in eliminating race conditions from unit tests is handling goroutines. Non-deterministic by 73 | nature, goroutines provide no guarantee on the order in which concurrent code will be executed by the Go runtime. To 74 | address this problem in unit tests, Timestone offers a `Scheduler` interface designed to run code concurrently while 75 | encapsulating the underlying complexity: 76 | 77 | ```go 78 | type Scheduler interface { 79 | Clock 80 | PerformNow(ctx context.Context, action Action, tags ...string) 81 | PerformAfter(ctx context.Context, action Action, duration time.Duration, tags ...string) 82 | PerformRepeatedly(ctx context.Context, action Action, until *time.Time, interval time.Duration, tags ...string) 83 | } 84 | ``` 85 | 86 | Where you would normally call `go func() {...}()`, when working with Timestone you instead use the `PerformNow` method 87 | of the `Scheduler`. The `PerformAfter` and `PerformRepeatedly` methods offer convenient alternatives to using 88 | `time.Timer` and `time.Ticker` within goroutines for scheduling function execution. 89 | 90 | While the `system.Scheduler` implementation of the `Scheduler` interface uses the mentioned runtime scheduling 91 | primitives, the `simulation.Scheduler` implementation is where the real magic happens. 92 | 93 | Rather than immediately running an action within a goroutine, the `simulation.Scheduler` creates an event generator for 94 | it. The events it materializes will then be executed from either the `ForwardOne` or `Forward` methods, advancing the 95 | `simulation.Scheduler`’s clock either to the next event or through all events scheduled to occur within a specified 96 | `time.Duration`. Additional configuration can be provided for individual actions or entire groups, allowing control over 97 | the execution order of simultaneous events or injecting dependencies between actions, delaying the execution of certain 98 | actions until their dependencies have completed. 99 | 100 | To provide this level of control, the `simulation.Scheduler` uses a run loop that iterates over all events in a 101 | well-defined order until no event remains. For each event, its configuration and default settings are considered to 102 | determine whether it should execute sequentially or asynchronously, if it must wait on other events, or if it will 103 | register a new event generator the run loop has to wait for. 104 | 105 | ### Action 106 | 107 | An `Action` defines an interface for a function to be executed. 108 | 109 | ```golang 110 | type Action interface { 111 | Perform(context.Context) 112 | } 113 | ``` 114 | 115 | The `context.Context` provided to the `Perform` method offers contextual information, that is currently a clock. You can 116 | either use the included `SimpleAction` as a convenient wrapper or create your own implementation. 117 | 118 | ### Events and event generators 119 | 120 | An `Event` is an internal concept of the `simulation.Scheduler` that combines an `Action` with some identifying `Tags` 121 | and a `time.Time` that determines when it should be executed. These events are produced from actions by 122 | `simulation.EventGenerator`s. For example, when calling `simulation.Scheduler.PerformRepeatedly`, a corresponding event 123 | generator is registered, which repeatedly materializes events into the event queue according to its settings. 124 | 125 | When using the `simulation.Scheduler` for deterministic unit tests, you configure events by providing 126 | `EventConfiguration`s. These configurations can target events by the tags, or more specifically by including their 127 | execution time. 128 | 129 | ### Event generators and event queue 130 | 131 | `simulation.EventGenerator`s hold information about at the next and potentially following events materialized by them. 132 | They are either created when calling one of the `simulation.Scheduler.Perform...` methods or simply by adding 133 | your own generator implementation to a `simulation.Scheduler` (e.g. if you want to use Timestone for pure simulation 134 | purposes). 135 | 136 | ```golang 137 | type EventGenerator interface { 138 | Pop() *Event 139 | Peek() Event 140 | Finished() bool 141 | } 142 | ``` 143 | 144 | This interface is then used by the event queue to materialize and sort new events as they are needed in a stream like 145 | fashion. 146 | 147 | Knowing this concept is important when it comes to designing tests for business logic where actions will recursively 148 | schedule more actions (which might schedule more actions). Imagine you have an action `firstAction` that you want to 149 | execute asynchronously, which is supposed to schedule a `secondAction` via `simulation.Scheduler.PerformNow`. 150 | In this case the `simulation.Scheduler`'s run loop needs to wait for the generator later materializing 151 | `secondAction` to be added – otherwise the run loop might terminate in the next iteration, not knowing yet that a new 152 | generator will provide another event shortly. 153 | 154 | To avoid this race condition, you add a new `config.Config` targeting the `firstAction` that looks probably 155 | like: 156 | 157 | ```golang 158 | scheduler.ConfigureEvents( 159 | config.Config{ 160 | Tags: []string{"firstAction"}, 161 | Adds: []*config.Generator{ 162 | Tags: []string{"secondAction"}, 163 | Count: 1, 164 | }, 165 | }, 166 | ) 167 | ``` 168 | 169 | Now after executing every `firstAction` event, the scheduler will pause its run loop until a generator producing 170 | `secondAction` events has been registered. 171 | 172 | ## Contributing 173 | 174 | This project is still under development, and contributions are welcome. Feel free to fork the repository and submit a PR. 175 | When submitting a PR, it would be helpful to reference an open issue for better documentation. 176 | 177 | Currently, the most important features on the agenda for this project are: 178 | - Pipeline for linting and automatic unit tests before merging 179 | - Support for canceled contexts 180 | 181 | ## Reporting a Bug 182 | 183 | To report a bug, please create an issue ticket. Include sufficient code samples and contextual information to reproduce 184 | the bug. If you can provide a fix, it will be greatly appreciated. 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /simulation/internal/events/generator_periodic_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "github.com/metamogul/timestone/v2/internal" 6 | "testing" 7 | "time" 8 | 9 | "github.com/metamogul/timestone/v2" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_NewPeriodicGenerator(t *testing.T) { 14 | t.Parallel() 15 | 16 | type args struct { 17 | action timestone.Action 18 | from time.Time 19 | to *time.Time 20 | interval time.Duration 21 | ctx context.Context 22 | tags []string 23 | } 24 | 25 | ctx := context.Background() 26 | 27 | tests := []struct { 28 | name string 29 | args args 30 | want *PeriodicGenerator 31 | requirePanic bool 32 | }{ 33 | { 34 | name: "no Action", 35 | args: args{ 36 | action: nil, 37 | from: time.Time{}, 38 | to: internal.Ptr(time.Time{}.Add(time.Second)), 39 | interval: time.Second, 40 | ctx: ctx, 41 | }, 42 | requirePanic: true, 43 | }, 44 | { 45 | name: "to before from", 46 | args: args{ 47 | action: timestone.NewMockAction(t), 48 | from: time.Time{}.Add(time.Second), 49 | to: internal.Ptr(time.Time{}), 50 | interval: time.Second, 51 | ctx: ctx, 52 | }, 53 | requirePanic: true, 54 | }, 55 | { 56 | name: "to equals from", 57 | args: args{ 58 | action: timestone.NewMockAction(t), 59 | from: time.Time{}.Add(time.Second), 60 | to: internal.Ptr(time.Time{}.Add(time.Second)), 61 | interval: time.Second, 62 | ctx: ctx, 63 | }, 64 | requirePanic: true, 65 | }, 66 | { 67 | name: "interval is zero", 68 | args: args{ 69 | action: timestone.NewMockAction(t), 70 | from: time.Time{}, 71 | to: internal.Ptr(time.Time{}.Add(time.Second)), 72 | interval: 0, 73 | ctx: ctx, 74 | }, 75 | requirePanic: true, 76 | }, 77 | { 78 | name: "interval is too long", 79 | args: args{ 80 | action: timestone.NewMockAction(t), 81 | from: time.Time{}, 82 | to: internal.Ptr(time.Time{}.Add(time.Second)), 83 | interval: time.Second * 2, 84 | ctx: ctx, 85 | }, 86 | requirePanic: true, 87 | }, 88 | { 89 | name: "success", 90 | args: args{ 91 | action: timestone.NewMockAction(t), 92 | from: time.Time{}, 93 | to: internal.Ptr(time.Time{}.Add(2 * time.Second)), 94 | interval: time.Second, 95 | ctx: ctx, 96 | tags: []string{"test"}, 97 | }, 98 | want: &PeriodicGenerator{ 99 | action: timestone.NewMockAction(t), 100 | from: time.Time{}, 101 | to: internal.Ptr(time.Time{}.Add(2 * time.Second)), 102 | interval: time.Second, 103 | tags: []string{"test"}, 104 | nextEvent: &Event{ 105 | Action: timestone.NewMockAction(t), 106 | Time: time.Time{}.Add(time.Second), 107 | Context: ctx, 108 | tags: []string{"test"}, 109 | }, 110 | ctx: ctx, 111 | }, 112 | }, 113 | } 114 | 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | t.Parallel() 118 | 119 | if tt.requirePanic { 120 | require.Panics(t, func() { 121 | _ = NewPeriodicGenerator(tt.args.ctx, tt.args.action, tt.args.from, tt.args.to, tt.args.interval, tt.args.tags) 122 | }) 123 | return 124 | } 125 | 126 | newGenerator := NewPeriodicGenerator(tt.args.ctx, tt.args.action, tt.args.from, tt.args.to, tt.args.interval, tt.args.tags) 127 | require.Equal(t, tt.want, newGenerator) 128 | }) 129 | } 130 | } 131 | 132 | func Test_PeriodicGenerator_Pop(t *testing.T) { 133 | t.Parallel() 134 | 135 | type fields struct { 136 | action timestone.Action 137 | from time.Time 138 | to *time.Time 139 | interval time.Duration 140 | currentEvent *Event 141 | ctx context.Context 142 | } 143 | 144 | ctx := context.Background() 145 | 146 | tests := []struct { 147 | name string 148 | fields fields 149 | want *Event 150 | requirePanic bool 151 | requireFinished bool 152 | }{ 153 | { 154 | name: "already finished", 155 | fields: fields{ 156 | action: timestone.NewMockAction(t), 157 | from: time.Time{}, 158 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 159 | interval: 10 * time.Second, 160 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(55*time.Second), []string{}), 161 | ctx: context.Background(), 162 | }, 163 | requirePanic: true, 164 | }, 165 | { 166 | name: "success, not finished 1", 167 | fields: fields{ 168 | action: timestone.NewMockAction(t), 169 | from: time.Time{}, 170 | to: nil, 171 | interval: time.Second, 172 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(time.Second), []string{}), 173 | ctx: context.Background(), 174 | }, 175 | want: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(time.Second), []string{}), 176 | }, 177 | { 178 | name: "success, not finished 2", 179 | fields: fields{ 180 | action: timestone.NewMockAction(t), 181 | from: time.Time{}, 182 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 183 | interval: 10 * time.Second, 184 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(40*time.Second), []string{}), 185 | ctx: context.Background(), 186 | }, 187 | want: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(40*time.Second), []string{}), 188 | }, 189 | { 190 | name: "success, finished", 191 | fields: fields{ 192 | action: timestone.NewMockAction(t), 193 | from: time.Time{}, 194 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 195 | interval: 10 * time.Second, 196 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(50*time.Second), []string{}), 197 | ctx: context.Background(), 198 | }, 199 | want: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(50*time.Second), []string{}), 200 | requireFinished: true, 201 | }, 202 | } 203 | for _, tt := range tests { 204 | t.Run(tt.name, func(t *testing.T) { 205 | t.Parallel() 206 | 207 | p := &PeriodicGenerator{ 208 | action: tt.fields.action, 209 | from: tt.fields.from, 210 | to: tt.fields.to, 211 | interval: tt.fields.interval, 212 | nextEvent: tt.fields.currentEvent, 213 | ctx: tt.fields.ctx, 214 | } 215 | 216 | if tt.requirePanic { 217 | require.Panics(t, func() { 218 | _ = p.Pop() 219 | }) 220 | return 221 | } 222 | 223 | require.Equal(t, tt.want, p.Pop()) 224 | 225 | if tt.requireFinished { 226 | require.True(t, p.Finished()) 227 | } else { 228 | require.False(t, p.Finished()) 229 | } 230 | }) 231 | } 232 | } 233 | 234 | func Test_PeriodicGenerator_Peek(t *testing.T) { 235 | t.Parallel() 236 | 237 | type fields struct { 238 | action timestone.Action 239 | from time.Time 240 | to *time.Time 241 | interval time.Duration 242 | currentEvent *Event 243 | ctx context.Context 244 | } 245 | 246 | ctx := context.Background() 247 | 248 | tests := []struct { 249 | name string 250 | fields fields 251 | want Event 252 | requirePanic bool 253 | }{ 254 | { 255 | name: "already finished", 256 | fields: fields{ 257 | action: timestone.NewMockAction(t), 258 | from: time.Time{}, 259 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 260 | interval: 10 * time.Second, 261 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(55*time.Second), []string{}), 262 | ctx: context.Background(), 263 | }, 264 | requirePanic: true, 265 | }, 266 | { 267 | name: "success", 268 | fields: fields{ 269 | action: timestone.NewMockAction(t), 270 | from: time.Time{}, 271 | to: nil, 272 | interval: time.Second, 273 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(time.Second), []string{}), 274 | ctx: context.Background(), 275 | }, 276 | want: *NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(time.Second), []string{}), 277 | }, 278 | } 279 | 280 | for _, tt := range tests { 281 | t.Run(tt.name, func(t *testing.T) { 282 | t.Parallel() 283 | 284 | p := &PeriodicGenerator{ 285 | action: tt.fields.action, 286 | from: tt.fields.from, 287 | to: tt.fields.to, 288 | interval: tt.fields.interval, 289 | nextEvent: tt.fields.currentEvent, 290 | ctx: tt.fields.ctx, 291 | } 292 | 293 | if tt.requirePanic { 294 | require.Panics(t, func() { 295 | _ = p.Peek() 296 | }) 297 | return 298 | } 299 | 300 | require.Equal(t, tt.want, p.Peek()) 301 | 302 | require.False(t, p.Finished()) 303 | }) 304 | } 305 | } 306 | 307 | func Test_PeriodicGenerator_Finished(t *testing.T) { 308 | t.Parallel() 309 | 310 | type fields struct { 311 | action timestone.Action 312 | from time.Time 313 | to *time.Time 314 | interval time.Duration 315 | currentEvent *Event 316 | ctx context.Context 317 | } 318 | 319 | ctx, cancel := context.WithCancel(context.Background()) 320 | cancel() 321 | 322 | tests := []struct { 323 | name string 324 | fields fields 325 | want bool 326 | }{ 327 | { 328 | name: "context is done", 329 | fields: fields{ 330 | action: timestone.NewMockAction(t), 331 | from: time.Time{}, 332 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 333 | interval: 10 * time.Second, 334 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(45*time.Second), []string{}), 335 | ctx: ctx, 336 | }, 337 | want: true, 338 | }, 339 | { 340 | name: "to is nil", 341 | fields: fields{ 342 | action: timestone.NewMockAction(t), 343 | from: time.Time{}, 344 | to: nil, 345 | interval: 0, 346 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}, []string{}), 347 | ctx: context.Background(), 348 | }, 349 | want: false, 350 | }, 351 | { 352 | name: "to is set, finished", 353 | fields: fields{ 354 | action: timestone.NewMockAction(t), 355 | from: time.Time{}, 356 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 357 | interval: 10 * time.Second, 358 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(55*time.Second), []string{}), 359 | ctx: context.Background(), 360 | }, 361 | want: true, 362 | }, 363 | { 364 | name: "to is set, not finished yet", 365 | fields: fields{ 366 | action: timestone.NewMockAction(t), 367 | from: time.Time{}, 368 | to: internal.Ptr(time.Time{}.Add(time.Minute)), 369 | interval: 10 * time.Second, 370 | currentEvent: NewEvent(ctx, timestone.NewMockAction(t), time.Time{}.Add(45*time.Second), []string{}), 371 | ctx: context.Background(), 372 | }, 373 | want: false, 374 | }, 375 | } 376 | 377 | for _, tt := range tests { 378 | t.Run(tt.name, func(t *testing.T) { 379 | t.Parallel() 380 | 381 | p := &PeriodicGenerator{ 382 | action: tt.fields.action, 383 | from: tt.fields.from, 384 | to: tt.fields.to, 385 | interval: tt.fields.interval, 386 | nextEvent: tt.fields.currentEvent, 387 | ctx: tt.fields.ctx, 388 | } 389 | 390 | require.Equal(t, tt.want, p.Finished()) 391 | }) 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /simulation/internal/events/queue_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "github.com/metamogul/timestone/v2/simulation/config" 6 | "slices" 7 | "testing" 8 | "time" 9 | 10 | "github.com/metamogul/timestone/v2" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func Test_NewQueue(t *testing.T) { 15 | t.Parallel() 16 | 17 | got := NewQueue(NewConfigs()) 18 | 19 | require.NotNil(t, got.configs) 20 | require.NotNil(t, got.activeGenerators) 21 | require.NotNil(t, got.finishedGenerators) 22 | require.NotNil(t, got.NewGeneratorsWaitGroups) 23 | 24 | require.Len(t, got.activeGenerators, 0) 25 | require.Len(t, got.finishedGenerators, 0) 26 | 27 | } 28 | 29 | func TestQueue_Add(t *testing.T) { 30 | t.Parallel() 31 | 32 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 33 | 34 | tests := []struct { 35 | name string 36 | generator func() Generator 37 | generatorIsFinished bool 38 | }{ 39 | { 40 | name: "generator finished", 41 | generator: func() Generator { 42 | mockEventGenerator := NewMockGenerator(t) 43 | mockEventGenerator.EXPECT(). 44 | Finished(). 45 | Return(true). 46 | Once() 47 | 48 | return mockEventGenerator 49 | }, 50 | generatorIsFinished: true, 51 | }, 52 | { 53 | name: "generator not finished", 54 | generator: func() Generator { 55 | mockEventGenerator := NewMockGenerator(t) 56 | mockEventGenerator.EXPECT(). 57 | Finished(). 58 | Return(false). 59 | Once() 60 | mockEventGenerator.EXPECT(). 61 | Peek(). 62 | Return( 63 | *NewEvent( 64 | context.Background(), 65 | timestone.SimpleAction(func(context.Context) {}), 66 | now, 67 | []string{"test"}, 68 | ), 69 | ). 70 | Once() 71 | 72 | return mockEventGenerator 73 | }, 74 | generatorIsFinished: false, 75 | }, 76 | } 77 | 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | t.Parallel() 81 | 82 | e := NewQueue(NewConfigs()) 83 | 84 | e.Add(tt.generator()) 85 | 86 | if !tt.generatorIsFinished { 87 | require.Len(t, e.activeGenerators, 1) 88 | require.Len(t, e.finishedGenerators, 0) 89 | e.NewGeneratorsWaitGroups.Add(1, []string{"test"}) 90 | go func() { e.NewGeneratorsWaitGroups.Done([]string{"test"}) }() 91 | e.NewGeneratorsWaitGroups.WaitFor([]string{"test"}) 92 | } else { 93 | require.Len(t, e.activeGenerators, 0) 94 | require.Len(t, e.finishedGenerators, 1) 95 | } 96 | 97 | sorted := slices.IsSortedFunc(e.activeGenerators, func(a, b Generator) int { 98 | return a.Peek().Time.Compare(b.Peek().Time) 99 | }) 100 | require.True(t, sorted) 101 | }) 102 | } 103 | } 104 | 105 | func TestQueue_ExpectGenerators(t *testing.T) { 106 | t.Parallel() 107 | 108 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 109 | 110 | generatorMock := NewMockGenerator(t) 111 | generatorMock.EXPECT(). 112 | Finished(). 113 | Return(false). 114 | Once() 115 | generatorMock.EXPECT(). 116 | Peek(). 117 | Return( 118 | *NewEvent( 119 | context.Background(), 120 | timestone.SimpleAction(func(context.Context) {}), 121 | now, 122 | []string{"test", "group", "foo"}, 123 | ), 124 | ). 125 | Once() 126 | 127 | e := NewQueue(NewConfigs()) 128 | 129 | generatorExpectations := []*config.Generator{{Tags: []string{"test"}, Count: 1}} 130 | 131 | e.ExpectGenerators(generatorExpectations) 132 | go func() { e.Add(generatorMock) }() 133 | e.WaitForExpectedGenerators(generatorExpectations) 134 | } 135 | 136 | func TestQueue_WaitForExpectedGenerators(t *testing.T) { 137 | t.Parallel() 138 | 139 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 140 | 141 | generatorMock := NewMockGenerator(t) 142 | generatorMock.EXPECT(). 143 | Finished(). 144 | Return(false). 145 | Once() 146 | generatorMock.EXPECT(). 147 | Peek(). 148 | Return( 149 | *NewEvent( 150 | context.Background(), 151 | timestone.SimpleAction(func(context.Context) {}), 152 | now, 153 | []string{"test", "group", "foo"}, 154 | ), 155 | ). 156 | Once() 157 | 158 | e := NewQueue(NewConfigs()) 159 | 160 | generatorExpectations := []*config.Generator{{Tags: []string{"test"}, Count: 1}} 161 | 162 | e.ExpectGenerators(generatorExpectations) 163 | go func() { e.Add(generatorMock) }() 164 | e.WaitForExpectedGenerators(generatorExpectations) 165 | } 166 | 167 | func TestQueue_Pop(t *testing.T) { 168 | t.Parallel() 169 | 170 | type fields struct { 171 | activeGenerators func() []Generator 172 | finishedGenerators func() []Generator 173 | } 174 | 175 | ctx := context.Background() 176 | 177 | tests := []struct { 178 | name string 179 | fields fields 180 | finishesGenerator bool 181 | want *Event 182 | requirePanic bool 183 | }{ 184 | { 185 | name: "all generators finished", 186 | fields: fields{ 187 | activeGenerators: func() []Generator { 188 | return make([]Generator, 0) 189 | }, 190 | finishedGenerators: func() []Generator { 191 | return make([]Generator, 0) 192 | }, 193 | }, 194 | requirePanic: true, 195 | }, 196 | { 197 | name: "success, generator not finished", 198 | fields: fields{ 199 | activeGenerators: func() []Generator { 200 | eventGenerator1 := NewPeriodicGenerator(ctx, timestone.NewMockAction(t), time.Time{}, nil, time.Minute, []string{"test1"}) 201 | eventGenerator2 := NewPeriodicGenerator(ctx, timestone.NewMockAction(t), time.Time{}, nil, time.Second, []string{"test2"}) 202 | return []Generator{eventGenerator1, eventGenerator2} 203 | }, 204 | finishedGenerators: func() []Generator { 205 | return make([]Generator, 0) 206 | }, 207 | }, 208 | finishesGenerator: false, 209 | want: &Event{ 210 | Action: timestone.NewMockAction(t), 211 | Time: time.Time{}.Add(time.Second), 212 | Context: ctx, 213 | tags: []string{"test2"}, 214 | }, 215 | }, 216 | { 217 | name: "success, generator finished", 218 | fields: fields{ 219 | activeGenerators: func() []Generator { 220 | eventGenerator1 := NewOnceGenerator(context.Background(), timestone.NewMockAction(t), time.Time{}, []string{"test1"}) 221 | eventGenerator2 := NewPeriodicGenerator(context.Background(), timestone.NewMockAction(t), time.Time{}, nil, time.Second, []string{"test2"}) 222 | return []Generator{eventGenerator1, eventGenerator2} 223 | }, 224 | finishedGenerators: func() []Generator { 225 | return make([]Generator, 0) 226 | }, 227 | }, 228 | finishesGenerator: true, 229 | want: &Event{ 230 | Action: timestone.NewMockAction(t), 231 | Time: time.Time{}, 232 | Context: ctx, 233 | tags: []string{"test1"}, 234 | }, 235 | }, 236 | } 237 | 238 | for _, tt := range tests { 239 | t.Run(tt.name, func(t *testing.T) { 240 | t.Parallel() 241 | 242 | e := &Queue{ 243 | activeGenerators: tt.fields.activeGenerators(), 244 | finishedGenerators: tt.fields.finishedGenerators(), 245 | } 246 | e.sortActiveGenerators() 247 | 248 | if tt.requirePanic { 249 | require.Panics(t, func() { 250 | _ = e.Pop() 251 | }) 252 | return 253 | } 254 | 255 | require.Equal(t, tt.want.tags, e.Pop().tags) 256 | 257 | if !tt.finishesGenerator { 258 | require.Len(t, e.activeGenerators, len(tt.fields.activeGenerators())) 259 | require.Len(t, e.finishedGenerators, len(tt.fields.finishedGenerators())) 260 | } else { 261 | require.Len(t, e.activeGenerators, len(tt.fields.activeGenerators())-1) 262 | require.Len(t, e.finishedGenerators, len(tt.fields.finishedGenerators())+1) 263 | } 264 | }) 265 | } 266 | } 267 | 268 | func TestQueue_Peek(t *testing.T) { 269 | t.Parallel() 270 | 271 | type fields struct { 272 | activeGenerators func() []Generator 273 | finishedGenerators func() []Generator 274 | } 275 | 276 | ctx := context.Background() 277 | 278 | tests := []struct { 279 | name string 280 | fields fields 281 | want Event 282 | requirePanic bool 283 | }{ 284 | { 285 | name: "all generators finished", 286 | fields: fields{ 287 | activeGenerators: func() []Generator { 288 | return make([]Generator, 0) 289 | }, 290 | finishedGenerators: func() []Generator { 291 | return make([]Generator, 0) 292 | }, 293 | }, 294 | requirePanic: true, 295 | }, 296 | { 297 | name: "success", 298 | fields: fields{ 299 | activeGenerators: func() []Generator { 300 | eventGenerator1 := NewPeriodicGenerator(ctx, timestone.NewMockAction(t), time.Time{}, nil, time.Minute, []string{"test1"}) 301 | eventGenerator2 := NewPeriodicGenerator(ctx, timestone.NewMockAction(t), time.Time{}, nil, time.Second, []string{"test2"}) 302 | return []Generator{eventGenerator1, eventGenerator2} 303 | }, 304 | finishedGenerators: func() []Generator { 305 | return make([]Generator, 0) 306 | }, 307 | }, 308 | want: Event{ 309 | Action: timestone.NewMockAction(t), 310 | Time: time.Time{}.Add(time.Second), 311 | Context: ctx, 312 | tags: []string{"test2"}, 313 | }, 314 | }, 315 | } 316 | 317 | for _, tt := range tests { 318 | t.Run(tt.name, func(t *testing.T) { 319 | t.Parallel() 320 | 321 | e := &Queue{ 322 | activeGenerators: tt.fields.activeGenerators(), 323 | finishedGenerators: tt.fields.finishedGenerators(), 324 | } 325 | e.sortActiveGenerators() 326 | 327 | if tt.requirePanic { 328 | require.Panics(t, func() { 329 | _ = e.Peek() 330 | }) 331 | return 332 | } 333 | 334 | require.Equal(t, tt.want, e.Peek()) 335 | require.Len(t, e.activeGenerators, len(tt.fields.activeGenerators())) 336 | require.Len(t, e.finishedGenerators, len(tt.fields.finishedGenerators())) 337 | 338 | }) 339 | } 340 | } 341 | 342 | func TestQueue_Finished(t *testing.T) { 343 | t.Parallel() 344 | 345 | type fields struct { 346 | activeGenerators []Generator 347 | finishedGenerators []Generator 348 | } 349 | 350 | tests := []struct { 351 | name string 352 | fields fields 353 | want bool 354 | }{ 355 | { 356 | name: "not finished", 357 | fields: fields{ 358 | activeGenerators: []Generator{NewMockGenerator(t)}, 359 | finishedGenerators: make([]Generator, 0), 360 | }, 361 | want: false, 362 | }, 363 | { 364 | name: "finished", 365 | fields: fields{ 366 | activeGenerators: make([]Generator, 0), 367 | finishedGenerators: make([]Generator, 0), 368 | }, 369 | want: true, 370 | }, 371 | } 372 | 373 | for _, tt := range tests { 374 | t.Run(tt.name, func(t *testing.T) { 375 | t.Parallel() 376 | 377 | e := &Queue{ 378 | activeGenerators: tt.fields.activeGenerators, 379 | finishedGenerators: tt.fields.finishedGenerators, 380 | } 381 | 382 | require.Equal(t, tt.want, e.Finished()) 383 | }) 384 | } 385 | } 386 | 387 | func TestQueue_sortActiveGenerators(t *testing.T) { 388 | t.Parallel() 389 | 390 | eventGenerator1 := NewPeriodicGenerator(context.Background(), timestone.NewMockAction(t), time.Time{}, nil, time.Minute, []string{}) 391 | eventGenerator2 := NewPeriodicGenerator(context.Background(), timestone.NewMockAction(t), time.Time{}, nil, time.Second, []string{}) 392 | eventGenerator3 := NewPeriodicGenerator(context.Background(), timestone.NewMockAction(t), time.Time{}, nil, time.Hour, []string{}) 393 | 394 | activeGenerators := []Generator{eventGenerator1, eventGenerator2, eventGenerator3} 395 | 396 | e := &Queue{ 397 | activeGenerators: activeGenerators, 398 | finishedGenerators: make([]Generator, 0), 399 | } 400 | e.sortActiveGenerators() 401 | 402 | sorted := slices.IsSortedFunc(e.activeGenerators, func(a, b Generator) int { 403 | return a.Peek().Time.Compare(b.Peek().Time) 404 | }) 405 | require.True(t, sorted) 406 | } 407 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /simulation/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/metamogul/timestone/v2/simulation/config" 7 | "github.com/metamogul/timestone/v2/simulation/internal/clock" 8 | "github.com/metamogul/timestone/v2/simulation/internal/events" 9 | "github.com/metamogul/timestone/v2/simulation/internal/waitgroups" 10 | "slices" 11 | "sync" 12 | "testing" 13 | "time" 14 | 15 | "github.com/metamogul/timestone/v2" 16 | "github.com/stretchr/testify/mock" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestNewScheduler(t *testing.T) { 21 | t.Parallel() 22 | 23 | now := time.Now() 24 | 25 | newEventScheduler := NewScheduler(now) 26 | 27 | require.NotNil(t, newEventScheduler) 28 | require.IsType(t, &Scheduler{}, newEventScheduler) 29 | 30 | require.NotNil(t, newEventScheduler.clock) 31 | require.Equal(t, now, newEventScheduler.clock.Now()) 32 | 33 | require.NotNil(t, newEventScheduler.eventQueue) 34 | require.NotNil(t, newEventScheduler.eventConfigs) 35 | require.NotNil(t, newEventScheduler.eventWaitGroups) 36 | } 37 | 38 | func TestScheduler_Now(t *testing.T) { 39 | t.Parallel() 40 | 41 | now := time.Now() 42 | s := NewScheduler(now) 43 | 44 | require.Equal(t, now, s.Now()) 45 | } 46 | 47 | func TestScheduler_ConfigureEvent(t *testing.T) { 48 | t.Parallel() 49 | 50 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 51 | 52 | NewScheduler(now).ConfigureEvents(config.Config{Tags: []string{"test"}}) 53 | } 54 | 55 | func TestScheduler_ForwardOne(t *testing.T) { 56 | t.Parallel() 57 | 58 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 59 | 60 | mu := sync.Mutex{} 61 | eventTimes := make([]time.Time, 0) 62 | 63 | longRunningAction1 := timestone.NewMockAction(t) 64 | longRunningAction1.EXPECT(). 65 | Perform(mock.Anything). 66 | Run(func(ctx context.Context) { 67 | time.Sleep(100 * time.Millisecond) 68 | 69 | mu.Lock() 70 | eventTimes = append(eventTimes, ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now()) 71 | mu.Unlock() 72 | }). 73 | Once() 74 | 75 | longRunningAction2 := timestone.NewMockAction(t) 76 | longRunningAction2.EXPECT(). 77 | Perform(mock.Anything). 78 | Run(func(ctx context.Context) { 79 | time.Sleep(50 * time.Millisecond) 80 | 81 | mu.Lock() 82 | eventTimes = append(eventTimes, ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now()) 83 | mu.Unlock() 84 | }). 85 | Once() 86 | 87 | s := NewScheduler(now) 88 | s.PerformAfter(context.Background(), longRunningAction1, 1*time.Second, "longRunningAction1") 89 | s.PerformAfter(context.Background(), longRunningAction2, 2*time.Second, "longRunningAction2") 90 | 91 | s.ForwardOne() 92 | s.WaitFor(config.All{Tags: []string{"longRunningAction1"}}) 93 | require.Len(t, eventTimes, 1) 94 | require.Equal(t, now.Add(1*time.Second), eventTimes[0]) 95 | require.Equal(t, now.Add(1*time.Second), s.clock.Now()) 96 | 97 | s.ForwardOne() 98 | s.WaitFor(config.All{Tags: []string{"longRunningAction2"}}) 99 | require.Len(t, eventTimes, 2) 100 | require.Equal(t, now.Add(2*time.Second), eventTimes[1]) 101 | require.Equal(t, now.Add(2*time.Second), s.clock.Now()) 102 | } 103 | 104 | func TestScheduler_ForwardOne_Recursive(t *testing.T) { 105 | t.Parallel() 106 | 107 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 108 | 109 | mu := sync.Mutex{} 110 | eventTimes := make([]time.Time, 0) 111 | 112 | s := NewScheduler(now) 113 | 114 | innerAction := timestone.NewMockAction(t) 115 | innerAction.EXPECT(). 116 | Perform(mock.Anything). 117 | Run(func(ctx context.Context) { 118 | mu.Lock() 119 | eventTimes = append( 120 | eventTimes, 121 | ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now(), 122 | ) 123 | mu.Unlock() 124 | }). 125 | Once() 126 | 127 | outerAction := timestone.NewMockAction(t) 128 | outerAction.EXPECT(). 129 | Perform(mock.Anything). 130 | Run(func(ctx context.Context) { 131 | s.PerformAfter(ctx, innerAction, time.Second, "innerAction") 132 | 133 | mu.Lock() 134 | eventTimes = append( 135 | eventTimes, 136 | ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now(), 137 | ) 138 | mu.Unlock() 139 | }). 140 | Once() 141 | 142 | s.PerformAfter(context.Background(), outerAction, 1*time.Second, "outerAction") 143 | s.ConfigureEvents(config.Config{ 144 | Tags: []string{"outerAction"}, 145 | Adds: []*config.Generator{{Tags: []string{"innerAction"}, Count: 1}}, 146 | }) 147 | 148 | s.ForwardOne() 149 | s.WaitFor(config.All{Tags: []string{"outerAction"}}) 150 | require.Len(t, eventTimes, 1) 151 | require.Equal(t, now.Add(1*time.Second), eventTimes[0]) 152 | require.Equal(t, now.Add(1*time.Second), s.clock.Now()) 153 | 154 | s.ForwardOne() 155 | s.WaitFor(config.All{Tags: []string{"innerAction"}}) 156 | require.Len(t, eventTimes, 2) 157 | require.Equal(t, now.Add(2*time.Second), eventTimes[1]) 158 | require.Equal(t, now.Add(2*time.Second), s.clock.Now()) 159 | } 160 | 161 | func TestScheduler_WaitFor(t *testing.T) { 162 | t.Parallel() 163 | 164 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 165 | 166 | s := NewScheduler(now) 167 | 168 | wg1 := s.eventWaitGroups.New(now, []string{"test1", "group"}) 169 | wg2 := s.eventWaitGroups.New(now, []string{"test2", "group"}) 170 | go func() { 171 | wg1.Done() 172 | wg2.Done() 173 | }() 174 | 175 | s.eventWaitGroups.WaitFor([]config.Event{config.All{Tags: []string{"group"}}}) 176 | } 177 | 178 | func TestScheduler_Wait(t *testing.T) { 179 | t.Parallel() 180 | 181 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 182 | 183 | s := NewScheduler(now) 184 | 185 | for i := range 5 { 186 | testName := fmt.Sprintf("test%d", i) 187 | wg := s.eventWaitGroups.New(now, []string{testName}) 188 | go func() { wg.Done() }() 189 | } 190 | 191 | s.eventWaitGroups.Wait() 192 | } 193 | 194 | func TestScheduler_Forward(t *testing.T) { 195 | t.Parallel() 196 | 197 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 198 | 199 | const fooActionSimulateLoad = 500 * time.Millisecond 200 | const barActionSimulateLoad = 50 * time.Millisecond 201 | 202 | testcases := []struct { 203 | name string 204 | fooActionScheduleAfter time.Duration 205 | barActionScheduleAfter time.Duration 206 | configureScheduler func(s *Scheduler) 207 | wantResult string 208 | wantTimes []time.Time 209 | }{ 210 | { 211 | name: "no syncing", 212 | fooActionScheduleAfter: 1 * time.Millisecond, 213 | barActionScheduleAfter: 2 * time.Millisecond, 214 | wantResult: "barfoo", 215 | wantTimes: []time.Time{ 216 | now.Add(2 * time.Millisecond), 217 | now.Add(1 * time.Millisecond), 218 | }, 219 | }, 220 | { 221 | name: "wait for actions", 222 | fooActionScheduleAfter: 1 * time.Millisecond, 223 | barActionScheduleAfter: 2 * time.Millisecond, 224 | configureScheduler: func(s *Scheduler) { 225 | s.ConfigureEvents(config.Config{ 226 | Tags: []string{"barAction"}, 227 | WaitFor: []config.Event{config.All{Tags: []string{"fooAction"}}}, 228 | }) 229 | }, 230 | wantResult: "foobar", 231 | wantTimes: []time.Time{ 232 | now.Add(1 * time.Millisecond), 233 | now.Add(2 * time.Millisecond), 234 | }, 235 | }, 236 | } 237 | 238 | for _, tt := range testcases { 239 | t.Run(tt.name, func(t *testing.T) { 240 | t.Parallel() 241 | 242 | result := "" 243 | executionTimes := make([]time.Time, 0) 244 | 245 | fooAction := timestone.NewMockAction(t) 246 | fooAction.EXPECT(). 247 | Perform(mock.Anything). 248 | Run(func(ctx context.Context) { 249 | time.Sleep(fooActionSimulateLoad) 250 | result += "foo" 251 | executionTimes = append( 252 | executionTimes, ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now(), 253 | ) 254 | }). 255 | Once() 256 | 257 | barAction := timestone.NewMockAction(t) 258 | barAction.EXPECT(). 259 | Perform(mock.Anything). 260 | Run(func(ctx context.Context) { 261 | time.Sleep(barActionSimulateLoad) 262 | result += "bar" 263 | executionTimes = append( 264 | executionTimes, ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now(), 265 | ) 266 | }). 267 | Once() 268 | 269 | s := NewScheduler(now) 270 | s.PerformAfter(context.Background(), fooAction, tt.fooActionScheduleAfter, "fooAction") 271 | s.PerformAfter(context.Background(), barAction, tt.barActionScheduleAfter, "barAction") 272 | 273 | if tt.configureScheduler != nil { 274 | tt.configureScheduler(s) 275 | } 276 | 277 | s.Forward(tt.barActionScheduleAfter + tt.fooActionScheduleAfter) 278 | 279 | require.Equal(t, tt.wantResult, result) 280 | require.Equal(t, tt.wantTimes, executionTimes) 281 | }) 282 | } 283 | } 284 | 285 | func TestScheduler_Forward_Recursive(t *testing.T) { 286 | t.Parallel() 287 | 288 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 289 | 290 | executionTimes := make([]time.Time, 0) 291 | 292 | s := NewScheduler(now) 293 | 294 | innerAction := timestone.NewMockAction(t) 295 | innerAction.EXPECT(). 296 | Perform(mock.Anything). 297 | Run(func(ctx context.Context) { 298 | executionTimes = append( 299 | executionTimes, 300 | ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now(), 301 | ) 302 | }). 303 | Once() 304 | 305 | outerAction := timestone.NewMockAction(t) 306 | outerAction.EXPECT(). 307 | Perform(mock.Anything). 308 | Run(func(ctx context.Context) { 309 | s.PerformAfter(context.Background(), innerAction, time.Second, "innerAction") 310 | executionTimes = append( 311 | executionTimes, 312 | ctx.Value(timestone.ActionContextClockKey).(timestone.Clock).Now(), 313 | ) 314 | }). 315 | Once() 316 | 317 | s.ConfigureEvents(config.Config{ 318 | Tags: []string{"outerAction"}, 319 | Adds: []*config.Generator{{[]string{"innerAction"}, 1}}, 320 | }) 321 | 322 | s.PerformAfter(context.Background(), outerAction, time.Second, "outerAction") 323 | 324 | s.Forward(3 * time.Second) 325 | 326 | sorted := slices.IsSortedFunc(executionTimes, func(a, b time.Time) int { 327 | return a.Compare(b) 328 | }) 329 | require.True(t, sorted) 330 | 331 | } 332 | 333 | func TestScheduler_execNextEvent(t *testing.T) { 334 | t.Parallel() 335 | 336 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 337 | targetTime := now.Add(time.Minute) 338 | 339 | tests := []struct { 340 | name string 341 | eventGenerators func() []events.Generator 342 | wantShouldContinue bool 343 | }{ 344 | { 345 | name: "all event generators finished", 346 | eventGenerators: func() []events.Generator { return nil }, 347 | }, 348 | { 349 | name: "next event after target time", 350 | eventGenerators: func() []events.Generator { 351 | mockAction := timestone.NewMockAction(t) 352 | return []events.Generator{events.NewOnceGenerator(context.Background(), mockAction, now.Add(1*time.Hour), []string{"test"})} 353 | }, 354 | }, 355 | { 356 | name: "event dispatched successfully", 357 | eventGenerators: func() []events.Generator { 358 | mockAction := timestone.NewMockAction(t) 359 | mockAction.EXPECT(). 360 | Perform(mock.Anything). 361 | Once() 362 | return []events.Generator{events.NewOnceGenerator(context.Background(), mockAction, now.Add(1*time.Second), []string{"test"})} 363 | }, 364 | wantShouldContinue: true, 365 | }, 366 | } 367 | 368 | for _, tt := range tests { 369 | t.Run(tt.name, func(t *testing.T) { 370 | t.Parallel() 371 | 372 | eventConfigs := events.NewConfigs() 373 | eventQueue := events.NewQueue(eventConfigs) 374 | for _, generator := range tt.eventGenerators() { 375 | eventQueue.Add(generator) 376 | } 377 | 378 | s := &Scheduler{ 379 | clock: clock.NewClock(now), 380 | eventQueue: eventQueue, 381 | eventConfigs: eventConfigs, 382 | eventWaitGroups: waitgroups.NewEventWaitGroups(), 383 | } 384 | 385 | if gotShouldContinue := s.execNextEvent(targetTime); gotShouldContinue != tt.wantShouldContinue { 386 | t.Errorf("performNextEvent() = %v, want %v", gotShouldContinue, tt.wantShouldContinue) 387 | } 388 | s.eventWaitGroups.Wait() 389 | 390 | if tt.wantShouldContinue == true { 391 | require.Equal(t, now.Add(time.Second), s.clock.Now()) 392 | } else { 393 | require.Equal(t, targetTime, s.clock.Now()) 394 | } 395 | }) 396 | } 397 | } 398 | 399 | func TestScheduler_execEvent(t *testing.T) { 400 | t.Parallel() 401 | 402 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 403 | 404 | t.Run("wants newMatching generators", func(t *testing.T) { 405 | t.Parallel() 406 | 407 | s := NewScheduler(now) 408 | 409 | mockAction := timestone.NewMockAction(t) 410 | mockAction.EXPECT(). 411 | Perform(mock.Anything). 412 | Run(func(context.Context) { 413 | s.PerformNow(context.Background(), timestone.NewMockAction(t), "scheduledByTest") 414 | }). 415 | Once() 416 | 417 | eventToExec := events.NewEvent(context.Background(), mockAction, now.Add(time.Minute), []string{"test"}) 418 | eventConfig := config.Config{ 419 | Tags: []string{"test"}, 420 | Adds: []*config.Generator{{[]string{"scheduledByTest"}, 1}}, 421 | } 422 | 423 | s.eventConfigs.Set(eventConfig) 424 | 425 | s.execEvent(eventToExec) 426 | s.WaitFor(config.All{Tags: []string{"test"}}) 427 | 428 | require.Equal(t, now.Add(time.Minute), s.clock.Now()) 429 | }) 430 | 431 | t.Run("no newMatching generators", func(t *testing.T) { 432 | t.Parallel() 433 | 434 | mockAction := timestone.NewMockAction(t) 435 | mockAction.EXPECT(). 436 | Perform(mock.Anything). 437 | Once() 438 | 439 | eventToExec := events.NewEvent(context.Background(), mockAction, now.Add(time.Minute), []string{"test"}) 440 | 441 | s := NewScheduler(now) 442 | 443 | s.execEvent(eventToExec) 444 | s.WaitFor(config.All{Tags: []string{"test"}}) 445 | 446 | require.Equal(t, now.Add(time.Minute), s.clock.Now()) 447 | }) 448 | } 449 | 450 | func TestScheduler_PerformNow(t *testing.T) { 451 | t.Parallel() 452 | 453 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 454 | 455 | s := NewScheduler(now) 456 | 457 | s.PerformNow(context.Background(), timestone.NewMockAction(t), "mockAction") 458 | 459 | require.False(t, s.eventQueue.Finished()) 460 | } 461 | 462 | func TestScheduler_PerformAfter(t *testing.T) { 463 | t.Parallel() 464 | 465 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 466 | 467 | s := NewScheduler(now) 468 | 469 | s.PerformAfter(context.Background(), timestone.NewMockAction(t), time.Second, "mockAction") 470 | 471 | require.False(t, s.eventQueue.Finished()) 472 | } 473 | 474 | func TestScheduler_PerformRepeatedly(t *testing.T) { 475 | t.Parallel() 476 | 477 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 478 | 479 | s := NewScheduler(now) 480 | 481 | s.PerformRepeatedly(context.Background(), timestone.NewMockAction(t), nil, time.Second, "mockAction") 482 | 483 | require.False(t, s.eventQueue.Finished()) 484 | } 485 | 486 | func TestScheduler_AddEventGenerators(t *testing.T) { 487 | t.Parallel() 488 | 489 | mockEvent := events.NewEvent(context.Background(), timestone.NewMockAction(t), time.Time{}, []string{"mockAction"}) 490 | 491 | mockEventGenerator1 := events.NewMockGenerator(t) 492 | mockEventGenerator1.EXPECT(). 493 | Peek(). 494 | Return(*mockEvent). 495 | Maybe() 496 | mockEventGenerator1.EXPECT(). 497 | Finished(). 498 | Return(false). 499 | Maybe() 500 | 501 | mockEventGenerator2 := events.NewMockGenerator(t) 502 | mockEventGenerator2.EXPECT(). 503 | Peek(). 504 | Return(*mockEvent). 505 | Maybe() 506 | mockEventGenerator2.EXPECT(). 507 | Finished(). 508 | Return(false). 509 | Maybe() 510 | 511 | now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) 512 | 513 | s := NewScheduler(now) 514 | 515 | s.AddEventGenerators(mockEventGenerator1, mockEventGenerator2) 516 | 517 | require.False(t, s.eventQueue.Finished()) 518 | } 519 | --------------------------------------------------------------------------------