Class: DevDoc::Test::Lints::CronScheduleChecker

Inherits:
Object
  • Object
show all
Defined in:
lib/dev_doc/test/lints/cron_schedule.rb

Overview

Framework-agnostic check: reads a sidekiq-cron-style schedules.yml and returns a list of offender descriptions for any cron expression that fires less often than max_interval_seconds, and optionally checks that each job's staleness threshold is greater than the cron interval.

Wrapped by the Minitest module CronSchedule below — see that module for the rationale and examples. Tests cover the checker directly so they don't have to mock a test framework.

Constant Summary collapse

DEFAULT_MAX_INTERVAL_SECONDS =

1 day. Weekly (and longer) crons are always wrong: no staleness threshold can usefully exceed the cron interval, so a weekly cron can't be paired with a meaningful self-throttle. Daily cron is the most generous interval the pattern supports (works for weekly-desired refreshes like Myki). Hourly+ is always safe.

86_400
MissingFile =

Sentinel returned by #offenders when the schedules file doesn't exist. Distinguishable from [] (file present, all good) so the Minitest wrapper can skip rather than assert.

Module.new
DEFAULT_STALENESS_CONSTANT =

Default constant name to look up in job source files when checking that the staleness threshold is larger than the cron interval.

'STALENESS_THRESHOLD'.freeze
SAMPLE_ANCHOR =

Anchored reference for sampling cron fires. A fixed Monday so weekday-restricted crons behave the same regardless of when the test suite runs.

Time.utc(2025, 1, 6).freeze
SAMPLE_COUNT =

How many consecutive fires to sample. 100 covers:

- sub-hourly crons (max gap surfaces within minutes)
- business-hours patterns like `0 9-17 * * *` (overnight gap
visible after the first day)
- weekly/monthly (max gap visible within a few cycles)
- yearly (still gets ≥2 fires)
100

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schedules_path, max_interval_seconds: DEFAULT_MAX_INTERVAL_SECONDS) ⇒ CronScheduleChecker

Returns a new instance of CronScheduleChecker.



100
101
102
103
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 100

def initialize(schedules_path, max_interval_seconds: DEFAULT_MAX_INTERVAL_SECONDS)
  @schedules_path = schedules_path
  @max_interval_seconds = max_interval_seconds
end

Class Method Details

.format_seconds(seconds) ⇒ Object



146
147
148
149
150
151
152
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 146

def self.format_seconds(seconds)
  return "#{seconds}s" if seconds < 60
  return "#{seconds / 60}m" if seconds < 3600
  return "#{seconds / 3600}h" if seconds < 86_400

  "#{seconds / 86_400}d"
end

Instance Method Details

#offendersObject

Returns an Array of offender descriptions (one per invalid or too-infrequent cron), [] if all crons are compliant, or MissingFile if the schedules file doesn't exist.



109
110
111
112
113
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 109

def offenders
  return MissingFile unless @schedules_path.exist?

  schedule_entries.filter_map { |name, cron, _opts| interval_offense(name, cron) }
end

#staleness_offenders(jobs_path: nil, constant: DEFAULT_STALENESS_CONSTANT) ⇒ Object

Checks that each job's staleness threshold constant is strictly greater than the cron interval. Returns an Array of offender descriptions, [] if all are compliant, or MissingFile if the schedules file doesn't exist.

Parameters:

jobs_path   - Pathname/String pointing to `app/jobs/` (or any
            directory tree to search for job source files).
            If nil or the path doesn't exist, returns `[]`
            (nothing to check — silently skipped so projects
            without job-level constants don't have to opt out).
constant    - Name of the constant to look up (default:
            `STALENESS_THRESHOLD`). Projects that use a
            different convention (e.g. `STALE_AFTER`) can
            override this.

The constant value is extracted by grepping the job source files for the pattern CONSTANT = <number>.<unit> (e.g. 1.hour, 45.minutes). Jobs that use a per-record stale? method instead of a job-level constant produce no constant to inspect and are skipped silently.



135
136
137
138
139
140
141
142
143
144
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 135

def staleness_offenders(jobs_path: nil, constant: DEFAULT_STALENESS_CONSTANT)
  return MissingFile unless @schedules_path.exist?

  jobs_root = jobs_path && Pathname.new(jobs_path)
  return [] unless jobs_root&.exist?

  schedule_entries.filter_map do |name, cron, opts|
    staleness_offense(name, cron, opts['class'], jobs_root, constant)
  end
end