wheneverd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rubocop.yml +41 -0
  4. data/.yardopts +8 -0
  5. data/AGENTS.md +42 -0
  6. data/CHANGELOG.md +28 -0
  7. data/FEATURE_SUMMARY.md +38 -0
  8. data/Gemfile +16 -0
  9. data/Gemfile.lock +129 -0
  10. data/LICENSE +21 -0
  11. data/README.md +204 -0
  12. data/Rakefile +196 -0
  13. data/bin/console +8 -0
  14. data/bin/setup +5 -0
  15. data/exe/wheneverd +9 -0
  16. data/lib/wheneverd/cli/activate.rb +19 -0
  17. data/lib/wheneverd/cli/current.rb +22 -0
  18. data/lib/wheneverd/cli/deactivate.rb +19 -0
  19. data/lib/wheneverd/cli/delete.rb +20 -0
  20. data/lib/wheneverd/cli/help.rb +18 -0
  21. data/lib/wheneverd/cli/init.rb +78 -0
  22. data/lib/wheneverd/cli/reload.rb +40 -0
  23. data/lib/wheneverd/cli/show.rb +23 -0
  24. data/lib/wheneverd/cli/write.rb +32 -0
  25. data/lib/wheneverd/cli.rb +87 -0
  26. data/lib/wheneverd/core_ext/numeric_duration.rb +56 -0
  27. data/lib/wheneverd/dsl/at_normalizer.rb +48 -0
  28. data/lib/wheneverd/dsl/calendar_symbol_period_list.rb +42 -0
  29. data/lib/wheneverd/dsl/context.rb +72 -0
  30. data/lib/wheneverd/dsl/errors.rb +29 -0
  31. data/lib/wheneverd/dsl/loader.rb +49 -0
  32. data/lib/wheneverd/dsl/period_parser.rb +135 -0
  33. data/lib/wheneverd/duration.rb +27 -0
  34. data/lib/wheneverd/entry.rb +31 -0
  35. data/lib/wheneverd/errors.rb +9 -0
  36. data/lib/wheneverd/interval.rb +37 -0
  37. data/lib/wheneverd/job/command.rb +29 -0
  38. data/lib/wheneverd/schedule.rb +25 -0
  39. data/lib/wheneverd/systemd/calendar_spec.rb +109 -0
  40. data/lib/wheneverd/systemd/cron_parser.rb +352 -0
  41. data/lib/wheneverd/systemd/errors.rb +23 -0
  42. data/lib/wheneverd/systemd/renderer.rb +153 -0
  43. data/lib/wheneverd/systemd/systemctl.rb +38 -0
  44. data/lib/wheneverd/systemd/time_parser.rb +75 -0
  45. data/lib/wheneverd/systemd/unit_deleter.rb +64 -0
  46. data/lib/wheneverd/systemd/unit_lister.rb +59 -0
  47. data/lib/wheneverd/systemd/unit_namer.rb +69 -0
  48. data/lib/wheneverd/systemd/unit_writer.rb +132 -0
  49. data/lib/wheneverd/trigger/boot.rb +26 -0
  50. data/lib/wheneverd/trigger/calendar.rb +26 -0
  51. data/lib/wheneverd/trigger/interval.rb +30 -0
  52. data/lib/wheneverd/version.rb +6 -0
  53. data/lib/wheneverd.rb +41 -0
  54. data/test/cli_activate_test.rb +110 -0
  55. data/test/cli_current_test.rb +94 -0
  56. data/test/cli_deactivate_test.rb +111 -0
  57. data/test/cli_end_to_end_test.rb +98 -0
  58. data/test/cli_reload_test.rb +132 -0
  59. data/test/cli_systemctl_integration_test.rb +76 -0
  60. data/test/cli_systemd_analyze_test.rb +64 -0
  61. data/test/cli_test.rb +332 -0
  62. data/test/domain_model_test.rb +108 -0
  63. data/test/dsl_calendar_symbol_period_list_test.rb +53 -0
  64. data/test/dsl_loader_test.rb +384 -0
  65. data/test/support/cli_subprocess_test_helpers.rb +38 -0
  66. data/test/support/cli_test_helpers.rb +114 -0
  67. data/test/systemd_calendar_spec_test.rb +45 -0
  68. data/test/systemd_cron_parser_test.rb +114 -0
  69. data/test/systemd_renderer_errors_test.rb +85 -0
  70. data/test/systemd_renderer_test.rb +161 -0
  71. data/test/systemd_systemctl_test.rb +46 -0
  72. data/test/systemd_time_parser_test.rb +25 -0
  73. data/test/systemd_unit_deleter_test.rb +83 -0
  74. data/test/systemd_unit_writer_prune_test.rb +85 -0
  75. data/test/systemd_unit_writer_test.rb +71 -0
  76. data/test/test_helper.rb +34 -0
  77. data/test/wheneverd_test.rb +9 -0
  78. data/wheneverd.gemspec +35 -0
  79. metadata +136 -0
data/test/cli_test.rb ADDED
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require_relative "support/cli_test_helpers"
5
+
6
+ class CLIHelpAndVersionTest < Minitest::Test
7
+ include CLITestHelpers
8
+
9
+ def test_help_prints_usage_and_exits_zero
10
+ out, err = capture_io { Wheneverd::CLI.run("wheneverd", ["--help"]) }
11
+ assert_includes out, "Usage:"
12
+ assert_includes out, "wheneverd [OPTIONS]"
13
+ assert_equal "", err
14
+ end
15
+
16
+ def test_version_prints_version_and_exits_zero
17
+ status, out, err = run_cli(["--version"])
18
+ assert_equal 0, status
19
+ assert_includes out, Wheneverd::VERSION
20
+ assert_equal "", err
21
+ end
22
+
23
+ def test_no_args_exits_one_and_prints_help_to_stderr
24
+ status, out, err = run_cli([])
25
+ assert_equal 1, status
26
+ assert_equal "", out
27
+ assert_includes err, "Usage:"
28
+ assert_includes err, "wheneverd [OPTIONS]"
29
+ end
30
+
31
+ def test_invalid_option_exits_one_and_prints_error
32
+ out, err = capture_io do
33
+ exit_error = assert_raises(SystemExit) do
34
+ Wheneverd::CLI.run("wheneverd", ["--definitely-not-a-real-flag"])
35
+ end
36
+
37
+ assert_equal 1, exit_error.status
38
+ end
39
+
40
+ assert_equal "", out
41
+ assert_includes err, "ERROR:"
42
+ assert_includes err, "--definitely-not-a-real-flag"
43
+ assert_includes err, "See:"
44
+ end
45
+ end
46
+
47
+ class CLIInitTest < Minitest::Test
48
+ include CLITestHelpers
49
+
50
+ def test_init_creates_schedule_file
51
+ with_project_dir do
52
+ status, out, err = run_cli(["init"])
53
+ assert_equal 0, status
54
+ assert_includes out, "Wrote schedule template"
55
+ assert_equal "", err
56
+ assert File.file?(File.join("config", "schedule.rb"))
57
+ end
58
+ end
59
+
60
+ def test_init_refuses_overwrite_without_force
61
+ with_project_dir do
62
+ path = File.join("config", "schedule.rb")
63
+ FileUtils.mkdir_p(File.dirname(path))
64
+ File.write(path, "# existing\n")
65
+
66
+ status, out, err = run_cli(["init"])
67
+ assert_equal 1, status
68
+ assert_equal "", out
69
+ assert_includes err, "already exists"
70
+ end
71
+ end
72
+
73
+ def test_init_overwrites_with_force
74
+ with_project_dir do
75
+ path = File.join("config", "schedule.rb")
76
+ FileUtils.mkdir_p(File.dirname(path))
77
+ File.write(path, "# existing\n")
78
+
79
+ status, out, err = run_cli(["init", "--force"])
80
+ assert_equal 0, status
81
+ assert_includes out, "Overwrote schedule template"
82
+ assert_equal "", err
83
+ assert_includes File.read(path), "every \"5m\""
84
+ end
85
+ end
86
+
87
+ def test_init_handles_filesystem_errors
88
+ with_project_dir do
89
+ FileUtils.mkdir_p("config")
90
+ status, out, err = run_cli(["init", "--schedule", "config", "--force"])
91
+ assert_equal 1, status
92
+ assert_equal "", out
93
+ assert_includes err, "Is a directory"
94
+ end
95
+ end
96
+
97
+ def test_init_verbose_includes_error_details
98
+ with_project_dir do
99
+ FileUtils.mkdir_p("config")
100
+ status, out, err = run_cli(["init", "--schedule", "config", "--force", "--verbose"])
101
+ assert_equal 1, status
102
+ assert_equal "", out
103
+ assert_includes err, "Is a directory"
104
+ assert_includes err, "lib/wheneverd/cli/init.rb"
105
+ end
106
+ end
107
+ end
108
+
109
+ class CLIShowTest < Minitest::Test
110
+ include CLITestHelpers
111
+
112
+ EXPECTED_SHOW_OUTPUT_SNIPPETS = [
113
+ "OnActiveSec=300",
114
+ "OnUnitActiveSec=300",
115
+ "OnCalendar=hourly",
116
+ "OnCalendar=*-*-27..31 00:00:00",
117
+ "ExecStart=echo hello"
118
+ ].freeze
119
+
120
+ def test_show_renders_units_to_stdout
121
+ with_project_dir do
122
+ assert_equal 0, run_cli(["init"]).first
123
+ status, out, err = run_cli(["show", "--identifier", "demo"])
124
+
125
+ assert_equal 0, status
126
+ assert_equal "", err
127
+ EXPECTED_SHOW_OUTPUT_SNIPPETS.each { |expected| assert_includes out, expected }
128
+ end
129
+ end
130
+
131
+ def test_show_reports_missing_schedule_file
132
+ with_project_dir do
133
+ status, out, err = run_cli(["show"])
134
+ assert_equal 1, status
135
+ assert_equal "", out
136
+ assert_includes err, "Schedule file not found"
137
+ assert_includes err, "config/schedule.rb"
138
+ end
139
+ end
140
+ end
141
+
142
+ class CLIWriteTest < Minitest::Test
143
+ include CLITestHelpers
144
+
145
+ def test_write_creates_unit_files_in_unit_dir
146
+ with_project_dir do |project_dir|
147
+ unit_dir = File.join(project_dir, "tmp_units")
148
+ assert_equal 0, run_cli(["init"]).first
149
+ status, out, err = run_write(unit_dir)
150
+
151
+ assert_cli_success(status, err)
152
+ assert_first_job_written(unit_dir, out)
153
+ end
154
+ end
155
+
156
+ def test_write_dry_run_does_not_create_files
157
+ with_project_dir do |project_dir|
158
+ unit_dir = File.join(project_dir, "tmp_units")
159
+ assert_equal 0, run_cli(["init"]).first
160
+ status, out, err = run_write(unit_dir, "--dry-run")
161
+
162
+ assert_equal 0, status
163
+ assert_equal "", err
164
+ assert_includes out, File.join(unit_dir, expected_timer_basenames.fetch(0))
165
+ refute Dir.exist?(unit_dir)
166
+ end
167
+ end
168
+
169
+ def test_write_prunes_old_units_by_default
170
+ with_project_dir do |project_dir|
171
+ unit_dir = File.join(project_dir, "tmp_units")
172
+ units1 = write_schedule_and_expected_units(schedule_with_two_jobs)
173
+ write_and_assert_success(unit_dir)
174
+ assert_unit_files_present(unit_dir, units1)
175
+ units2 = write_schedule_and_expected_units(schedule_with_one_job)
176
+ write_and_assert_success(unit_dir)
177
+ assert_unit_files_pruned(unit_dir, units_before: units1, units_after: units2)
178
+ end
179
+ end
180
+
181
+ def test_write_no_prune_keeps_old_units
182
+ with_project_dir do |project_dir|
183
+ unit_dir = File.join(project_dir, "tmp_units")
184
+ units1 = write_schedule_and_expected_units(schedule_with_two_jobs)
185
+
186
+ status, _out, err = run_write(unit_dir)
187
+ assert_cli_success(status, err)
188
+
189
+ write_schedule(schedule_with_one_job)
190
+ status, _out, err = run_write(unit_dir, "--no-prune")
191
+ assert_cli_success(status, err)
192
+
193
+ assert_unit_files_present(unit_dir, units1)
194
+ end
195
+ end
196
+
197
+ def test_write_reports_missing_schedule_file
198
+ with_project_dir do |project_dir|
199
+ unit_dir = File.join(project_dir, "tmp_units")
200
+ status, out, err = run_cli(["write", "--unit-dir", unit_dir])
201
+ assert_equal 1, status
202
+ assert_equal "", out
203
+ assert_includes err, "Schedule file not found"
204
+ end
205
+ end
206
+
207
+ def write_schedule(contents)
208
+ path = File.join("config", "schedule.rb")
209
+ FileUtils.mkdir_p(File.dirname(path))
210
+ File.write(path, contents)
211
+ end
212
+
213
+ def schedule_with_two_jobs
214
+ <<~RUBY
215
+ # frozen_string_literal: true
216
+
217
+ every "1m" do
218
+ command "echo a"
219
+ end
220
+
221
+ every "2m" do
222
+ command "echo b"
223
+ end
224
+ RUBY
225
+ end
226
+
227
+ def schedule_with_one_job
228
+ <<~RUBY
229
+ # frozen_string_literal: true
230
+
231
+ every "1m" do
232
+ command "echo a"
233
+ end
234
+ RUBY
235
+ end
236
+
237
+ private
238
+
239
+ def run_write(unit_dir, *extra_args)
240
+ run_cli(["write", "--identifier", "demo", "--unit-dir", unit_dir, *extra_args])
241
+ end
242
+
243
+ def assert_first_job_written(unit_dir, out)
244
+ assert_includes out, File.join(unit_dir, expected_service_basenames.fetch(0))
245
+ assert File.exist?(File.join(unit_dir, expected_timer_basenames.fetch(0)))
246
+ end
247
+
248
+ def write_schedule_and_expected_units(contents)
249
+ write_schedule(contents)
250
+ expected_units(identifier: "demo")
251
+ end
252
+
253
+ def assert_unit_files_present(unit_dir, units)
254
+ units.each { |unit| assert File.exist?(File.join(unit_dir, unit.path_basename)) }
255
+ end
256
+
257
+ def assert_unit_files_pruned(unit_dir, units_before:, units_after:)
258
+ keep = units_after.map(&:path_basename)
259
+ stale = units_before.map(&:path_basename) - keep
260
+
261
+ keep.each { |basename| assert File.exist?(File.join(unit_dir, basename)) }
262
+ stale.each { |basename| refute File.exist?(File.join(unit_dir, basename)) }
263
+ end
264
+
265
+ def write_and_assert_success(unit_dir, *extra_args)
266
+ status, _out, err = run_write(unit_dir, *extra_args)
267
+ assert_cli_success(status, err)
268
+ end
269
+ end
270
+
271
+ class CLIDeleteTest < Minitest::Test
272
+ include CLITestHelpers
273
+
274
+ def test_delete_removes_units_for_identifier
275
+ with_project_dir do |project_dir|
276
+ unit_dir = File.join(project_dir, "tmp_units")
277
+ init_template_schedule
278
+ write_demo_units(unit_dir)
279
+
280
+ status, out, err = delete_demo_units(unit_dir)
281
+ assert_cli_success(status, err)
282
+ assert_demo_timer_deleted(unit_dir, out)
283
+ end
284
+ end
285
+
286
+ def test_delete_reports_invalid_identifier
287
+ with_project_dir do |project_dir|
288
+ unit_dir = File.join(project_dir, "tmp_units")
289
+ FileUtils.mkdir_p(unit_dir)
290
+
291
+ status, out, err = run_cli(["delete", "--identifier", "!!!", "--unit-dir", unit_dir])
292
+ assert_equal 1, status
293
+ assert_equal "", out
294
+ assert_includes err, "identifier must include at least one alphanumeric character"
295
+ end
296
+ end
297
+
298
+ private
299
+
300
+ def init_template_schedule
301
+ assert_equal 0, run_cli(["init"]).first
302
+ end
303
+
304
+ def write_demo_units(unit_dir)
305
+ assert_equal 0, run_cli(["write", "--identifier", "demo", "--unit-dir", unit_dir]).first
306
+ end
307
+
308
+ def delete_demo_units(unit_dir)
309
+ run_cli(["delete", "--identifier", "demo", "--unit-dir", unit_dir])
310
+ end
311
+
312
+ def assert_demo_timer_deleted(unit_dir, out)
313
+ expected_timer = expected_timer_basenames.fetch(0)
314
+ expected_timer_path = File.join(unit_dir, expected_timer)
315
+ assert_includes out, expected_timer_path
316
+ refute File.exist?(expected_timer_path)
317
+ end
318
+ end
319
+
320
+ class CLIVerboseTest < Minitest::Test
321
+ include CLITestHelpers
322
+
323
+ def test_verbose_prints_error_details
324
+ with_project_dir do
325
+ status, out, err = run_cli(["show", "--verbose"])
326
+ assert_equal 1, status
327
+ assert_equal "", out
328
+ assert_includes err, "Schedule file not found"
329
+ assert_includes err, "lib/wheneverd/cli.rb"
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class DomainModelTest < Minitest::Test
6
+ def test_interval_parses_supported_units
7
+ assert_equal 30, Wheneverd::Interval.parse("30s")
8
+ assert_equal 300, Wheneverd::Interval.parse("5m")
9
+ assert_equal 3600, Wheneverd::Interval.parse("1h")
10
+ assert_equal 86_400, Wheneverd::Interval.parse("1d")
11
+ assert_equal 604_800, Wheneverd::Interval.parse("1w")
12
+ end
13
+
14
+ def test_interval_rejects_invalid_inputs
15
+ error = assert_raises(Wheneverd::InvalidIntervalError) { Wheneverd::Interval.parse("abc") }
16
+ assert_includes error.message, "Invalid interval"
17
+
18
+ assert_raises(Wheneverd::InvalidIntervalError) { Wheneverd::Interval.parse("0m") }
19
+ assert_raises(Wheneverd::InvalidIntervalError) { Wheneverd::Interval.parse("-5m") }
20
+ assert_raises(Wheneverd::InvalidIntervalError) { Wheneverd::Interval.parse("5") }
21
+ assert_raises(Wheneverd::InvalidIntervalError) { Wheneverd::Interval.parse("5mm") }
22
+ assert_raises(Wheneverd::InvalidIntervalError) { Wheneverd::Interval.parse(nil) }
23
+ end
24
+
25
+ def test_duration_wraps_seconds
26
+ duration = Wheneverd::Duration.new(42)
27
+ assert_equal 42, duration.seconds
28
+ assert_equal 42, duration.to_i
29
+
30
+ assert_raises(ArgumentError) { Wheneverd::Duration.new(1.0) }
31
+ assert_raises(ArgumentError) { Wheneverd::Duration.new(0) }
32
+ end
33
+
34
+ def test_numeric_duration_helpers_singular
35
+ assert_equal 1, 1.second.to_i
36
+ assert_equal 60, 1.minute.to_i
37
+ assert_equal 3600, 1.hour.to_i
38
+ assert_equal 86_400, 1.day.to_i
39
+ assert_equal 604_800, 1.week.to_i
40
+ end
41
+
42
+ def test_numeric_duration_helpers_plural_seconds_and_minutes
43
+ assert_equal 2, 2.seconds.to_i
44
+ assert_equal 120, 2.minutes.to_i
45
+ end
46
+
47
+ def test_numeric_duration_helpers_plural_hours_days_weeks
48
+ assert_equal 7200, 2.hours.to_i
49
+ assert_equal 172_800, 2.days.to_i
50
+ assert_equal 1_209_600, 2.weeks.to_i
51
+
52
+ error = assert_raises(ArgumentError) { 1.5.hours }
53
+ assert_includes error.message, "Integer receiver"
54
+ end
55
+
56
+ def test_job_command_validates_command
57
+ command = Wheneverd::Job::Command.new(command: " echo hello ")
58
+ assert_equal "echo hello", command.command
59
+
60
+ assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: "") }
61
+ assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: " ") }
62
+
63
+ error = assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: 123) }
64
+ assert_includes error.message, "String"
65
+ end
66
+
67
+ def test_triggers_render_timer_lines
68
+ interval = Wheneverd::Trigger::Interval.new(seconds: 60)
69
+ assert_equal ["OnActiveSec=60", "OnUnitActiveSec=60"], interval.systemd_timer_lines
70
+
71
+ boot = Wheneverd::Trigger::Boot.new(seconds: 5)
72
+ assert_equal ["OnBootSec=5"], boot.systemd_timer_lines
73
+
74
+ calendar = Wheneverd::Trigger::Calendar.new(on_calendar: %w[hourly daily])
75
+ assert_equal ["OnCalendar=hourly", "OnCalendar=daily"], calendar.systemd_timer_lines
76
+ end
77
+
78
+ def test_trigger_validations
79
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Interval.new(seconds: "60") }
80
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Interval.new(seconds: 0) }
81
+
82
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Boot.new(seconds: "1") }
83
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Boot.new(seconds: 0) }
84
+
85
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Calendar.new(on_calendar: []) }
86
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Calendar.new(on_calendar: [" "]) }
87
+ assert_raises(ArgumentError) { Wheneverd::Trigger::Calendar.new(on_calendar: "hourly") }
88
+ end
89
+
90
+ def test_entry_is_an_ordered_container
91
+ entry = Wheneverd::Entry.new(trigger: Wheneverd::Trigger::Interval.new(seconds: 60))
92
+ assert_equal [], entry.jobs
93
+ entry.add_job(Wheneverd::Job::Command.new(command: "echo hi"))
94
+ assert_equal 1, entry.jobs.length
95
+ end
96
+
97
+ def test_schedule_is_an_ordered_container
98
+ schedule = Wheneverd::Schedule.new
99
+ assert_equal [], schedule.entries
100
+ entry = Wheneverd::Entry.new(trigger: Wheneverd::Trigger::Interval.new(seconds: 60))
101
+ schedule.add_entry(entry)
102
+ assert_equal [entry], schedule.entries
103
+ end
104
+
105
+ def test_entry_requires_a_trigger
106
+ assert_raises(ArgumentError) { Wheneverd::Entry.new(trigger: nil) }
107
+ end
108
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class DSLCalendarSymbolPeriodListTest < Minitest::Test
6
+ def test_validate_raises_when_not_array
7
+ error = assert_raises(Wheneverd::DSL::InvalidPeriodError) do
8
+ Wheneverd::DSL::CalendarSymbolPeriodList.validate(
9
+ "day",
10
+ allowed_symbols: [:day],
11
+ path: "x.rb"
12
+ )
13
+ end
14
+ assert_includes error.message, "non-empty Array"
15
+ end
16
+
17
+ def test_validate_raises_when_empty_array
18
+ error = assert_raises(Wheneverd::DSL::InvalidPeriodError) do
19
+ Wheneverd::DSL::CalendarSymbolPeriodList.validate(
20
+ [],
21
+ allowed_symbols: [:day],
22
+ path: "x.rb"
23
+ )
24
+ end
25
+ assert_includes error.message, "non-empty Array"
26
+ end
27
+
28
+ def test_validate_raises_when_array_contains_non_symbols
29
+ error = assert_raises(Wheneverd::DSL::InvalidPeriodError) do
30
+ Wheneverd::DSL::CalendarSymbolPeriodList.validate([:day, "hour"], allowed_symbols: [:day],
31
+ path: "x.rb")
32
+ end
33
+ assert_includes error.message, "must be Symbols"
34
+ end
35
+
36
+ def test_validate_raises_when_symbols_are_unknown
37
+ error = assert_raises(Wheneverd::DSL::InvalidPeriodError) do
38
+ Wheneverd::DSL::CalendarSymbolPeriodList.validate(%i[day nope nope], allowed_symbols: [:day],
39
+ path: "x.rb")
40
+ end
41
+ assert_includes error.message, "Unknown period symbol"
42
+ assert_includes error.message, ":nope"
43
+ end
44
+
45
+ def test_validate_returns_periods_when_valid
46
+ result = Wheneverd::DSL::CalendarSymbolPeriodList.validate(
47
+ %i[day day],
48
+ allowed_symbols: [:day],
49
+ path: "x.rb"
50
+ )
51
+ assert_equal %i[day day], result
52
+ end
53
+ end