ruby_raider 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +59 -1
- data/RELEASE.md +12 -0
- data/lib/commands/flow_commands.rb +104 -0
- data/lib/generators/templates/flows/flow_helper.tt +42 -0
- data/lib/generators/templates/flows/run.tt +332 -0
- data/lib/ruby_raider.rb +16 -0
- data/lib/utilities/flows/next_run.rb +61 -0
- data/lib/utilities/flows/notifier.rb +76 -0
- data/lib/utilities/flows/os/command_runner.rb +73 -0
- data/lib/utilities/flows/os/cron.rb +137 -0
- data/lib/utilities/flows/os/launchd.rb +164 -0
- data/lib/utilities/flows/os/linux.rb +63 -0
- data/lib/utilities/flows/os/systemd.rb +178 -0
- data/lib/utilities/flows/os/task_scheduler.rb +179 -0
- data/lib/utilities/flows/project.rb +170 -0
- data/lib/utilities/flows/run_log.rb +61 -0
- data/lib/utilities/flows/runner.rb +206 -0
- data/lib/utilities/flows/schedule.rb +221 -0
- data/lib/utilities/flows/schedule_store.rb +135 -0
- data/lib/utilities/flows/scheduler.rb +187 -0
- data/lib/version +1 -1
- data/sig/commands/flow_commands.rbs +13 -0
- data/sig/ruby_raider.rbs +2 -0
- data/sig/utilities/flows/next_run.rbs +15 -0
- data/sig/utilities/flows/notifier.rbs +29 -0
- data/sig/utilities/flows/os/command_runner.rbs +27 -0
- data/sig/utilities/flows/os/cron.rbs +34 -0
- data/sig/utilities/flows/os/launchd.rbs +45 -0
- data/sig/utilities/flows/os/linux.rbs +27 -0
- data/sig/utilities/flows/os/systemd.rbs +43 -0
- data/sig/utilities/flows/os/task_scheduler.rbs +53 -0
- data/sig/utilities/flows/project.rbs +46 -0
- data/sig/utilities/flows/run_log.rbs +22 -0
- data/sig/utilities/flows/runner.rbs +35 -0
- data/sig/utilities/flows/schedule.rbs +61 -0
- data/sig/utilities/flows/schedule_store.rbs +34 -0
- data/sig/utilities/flows/scheduler.rbs +42 -0
- data/spec/commands/flow_commands_spec.rb +128 -0
- data/spec/generators/flow_helper_spec.rb +40 -0
- data/spec/integration/content/gemfile_content_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +63 -0
- data/spec/integration/flows/flow_project.rb +62 -0
- data/spec/integration/flows/run_command_spec.rb +91 -0
- data/spec/integration/flows/run_lifecycle_spec.rb +383 -0
- data/spec/utilities/flows/next_run_spec.rb +119 -0
- data/spec/utilities/flows/notifier_spec.rb +121 -0
- data/spec/utilities/flows/os/command_runner_spec.rb +56 -0
- data/spec/utilities/flows/os/cron_spec.rb +287 -0
- data/spec/utilities/flows/os/launchd_spec.rb +293 -0
- data/spec/utilities/flows/os/linux_spec.rb +149 -0
- data/spec/utilities/flows/os/systemd_spec.rb +329 -0
- data/spec/utilities/flows/os/task_scheduler_spec.rb +505 -0
- data/spec/utilities/flows/project_spec.rb +178 -0
- data/spec/utilities/flows/run_log_spec.rb +47 -0
- data/spec/utilities/flows/runner_spec.rb +313 -0
- data/spec/utilities/flows/schedule_spec.rb +233 -0
- data/spec/utilities/flows/schedule_store_spec.rb +137 -0
- data/spec/utilities/flows/scheduler_spec.rb +329 -0
- metadata +53 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RubyRaider
|
|
2
|
+
module Flows
|
|
3
|
+
class ScheduleStore
|
|
4
|
+
DIR: String
|
|
5
|
+
FILE: String
|
|
6
|
+
NOFOLLOW: Integer
|
|
7
|
+
NONBLOCK: Integer
|
|
8
|
+
|
|
9
|
+
class Removed
|
|
10
|
+
attr_reader name: String
|
|
11
|
+
attr_reader os_id: String
|
|
12
|
+
|
|
13
|
+
def initialize: (String name, String os_id) -> void
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize: (Project project) -> void
|
|
17
|
+
def all: () -> Array[Schedule]
|
|
18
|
+
def find: (String name) -> Schedule?
|
|
19
|
+
def save: (Schedule schedule) -> Schedule
|
|
20
|
+
def writable!: () -> nil
|
|
21
|
+
def delete: (String name) -> (Schedule | Removed)?
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def from_entry: (untyped entry) -> Schedule?
|
|
26
|
+
def named?: (untyped entry, String name) -> bool
|
|
27
|
+
def read_entries: () -> Array[untyped]?
|
|
28
|
+
def writable_entries: () -> Array[untyped]
|
|
29
|
+
def read_text: () -> (String | false)?
|
|
30
|
+
def write: (Array[untyped] entries) -> void
|
|
31
|
+
def private_dir: () -> String
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module RubyRaider
|
|
2
|
+
module Flows
|
|
3
|
+
class Scheduler
|
|
4
|
+
NOT_SET_UP: String
|
|
5
|
+
RUBY_NOT_FOUND: String
|
|
6
|
+
FLOW_NOT_FOUND: String
|
|
7
|
+
LAST: Array[String]
|
|
8
|
+
TIME: String
|
|
9
|
+
|
|
10
|
+
type row = { json: Hash[String, untyped], text: String }
|
|
11
|
+
|
|
12
|
+
@project: Project
|
|
13
|
+
@adapter: untyped
|
|
14
|
+
@store: ScheduleStore
|
|
15
|
+
@out: IO | StringIO
|
|
16
|
+
@err: IO | StringIO
|
|
17
|
+
@now: ^() -> Time
|
|
18
|
+
|
|
19
|
+
def self.adapter: (?runner: Os::CommandRunner, ?host_os: String) -> (Os::Launchd | Os::TaskScheduler | Os::Linux)
|
|
20
|
+
|
|
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
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
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
|
|
33
|
+
def row: (Schedule schedule, Time now) -> row
|
|
34
|
+
def unlisted_rows: (Array[String] listed) -> Array[row]
|
|
35
|
+
def present?: (String os_id) -> bool
|
|
36
|
+
def os_state: (Schedule schedule) -> String
|
|
37
|
+
def rule: (Schedule schedule) -> String
|
|
38
|
+
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
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative '../../lib/ruby_raider'
|
|
6
|
+
|
|
7
|
+
RSpec.describe FlowCommands do
|
|
8
|
+
let(:raider_bin) { File.expand_path('../../bin/raider', __dir__) }
|
|
9
|
+
|
|
10
|
+
def run_raider(*args)
|
|
11
|
+
Open3.capture3(RbConfig.ruby, raider_bin, *args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'lists init, run and list in raider flow help' do
|
|
15
|
+
out, _err, status = run_raider('flow', 'help')
|
|
16
|
+
|
|
17
|
+
expect(status.exitstatus).to eq(0)
|
|
18
|
+
expect(out).to include('flow init', 'flow run NAME', 'flow list')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'maps raider f to the flow commands' do
|
|
22
|
+
out, _err, status = run_raider('f', 'help')
|
|
23
|
+
|
|
24
|
+
expect(status.exitstatus).to eq(0)
|
|
25
|
+
expect(out).to include('flow run NAME')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'is listed in raider help' do
|
|
29
|
+
out, = run_raider('help')
|
|
30
|
+
|
|
31
|
+
expect(out).to include('raider flow')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'init' do
|
|
35
|
+
it 'sets up flows in the --project folder from any working directory' do
|
|
36
|
+
Dir.mktmpdir('flow-init') do |dir|
|
|
37
|
+
File.write(File.join(dir, 'Gemfile'), "gem 'ruby_raider', '~> 4.0'\ngem 'selenium-webdriver'\n")
|
|
38
|
+
FileUtils.mkdir_p(File.join(dir, 'helpers'))
|
|
39
|
+
File.write(File.join(dir, 'helpers/driver_helper.rb'), "require 'ruby_raider/runtime'\n")
|
|
40
|
+
|
|
41
|
+
out, _err, status = run_raider('flow', 'init', '--project', dir)
|
|
42
|
+
|
|
43
|
+
expect(status.exitstatus).to eq(0)
|
|
44
|
+
expect(out).to include('Created flows/flow_helper.rb', 'Run bundle install')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'exits 2 for a project that cannot hold flows' do
|
|
49
|
+
Dir.mktmpdir('flow-init') do |dir|
|
|
50
|
+
_out, err, status = run_raider('flow', 'init', '--project', dir)
|
|
51
|
+
|
|
52
|
+
expect(status.exitstatus).to eq(2)
|
|
53
|
+
expect(err).to include('Flows need a Selenium web project made with Ruby Raider 4.0 or later')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'exits 2 for a missing project folder' do
|
|
58
|
+
_out, err, status = run_raider('flow', 'init', '--project', '/nonexistent/raider')
|
|
59
|
+
|
|
60
|
+
expect(status.exitstatus).to eq(2)
|
|
61
|
+
expect(err).to include('Project folder not found')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'offers --notify on flow run and flow schedule' do
|
|
66
|
+
run_help, = run_raider('flow', 'help', 'run')
|
|
67
|
+
schedule_help, = run_raider('flow', 'help', 'schedule')
|
|
68
|
+
|
|
69
|
+
expect([run_help, schedule_help]).to all(include('--notify'))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe 'schedules' do
|
|
73
|
+
# Only paths refused before the OS scheduler is asked: this machine's launchd is real
|
|
74
|
+
it 'lists schedule, unschedule and schedules in raider flow help' do
|
|
75
|
+
out, _err, status = run_raider('flow', 'help')
|
|
76
|
+
|
|
77
|
+
expect(status.exitstatus).to eq(0)
|
|
78
|
+
expect(out).to include('flow schedule NAME', 'flow unschedule NAME', 'flow schedules')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'dispatches raider flow schedule NAME to schedule, not schedules' do
|
|
82
|
+
Dir.mktmpdir('flow-schedule') do |dir|
|
|
83
|
+
_out, err, status = run_raider('flow', 'schedule', 'x', '--every', 'day', '--at', '08:00', '--project', dir)
|
|
84
|
+
|
|
85
|
+
expect(status.exitstatus).to eq(2)
|
|
86
|
+
expect(err).to include('No flow x: flows/x_flow.rb not found')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'exits 2 through the rule check when --every is left out' do
|
|
91
|
+
Dir.mktmpdir('flow-schedule') do |dir|
|
|
92
|
+
%w[flows/flow_helper.rb flows/support/run.rb flows/report_flow.rb].each do |file|
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(File.join(dir, file)))
|
|
94
|
+
File.write(File.join(dir, file), '')
|
|
95
|
+
end
|
|
96
|
+
_out, err, status = run_raider('flow', 'schedule', 'report', '--at', '08:00', '--project', dir)
|
|
97
|
+
|
|
98
|
+
expect(status.exitstatus).to eq(2)
|
|
99
|
+
expect(err).to include('Invalid every nil: use hour, day, weekday or week')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'exits 2 for an invalid name to unschedule' do
|
|
104
|
+
Dir.mktmpdir('flow-schedule') do |dir|
|
|
105
|
+
_out, err, status = run_raider('flow', 'unschedule', 'Bad-Name', '--project', dir)
|
|
106
|
+
|
|
107
|
+
expect(status.exitstatus).to eq(2)
|
|
108
|
+
expect(err).to include('Invalid flow name "Bad-Name"')
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'exits 2 with a message for an argument that is not valid UTF-8' do
|
|
113
|
+
_out, err, status = run_raider('flow', 'schedule', "a\xFF".b, '--every', 'day')
|
|
114
|
+
|
|
115
|
+
expect(status.exitstatus).to eq(2)
|
|
116
|
+
expect(err).to eq("Invalid argument \"a\\xFF\": use valid UTF-8\n")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'says there are no schedules, as text and as JSON' do
|
|
120
|
+
Dir.mktmpdir('flow-schedule') do |dir|
|
|
121
|
+
text, = run_raider('flow', 'schedules', '--project', dir)
|
|
122
|
+
json, = run_raider('flow', 'schedules', '--json', '--project', dir)
|
|
123
|
+
|
|
124
|
+
expect([text, json]).to eq(["No schedules yet: raider flow schedule <name> --every day --at 08:00\n", "[]\n"])
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
|
|
5
|
+
# The flow templates are copied into projects by `raider flow init`
|
|
6
|
+
RSpec.describe 'flow templates' do
|
|
7
|
+
let(:templates) { File.expand_path('../../lib/generators/templates', __dir__) }
|
|
8
|
+
let(:helper) { render('flows/flow_helper.tt') }
|
|
9
|
+
|
|
10
|
+
def render(relative) = ERB.new(File.read(File.join(templates, relative)), trim_mode: '-').result
|
|
11
|
+
|
|
12
|
+
it 'starts the run before loading spec_helper, so the lock and HEADLESS come before the driver' do
|
|
13
|
+
expect(helper.index('RaiderFlows::Run.start!')).to be < helper.index("require_relative '../helpers/spec_helper'")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'sends the report to the flow results folder without cleaning it, even when start! refuses the run' do
|
|
17
|
+
expect(helper).to match(/begin\n RaiderFlows::Run.start!\nensure\n RaiderFlows::Run.configure_reporter\nend/)
|
|
18
|
+
expect(render('flows/run.tt')).to include('reporter.output_dir = results_dir ||', 'reporter.clean_output_dir = false')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'never retries a flow inside one run' do
|
|
22
|
+
expect(helper).to include('config.default_retry_count = 0')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'marks the start of the steps after spec_helper set up the driver' do
|
|
26
|
+
expect(helper).to include('config.append_before(:each) { RaiderFlows::Run.steps_started! }')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'never sets the reporter output folder from spec_helper, so a test run cannot wipe flow results' do
|
|
30
|
+
spec_helper = File.read(File.join(templates, 'helpers/spec_helper.tt'))
|
|
31
|
+
|
|
32
|
+
expect(spec_helper).not_to match(/output_dir|clean_output_dir/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'renders Ruby that parses' do
|
|
36
|
+
[helper, render('flows/run.tt')].each do |source|
|
|
37
|
+
expect { RubyVM::InstructionSequence.compile(source) }.not_to raise_error
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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.
|
|
89
|
-
expect(gemfile).to include("gem 'ruby_raider', '~> 4.
|
|
88
|
+
it 'pins ruby_raider to the 4.1 release line' do
|
|
89
|
+
expect(gemfile).to include("gem 'ruby_raider', '~> 4.1'")
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -447,6 +447,69 @@ 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
|
+
|
|
450
513
|
# Trace replay against headless Chrome, and one example on headless Firefox. The page is served over HTTP from this process (never file://), so
|
|
451
514
|
# WebDriver BiDi reports real network and console entries. raider_reporter ~> 1.0 resolves from RubyGems, or
|
|
452
515
|
# from a checkout at RAIDER_REPORTER_PATH. The example with video on also needs RAIDER_FFMPEG_PATH.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
# A project with the flow files `raider flow init` writes and a stand-in helpers/spec_helper.rb:
|
|
7
|
+
# no browser and no raider_reporter. FAKE_DRIVER_FAIL makes the driver fail in before(:each);
|
|
8
|
+
# FAKE_STEPS writes a run.json whose desktop step map names step 2 of flows/recorded_flow.rb.
|
|
9
|
+
module FlowProject
|
|
10
|
+
TEMPLATES = File.expand_path('../../../lib/generators/templates/flows', __dir__)
|
|
11
|
+
|
|
12
|
+
STUB_SPEC_HELPER = <<~RUBY
|
|
13
|
+
# frozen_string_literal: true
|
|
14
|
+
|
|
15
|
+
require 'json'
|
|
16
|
+
require 'rspec'
|
|
17
|
+
|
|
18
|
+
module RaiderReporter
|
|
19
|
+
Config = Struct.new(:output_dir, :clean_output_dir)
|
|
20
|
+
def self.configuration = (@configuration ||= Config.new(output_dir: 'raider-results', clean_output_dir: true))
|
|
21
|
+
def self.configure = yield(configuration)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# What the real reporter would write to and clean, for the specs to read
|
|
25
|
+
at_exit { File.write('reporter.json', JSON.generate(RaiderReporter.configuration.to_h)) }
|
|
26
|
+
|
|
27
|
+
module DriverHelper
|
|
28
|
+
def driver
|
|
29
|
+
raise 'the driver could not start' if ENV['FAKE_DRIVER_FAIL']
|
|
30
|
+
|
|
31
|
+
@driver ||= Object.new
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
RSpec.configure do |config|
|
|
36
|
+
config.include(DriverHelper)
|
|
37
|
+
config.before(:each) { driver }
|
|
38
|
+
config.after(:each) { File.write('driver_quit', '') }
|
|
39
|
+
# The real reporter writes run.json when it closes; the desktop sidecar names the failed step there
|
|
40
|
+
config.after(:suite) do
|
|
41
|
+
dir = RaiderReporter.configuration.output_dir
|
|
42
|
+
steps = { 'items' => [{ 'id' => 's1', 'title' => 'open' }, { 'id' => 's2', 'title' => 'click "Export"' }], 'failed' => 1 }
|
|
43
|
+
results = [{ 'file_path' => 'flows/recorded_flow.rb', 'status' => 'failed', 'steps' => steps }]
|
|
44
|
+
File.write(File.join(dir, 'run.json'), JSON.generate('results' => results)) if dir && ENV['FAKE_STEPS']
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
def build(dir, flows)
|
|
52
|
+
FileUtils.mkdir_p(File.join(dir, 'flows/support'))
|
|
53
|
+
FileUtils.mkdir_p(File.join(dir, 'helpers'))
|
|
54
|
+
File.write(File.join(dir, 'flows/flow_helper.rb'), render('flow_helper.tt'))
|
|
55
|
+
File.write(File.join(dir, 'flows/support/run.rb'), render('run.tt'))
|
|
56
|
+
File.write(File.join(dir, 'helpers/spec_helper.rb'), STUB_SPEC_HELPER)
|
|
57
|
+
File.write(File.join(dir, '.rspec'), "-I .\n--require helpers/spec_helper\n")
|
|
58
|
+
flows.each { |name, source| File.write(File.join(dir, "flows/#{name}_flow.rb"), source) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render(name) = ERB.new(File.read(File.join(TEMPLATES, name)), trim_mode: '-').result
|
|
62
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require_relative 'flow_project'
|
|
7
|
+
|
|
8
|
+
# `raider flow run` and `raider flow list` against a project with its own Gemfile, from another folder.
|
|
9
|
+
RSpec.describe 'raider flow run and list' do
|
|
10
|
+
raider = File.expand_path('../../../bin/raider', __dir__)
|
|
11
|
+
|
|
12
|
+
flows = {
|
|
13
|
+
'ok' => <<~RUBY,
|
|
14
|
+
require_relative 'flow_helper'
|
|
15
|
+
|
|
16
|
+
RSpec.describe 'ok' do
|
|
17
|
+
it('runs every step') { puts "who=\#{flow_input('who', 'nobody')} headless=\#{ENV.fetch('HEADLESS', 'unset')}" }
|
|
18
|
+
end
|
|
19
|
+
RUBY
|
|
20
|
+
'broken' => <<~RUBY
|
|
21
|
+
require_relative 'flow_helper'
|
|
22
|
+
|
|
23
|
+
RSpec.describe 'broken' do
|
|
24
|
+
it 'downloads the report' do
|
|
25
|
+
raise 'no such element'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
RUBY
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Built once: bundling the project is the slow part
|
|
32
|
+
dir = nil
|
|
33
|
+
|
|
34
|
+
before(:context) do # rubocop:disable RSpec/BeforeAfterAll
|
|
35
|
+
dir = File.realpath(Dir.mktmpdir('flow-command'))
|
|
36
|
+
FlowProject.build(dir, flows)
|
|
37
|
+
File.write(File.join(dir, 'Gemfile'), "source 'https://rubygems.org'\ngem 'rspec'\n")
|
|
38
|
+
_out, err, status = Bundler.with_unbundled_env do
|
|
39
|
+
Open3.capture3('bundle', 'install', '--prefer-local', '--quiet', chdir: dir)
|
|
40
|
+
end
|
|
41
|
+
raise "bundle install failed: #{err}" unless status.success?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
after(:context) { FileUtils.rm_rf(dir) } # rubocop:disable RSpec/BeforeAfterAll
|
|
45
|
+
|
|
46
|
+
define_method(:raider_flow) do |*args|
|
|
47
|
+
out, err, status = Open3.capture3(RbConfig.ruby, raider, 'flow', *args, '--project', dir, chdir: Dir.tmpdir)
|
|
48
|
+
[out, err, status.exitstatus]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'runs a completed flow, passing inputs and headless through, and exits 0' do
|
|
52
|
+
out, _err, code = raider_flow('run', 'ok', '--input', 'who=ada')
|
|
53
|
+
|
|
54
|
+
expect(code).to eq(0)
|
|
55
|
+
expect(out).to include('who=ada headless=true', 'Flow ok completed in ')
|
|
56
|
+
expect(out).to match(%r{^Results: flow-results/ok/\d{8}T\d{6}Z(-\d+)?$})
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'exits 1 and names the failed line' do
|
|
60
|
+
out, _err, code = raider_flow('run', 'broken')
|
|
61
|
+
|
|
62
|
+
expect(code).to eq(1)
|
|
63
|
+
expect(out).to include('Flow broken failed at line 5 (downloads the report)')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'exits 2 for a flow that does not exist' do
|
|
67
|
+
_out, err, code = raider_flow('run', 'missing')
|
|
68
|
+
|
|
69
|
+
expect(code).to eq(2)
|
|
70
|
+
expect(err).to include('No flow missing: flows/missing_flow.rb not found')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'lists every flow with its last result as JSON for the desktop' do
|
|
74
|
+
raider_flow('run', 'ok')
|
|
75
|
+
out, _err, code = raider_flow('list', '--json')
|
|
76
|
+
listed = JSON.parse(out)
|
|
77
|
+
|
|
78
|
+
expect(code).to eq(0)
|
|
79
|
+
expect(listed.map { |flow| flow['name'] }).to eq(%w[broken ok])
|
|
80
|
+
expect(listed.last).to include('file' => 'flows/ok_flow.rb')
|
|
81
|
+
expect(listed.last['last'].keys).to eq(%w[status finished_at failed_step results_dir])
|
|
82
|
+
expect(listed.last['last']['status']).to eq('completed')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'lists flows as text' do
|
|
86
|
+
raider_flow('run', 'ok')
|
|
87
|
+
out, = raider_flow('list')
|
|
88
|
+
|
|
89
|
+
expect(out).to match(/^ok\s+completed\s+\d{4}-\d\d-\d\dT/)
|
|
90
|
+
end
|
|
91
|
+
end
|