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
@@ -0,0 +1,45 @@
1
+ module RubyRaider
2
+ module Schedules
3
+ class RunWrapper
4
+ EXIT_CODES: Hash[String, Integer]
5
+ TRIGGERS: Array[String]
6
+ LOG_BYTES: Integer
7
+ MISSING: String
8
+ ZERO: Hash[String, Integer]
9
+ SPAWNER: ^(Hash[String, String?], Array[String], String) -> [Integer?, bool]
10
+
11
+ @project: Project
12
+ @out: IO | StringIO
13
+ @err: IO | StringIO
14
+ @spawner: untyped
15
+ @clock: ^() -> Time
16
+ @notifier: untyped
17
+ @lock: RunLock
18
+
19
+ def initialize: (Project project, ?out: IO | StringIO, ?err: IO | StringIO, ?spawner: untyped, ?clock: ^() -> Time, ?notifier: untyped) -> void
20
+ def run: (String name, ?trigger: String) -> Integer
21
+
22
+ private
23
+
24
+ def load: (String name) -> Schedule
25
+ def attempt: (Schedule schedule, Hash[String, String] record) -> Hash[Symbol, untyped]
26
+ def stopped: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome) -> Hash[Symbol, untyped]
27
+ def spawn: (Schedule schedule) -> Hash[Symbol, untyped]
28
+ def status: (Integer? code, bool stopped, Hash[String, Integer]? counts) -> String
29
+ def argv: (Schedule schedule) -> Array[String]
30
+ def env: (Schedule schedule) -> Hash[String, String?]
31
+ def gem_env: (Schedule schedule) -> Hash[String, String?]
32
+ def copy_report: (Schedule schedule, RunReport? report) -> String?
33
+ def append: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome) -> void
34
+ def finish: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome) -> Integer
35
+ def append_quietly: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome) -> void
36
+ def result_text: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome) -> String
37
+ def failures: (Hash[String, Integer] counts) -> String
38
+ def tests: (Integer total) -> String
39
+ def notify: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome) -> bool
40
+ def start_reason: (Schedule schedule, Hash[String, String] record, Hash[Symbol, untyped] outcome, long: bool) -> String
41
+ def rotate_log: (Schedule schedule) -> void
42
+ def stamp: () -> String
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module RubyRaider
2
+ module Schedules
3
+ class Schedule < Rule
4
+ KIND: Kind
5
+ KEYS: Array[String]
6
+ LINUX: Regexp
7
+ NO_BROWSER_ON_LINUX: String
8
+
9
+ attr_reader show_browser: bool
10
+ attr_reader selection: Selection
11
+
12
+ def self.kind: () -> Kind
13
+ def self.build: (project: Project, name: String, every: String, ?at: String?, ?on: String?, ?retries: untyped, ?notify: bool, ?show_browser: bool,
14
+ ?selection: Selection, ?now: Time, ?host_os: String) -> Schedule
15
+ def self.from_h: (untyped hash, String project_root, ?host_os: String) -> Schedule
16
+
17
+ def initialize: (name: untyped, every: untyped, at: untyped, on: untyped, retries: untyped, selection: untyped, os_id: untyped, ruby: untyped, bundle: untyped,
18
+ gem_env: untyped, project_root: untyped, created_at: untyped, ?notify: untyped, ?show_browser: untyped) -> void
19
+ def to_h: () -> Hash[String, untyped]
20
+
21
+ private
22
+
23
+ def checked_name: (untyped name) -> String
24
+ def checked_selection: (untyped selection) -> Selection
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module RubyRaider
2
+ module Schedules
3
+ class ScheduleStore < Store
4
+ DIR: String
5
+ GITIGNORE_LINE: String
6
+
7
+ def initialize: (Project project) -> void
8
+ def all: () -> Array[Schedule]
9
+ def find: (String name) -> Schedule?
10
+ def save: (Schedule schedule) -> Schedule
11
+ def delete: (String name) -> (Schedule | Store::Removed)?
12
+ def ignore_in_git: () -> bool
13
+
14
+ private
15
+
16
+ def gitignore: () -> String
17
+ def gitignore_text: (File file) -> String
18
+ def ignored?: (String content) -> bool
19
+ end
20
+ end
21
+ end
@@ -1,11 +1,15 @@
1
1
  module RubyRaider
2
- module Flows
2
+ module Schedules
3
3
  class Scheduler
4
- NOT_SET_UP: String
5
- RUBY_NOT_FOUND: String
6
- FLOW_NOT_FOUND: String
7
- LAST: Array[String]
4
+ include SchedulerSteps
5
+
6
+ KIND: Kind
8
7
  TIME: String
8
+ LIST_TIME: String
9
+ LAST: Array[String]
10
+ EMPTY: String
11
+ UNLISTED: String
12
+ RULE_KEYS: Array[String]
9
13
 
10
14
  type row = { json: Hash[String, untyped], text: String }
11
15
 
@@ -15,28 +19,31 @@ module RubyRaider
15
19
  @out: IO | StringIO
16
20
  @err: IO | StringIO
17
21
  @now: ^() -> Time
22
+ @host_os: String
18
23
 
19
24
  def self.adapter: (?runner: Os::CommandRunner, ?host_os: String) -> (Os::Launchd | Os::TaskScheduler | Os::Linux)
20
25
 
21
- def initialize: (Project project, ?adapter: untyped, ?store: ScheduleStore, ?out: IO | StringIO, ?err: IO | StringIO, ?now: ^() -> Time) -> void
22
- def schedule: (String name, every: String?, ?at: String?, ?on: String?, ?retries: untyped, ?inputs: Array[String], ?notify: bool) -> void
23
- def unschedule: (String name) -> void
24
- def schedules: (?json: bool) -> void
26
+ def initialize: (Project project, ?adapter: untyped, ?store: ScheduleStore, ?out: IO | StringIO, ?err: IO | StringIO, ?now: ^() -> Time,
27
+ ?host_os: String) -> void
28
+ def add: (String name, every: String?, ?files: Array[String], ?tag: String?, ?at: String?, ?on: String?, ?retries: untyped, ?notify: bool,
29
+ ?show_browser: bool) -> void
30
+ def remove: (String name) -> void
31
+ def list: (?json: bool, ?notice: String?) -> void
25
32
 
26
33
  private
27
34
 
35
+ attr_reader store: ScheduleStore
36
+
28
37
  def adapter: () -> untyped
29
- def save: (Schedule schedule) -> Schedule
30
- def delete: (String name, String label) -> (Schedule | ScheduleStore::Removed)?
31
- def flow_dir: (String name) -> void
32
- def private_dir: (String relative) -> void
38
+ def checked_name: (untyped name) -> void
39
+ def schedule_dirs: (String name) -> void
40
+ def ignore_in_git: () -> void
33
41
  def row: (Schedule schedule, Time now) -> row
34
42
  def unlisted_rows: (Array[String] listed) -> Array[row]
35
- def present?: (String os_id) -> bool
43
+ def unlisted_names: () -> Array[String]
36
44
  def os_state: (Schedule schedule) -> String
37
45
  def rule: (Schedule schedule) -> String
38
46
  def last_run: (String name) -> Hash[String, untyped]?
39
- def line: (String name, String rule, String next_run, Hash[String, untyped]? last, String state) -> String
40
47
  end
41
48
  end
42
49
  end
@@ -0,0 +1,22 @@
1
+ module RubyRaider
2
+ module Schedules
3
+ module SchedulerSteps
4
+ RUBY_NOT_FOUND: String
5
+
6
+ private
7
+
8
+ # Given by the includer
9
+ def adapter: () -> untyped
10
+ def store: () -> Store
11
+
12
+ def save: (untyped schedule) -> untyped
13
+ def delete: (String name, String label) -> untyped
14
+ def unreadable_warning: (String name, String label) -> String
15
+ def ruby_found?: (untyped schedule) -> bool
16
+ def os_entry_state: (String os_id) -> String
17
+ def present?: (String os_id) -> bool
18
+ def missed_runs_note: () -> String?
19
+ def line: (String name, String rule, String next_run, Hash[String, untyped]? last, String state) -> String
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ module RubyRaider
2
+ module Schedules
3
+ class Selection
4
+ KINDS: Array[String]
5
+ KEYS: Array[String]
6
+ TAG: Regexp
7
+ MAX_FILES: Integer
8
+ WINDOWS: Regexp
9
+ FILE_RULE: String
10
+ WINDOWS_RULE: String
11
+
12
+ attr_reader kind: String
13
+ attr_reader tag: String?
14
+ attr_reader files: Array[String]
15
+
16
+ def self.all: () -> Selection
17
+ def self.parse: (files: untyped, tag: untyped, root: String, ?host_os: String) -> Selection
18
+ def self.from_h: (untyped hash, root: String, ?host_os: String) -> Selection
19
+
20
+ def initialize: (kind: untyped, ?tag: untyped, ?files: untyped, ?host_os: String) -> void
21
+ def missing_file: (String root) -> String?
22
+ def check!: (String root) -> self
23
+ def to_h: () -> Hash[String, untyped]
24
+
25
+ private
26
+
27
+ def nothing: (String label, untyped value) -> nil
28
+ def checked_tag: (untyped tag) -> String
29
+ def checked_files: (untyped files, bool windows) -> Array[String]
30
+ def checked_file: (untyped file, bool windows) -> String
31
+ def spec_path?: (untyped file, bool windows) -> bool
32
+ def relative?: (String file, bool windows) -> bool
33
+ def present?: (String root, String file) -> bool
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ module RubyRaider
2
+ module Schedules
3
+ module Spawn
4
+ def self.call: (Hash[String, String?] env, Array[String] argv, String chdir, ?forward_int: bool) -> [Integer?, bool]
5
+ def self.run: (Hash[String, String?] env, Array[String] argv, String chdir, Hash[Symbol, untyped] state, bool forward_int) -> Integer?
6
+ def self.stop: (Hash[Symbol, untyped] state, String signal, bool forward_int) -> void
7
+ def self.interrupt: (Hash[Symbol, untyped] state) -> void
8
+ end
9
+ end
10
+ end
@@ -1,7 +1,6 @@
1
1
  module RubyRaider
2
- module Flows
3
- class ScheduleStore
4
- DIR: String
2
+ module Schedules
3
+ class Store
5
4
  FILE: String
6
5
  NOFOLLOW: Integer
7
6
  NONBLOCK: Integer
@@ -13,16 +12,22 @@ module RubyRaider
13
12
  def initialize: (String name, String os_id) -> void
14
13
  end
15
14
 
16
- def initialize: (Project project) -> void
17
- def all: () -> Array[Schedule]
18
- def find: (String name) -> Schedule?
19
- def save: (Schedule schedule) -> Schedule
15
+ @project: Project
16
+ @schedule_class: untyped
17
+ @dir: String
18
+ @file: String
19
+
20
+ def initialize: (Project project, untyped schedule_class) -> void
21
+ def all: () -> Array[untyped]
22
+ def find: (String name) -> untyped
23
+ def save: (untyped schedule) -> untyped
20
24
  def writable!: () -> nil
21
- def delete: (String name) -> (Schedule | Removed)?
25
+ def delete: (String name) -> untyped
22
26
 
23
27
  private
24
28
 
25
- def from_entry: (untyped entry) -> Schedule?
29
+ attr_reader project: Project
30
+ def from_entry: (untyped entry) -> untyped
26
31
  def named?: (untyped entry, String name) -> bool
27
32
  def read_entries: () -> Array[untyped]?
28
33
  def writable_entries: () -> Array[untyped]
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'tmpdir'
5
+ require_relative '../../lib/ruby_raider'
6
+ require_relative '../../lib/commands/schedule_commands'
7
+
8
+ # Runs in this process through RubyRaider::Raider.start, so the `raider schedule` subcommand path is the real
9
+ # one (thor 1.2.2). Every example stubs Scheduler.new, RunWrapper.new and Legacy.new (or gives Legacy a
10
+ # recording adapter): nothing reaches the OS scheduler or spawns rspec.
11
+ RSpec.describe ScheduleCommands do
12
+ let(:scheduler) { instance_double(RubyRaider::Schedules::Scheduler, add: nil, list: nil, remove: nil) }
13
+ let(:wrapper) { instance_double(RubyRaider::Schedules::RunWrapper, run: 0) }
14
+ let(:legacy) { instance_double(RubyRaider::Schedules::Legacy, cleanup: nil) }
15
+ let(:dir) { File.realpath(Dir.mktmpdir('schedule-commands')) }
16
+
17
+ before do
18
+ allow(RubyRaider::Schedules::Scheduler).to receive(:new).and_return(scheduler)
19
+ allow(RubyRaider::Schedules::RunWrapper).to receive(:new).and_return(wrapper)
20
+ allow(RubyRaider::Schedules::Legacy).to receive(:new).and_return(legacy)
21
+ end
22
+
23
+ after { FileUtils.rm_rf(dir) }
24
+
25
+ # [stdout, stderr, exit code or nil when the command did not exit]
26
+ def raider(*args)
27
+ out = StringIO.new
28
+ err = StringIO.new
29
+ stdout = $stdout
30
+ stderr = $stderr
31
+ $stdout = out
32
+ $stderr = err
33
+ code = begin
34
+ RubyRaider::Raider.start(args)
35
+ nil
36
+ rescue SystemExit => e
37
+ e.status
38
+ ensure
39
+ $stdout = stdout
40
+ $stderr = stderr
41
+ end
42
+ [out.string, err.string, code]
43
+ end
44
+
45
+ def project_root
46
+ satisfy { |project| project.root == dir }
47
+ end
48
+
49
+ describe 'add' do
50
+ it 'takes the spec files and the options in any order' do
51
+ raider('schedule', 'add', 'nightly', '--every', 'day', 'spec/a_spec.rb', '--show-browser', 'spec/b_spec.rb',
52
+ '--project', dir)
53
+
54
+ expect(scheduler).to have_received(:add).with('nightly', every: 'day', files: %w[spec/a_spec.rb spec/b_spec.rb],
55
+ tag: nil, at: nil, on: nil, retries: 0, notify: false,
56
+ show_browser: true)
57
+ expect(RubyRaider::Schedules::Scheduler).to have_received(:new).with(project_root)
58
+ end
59
+
60
+ it 'passes --at, --on, --tag, --retries and --notify' do
61
+ raider('schedule', 'add', 'smoke', '--tag', 'smoke', '--every', 'week', '--on', 'mon', '--at', '08:30',
62
+ '--retries', '2', '--notify', '--project', dir)
63
+
64
+ expect(scheduler).to have_received(:add).with('smoke', every: 'week', files: [], tag: 'smoke', at: '08:30',
65
+ on: 'mon', retries: 2, notify: true, show_browser: false)
66
+ end
67
+
68
+ it 'is reached through raider sc' do
69
+ raider('sc', 'add', 'nightly', '--every', 'day', '--project', dir)
70
+
71
+ expect(scheduler).to have_received(:add).with('nightly', hash_including(every: 'day', files: []))
72
+ end
73
+
74
+ it 'exits 2 with one stderr line when --every is left out' do
75
+ # The real Scheduler refuses; its adapter is a double that fails on any call
76
+ allow(RubyRaider::Schedules::Scheduler).to receive(:new).and_wrap_original do |original, project|
77
+ original.call(project, adapter: instance_double(RubyRaider::Schedules::Os::Launchd), out: $stdout, err: $stderr)
78
+ end
79
+
80
+ out, err, code = raider('schedule', 'add', 'nightly', '--at', '08:00', '--project', dir)
81
+
82
+ expect([out, err, code]).to eq(['', "Invalid every nil: use hour, day, weekday or week\n", 2])
83
+ end
84
+
85
+ it 'exits 2 with one stderr line when the project has no Gemfile, before the OS scheduler is touched' do
86
+ allow(RubyRaider::Schedules::Scheduler).to receive(:new).and_wrap_original do |original, project|
87
+ original.call(project, adapter: instance_double(RubyRaider::Schedules::Os::Launchd), out: $stdout, err: $stderr)
88
+ end
89
+ version = File.read(File.expand_path('../../lib/version', __dir__)).strip
90
+
91
+ out, err, code = raider('schedule', 'add', 'nightly', '--every', 'day', '--at', '08:00', '--project', dir)
92
+
93
+ expect([out, err, code]).to eq(['', "Scheduling needs ruby_raider #{version} or later in this project: no Gemfile found\n", 2])
94
+ expect(File.exist?(File.join(dir, '.raider'))).to be(false)
95
+ end
96
+ end
97
+
98
+ describe 'refusals' do
99
+ it 'exits 2 with the error on one stderr line' do
100
+ allow(scheduler).to receive(:remove).and_raise(RubyRaider::Schedules::Error, 'No schedule gone')
101
+
102
+ out, err, code = raider('schedule', 'remove', 'gone', '--project', dir)
103
+
104
+ expect([out, err, code]).to eq(['', "No schedule gone\n", 2])
105
+ end
106
+
107
+ it 'keeps a --project with a newline on one stderr line' do
108
+ out, err, code = raider('schedule', 'list', '--project', "/nonexistent/a\nb")
109
+
110
+ expect([out, err, code]).to eq(['', "Project folder not found: /nonexistent/a?b\n", 2])
111
+ expect(RubyRaider::Schedules::Scheduler).not_to have_received(:new)
112
+ expect(RubyRaider::Schedules::Legacy).not_to have_received(:new)
113
+ end
114
+
115
+ it 'exits 2 for an argument that is not valid UTF-8, through schedule and sc' do
116
+ results = [raider('schedule', 'add', "a\xFF", '--every', 'day'), raider('sc', 'remove', "a\xFF")]
117
+
118
+ expect(results).to all(eq(['', "Invalid argument \"a\\xFF\": use valid UTF-8\n", 2]))
119
+ expect(RubyRaider::Schedules::Scheduler).not_to have_received(:new)
120
+ end
121
+ end
122
+
123
+ describe 'list and remove' do
124
+ it 'lists as text, or as JSON with --json' do
125
+ raider('schedule', 'list', '--project', dir)
126
+ raider('schedule', 'list', '--json', '--project', dir)
127
+
128
+ expect(scheduler).to have_received(:list).with(json: false, notice: nil).ordered
129
+ expect(scheduler).to have_received(:list).with(json: true, notice: nil).ordered
130
+ end
131
+
132
+ it 'removes by name' do
133
+ raider('schedule', 'remove', 'nightly', '--project', dir)
134
+
135
+ expect(scheduler).to have_received(:remove).with('nightly')
136
+ end
137
+ end
138
+
139
+ describe 'run' do
140
+ it 'maps run to the wrapper with the manual trigger by default' do
141
+ _out, _err, code = raider('schedule', 'run', 'nightly', '--project', dir)
142
+
143
+ expect(code).to eq(0)
144
+ expect(wrapper).to have_received(:run).with('nightly', trigger: 'manual')
145
+ expect(RubyRaider::Schedules::RunWrapper).to have_received(:new).with(project_root)
146
+ end
147
+
148
+ it "exits with the wrapper's code and passes --trigger" do
149
+ codes = [1, 2, 3].map do |status|
150
+ allow(wrapper).to receive(:run).and_return(status)
151
+ raider('sc', 'run', 'nightly', '--trigger', 'schedule', '--project', dir).last
152
+ end
153
+
154
+ expect(codes).to eq([1, 2, 3])
155
+ expect(wrapper).to have_received(:run).with('nightly', trigger: 'schedule').exactly(3).times
156
+ end
157
+
158
+ it 'exits 2 with one stderr line when the wrapper refuses' do
159
+ allow(wrapper).to receive(:run).and_raise(RubyRaider::Schedules::Error, 'Invalid trigger "x": use manual or schedule')
160
+
161
+ out, err, code = raider('schedule', 'run', 'nightly', '--trigger', 'x', '--project', dir)
162
+
163
+ expect([out, err, code]).to eq(['', "Invalid trigger \"x\": use manual or schedule\n", 2])
164
+ end
165
+ end
166
+
167
+ describe 'the 4.1 cleanup' do # rubocop:disable RSpec/MultipleMemoizedHelpers
168
+ # Plays the OS scheduler for Legacy: one 4.1 entry of this project, removed or refused
169
+ let(:fake_adapter_class) do
170
+ Class.new do
171
+ attr_reader :ids, :removed
172
+ attr_accessor :failure
173
+
174
+ def initialize(ids)
175
+ @ids = ids
176
+ @removed = []
177
+ end
178
+
179
+ def list_ids(prefix) = ids.select { |id| id.start_with?(prefix) }
180
+
181
+ def remove(os_id)
182
+ raise failure if failure
183
+
184
+ @removed << os_id
185
+ true
186
+ end
187
+ end
188
+ end
189
+ let(:adapter) { fake_adapter_class.new([RubyRaider::Schedules::Legacy::OldRule.os_id_for(dir, 'tredssd')]) }
190
+ let(:notice) { "Removed 1 schedule made with Ruby Raider 4.1 (tredssd); it can't run in 4.2. Add it again with raider schedule add." }
191
+
192
+ before do
193
+ FileUtils.mkdir_p(File.join(dir, '.raider', 'flows'))
194
+ allow(RubyRaider::Schedules::Legacy).to receive(:new).and_wrap_original do |original, project|
195
+ original.call(project, adapter: adapter)
196
+ end
197
+ end
198
+
199
+ it 'runs before every command, with the project' do
200
+ allow(RubyRaider::Schedules::Legacy).to receive(:new).and_return(legacy)
201
+ [%w[add nightly --every day], %w[list], %w[remove nightly], %w[run nightly]].each do |args|
202
+ raider('schedule', *args, '--project', dir)
203
+ end
204
+
205
+ expect(RubyRaider::Schedules::Legacy).to have_received(:new).with(project_root).exactly(4).times
206
+ expect(legacy).to have_received(:cleanup).with(no_args).exactly(4).times
207
+ end
208
+
209
+ it 'cleans up before the command runs' do
210
+ allow(scheduler).to receive(:remove) { expect(adapter.removed.size).to eq(1) }
211
+
212
+ raider('schedule', 'remove', 'nightly', '--project', dir)
213
+
214
+ expect(scheduler).to have_received(:remove).with('nightly')
215
+ end
216
+
217
+ # The fake keeps listing the entry, so the second list finds it again
218
+ it 'writes the notice to stderr in text mode, and passes it to the JSON' do
219
+ _out, err, code = raider('schedule', 'list', '--project', dir)
220
+ _json_out, json_err, = raider('schedule', 'list', '--json', '--project', dir)
221
+
222
+ expect([err, code, json_err]).to eq(["#{notice}\n", nil, "#{notice}\n"])
223
+ expect(scheduler).to have_received(:list).with(json: false, notice: notice).ordered
224
+ expect(scheduler).to have_received(:list).with(json: true, notice: notice).ordered
225
+ end
226
+
227
+ it "keeps run's exit code when the cleanup fails" do
228
+ adapter.failure = RubyRaider::Schedules::Error.new('denied')
229
+ allow(wrapper).to receive(:run).and_return(1)
230
+
231
+ out, err, code = raider('schedule', 'run', 'nightly', '--project', dir)
232
+
233
+ expect([out, code]).to eq(['', 1])
234
+ expect(err).to eq("Could not remove 1 schedule made with Ruby Raider 4.1 (tredssd: denied); it is tried again next time.\n")
235
+ end
236
+
237
+ it 'is skipped, with no adapter, when .raider/flows is not a folder' do
238
+ allow(RubyRaider::Schedules).to receive(:adapter)
239
+ FileUtils.rm_rf(File.join(dir, '.raider', 'flows'))
240
+ outside = File.realpath(Dir.mktmpdir('schedule-commands-outside'))
241
+ File.symlink(outside, File.join(dir, '.raider', 'flows'))
242
+ results = [raider('schedule', 'run', 'nightly', '--project', dir)]
243
+ FileUtils.rm_rf(File.join(dir, '.raider'))
244
+ results << raider('schedule', 'list', '--project', dir)
245
+
246
+ expect(results).to eq([['', '', 0], ['', '', nil]])
247
+ expect(RubyRaider::Schedules::Legacy).not_to have_received(:new)
248
+ expect(RubyRaider::Schedules).not_to have_received(:adapter)
249
+ ensure
250
+ FileUtils.rm_rf(outside) if outside
251
+ end
252
+
253
+ it 'prints nothing when there is nothing to clean' do
254
+ adapter.ids.clear
255
+
256
+ out, err, code = raider('schedule', 'run', 'nightly', '--project', dir)
257
+
258
+ expect([out, err, code]).to eq(['', '', 0])
259
+ end
260
+ end
261
+
262
+ describe 'help' do
263
+ it 'lists the four commands in raider help schedule' do
264
+ out, = raider('help', 'schedule')
265
+
266
+ expect(out).to include('schedule add NAME [SPEC_FILE ...]', 'schedule list', 'schedule remove NAME',
267
+ 'schedule run NAME')
268
+ end
269
+
270
+ # Thor would also reach run_schedule from `run` by prefix; the map is what keeps `run` working if that changes
271
+ it 'maps run to run_schedule explicitly and shows it as run NAME' do
272
+ out, = raider('help', 'schedule')
273
+
274
+ expect(described_class.map).to include('run' => :run_schedule)
275
+ expect(out).to match(/^ +\S+ schedule run NAME +# Runs the schedule NAME now/)
276
+ end
277
+
278
+ it 'lists schedule in raider help' do
279
+ out, = raider('help')
280
+
281
+ expect(out).to include('schedule', 'Runs the tests on a schedule')
282
+ end
283
+ end
284
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'content_helper'
4
+ require_relative '../../../lib/utilities/schedules/schedule_store'
4
5
 
5
6
  describe 'Common file content' do
6
7
  # --- Rakefile ---
@@ -77,6 +78,10 @@ describe 'Common file content' do
77
78
  it 'includes raider-results' do
78
79
  expect(gitignore).to include('raider-results')
79
80
  end
81
+
82
+ it 'ignores .raider/schedules/ with the exact line raider schedule add looks for' do
83
+ expect(gitignore.lines.map(&:chomp)).to include(RubyRaider::Schedules::ScheduleStore::GITIGNORE_LINE)
84
+ end
80
85
  end
81
86
 
82
87
  shared_examples 'web gitignore for saved logins' do |project_name|
@@ -89,8 +94,9 @@ describe 'Common file content' do
89
94
  end
90
95
 
91
96
  shared_examples 'mobile gitignore without saved logins' do |project_name|
92
- it 'has no .raider line' do
93
- expect(read_generated(project_name, '.gitignore')).not_to include('.raider')
97
+ it 'has no .raider line but .raider/schedules/' do
98
+ lines = read_generated(project_name, '.gitignore').lines.map(&:strip)
99
+ expect(lines.grep(/\A\.raider/)).to eq(['.raider/schedules/'])
94
100
  end
95
101
  end
96
102
 
@@ -85,8 +85,8 @@ describe 'Gemfile content' do
85
85
  expect(gemfile).to include("gem 'ruby_raider', '~> #{major_minor}'")
86
86
  end
87
87
 
88
- it 'pins ruby_raider to the 4.1 release line' do
89
- expect(gemfile).to include("gem 'ruby_raider', '~> 4.1'")
88
+ it 'pins ruby_raider to the 4.2 release line' do
89
+ expect(gemfile).to include("gem 'ruby_raider', '~> 4.2'")
90
90
  end
91
91
  end
92
92
  end
@@ -447,69 +447,6 @@ describe 'End-to-End Project Generation and Execution' do
447
447
  end
448
448
  end
449
449
 
450
- # raider flow init and raider flow run on a generated Selenium project, headless, against a local page
451
- context 'Flows', :slow do
452
- let(:project) { generate_video_project('rspec', 'selenium') }
453
-
454
- def write_flows(project)
455
- page = File.join(File.expand_path(project), 'flow_page.html')
456
- File.write(page, <<~HTML)
457
- <!doctype html><title>Timesheet</title>
458
- <label for="hours">Hours</label><input id="hours">
459
- <button onclick="document.getElementById('done').textContent = 'Saved ' + document.getElementById('hours').value">Save</button>
460
- <p id="done"></p>
461
- HTML
462
- { 'timesheet' => "driver.find_element(label: 'Hours').send_keys(flow_input('hours', '8'))\n " \
463
- "driver.find_element(by_role('button', name: 'Save')).click\n " \
464
- "driver.find_element(text: \"Saved \#{flow_input('hours', '8')}\")",
465
- 'export' => "driver.find_element(id: 'export').click" }.each do |name, steps|
466
- File.write(File.join(project, "flows/#{name}_flow.rb"), <<~RUBY)
467
- # frozen_string_literal: true
468
-
469
- require_relative 'flow_helper'
470
-
471
- RSpec.describe '#{name}' do
472
- it 'runs' do
473
- driver.navigate.to 'file://#{page}'
474
- #{steps}
475
- end
476
- end
477
- RUBY
478
- end
479
- end
480
-
481
- def runs(project, name)
482
- File.readlines(File.join(project, ".raider/flows/#{name}/runs.jsonl")).map { |line| JSON.parse(line) }
483
- end
484
-
485
- it 'runs a flow headless into flow-results and records completed and failed runs', :aggregate_failures do
486
- skip 'Google Chrome is not installed' unless browser_installed?('chrome')
487
- FileUtils.rm_rf([File.join(project, 'flows'), File.join(project, '.raider/flows'), File.join(project, 'flow-results')])
488
-
489
- init = run_in_project(project, 'bundle exec raider flow init')
490
- expect(init[:exit_code]).to eq(0), init[:stdout] + init[:stderr]
491
- write_flows(project)
492
- env = { 'VIDEO' => 'off', 'TRACE' => 'off', 'RAIDER_TOKEN' => nil, 'RAIDER_ENDPOINT' => nil }
493
- completed = run_in_project(project, 'bundle exec raider flow run timesheet --input hours=7', env:)
494
- failed = run_in_project(project, 'bundle exec raider flow run export', env: env.merge('RAIDER_TIMEOUT' => '1'))
495
-
496
- expect(completed[:exit_code]).to eq(0), completed[:stdout] + completed[:stderr]
497
- expect(completed[:stdout]).to match(%r{Flow timesheet completed in .* s\nResults: flow-results/timesheet/\d{8}T\d{6}Z})
498
- expect(runs(project, 'timesheet').last).to include('status' => 'completed', 'trigger' => 'manual')
499
- expect(failed[:exit_code]).to eq(1), failed[:stdout] + failed[:stderr]
500
- expect(runs(project, 'export').last).to include('status' => 'failed', 'failed_step' => { 'title' => 'runs', 'line' => 8 })
501
- expect(File).to exist(File.join(project, runs(project, 'export').last['results_dir'], 'run.json'))
502
- expect(File).not_to exist(File.join(project, 'raider-results'))
503
- end
504
-
505
- it 'passes its own RuboCop with the flow files' do
506
- FileUtils.rm_rf(File.join(project, 'flows'))
507
- run_in_project(project, 'bundle exec raider flow init')
508
-
509
- expect_project_rubocop_clean(project)
510
- end
511
- end
512
-
513
450
  # Trace replay against headless Chrome, and one example on headless Firefox. The page is served over HTTP from this process (never file://), so
514
451
  # WebDriver BiDi reports real network and console entries. raider_reporter ~> 1.0 resolves from RubyGems, or
515
452
  # from a checkout at RAIDER_REPORTER_PATH. The example with video on also needs RAIDER_FFMPEG_PATH.