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
@@ -1,383 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
- require 'json'
5
- require 'open3'
6
- require 'time'
7
- require 'timeout'
8
- require 'tmpdir'
9
- require_relative 'flow_project'
10
-
11
- # Runs flows/support/run.rb and flows/flow_helper.rb, as `raider flow init` writes them, under a real
12
- # rspec process (see FlowProject: no browser, no raider_reporter).
13
- RSpec.describe 'flow run lifecycle' do
14
- flows = {
15
- 'ok' => <<~RUBY,
16
- require_relative 'flow_helper'
17
-
18
- RSpec.describe 'ok' do
19
- it 'runs every step' do
20
- driver
21
- puts "headless=\#{ENV.fetch('HEADLESS', 'unset')} who=\#{flow_input('who', 'nobody')}"
22
- puts "secret loaded=\#{ENV['RAIDER_SECRET_TOKEN'] == 'hunter2'}"
23
- end
24
- end
25
- RUBY
26
- 'broken' => <<~RUBY,
27
- require_relative 'flow_helper'
28
-
29
- RSpec.describe 'broken' do
30
- it 'downloads the report' do
31
- driver
32
- driver
33
- raise 'no such element: { id: "export" } (waited 10s)'
34
- end
35
- end
36
- RUBY
37
- 'recorded' => <<~RUBY,
38
- require_relative 'flow_helper'
39
-
40
- RSpec.describe 'recorded' do
41
- it 'exports' do
42
- raise 'no such element'
43
- end
44
- end
45
- RUBY
46
- 'leaky' => <<~RUBY,
47
- require_relative 'flow_helper'
48
-
49
- RSpec.describe 'leaky' do
50
- it "types \#{ENV.fetch('RAIDER_SECRET_TOKEN', 'x')} somewhere" do
51
- raise 'failed'
52
- end
53
- end
54
- RUBY
55
- 'env' => <<~RUBY,
56
- require_relative 'flow_helper'
57
-
58
- RSpec.describe('env') { it('runs') { puts 'ran' } }
59
- RUBY
60
- 'long' => <<~RUBY,
61
- require_relative 'flow_helper'
62
-
63
- RSpec.describe 'long' do
64
- it 'waits' do
65
- File.write('ready', '')
66
- sleep 20
67
- end
68
- end
69
- RUBY
70
- 'unloadable' => <<~RUBY
71
- require_relative 'flow_helper'
72
-
73
- UndefinedConstant.call
74
- RUBY
75
- }
76
-
77
- let(:dir) { File.realpath(Dir.mktmpdir('flow-lifecycle')) }
78
-
79
- before { FlowProject.build(dir, flows) }
80
-
81
- after { FileUtils.rm_rf(dir) }
82
-
83
- # Unsets the variables the test process may carry that would change a run
84
- def clean_env
85
- %w[HEADLESS RAIDER_FLOW_NAME RAIDER_FLOW_HEADED RAIDER_FLOW_ATTEMPT RAIDER_FLOW_TRIGGER RAIDER_FLOW_RUN_ID
86
- RAIDER_SECRET_TOKEN RAIDER_INPUT_WHO FAKE_DRIVER_FAIL FAKE_STEPS].to_h { |key| [key, nil] }
87
- end
88
-
89
- def run_flow(name, env = {})
90
- out, err, status = Open3.capture3(clean_env.merge(env), RbConfig.ruby, '-S', 'rspec', "flows/#{name}_flow.rb", chdir: dir)
91
- [out + err, status.exitstatus]
92
- end
93
-
94
- def log(name)
95
- file = File.join(dir, ".raider/flows/#{name}/runs.jsonl")
96
- File.exist?(file) ? File.readlines(file).map { |line| JSON.parse(line) } : []
97
- end
98
-
99
- def mode(relative) = File.stat(File.join(dir, relative)).mode & 0o777
100
-
101
- def write_env_file(content, permissions)
102
- FileUtils.mkdir_p(File.join(dir, '.raider/flows'))
103
- File.write(File.join(dir, '.raider/flows/env'), content)
104
- File.chmod(permissions, File.join(dir, '.raider/flows/env'))
105
- end
106
-
107
- it 'records a completed run with its results folder, headless by default', :aggregate_failures do
108
- output, code = run_flow('ok', 'RAIDER_INPUT_WHO' => 'ada')
109
- line = log('ok').last
110
-
111
- expect(code).to eq(0)
112
- expect(output).to include('headless=true who=ada')
113
- expect(line).to include('status' => 'completed', 'attempt' => 1, 'trigger' => 'manual')
114
- expect(line['results_dir']).to match(%r{\Aflow-results/ok/\d{8}T\d{6}Z\z})
115
- expect(File).to be_directory(File.join(dir, line['results_dir']))
116
- expect(Time.iso8601(line['finished_at'])).to be >= Time.iso8601(line['started_at'])
117
- end
118
-
119
- it 'keeps the run log and lock private' do
120
- run_flow('ok')
121
-
122
- expect([mode('.raider/flows'), mode('.raider/flows/ok'), mode('.raider/flows/ok/runs.jsonl'), mode('.raider/flows/ok.lock')])
123
- .to eq([0o700, 0o700, 0o600, 0o600])
124
- end
125
-
126
- it 'opens the browser when RAIDER_FLOW_HEADED=1' do
127
- output, = run_flow('ok', 'RAIDER_FLOW_HEADED' => '1')
128
-
129
- expect(output).to include('headless=unset')
130
- end
131
-
132
- it 'takes the attempt, trigger and run id from the wrapper' do
133
- run_flow('ok', 'RAIDER_FLOW_ATTEMPT' => '2', 'RAIDER_FLOW_TRIGGER' => 'schedule', 'RAIDER_FLOW_RUN_ID' => 'abcdef0123456789')
134
-
135
- expect(log('ok').last).to include('attempt' => 2, 'trigger' => 'schedule', 'run_id' => 'abcdef0123456789')
136
- end
137
-
138
- it 'falls back to manual for an unknown trigger and drops a malformed run id' do
139
- run_flow('ok', 'RAIDER_FLOW_TRIGGER' => 'cron; rm -rf /', 'RAIDER_FLOW_RUN_ID' => '../x')
140
-
141
- expect(log('ok').last).to include('trigger' => 'manual').and(satisfy { |line| !line.key?('run_id') })
142
- end
143
-
144
- it 'records a failed step with its line in the flow file' do
145
- _output, code = run_flow('broken')
146
- line = log('broken').last
147
-
148
- expect(code).to eq(1)
149
- expect(line).to include('status' => 'failed', 'failed_step' => { 'title' => 'downloads the report', 'line' => 7 })
150
- end
151
-
152
- it 'names the recorded step when the report has the desktop step map' do
153
- run_flow('recorded', 'FAKE_STEPS' => '1')
154
-
155
- expect(log('recorded').last['failed_step']).to eq('number' => 2, 'title' => 'click "Export"', 'line' => 5)
156
- end
157
-
158
- it 'records a setup error when the driver fails before the first step' do
159
- _output, code = run_flow('ok', 'FAKE_DRIVER_FAIL' => '1')
160
-
161
- expect(code).to eq(1)
162
- expect(log('ok').last).to include('status' => 'setup_error').and(satisfy { |line| !line.key?('failed_step') })
163
- end
164
-
165
- it 'records a setup error when the flow file does not load' do
166
- _output, code = run_flow('unloadable')
167
-
168
- expect(code).not_to eq(0)
169
- expect(log('unloadable').last['status']).to eq('setup_error')
170
- end
171
-
172
- it 'refuses a RAIDER_FLOW_NAME that is not the flow being run' do
173
- output, code = run_flow('ok', 'RAIDER_FLOW_NAME' => 'other')
174
-
175
- expect(code).not_to eq(0)
176
- expect(output).to include('RAIDER_FLOW_NAME is "other" but the flow is "ok"')
177
- expect(log('ok')).to be_empty
178
- end
179
-
180
- it 'reports into the run folder and never cleans raider-results' do
181
- run_flow('ok')
182
- reporter = JSON.parse(File.read(File.join(dir, 'reporter.json')))
183
-
184
- expect(reporter).to eq('output_dir' => File.join(dir, log('ok').last['results_dir']), 'clean_output_dir' => false)
185
- end
186
-
187
- it 'keeps a refused flow away from raider-results too' do
188
- run_flow('ok', 'RAIDER_FLOW_NAME' => 'other')
189
- reporter = JSON.parse(File.read(File.join(dir, 'reporter.json')))
190
-
191
- expect(reporter['clean_output_dir']).to be(false)
192
- expect(reporter['output_dir']).to start_with(File.realpath(Dir.tmpdir)).or start_with(Dir.tmpdir)
193
- end
194
-
195
- it 'drops a HEADLESS left in the shell for RAIDER_FLOW_HEADED=1' do
196
- output, = run_flow('ok', 'RAIDER_FLOW_HEADED' => '1', 'HEADLESS' => 'true')
197
-
198
- expect(output).to include('headless=unset')
199
- end
200
-
201
- it 'exits 3 with one busy line when another run holds the lock, and never waits' do
202
- FileUtils.mkdir_p(File.join(dir, '.raider/flows'))
203
- File.open(File.join(dir, '.raider/flows/ok.lock'), File::RDWR | File::CREAT, 0o600) do |held|
204
- held.flock(File::LOCK_EX)
205
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
206
-
207
- output, code = run_flow('ok')
208
-
209
- expect(code).to eq(3)
210
- expect(output).to include('ok is already running')
211
- expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 30
212
- end
213
- expect(log('ok').map { |line| line['status'] }).to eq(['busy'])
214
- expect(log('ok').last['results_dir']).to be_nil
215
- end
216
-
217
- it 'ends the step on the first Ctrl-C, ignores the second, quits the driver and records the run' do
218
- pid = Process.spawn(clean_env, RbConfig.ruby, '-S', 'rspec', 'flows/long_flow.rb', chdir: dir, %i[out err] => File::NULL)
219
- Timeout.timeout(30) { sleep 0.05 until File.exist?(File.join(dir, 'ready')) }
220
- 2.times do
221
- Process.kill('INT', pid)
222
- sleep 0.2
223
- end
224
- Timeout.timeout(15) { Process.wait(pid) }
225
-
226
- expect(log('long').map { |line| line['status'] }).to eq(['setup_error'])
227
- expect(File).to exist(File.join(dir, 'driver_quit'))
228
- end
229
-
230
- it 'releases the lock when the run ends' do
231
- run_flow('ok')
232
- _output, code = run_flow('ok')
233
-
234
- expect(code).to eq(0)
235
- expect(log('ok').map { |line| line['status'] }).to eq(%w[completed completed])
236
- end
237
-
238
- context 'with the flow env file' do
239
- it 'loads a 0600 file without printing its values' do
240
- write_env_file("# secrets\nRAIDER_SECRET_TOKEN=\"hunter2\"\n\nbad line\n", 0o600)
241
-
242
- output, = run_flow('ok')
243
-
244
- expect(output).to include('secret loaded=true', '.raider/flows/env line 4 ignored: expected KEY=value')
245
- expect(output).not_to include('hunter2')
246
- expect(File.read(File.join(dir, '.raider/flows/ok/runs.jsonl'))).not_to include('hunter2')
247
- end
248
-
249
- it 'ignores a 0644 file with a warning' do
250
- write_env_file("RAIDER_SECRET_TOKEN=hunter2\n", 0o644)
251
-
252
- output, code = run_flow('ok')
253
-
254
- expect(code).to eq(0)
255
- expect(output).to include('ignored .raider/flows/env: permissions must be 600', 'secret loaded=false')
256
- end
257
-
258
- it 'ignores a link to a file elsewhere' do
259
- target = File.join(dir, 'elsewhere')
260
- File.write(target, "RAIDER_SECRET_TOKEN=hunter2\n")
261
- File.chmod(0o600, target)
262
- FileUtils.mkdir_p(File.join(dir, '.raider/flows'))
263
- File.symlink(target, File.join(dir, '.raider/flows/env'))
264
-
265
- output, = run_flow('ok')
266
-
267
- expect(output).to include('ignored .raider/flows/env: it must be a regular file you own', 'secret loaded=false')
268
- end
269
-
270
- it 'does not override a variable already set' do
271
- write_env_file("RAIDER_SECRET_TOKEN=hunter2\n", 0o600)
272
-
273
- output, = run_flow('ok', 'RAIDER_SECRET_TOKEN' => 'from-shell')
274
-
275
- expect(output).to include('secret loaded=false')
276
- end
277
-
278
- it 'keeps secret values out of the failed step title' do
279
- write_env_file("RAIDER_SECRET_TOKEN=hunter2\n", 0o600)
280
-
281
- run_flow('leaky')
282
-
283
- expect(log('leaky').last['failed_step']['title']).to eq('types [secret] somewhere')
284
- expect(File.read(File.join(dir, '.raider/flows/leaky/runs.jsonl'))).not_to include('hunter2')
285
- end
286
- end
287
-
288
- context 'when something fails after the lock is taken' do
289
- it 'still records the run when flow-results is not a folder' do
290
- File.write(File.join(dir, 'flow-results'), '')
291
-
292
- _output, code = run_flow('ok')
293
-
294
- expect(code).not_to eq(0)
295
- expect(log('ok').last).to include('status' => 'setup_error', 'results_dir' => nil)
296
- end
297
-
298
- it 'skips an env line that is not UTF-8 and runs the flow' do
299
- write_env_file("RAIDER_SECRET_TOKEN=hunter2\nBAD=\xFF\n".b, 0o600)
300
-
301
- output, code = run_flow('ok')
302
-
303
- expect(code).to eq(0)
304
- expect(output).to include('.raider/flows/env line 2 ignored', 'secret loaded=true')
305
- end
306
- end
307
-
308
- it 'refuses a flow named env, whose log folder would be the secrets file' do
309
- output, code = run_flow('env')
310
-
311
- expect(code).not_to eq(0)
312
- expect(output).to include('invalid flow name "env"')
313
- expect(File).not_to exist(File.join(dir, '.raider/flows/env'))
314
- end
315
-
316
- it 'leaves no lock, record or results for rspec --dry-run' do
317
- Open3.capture3(clean_env, RbConfig.ruby, '-S', 'rspec', '--dry-run', 'flows/ok_flow.rb', chdir: dir)
318
-
319
- expect([log('ok'), File.exist?(File.join(dir, 'flow-results'))]).to eq([[], false])
320
- end
321
-
322
- it 'never lets the env file set RAIDER_FLOW_* and clamps the attempt to 1' do
323
- write_env_file("RAIDER_FLOW_TRIGGER=schedule\n", 0o600)
324
-
325
- output, = run_flow('ok', 'RAIDER_FLOW_ATTEMPT' => '0')
326
-
327
- expect(output).to include('.raider/flows/env line 1 ignored')
328
- expect(log('ok').last).to include('trigger' => 'manual', 'attempt' => 1)
329
- end
330
-
331
- it 'ignores a FIFO env file without waiting for a writer' do
332
- FileUtils.mkdir_p(File.join(dir, '.raider/flows'))
333
- File.mkfifo(File.join(dir, '.raider/flows/env'), 0o600)
334
-
335
- output, code = Timeout.timeout(60) { run_flow('ok') }
336
-
337
- expect(code).to eq(0)
338
- expect(output).to include('ignored .raider/flows/env: it must be a regular file you own')
339
- end
340
-
341
- it 'redacts the longest secret first' do
342
- write_env_file("RAIDER_SECRET_A=hunt\nRAIDER_SECRET_TOKEN=hunter2\n", 0o600)
343
-
344
- run_flow('leaky')
345
-
346
- expect(log('leaky').last['failed_step']['title']).to eq('types [secret] somewhere')
347
- end
348
-
349
- it 'redacts the value in use when the shell already set the key' do
350
- write_env_file("RAIDER_SECRET_TOKEN=hunter2\n", 0o600)
351
-
352
- run_flow('leaky', 'RAIDER_SECRET_TOKEN' => 'shell-secret')
353
-
354
- expect(log('leaky').last['failed_step']['title']).to eq('types [secret] somewhere')
355
- end
356
-
357
- it 'never prunes through a linked flow-results folder' do
358
- elsewhere = File.join(dir, 'elsewhere')
359
- Array.new(25) { |i| FileUtils.mkdir_p(File.join(elsewhere, format('20200101T0000%02dZ', i))) }
360
- FileUtils.mkdir_p(File.join(dir, 'flow-results'))
361
- File.symlink(elsewhere, File.join(dir, 'flow-results/ok'))
362
-
363
- run_flow('ok')
364
-
365
- expect(Dir.children(elsewhere).grep(/\A2020/).size).to eq(25)
366
- end
367
-
368
- it 'keeps the last 50 log lines and the last 20 results folders, and only stamp-named folders' do
369
- FileUtils.mkdir_p(File.join(dir, '.raider/flows/ok'))
370
- File.write(File.join(dir, '.raider/flows/ok/runs.jsonl'), Array.new(60) { |i| "#{JSON.generate('n' => i)}\n" }.join)
371
- old = Array.new(25) { |i| format('flow-results/ok/20200101T0000%02dZ', i) }
372
- (old + ['flow-results/ok/keep-me']).each { |folder| FileUtils.mkdir_p(File.join(dir, folder)) }
373
-
374
- run_flow('ok')
375
-
376
- lines = log('ok')
377
- stamped = Dir.children(File.join(dir, 'flow-results/ok')).grep(/\A\d{8}T/)
378
- expect([lines.size, lines.first['n'], lines.last['status']]).to eq([50, 11, 'completed'])
379
- expect(stamped.size).to eq(20)
380
- expect(stamped).to include(File.basename(lines.last['results_dir']))
381
- expect(File).to be_directory(File.join(dir, 'flow-results/ok/keep-me'))
382
- end
383
- end
@@ -1,178 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'stringio'
4
- require 'tmpdir'
5
- require_relative '../../../lib/utilities/flows/project'
6
-
7
- # Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
8
- RSpec.describe RubyRaider::Flows::Project do # rubocop:disable RSpec/FilePath
9
- let(:dir) { Dir.mktmpdir('flows-project') }
10
- let(:out) { StringIO.new }
11
- let(:err) { StringIO.new }
12
- let(:project) { described_class.new(dir) }
13
-
14
- def write(relative, content)
15
- file = File.join(dir, relative)
16
- FileUtils.mkdir_p(File.dirname(file))
17
- File.write(file, content)
18
- end
19
-
20
- def read(relative) = File.read(File.join(dir, relative))
21
-
22
- def selenium_project(pin: "'~> 4.0'")
23
- write('Gemfile', "gem 'raider_reporter', '~> 1.0'\ngem 'ruby_raider', #{pin}\ngem 'selenium-webdriver'\n")
24
- write('helpers/driver_helper.rb', "require 'selenium-webdriver'\nrequire 'ruby_raider/runtime'\n")
25
- write('.gitignore', "raider-results\nspec/examples.txt\n.raider/auth/\n")
26
- end
27
-
28
- after { FileUtils.rm_rf(dir) }
29
-
30
- describe '#init' do
31
- it 'creates the flow helper, the run recorder and the gitignore lines' do
32
- selenium_project
33
-
34
- expect(project.init(out:, err:)).to eq(0)
35
- expect(File).to exist(File.join(dir, 'flows/flow_helper.rb'))
36
- expect(File).to exist(File.join(dir, 'flows/support/run.rb'))
37
- expect(read('.gitignore').lines.map(&:strip)).to include('flow-results/', '.raider/flows/', '.raider/auth/')
38
- end
39
-
40
- it 'raises a ~> 4.0 ruby_raider pin to ~> 4.1 and says to bundle' do
41
- selenium_project
42
-
43
- project.init(out:, err:)
44
-
45
- expect(read('Gemfile')).to include("gem 'ruby_raider', '~> 4.1'")
46
- expect(out.string).to include('Run bundle install')
47
- end
48
-
49
- it 'keeps a pin that already allows flows' do
50
- selenium_project(pin: "'~> 4.2'")
51
-
52
- project.init(out:, err:)
53
-
54
- expect(read('Gemfile')).to include("gem 'ruby_raider', '~> 4.2'")
55
- expect(out.string).not_to include('Run bundle install')
56
- end
57
-
58
- it 'asks to check a ruby_raider line it cannot raise' do
59
- selenium_project(pin: "'4.0.1'")
60
-
61
- expect(project.init(out:, err:)).to eq(0)
62
- expect(err.string).to include('Check that the ruby_raider line in your Gemfile allows 4.1 or later')
63
- end
64
-
65
- it 'is idempotent' do
66
- selenium_project
67
- project.init(out:, err:)
68
- helper = read('flows/flow_helper.rb')
69
- second = StringIO.new
70
-
71
- expect(project.init(out: second, err:)).to eq(0)
72
- expect(second.string).to eq("Flows already set up\n")
73
- expect(read('flows/flow_helper.rb')).to eq(helper)
74
- expect(read('.gitignore').scan('flow-results/').size).to eq(1)
75
- end
76
-
77
- it 'never overwrites a flow helper the user changed' do
78
- selenium_project
79
- write('flows/flow_helper.rb', "# mine\n")
80
-
81
- project.init(out:, err:)
82
-
83
- expect(read('flows/flow_helper.rb')).to eq("# mine\n")
84
- end
85
-
86
- it 'adds a newline before the gitignore lines when the file does not end with one' do
87
- selenium_project
88
- write('.gitignore', 'raider-results')
89
-
90
- project.init(out:, err:)
91
-
92
- expect(read('.gitignore')).to eq("raider-results\nflow-results/\n.raider/flows/\n")
93
- end
94
-
95
- shared_examples 'a refused project' do
96
- it 'refuses with exit 2 and writes nothing' do
97
- expect(project.init(out:, err:)).to eq(2)
98
- expect(err.string).to eq("Flows need a Selenium web project made with Ruby Raider 4.0 or later\n")
99
- expect(File).not_to exist(File.join(dir, 'flows'))
100
- end
101
- end
102
-
103
- context 'with a mobile project' do
104
- before do
105
- selenium_project
106
- write('Gemfile', "gem 'appium_lib'\ngem 'ruby_raider', '~> 4.0'\n")
107
- end
108
-
109
- it_behaves_like 'a refused project'
110
- end
111
-
112
- context 'with a 3.x project' do
113
- before do
114
- selenium_project(pin: "'~> 3.15'")
115
- write('helpers/driver_helper.rb', "require 'selenium-webdriver'\n")
116
- end
117
-
118
- it_behaves_like 'a refused project'
119
- end
120
-
121
- context 'with a 3.x pin on 4.0 helpers' do
122
- before { selenium_project(pin: "'~> 3.15'") }
123
-
124
- it_behaves_like 'a refused project'
125
- end
126
-
127
- context 'without a Gemfile' do
128
- it_behaves_like 'a refused project'
129
- end
130
- end
131
-
132
- describe '#flow_file' do
133
- before { selenium_project }
134
-
135
- it 'returns the real path of flows/<name>_flow.rb' do
136
- write('flows/report_flow.rb', '')
137
-
138
- expect(project.flow_file('report')).to eq(File.realpath(File.join(dir, 'flows/report_flow.rb')))
139
- end
140
-
141
- it 'rejects a name outside the allowed characters' do
142
- expect { project.flow_file('../x') }.to raise_error(RubyRaider::Flows::Error, /Invalid flow name/)
143
- end
144
-
145
- it 'rejects a name longer than 60 characters' do
146
- expect { project.flow_file('a' * 61) }.to raise_error(RubyRaider::Flows::Error, /Invalid flow name/)
147
- end
148
-
149
- it 'reports a missing flow' do
150
- expect { project.flow_file('nope') }.to raise_error(RubyRaider::Flows::Error, 'No flow nope: flows/nope_flow.rb not found')
151
- end
152
-
153
- it 'rejects a flow file that links outside the project' do
154
- outside = Dir.mktmpdir('outside')
155
- File.write(File.join(outside, 'evil_flow.rb'), '')
156
- FileUtils.mkdir_p(File.join(dir, 'flows'))
157
- File.symlink(File.join(outside, 'evil_flow.rb'), File.join(dir, 'flows/evil_flow.rb'))
158
-
159
- expect { project.flow_file('evil') }.to raise_error(RubyRaider::Flows::Error, /outside the project/)
160
- ensure
161
- FileUtils.rm_rf(outside)
162
- end
163
- end
164
-
165
- describe '#flow_names' do
166
- it 'lists flow files by name, sorted' do
167
- write('flows/b_flow.rb', '')
168
- write('flows/a_flow.rb', '')
169
- write('flows/flow_helper.rb', '')
170
-
171
- expect(project.flow_names).to eq(%w[a b])
172
- end
173
- end
174
-
175
- it 'raises a Flows::Error for a missing project folder' do
176
- expect { described_class.new(File.join(dir, 'missing')) }.to raise_error(RubyRaider::Flows::Error, /Project folder not found/)
177
- end
178
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'tmpdir'
4
- require_relative '../../../lib/utilities/flows/run_log'
5
- require_relative '../../../lib/utilities/flows/project'
6
-
7
- # Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
8
- RSpec.describe RubyRaider::Flows::RunLog do # rubocop:disable RSpec/FilePath
9
- let(:dir) { File.realpath(Dir.mktmpdir('flows-log')) }
10
- let(:log) { described_class.new(RubyRaider::Flows::Project.new(dir), 'report') }
11
- let(:file) { File.join(dir, '.raider/flows/report/runs.jsonl') }
12
-
13
- before { FileUtils.mkdir_p(File.dirname(file)) }
14
-
15
- after { FileUtils.rm_rf(dir) }
16
-
17
- it 'skips broken and non-object lines' do
18
- File.write(file, %({"status":"failed"}\nnot json\n[1]\n{"status":"completed","run_id":"a"}\n))
19
-
20
- expect([log.lines.size, log.last['status'], log.find('a')['status']]).to eq([2, 'completed', 'completed'])
21
- end
22
-
23
- it 'reads only the last megabyte of a big log, from a whole line' do
24
- File.write(file, %({"status":"old","pad":"#{'x' * 2_000_000}"}\n{"status":"completed"}\n))
25
-
26
- expect(log.lines).to eq([{ 'status' => 'completed' }])
27
- end
28
-
29
- it 'does not follow a linked log' do
30
- File.write(File.join(dir, 'other.jsonl'), %({"status":"completed"}\n))
31
- File.symlink(File.join(dir, 'other.jsonl'), file)
32
-
33
- expect(log.lines).to eq([])
34
- end
35
-
36
- it 'lists the last run that ran, not a busy line after it' do
37
- FileUtils.mkdir_p(File.join(dir, 'flows'))
38
- File.write(File.join(dir, 'flows/report_flow.rb'), '')
39
- File.write(file, %({"status":"failed"}\n{"status":"busy"}\n))
40
-
41
- expect(described_class.summaries(RubyRaider::Flows::Project.new(dir)).first['last']['status']).to eq('failed')
42
- end
43
-
44
- it 'is empty without a log' do
45
- expect(log.last).to be_nil
46
- end
47
- end