Skip to content

Commit ef63dd9

Browse files
committed
test: improve mutation test coverage for boundary conditions
Add targeted tests to kill surviving boundary condition mutations: - Circuit breaker: threshold boundary tests for isHalfOpen/resetOnSuccess - DST normalization: hour==12 boundary and arithmetic tests - Index compaction: exact threshold boundary test Kills mutations at cron.go:820, spec.go:128-142 that were previously surviving. Remaining LIVED mutations are equivalent mutants (logging-only code paths, always-true conditions, intentional randomness).
1 parent 1fbe3d6 commit ef63dd9

File tree

3 files changed

+382
-0
lines changed

3 files changed

+382
-0
lines changed

cron_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,81 @@ func TestIndexCompactionBoundaryDeletionsEqualSize(t *testing.T) {
17241724
}
17251725
}
17261726

1727+
1728+
// TestIndexCompactionBoundaryDeletionsEqualSizeExplicit is a focused test for cron.go:820.
1729+
// This explicitly tests the exact boundary where indexDeletions == currentSize.
1730+
// The mutation `<=` → `<` would incorrectly trigger compaction at this boundary.
1731+
func TestIndexCompactionBoundaryDeletionsEqualSizeExplicit(t *testing.T) {
1732+
c := New()
1733+
defer c.Stop()
1734+
1735+
// Strategy: Create a state where after threshold deletions,
1736+
// indexDeletions == currentSize exactly.
1737+
//
1738+
// We need: total entries = threshold + X, delete threshold entries
1739+
// After threshold deletions: currentSize = X, indexDeletions = threshold
1740+
// For indexDeletions == currentSize, we need X = threshold
1741+
// So total = 2 * threshold
1742+
1743+
total := 2 * indexCompactionThreshold
1744+
ids := make([]EntryID, total)
1745+
1746+
for i := 0; i < total; i++ {
1747+
id, err := c.AddFunc("@every 1s", func() {}, WithName(fmt.Sprintf("explicit-%d", i)))
1748+
if err != nil {
1749+
t.Fatalf("failed to add job %d: %v", i, err)
1750+
}
1751+
ids[i] = id
1752+
}
1753+
1754+
// Verify starting state
1755+
if len(c.entryIndex) != total {
1756+
t.Fatalf("expected %d entries, got %d", total, len(c.entryIndex))
1757+
}
1758+
if c.indexDeletions != 0 {
1759+
t.Fatalf("expected indexDeletions = 0, got %d", c.indexDeletions)
1760+
}
1761+
1762+
// Delete exactly threshold entries
1763+
for i := 0; i < indexCompactionThreshold; i++ {
1764+
c.Remove(ids[i])
1765+
}
1766+
1767+
// After threshold deletions:
1768+
// - currentSize = total - threshold = threshold (1000)
1769+
// - indexDeletions = threshold (1000)
1770+
// - Condition: threshold >= threshold (true) && currentSize > 0 (true) && deletions <= size (1000 <= 1000 = true)
1771+
// - With <= true: return early, no compaction
1772+
// - With mutation < false: don't return, do compact, reset indexDeletions to 0
1773+
1774+
currentSize := len(c.entryIndex)
1775+
if currentSize != indexCompactionThreshold {
1776+
t.Errorf("expected currentSize = %d, got %d", indexCompactionThreshold, currentSize)
1777+
}
1778+
1779+
// CRITICAL ASSERTION: indexDeletions should equal threshold (no compaction)
1780+
// With mutation, it would be 0 (after compaction)
1781+
if c.indexDeletions != indexCompactionThreshold {
1782+
t.Errorf("MUTATION DETECTED: indexDeletions should be %d (no compaction at equality boundary), got %d. "+
1783+
"If this is 0, the `<=` was mutated to `<` at cron.go:820",
1784+
indexCompactionThreshold, c.indexDeletions)
1785+
}
1786+
1787+
// Additional verification: deleting one more should trigger compaction
1788+
c.Remove(ids[indexCompactionThreshold])
1789+
1790+
// After one more deletion:
1791+
// - currentSize = threshold - 1 (999)
1792+
// - indexDeletions would be threshold + 1 (1001) before compaction check
1793+
// - Condition: 1001 >= 1000 (true) && 999 > 0 (true) && 1001 <= 999 (false)
1794+
// - Since condition is false, compaction triggers, indexDeletions resets to 0
1795+
1796+
if c.indexDeletions != 0 {
1797+
t.Errorf("expected indexDeletions = 0 after exceeding boundary (compaction should trigger), got %d",
1798+
c.indexDeletions)
1799+
}
1800+
}
1801+
17271802
func TestScheduleJobWithOptions(t *testing.T) {
17281803
c := New()
17291804
defer c.Stop()

retry_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,159 @@ func TestCircuitBreaker_IntegrationWithCron(t *testing.T) {
472472
t.Errorf("expected 2 executions (circuit should open), got %d", got)
473473
}
474474
}
475+
476+
477+
// TestCircuitBreaker_BoundaryThresholdExact tests retry.go:267 (isHalfOpen) boundary condition.
478+
// This kills CONDITIONALS_BOUNDARY mutation where `>= threshold` could become `> threshold`.
479+
// When failures == threshold exactly, isHalfOpen should return true.
480+
func TestCircuitBreaker_BoundaryThresholdExact(t *testing.T) {
481+
var executions int32
482+
threshold := 2 // Use small threshold for clarity
483+
484+
wrapped := CircuitBreaker(DiscardLogger, threshold, time.Hour)(
485+
FuncJob(func() {
486+
atomic.AddInt32(&executions, 1)
487+
panic("always fails")
488+
}),
489+
)
490+
491+
// Fail exactly `threshold` times (2 times)
492+
for i := 0; i < threshold; i++ {
493+
func() {
494+
defer func() { recover() }()
495+
wrapped.Run()
496+
}()
497+
}
498+
499+
// At this point: failures == threshold (2 == 2)
500+
// Circuit should be OPEN (isHalfOpen returns true for failures >= threshold)
501+
// With mutation >= → >: isHalfOpen would return false (2 > 2 is false)
502+
// and circuit would incorrectly allow execution
503+
504+
execsBefore := atomic.LoadInt32(&executions)
505+
506+
// This run should be SKIPPED if circuit is correctly open
507+
wrapped.Run()
508+
509+
execsAfter := atomic.LoadInt32(&executions)
510+
511+
// If circuit is correctly open, executions should NOT increase
512+
if execsAfter != execsBefore {
513+
t.Errorf("circuit should be open at exact threshold: failures=%d, threshold=%d, "+
514+
"expected executions to stay at %d, got %d",
515+
threshold, threshold, execsBefore, execsAfter)
516+
}
517+
518+
// Verify we had exactly threshold executions before circuit opened
519+
if execsBefore != int32(threshold) {
520+
t.Errorf("expected %d executions before circuit opened, got %d", threshold, execsBefore)
521+
}
522+
}
523+
524+
// TestCircuitBreaker_ResetOnSuccessBoundary tests retry.go:282 (resetOnSuccess) boundary condition.
525+
// This kills CONDITIONALS_BOUNDARY mutation where `>= threshold` could become `> threshold`.
526+
// When failures == threshold, wasOpen should return true on successful reset.
527+
func TestCircuitBreaker_ResetOnSuccessBoundary(t *testing.T) {
528+
var executions int32
529+
var shouldFail atomic.Bool
530+
threshold := 2
531+
cooldown := 50 * time.Millisecond
532+
533+
shouldFail.Store(true)
534+
535+
wrapped := CircuitBreaker(DiscardLogger, threshold, cooldown)(
536+
FuncJob(func() {
537+
atomic.AddInt32(&executions, 1)
538+
if shouldFail.Load() {
539+
panic("controlled failure")
540+
}
541+
}),
542+
)
543+
544+
// Fail exactly threshold times to open circuit
545+
for i := 0; i < threshold; i++ {
546+
func() {
547+
defer func() { recover() }()
548+
wrapped.Run()
549+
}()
550+
}
551+
552+
// At this point: failures == threshold (circuit open)
553+
// Wait for cooldown to allow half-open state
554+
time.Sleep(cooldown + 10*time.Millisecond)
555+
556+
// Set up to succeed
557+
shouldFail.Store(false)
558+
559+
execsBefore := atomic.LoadInt32(&executions)
560+
561+
// Execute in half-open state - should succeed and reset circuit
562+
wrapped.Run()
563+
564+
execsAfter := atomic.LoadInt32(&executions)
565+
566+
// Execution should have happened (half-open allows one attempt)
567+
if execsAfter != execsBefore+1 {
568+
t.Errorf("half-open state should allow execution: expected %d, got %d",
569+
execsBefore+1, execsAfter)
570+
}
571+
572+
// Now circuit should be closed, run again to verify
573+
wrapped.Run()
574+
575+
execsFinal := atomic.LoadInt32(&executions)
576+
if execsFinal != execsAfter+1 {
577+
t.Errorf("circuit should be closed after successful reset: expected %d, got %d",
578+
execsAfter+1, execsFinal)
579+
}
580+
}
581+
582+
// TestCircuitBreaker_IsOpenBoundary tests retry.go:259 (isOpen) at exact threshold.
583+
// Combined with cooldown to verify open state detection at boundary.
584+
func TestCircuitBreaker_IsOpenBoundary(t *testing.T) {
585+
var executions int32
586+
threshold := 3
587+
cooldown := 100 * time.Millisecond
588+
589+
wrapped := CircuitBreaker(DiscardLogger, threshold, cooldown)(
590+
FuncJob(func() {
591+
atomic.AddInt32(&executions, 1)
592+
panic("always fails")
593+
}),
594+
)
595+
596+
// Fail exactly threshold times
597+
for i := 0; i < threshold; i++ {
598+
func() {
599+
defer func() { recover() }()
600+
wrapped.Run()
601+
}()
602+
}
603+
604+
// Verify exactly threshold executions happened
605+
if got := atomic.LoadInt32(&executions); got != int32(threshold) {
606+
t.Fatalf("expected %d executions, got %d", threshold, got)
607+
}
608+
609+
// Circuit should be open - next call should be skipped (within cooldown)
610+
wrapped.Run()
611+
612+
if got := atomic.LoadInt32(&executions); got != int32(threshold) {
613+
t.Errorf("circuit should be open at exact threshold, executions should stay at %d, got %d",
614+
threshold, got)
615+
}
616+
617+
// Wait past cooldown - half-open state should allow one execution
618+
time.Sleep(cooldown + 20*time.Millisecond)
619+
620+
func() {
621+
defer func() { recover() }()
622+
wrapped.Run()
623+
}()
624+
625+
// Should have one more execution (half-open attempt)
626+
if got := atomic.LoadInt32(&executions); got != int32(threshold)+1 {
627+
t.Errorf("half-open state should allow execution, expected %d, got %d",
628+
threshold+1, got)
629+
}
630+
}

0 commit comments

Comments
 (0)