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.
- checksums.yaml +4 -4
- data/.github/workflows/e2e_tests.yml +3 -0
- data/README.md +52 -53
- data/RELEASE.md +14 -1
- data/lib/commands/schedule_commands.rb +84 -0
- data/lib/generators/templates/common/git_ignore.tt +1 -0
- data/lib/ruby_raider/runtime/retry_reset.rb +32 -0
- data/lib/ruby_raider/runtime/rspec.rb +1 -0
- data/lib/ruby_raider/runtime/show_browser.rb +53 -0
- data/lib/ruby_raider/runtime.rb +2 -0
- data/lib/ruby_raider.rb +22 -7
- data/lib/utilities/schedules/adapter.rb +22 -0
- data/lib/utilities/schedules/base.rb +27 -0
- data/lib/utilities/schedules/kind.rb +74 -0
- data/lib/utilities/schedules/legacy.rb +196 -0
- data/lib/utilities/schedules/legacy_stub.rb +62 -0
- data/lib/utilities/{flows → schedules}/next_run.rb +2 -2
- data/lib/utilities/{flows → schedules}/notifier.rb +6 -6
- data/lib/utilities/{flows → schedules}/os/command_runner.rb +1 -1
- data/lib/utilities/{flows → schedules}/os/cron.rb +27 -17
- data/lib/utilities/{flows → schedules}/os/launchd.rb +32 -14
- data/lib/utilities/{flows → schedules}/os/linux.rb +23 -5
- data/lib/utilities/{flows → schedules}/os/systemd.rb +33 -17
- data/lib/utilities/{flows → schedules}/os/task_scheduler.rb +36 -20
- data/lib/utilities/schedules/os_enumerator.rb +30 -0
- data/lib/utilities/schedules/project.rb +70 -0
- data/lib/utilities/schedules/rule.rb +162 -0
- data/lib/utilities/schedules/run_lock.rb +160 -0
- data/lib/utilities/schedules/run_log.rb +122 -0
- data/lib/utilities/schedules/run_report.rb +139 -0
- data/lib/utilities/schedules/run_wrapper.rb +216 -0
- data/lib/utilities/schedules/schedule.rb +89 -0
- data/lib/utilities/schedules/schedule_store.rb +53 -0
- data/lib/utilities/schedules/scheduler.rb +178 -0
- data/lib/utilities/schedules/scheduler_steps.rb +69 -0
- data/lib/utilities/schedules/selection.rb +133 -0
- data/lib/utilities/schedules/spawn.rb +68 -0
- data/lib/utilities/{flows/schedule_store.rb → schedules/store.rb} +17 -13
- data/lib/version +1 -1
- data/ruby_raider.gemspec +1 -0
- data/sig/commands/schedule_commands.rbs +12 -0
- data/sig/ruby_raider/runtime/retry_reset.rbs +7 -0
- data/sig/ruby_raider/runtime/show_browser.rbs +27 -0
- data/sig/ruby_raider.rbs +4 -0
- data/sig/utilities/schedules/adapter.rbs +5 -0
- data/sig/utilities/schedules/base.rbs +15 -0
- data/sig/utilities/schedules/kind.rbs +32 -0
- data/sig/utilities/schedules/legacy.rbs +45 -0
- data/sig/utilities/schedules/legacy_stub.rbs +15 -0
- data/sig/utilities/{flows → schedules}/next_run.rbs +3 -3
- data/sig/utilities/{flows → schedules}/notifier.rbs +1 -1
- data/sig/utilities/{flows → schedules}/os/command_runner.rbs +1 -1
- data/sig/utilities/{flows → schedules}/os/cron.rbs +7 -7
- data/sig/utilities/{flows → schedules}/os/launchd.rbs +7 -6
- data/sig/utilities/{flows → schedules}/os/linux.rbs +10 -4
- data/sig/utilities/{flows → schedules}/os/systemd.rbs +8 -7
- data/sig/utilities/{flows → schedules}/os/task_scheduler.rbs +13 -10
- data/sig/utilities/schedules/os_enumerator.rbs +12 -0
- data/sig/utilities/schedules/project.rbs +22 -0
- data/sig/utilities/{flows/schedule.rbs → schedules/rule.rbs} +23 -12
- data/sig/utilities/schedules/run_lock.rbs +29 -0
- data/sig/utilities/schedules/run_log.rbs +33 -0
- data/sig/utilities/schedules/run_report.rbs +34 -0
- data/sig/utilities/schedules/run_wrapper.rbs +45 -0
- data/sig/utilities/schedules/schedule.rbs +27 -0
- data/sig/utilities/schedules/schedule_store.rbs +21 -0
- data/sig/utilities/{flows → schedules}/scheduler.rbs +22 -15
- data/sig/utilities/schedules/scheduler_steps.rbs +22 -0
- data/sig/utilities/schedules/selection.rbs +36 -0
- data/sig/utilities/schedules/spawn.rbs +10 -0
- data/sig/utilities/{flows/schedule_store.rbs → schedules/store.rbs} +14 -9
- data/spec/commands/schedule_commands_spec.rb +284 -0
- data/spec/integration/content/common_content_spec.rb +8 -2
- data/spec/integration/content/gemfile_content_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +0 -63
- data/spec/runtime/retry_reset_spec.rb +112 -0
- data/spec/runtime/show_browser_project_spec.rb +70 -0
- data/spec/runtime/show_browser_spec.rb +101 -0
- data/spec/utilities/schedules/kind_spec.rb +81 -0
- data/spec/utilities/schedules/legacy_spec.rb +433 -0
- data/spec/utilities/schedules/legacy_stub_spec.rb +144 -0
- data/spec/utilities/{flows → schedules}/next_run_spec.rb +5 -5
- data/spec/utilities/{flows → schedules}/notifier_spec.rb +3 -4
- data/spec/utilities/{flows → schedules}/os/command_runner_spec.rb +2 -3
- data/spec/utilities/{flows → schedules}/os/cron_spec.rb +34 -29
- data/spec/utilities/{flows → schedules}/os/launchd_spec.rb +42 -29
- data/spec/utilities/{flows → schedules}/os/linux_spec.rb +27 -13
- data/spec/utilities/schedules/os/list_ids_spec.rb +202 -0
- data/spec/utilities/{flows → schedules}/os/systemd_spec.rb +35 -31
- data/spec/utilities/{flows → schedules}/os/task_scheduler_spec.rb +40 -34
- data/spec/utilities/schedules/os_entry_spec.rb +54 -0
- data/spec/utilities/schedules/os_enumerator_spec.rb +57 -0
- data/spec/utilities/schedules/project_spec.rb +125 -0
- data/spec/utilities/schedules/run_lock_spec.rb +143 -0
- data/spec/utilities/schedules/run_log_spec.rb +83 -0
- data/spec/utilities/schedules/run_report_spec.rb +150 -0
- data/spec/utilities/schedules/run_wrapper_integration_spec.rb +166 -0
- data/spec/utilities/schedules/run_wrapper_spec.rb +430 -0
- data/spec/utilities/schedules/schedule_spec.rb +120 -0
- data/spec/utilities/schedules/schedule_store_spec.rb +151 -0
- data/spec/utilities/schedules/scheduler_launchd_spec.rb +77 -0
- data/spec/utilities/schedules/scheduler_spec.rb +445 -0
- data/spec/utilities/schedules/selection_spec.rb +176 -0
- data/spec/utilities/schedules/spawn_spec.rb +104 -0
- metadata +106 -53
- data/lib/commands/flow_commands.rb +0 -104
- data/lib/generators/templates/flows/flow_helper.tt +0 -42
- data/lib/generators/templates/flows/run.tt +0 -332
- data/lib/utilities/flows/project.rb +0 -170
- data/lib/utilities/flows/run_log.rb +0 -61
- data/lib/utilities/flows/runner.rb +0 -206
- data/lib/utilities/flows/schedule.rb +0 -221
- data/lib/utilities/flows/scheduler.rb +0 -187
- data/sig/commands/flow_commands.rbs +0 -13
- data/sig/utilities/flows/project.rbs +0 -46
- data/sig/utilities/flows/run_log.rbs +0 -22
- data/sig/utilities/flows/runner.rbs +0 -35
- data/spec/commands/flow_commands_spec.rb +0 -128
- data/spec/generators/flow_helper_spec.rb +0 -40
- data/spec/integration/flows/flow_project.rb +0 -62
- data/spec/integration/flows/run_command_spec.rb +0 -91
- data/spec/integration/flows/run_lifecycle_spec.rb +0 -383
- data/spec/utilities/flows/project_spec.rb +0 -178
- data/spec/utilities/flows/run_log_spec.rb +0 -47
- data/spec/utilities/flows/runner_spec.rb +0 -313
- data/spec/utilities/flows/schedule_spec.rb +0 -233
- data/spec/utilities/flows/schedule_store_spec.rb +0 -137
- 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
|