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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../../../lib/utilities/schedules/schedule'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/os/launchd'
|
|
6
|
+
require_relative '../../../lib/utilities/schedules/os/task_scheduler'
|
|
7
|
+
require_relative '../../../lib/utilities/schedules/os/systemd'
|
|
8
|
+
require_relative '../../../lib/utilities/schedules/os/cron'
|
|
9
|
+
|
|
10
|
+
# The OS entry of a test schedule holds its name only: the selection, retries, notify and show_browser
|
|
11
|
+
# are read from schedules.yml at run time, so no file name or tag can reach a plist, XML, unit or crontab.
|
|
12
|
+
RSpec.describe 'A test schedule OS entry' do
|
|
13
|
+
let(:tmp) { File.realpath(Dir.mktmpdir('schedules-entry')) }
|
|
14
|
+
let(:root) { File.join(tmp, 'project') }
|
|
15
|
+
let(:runner) { instance_double(RubyRaider::Schedules::Os::CommandRunner) }
|
|
16
|
+
let(:now) { Time.utc(2026, 11, 9, 10, 0, 0) }
|
|
17
|
+
let(:hostile) { %q(spec/$(id) & <x> "q" 'p' %PATH% `w`; rm -rf ~_spec.rb) }
|
|
18
|
+
|
|
19
|
+
before do
|
|
20
|
+
FileUtils.mkdir_p(File.join(root, 'spec'))
|
|
21
|
+
[hostile, 'spec/a_spec.rb'].each { |file| File.write(File.join(root, file), '') }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
after { FileUtils.rm_rf(tmp) }
|
|
25
|
+
|
|
26
|
+
def schedule(**options)
|
|
27
|
+
project = RubyRaider::Schedules::Project.new(root)
|
|
28
|
+
RubyRaider::Schedules::Schedule.build(project: project, name: 'nightly', every: 'weekday', at: '07:30', now: now, host_os: 'darwin24', **options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def kind = RubyRaider::Schedules::Kind::TESTS
|
|
32
|
+
|
|
33
|
+
def entries(item)
|
|
34
|
+
clock = Struct.new(:now).new(Time.new(2026, 11, 9, 10, 30, 0))
|
|
35
|
+
systemd = RubyRaider::Schedules::Os::Systemd.new(kind: kind, runner: runner, home: tmp, config_home: nil)
|
|
36
|
+
[RubyRaider::Schedules::Os::Launchd.new(kind: kind, runner: runner, home: tmp, uid: 501).plist(item),
|
|
37
|
+
RubyRaider::Schedules::Os::TaskScheduler.new(kind: kind, runner: runner, clock: clock, schtasks: 'C:\\schtasks.exe').task_xml(item),
|
|
38
|
+
systemd.service(item), systemd.timer(item), RubyRaider::Schedules::Os::Cron.new(kind: kind, runner: runner).line(item)]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'is byte-identical for two different selections, retries, notify and show_browser' do
|
|
42
|
+
files = RubyRaider::Schedules::Selection.parse(files: [hostile, 'spec/a_spec.rb'], tag: nil, root: root, host_os: 'darwin24')
|
|
43
|
+
all_tests = entries(schedule)
|
|
44
|
+
picked = entries(schedule(selection: files, retries: 3, notify: true, show_browser: true))
|
|
45
|
+
tagged = entries(schedule(selection: RubyRaider::Schedules::Selection.parse(files: [], tag: 'zz_tag_sentinel', root: root, host_os: 'darwin24')))
|
|
46
|
+
|
|
47
|
+
expect([picked, tagged]).to eq([all_tests, all_tests])
|
|
48
|
+
expect(all_tests.join).not_to include('rm -rf', '%PATH%', 'a_spec.rb', 'zz_tag_sentinel', '--retries', '--notify', 'show-browser', 'show_browser')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'names the test schedule kind and the schedule in every entry' do
|
|
52
|
+
expect(entries(schedule)).to all(include('nightly', 'schedule'))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../../../lib/utilities/schedules/os_enumerator'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/schedule'
|
|
6
|
+
|
|
7
|
+
RSpec.describe RubyRaider::Schedules::OsEnumerator do # rubocop:disable RSpec/FilePath
|
|
8
|
+
# Answers list_ids with what it was given, recording the prefix asked for
|
|
9
|
+
let(:fake_adapter) do
|
|
10
|
+
Class.new do
|
|
11
|
+
attr_reader :asked
|
|
12
|
+
|
|
13
|
+
def initialize(ids = [], error: nil)
|
|
14
|
+
@ids = ids
|
|
15
|
+
@error = error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def list_ids(prefix)
|
|
19
|
+
@asked = prefix
|
|
20
|
+
raise @error if @error
|
|
21
|
+
|
|
22
|
+
@ids
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:root) { '/projects/shop' }
|
|
28
|
+
let(:prefix) { RubyRaider::Schedules::Schedule.os_id_prefix_for(root) }
|
|
29
|
+
let(:other) { RubyRaider::Schedules::Schedule.os_id_prefix_for('/projects/other') }
|
|
30
|
+
let(:legacy) { Class.new(RubyRaider::Schedules::Rule) { def self.kind = RubyRaider::Schedules::Kind::FLOW_LEGACY }.os_id_prefix_for(root) }
|
|
31
|
+
|
|
32
|
+
def enumerator(adapter) = described_class.new(adapter, schedule_class: RubyRaider::Schedules::Schedule, root: root)
|
|
33
|
+
|
|
34
|
+
it "asks for this project's prefix and returns the schedule names, sorted and once each" do
|
|
35
|
+
adapter = fake_adapter.new(["#{prefix}smoke", "#{prefix}nightly", "#{prefix}smoke"])
|
|
36
|
+
|
|
37
|
+
expect(enumerator(adapter).names).to eq(%w[nightly smoke])
|
|
38
|
+
expect(adapter.asked).to eq(prefix)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "ignores what an adapter should not have returned: another project's, a flow's and hostile ids" do
|
|
42
|
+
hostile = ["#{other}nightly", "#{legacy}nightly", "#{prefix}../../x", "#{prefix}a;rm -rf ~", "#{prefix}Bad", nil, 42,
|
|
43
|
+
"#{prefix}ok\nx", "#{prefix}a\xFF".dup.force_encoding(Encoding::UTF_8), "#{prefix}#{'a' * 61}"]
|
|
44
|
+
|
|
45
|
+
expect(enumerator(fake_adapter.new(hostile + ["#{prefix}ok"])).names).to eq(%w[ok])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'is empty when the OS scheduler can not be read, or the adapter can not enumerate' do
|
|
49
|
+
failing = fake_adapter.new(error: RubyRaider::Schedules::Error.new('task list unreadable'))
|
|
50
|
+
|
|
51
|
+
expect([enumerator(failing).names, enumerator(Object.new).names, enumerator(fake_adapter.new(nil)).names]).to eq([[], [], []])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'lets an unexpected error through' do
|
|
55
|
+
expect { enumerator(fake_adapter.new(error: ArgumentError.new('bug'))).names }.to raise_error(ArgumentError, 'bug')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/project'
|
|
6
|
+
|
|
7
|
+
RSpec.describe RubyRaider::Schedules::Project do # rubocop:disable RSpec/FilePath
|
|
8
|
+
let(:dir) { File.realpath(Dir.mktmpdir('raider-project')) }
|
|
9
|
+
let(:outside) { File.realpath(Dir.mktmpdir('raider-outside')) }
|
|
10
|
+
let(:project) { described_class.new(dir) }
|
|
11
|
+
let(:helper) { File.join(dir, 'helpers', 'driver_helper.rb') }
|
|
12
|
+
let(:runtime_helper) { "# frozen_string_literal: true\n\nrequire 'yaml'\nrequire 'ruby_raider/runtime'\n" }
|
|
13
|
+
|
|
14
|
+
before { FileUtils.mkdir_p(File.dirname(helper)) }
|
|
15
|
+
|
|
16
|
+
after { FileUtils.rm_rf([dir, outside]) }
|
|
17
|
+
|
|
18
|
+
describe '#runtime_driver?' do
|
|
19
|
+
it 'is true when helpers/driver_helper.rb requires the runtime' do
|
|
20
|
+
File.write(helper, runtime_helper)
|
|
21
|
+
expect(project.runtime_driver?).to be(true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'is true for a helper with bytes that are not UTF-8' do
|
|
25
|
+
File.binwrite(helper, "# \xFF\xFE\n#{runtime_helper}")
|
|
26
|
+
expect(project.runtime_driver?).to be(true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'is true when the helper links to a file inside the project' do
|
|
30
|
+
File.write(File.join(dir, 'real_helper.rb'), runtime_helper)
|
|
31
|
+
File.symlink(File.join(dir, 'real_helper.rb'), helper)
|
|
32
|
+
expect(project.runtime_driver?).to be(true)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'is false when the helper is missing, made before 4.0, or only mentions the runtime in a comment' do
|
|
36
|
+
results = [project.runtime_driver?]
|
|
37
|
+
File.write(helper, "require 'selenium-webdriver'\nrequire 'yaml'\n")
|
|
38
|
+
results << project.runtime_driver?
|
|
39
|
+
File.write(helper, "# require 'ruby_raider/runtime'\n")
|
|
40
|
+
results << project.runtime_driver?
|
|
41
|
+
expect(results).to eq([false, false, false])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'is false when the helper links out of the project' do
|
|
45
|
+
File.write(File.join(outside, 'driver_helper.rb'), runtime_helper)
|
|
46
|
+
File.symlink(File.join(outside, 'driver_helper.rb'), helper)
|
|
47
|
+
expect(project.runtime_driver?).to be(false)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'is false when helpers/ links out of the project' do
|
|
51
|
+
FileUtils.mkdir_p(File.join(outside, 'helpers'))
|
|
52
|
+
File.write(File.join(outside, 'helpers', 'driver_helper.rb'), runtime_helper)
|
|
53
|
+
FileUtils.rm_rf(File.dirname(helper))
|
|
54
|
+
File.symlink(File.join(outside, 'helpers'), File.dirname(helper))
|
|
55
|
+
expect(project.runtime_driver?).to be(false)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'is false when the helper is not a regular file' do
|
|
59
|
+
File.mkfifo(helper)
|
|
60
|
+
expect(project.runtime_driver?).to be(false)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#check_show_browser!' do
|
|
65
|
+
it 'refuses a project made before Ruby Raider 4.0' do
|
|
66
|
+
File.write(helper, "require 'selenium-webdriver'\n")
|
|
67
|
+
expect { project.check_show_browser! }
|
|
68
|
+
.to raise_error(RubyRaider::Schedules::Error, 'Show the browser needs a project made with Ruby Raider 4.0 or later')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'accepts a project whose driver_helper requires the runtime' do
|
|
72
|
+
File.write(helper, runtime_helper)
|
|
73
|
+
expect { project.check_show_browser! }.not_to raise_error
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#check_raider_version!' do
|
|
78
|
+
own = RubyRaider::Schedules::Project::OWN_VERSION
|
|
79
|
+
needs = "Scheduling needs ruby_raider #{own} or later in this project"
|
|
80
|
+
|
|
81
|
+
def lock(*spec_lines)
|
|
82
|
+
File.write(File.join(dir, 'Gemfile'), "gem 'ruby_raider'\n")
|
|
83
|
+
File.write(File.join(dir, 'Gemfile.lock'), "GEM\n remote: https://rubygems.org/\n specs:\n#{spec_lines.join("\n")}\n\nDEPENDENCIES\n ruby_raider\n")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'reads this gem\'s version from lib/version' do
|
|
87
|
+
expect(own).to eq(File.read(File.expand_path('../../../lib/version', __dir__)).strip)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'accepts a lock at this version or later, from rubygems, a path or git' do
|
|
91
|
+
[' ruby_raider (%s)', ' ruby_raider (99.0.0)'].each do |line|
|
|
92
|
+
lock(format(line, own), ' selenium-webdriver (4.52.0)')
|
|
93
|
+
expect { project.check_raider_version! }.not_to raise_error
|
|
94
|
+
end
|
|
95
|
+
File.write(File.join(dir, 'Gemfile.lock'), "PATH\n remote: ../ruby_raider\n specs:\n ruby_raider (#{own})\n thor\n")
|
|
96
|
+
expect { project.check_raider_version! }.not_to raise_error
|
|
97
|
+
File.write(File.join(dir, 'Gemfile.lock'), "GIT\n remote: https://github.com/RaiderHQ/ruby_raider\n specs:\n ruby_raider (#{own})\n")
|
|
98
|
+
expect { project.check_raider_version! }.not_to raise_error
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'reads a lock with Windows line endings' do
|
|
102
|
+
lock(" ruby_raider (#{own})")
|
|
103
|
+
File.write(File.join(dir, 'Gemfile.lock'), File.read(File.join(dir, 'Gemfile.lock')).gsub("\n", "\r\n"))
|
|
104
|
+
expect { project.check_raider_version! }.not_to raise_error
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'refuses a lock that resolves an older ruby_raider, or none' do
|
|
108
|
+
lock(' ruby_raider (4.0.1)')
|
|
109
|
+
expect { project.check_raider_version!('4.2.0') }.to raise_error(RubyRaider::Schedules::Error, 'Scheduling needs ruby_raider 4.2.0 or later in this project: bundle update ruby_raider')
|
|
110
|
+
lock(' rspec (3.13.0)', ' ruby_raider (= 9.0.0)')
|
|
111
|
+
expect { project.check_raider_version! }.to raise_error(RubyRaider::Schedules::Error, "#{needs}: bundle update ruby_raider")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'refuses a project with no Gemfile, or no Gemfile.lock yet' do
|
|
115
|
+
expect { project.check_raider_version! }.to raise_error(RubyRaider::Schedules::Error, "#{needs}: no Gemfile found")
|
|
116
|
+
File.write(File.join(dir, 'Gemfile'), "gem 'ruby_raider'\n")
|
|
117
|
+
expect { project.check_raider_version! }.to raise_error(RubyRaider::Schedules::Error, "#{needs}: run bundle install first")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'never evaluates the lock' do
|
|
121
|
+
lock(" ruby_raider (\#{`id`})", " ruby_raider (\#{exit!})")
|
|
122
|
+
expect { project.check_raider_version! }.to raise_error(RubyRaider::Schedules::Error, "#{needs}: bundle update ruby_raider")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/run_lock'
|
|
6
|
+
require_relative '../../../lib/utilities/schedules/project'
|
|
7
|
+
|
|
8
|
+
# The lock, its folder and running.json sit next to the project they belong to
|
|
9
|
+
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
10
|
+
RSpec.describe RubyRaider::Schedules::RunLock do # rubocop:disable RSpec/FilePath
|
|
11
|
+
let(:dir) { File.realpath(Dir.mktmpdir('schedules-lock')) }
|
|
12
|
+
let(:project) { RubyRaider::Schedules::Project.new(dir) }
|
|
13
|
+
let(:lock) { described_class.new(project) }
|
|
14
|
+
let(:folder) { File.join(dir, '.raider/schedules') }
|
|
15
|
+
let(:running) { File.join(folder, 'running.json') }
|
|
16
|
+
let(:lock_file) { File.join(folder, 'run.lock') }
|
|
17
|
+
let(:started_at) { '2026-11-11T08:00:00.000Z' }
|
|
18
|
+
|
|
19
|
+
after do
|
|
20
|
+
lock.release
|
|
21
|
+
FileUtils.rm_rf(dir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def acquire = lock.acquire(name: 'nightly', run_id: 'r1', started_at: started_at)
|
|
25
|
+
|
|
26
|
+
def dead_pid
|
|
27
|
+
pid = Process.spawn(RbConfig.ruby, '-e', 'exit 0')
|
|
28
|
+
Process.wait(pid)
|
|
29
|
+
pid
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write_running(pid)
|
|
33
|
+
FileUtils.mkdir_p(folder)
|
|
34
|
+
File.write(running, JSON.generate('pid' => pid, 'name' => 'other', 'run_id' => 'x', 'started_at' => started_at))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'takes the lock and writes running.json for the desktop, private' do
|
|
38
|
+
expect(acquire).to be(true)
|
|
39
|
+
expect(JSON.parse(File.read(running))).to eq('pid' => Process.pid, 'name' => 'nightly', 'run_id' => 'r1', 'started_at' => started_at)
|
|
40
|
+
expect([File.stat(running).mode & 0o777, File.stat(lock_file).mode & 0o777, File.stat(folder).mode & 0o777]).to eq([0o600, 0o600, 0o700])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'removes running.json and frees the lock on release, keeping run.lock' do
|
|
44
|
+
acquire
|
|
45
|
+
lock.release
|
|
46
|
+
|
|
47
|
+
expect([File.exist?(running), File.exist?(lock_file)]).to eq([false, true])
|
|
48
|
+
expect(described_class.new(project).acquire(name: 'nightly', run_id: 'r2', started_at: started_at)).to be(true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'is busy while another run holds the lock, and writes nothing' do
|
|
52
|
+
FileUtils.mkdir_p(folder)
|
|
53
|
+
File.open(lock_file, File::RDWR | File::CREAT, 0o600) do |held|
|
|
54
|
+
held.flock(File::LOCK_EX)
|
|
55
|
+
|
|
56
|
+
expect(acquire).to be(false)
|
|
57
|
+
expect(File.exist?(running)).to be(false)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'is busy while running.json names a live process, and leaves that file alone' do
|
|
62
|
+
write_running(Process.ppid)
|
|
63
|
+
before = File.read(running)
|
|
64
|
+
|
|
65
|
+
expect(acquire).to be(false)
|
|
66
|
+
lock.release
|
|
67
|
+
expect(File.read(running)).to eq(before)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'replaces a running.json whose process is gone' do
|
|
71
|
+
write_running(dead_pid)
|
|
72
|
+
|
|
73
|
+
expect(acquire).to be(true)
|
|
74
|
+
expect(JSON.parse(File.read(running))['run_id']).to eq('r1')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
[0, -1, 'abc', nil, Process.pid].each do |pid|
|
|
78
|
+
it "treats a running.json with pid #{pid.inspect} as stale" do
|
|
79
|
+
write_running(pid)
|
|
80
|
+
|
|
81
|
+
expect(acquire).to be(true)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'treats a running.json that is not JSON as stale' do
|
|
86
|
+
FileUtils.mkdir_p(folder)
|
|
87
|
+
File.write(running, 'not json')
|
|
88
|
+
|
|
89
|
+
expect(acquire).to be(true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'never follows a linked running.json' do
|
|
93
|
+
FileUtils.mkdir_p(folder)
|
|
94
|
+
target = File.join(dir, 'target.json')
|
|
95
|
+
File.write(target, JSON.generate('pid' => Process.ppid))
|
|
96
|
+
File.symlink(target, running)
|
|
97
|
+
|
|
98
|
+
expect(acquire).to be(true)
|
|
99
|
+
expect([File.symlink?(running), File.read(target)]).to eq([false, JSON.generate('pid' => Process.ppid)])
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'closes run.lock when it is not a regular file' do
|
|
103
|
+
FileUtils.mkdir_p(folder)
|
|
104
|
+
File.mkfifo(lock_file)
|
|
105
|
+
open_before = Dir.children('/dev/fd').size
|
|
106
|
+
|
|
107
|
+
expect { acquire }.to raise_error(RubyRaider::Schedules::Error, /run.lock must be a file/)
|
|
108
|
+
expect(Dir.children('/dev/fd').size).to eq(open_before)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'is busy, and keeps the new file, when running.json is replaced between the check and the removal' do
|
|
112
|
+
write_running(dead_pid)
|
|
113
|
+
real_rename = File.method(:rename)
|
|
114
|
+
allow(File).to receive(:rename) do |from, to|
|
|
115
|
+
real_rename.call(from, to)
|
|
116
|
+
# The desktop writes its own running.json right after ours moved the stale one aside
|
|
117
|
+
File.write(running, JSON.generate('pid' => Process.ppid, 'run_id' => 'desktop')) if to.end_with?('.stale') && !File.exist?(running)
|
|
118
|
+
end
|
|
119
|
+
seen = File.lstat(running)
|
|
120
|
+
allow(File).to receive(:lstat).and_call_original
|
|
121
|
+
allow(File).to receive(:lstat).with(a_string_ending_with('.stale')).and_return(instance_double(File::Stat, dev: seen.dev, ino: seen.ino + 1))
|
|
122
|
+
|
|
123
|
+
expect(acquire).to be(false)
|
|
124
|
+
expect([JSON.parse(File.read(running))['run_id'], Dir.children(folder).grep(/stale/)]).to eq(['desktop', []])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'refuses a linked run.lock' do
|
|
128
|
+
FileUtils.mkdir_p(folder)
|
|
129
|
+
File.write(File.join(dir, 'target.lock'), '')
|
|
130
|
+
File.symlink(File.join(dir, 'target.lock'), lock_file)
|
|
131
|
+
|
|
132
|
+
expect { acquire }.to raise_error(RubyRaider::Schedules::Error, /run.lock/)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'leaves a running.json that another run wrote after ours' do
|
|
136
|
+
acquire
|
|
137
|
+
File.write(running, JSON.generate('pid' => Process.ppid, 'run_id' => 'theirs'))
|
|
138
|
+
lock.release
|
|
139
|
+
|
|
140
|
+
expect(JSON.parse(File.read(running))['run_id']).to eq('theirs')
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
# rubocop:enable RSpec/MultipleMemoizedHelpers
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/run_log'
|
|
6
|
+
require_relative '../../../lib/utilities/schedules/project'
|
|
7
|
+
|
|
8
|
+
RSpec.describe RubyRaider::Schedules::RunLog do # rubocop:disable RSpec/FilePath
|
|
9
|
+
let(:dir) { File.realpath(Dir.mktmpdir('schedules-log')) }
|
|
10
|
+
let(:log) { described_class.new(RubyRaider::Schedules::Project.new(dir), 'nightly') }
|
|
11
|
+
let(:folder) { File.join(dir, '.raider/schedules/nightly') }
|
|
12
|
+
let(:file) { File.join(folder, 'runs.jsonl') }
|
|
13
|
+
|
|
14
|
+
after { FileUtils.rm_rf(dir) }
|
|
15
|
+
|
|
16
|
+
it 'appends one JSON line per run in a private folder' do
|
|
17
|
+
log.append({ 'run_id' => 'a', 'status' => 'passed' })
|
|
18
|
+
log.append({ 'run_id' => 'b', 'status' => 'failed' })
|
|
19
|
+
|
|
20
|
+
expect(File.readlines(file).map { |line| JSON.parse(line)['run_id'] }).to eq(%w[a b])
|
|
21
|
+
expect([File.stat(folder).mode & 0o777, File.stat(file).mode & 0o777]).to eq([0o700, 0o600])
|
|
22
|
+
expect(log.find('a')['status']).to eq('passed')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'keeps the last 50 lines' do
|
|
26
|
+
52.times { |n| log.append({ 'run_id' => n.to_s }) }
|
|
27
|
+
|
|
28
|
+
ids = log.lines.map { |line| line['run_id'] }
|
|
29
|
+
expect([ids.size, ids.first, ids.last]).to eq([50, '2', '51'])
|
|
30
|
+
# No temp file left behind: only the log and its lock
|
|
31
|
+
expect(Dir.children(folder).sort).to eq(%w[runs.jsonl runs.jsonl.lock])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'waits for runs.jsonl.lock, so a busy line never overwrites a trim in progress' do
|
|
35
|
+
50.times { |n| log.append({ 'run_id' => n.to_s }) }
|
|
36
|
+
File.open(File.join(folder, 'runs.jsonl.lock'), File::RDWR) do |held|
|
|
37
|
+
held.flock(File::LOCK_EX)
|
|
38
|
+
busy = Thread.new { log.append({ 'run_id' => 'busy' }) }
|
|
39
|
+
|
|
40
|
+
expect([busy.join(0.3), log.lines.size]).to eq([nil, 50])
|
|
41
|
+
held.flock(File::LOCK_UN)
|
|
42
|
+
busy.join
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
expect(log.lines.map { |line| line['run_id'] }.values_at(0, -1)).to eq(%w[1 busy])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'refuses a linked lock and leaves its target alone' do
|
|
49
|
+
FileUtils.mkdir_p(folder)
|
|
50
|
+
target = File.join(dir, 'elsewhere.lock')
|
|
51
|
+
File.symlink(target, File.join(folder, 'runs.jsonl.lock'))
|
|
52
|
+
|
|
53
|
+
expect { log.append({ 'run_id' => 'a' }) }.to raise_error(RubyRaider::Schedules::Error, /runs.jsonl.lock must be a file, not a link/)
|
|
54
|
+
expect([File.exist?(target), File.exist?(file)]).to eq([false, false])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'refuses a linked log and leaves its target alone' do
|
|
58
|
+
FileUtils.mkdir_p(folder)
|
|
59
|
+
target = File.join(dir, 'target.jsonl')
|
|
60
|
+
File.write(target, "keep\n")
|
|
61
|
+
File.symlink(target, file)
|
|
62
|
+
|
|
63
|
+
expect { log.append({ 'run_id' => 'a' }) }.to raise_error(RubyRaider::Schedules::Error, /runs.jsonl/)
|
|
64
|
+
expect(File.read(target)).to eq("keep\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'refuses a linked schedule folder' do
|
|
68
|
+
other = File.join(dir, 'elsewhere')
|
|
69
|
+
FileUtils.mkdir_p([other, File.join(dir, '.raider/schedules')])
|
|
70
|
+
File.symlink(other, folder)
|
|
71
|
+
|
|
72
|
+
expect { log.append({ 'run_id' => 'a' }) }.to raise_error(RubyRaider::Schedules::Error, /not a link/)
|
|
73
|
+
expect(Dir.children(other)).to eq([])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'reads a log of another kind from its own folder' do
|
|
77
|
+
flow_log = File.join(dir, '.raider/flows/report/runs.jsonl')
|
|
78
|
+
FileUtils.mkdir_p(File.dirname(flow_log))
|
|
79
|
+
File.write(flow_log, %({"status":"completed"}\n))
|
|
80
|
+
|
|
81
|
+
expect(described_class.new(RubyRaider::Schedules::Project.new(dir), 'report', dir: '.raider/flows').last['status']).to eq('completed')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/run_report'
|
|
6
|
+
require_relative '../../../lib/utilities/schedules/project'
|
|
7
|
+
|
|
8
|
+
RSpec.describe RubyRaider::Schedules::RunReport do # rubocop:disable RSpec/FilePath
|
|
9
|
+
let(:dir) { File.realpath(Dir.mktmpdir('schedules-report')) }
|
|
10
|
+
let(:project) { RubyRaider::Schedules::Project.new(dir) }
|
|
11
|
+
let(:results) { File.join(dir, 'raider-results') }
|
|
12
|
+
let(:spawned_at) { Time.utc(2026, 11, 11, 8, 0, 0) }
|
|
13
|
+
let(:last) { File.join(dir, '.raider/schedules/nightly/last') }
|
|
14
|
+
|
|
15
|
+
before { FileUtils.mkdir_p(File.join(dir, '.raider/schedules/nightly')) }
|
|
16
|
+
|
|
17
|
+
after { FileUtils.rm_rf(dir) }
|
|
18
|
+
|
|
19
|
+
def write_run(statuses, started_at: '2026-11-11T08:00:01.000Z')
|
|
20
|
+
FileUtils.mkdir_p(results)
|
|
21
|
+
run = started_at ? { 'started_at' => started_at } : {}
|
|
22
|
+
File.write(File.join(results, 'run.json'), JSON.generate('run' => run, 'results' => statuses.map { |status| { 'status' => status } }))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def load = described_class.load(project, since: spawned_at)
|
|
26
|
+
|
|
27
|
+
it 'counts results: quarantined as failed, pending as skipped' do
|
|
28
|
+
write_run(%w[passed failed quarantined pending skipped passed])
|
|
29
|
+
|
|
30
|
+
expect(load.counts).to eq('total' => 6, 'passed' => 2, 'failed' => 2, 'skipped' => 2)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'is nil without a run.json' do
|
|
34
|
+
expect(load).to be_nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'ignores a run.json left by an earlier run' do
|
|
38
|
+
write_run(%w[passed], started_at: '2026-11-11T07:00:00.000Z')
|
|
39
|
+
|
|
40
|
+
expect(load).to be_nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'accepts a run that started up to 2 s before the spawn' do
|
|
44
|
+
write_run(%w[passed], started_at: '2026-11-11T07:59:58.500Z')
|
|
45
|
+
|
|
46
|
+
expect(load.counts['total']).to eq(1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'falls back to the file time when run.json has no start time' do
|
|
50
|
+
write_run(%w[passed], started_at: nil)
|
|
51
|
+
file = File.join(results, 'run.json')
|
|
52
|
+
File.utime(spawned_at + 5, spawned_at + 5, file)
|
|
53
|
+
fresh = load
|
|
54
|
+
|
|
55
|
+
File.utime(spawned_at - 1, spawned_at - 1, file)
|
|
56
|
+
slack = load
|
|
57
|
+
File.utime(spawned_at - 60, spawned_at - 60, file)
|
|
58
|
+
|
|
59
|
+
expect([fresh&.counts&.fetch('total'), slack&.counts&.fetch('total'), load]).to eq([1, 1, nil])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'is nil for a run.json that is not a report' do
|
|
63
|
+
FileUtils.mkdir_p(results)
|
|
64
|
+
['not json', '[1]', '{"results":"x"}'].each do |text|
|
|
65
|
+
File.write(File.join(results, 'run.json'), text)
|
|
66
|
+
expect(load).to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'never follows a linked run.json or results folder' do
|
|
71
|
+
other = File.join(dir, 'other')
|
|
72
|
+
FileUtils.mkdir_p(other)
|
|
73
|
+
File.write(File.join(other, 'run.json'), JSON.generate('run' => { 'started_at' => '2026-11-11T08:00:01Z' }, 'results' => []))
|
|
74
|
+
File.symlink(other, results)
|
|
75
|
+
linked_folder = load
|
|
76
|
+
FileUtils.rm(results)
|
|
77
|
+
FileUtils.mkdir_p(results)
|
|
78
|
+
File.symlink(File.join(other, 'run.json'), File.join(results, 'run.json'))
|
|
79
|
+
|
|
80
|
+
expect([linked_folder, load]).to eq([nil, nil])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#copy_to' do
|
|
84
|
+
before do
|
|
85
|
+
write_run(%w[passed])
|
|
86
|
+
File.write(File.join(results, 'index.html'), '<html>')
|
|
87
|
+
FileUtils.mkdir_p(File.join(results, 'attachments/sub'))
|
|
88
|
+
File.write(File.join(results, 'attachments/0-shot.png'), 'png')
|
|
89
|
+
File.write(File.join(results, 'attachments/sub/trace.zip'), 'zip')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'copies run.json, index.html and the attachments, replacing the last copy' do
|
|
93
|
+
FileUtils.mkdir_p(last)
|
|
94
|
+
File.write(File.join(last, 'old.txt'), 'old')
|
|
95
|
+
|
|
96
|
+
expect(load.copy_to(last)).to be(true)
|
|
97
|
+
expect(Dir.glob('**/*', base: last).sort).to eq(%w[attachments attachments/0-shot.png attachments/sub attachments/sub/trace.zip index.html run.json])
|
|
98
|
+
expect(File.read(File.join(last, 'attachments/sub/trace.zip'))).to eq('zip')
|
|
99
|
+
expect(Dir.children(File.dirname(last))).to eq(['last'])
|
|
100
|
+
expect(File.stat(last).mode & 0o777).to eq(0o700)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'leaves out the attachments when there are too many files or folders too deep' do
|
|
104
|
+
stub_const("#{described_class}::MAX_ATTACHMENT_FILES", 1)
|
|
105
|
+
load.copy_to(last)
|
|
106
|
+
many = Dir.children(last).sort
|
|
107
|
+
FileUtils.rm_rf(last)
|
|
108
|
+
stub_const("#{described_class}::MAX_ATTACHMENT_FILES", 1000)
|
|
109
|
+
stub_const("#{described_class}::MAX_ATTACHMENT_DEPTH", 0)
|
|
110
|
+
load.copy_to(last)
|
|
111
|
+
|
|
112
|
+
expect([many, Dir.children(last).sort]).to eq([%w[index.html run.json], %w[index.html run.json]])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'copies no more of a file than was counted for it' do
|
|
116
|
+
target = File.join(dir, 'copy.png')
|
|
117
|
+
load.send(:copy_file, File.join(results, 'attachments/0-shot.png'), target, 2)
|
|
118
|
+
|
|
119
|
+
expect(File.read(target)).to eq('pn')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'leaves out the attachments when they are over the cap' do
|
|
123
|
+
stub_const("#{described_class}::MAX_ATTACHMENT_BYTES", 5)
|
|
124
|
+
|
|
125
|
+
load.copy_to(last)
|
|
126
|
+
|
|
127
|
+
expect(Dir.children(last).sort).to eq(%w[index.html run.json])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'copies regular files only, never what a link points at' do
|
|
131
|
+
secret = File.join(dir, 'secret.txt')
|
|
132
|
+
File.write(secret, 'secret')
|
|
133
|
+
File.symlink(secret, File.join(results, 'attachments/leak.txt'))
|
|
134
|
+
File.symlink(File.dirname(secret), File.join(results, 'attachments/leakdir'))
|
|
135
|
+
FileUtils.rm(File.join(results, 'index.html'))
|
|
136
|
+
File.symlink(secret, File.join(results, 'index.html'))
|
|
137
|
+
|
|
138
|
+
load.copy_to(last)
|
|
139
|
+
|
|
140
|
+
expect(Dir.glob('**/*', base: last).sort).to eq(%w[attachments attachments/0-shot.png attachments/sub attachments/sub/trace.zip run.json])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'leaves raider-results as it was' do
|
|
144
|
+
before = Dir.glob('**/*', base: results).sort.to_h { |path| [path, File.file?(File.join(results, path)) && File.read(File.join(results, path))] }
|
|
145
|
+
load.copy_to(last)
|
|
146
|
+
|
|
147
|
+
expect(Dir.glob('**/*', base: results).sort.to_h { |path| [path, File.file?(File.join(results, path)) && File.read(File.join(results, path))] }).to eq(before)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|