Skip to content
quarry Artifact [9c2634bf0f]
Login

Artifact [9c2634bf0f]

Artifact 9c2634bf0f109536cf78f5d2873567b268bdd12957b6b9a4c7e40b2613fd3732:


// Salvaged-from: github.com/danmestas/dagnats internal/cronexpr/cronexpr_posix_day_test.go @ e1ca6a3 (Apache-2.0, same author)
// cronexpr/cronexpr_posix_day_test.go
// Tests for the POSIX day predicate: day-of-month and day-of-week are
// ORed when both are restricted, and ANDed (degenerately) otherwise.
// Methodology: unit tests over the four restriction combinations, with
// fixed UTC dates whose weekdays are pinned in the comments. No NATS.
package cronexpr

import (
	"testing"
	"time"
)

// Fixed fixtures. 2026-06-01 is a Monday, so it is the only date here
// where the old intersection semantics and the POSIX union semantics
// agree for "0 9 1 * 1" -- every other case is the regression.
var (
	monTheFirst   = time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)  // Mon, day 1
	wedTheFirst   = time.Date(2026, 7, 1, 9, 0, 0, 0, time.UTC)  // Wed, day 1
	monTheSixth   = time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)  // Mon, day 6
	thuTheSecond  = time.Date(2026, 7, 2, 9, 0, 0, 0, time.UTC)  // Thu, day 2
	monWrongHour  = time.Date(2026, 7, 6, 10, 0, 0, 0, time.UTC) // Mon, 10am
	tueTheNinth   = time.Date(2026, 6, 9, 9, 0, 0, 0, time.UTC)  // Tue, day 9
	augTheFirst   = time.Date(2026, 8, 1, 9, 0, 0, 0, time.UTC)  // Sat, day 1
	julyThirdWeek = time.Date(2026, 7, 13, 9, 0, 0, 0, time.UTC) // Mon, day 13
)

// TestDayPredicateBothRestrictedUnions is the issue #552 regression:
// "0 9 1 * 1" must fire on every Monday AND on the 1st of every month,
// not only on Mondays that fall on the 1st.
func TestDayPredicateBothRestrictedUnions(t *testing.T) {
	expr, err := ParseCron("0 9 1 * 1")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}

	// Positive: day-of-month matches, day-of-week does not.
	if !expr.Matches(wedTheFirst) {
		t.Errorf("Wed 2026-07-01 09:00 should match (it is the 1st)")
	}
	// Positive: day-of-week matches, day-of-month does not.
	if !expr.Matches(monTheSixth) {
		t.Errorf("Mon 2026-07-06 09:00 should match (it is a Monday)")
	}
	// Positive: both match.
	if !expr.Matches(monTheFirst) {
		t.Errorf("Mon 2026-06-01 09:00 should match (Monday and the 1st)")
	}
	// Negative: neither day field matches.
	if expr.Matches(thuTheSecond) {
		t.Errorf("Thu 2026-07-02 09:00 should not match (neither field)")
	}
	// Negative: the day predicate does not relax hour/minute -- those
	// stay conjunctive even when the day matches via the union.
	if expr.Matches(monWrongHour) {
		t.Errorf("Mon 2026-07-06 10:00 should not match (wrong hour)")
	}
}

// TestDayPredicateOnlyDayOfMonthRestricted proves the non-diverging case
// is unchanged: with day-of-week "*", only day-of-month constrains.
func TestDayPredicateOnlyDayOfMonthRestricted(t *testing.T) {
	expr, err := ParseCron("0 9 1 * *")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}

	// Positive: the 1st matches on a Wednesday.
	if !expr.Matches(wedTheFirst) {
		t.Errorf("Wed 2026-07-01 should match day-of-month 1")
	}
	// Positive: the 1st matches on a Saturday too -- weekday is irrelevant.
	if !expr.Matches(augTheFirst) {
		t.Errorf("Sat 2026-08-01 should match day-of-month 1")
	}
	// Negative: a Monday that is not the 1st must NOT match. An
	// unrestricted day-of-week must not widen the day predicate.
	if expr.Matches(monTheSixth) {
		t.Errorf("Mon 2026-07-06 should not match day-of-month 1")
	}
	// Negative: any other day-of-month.
	if expr.Matches(thuTheSecond) {
		t.Errorf("Thu 2026-07-02 should not match day-of-month 1")
	}
}

// TestDayPredicateOnlyDayOfWeekRestricted proves the non-diverging case
// is unchanged: with day-of-month "*", only day-of-week constrains.
func TestDayPredicateOnlyDayOfWeekRestricted(t *testing.T) {
	expr, err := ParseCron("0 9 * * 1")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}

	// Positive: a Monday mid-month matches.
	if !expr.Matches(monTheSixth) {
		t.Errorf("Mon 2026-07-06 should match day-of-week 1")
	}
	// Positive: a Monday that is also the 1st matches.
	if !expr.Matches(monTheFirst) {
		t.Errorf("Mon 2026-06-01 should match day-of-week 1")
	}
	// Negative: the 1st on a non-Monday must NOT match. An unrestricted
	// day-of-month must not widen the day predicate.
	if expr.Matches(wedTheFirst) {
		t.Errorf("Wed 2026-07-01 should not match day-of-week 1")
	}
	// Negative: any other weekday.
	if expr.Matches(tueTheNinth) {
		t.Errorf("Tue 2026-06-09 should not match day-of-week 1")
	}
}

// TestDayPredicateNeitherRestricted proves the day predicate is vacuous
// when both day fields are "*".
func TestDayPredicateNeitherRestricted(t *testing.T) {
	expr, err := ParseCron("0 9 * * *")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}

	// Positive: every day at 09:00 matches, whatever its weekday.
	for _, at := range []time.Time{
		monTheFirst, wedTheFirst, monTheSixth, thuTheSecond, tueTheNinth,
	} {
		if !expr.Matches(at) {
			t.Errorf("%v should match with both day fields unrestricted",
				at)
		}
	}
	// Negative: hour still constrains.
	if expr.Matches(monWrongHour) {
		t.Errorf("Mon 2026-07-06 10:00 should not match hour 9")
	}
}

// TestDayPredicateRangeCountsAsRestricted confirms restriction is about
// the field being "*", not about how many values it expands to: "1-31"
// covers every day-of-month yet must still count as restricted, so
// "0 9 1-31 * 1" unions rather than intersects.
func TestDayPredicateRangeCountsAsRestricted(t *testing.T) {
	expr, err := ParseCron("0 9 1-31 * 1")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}

	// Positive: a Thursday matches, because day-of-month 1-31 covers it
	// and the union only needs one side.
	if !expr.Matches(thuTheSecond) {
		t.Errorf("Thu 2026-07-02 should match via day-of-month 1-31")
	}
	// Positive: a Monday matches via day-of-week.
	if !expr.Matches(monTheSixth) {
		t.Errorf("Mon 2026-07-06 should match via day-of-week 1")
	}
}

// TestNextNUsesPosixDayPredicate confirms NextN inherits the new
// semantics -- it drives scheduling and backfill, so a divergence here
// would leave triggers firing on the old intersection.
func TestNextNUsesPosixDayPredicate(t *testing.T) {
	expr, err := ParseCron("0 9 1 * 1")
	if err != nil {
		t.Fatalf("parse: %v", err)
	}

	ref := time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)
	times := expr.NextN(ref, 3)

	// Positive: three fires within two weeks, not one per year.
	if len(times) != 3 {
		t.Fatalf("got %d fire times, want 3", len(times))
	}
	want := []time.Time{wedTheFirst, monTheSixth, julyThirdWeek}
	for i := range want {
		if !times[i].Equal(want[i]) {
			t.Errorf("times[%d] = %v, want %v", i, times[i], want[i])
		}
	}
	// Negative: no fire lands on a day matching neither field.
	for _, at := range times {
		if at.Day() != 1 && at.Weekday() != time.Monday {
			t.Errorf("%v matches neither day-of-month 1 nor Monday", at)
		}
	}
}