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.
- checksums.yaml +7 -0
- data/.gitignore +26 -0
- data/.rubocop.yml +41 -0
- data/.yardopts +8 -0
- data/AGENTS.md +42 -0
- data/CHANGELOG.md +28 -0
- data/FEATURE_SUMMARY.md +38 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +129 -0
- data/LICENSE +21 -0
- data/README.md +204 -0
- data/Rakefile +196 -0
- data/bin/console +8 -0
- data/bin/setup +5 -0
- data/exe/wheneverd +9 -0
- data/lib/wheneverd/cli/activate.rb +19 -0
- data/lib/wheneverd/cli/current.rb +22 -0
- data/lib/wheneverd/cli/deactivate.rb +19 -0
- data/lib/wheneverd/cli/delete.rb +20 -0
- data/lib/wheneverd/cli/help.rb +18 -0
- data/lib/wheneverd/cli/init.rb +78 -0
- data/lib/wheneverd/cli/reload.rb +40 -0
- data/lib/wheneverd/cli/show.rb +23 -0
- data/lib/wheneverd/cli/write.rb +32 -0
- data/lib/wheneverd/cli.rb +87 -0
- data/lib/wheneverd/core_ext/numeric_duration.rb +56 -0
- data/lib/wheneverd/dsl/at_normalizer.rb +48 -0
- data/lib/wheneverd/dsl/calendar_symbol_period_list.rb +42 -0
- data/lib/wheneverd/dsl/context.rb +72 -0
- data/lib/wheneverd/dsl/errors.rb +29 -0
- data/lib/wheneverd/dsl/loader.rb +49 -0
- data/lib/wheneverd/dsl/period_parser.rb +135 -0
- data/lib/wheneverd/duration.rb +27 -0
- data/lib/wheneverd/entry.rb +31 -0
- data/lib/wheneverd/errors.rb +9 -0
- data/lib/wheneverd/interval.rb +37 -0
- data/lib/wheneverd/job/command.rb +29 -0
- data/lib/wheneverd/schedule.rb +25 -0
- data/lib/wheneverd/systemd/calendar_spec.rb +109 -0
- data/lib/wheneverd/systemd/cron_parser.rb +352 -0
- data/lib/wheneverd/systemd/errors.rb +23 -0
- data/lib/wheneverd/systemd/renderer.rb +153 -0
- data/lib/wheneverd/systemd/systemctl.rb +38 -0
- data/lib/wheneverd/systemd/time_parser.rb +75 -0
- data/lib/wheneverd/systemd/unit_deleter.rb +64 -0
- data/lib/wheneverd/systemd/unit_lister.rb +59 -0
- data/lib/wheneverd/systemd/unit_namer.rb +69 -0
- data/lib/wheneverd/systemd/unit_writer.rb +132 -0
- data/lib/wheneverd/trigger/boot.rb +26 -0
- data/lib/wheneverd/trigger/calendar.rb +26 -0
- data/lib/wheneverd/trigger/interval.rb +30 -0
- data/lib/wheneverd/version.rb +6 -0
- data/lib/wheneverd.rb +41 -0
- data/test/cli_activate_test.rb +110 -0
- data/test/cli_current_test.rb +94 -0
- data/test/cli_deactivate_test.rb +111 -0
- data/test/cli_end_to_end_test.rb +98 -0
- data/test/cli_reload_test.rb +132 -0
- data/test/cli_systemctl_integration_test.rb +76 -0
- data/test/cli_systemd_analyze_test.rb +64 -0
- data/test/cli_test.rb +332 -0
- data/test/domain_model_test.rb +108 -0
- data/test/dsl_calendar_symbol_period_list_test.rb +53 -0
- data/test/dsl_loader_test.rb +384 -0
- data/test/support/cli_subprocess_test_helpers.rb +38 -0
- data/test/support/cli_test_helpers.rb +114 -0
- data/test/systemd_calendar_spec_test.rb +45 -0
- data/test/systemd_cron_parser_test.rb +114 -0
- data/test/systemd_renderer_errors_test.rb +85 -0
- data/test/systemd_renderer_test.rb +161 -0
- data/test/systemd_systemctl_test.rb +46 -0
- data/test/systemd_time_parser_test.rb +25 -0
- data/test/systemd_unit_deleter_test.rb +83 -0
- data/test/systemd_unit_writer_prune_test.rb +85 -0
- data/test/systemd_unit_writer_test.rb +71 -0
- data/test/test_helper.rb +34 -0
- data/test/wheneverd_test.rb +9 -0
- data/wheneverd.gemspec +35 -0
- metadata +136 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module DSLLoaderTestHelpers
|
|
8
|
+
def setup
|
|
9
|
+
super
|
|
10
|
+
@tmpdir = Dir.mktmpdir("wheneverd-")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
FileUtils.rm_rf(@tmpdir) if @tmpdir
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def load_schedule(source)
|
|
21
|
+
path = write_schedule(source)
|
|
22
|
+
Wheneverd::DSL::Loader.load_file(path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write_schedule(source)
|
|
26
|
+
path = File.join(@tmpdir, "schedule.rb")
|
|
27
|
+
File.write(path, source)
|
|
28
|
+
path
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class DSLLoaderIntervalAndDurationTest < Minitest::Test
|
|
33
|
+
include DSLLoaderTestHelpers
|
|
34
|
+
|
|
35
|
+
def test_loads_interval_string_every_block
|
|
36
|
+
schedule = load_schedule(<<~RUBY)
|
|
37
|
+
every "5m" do
|
|
38
|
+
command "echo hello"
|
|
39
|
+
end
|
|
40
|
+
RUBY
|
|
41
|
+
|
|
42
|
+
entry = schedule.entries.fetch(0)
|
|
43
|
+
assert_instance_of Wheneverd::Trigger::Interval, entry.trigger
|
|
44
|
+
assert_equal 300, entry.trigger.seconds
|
|
45
|
+
assert_equal ["echo hello"], entry.jobs.map(&:command)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_loads_duration_with_at_as_calendar
|
|
49
|
+
schedule = load_schedule(<<~RUBY)
|
|
50
|
+
every 1.day, at: "4:30 am" do
|
|
51
|
+
command "echo four_thirty"
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
|
|
55
|
+
entry = schedule.entries.fetch(0)
|
|
56
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
57
|
+
assert_equal ["day@4:30 am"], entry.trigger.on_calendar
|
|
58
|
+
assert_equal ["echo four_thirty"], entry.jobs.map(&:command)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_loads_duration_without_at_as_interval
|
|
62
|
+
schedule = load_schedule(<<~RUBY)
|
|
63
|
+
every 1.day do
|
|
64
|
+
command "echo daily"
|
|
65
|
+
end
|
|
66
|
+
RUBY
|
|
67
|
+
|
|
68
|
+
entry = schedule.entries.fetch(0)
|
|
69
|
+
assert_instance_of Wheneverd::Trigger::Interval, entry.trigger
|
|
70
|
+
assert_equal 86_400, entry.trigger.seconds
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_loads_duration_with_at_array_as_calendar
|
|
74
|
+
schedule = load_schedule(<<~RUBY)
|
|
75
|
+
every 1.day, at: ["4:30 am", "6:00 pm"] do
|
|
76
|
+
command "echo twice_daily"
|
|
77
|
+
end
|
|
78
|
+
RUBY
|
|
79
|
+
|
|
80
|
+
entry = schedule.entries.fetch(0)
|
|
81
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
82
|
+
assert_equal ["day@4:30 am", "day@6:00 pm"], entry.trigger.on_calendar
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_loads_cron_string_as_calendar_trigger
|
|
86
|
+
schedule = load_schedule(<<~RUBY)
|
|
87
|
+
every "0 0 27-31 * *" do
|
|
88
|
+
command "echo raw_cron"
|
|
89
|
+
end
|
|
90
|
+
RUBY
|
|
91
|
+
|
|
92
|
+
entry = schedule.entries.fetch(0)
|
|
93
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
94
|
+
assert_equal ["cron:0 0 27-31 * *"], entry.trigger.on_calendar
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class DSLLoaderSymbolPeriodsTest < Minitest::Test
|
|
99
|
+
include DSLLoaderTestHelpers
|
|
100
|
+
|
|
101
|
+
def test_loads_symbol_shortcut_hour
|
|
102
|
+
schedule = load_schedule(<<~RUBY)
|
|
103
|
+
every :hour do
|
|
104
|
+
command "echo hourly"
|
|
105
|
+
end
|
|
106
|
+
RUBY
|
|
107
|
+
|
|
108
|
+
entry = schedule.entries.fetch(0)
|
|
109
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
110
|
+
assert_equal ["hour"], entry.trigger.on_calendar
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_reboot_symbol_is_rejected
|
|
114
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
115
|
+
every :reboot do
|
|
116
|
+
command "echo reboot"
|
|
117
|
+
end
|
|
118
|
+
RUBY
|
|
119
|
+
|
|
120
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
121
|
+
assert_includes error.message, schedule_path
|
|
122
|
+
assert_includes error.message, ":reboot"
|
|
123
|
+
assert_includes error.message, "not supported"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_loads_day_selector_symbol_with_at
|
|
127
|
+
schedule = load_schedule(<<~RUBY)
|
|
128
|
+
every :sunday, at: "12pm" do
|
|
129
|
+
command "echo weekly"
|
|
130
|
+
end
|
|
131
|
+
RUBY
|
|
132
|
+
|
|
133
|
+
entry = schedule.entries.fetch(0)
|
|
134
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
135
|
+
assert_equal ["sunday@12pm"], entry.trigger.on_calendar
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_loads_weekday_symbol
|
|
139
|
+
schedule = load_schedule(<<~RUBY)
|
|
140
|
+
every :weekday do
|
|
141
|
+
command "echo weekday"
|
|
142
|
+
end
|
|
143
|
+
RUBY
|
|
144
|
+
|
|
145
|
+
entry = schedule.entries.fetch(0)
|
|
146
|
+
assert_equal ["weekday"], entry.trigger.on_calendar
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_loads_weekend_symbol
|
|
150
|
+
schedule = load_schedule(<<~RUBY)
|
|
151
|
+
every :weekend do
|
|
152
|
+
command "echo weekend"
|
|
153
|
+
end
|
|
154
|
+
RUBY
|
|
155
|
+
|
|
156
|
+
entry = schedule.entries.fetch(0)
|
|
157
|
+
assert_equal ["weekend"], entry.trigger.on_calendar
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_loads_multiple_day_symbols_as_calendar_trigger
|
|
161
|
+
schedule = load_schedule(<<~RUBY)
|
|
162
|
+
every :tuesday, :wednesday, at: "12pm" do
|
|
163
|
+
command "echo midweek"
|
|
164
|
+
end
|
|
165
|
+
RUBY
|
|
166
|
+
|
|
167
|
+
entry = schedule.entries.fetch(0)
|
|
168
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
169
|
+
assert_equal ["tuesday@12pm", "wednesday@12pm"], entry.trigger.on_calendar
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_loads_period_symbol_array_as_calendar_trigger
|
|
173
|
+
schedule = load_schedule(<<~RUBY)
|
|
174
|
+
every %i[tuesday wednesday], at: "12pm" do
|
|
175
|
+
command "echo midweek"
|
|
176
|
+
end
|
|
177
|
+
RUBY
|
|
178
|
+
|
|
179
|
+
entry = schedule.entries.fetch(0)
|
|
180
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
181
|
+
assert_equal ["tuesday@12pm", "wednesday@12pm"], entry.trigger.on_calendar
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def test_loads_multiple_day_symbols_with_at_array_as_calendar_trigger
|
|
185
|
+
entry = load_schedule(<<~RUBY).entries.fetch(0)
|
|
186
|
+
every :tuesday, :wednesday, at: ["4:30 am", "6:00 pm"] do
|
|
187
|
+
command "echo twice_midweek"
|
|
188
|
+
end
|
|
189
|
+
RUBY
|
|
190
|
+
|
|
191
|
+
assert_instance_of Wheneverd::Trigger::Calendar, entry.trigger
|
|
192
|
+
assert_equal(
|
|
193
|
+
["tuesday@4:30 am", "tuesday@6:00 pm", "wednesday@4:30 am", "wednesday@6:00 pm"],
|
|
194
|
+
entry.trigger.on_calendar
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class DSLLoaderPeriodErrorsTest < Minitest::Test
|
|
200
|
+
include DSLLoaderTestHelpers
|
|
201
|
+
|
|
202
|
+
def test_invalid_interval_raises_error_with_path
|
|
203
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
204
|
+
every "0m" do
|
|
205
|
+
command "echo no"
|
|
206
|
+
end
|
|
207
|
+
RUBY
|
|
208
|
+
|
|
209
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
210
|
+
assert_includes error.message, schedule_path
|
|
211
|
+
assert_includes error.message, "Interval must be positive"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_interval_string_does_not_accept_at
|
|
215
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
216
|
+
every "5m", at: "12pm" do
|
|
217
|
+
command "echo nope"
|
|
218
|
+
end
|
|
219
|
+
RUBY
|
|
220
|
+
|
|
221
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
222
|
+
assert_includes error.message, schedule_path
|
|
223
|
+
assert_includes error.message, "interval periods"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def test_cron_string_does_not_accept_at
|
|
227
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
228
|
+
every "0 0 27-31 * *", at: "12pm" do
|
|
229
|
+
command "echo nope"
|
|
230
|
+
end
|
|
231
|
+
RUBY
|
|
232
|
+
|
|
233
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
234
|
+
assert_includes error.message, schedule_path
|
|
235
|
+
assert_includes error.message, "cron periods"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def test_duration_at_is_rejected_for_non_daily_durations
|
|
239
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
240
|
+
every 2.days, at: "4:30 am" do
|
|
241
|
+
command "echo nope"
|
|
242
|
+
end
|
|
243
|
+
RUBY
|
|
244
|
+
|
|
245
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
246
|
+
assert_includes error.message, schedule_path
|
|
247
|
+
assert_includes error.message, "at: is only supported"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_every_requires_a_block
|
|
251
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
252
|
+
every "5m"
|
|
253
|
+
RUBY
|
|
254
|
+
|
|
255
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
256
|
+
assert_includes error.message, schedule_path
|
|
257
|
+
assert_includes error.message, "requires a block"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_every_rejects_unknown_period_type
|
|
261
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
262
|
+
every Object.new do
|
|
263
|
+
command "echo nope"
|
|
264
|
+
end
|
|
265
|
+
RUBY
|
|
266
|
+
|
|
267
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
268
|
+
assert_includes error.message, schedule_path
|
|
269
|
+
assert_includes error.message, "Unsupported period type"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_every_rejects_unrecognized_string_period
|
|
273
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
274
|
+
every "not a schedule" do
|
|
275
|
+
command "echo nope"
|
|
276
|
+
end
|
|
277
|
+
RUBY
|
|
278
|
+
|
|
279
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
280
|
+
assert_includes error.message, schedule_path
|
|
281
|
+
assert_includes error.message, "Unrecognized period"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def test_unknown_symbol_period_raises_error
|
|
285
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
286
|
+
every :fortnight do
|
|
287
|
+
command "echo nope"
|
|
288
|
+
end
|
|
289
|
+
RUBY
|
|
290
|
+
|
|
291
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
292
|
+
assert_includes error.message, schedule_path
|
|
293
|
+
assert_includes error.message, "Unknown period symbol"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_reboot_does_not_accept_at
|
|
297
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
298
|
+
every :reboot, at: "12pm" do
|
|
299
|
+
command "echo nope"
|
|
300
|
+
end
|
|
301
|
+
RUBY
|
|
302
|
+
|
|
303
|
+
error = assert_raises(Wheneverd::DSL::InvalidPeriodError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
304
|
+
assert_includes error.message, schedule_path
|
|
305
|
+
assert_includes error.message, ":reboot"
|
|
306
|
+
assert_includes error.message, "not supported"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
class DSLLoaderAtValidationErrorsTest < Minitest::Test
|
|
311
|
+
include DSLLoaderTestHelpers
|
|
312
|
+
|
|
313
|
+
def test_invalid_at_type_raises_error_with_path
|
|
314
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
315
|
+
every 1.day, at: 123 do
|
|
316
|
+
command "echo nope"
|
|
317
|
+
end
|
|
318
|
+
RUBY
|
|
319
|
+
|
|
320
|
+
error = assert_raises(Wheneverd::DSL::InvalidAtError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
321
|
+
assert_includes error.message, schedule_path
|
|
322
|
+
assert_includes error.message, "at:"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def test_invalid_at_array_rejects_non_string
|
|
326
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
327
|
+
every 1.day, at: ["4:30 am", 123] do
|
|
328
|
+
command "echo nope"
|
|
329
|
+
end
|
|
330
|
+
RUBY
|
|
331
|
+
|
|
332
|
+
error = assert_raises(Wheneverd::DSL::InvalidAtError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
333
|
+
assert_includes error.message, schedule_path
|
|
334
|
+
assert_includes error.message, "Array of Strings"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def test_invalid_at_array_rejects_empty
|
|
338
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
339
|
+
every 1.day, at: [] do
|
|
340
|
+
command "echo nope"
|
|
341
|
+
end
|
|
342
|
+
RUBY
|
|
343
|
+
|
|
344
|
+
error = assert_raises(Wheneverd::DSL::InvalidAtError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
345
|
+
assert_includes error.message, schedule_path
|
|
346
|
+
assert_includes error.message, "must not be empty"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
class DSLLoaderCommandAndEvalErrorsTest < Minitest::Test
|
|
351
|
+
include DSLLoaderTestHelpers
|
|
352
|
+
|
|
353
|
+
def test_invalid_command_raises_error_with_path
|
|
354
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
355
|
+
every "5m" do
|
|
356
|
+
command ""
|
|
357
|
+
end
|
|
358
|
+
RUBY
|
|
359
|
+
|
|
360
|
+
error = assert_raises(Wheneverd::DSL::LoadError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
361
|
+
assert_includes error.message, schedule_path
|
|
362
|
+
assert_includes error.message, "Command must not be empty"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def test_command_outside_every_is_rejected
|
|
366
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
367
|
+
command "echo nope"
|
|
368
|
+
RUBY
|
|
369
|
+
|
|
370
|
+
error = assert_raises(Wheneverd::DSL::LoadError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
371
|
+
assert_includes error.message, schedule_path
|
|
372
|
+
assert_includes error.message, "inside every()"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def test_unknown_method_in_schedule_is_wrapped_as_load_error
|
|
376
|
+
schedule_path = write_schedule(<<~RUBY)
|
|
377
|
+
totally_unknown_dsl_method "nope"
|
|
378
|
+
RUBY
|
|
379
|
+
|
|
380
|
+
error = assert_raises(Wheneverd::DSL::LoadError) { Wheneverd::DSL::Loader.load_file(schedule_path) }
|
|
381
|
+
assert_includes error.message, schedule_path
|
|
382
|
+
assert_includes error.message, "totally_unknown_dsl_method"
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module CLISubprocessTestHelpers
|
|
8
|
+
def with_temp_project_dir
|
|
9
|
+
Dir.mktmpdir("wheneverd-e2e-") do |tmp|
|
|
10
|
+
project_dir = File.join(tmp, "myapp")
|
|
11
|
+
FileUtils.mkdir_p(project_dir)
|
|
12
|
+
yield project_dir
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def exe_path
|
|
17
|
+
File.expand_path("../../exe/wheneverd", __dir__)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_exe(args, chdir:, env: {})
|
|
21
|
+
stdout, stderr, status = Open3.capture3(env, RbConfig.ruby, exe_path, *args, chdir: chdir)
|
|
22
|
+
[status.exitstatus, stdout, stderr]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write_schedule(project_dir, contents)
|
|
26
|
+
path = File.join(project_dir, "config", "schedule.rb")
|
|
27
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
28
|
+
File.write(path, contents)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_executable(name)
|
|
32
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
33
|
+
candidate = File.join(dir, name)
|
|
34
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "wheneverd/cli"
|
|
6
|
+
|
|
7
|
+
module CLITestHelpers
|
|
8
|
+
SYSTEMCTL_USER_PREFIX = ["systemctl", "--user", "--no-pager"].freeze
|
|
9
|
+
|
|
10
|
+
def run_cli(args)
|
|
11
|
+
status = nil
|
|
12
|
+
out, err = capture_io { status = Wheneverd::CLI.run("wheneverd", args) }
|
|
13
|
+
[status, out, err]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run_activate_with_capture3_stub(identifier: "demo", **kwargs)
|
|
17
|
+
run_cli_with_capture3_stub(["activate", "--identifier", identifier], **kwargs)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_deactivate_with_capture3_stub(identifier: "demo", **kwargs)
|
|
21
|
+
run_cli_with_capture3_stub(["deactivate", "--identifier", identifier], **kwargs)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run_reload_with_capture3_stub(unit_dir:, identifier: "demo", **kwargs)
|
|
25
|
+
run_cli_with_capture3_stub(
|
|
26
|
+
["reload", "--identifier", identifier, "--unit-dir", unit_dir],
|
|
27
|
+
**kwargs
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write_empty_schedule(path = File.join("config", "schedule.rb"))
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
33
|
+
File.write(path, "# frozen_string_literal: true\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def with_capture3_stub(exitstatus: 0, stdout: "", stderr: "")
|
|
37
|
+
calls = []
|
|
38
|
+
Thread.current[:open3_capture3_stub] = {
|
|
39
|
+
calls: calls,
|
|
40
|
+
stdout: stdout,
|
|
41
|
+
stderr: stderr,
|
|
42
|
+
exitstatus: exitstatus
|
|
43
|
+
}
|
|
44
|
+
yield calls
|
|
45
|
+
ensure
|
|
46
|
+
Thread.current[:open3_capture3_stub] = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_cli_with_capture3_stub(args, exitstatus: 0, stdout: "", stderr: "")
|
|
50
|
+
status = out = err = nil
|
|
51
|
+
calls = nil
|
|
52
|
+
with_capture3_stub(exitstatus: exitstatus, stdout: stdout, stderr: stderr) do |stub_calls|
|
|
53
|
+
calls = stub_calls
|
|
54
|
+
status, out, err = run_cli(args)
|
|
55
|
+
end
|
|
56
|
+
[status, out, err, calls]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def assert_cli_success(status, err)
|
|
60
|
+
assert_equal 0, status
|
|
61
|
+
assert_equal "", err
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def assert_systemctl_call(calls, index, expected_args)
|
|
65
|
+
args, kwargs = calls.fetch(index)
|
|
66
|
+
assert_equal expected_args, args
|
|
67
|
+
assert_equal({}, kwargs)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def assert_systemctl_call_starts_with(calls, index, prefix, includes: nil)
|
|
71
|
+
args, kwargs = calls.fetch(index)
|
|
72
|
+
assert_equal prefix, args.take(prefix.length)
|
|
73
|
+
assert_equal({}, kwargs)
|
|
74
|
+
return if includes.nil?
|
|
75
|
+
|
|
76
|
+
Array(includes).each { |expected| assert_includes args, expected }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_project_dir
|
|
80
|
+
Dir.mktmpdir("wheneverd-project-") do |tmp|
|
|
81
|
+
project_dir = File.join(tmp, "myapp")
|
|
82
|
+
FileUtils.mkdir_p(project_dir)
|
|
83
|
+
Dir.chdir(project_dir) { yield project_dir }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def with_inited_project_dir
|
|
88
|
+
with_project_dir do |project_dir|
|
|
89
|
+
assert_equal 0, run_cli(["init"]).first
|
|
90
|
+
yield project_dir
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def expected_units(identifier: "demo", schedule_path: File.join("config", "schedule.rb"))
|
|
95
|
+
schedule = Wheneverd::DSL::Loader.load_file(File.expand_path(schedule_path))
|
|
96
|
+
Wheneverd::Systemd::Renderer.render(schedule, identifier: identifier)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def expected_timer_basenames(identifier: "demo",
|
|
100
|
+
schedule_path: File.join("config", "schedule.rb"))
|
|
101
|
+
expected_units(identifier: identifier, schedule_path: schedule_path)
|
|
102
|
+
.select { |unit| unit.kind == :timer }
|
|
103
|
+
.map(&:path_basename)
|
|
104
|
+
.uniq
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def expected_service_basenames(identifier: "demo",
|
|
108
|
+
schedule_path: File.join("config", "schedule.rb"))
|
|
109
|
+
expected_units(identifier: identifier, schedule_path: schedule_path)
|
|
110
|
+
.select { |unit| unit.kind == :service }
|
|
111
|
+
.map(&:path_basename)
|
|
112
|
+
.uniq
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class SystemdCalendarSpecTest < Minitest::Test
|
|
6
|
+
def test_translates_base_calendar_specs
|
|
7
|
+
assert_equal "hourly", Wheneverd::Systemd::CalendarSpec.to_on_calendar("hour")
|
|
8
|
+
assert_equal "daily", Wheneverd::Systemd::CalendarSpec.to_on_calendar("day")
|
|
9
|
+
assert_equal "monthly", Wheneverd::Systemd::CalendarSpec.to_on_calendar("month")
|
|
10
|
+
assert_equal "yearly", Wheneverd::Systemd::CalendarSpec.to_on_calendar("year")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_translates_weekday_weekend_and_days_without_at_to_midnight
|
|
14
|
+
assert_equal "Mon..Fri *-*-* 00:00:00", Wheneverd::Systemd::CalendarSpec.to_on_calendar("weekday")
|
|
15
|
+
assert_equal "Sat,Sun *-*-* 00:00:00", Wheneverd::Systemd::CalendarSpec.to_on_calendar("weekend")
|
|
16
|
+
assert_equal "Mon *-*-* 00:00:00", Wheneverd::Systemd::CalendarSpec.to_on_calendar("monday")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_translates_calendar_specs_with_at
|
|
20
|
+
assert_equal "*-*-* 04:30:00", Wheneverd::Systemd::CalendarSpec.to_on_calendar("day@4:30 am")
|
|
21
|
+
assert_equal "Mon..Fri *-*-* 00:15:00", Wheneverd::Systemd::CalendarSpec.to_on_calendar("weekday@00:15")
|
|
22
|
+
assert_equal "Sat,Sun *-*-* 18:00:00",
|
|
23
|
+
Wheneverd::Systemd::CalendarSpec.to_on_calendar("weekend@6:00 pm")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_rejects_invalid_calendar_specs
|
|
27
|
+
assert_raises(Wheneverd::Systemd::InvalidCalendarSpecError) do
|
|
28
|
+
Wheneverd::Systemd::CalendarSpec.to_on_calendar("")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
assert_raises(Wheneverd::Systemd::InvalidCalendarSpecError) do
|
|
32
|
+
Wheneverd::Systemd::CalendarSpec.to_on_calendar("not-a-period")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
assert_raises(Wheneverd::Systemd::InvalidCalendarSpecError) do
|
|
36
|
+
Wheneverd::Systemd::CalendarSpec.to_on_calendar("hour@12pm")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_rejects_calendar_specs_that_expand_to_multiple_on_calendar_values
|
|
41
|
+
assert_raises(Wheneverd::Systemd::InvalidCalendarSpecError) do
|
|
42
|
+
Wheneverd::Systemd::CalendarSpec.to_on_calendar("cron:0 0 1 * Mon")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class SystemdCronParserTest < Minitest::Test
|
|
6
|
+
def test_translates_simple_cron_to_single_value
|
|
7
|
+
assert_values("0 0 * * *", ["*-*-* 00:00:00"])
|
|
8
|
+
assert_values("59 23 5 * *", ["*-*-5 23:59:00"])
|
|
9
|
+
assert_values("0 0 27-31 * *", ["*-*-27..31 00:00:00"])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_translates_months_and_steps
|
|
13
|
+
assert_values("0 0 * 1 *", ["*-1-* 00:00:00"])
|
|
14
|
+
assert_values("0 0 * Jan *", ["*-1-* 00:00:00"])
|
|
15
|
+
assert_values("*/15 9-17 * * *", ["*-*-* 09..17:00/15:00"])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_translates_lists_and_steps
|
|
19
|
+
assert_values("0,30 8,16 * * *", ["*-*-* 08,16:00,30:00"])
|
|
20
|
+
assert_values("0 0 */2 * *", ["*-*-1/2 00:00:00"])
|
|
21
|
+
assert_values("0 0 * */2 *", ["*-1/2-* 00:00:00"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_translates_day_of_week_variants
|
|
25
|
+
assert_values("0 0 * * Mon", ["Mon *-*-* 00:00:00"])
|
|
26
|
+
assert_values("0 0 * * 1-5", ["Mon..Fri *-*-* 00:00:00"])
|
|
27
|
+
assert_values("0 0 * * */2", ["Tue,Thu,Sat..Sun *-*-* 00:00:00"])
|
|
28
|
+
assert_values("0 0 * * Fri-Mon", ["Mon,Fri..Sun *-*-* 00:00:00"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_dom_and_dow_or_semantics_expands_to_multiple_values
|
|
32
|
+
assert_values("0 0 1 * Mon", ["Mon *-*-* 00:00:00", "*-*-1 00:00:00"])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_to_on_calendar_rejects_multiple_values
|
|
36
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
37
|
+
Wheneverd::Systemd::CronParser.to_on_calendar("0 0 1 * Mon")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_private_helpers_reject_empty_numeric_field
|
|
42
|
+
assert_private_unsupported(
|
|
43
|
+
:parse_mapped_numeric_expression,
|
|
44
|
+
"",
|
|
45
|
+
0..59,
|
|
46
|
+
field: "minute",
|
|
47
|
+
input: "x",
|
|
48
|
+
names: {}
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_private_helpers_reject_invalid_numeric_tokens
|
|
53
|
+
common = { field: "minute", input: "x", names: {} }
|
|
54
|
+
assert_private_unsupported(:parse_mapped_value, " ", 0..59, **common)
|
|
55
|
+
assert_private_unsupported(:parse_positive_int, "x", field: "minute", input: "x", label: "step")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_private_helpers_reject_empty_dow_and_invalid_tokens
|
|
59
|
+
assert_private_unsupported(:parse_dow_set, "", input: "x")
|
|
60
|
+
assert_private_unsupported(:apply_dow_part, Array.new(7, false), "", input: "x")
|
|
61
|
+
assert_private_unsupported(:parse_dow_value, "", input: "x")
|
|
62
|
+
assert_private_unsupported(:parse_dow_value, "8", input: "x")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_rejects_unsupported_cron_patterns
|
|
66
|
+
assert_unsupported(
|
|
67
|
+
"0 0",
|
|
68
|
+
"x 0 * * *",
|
|
69
|
+
"60 0 * * *",
|
|
70
|
+
"0 0 0 * *",
|
|
71
|
+
"0 0 10 13 *"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_rejects_unsupported_cron_tokens
|
|
76
|
+
assert_unsupported(
|
|
77
|
+
"0 0 31-27 * *",
|
|
78
|
+
"0 0 * * Funday",
|
|
79
|
+
"0 0 */0 * *",
|
|
80
|
+
"0 0 ? * *"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_rejects_empty_tokens_and_invalid_steps
|
|
85
|
+
assert_unsupported(
|
|
86
|
+
"0,,15 0 * * *",
|
|
87
|
+
"*/x 0 * * *",
|
|
88
|
+
"0 0 * * Mon,,Tue",
|
|
89
|
+
"0 0 * * Mon-",
|
|
90
|
+
"0 0 * * 8"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def assert_values(cron, expected)
|
|
97
|
+
assert_equal expected, Wheneverd::Systemd::CronParser.to_on_calendar_values(cron)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def assert_private_unsupported(method, *args, **kwargs)
|
|
101
|
+
parser = Wheneverd::Systemd::CronParser
|
|
102
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
103
|
+
parser.send(method, *args, **kwargs)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def assert_unsupported(*crons)
|
|
108
|
+
crons.each do |cron|
|
|
109
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
110
|
+
Wheneverd::Systemd::CronParser.to_on_calendar_values(cron)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|