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,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/schedule'
|
|
6
|
+
|
|
7
|
+
RSpec.describe RubyRaider::Schedules::Schedule do # rubocop:disable RSpec/FilePath
|
|
8
|
+
let(:dir) { File.realpath(Dir.mktmpdir('schedules-schedule')) }
|
|
9
|
+
let(:project) { RubyRaider::Schedules::Project.new(dir) }
|
|
10
|
+
let(:now) { Time.utc(2026, 11, 9, 10, 0, 0) }
|
|
11
|
+
let(:mac) { 'darwin24' }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
FileUtils.mkdir_p(File.join(dir, 'spec'))
|
|
15
|
+
File.write(File.join(dir, 'spec/a_spec.rb'), '')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after { FileUtils.rm_rf(dir) }
|
|
19
|
+
|
|
20
|
+
def build(**options)
|
|
21
|
+
described_class.build(project: project, name: 'nightly', every: 'day', at: '08:00', now: now, host_os: mac, **options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def error_for(**options)
|
|
25
|
+
build(**options)
|
|
26
|
+
nil
|
|
27
|
+
rescue RubyRaider::Schedules::Error => e
|
|
28
|
+
e.message
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def files(*names) = RubyRaider::Schedules::Selection.parse(files: names, tag: nil, root: dir, host_os: mac)
|
|
32
|
+
|
|
33
|
+
it 'keeps every field of the entry, with all tests by default' do
|
|
34
|
+
schedule = build(every: 'week', on: 'tue', retries: 2, notify: true, show_browser: true)
|
|
35
|
+
hash = schedule.to_h
|
|
36
|
+
|
|
37
|
+
expect(hash.keys).to eq(%w[name every at on retries notify show_browser selection os_id ruby bundle gem_env created_at])
|
|
38
|
+
expect(hash.slice('name', 'every', 'at', 'on', 'retries', 'notify', 'show_browser', 'selection', 'created_at'))
|
|
39
|
+
.to eq({ 'name' => 'nightly', 'every' => 'week', 'at' => '08:00', 'on' => 'tue', 'retries' => 2, 'notify' => true,
|
|
40
|
+
'show_browser' => true, 'selection' => { 'kind' => 'all' }, 'created_at' => '2026-11-09T10:00:00Z' })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'names the os_id after the project and the name' do
|
|
44
|
+
expect(build.os_id).to eq("dev.rubyraider.schedule.#{Digest::SHA256.hexdigest(dir)[0, 10]}.nightly")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'runs raider schedule run NAME and nothing else from the OS entry' do
|
|
48
|
+
schedule = build(retries: 3, notify: true, show_browser: true, selection: files('spec/a_spec.rb'))
|
|
49
|
+
|
|
50
|
+
expect(schedule.argv).to eq([schedule.ruby, schedule.bundle, 'exec', 'raider', 'schedule', 'run', 'nightly', '--project', dir, '--trigger', 'schedule'])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'logs the scheduler output under .raider/schedules and keeps the 4.1 env' do
|
|
54
|
+
schedule = build
|
|
55
|
+
|
|
56
|
+
expect([schedule.log_path, schedule.env.keys.first(2), schedule.working_directory])
|
|
57
|
+
.to eq([File.join(dir, '.raider/schedules/nightly/scheduler.log'), %w[PATH LANG], dir])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'refuses show_browser on Linux and nowhere else' do
|
|
61
|
+
expect([error_for(show_browser: true, host_os: 'x86_64-linux-gnu'), error_for(show_browser: false, host_os: 'x86_64-linux-gnu'),
|
|
62
|
+
error_for(show_browser: true, host_os: 'mingw32')])
|
|
63
|
+
.to eq(["Show the browser isn't available for scheduled runs on Linux", nil, nil])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'refuses a bad name, show_browser or selection' do
|
|
67
|
+
errors = [error_for(name: 'Nightly'), error_for(name: 'a' * 61), error_for(show_browser: 'yes'), error_for(selection: { 'kind' => 'all' })]
|
|
68
|
+
|
|
69
|
+
expect(errors).to eq(['Invalid schedule name "Nightly": use 1 to 60 of a-z, 0-9 and _', "Invalid schedule name \"#{'a' * 61}\": use 1 to 60 of a-z, 0-9 and _",
|
|
70
|
+
'Invalid show_browser "yes": use true or false', 'Invalid selection: use a Selection'])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'shares the rule checks with flow schedules' do
|
|
74
|
+
expect([error_for(every: 'month'), error_for(at: '8:00'), error_for(retries: 4), error_for(notify: 'yes')])
|
|
75
|
+
.to eq(['Invalid every "month": use hour, day, weekday or week', 'Invalid at "8:00": use HH:MM (24h)',
|
|
76
|
+
'Invalid retries 4: use 0 to 3', 'Invalid notify "yes": use true or false'])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '.from_h' do
|
|
80
|
+
it 'reads back what to_h wrote' do
|
|
81
|
+
schedule = build(selection: files('spec/a_spec.rb'), show_browser: true)
|
|
82
|
+
|
|
83
|
+
expect(described_class.from_h(schedule.to_h, dir, host_os: mac).to_h).to eq(schedule.to_h)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'loads an entry whose spec file is gone' do
|
|
87
|
+
hash = build(selection: files('spec/a_spec.rb')).to_h
|
|
88
|
+
File.delete(File.join(dir, 'spec/a_spec.rb'))
|
|
89
|
+
|
|
90
|
+
expect(described_class.from_h(hash, dir, host_os: mac).selection.missing_file(dir)).to eq('spec/a_spec.rb')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'treats an entry without show_browser as off' do
|
|
94
|
+
expect(described_class.from_h(build.to_h.except('show_browser'), dir, host_os: mac).show_browser).to be(false)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'refuses an entry from another project, a flow entry, a broken selection or a non-mapping' do
|
|
98
|
+
other = File.realpath(Dir.mktmpdir('schedules-other'))
|
|
99
|
+
flow = build.to_h.merge('os_id' => build.os_id.sub('.schedule.', '.flow.'))
|
|
100
|
+
entries = [[build.to_h, other], [flow, dir], [build.to_h.merge('selection' => { 'kind' => 'tag', 'tag' => 'a;b' }), dir],
|
|
101
|
+
[build.to_h.except('selection'), dir], ['nightly', dir]]
|
|
102
|
+
errors = entries.map do |hash, root|
|
|
103
|
+
described_class.from_h(hash, root, host_os: mac)
|
|
104
|
+
nil
|
|
105
|
+
rescue RubyRaider::Schedules::Error => e
|
|
106
|
+
e.message
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
expect(errors).to all(be_a(String))
|
|
110
|
+
ensure
|
|
111
|
+
FileUtils.rm_rf(other)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'is frozen' do
|
|
116
|
+
schedule = build(selection: files('spec/a_spec.rb'))
|
|
117
|
+
|
|
118
|
+
expect([schedule, schedule.selection, schedule.name]).to all(be_frozen)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../../../lib/utilities/schedules/schedule_store'
|
|
5
|
+
|
|
6
|
+
RSpec.describe RubyRaider::Schedules::ScheduleStore do # rubocop:disable RSpec/FilePath
|
|
7
|
+
let(:dir) { File.realpath(Dir.mktmpdir('schedules-store')) }
|
|
8
|
+
let(:project) { RubyRaider::Schedules::Project.new(dir) }
|
|
9
|
+
let(:store) { described_class.new(project) }
|
|
10
|
+
let(:file) { File.join(dir, '.raider/schedules/schedules.yml') }
|
|
11
|
+
let(:gitignore) { File.join(dir, '.gitignore') }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
FileUtils.mkdir_p(File.join(dir, 'spec'))
|
|
15
|
+
File.write(File.join(dir, 'spec/a_spec.rb'), '')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after { FileUtils.rm_rf(dir) }
|
|
19
|
+
|
|
20
|
+
def schedule(name, **options)
|
|
21
|
+
RubyRaider::Schedules::Schedule.build(project: project, name: name, every: 'day', at: '08:00', host_os: 'darwin24', **options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def files(*names) = RubyRaider::Schedules::Selection.parse(files: names, tag: nil, root: dir, host_os: 'darwin24')
|
|
25
|
+
|
|
26
|
+
it 'saves to .raider/schedules/schedules.yml, 0600 in a 0700 folder, sorted, and reads it back' do
|
|
27
|
+
store.save(schedule('zeta', selection: files('spec/a_spec.rb'), show_browser: true))
|
|
28
|
+
store.save(schedule('alpha'))
|
|
29
|
+
|
|
30
|
+
expect([store.all.map(&:name), store.find('zeta').selection.files, store.find('zeta').show_browser]).to eq([%w[alpha zeta], %w[spec/a_spec.rb], true])
|
|
31
|
+
expect([File.stat(file).mode & 0o777, File.stat(File.dirname(file)).mode & 0o777]).to eq([0o600, 0o700])
|
|
32
|
+
expect(YAML.safe_load_file(file)['schedules'].last['selection']).to eq({ 'kind' => 'files', 'files' => %w[spec/a_spec.rb] })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'still lists a schedule whose spec file is gone' do
|
|
36
|
+
store.save(schedule('nightly', selection: files('spec/a_spec.rb')))
|
|
37
|
+
File.delete(File.join(dir, 'spec/a_spec.rb'))
|
|
38
|
+
|
|
39
|
+
expect(store.find('nightly').selection.missing_file(dir)).to eq('spec/a_spec.rb')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'skips an entry that fails re-validation and keeps it when it writes' do
|
|
43
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
44
|
+
hostile = schedule('bad').to_h.merge('selection' => { 'kind' => 'files', 'files' => ['../x_spec.rb'] })
|
|
45
|
+
File.write(file, { 'schedules' => [hostile, 'junk'] }.to_yaml)
|
|
46
|
+
store.save(schedule('nightly'))
|
|
47
|
+
|
|
48
|
+
expect([store.all.map(&:name), YAML.safe_load_file(file)['schedules'].size]).to eq([%w[nightly], 3])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'refuses to overwrite a file that is not a schedules file' do
|
|
52
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
53
|
+
File.write(file, "schedules: [\n")
|
|
54
|
+
|
|
55
|
+
expect { store.save(schedule('nightly')) }
|
|
56
|
+
.to raise_error(RubyRaider::Schedules::Error, '.raider/schedules/schedules.yml is not a schedules file: fix or remove it, then try again')
|
|
57
|
+
expect(File.read(file)).to eq("schedules: [\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'refuses a linked schedules folder' do
|
|
61
|
+
FileUtils.mkdir_p(File.join(dir, 'elsewhere'))
|
|
62
|
+
FileUtils.mkdir_p(File.join(dir, '.raider'))
|
|
63
|
+
File.symlink(File.join(dir, 'elsewhere'), File.join(dir, '.raider/schedules'))
|
|
64
|
+
|
|
65
|
+
expect { store.save(schedule('nightly')) }.to raise_error(RubyRaider::Schedules::Error, '.raider/schedules must be a folder, not a link')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'does not load Ruby objects' do
|
|
69
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
70
|
+
File.write(file, "--- !ruby/object:OpenStruct\ntable: {}\n")
|
|
71
|
+
|
|
72
|
+
expect(store.all).to eq([])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns the name and test os_id of an entry it removed but could not read' do
|
|
76
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
77
|
+
File.write(file, { 'schedules' => [{ 'name' => 'nightly' }] }.to_yaml)
|
|
78
|
+
removed = store.delete('nightly')
|
|
79
|
+
|
|
80
|
+
expect([removed.name, removed.os_id]).to eq(['nightly', RubyRaider::Schedules::Schedule.os_id_for(dir, 'nightly')])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#ignore_in_git' do
|
|
84
|
+
it 'creates .gitignore with the line when there is none' do
|
|
85
|
+
expect([store.ignore_in_git, File.read(gitignore)]).to eq([true, ".raider/schedules/\n"])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'appends the line, keeping every byte and adding a newline first when the file lacks one' do
|
|
89
|
+
File.binwrite(gitignore, "node_modules/\r\n\xFFraw")
|
|
90
|
+
|
|
91
|
+
expect([store.ignore_in_git, File.binread(gitignore)]).to eq([true, "node_modules/\r\n\xFFraw\n.raider/schedules/\n".b])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'leaves the file alone when the exact line is there, and only adds it once' do
|
|
95
|
+
File.write(gitignore, "a/\n.raider/schedules/\r\nb/")
|
|
96
|
+
before = File.binread(gitignore)
|
|
97
|
+
|
|
98
|
+
expect([store.ignore_in_git, File.binread(gitignore)]).to eq([false, before])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'adds the line when only a comment, a longer line or a prefix mentions it' do
|
|
102
|
+
File.write(gitignore, "# .raider/schedules/\n.raider/schedules/x\n.raider/\n")
|
|
103
|
+
first = store.ignore_in_git
|
|
104
|
+
|
|
105
|
+
expect([first, store.ignore_in_git, File.read(gitignore).lines.last]).to eq([true, false, ".raider/schedules/\n"])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'refuses a .gitignore that is not a file' do
|
|
109
|
+
FileUtils.mkdir_p(gitignore)
|
|
110
|
+
|
|
111
|
+
expect { store.ignore_in_git }.to raise_error(RubyRaider::Schedules::Error, '.gitignore must be a file')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'refuses a linked .gitignore and never writes through it' do
|
|
115
|
+
target = File.join(File.realpath(Dir.mktmpdir('outside')), 'ignored')
|
|
116
|
+
File.write(target, "keep\n")
|
|
117
|
+
File.symlink(target, gitignore)
|
|
118
|
+
|
|
119
|
+
expect { store.ignore_in_git }.to raise_error(RubyRaider::Schedules::Error, '.gitignore is a link: add .raider/schedules/ to it yourself')
|
|
120
|
+
expect([File.read(target), File.symlink?(gitignore)]).to eq(["keep\n", true])
|
|
121
|
+
ensure
|
|
122
|
+
FileUtils.rm_rf(File.dirname(target))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'refuses a dangling .gitignore link without making its target' do
|
|
126
|
+
target = File.join(dir, 'made_by_the_link')
|
|
127
|
+
File.symlink(target, gitignore)
|
|
128
|
+
|
|
129
|
+
expect { store.ignore_in_git }.to raise_error(RubyRaider::Schedules::Error, /is a link/)
|
|
130
|
+
expect(File.exist?(target)).to be(false)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'leaves a read-only .gitignore that has the line alone, and says so when it lacks it' do
|
|
134
|
+
File.write(gitignore, ".raider/schedules/\n")
|
|
135
|
+
File.chmod(0o444, gitignore)
|
|
136
|
+
expect(store.ignore_in_git).to be(false)
|
|
137
|
+
|
|
138
|
+
File.chmod(0o644, gitignore)
|
|
139
|
+
File.write(gitignore, "a/\n")
|
|
140
|
+
File.chmod(0o444, gitignore)
|
|
141
|
+
expect { store.ignore_in_git }.to raise_error(Errno::EACCES)
|
|
142
|
+
expect(File.read(gitignore)).to eq("a/\n")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'refuses a .gitignore that is a FIFO without blocking' do
|
|
146
|
+
File.mkfifo(gitignore)
|
|
147
|
+
|
|
148
|
+
expect { store.ignore_in_git }.to raise_error(RubyRaider::Schedules::Error, '.gitignore must be a file')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative '../../../lib/utilities/schedules/scheduler'
|
|
6
|
+
|
|
7
|
+
# The scheduler with the real launchd adapter: plists in a temp home, launchctl and plutil played by a fake
|
|
8
|
+
# runner. Nothing real is run and the real ~/Library/LaunchAgents is never read.
|
|
9
|
+
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
10
|
+
RSpec.describe RubyRaider::Schedules::Scheduler do # rubocop:disable RSpec/FilePath
|
|
11
|
+
# Records every command and answers success; launchctl print succeeds only for a loaded service
|
|
12
|
+
let(:runner_class) do
|
|
13
|
+
Class.new do
|
|
14
|
+
attr_reader :calls, :loaded
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@calls = []
|
|
18
|
+
@loaded = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(*argv, **)
|
|
22
|
+
@calls << argv
|
|
23
|
+
verb = argv[1]
|
|
24
|
+
target = argv.last
|
|
25
|
+
status = 0
|
|
26
|
+
case verb
|
|
27
|
+
when 'bootstrap' then @loaded << File.basename(target, '.plist')
|
|
28
|
+
when 'bootout' then status = @loaded.delete(target.split('/').last) ? 0 : 3
|
|
29
|
+
when 'print' then status = @loaded.include?(target.split('/').last) ? 0 : 113
|
|
30
|
+
end
|
|
31
|
+
RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: '', stderr: '', exitstatus: status)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
let(:tmp) { File.realpath(Dir.mktmpdir('schedules-launchd-wiring')) }
|
|
37
|
+
let(:home) { File.join(tmp, 'home') }
|
|
38
|
+
let(:root) do
|
|
39
|
+
File.join(tmp, 'project').tap do |dir|
|
|
40
|
+
FileUtils.mkdir_p(dir)
|
|
41
|
+
# add checks the project's bundle has `raider schedule`
|
|
42
|
+
File.write(File.join(dir, 'Gemfile'), "gem 'ruby_raider'\n")
|
|
43
|
+
File.write(File.join(dir, 'Gemfile.lock'), "GEM\n specs:\n ruby_raider (#{RubyRaider::Schedules::Project::OWN_VERSION})\n")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
let(:project) { RubyRaider::Schedules::Project.new(root) }
|
|
47
|
+
let(:runner) { runner_class.new }
|
|
48
|
+
let(:launchd) { RubyRaider::Schedules::Os::Launchd.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, uid: 501) }
|
|
49
|
+
let(:out) { StringIO.new }
|
|
50
|
+
let(:scheduler) do
|
|
51
|
+
described_class.new(project, adapter: launchd, out: out, err: StringIO.new, now: -> { Time.local(2026, 11, 8, 12) }, host_os: 'darwin25')
|
|
52
|
+
end
|
|
53
|
+
let(:os_id) { RubyRaider::Schedules::Schedule.os_id_for(project.root, 'nightly') }
|
|
54
|
+
let(:plist) { File.join(home, 'Library', 'LaunchAgents', "#{os_id}.plist") }
|
|
55
|
+
|
|
56
|
+
after { FileUtils.rm_rf(tmp) }
|
|
57
|
+
|
|
58
|
+
def states
|
|
59
|
+
out.truncate(0)
|
|
60
|
+
out.rewind
|
|
61
|
+
scheduler.list(json: true)
|
|
62
|
+
JSON.parse(out.string)['schedules'].map { |row| [row['name'], row['os_state']] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'adds, lists an entry schedules.yml lost, and removes it, touching only the temp home' do
|
|
66
|
+
scheduler.add('nightly', every: 'day', at: '08:00')
|
|
67
|
+
expect([File.file?(plist), states]).to eq([true, [%w[nightly ok]]])
|
|
68
|
+
|
|
69
|
+
File.write(File.join(project.root, '.raider/schedules/schedules.yml'), "schedules: []\n")
|
|
70
|
+
expect(states).to eq([['nightly', 'missing from schedules.yml']])
|
|
71
|
+
|
|
72
|
+
scheduler.remove('nightly')
|
|
73
|
+
expect([File.exist?(plist), states, out.string]).to eq([false, [], %({"notice":null,"schedules":[]}\n)])
|
|
74
|
+
expect(runner.calls.flatten.select { |arg| arg.start_with?('/') && !arg.start_with?('/bin/', '/usr/bin/') }).to all(start_with(tmp))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
# rubocop:enable RSpec/MultipleMemoizedHelpers
|