ruby_raider 4.1.0 → 4.2.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +3 -0
  3. data/README.md +52 -53
  4. data/RELEASE.md +14 -1
  5. data/lib/commands/schedule_commands.rb +84 -0
  6. data/lib/generators/templates/common/git_ignore.tt +1 -0
  7. data/lib/ruby_raider/runtime/retry_reset.rb +32 -0
  8. data/lib/ruby_raider/runtime/rspec.rb +1 -0
  9. data/lib/ruby_raider/runtime/show_browser.rb +53 -0
  10. data/lib/ruby_raider/runtime.rb +2 -0
  11. data/lib/ruby_raider.rb +22 -7
  12. data/lib/utilities/schedules/adapter.rb +22 -0
  13. data/lib/utilities/schedules/base.rb +27 -0
  14. data/lib/utilities/schedules/kind.rb +74 -0
  15. data/lib/utilities/schedules/legacy.rb +196 -0
  16. data/lib/utilities/schedules/legacy_stub.rb +62 -0
  17. data/lib/utilities/{flows → schedules}/next_run.rb +2 -2
  18. data/lib/utilities/{flows → schedules}/notifier.rb +6 -6
  19. data/lib/utilities/{flows → schedules}/os/command_runner.rb +1 -1
  20. data/lib/utilities/{flows → schedules}/os/cron.rb +27 -17
  21. data/lib/utilities/{flows → schedules}/os/launchd.rb +32 -14
  22. data/lib/utilities/{flows → schedules}/os/linux.rb +23 -5
  23. data/lib/utilities/{flows → schedules}/os/systemd.rb +33 -17
  24. data/lib/utilities/{flows → schedules}/os/task_scheduler.rb +36 -20
  25. data/lib/utilities/schedules/os_enumerator.rb +30 -0
  26. data/lib/utilities/schedules/project.rb +70 -0
  27. data/lib/utilities/schedules/rule.rb +162 -0
  28. data/lib/utilities/schedules/run_lock.rb +160 -0
  29. data/lib/utilities/schedules/run_log.rb +122 -0
  30. data/lib/utilities/schedules/run_report.rb +139 -0
  31. data/lib/utilities/schedules/run_wrapper.rb +216 -0
  32. data/lib/utilities/schedules/schedule.rb +89 -0
  33. data/lib/utilities/schedules/schedule_store.rb +53 -0
  34. data/lib/utilities/schedules/scheduler.rb +178 -0
  35. data/lib/utilities/schedules/scheduler_steps.rb +69 -0
  36. data/lib/utilities/schedules/selection.rb +133 -0
  37. data/lib/utilities/schedules/spawn.rb +68 -0
  38. data/lib/utilities/{flows/schedule_store.rb → schedules/store.rb} +17 -13
  39. data/lib/version +1 -1
  40. data/ruby_raider.gemspec +1 -0
  41. data/sig/commands/schedule_commands.rbs +12 -0
  42. data/sig/ruby_raider/runtime/retry_reset.rbs +7 -0
  43. data/sig/ruby_raider/runtime/show_browser.rbs +27 -0
  44. data/sig/ruby_raider.rbs +4 -0
  45. data/sig/utilities/schedules/adapter.rbs +5 -0
  46. data/sig/utilities/schedules/base.rbs +15 -0
  47. data/sig/utilities/schedules/kind.rbs +32 -0
  48. data/sig/utilities/schedules/legacy.rbs +45 -0
  49. data/sig/utilities/schedules/legacy_stub.rbs +15 -0
  50. data/sig/utilities/{flows → schedules}/next_run.rbs +3 -3
  51. data/sig/utilities/{flows → schedules}/notifier.rbs +1 -1
  52. data/sig/utilities/{flows → schedules}/os/command_runner.rbs +1 -1
  53. data/sig/utilities/{flows → schedules}/os/cron.rbs +7 -7
  54. data/sig/utilities/{flows → schedules}/os/launchd.rbs +7 -6
  55. data/sig/utilities/{flows → schedules}/os/linux.rbs +10 -4
  56. data/sig/utilities/{flows → schedules}/os/systemd.rbs +8 -7
  57. data/sig/utilities/{flows → schedules}/os/task_scheduler.rbs +13 -10
  58. data/sig/utilities/schedules/os_enumerator.rbs +12 -0
  59. data/sig/utilities/schedules/project.rbs +22 -0
  60. data/sig/utilities/{flows/schedule.rbs → schedules/rule.rbs} +23 -12
  61. data/sig/utilities/schedules/run_lock.rbs +29 -0
  62. data/sig/utilities/schedules/run_log.rbs +33 -0
  63. data/sig/utilities/schedules/run_report.rbs +34 -0
  64. data/sig/utilities/schedules/run_wrapper.rbs +45 -0
  65. data/sig/utilities/schedules/schedule.rbs +27 -0
  66. data/sig/utilities/schedules/schedule_store.rbs +21 -0
  67. data/sig/utilities/{flows → schedules}/scheduler.rbs +22 -15
  68. data/sig/utilities/schedules/scheduler_steps.rbs +22 -0
  69. data/sig/utilities/schedules/selection.rbs +36 -0
  70. data/sig/utilities/schedules/spawn.rbs +10 -0
  71. data/sig/utilities/{flows/schedule_store.rbs → schedules/store.rbs} +14 -9
  72. data/spec/commands/schedule_commands_spec.rb +284 -0
  73. data/spec/integration/content/common_content_spec.rb +8 -2
  74. data/spec/integration/content/gemfile_content_spec.rb +2 -2
  75. data/spec/integration/end_to_end_spec.rb +0 -63
  76. data/spec/runtime/retry_reset_spec.rb +112 -0
  77. data/spec/runtime/show_browser_project_spec.rb +70 -0
  78. data/spec/runtime/show_browser_spec.rb +101 -0
  79. data/spec/utilities/schedules/kind_spec.rb +81 -0
  80. data/spec/utilities/schedules/legacy_spec.rb +433 -0
  81. data/spec/utilities/schedules/legacy_stub_spec.rb +144 -0
  82. data/spec/utilities/{flows → schedules}/next_run_spec.rb +5 -5
  83. data/spec/utilities/{flows → schedules}/notifier_spec.rb +3 -4
  84. data/spec/utilities/{flows → schedules}/os/command_runner_spec.rb +2 -3
  85. data/spec/utilities/{flows → schedules}/os/cron_spec.rb +34 -29
  86. data/spec/utilities/{flows → schedules}/os/launchd_spec.rb +42 -29
  87. data/spec/utilities/{flows → schedules}/os/linux_spec.rb +27 -13
  88. data/spec/utilities/schedules/os/list_ids_spec.rb +202 -0
  89. data/spec/utilities/{flows → schedules}/os/systemd_spec.rb +35 -31
  90. data/spec/utilities/{flows → schedules}/os/task_scheduler_spec.rb +40 -34
  91. data/spec/utilities/schedules/os_entry_spec.rb +54 -0
  92. data/spec/utilities/schedules/os_enumerator_spec.rb +57 -0
  93. data/spec/utilities/schedules/project_spec.rb +125 -0
  94. data/spec/utilities/schedules/run_lock_spec.rb +143 -0
  95. data/spec/utilities/schedules/run_log_spec.rb +83 -0
  96. data/spec/utilities/schedules/run_report_spec.rb +150 -0
  97. data/spec/utilities/schedules/run_wrapper_integration_spec.rb +166 -0
  98. data/spec/utilities/schedules/run_wrapper_spec.rb +430 -0
  99. data/spec/utilities/schedules/schedule_spec.rb +120 -0
  100. data/spec/utilities/schedules/schedule_store_spec.rb +151 -0
  101. data/spec/utilities/schedules/scheduler_launchd_spec.rb +77 -0
  102. data/spec/utilities/schedules/scheduler_spec.rb +445 -0
  103. data/spec/utilities/schedules/selection_spec.rb +176 -0
  104. data/spec/utilities/schedules/spawn_spec.rb +104 -0
  105. metadata +106 -53
  106. data/lib/commands/flow_commands.rb +0 -104
  107. data/lib/generators/templates/flows/flow_helper.tt +0 -42
  108. data/lib/generators/templates/flows/run.tt +0 -332
  109. data/lib/utilities/flows/project.rb +0 -170
  110. data/lib/utilities/flows/run_log.rb +0 -61
  111. data/lib/utilities/flows/runner.rb +0 -206
  112. data/lib/utilities/flows/schedule.rb +0 -221
  113. data/lib/utilities/flows/scheduler.rb +0 -187
  114. data/sig/commands/flow_commands.rbs +0 -13
  115. data/sig/utilities/flows/project.rbs +0 -46
  116. data/sig/utilities/flows/run_log.rbs +0 -22
  117. data/sig/utilities/flows/runner.rbs +0 -35
  118. data/spec/commands/flow_commands_spec.rb +0 -128
  119. data/spec/generators/flow_helper_spec.rb +0 -40
  120. data/spec/integration/flows/flow_project.rb +0 -62
  121. data/spec/integration/flows/run_command_spec.rb +0 -91
  122. data/spec/integration/flows/run_lifecycle_spec.rb +0 -383
  123. data/spec/utilities/flows/project_spec.rb +0 -178
  124. data/spec/utilities/flows/run_log_spec.rb +0 -47
  125. data/spec/utilities/flows/runner_spec.rb +0 -313
  126. data/spec/utilities/flows/schedule_spec.rb +0 -233
  127. data/spec/utilities/flows/schedule_store_spec.rb +0 -137
  128. data/spec/utilities/flows/scheduler_spec.rb +0 -329
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df32b6a642777030f39db11a96cc9a85a5f59fce1528e9d096f2acd5e6bca3f9
4
- data.tar.gz: 49713a619c80897c37b68e91c0c3b5acff9917fe9d850cbfde2fa6827345914b
3
+ metadata.gz: 07307a2abf36b62e163df5530bb3666faa9ee1837623909e5242dce43086b8b1
4
+ data.tar.gz: 32388fc57a5acb2e497f68c654a750379a3c40841ac849321d2d083bb0019055
5
5
  SHA512:
6
- metadata.gz: 02fea0f644525a60e0cbdf2fbac17194272480915cc57f486dade1c12f2ca6ecf64b021d120aaf25fa84a9bad085c5e1e958b1537e9f2e979361fbb0353aae65
7
- data.tar.gz: 1da4cd4c6161a49663c9198f67847724edf1c7baa4c0988df86ea5c80361ad411af3ff83a0cdf9ed6e34a76d22208c782d44bd451e04c15d3d72bda582cb7186
6
+ metadata.gz: a6ec8338233eeba0dbe5bc400ff16ea0ec2fa3a3003464b6a9f7c230e7f7ee004f0db0ebd5f8c6ee8759f2192ddd83d5d5298025d40f829fc09e20c74a75647d
7
+ data.tar.gz: 9ce051d82e86b5f28e9d1fb9816f4069d11418ef1b12db53c1c7884a271d08225c45ac7d4f6a1b34545a64761e113140a7edbe7c98eba5c973f5e63b33d176de
@@ -37,5 +37,8 @@ jobs:
37
37
  - name: Run command routing tests
38
38
  run: bundle exec rspec spec/commands/ --format documentation
39
39
 
40
+ - name: Run schedule tests
41
+ run: bundle exec rspec spec/utilities/schedules --tag ~slow --format documentation
42
+
40
43
  - name: Run menu tests
41
44
  run: bundle exec rspec spec/menus/ --format documentation
data/README.md CHANGED
@@ -198,62 +198,61 @@ Example projects log in once and restore the saved session for examples tagged `
198
198
  - The state stays in git-ignored `.raider/auth/`.
199
199
  - Skeletons (`examples:false`) get `SessionState#reuse` to wire your own login.
200
200
 
201
- ### Flows: run without assertions (web)
201
+ ### Scheduling tests
202
202
 
203
- A flow is a recorded chore with no checks, such as logging in and downloading a report. It is done when every step ran. Flows are RSpec files under `flows/`, so they use the same driver, locators, waiting, screenshots, video and trace as your tests.
204
-
205
- ```
206
- raider flow init # adds flows/flow_helper.rb, flows/support/run.rb and .gitignore lines
207
- raider flow run timesheet # runs flows/timesheet_flow.rb headless
208
- raider flow run timesheet --headed --retries 2 --input hours=7
209
- raider flow list [--json] # each flow and its last result
210
- ```
211
-
212
- ```ruby
213
- # flows/timesheet_flow.rb
214
- require_relative 'flow_helper'
215
-
216
- RSpec.describe 'Fill the timesheet' do
217
- it 'saves the hours' do
218
- driver.navigate.to 'https://example.com/timesheet'
219
- driver.find_element(label: 'Hours').send_keys(flow_input('hours', '8'))
220
- driver.find_element(by_role('button', name: 'Save')).click
221
- end
222
- end
223
- ```
224
-
225
- - **Exit codes:** 0 completed, 1 a step failed, 2 setup failed (bundle, driver, missing helper), 3 the flow is already running. Each flow holds its own lock in `.raider/flows/`, and a second run exits 3 straight away.
226
- - **Results:** each run gets its own `flow-results/<name>/<UTC time>/` (report, failure screenshot, video and trace), and `.raider/flows/<name>/runs.jsonl` gains one line: `started_at`, `finished_at`, `status` (`completed`, `failed`, `setup_error`, `busy`), `failed_step`, `attempt`, `trigger`, `results_dir`. The last 20 result folders and 50 lines are kept. Flow runs never touch `raider-results/`, and test runs never touch `flow-results/`.
227
- - **Inputs:** `flow_input('name', 'default')` reads `--input name=value` (`RAIDER_INPUT_NAME`). Inputs show up in your shell history and process list, so keep secrets out of them.
228
- - **Secrets:** put them in `.raider/flows/env` as `KEY=value` lines (`#` comments, no interpolation). The file is loaded only when you own it and it has permissions 600 (`chmod 600 .raider/flows/env`); otherwise the run warns and goes on without it. Values are never written to the run log (they are replaced with `[secret]`), but your flow's own output and errors are printed as they are. On Windows there are no permission bits, so the folder's access rules protect the file: keep the project in your user folder.
229
- - **The same record every way:** `bundle exec rspec flows/timesheet_flow.rb` writes the same run log as `raider flow run`; `raider flow run` adds retries (whole flow, 30 s apart) and the exit codes. Set `RAIDER_FLOW_HEADED=1` to watch a plain rspec run (`headless: true` in `config/config.yml` still wins).
230
- - Projects made with 4.0 can add flows: `raider flow init` raises the Gemfile's `ruby_raider` pin to `~> 4.1`; run `bundle install` after it.
231
-
232
- ### Scheduling flows
233
-
234
- Your computer's own scheduler runs the flow, so nothing needs to stay open. Scheduling never asks for sudo or admin rights.
203
+ Your computer's own scheduler runs the project's tests: all of them, one tag, or a list of spec files. Nothing needs to stay open, and scheduling never asks for sudo or admin rights. `raider sc` is short for `raider schedule`.
235
204
 
236
205
  ```bash
237
- raider flow schedule timesheet --every weekday --at 17:30 --input hours=7 --notify
238
- raider flow schedule backup --every hour --at :15
239
- raider flow schedule report --every week --on fri --at 09:00 --retries 2
240
- raider flow unschedule timesheet
241
- raider flow schedules [--json] # each schedule, its next run, last result and OS state
206
+ raider schedule add nightly --every day --at 02:00 --notify
207
+ raider schedule add smoke --every hour --at :15 --tag smoke --retries 1
208
+ raider schedule add checkout spec/cart_spec.rb spec/checkout_spec.rb --every weekday --at 08:30 --show-browser
209
+ raider schedule add weekly --every week --on fri --at 18:00 --project ~/code/my_tests
210
+ raider schedule list [--json] # each schedule, its next run, last result and OS state
211
+ raider schedule run nightly # run it now, the way the scheduler does
212
+ raider schedule remove nightly
242
213
  ```
243
214
 
244
- - **Rules:** `--every hour|day|weekday|week`. `--at HH:MM` (24h, local time) is required for `day`, `weekday` and `week`; for `hour` use `--at :MM` (default `:00`). `--on mon|tue|wed|thu|fri|sat|sun` is required for `week` and refused otherwise. `--retries N` (0 to 3) and `--input name=value` are passed to `raider flow run`. Scheduling a flow again replaces its schedule.
245
- - **Schedulers:** macOS uses launchd (`~/Library/LaunchAgents/dev.rubyraider.flow.<hash>.<name>.plist`). Windows uses Task Scheduler (a task in the `\RubyRaider\` folder). Linux uses a systemd user timer in `~/.config/systemd/user`, or, when `systemctl --user` does not answer, one tagged line in your crontab. Times are local time (cron uses the system time zone).
246
- - **Missed runs:** launchd runs a run missed while the Mac slept once on wake. Task Scheduler runs it as soon as it can. systemd (`Persistent=true`) runs it once your user manager is back, at the next boot or login. cron skips it, and `raider flow schedules` says so.
247
- - **Logged in:** on macOS (a launchd agent in your GUI session) and Windows (a task that runs only while you are logged on) a run happens only while you are logged in.
248
- - **Linux:** systemd user timers stop when you log out unless lingering is on: `loginctl enable-linger $USER` (raider does not run it). A systemd schedule can only be removed from a session where `systemctl --user` works; until then `unschedule` refuses and keeps the entry.
249
- - **Per machine:** schedules live in `.raider/flows/schedules.yml`, which `raider flow init` git-ignores, so a fresh clone has none. schedules.yml is the source of truth, and the next run is worked out from the rule. Each OS entry is tied to the project's path: unschedule before you move the project.
250
- - **States:** `ok`, `missing from launchd` (or `Task Scheduler`, `systemd`, `cron`) when the OS entry is gone, `missing from schedules.yml` when the OS still runs a flow the file does not list, `Ruby not found: re-schedule`, `flow not found: unschedule` when `flows/<name>_flow.rb` was deleted or renamed, and `unknown (...)` when the scheduler could not be asked.
251
- - **Ruby:** the OS entry runs the absolute `ruby` and `bundle` paths captured when you scheduled, plus `GEM_HOME` and `GEM_PATH` on macOS and Linux. After a Ruby upgrade or move, `schedules` says `Ruby not found: re-schedule`: run `raider flow schedule` again.
252
- - **Logs:** on macOS and Linux the scheduler's output goes to `.raider/flows/<name>/scheduler.log`. Windows tasks run ruby.exe directly with your user environment and keep no scheduler.log; each run's result is in `runs.jsonl` as always.
253
- - **Notify:** `--notify` shows a desktop notification only when a run ends failed or could not start (not when it completed, was already running, or was stopped). macOS uses `osascript`, Linux `notify-send`, Windows a PowerShell toast. When a run could not start, the notification points at `.raider/flows/<name>/scheduler.log` for a scheduled run on macOS and Linux, and at the run output otherwise. A notification that can't be shown prints a warning and never changes the exit code. On Linux `notify-send` needs your desktop session bus: under cron, and in a systemd user session that has not imported it, it usually can't show, and only the warning lands in scheduler.log. `raider flow run --notify` does the same for a manual run.
254
- - **Secrets:** keep them in `.raider/flows/env`, never in `--input`: inputs are stored in plain text in the OS entry and in schedules.yml.
255
- - **Windows refusals:** Task Scheduler expands `%NAME%` with no way to escape it, so an input or path holding `%...%` is refused. An argument holding both `'` and one of `* ? [ {` is refused too, because ruby.exe would glob it.
256
- - **Tested on:** macOS (launchd) is tested against the real scheduler. Task Scheduler, systemd and cron are covered by unit tests with a fake scheduler only.
215
+ - **Rules:** `--every hour|day|weekday|week`. `--at HH:MM` (24h, local time) is required for `day`, `weekday` and `week`; for `hour` use `--at :MM` (default `:00`). `--on mon|tue|wed|thu|fri|sat|sun` is required for `week` and refused otherwise. Names use 1 to 60 of `a-z`, `0-9` and `_`. Adding a name again replaces its schedule.
216
+ - **What runs:** no files and no `--tag` runs all tests. `--tag TAG` runs one tag (letters, digits, `_`, `:` and `-`). Spec files are relative paths inside the project ending in `_spec.rb`, at most 200, and they can go before or after the options. `--tag` and files can't be used together.
217
+ - **Your project's gem:** the scheduler runs `bundle exec raider schedule run` in your project, so `add` checks its `Gemfile.lock` first and refuses an older ruby_raider ("Scheduling needs ruby_raider X or later in this project: bundle update ruby_raider"), or a project with no `Gemfile.lock` yet (run `bundle install` first).
218
+ - **`--project PATH`:** works from any folder. Put it after the subcommand (`raider schedule add nightly ... --project DIR`); before the subcommand Thor shows the help instead.
219
+ - **Per machine:** schedules live in `.raider/schedules/schedules.yml`, which `add` git-ignores (it holds machine paths; projects made with 4.2 have the line already), so a fresh clone has none. The OS entry holds only the schedule's name: `raider schedule run NAME --project DIR --trigger schedule`. The selection, retries, notify and show-the-browser setting are read from schedules.yml at each run, which checks every entry again before anything starts.
220
+ - **Show the browser:** scheduled runs are hidden (`HEADLESS=true`) unless you add `--show-browser`. It shows the browser whatever `headless` says in `config/config.yml`, and no driver change is needed: the runtime handles it (`RAIDER_SHOW_BROWSER=1` drops `--headless`). Neither config.yml nor `helpers/driver_helper.rb` is written.
221
+
222
+ | Project Headless (config.yml) | Show the browser off | Show the browser on |
223
+ |---|---|---|
224
+ | off / absent | `HEADLESS=true`: hidden | `RAIDER_SHOW_BROWSER=1`, `HEADLESS` removed: visible |
225
+ | on | `HEADLESS=true`: hidden | `RAIDER_SHOW_BROWSER=1`: visible (the runtime drops `--headless`) |
226
+
227
+ - **Where a browser can show:** on macOS while you are logged in (a launchd agent runs in your GUI session). On Windows the task runs in your session while you are logged on; a visible browser there is not verified yet. On Linux `--show-browser` is refused ("Show the browser isn't available for scheduled runs on Linux"): systemd user units and cron have no display. A project made before 4.0 is refused too ("Show the browser needs a project made with Ruby Raider 4.0 or later").
228
+ - **Retries:** `--retries N` (0 to 3) re-runs each failed test up to N times inside the same run, with rspec-retry (`RETRY_COUNT` is N + 1). From 4.2 the runtime gives a retried test a fresh browser. Before 4.2, a retry in a generated project failed with a refused connection, because the driver was already quit. The fix is in the gem (`ruby_raider/runtime/rspec`), so it also applies to manual runs such as `RETRY_COUNT=2 bundle exec rspec`; `bundle update ruby_raider` brings it to existing projects. A stop doesn't cut rspec-retry's tries short: the test running when you stop may still get its remaining tries before the run ends.
229
+ - **Results:** a run is a normal `bundle exec rspec`, so its report goes to `raider-results/` and shows on the Dashboard. Each run also adds one line to `.raider/schedules/<name>/runs.jsonl` (`status` `passed`, `failed`, `setup_error` or `busy`, the counts and the trigger; the last 50 lines are kept), and the last report is copied to `.raider/schedules/<name>/last/` (run.json and index.html, plus the attachments when they come to 50 MB or less).
230
+ - **Exit codes:** `raider schedule run` exits 0 passed, 1 failed, 2 could not start or refused, 3 another run was going (another `raider schedule run` in the project, or any run that holds `.raider/schedules/running.json`). Only one scheduled run per project goes at a time, and a busy run leaves `raider-results/` alone. Other commands exit 0, or 2 with one line on stderr. Thor's own usage errors (a missing NAME, a bad `--retries`) exit 1 with Thor's text.
231
+ - **Notify:** `--notify` shows a desktop notification only when a run failed or could not start (not when it passed, was busy or was stopped). The title is "Ruby Raider: <name> failed"; the body says how many tests failed, or why the run could not start. macOS uses `osascript`, Linux `notify-send`, Windows a PowerShell toast. A notification that can't be shown prints a warning and never changes the exit code. On Linux `notify-send` needs your desktop session bus, so under cron, and in a systemd user session that has not imported it, it usually can't show.
232
+ - **Schedulers:** macOS uses launchd (`~/Library/LaunchAgents/dev.rubyraider.schedule.<hash>.<name>.plist`). Windows uses Task Scheduler (a task in the `\RubyRaider\` folder). Linux uses a systemd user timer in `~/.config/systemd/user`, or, when `systemctl --user` does not answer, one tagged line in your crontab. Times are local time (cron uses the system time zone).
233
+ - **Missed runs:** launchd runs a run missed while the Mac slept once on wake. Task Scheduler runs it as soon as it can. systemd (`Persistent=true`) runs it once your user manager is back, at the next boot or login. cron skips it, and `raider schedule list` says so.
234
+ - **Logged in:** on macOS and Windows a run happens only while you are logged in. On Linux, systemd user timers stop when you log out unless lingering is on: `loginctl enable-linger $USER` (raider does not run it). A systemd schedule can only be removed from a session where `systemctl --user` works; until then `remove` refuses and keeps the entry.
235
+ - **States:** `ok`, `missing from launchd` (or `Task Scheduler`, `systemd`, `cron`) when the OS entry is gone, `missing from schedules.yml` when the OS still runs a schedule the file does not list, `Ruby not found: re-schedule`, `test file not found: <file>` when a listed spec file was deleted or renamed, and `unknown (...)` when the scheduler could not be asked.
236
+ - **Ruby:** the OS entry runs the absolute `ruby` and `bundle` paths captured when you added it, plus `GEM_HOME` and `GEM_PATH` on macOS and Linux. After a Ruby upgrade or move, run `raider schedule add` again.
237
+ - **Logs:** on macOS and Linux the scheduler's output goes to `.raider/schedules/<name>/scheduler.log`, moved to `scheduler.log.1` at the start of a scheduled run once it is over 1 MB. Windows tasks keep no scheduler.log; each run's result is in `runs.jsonl` as always.
238
+ - **Windows refusals:** Task Scheduler and ruby.exe would expand `%...%`, `*`, `?`, `[` or `{`, so a spec file or path holding them is refused: rename the file or use a tag.
239
+ - **Moving the project:** each OS entry is tied to the project's path (`<hash>` is made from it). Run `raider schedule remove` before you move the project. Entries made before a move keep the old path's id, so `list` in the new place can't find them. Remove them by hand; their names start with `dev.rubyraider.schedule.`:
240
+ - macOS: `launchctl bootout gui/$UID/<label>`, then delete `~/Library/LaunchAgents/<label>.plist`.
241
+ - Windows: `schtasks /Delete /TN \RubyRaider\<id> /F`.
242
+ - Linux (systemd): `systemctl --user disable --now <id>.timer`, delete `<id>.timer` and `<id>.service` from `~/.config/systemd/user`, then `systemctl --user daemon-reload`.
243
+ - Linux (cron): `crontab -e` and delete the `# raider-schedule <id>` line and the line under it.
244
+ - **Tested on:** macOS (launchd) is tested against the real scheduler: adding, a run started by launchd hidden and shown with Headless on, a failed run with a notification, a busy run, and removing. Task Scheduler, systemd and cron are covered by unit tests with a fake scheduler only.
245
+
246
+ ### Upgrading from 4.1
247
+
248
+ - **Old schedules are removed automatically.** Schedules made with Ruby Raider 4.1 can't run in 4.2. They are removed the first time any `raider schedule` command runs in the project (such as `raider schedule list`), or the next time one of them fires. One line says which were removed.
249
+ - **Add them again** with `raider schedule add`.
250
+ - **Leftover folders:** `flows/`, `flow-results/` and `.raider/flows/` (it holds old secrets in `env`) are left alone. Delete them only after the line saying the old schedules were removed has appeared (run any `raider schedule` command, such as `raider schedule list`): the cleanup looks for `.raider/flows/`, and the old entries write their logs there.
251
+ - **A project moved after scheduling** can't be cleaned automatically: its entries are tied to the old path. Remove them by hand; their names start with `dev.rubyraider.flow.`:
252
+ - macOS: find them with `ls ~/Library/LaunchAgents | grep dev.rubyraider.flow.`, run `launchctl bootout gui/$(id -u)/<label>`, then delete `~/Library/LaunchAgents/<label>.plist` (`<label>` is `dev.rubyraider.flow.<hash>.<name>`).
253
+ - Windows: find them with `schtasks /Query /FO CSV | findstr dev.rubyraider.flow.`, then `schtasks /Delete /TN "\RubyRaider\<id>" /F`.
254
+ - Linux (systemd): find them with `ls ~/.config/systemd/user | grep dev.rubyraider.flow.` (or `$XDG_CONFIG_HOME/systemd/user` when it is set), run `systemctl --user disable --now <id>.timer`, delete `<id>.timer` and `<id>.service` from `~/.config/systemd/user`, then `systemctl --user daemon-reload`.
255
+ - Linux (cron): find them with `crontab -l | grep -A1 '# raider-flow '`, then `crontab -e` and delete each `# raider-flow <id>` line and the line under it.
257
256
 
258
257
  ### Also included
259
258
 
@@ -327,12 +326,12 @@ The skeleton keeps the helpers, config, CI and README, and its empty suite passe
327
326
  raider new [PROJECT_NAME] # Create a new framework project
328
327
  raider generate # Access scaffolding commands
329
328
  raider utility # Access utility commands
330
- raider flow # Run flows: init, run, list, schedule
329
+ raider schedule # Schedule the tests: add, list, remove, run
331
330
  raider version # Show current version
332
331
  raider help [COMMAND] # Describe available commands
333
332
  ```
334
333
 
335
- Shortcuts: `n` (new), `g` (generate), `u` (utility), `f` (flow), `v` (version)
334
+ Shortcuts: `n` (new), `g` (generate), `u` (utility), `sc` (schedule), `v` (version)
336
335
 
337
336
  ### Scaffolding Commands
338
337
 
data/RELEASE.md CHANGED
@@ -2,7 +2,20 @@
2
2
 
3
3
  ## Release Notes
4
4
 
5
- ### 4.1.0 (unreleased)
5
+ ### 4.2.0
6
+
7
+ - `raider schedule add NAME [SPEC_FILE ...] --every hour|day|weekday|week [--at HH:MM | :MM] [--on mon..sun] [--tag TAG] [--show-browser] [--retries 0..3] [--notify]` runs the project's tests with the computer's own scheduler (launchd, Task Scheduler, a systemd user timer or cron, as in 4.1). `raider schedule list [--json]`, `remove NAME` and `run NAME [--trigger manual|schedule]` complete it; every command takes `--project PATH`. `raider sc` is short for `raider schedule`.
8
+ - A schedule runs all tests, one tag (`--tag`), or up to 200 spec files given by path (relative, inside the project, ending `_spec.rb`); a tag and files can't be combined. Schedules live in `.raider/schedules/schedules.yml`, which `add` git-ignores; projects made with `raider new` 4.2 have the `.raider/schedules/` line from the start. The OS entry holds only the schedule's name: the selection and options are read from the yml, and checked again, at each run.
9
+ - `--show-browser` shows the browser in a scheduled run whatever the project's `headless` setting says, with no project file changed: when `RAIDER_SHOW_BROWSER=1`, the runtime drops `--headless` from the Chrome, Edge and Firefox options. Refused for projects made before 4.0 and on Linux; on macOS the user must be logged in, and Windows is unverified.
10
+ - `--retries N` re-runs each failed test up to N times inside the same run with rspec-retry (`RETRY_COUNT = N + 1`). A stop doesn't cut the tries of the test in flight short.
11
+ - `raider schedule add` refuses a project whose `Gemfile.lock` resolves an older ruby_raider, or that has no `Gemfile.lock` yet, so a schedule never fires into a bundle without `raider schedule`.
12
+ - Retried tests get a fresh browser. Before this, a retry in a generated 4.x project failed with a refused connection because the driver was already quit. The fix is in the runtime (`ruby_raider/runtime/rspec`), so it reaches every 4.x project on `bundle update ruby_raider` and also applies to manual `RETRY_COUNT=2 bundle exec rspec` runs.
13
+ - Scheduled results go to `raider-results/` (the Dashboard), one line per run in `.raider/schedules/<name>/runs.jsonl` (last 50 kept), and a copy of the last report in `.raider/schedules/<name>/last/`. `scheduler.log` is rotated once over 1 MB.
14
+ - `--notify` shows a desktop notification ("Ruby Raider: <name> failed") only when a run failed or could not start, with 4.1's channels.
15
+ - `raider schedule run` exits 0 passed, 1 failed, 2 could not start or refused, 3 another run was going: another `raider schedule run` in the project, or any run that holds `.raider/schedules/running.json` (a busy run leaves `raider-results/` alone).
16
+ - `raider schedule` runs your tests on a schedule. The 4.1 recording-without-checks commands are gone, and their schedules are removed automatically: the first `raider schedule` command in a project, or the old entry's next firing, removes them and prints one line. The folders they used are left for you to delete (see Upgrading from 4.1 in the README).
17
+
18
+ ### 4.1.0 (2026-11-10)
6
19
 
7
20
  - Flows: recordings without assertions that run on their own. `raider flow init` adds `flows/flow_helper.rb` and `flows/support/run.rb` to a Selenium web project made with 4.0 or later (and raises a `~> 4.0` Gemfile pin to `~> 4.1`); mobile and 3.x projects are refused with exit 2.
8
21
  - `raider flow run NAME` runs `flows/NAME_flow.rb` headless with `bundle exec rspec`, exiting 0 completed, 1 a step failed, 2 setup failed, 3 already running. `--headed`, `--retries N` (0 to 3, whole flow, 30 s apart), `--input name=value` (read with `flow_input`) and `--trigger manual|schedule|desktop`. `--project PATH` works from any folder.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../utilities/schedules/legacy'
5
+ require_relative '../utilities/schedules/scheduler'
6
+ require_relative '../utilities/schedules/run_wrapper'
7
+
8
+ # raider schedule: runs the project's tests (all, one tag or listed spec files) with the OS scheduler.
9
+ # Spec files are positional, never an array option, so they mix with the options in any order.
10
+ # Every command first removes the project's 4.1 schedules (Legacy), which never changes its exit code or stdout.
11
+ class ScheduleCommands < Thor
12
+ def self.exit_on_failure? = true
13
+
14
+ class_option :project, type: :string, desc: 'The project folder (default: the current folder)'
15
+
16
+ desc 'add NAME [SPEC_FILE ...]', 'Schedules the tests (all, --tag or the spec files), or replaces the schedule NAME'
17
+ option :every, type: :string, desc: 'hour, day, weekday or week'
18
+ option :at, type: :string, desc: 'HH:MM (24h, local time); for every hour :MM'
19
+ option :on, type: :string, desc: 'mon to sun, for every week'
20
+ option :tag, type: :string, desc: 'Run only the tests with this tag'
21
+ option :show_browser, type: :boolean, desc: 'Show the browser, whatever the Headless setting'
22
+ option :retries, type: :numeric, default: 0, desc: 'Re-run each failed test up to N times (0-3)'
23
+ option :notify, type: :boolean, desc: 'Show a desktop notification when a scheduled run fails'
24
+
25
+ def add(name, *files)
26
+ scheduler.add(name, every: options[:every], files: files, tag: options[:tag], at: options[:at], on: options[:on],
27
+ retries: options[:retries], notify: options[:notify] || false,
28
+ show_browser: options[:show_browser] || false)
29
+ rescue RubyRaider::Schedules::Error => e
30
+ stop(e)
31
+ end
32
+
33
+ desc 'list', 'Lists the schedules with their next run and last result'
34
+ option :json, type: :boolean, desc: 'Print JSON for the desktop app'
35
+
36
+ def list
37
+ project = new_project
38
+ notice = cleanup(project)
39
+ RubyRaider::Schedules::Scheduler.new(project).list(json: options[:json] || false, notice: notice)
40
+ rescue RubyRaider::Schedules::Error => e
41
+ stop(e)
42
+ end
43
+
44
+ desc 'remove NAME', 'Removes the schedule NAME from the OS scheduler and schedules.yml'
45
+
46
+ def remove(name)
47
+ scheduler.remove(name)
48
+ rescue RubyRaider::Schedules::Error => e
49
+ stop(e)
50
+ end
51
+
52
+ # `run` is a Thor reserved word, so the method has another name
53
+ desc 'run NAME', 'Runs the schedule NAME now: exit 0 passed, 1 failed, 2 setup error, 3 busy'
54
+ option :trigger, type: :string, default: 'manual', desc: 'manual or schedule (what the OS entry passes)'
55
+
56
+ def run_schedule(name)
57
+ exit(RubyRaider::Schedules::RunWrapper.new(cleaned_project).run(name, trigger: options[:trigger]))
58
+ rescue RubyRaider::Schedules::Error => e
59
+ stop(e)
60
+ end
61
+
62
+ map 'run' => :run_schedule
63
+
64
+ no_commands do
65
+ def new_project = RubyRaider::Schedules::Project.new(options[:project] || Dir.pwd)
66
+
67
+ # The project, once its 4.1 schedules are cleaned up
68
+ def cleaned_project = new_project.tap { |project| cleanup(project) }
69
+
70
+ # Removes the project's 4.1 schedules; the notice (on stderr already), nil when there was nothing to remove.
71
+ # Without the 4.1 folder (Legacy.folder?) there is none, so that costs one lstat.
72
+ def cleanup(project)
73
+ RubyRaider::Schedules::Legacy.new(project).cleanup if RubyRaider::Schedules::Legacy.folder?(project)
74
+ end
75
+
76
+ def scheduler = RubyRaider::Schedules::Scheduler.new(cleaned_project)
77
+
78
+ # One stderr line, whatever the message holds (a --project path can carry a newline)
79
+ def stop(error)
80
+ warn(RubyRaider::Schedules.printable(error.message))
81
+ exit(error.exit_code)
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,6 @@
1
1
  raider-results
2
2
  spec/examples.txt
3
+ .raider/schedules/
3
4
  <%- if web? -%>
4
5
  .raider/auth/
5
6
  <%- end -%>
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raider
4
+ # rspec-retry runs a retried example again in the same example group instance. The project's after
5
+ # hook has quit the driver by then, and its driver_helper memoises it in @driver, so the retry would
6
+ # reuse a dead session. Before each retry this clears @driver: the next `driver` call starts a fresh
7
+ # one. Nothing is quit here.
8
+ #
9
+ # Installed from a before(:suite) hook, so it sees the final configuration whatever the require
10
+ # order. Without rspec-retry it does nothing. A retry_callback the project set is kept as it is,
11
+ # and then the project owns the reset.
12
+ #
13
+ # It relies on the after hook quitting the driver. A project whose after hook does not quit it leaves
14
+ # one browser open per retry (the cleared @driver is never quit); such a project should set its own
15
+ # retry_callback that quits and clears the driver.
16
+ module RetryReset
17
+ CALLBACK = proc { |_example| @driver = nil }
18
+
19
+ module_function
20
+
21
+ # True when the callback was set
22
+ def install(config = ::RSpec.configuration)
23
+ return false unless config.respond_to?(:retry_callback=)
24
+ return false if config.retry_callback
25
+
26
+ config.retry_callback = CALLBACK
27
+ true
28
+ end
29
+ end
30
+ end
31
+
32
+ ::RSpec.configure { |config| config.before(:suite) { Raider::RetryReset.install } } if defined?(::RSpec::Core)
@@ -7,6 +7,7 @@ require_relative 'matchers/element'
7
7
  require_relative 'matchers/page'
8
8
  require_relative 'matchers/presence'
9
9
  require_relative 'matchers/retrying_block'
10
+ require_relative 'retry_reset'
10
11
 
11
12
  module Raider
12
13
  # Web-first RSpec matchers that retry until the page settles.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raider
4
+ # Show the browser whatever the project's headless setting says. A scheduled or desktop run sets
5
+ # RAIDER_SHOW_BROWSER=1; the project's driver_helper still adds --headless, and the filter prepended
6
+ # here drops it. config.yml and driver_helper.rb are never written. Unset, nothing is prepended.
7
+ module ShowBrowser
8
+ ENV_KEY = 'RAIDER_SHOW_BROWSER'
9
+ OPTIONS = %w[Chrome Edge Firefox].map { |browser| "Selenium::WebDriver::#{browser}::Options" }.freeze
10
+ SWITCHES = %w[--headless -headless].freeze
11
+ PREFIX = '--headless='
12
+ ARGS_KEYS = [:args, 'args'].freeze
13
+
14
+ module_function
15
+
16
+ def headless?(arg) = arg.is_a?(String) && (SWITCHES.include?(arg) || arg.start_with?(PREFIX))
17
+
18
+ def without_headless(args) = args.is_a?(Array) ? args.reject { |arg| headless?(arg) } : args
19
+
20
+ # The Options classes the filter is on; empty unless the env value is exactly '1'
21
+ def install(env = ENV)
22
+ return [] unless env[ENV_KEY] == '1'
23
+
24
+ OPTIONS.filter_map do |name|
25
+ next unless Object.const_defined?(name)
26
+
27
+ klass = Object.const_get(name)
28
+ klass.prepend(Filter) unless klass.include?(Filter)
29
+ klass
30
+ end
31
+ end
32
+
33
+ # Drops the headless switches from args: given to new, from add_argument and from args=
34
+ # :reek:ModuleInitialize -- prepended to the Options classes: its initialize wraps theirs
35
+ module Filter
36
+ def initialize(**opts)
37
+ ARGS_KEYS.each { |key| opts[key] = ShowBrowser.without_headless(opts[key]) if opts.key?(key) }
38
+ super
39
+ end
40
+
41
+ # The args, as Selenium's own add_argument returns them, also when the switch is dropped
42
+ def add_argument(arg)
43
+ return options[:args] if ShowBrowser.headless?(arg)
44
+
45
+ super
46
+ end
47
+
48
+ def args=(value)
49
+ super(ShowBrowser.without_headless(value))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -14,7 +14,9 @@ require_relative 'runtime/observer'
14
14
  require_relative 'runtime/actionability'
15
15
  require_relative 'runtime/strict'
16
16
  require_relative 'runtime/actionable'
17
+ require_relative 'runtime/show_browser'
17
18
 
18
19
  Raider::Finders.check_api
19
20
  Raider::Finders.install
20
21
  Raider::Finders.guard
22
+ Raider::ShowBrowser.install
data/lib/ruby_raider.rb CHANGED
@@ -5,23 +5,38 @@ require 'thor'
5
5
  # Lazy-load top-level command classes to reduce CLI startup time.
6
6
  autoload :ScaffoldingCommands, File.expand_path('commands/scaffolding_commands', __dir__)
7
7
  autoload :UtilityCommands, File.expand_path('commands/utility_commands', __dir__)
8
- autoload :FlowCommands, File.expand_path('commands/flow_commands', __dir__)
8
+ autoload :ScheduleCommands, File.expand_path('commands/schedule_commands', __dir__)
9
9
 
10
10
  module RubyRaider
11
11
  class Raider < Thor
12
12
  def self.exit_on_failure? = true
13
13
 
14
- # Thor runs a regex on every argument before any command, so invalid UTF-8 would crash it with a
15
- # backtrace; a flow command says which argument instead and exits 2 like its other refusals
14
+ # The commands whose arguments are checked for valid UTF-8 before Thor sees them
15
+ GUARDED = %w[schedule sc].freeze
16
+ # What a Ruby Raider 4.1 OS entry runs (`raider flow run ...`): a hidden stub, not a Thor command
17
+ LEGACY = %w[flow f].freeze
18
+
19
+ # The 4.1 stub goes first, whatever its argv holds. Thor runs a regex on every argument before any
20
+ # command, so invalid UTF-8 would crash it with a backtrace; a schedule command says which argument
21
+ # instead and exits 2 like its other refusals
16
22
  def self.start(given_args = ARGV, config = {})
23
+ exit(legacy_stub(given_args)) if LEGACY.include?(given_args.first)
17
24
  bad = given_args.find { |arg| arg.is_a?(String) && !arg.valid_encoding? }
18
- if bad && %w[flow f].include?(given_args.first)
25
+ if bad && GUARDED.include?(given_args.first)
19
26
  warn("Invalid argument #{bad.inspect}: use valid UTF-8")
20
27
  exit(2)
21
28
  end
22
29
  super
23
30
  end
24
31
 
32
+ # Removes the project's 4.1 schedules and says why the command is gone; always 2
33
+ def self.legacy_stub(given_args)
34
+ require_relative 'utilities/schedules/legacy_stub'
35
+ Schedules::LegacyStub.run(given_args.drop(1))
36
+ rescue StandardError, ScriptError
37
+ 2
38
+ end
39
+
25
40
  no_tasks do
26
41
  def current_version = File.read(File.expand_path('version', __dir__)).strip
27
42
  end
@@ -63,9 +78,9 @@ module RubyRaider
63
78
  subcommand 'utility', UtilityCommands
64
79
  map 'u' => 'utility'
65
80
 
66
- desc 'flow', 'Runs flows: recordings without assertions (init, run, list, schedule)'
67
- subcommand 'flow', FlowCommands
68
- map 'f' => 'flow'
81
+ desc 'schedule', 'Runs the tests on a schedule with the OS scheduler (add, list, remove, run)'
82
+ subcommand 'schedule', ScheduleCommands
83
+ map 'sc' => 'schedule'
69
84
 
70
85
  no_tasks do
71
86
  # -p values arrive as strings; examples must be true or false, in any case
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+ require_relative 'base'
5
+ require_relative 'os/command_runner'
6
+ require_relative 'os/launchd'
7
+ require_relative 'os/task_scheduler'
8
+ require_relative 'os/linux'
9
+
10
+ module RubyRaider
11
+ module Schedules
12
+ # The OS scheduler for this machine, for schedules of the kind; only Linux asks the system (is the user's systemd there?)
13
+ def self.adapter(kind:, runner: Os::CommandRunner.new, host_os: RbConfig::CONFIG['host_os'])
14
+ case host_os
15
+ when /darwin/ then Os::Launchd.new(kind: kind, runner: runner)
16
+ when /mswin|mingw|cygwin/ then Os::TaskScheduler.new(kind: kind, runner: runner)
17
+ when /linux/ then Os::Linux.pick(runner, kind: kind)
18
+ else raise Error, kind.unsupported(host_os)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRaider
4
+ # The OS scheduler (launchd, Task Scheduler, systemd, cron) shared by test schedules and the 4.1 ones
5
+ # the cleanup removes. What differs between them is a Kind.
6
+ module Schedules
7
+ NAME = /\A[a-z0-9_]{1,60}\z/
8
+ # Schedule::DAYS order, as every adapter counts it
9
+ DAYS = %w[mon tue wed thu fri sat sun].freeze
10
+
11
+ def self.valid_name?(name) = NAME.match?(name.to_s)
12
+
13
+ # Text from a log or a command, safe to print: it can't move the cursor or forge lines, and bytes that
14
+ # are not valid in its encoding become ? too (a regexp would raise on them)
15
+ def self.printable(text) = text.to_s.scrub('?').gsub(/[[:cntrl:]]/, '?')
16
+
17
+ # A problem that stops a schedule command; exit_code follows `raider schedule run` (2 = setup failed).
18
+ class Error < StandardError
19
+ attr_reader :exit_code
20
+
21
+ def initialize(message, exit_code: 2)
22
+ super(message)
23
+ @exit_code = exit_code
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module RubyRaider
6
+ module Schedules
7
+ # What kind of schedule an OS entry belongs to: the words that name it in each scheduler (os_id prefix,
8
+ # cron tag, descriptions), the command it runs, where its files live and which names it takes.
9
+ # Adapters get one, so an entry of one kind is never read or removed as another.
10
+ # :reek:TooManyInstanceVariables { max_instance_variables: 9 } -- the words, folder and reserved names of one kind
11
+ class Kind
12
+ attr_reader :command, :os_id_prefix, :os_id_pattern, :dir, :noun, :plural, :remove_command, :stranded_hint, :reserved
13
+
14
+ # command is the raider subcommand (`raider <command> run NAME`); it also names the os_id prefix and tags.
15
+ # stranded_hint: what to run for a stranded timer, when it is not `<remove_command> NAME`
16
+ def initialize(command:, dir:, noun:, plural:, remove_command:, stranded_hint: nil, reserved: [])
17
+ @command = command.dup.freeze
18
+ @os_id_prefix = "dev.rubyraider.#{command}".freeze
19
+ @os_id_pattern = /\A#{Regexp.escape(@os_id_prefix)}\.[0-9a-f]{10}\.([a-z0-9_]{1,60})\z/
20
+ @dir = dir.dup.freeze
21
+ @noun = noun.dup.freeze
22
+ @plural = plural.dup.freeze
23
+ @remove_command = remove_command.dup.freeze
24
+ @stranded_hint = stranded_hint&.dup&.freeze
25
+ @reserved = reserved.map { |word| word.dup.freeze }.freeze
26
+ freeze
27
+ end
28
+
29
+ def valid_name?(name) = Schedules.valid_name?(name) && !reserved.include?(name.to_s)
30
+
31
+ # The name in an os_id of this kind, nil when it is not one
32
+ def name_in(os_id)
33
+ # Text from the OS (file names, schtasks, crontab) may not be valid UTF-8; a regexp would raise on it
34
+ return nil unless os_id.is_a?(String) && os_id.valid_encoding?
35
+
36
+ name = os_id_pattern.match(os_id)&.[](1)
37
+ name if name && valid_name?(name)
38
+ end
39
+
40
+ # An os_id of this kind that starts with prefix (one project's: "<os_id_prefix>.<hash10>.")
41
+ def listed?(os_id, prefix) = !name_in(os_id).nil? && os_id.to_s.start_with?(prefix)
42
+
43
+ def cron_tag = "# raider-#{command} "
44
+
45
+ # The command line under a cron tag runs `raider <command> run <name> --project ... --trigger schedule`
46
+ def our_command?(text, name) = text.include?(" exec raider #{command} run #{name} --project ") && text.include?(' --trigger schedule')
47
+
48
+ def task_description(name, rule) = "Ruby Raider #{command} #{name} (#{rule})"
49
+
50
+ def task_file_prefix = "raider-#{command}-task"
51
+
52
+ def service_description(name) = "raider #{command} #{name}"
53
+
54
+ def timer_description(name) = "raider #{command} #{name} schedule"
55
+
56
+ def not_ours = "not a #{noun}"
57
+
58
+ def stranded(name) = "This schedule is a systemd timer; run #{stranded_hint || "#{remove_command} #{name}"} from a session where systemctl --user works"
59
+
60
+ def unsupported(host_os) = "Scheduling #{plural} is not supported on #{host_os}"
61
+
62
+ # The schedules Ruby Raider 4.1 made (`raider flow schedule`), only ever removed now, by the cleanup every
63
+ # `raider schedule` command runs first (Legacy). The words the user reads never say flow; the command
64
+ # (os_id prefix, cron tag, the line under it) is what 4.1 wrote.
65
+ # remove_command is never printed for it: stranded_hint overrides it in stranded (list takes no name).
66
+ FLOW_LEGACY = new(command: 'flow', dir: '.raider/flows', noun: '4.1 schedule', plural: '4.1 schedules',
67
+ remove_command: 'raider schedule list', stranded_hint: 'raider schedule list', reserved: %w[env])
68
+
69
+ # The schedules of `raider schedule`: the project's tests, all, one tag or listed spec files
70
+ TESTS = new(command: 'schedule', dir: '.raider/schedules', noun: 'test schedule', plural: 'tests',
71
+ remove_command: 'raider schedule remove')
72
+ end
73
+ end
74
+ end