summaryrefslogtreecommitdiff
path: root/internal/server/continuous_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/server/continuous_test.go')
-rw-r--r--internal/server/continuous_test.go202
1 files changed, 202 insertions, 0 deletions
diff --git a/internal/server/continuous_test.go b/internal/server/continuous_test.go
new file mode 100644
index 0000000..12d9f18
--- /dev/null
+++ b/internal/server/continuous_test.go
@@ -0,0 +1,202 @@
+package server
+
+import (
+ "context"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/mimecast/dtail/internal/clients"
+ "github.com/mimecast/dtail/internal/config"
+ "github.com/mimecast/dtail/internal/io/dlog"
+)
+
+func TestSameCalendarDay(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ a time.Time
+ b time.Time
+ want bool
+ }{
+ {
+ name: "same day",
+ a: time.Date(2026, time.January, 15, 10, 0, 0, 0, time.UTC),
+ b: time.Date(2026, time.January, 15, 23, 59, 59, 0, time.UTC),
+ want: true,
+ },
+ {
+ name: "same day-of-month in different months",
+ a: time.Date(2026, time.January, 15, 10, 0, 0, 0, time.UTC),
+ b: time.Date(2026, time.February, 15, 10, 0, 0, 0, time.UTC),
+ want: false,
+ },
+ {
+ name: "same day-of-month across years",
+ a: time.Date(2025, time.December, 31, 10, 0, 0, 0, time.UTC),
+ b: time.Date(2026, time.January, 31, 10, 0, 0, 0, time.UTC),
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ if got := sameCalendarDay(tt.a, tt.b); got != tt.want {
+ t.Fatalf("sameCalendarDay(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestContinuousRunJobsReleasesDayChangeWatcherAcrossRetries(t *testing.T) {
+ dlog.Server = &dlog.DLog{}
+
+ c := newContinuous(config.RuntimeConfig{
+ Server: &config.ServerConfig{
+ SSHBindAddress: "127.0.0.1",
+ },
+ })
+ c.retryInterval = 25 * time.Millisecond
+
+ var watcherStarts int32
+ var watcherExits int32
+ started := make(chan struct{}, 1)
+ release := make(chan struct{}, 1)
+ c.newMaprClient = func(args config.Args, mode clients.MaprClientMode) (continuousClient, error) {
+ return blockingContinuousClient{
+ started: started,
+ release: release,
+ }, nil
+ }
+ c.dayChangeWatcher = func(ctx context.Context) bool {
+ atomic.AddInt32(&watcherStarts, 1)
+ defer atomic.AddInt32(&watcherExits, 1)
+ return c.waitForDayChange(ctx)
+ }
+
+ job := config.Continuous{}
+ job.Enable = true
+ job.RestartOnDayChange = true
+ c.cfg.Server.Continuous = []config.Continuous{job}
+
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan struct{})
+ go func() {
+ c.runJobs(ctx)
+ close(done)
+ }()
+
+ for i := int32(1); i <= 5; i++ {
+ select {
+ case <-started:
+ case <-time.After(2 * time.Second):
+ t.Fatalf("timed out waiting for retry %d to start", i)
+ }
+
+ waitForCounterAtLeast(t, func() int32 {
+ return atomic.LoadInt32(&watcherStarts)
+ }, i)
+
+ release <- struct{}{}
+
+ waitForCounterAtLeast(t, func() int32 {
+ return atomic.LoadInt32(&watcherExits)
+ }, i)
+ }
+
+ cancel()
+
+ select {
+ case <-done:
+ case <-time.After(2 * time.Second):
+ t.Fatal("continuous job runner did not stop after cancellation")
+ }
+}
+
+func TestContinuousWaitForDayChangeDetectsMonthBoundary(t *testing.T) {
+ dlog.Server = &dlog.DLog{}
+
+ c := newContinuous(config.RuntimeConfig{})
+
+ start := time.Date(2026, time.January, 31, 23, 59, 59, 0, time.UTC)
+ sameDay := time.Date(2026, time.January, 31, 23, 59, 59, 500_000_000, time.UTC)
+ nextDay := time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC)
+
+ var nowCalls int32
+ c.now = func() time.Time {
+ switch atomic.AddInt32(&nowCalls, 1) {
+ case 1:
+ return start
+ case 2:
+ return sameDay
+ default:
+ return nextDay
+ }
+ }
+
+ tickCh := make(chan time.Time, 2)
+ c.newTicker = func(time.Duration) (<-chan time.Time, func()) {
+ return tickCh, func() {}
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ result := make(chan bool, 1)
+ go func() {
+ result <- c.waitForDayChange(ctx)
+ }()
+
+ tickCh <- start
+ select {
+ case got := <-result:
+ t.Fatalf("waitForDayChange returned after same-day tick: %v", got)
+ case <-time.After(100 * time.Millisecond):
+ }
+
+ tickCh <- nextDay
+ select {
+ case got := <-result:
+ if !got {
+ t.Fatal("waitForDayChange returned false after the month boundary tick")
+ }
+ case <-time.After(2 * time.Second):
+ t.Fatal("timed out waiting for waitForDayChange to detect the month boundary")
+ }
+}
+
+type blockingContinuousClient struct {
+ started chan<- struct{}
+ release <-chan struct{}
+}
+
+func (f blockingContinuousClient) Start(context.Context, <-chan string) int {
+ f.started <- struct{}{}
+ <-f.release
+ return 0
+}
+
+func waitForCounterAtLeast(t *testing.T, current func() int32, min int32) {
+ t.Helper()
+
+ deadline := time.NewTimer(2 * time.Second)
+ defer deadline.Stop()
+
+ ticker := time.NewTicker(10 * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ if current() >= min {
+ return
+ }
+
+ select {
+ case <-deadline.C:
+ t.Fatalf("timed out waiting for counter to reach %d, got %d", min, current())
+ case <-ticker.C:
+ }
+ }
+}