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,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../../../lib/utilities/flows/schedule_store'
|
|
5
|
+
|
|
6
|
+
# Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
|
|
7
|
+
RSpec.describe RubyRaider::Flows::ScheduleStore do # rubocop:disable RSpec/FilePath
|
|
8
|
+
let(:dir) { File.realpath(Dir.mktmpdir('flows-store')) }
|
|
9
|
+
let(:project) { RubyRaider::Flows::Project.new(dir) }
|
|
10
|
+
let(:store) { described_class.new(project) }
|
|
11
|
+
let(:file) { File.join(dir, '.raider/flows/schedules.yml') }
|
|
12
|
+
|
|
13
|
+
def schedule(name, at = '08:00')
|
|
14
|
+
RubyRaider::Flows::Schedule.build(project: project, name: name, every: 'day', at: at)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
after { FileUtils.rm_rf(dir) }
|
|
18
|
+
|
|
19
|
+
it 'is empty without a file' do
|
|
20
|
+
expect([store.all, store.find('report')]).to eq([[], nil])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'saves entries sorted by name and reads them back' do
|
|
24
|
+
%w[zeta alpha mid].each { |name| store.save(schedule(name)) }
|
|
25
|
+
|
|
26
|
+
expect([store.all.map(&:name), YAML.safe_load_file(file)['schedules'].map { |entry| entry['name'] }]).to eq([%w[alpha mid zeta]] * 2)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'replaces an entry by name' do
|
|
30
|
+
store.save(schedule('report'))
|
|
31
|
+
store.save(schedule('report', '09:30'))
|
|
32
|
+
|
|
33
|
+
expect([store.all.size, store.find('report').at]).to eq([1, '09:30'])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'deletes every entry with the name, and returns the name and os_id of one it could not read' do
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
38
|
+
File.write(file, { 'schedules' => [{ 'name' => 'report', 'every' => 'yearly' }, { 'name' => 'report' }, schedule('other').to_h] }.to_yaml)
|
|
39
|
+
removed = store.delete('report')
|
|
40
|
+
|
|
41
|
+
expect([removed.class, removed.name, removed.os_id]).to eq([described_class::Removed, 'report', RubyRaider::Flows::Schedule.os_id_for(dir, 'report')])
|
|
42
|
+
expect(YAML.safe_load_file(file)['schedules'].map { |entry| entry['name'] }).to eq(%w[other])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns the readable schedule when a broken entry shares its name' do
|
|
46
|
+
store.save(schedule('report'))
|
|
47
|
+
entries = YAML.safe_load_file(file)['schedules']
|
|
48
|
+
File.write(file, { 'schedules' => [{ 'name' => 'report' }, *entries] }.to_yaml)
|
|
49
|
+
|
|
50
|
+
expect([store.delete('report').class, store.all]).to eq([RubyRaider::Flows::Schedule, []])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'deletes an entry and returns it, or nil when it is not there' do
|
|
54
|
+
store.save(schedule('report'))
|
|
55
|
+
|
|
56
|
+
expect([store.delete('report').name, store.delete('report'), store.all]).to eq(['report', nil, []])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'skips invalid entries and keeps them when it writes' do
|
|
60
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
61
|
+
File.write(file, { 'schedules' => [{ 'name' => 'broken', 'every' => 'yearly' }, 'junk'] }.to_yaml)
|
|
62
|
+
store.save(schedule('report'))
|
|
63
|
+
|
|
64
|
+
expect([store.all.map(&:name), YAML.safe_load_file(file)['schedules'].size]).to eq([%w[report], 3])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'reads a file that is not YAML as empty but will not overwrite it' do
|
|
68
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
69
|
+
File.write(file, "schedules: [\n")
|
|
70
|
+
|
|
71
|
+
expect(store.all).to eq([])
|
|
72
|
+
expect { store.save(schedule('report')) }.to raise_error(RubyRaider::Flows::Error, /schedules.yml/)
|
|
73
|
+
expect { store.delete('report') }.to raise_error(RubyRaider::Flows::Error, /schedules.yml/)
|
|
74
|
+
expect(File.read(file)).to eq("schedules: [\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'says whether it could write, before anything else is changed' do
|
|
78
|
+
expect(store.writable!).to be_nil
|
|
79
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
80
|
+
File.write(file, "schedules: [\n")
|
|
81
|
+
|
|
82
|
+
expect { store.writable! }
|
|
83
|
+
.to raise_error(RubyRaider::Flows::Error, '.raider/flows/schedules.yml is not a schedules file: fix or remove it, then try again')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'will not overwrite a file it cannot read' do
|
|
87
|
+
store.save(schedule('report'))
|
|
88
|
+
before = File.read(file)
|
|
89
|
+
File.chmod(0o000, file)
|
|
90
|
+
|
|
91
|
+
expect(store.all).to eq([])
|
|
92
|
+
expect { store.save(schedule('other')) }.to raise_error(RubyRaider::Flows::Error, /schedules.yml/)
|
|
93
|
+
File.chmod(0o600, file)
|
|
94
|
+
expect(File.read(file)).to eq(before)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'does not load Ruby objects' do
|
|
98
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
99
|
+
File.write(file, "--- !ruby/object:OpenStruct\ntable: {}\n")
|
|
100
|
+
|
|
101
|
+
expect(store.all).to eq([])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'writes through a temp file and a rename in the same folder' do
|
|
105
|
+
allow(File).to receive(:rename).and_call_original
|
|
106
|
+
store.save(schedule('report'))
|
|
107
|
+
|
|
108
|
+
expect(File).to have_received(:rename).with(%r{\A#{Regexp.escape(File.dirname(file))}/}, file)
|
|
109
|
+
expect(Dir.children(File.dirname(file))).to eq(['schedules.yml'])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'makes the flows folder private' do
|
|
113
|
+
store.save(schedule('report'))
|
|
114
|
+
|
|
115
|
+
expect(File.stat(File.dirname(file)).mode & 0o777).to eq(0o700)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'refuses a linked flows folder' do
|
|
119
|
+
FileUtils.mkdir_p(File.join(dir, 'elsewhere'))
|
|
120
|
+
FileUtils.mkdir_p(File.join(dir, '.raider'))
|
|
121
|
+
File.symlink(File.join(dir, 'elsewhere'), File.join(dir, '.raider/flows'))
|
|
122
|
+
|
|
123
|
+
expect { store.save(schedule('report')) }.to raise_error(RubyRaider::Flows::Error, /not a link/)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'does not read or replace a linked schedules file' do
|
|
127
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
128
|
+
target = File.join(dir, 'other.yml')
|
|
129
|
+
File.write(target, { 'schedules' => [schedule('report').to_h] }.to_yaml)
|
|
130
|
+
before = File.read(target)
|
|
131
|
+
File.symlink(target, file)
|
|
132
|
+
|
|
133
|
+
expect(store.all).to eq([])
|
|
134
|
+
expect { store.save(schedule('other')) }.to raise_error(RubyRaider::Flows::Error, /schedules.yml/)
|
|
135
|
+
expect([File.symlink?(file), File.read(target)]).to eq([true, before])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require_relative '../../../lib/utilities/flows/scheduler'
|
|
7
|
+
|
|
8
|
+
# Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
|
|
9
|
+
# The fake adapter, store and streams sit next to the project and scheduler
|
|
10
|
+
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
11
|
+
RSpec.describe RubyRaider::Flows::Scheduler do # rubocop:disable RSpec/FilePath
|
|
12
|
+
# Stands in for an OS scheduler: records every call, keeps the installed os_ids
|
|
13
|
+
let(:fake_adapter) do
|
|
14
|
+
Class.new do
|
|
15
|
+
attr_reader :calls, :installed
|
|
16
|
+
attr_accessor :fail_present
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@calls = []
|
|
20
|
+
@installed = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def label = 'fakeos'
|
|
24
|
+
|
|
25
|
+
def install(schedule)
|
|
26
|
+
@calls << [:install, schedule.os_id]
|
|
27
|
+
@installed |= [schedule.os_id]
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def remove(os_id)
|
|
32
|
+
@calls << [:remove, os_id]
|
|
33
|
+
!@installed.delete(os_id).nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def present?(os_id)
|
|
37
|
+
raise RubyRaider::Flows::Error, 'task list unreadable' if fail_present
|
|
38
|
+
|
|
39
|
+
@installed.include?(os_id)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
let(:dir) { File.realpath(Dir.mktmpdir('flows-scheduler')) }
|
|
45
|
+
let(:project) { RubyRaider::Flows::Project.new(dir) }
|
|
46
|
+
let(:adapter) { fake_adapter.new }
|
|
47
|
+
let(:store) { RubyRaider::Flows::ScheduleStore.new(project) }
|
|
48
|
+
let(:out) { StringIO.new }
|
|
49
|
+
let(:err) { StringIO.new }
|
|
50
|
+
# Sun 8 Nov 2026 12:00 local
|
|
51
|
+
let(:now) { Time.local(2026, 11, 8, 12, 0) }
|
|
52
|
+
let(:scheduler) { described_class.new(project, adapter: adapter, store: store, out: out, err: err, now: -> { now }) }
|
|
53
|
+
let(:os_id) { RubyRaider::Flows::Schedule.os_id_for(dir, 'report') }
|
|
54
|
+
|
|
55
|
+
before do
|
|
56
|
+
%w[flows/flow_helper.rb flows/support/run.rb flows/report_flow.rb].each do |file|
|
|
57
|
+
FileUtils.mkdir_p(File.dirname(File.join(dir, file)))
|
|
58
|
+
File.write(File.join(dir, file), '')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
after { FileUtils.rm_rf(dir) }
|
|
63
|
+
|
|
64
|
+
def log_run(name, line)
|
|
65
|
+
file = File.join(dir, ".raider/flows/#{name}/runs.jsonl")
|
|
66
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
67
|
+
File.write(file, "#{JSON.generate(line)}\n", mode: 'a')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '.adapter' do
|
|
71
|
+
let(:runner) { instance_double(RubyRaider::Flows::Os::CommandRunner) }
|
|
72
|
+
|
|
73
|
+
def result(success)
|
|
74
|
+
RubyRaider::Flows::Os::CommandRunner::Result.new(stdout: '', stderr: '', exitstatus: success ? 0 : 1)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'picks launchd on macOS and Task Scheduler on Windows without calling them' do
|
|
78
|
+
picked = %w[darwin25 mingw32 mswin64 cygwin].map { |host| described_class.adapter(runner: runner, host_os: host).class }
|
|
79
|
+
|
|
80
|
+
expect(picked).to eq([RubyRaider::Flows::Os::Launchd] + ([RubyRaider::Flows::Os::TaskScheduler] * 3))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'picks the Linux adapter, which labels itself by the scheduler that answers' do
|
|
84
|
+
allow(runner).to receive(:call).and_return(result(true), result(false))
|
|
85
|
+
|
|
86
|
+
picked = 2.times.map { described_class.adapter(runner: runner, host_os: 'linux-gnu') }
|
|
87
|
+
|
|
88
|
+
expect(picked.map { |adapter| [adapter.class, adapter.label] })
|
|
89
|
+
.to eq([[RubyRaider::Flows::Os::Linux, 'systemd'], [RubyRaider::Flows::Os::Linux, 'cron']])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'refuses another OS' do
|
|
93
|
+
expect { described_class.adapter(runner: runner, host_os: 'solaris2') }
|
|
94
|
+
.to raise_error(RubyRaider::Flows::Error, 'Scheduling flows is not supported on solaris2')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe '#schedule' do
|
|
99
|
+
it 'installs the OS entry, then writes the yml, and says when it next runs' do
|
|
100
|
+
allow(store).to receive(:save).and_wrap_original do |original, schedule|
|
|
101
|
+
expect(adapter.installed).to include(schedule.os_id)
|
|
102
|
+
original.call(schedule)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
scheduler.schedule('report', every: 'weekday', at: '08:00')
|
|
106
|
+
|
|
107
|
+
expect(out.string).to eq("Scheduled report: every weekday at 08:00. Next run Mon 9 Nov 08:00.\n")
|
|
108
|
+
expect([adapter.calls, store.find('report').rule_text]).to eq([[[:install, os_id]], 'every weekday at 08:00'])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'stores notify and adds --notify to the OS entry' do
|
|
112
|
+
allow(adapter).to receive(:install).and_wrap_original do |original, schedule|
|
|
113
|
+
expect(schedule.argv).to include('--notify')
|
|
114
|
+
original.call(schedule)
|
|
115
|
+
end
|
|
116
|
+
scheduler.schedule('report', every: 'day', at: '08:00', notify: true)
|
|
117
|
+
|
|
118
|
+
expect([store.find('report').notify, out.string]).to eq([true, "Scheduled report: every day at 08:00 (notify). Next run Mon 9 Nov 08:00.\n"])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'says Replaced when the flow already had a schedule' do
|
|
122
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
123
|
+
scheduler.schedule('report', every: 'week', at: '09:30', on: 'wed')
|
|
124
|
+
|
|
125
|
+
expect(out.string.lines.last).to eq("Replaced the schedule for report: every week on wed at 09:30. Next run Wed 11 Nov 09:30.\n")
|
|
126
|
+
expect(store.all.map(&:rule_text)).to eq(['every week on wed at 09:30'])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'removes the OS entry again when the yml cannot be written' do
|
|
130
|
+
allow(store).to receive(:save).and_raise(RubyRaider::Flows::Error, 'disk full')
|
|
131
|
+
|
|
132
|
+
expect { scheduler.schedule('report', every: 'day', at: '08:00') }
|
|
133
|
+
.to raise_error(RubyRaider::Flows::Error, 'disk full: the fakeos entry was removed too, so report is not scheduled')
|
|
134
|
+
expect([adapter.calls, adapter.installed]).to eq([[[:install, os_id], [:remove, os_id]], []])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'needs the flow file and flows set up, and checks the rule, before touching the OS' do
|
|
138
|
+
expect { scheduler.schedule('nope', every: 'day', at: '08:00') }.to raise_error(RubyRaider::Flows::Error, /No flow nope/)
|
|
139
|
+
expect { scheduler.schedule('report', every: 'day') }.to raise_error(RubyRaider::Flows::Error, /needs at/)
|
|
140
|
+
File.delete(File.join(dir, 'flows/support/run.rb'))
|
|
141
|
+
expect { scheduler.schedule('report', every: 'day', at: '08:00') }
|
|
142
|
+
.to raise_error(RubyRaider::Flows::Error, 'Flows are not set up in this project: run raider flow init')
|
|
143
|
+
expect(adapter.calls).to be_empty
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'checks schedules.yml can be written before installing the OS entry' do
|
|
147
|
+
FileUtils.mkdir_p(File.join(dir, '.raider/flows'))
|
|
148
|
+
File.write(File.join(dir, '.raider/flows/schedules.yml'), "schedules: [\n")
|
|
149
|
+
|
|
150
|
+
expect { scheduler.schedule('report', every: 'day', at: '08:00') }
|
|
151
|
+
.to raise_error(RubyRaider::Flows::Error, /schedules.yml is not a schedules file/)
|
|
152
|
+
expect(adapter.calls).to be_empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'refuses with a Flows::Error when the flow folder cannot be made' do
|
|
156
|
+
File.write(File.join(dir, '.raider'), '')
|
|
157
|
+
|
|
158
|
+
expect { scheduler.schedule('report', every: 'day', at: '08:00') }
|
|
159
|
+
.to raise_error(RubyRaider::Flows::Error, %r{\ACould not make \.raider/flows: })
|
|
160
|
+
expect(adapter.calls).to be_empty
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'makes .raider/flows/<name> a 0700 folder and refuses a link there' do
|
|
164
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
165
|
+
expect(File.stat(File.join(dir, '.raider/flows/report')).mode & 0o777).to eq(0o700)
|
|
166
|
+
|
|
167
|
+
FileUtils.rm_rf(File.join(dir, '.raider/flows/report'))
|
|
168
|
+
File.symlink(FileUtils.mkdir_p(File.join(dir, 'elsewhere')).first, File.join(dir, '.raider/flows/report'))
|
|
169
|
+
expect { scheduler.schedule('report', every: 'day', at: '08:00') }
|
|
170
|
+
.to raise_error(RubyRaider::Flows::Error, '.raider/flows/report must be a folder, not a link')
|
|
171
|
+
expect(adapter.calls).to eq([[:install, os_id]])
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe '#unschedule' do
|
|
176
|
+
it 'keeps the yml entry when the OS scheduler refuses the removal' do
|
|
177
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
178
|
+
allow(adapter).to receive(:remove).and_raise(RubyRaider::Flows::Error, 'systemd units found')
|
|
179
|
+
|
|
180
|
+
expect { scheduler.unschedule('report') }.to raise_error(RubyRaider::Flows::Error, 'systemd units found')
|
|
181
|
+
expect(store.find('report')).not_to be_nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'removes the yml entry and the OS entry' do
|
|
185
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
186
|
+
scheduler.unschedule('report')
|
|
187
|
+
|
|
188
|
+
expect([out.string.lines.last, store.all, adapter.installed]).to eq(["Unscheduled report\n", [], []])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'says when the OS entry was already gone' do
|
|
192
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
193
|
+
adapter.installed.clear
|
|
194
|
+
scheduler.unschedule('report')
|
|
195
|
+
|
|
196
|
+
expect([out.string.lines.last, store.all]).to eq(["Unscheduled report (it was already gone from fakeos)\n", []])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'removes an OS entry with no yml entry, even for a deleted flow' do
|
|
200
|
+
adapter.installed << RubyRaider::Flows::Schedule.os_id_for(dir, 'gone')
|
|
201
|
+
scheduler.unschedule('gone')
|
|
202
|
+
|
|
203
|
+
expect([out.string, adapter.installed]).to eq(["Unscheduled gone\n", []])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'refuses a flow with no schedule anywhere, and an invalid name' do
|
|
207
|
+
expect { scheduler.unschedule('report') }.to raise_error(RubyRaider::Flows::Error, 'No schedule for report')
|
|
208
|
+
expect { scheduler.unschedule('Bad-Name') }.to raise_error(RubyRaider::Flows::Error, /Invalid flow name/)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'says the OS entry was already removed when schedules.yml can not be written' do
|
|
212
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
213
|
+
allow(store).to receive(:delete).and_raise(Errno::EACCES, 'schedules.yml')
|
|
214
|
+
|
|
215
|
+
expect { scheduler.unschedule('report') }.to raise_error(RubyRaider::Flows::Error) do |error|
|
|
216
|
+
expect([error.message, error.exit_code]).to eq(
|
|
217
|
+
['Permission denied - schedules.yml: the fakeos entry was already removed, but schedules.yml still lists report', 2]
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
expect(adapter.installed).to eq([])
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'warns that an OS entry from another project path may remain when the yml entry was unreadable' do
|
|
224
|
+
FileUtils.mkdir_p(File.join(dir, '.raider/flows'))
|
|
225
|
+
File.write(File.join(dir, '.raider/flows/schedules.yml'), { 'schedules' => [{ 'name' => 'report' }] }.to_yaml)
|
|
226
|
+
scheduler.unschedule('report')
|
|
227
|
+
|
|
228
|
+
expect(out.string).to eq("Unscheduled report (it was already gone from fakeos)\n")
|
|
229
|
+
expect(err.string).to include('could not be read', 'fakeos entry made from a different project path may remain')
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
describe '#schedules' do
|
|
234
|
+
it 'lists each schedule with its rule, next run, last result and OS state' do
|
|
235
|
+
scheduler.schedule('report', every: 'hour', at: ':15', retries: 2, inputs: ['who=me'], notify: true)
|
|
236
|
+
log_run('report', 'status' => 'failed', 'finished_at' => "2026-11-09T08:00:12Z\e[2J", 'trigger' => 'schedule',
|
|
237
|
+
'failed_step' => 'Open', 'results_dir' => 'flow-results/report/x', 'run_id' => 'abc')
|
|
238
|
+
out.truncate(0)
|
|
239
|
+
out.rewind
|
|
240
|
+
scheduler.schedules
|
|
241
|
+
|
|
242
|
+
expect(out.string).to eq("#{'report'.ljust(30)} every hour at :15 (notify) next Sun 8 Nov 12:15 failed 2026-11-09T08:00:12Z?[2J ok\n")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'prints the JSON the desktop app reads' do
|
|
246
|
+
scheduler.schedule('report', every: 'week', at: '08:00', on: 'mon', inputs: ['who=me'], notify: true)
|
|
247
|
+
log_run('report', 'status' => 'completed', 'finished_at' => 'f', 'trigger' => 't', 'failed_step' => nil, 'results_dir' => 'r', 'run_id' => 'x')
|
|
248
|
+
out.truncate(0)
|
|
249
|
+
out.rewind
|
|
250
|
+
scheduler.schedules(json: true)
|
|
251
|
+
|
|
252
|
+
expect(JSON.parse(out.string)).to eq(
|
|
253
|
+
[{ 'name' => 'report', 'every' => 'week', 'at' => '08:00', 'on' => 'mon', 'retries' => 0, 'inputs' => ['who=me'], 'notify' => true,
|
|
254
|
+
'next_run_at' => Time.local(2026, 11, 9, 8, 0).iso8601, 'os_state' => 'ok',
|
|
255
|
+
'last' => { 'status' => 'completed', 'finished_at' => 'f', 'trigger' => 't', 'failed_step' => nil, 'results_dir' => 'r' } }]
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'says Ruby not found when the stored ruby is gone, and missing when the OS entry is gone' do
|
|
260
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
261
|
+
FileUtils.touch(File.join(dir, 'flows/other_flow.rb'))
|
|
262
|
+
fields = store.find('report').to_h.transform_keys(&:to_sym)
|
|
263
|
+
other = { name: 'other', os_id: RubyRaider::Flows::Schedule.os_id_for(dir, 'other'), ruby: '/nonexistent/ruby' }
|
|
264
|
+
store.save(RubyRaider::Flows::Schedule.new(**fields, **other, project_root: dir))
|
|
265
|
+
adapter.installed.clear
|
|
266
|
+
out.truncate(0)
|
|
267
|
+
out.rewind
|
|
268
|
+
scheduler.schedules(json: true)
|
|
269
|
+
|
|
270
|
+
expect(JSON.parse(out.string).map { |row| [row['name'], row['os_state'], row['last']] })
|
|
271
|
+
.to eq([['other', 'Ruby not found: re-schedule', nil], ['report', 'missing from fakeos', nil]])
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'says flow not found when the flow file is gone, after Ruby not found and before the OS state' do
|
|
275
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
276
|
+
FileUtils.touch(File.join(dir, 'flows/other_flow.rb'))
|
|
277
|
+
fields = store.find('report').to_h.transform_keys(&:to_sym)
|
|
278
|
+
other = { name: 'other', os_id: RubyRaider::Flows::Schedule.os_id_for(dir, 'other'), ruby: '/nonexistent/ruby' }
|
|
279
|
+
store.save(RubyRaider::Flows::Schedule.new(**fields, **other, project_root: dir))
|
|
280
|
+
%w[report other].each { |name| File.delete(File.join(dir, "flows/#{name}_flow.rb")) }
|
|
281
|
+
adapter.fail_present = true
|
|
282
|
+
scheduler.schedules(json: true)
|
|
283
|
+
|
|
284
|
+
expect(JSON.parse(out.string.lines.last).map { |row| [row['name'], row['os_state']] })
|
|
285
|
+
.to eq([['other', 'Ruby not found: re-schedule'], ['report', 'flow not found: unschedule']])
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'adds a row for a flow the OS still schedules with no yml entry' do
|
|
289
|
+
adapter.installed << os_id
|
|
290
|
+
scheduler.schedules(json: true)
|
|
291
|
+
|
|
292
|
+
expect(JSON.parse(out.string)).to eq(
|
|
293
|
+
[{ 'name' => 'report', 'every' => nil, 'at' => nil, 'on' => nil, 'retries' => nil, 'inputs' => nil, 'notify' => nil,
|
|
294
|
+
'next_run_at' => nil, 'os_state' => 'missing from schedules.yml', 'last' => nil }]
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it 'shows unknown when the OS state cannot be read' do
|
|
299
|
+
scheduler.schedule('report', every: 'day', at: '08:00')
|
|
300
|
+
adapter.fail_present = true
|
|
301
|
+
out.truncate(0)
|
|
302
|
+
out.rewind
|
|
303
|
+
scheduler.schedules
|
|
304
|
+
|
|
305
|
+
expect(out.string).to end_with(" never run unknown (task list unreadable)\n")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'prints the missed runs note of an adapter that has one, once' do
|
|
309
|
+
adapter.define_singleton_method(:missed_runs_note) { '(fakeos: missed runs are skipped)' }
|
|
310
|
+
%w[report other].each do |name|
|
|
311
|
+
FileUtils.touch(File.join(dir, "flows/#{name}_flow.rb"))
|
|
312
|
+
scheduler.schedule(name, every: 'day', at: '08:00')
|
|
313
|
+
end
|
|
314
|
+
out.truncate(0)
|
|
315
|
+
out.rewind
|
|
316
|
+
scheduler.schedules
|
|
317
|
+
|
|
318
|
+
expect(out.string.lines.size).to eq(3)
|
|
319
|
+
expect(out.string.lines.last).to eq("(fakeos: missed runs are skipped)\n")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it 'says how to add one when there are none' do
|
|
323
|
+
scheduler.schedules
|
|
324
|
+
|
|
325
|
+
expect(out.string).to eq("No schedules yet: raider flow schedule <name> --every day --at 08:00\n")
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
# rubocop:enable RSpec/MultipleMemoizedHelpers
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_raider
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Agustin Pequeno
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler-audit
|
|
@@ -260,6 +260,7 @@ files:
|
|
|
260
260
|
- assets/ruby_raider_logo.svg
|
|
261
261
|
- bin/raider
|
|
262
262
|
- bin/release
|
|
263
|
+
- lib/commands/flow_commands.rb
|
|
263
264
|
- lib/commands/scaffolding_commands.rb
|
|
264
265
|
- lib/commands/utility_commands.rb
|
|
265
266
|
- lib/generators/automation/automation_generator.rb
|
|
@@ -305,6 +306,8 @@ files:
|
|
|
305
306
|
- lib/generators/templates/common/reek.tt
|
|
306
307
|
- lib/generators/templates/common/rspec.tt
|
|
307
308
|
- lib/generators/templates/common/rubocop.tt
|
|
309
|
+
- lib/generators/templates/flows/flow_helper.tt
|
|
310
|
+
- lib/generators/templates/flows/run.tt
|
|
308
311
|
- lib/generators/templates/helpers/appium_helper.tt
|
|
309
312
|
- lib/generators/templates/helpers/driver_helper.tt
|
|
310
313
|
- lib/generators/templates/helpers/gesture_helper.tt
|
|
@@ -352,12 +355,27 @@ files:
|
|
|
352
355
|
- lib/scaffolding/templates/page_object.tt
|
|
353
356
|
- lib/scaffolding/templates/spec.tt
|
|
354
357
|
- lib/utilities/desktop_downloader.rb
|
|
358
|
+
- lib/utilities/flows/next_run.rb
|
|
359
|
+
- lib/utilities/flows/notifier.rb
|
|
360
|
+
- lib/utilities/flows/os/command_runner.rb
|
|
361
|
+
- lib/utilities/flows/os/cron.rb
|
|
362
|
+
- lib/utilities/flows/os/launchd.rb
|
|
363
|
+
- lib/utilities/flows/os/linux.rb
|
|
364
|
+
- lib/utilities/flows/os/systemd.rb
|
|
365
|
+
- lib/utilities/flows/os/task_scheduler.rb
|
|
366
|
+
- lib/utilities/flows/project.rb
|
|
367
|
+
- lib/utilities/flows/run_log.rb
|
|
368
|
+
- lib/utilities/flows/runner.rb
|
|
369
|
+
- lib/utilities/flows/schedule.rb
|
|
370
|
+
- lib/utilities/flows/schedule_store.rb
|
|
371
|
+
- lib/utilities/flows/scheduler.rb
|
|
355
372
|
- lib/utilities/logger.rb
|
|
356
373
|
- lib/utilities/logo.rb
|
|
357
374
|
- lib/utilities/utilities.rb
|
|
358
375
|
- lib/version
|
|
359
376
|
- plugins.yml
|
|
360
377
|
- ruby_raider.gemspec
|
|
378
|
+
- sig/commands/flow_commands.rbs
|
|
361
379
|
- sig/commands/scaffolding_commands.rbs
|
|
362
380
|
- sig/commands/utility_commands.rbs
|
|
363
381
|
- sig/generators/automation/automation_generator.rbs
|
|
@@ -398,18 +416,34 @@ files:
|
|
|
398
416
|
- sig/scaffolding/project_detector.rbs
|
|
399
417
|
- sig/scaffolding/scaffolding.rbs
|
|
400
418
|
- sig/utilities/desktop_downloader.rbs
|
|
419
|
+
- sig/utilities/flows/next_run.rbs
|
|
420
|
+
- sig/utilities/flows/notifier.rbs
|
|
421
|
+
- sig/utilities/flows/os/command_runner.rbs
|
|
422
|
+
- sig/utilities/flows/os/cron.rbs
|
|
423
|
+
- sig/utilities/flows/os/launchd.rbs
|
|
424
|
+
- sig/utilities/flows/os/linux.rbs
|
|
425
|
+
- sig/utilities/flows/os/systemd.rbs
|
|
426
|
+
- sig/utilities/flows/os/task_scheduler.rbs
|
|
427
|
+
- sig/utilities/flows/project.rbs
|
|
428
|
+
- sig/utilities/flows/run_log.rbs
|
|
429
|
+
- sig/utilities/flows/runner.rbs
|
|
430
|
+
- sig/utilities/flows/schedule.rbs
|
|
431
|
+
- sig/utilities/flows/schedule_store.rbs
|
|
432
|
+
- sig/utilities/flows/scheduler.rbs
|
|
401
433
|
- sig/utilities/logger.rbs
|
|
402
434
|
- sig/utilities/logo.rbs
|
|
403
435
|
- sig/utilities/utilities.rbs
|
|
404
436
|
- sig/vendor/selenium_webdriver.rbs
|
|
405
437
|
- sig/vendor/thor.rbs
|
|
406
438
|
- sig/vendor/tty_prompt.rbs
|
|
439
|
+
- spec/commands/flow_commands_spec.rb
|
|
407
440
|
- spec/commands/raider_commands_spec.rb
|
|
408
441
|
- spec/commands/scaffolding_commands_spec.rb
|
|
409
442
|
- spec/commands/utility_commands_spec.rb
|
|
410
443
|
- spec/generators/fixtures/templates/test.tt
|
|
411
444
|
- spec/generators/fixtures/templates/test_partial.tt
|
|
412
445
|
- spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb
|
|
446
|
+
- spec/generators/flow_helper_spec.rb
|
|
413
447
|
- spec/generators/gesture_helper_spec.rb
|
|
414
448
|
- spec/generators/network_helper_spec.rb
|
|
415
449
|
- spec/generators/screenshot_helper_spec.rb
|
|
@@ -434,6 +468,9 @@ files:
|
|
|
434
468
|
- spec/integration/content/test_content_spec.rb
|
|
435
469
|
- spec/integration/end_to_end_features_spec.rb
|
|
436
470
|
- spec/integration/end_to_end_spec.rb
|
|
471
|
+
- spec/integration/flows/flow_project.rb
|
|
472
|
+
- spec/integration/flows/run_command_spec.rb
|
|
473
|
+
- spec/integration/flows/run_lifecycle_spec.rb
|
|
437
474
|
- spec/integration/frozen_string_literal_matcher.rb
|
|
438
475
|
- spec/integration/generators/automation_generator_spec.rb
|
|
439
476
|
- spec/integration/generators/axe_addon_spec.rb
|
|
@@ -489,6 +526,20 @@ files:
|
|
|
489
526
|
- spec/system/support/system_test_helper.rb
|
|
490
527
|
- spec/utilities/desktop_downloader_spec.rb
|
|
491
528
|
- spec/utilities/ffmpeg_spec.rb
|
|
529
|
+
- spec/utilities/flows/next_run_spec.rb
|
|
530
|
+
- spec/utilities/flows/notifier_spec.rb
|
|
531
|
+
- spec/utilities/flows/os/command_runner_spec.rb
|
|
532
|
+
- spec/utilities/flows/os/cron_spec.rb
|
|
533
|
+
- spec/utilities/flows/os/launchd_spec.rb
|
|
534
|
+
- spec/utilities/flows/os/linux_spec.rb
|
|
535
|
+
- spec/utilities/flows/os/systemd_spec.rb
|
|
536
|
+
- spec/utilities/flows/os/task_scheduler_spec.rb
|
|
537
|
+
- spec/utilities/flows/project_spec.rb
|
|
538
|
+
- spec/utilities/flows/run_log_spec.rb
|
|
539
|
+
- spec/utilities/flows/runner_spec.rb
|
|
540
|
+
- spec/utilities/flows/schedule_spec.rb
|
|
541
|
+
- spec/utilities/flows/schedule_store_spec.rb
|
|
542
|
+
- spec/utilities/flows/scheduler_spec.rb
|
|
492
543
|
- spec/utilities/headless_config_spec.rb
|
|
493
544
|
- spec/utilities/trace_command_spec.rb
|
|
494
545
|
- spec/utilities/utilities_spec.rb
|