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,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../../../../lib/utilities/schedules/adapter'
|
|
5
|
+
require_relative '../../../../lib/utilities/schedules/schedule'
|
|
6
|
+
|
|
7
|
+
# Every adapter's list_ids(prefix): the os_ids of one project's schedules of the adapter's Kind, found by
|
|
8
|
+
# enumerating the OS scheduler. Only a fake runner and a temp home: nothing real is read or run.
|
|
9
|
+
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
10
|
+
RSpec.describe 'Schedules adapters #list_ids' do
|
|
11
|
+
# Answers each command from a table keyed by its first words; never runs anything
|
|
12
|
+
let(:fake_runner_class) do
|
|
13
|
+
Class.new do
|
|
14
|
+
attr_reader :calls, :answers
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@calls = []
|
|
18
|
+
@answers = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(*argv, stdin_data: nil)
|
|
22
|
+
@calls << [argv, stdin_data]
|
|
23
|
+
key = if argv.first.to_s.end_with?('schtasks.exe') then argv[1]
|
|
24
|
+
elsif argv.first == 'systemctl' then argv.first(3).join(' ')
|
|
25
|
+
else argv.first(2).join(' ')
|
|
26
|
+
end
|
|
27
|
+
stdout, status, stderr = answers.fetch(key, ['', 0, ''])
|
|
28
|
+
RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: stdout, stderr: stderr.to_s, exitstatus: status)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
let(:kind) { RubyRaider::Schedules::Kind::TESTS }
|
|
34
|
+
let(:tmp) { File.realpath(Dir.mktmpdir('schedules-list-ids')) }
|
|
35
|
+
let(:home) { File.join(tmp, 'home') }
|
|
36
|
+
let(:runner) { fake_runner_class.new }
|
|
37
|
+
let(:root) { File.join(tmp, 'project') }
|
|
38
|
+
let(:prefix) { RubyRaider::Schedules::Schedule.os_id_prefix_for(root) }
|
|
39
|
+
let(:other_prefix) { RubyRaider::Schedules::Schedule.os_id_prefix_for(File.join(tmp, 'other')) }
|
|
40
|
+
let(:flow_prefix) { "#{RubyRaider::Schedules::Kind::FLOW_LEGACY.os_id_prefix}.#{prefix.split('.')[3]}." }
|
|
41
|
+
let(:bad_utf8) { "#{prefix}a\xFF".dup.force_encoding(Encoding::UTF_8) }
|
|
42
|
+
# What enumeration may see besides our two ids: another project, a flow schedule, a bad name, a longer
|
|
43
|
+
# name, path tricks and bytes that are not UTF-8
|
|
44
|
+
let(:noise) do
|
|
45
|
+
["#{other_prefix}nightly", "#{flow_prefix}nightly", "#{prefix}Bad", "#{prefix}#{'a' * 61}", "#{prefix}a.b", "#{prefix}..",
|
|
46
|
+
"#{prefix}a b", prefix, "x#{prefix}nightly", bad_utf8]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
let(:extra_children) { Hash.new { |hash, key| hash[key] = [] } }
|
|
50
|
+
|
|
51
|
+
after { FileUtils.rm_rf(tmp) }
|
|
52
|
+
|
|
53
|
+
# APFS refuses a file name that is not UTF-8, so those names are fed through Dir.children instead
|
|
54
|
+
def touch(dir, *names)
|
|
55
|
+
FileUtils.mkdir_p(dir)
|
|
56
|
+
names.each { |name| name.valid_encoding? ? File.write(File.join(dir, name), '') : extra_children[dir] << name }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
before do
|
|
60
|
+
allow(Dir).to(receive(:children).and_wrap_original { |original, dir| original.call(dir) + extra_children[dir] })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'builds the prefix from the kind and the project path hash, ending in a dot' do
|
|
64
|
+
expect(prefix).to match(/\Adev\.rubyraider\.schedule\.[0-9a-f]{10}\.\z/)
|
|
65
|
+
expect(RubyRaider::Schedules::Schedule.os_id_for(root, 'nightly')).to eq("#{prefix}nightly")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe RubyRaider::Schedules::Os::Launchd do
|
|
69
|
+
let(:adapter) { described_class.new(kind: kind, runner: runner, home: home, uid: 501) }
|
|
70
|
+
let(:agents) { File.join(home, 'Library', 'LaunchAgents') }
|
|
71
|
+
|
|
72
|
+
it 'lists the plist basenames of this project and kind, sorted, and runs nothing' do
|
|
73
|
+
touch(agents, "#{prefix}smoke.plist", "#{prefix}nightly.plist", "#{prefix}nightly.txt", ".#{prefix}x.plist.ab12.tmp",
|
|
74
|
+
*noise.map { |id| "#{id}.plist" })
|
|
75
|
+
|
|
76
|
+
expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
|
|
77
|
+
expect([runner.calls, extra_children[agents]]).to eq([[], ["#{bad_utf8}.plist"]])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'is empty when there is no LaunchAgents folder' do
|
|
81
|
+
expect(adapter.list_ids(prefix)).to eq([])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'raises an Error when the folder can not be read' do
|
|
85
|
+
FileUtils.mkdir_p(File.join(home, 'Library'))
|
|
86
|
+
File.write(agents, '')
|
|
87
|
+
|
|
88
|
+
expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, /Could not read .*LaunchAgents/)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe RubyRaider::Schedules::Os::TaskScheduler do
|
|
93
|
+
let(:adapter) { described_class.new(kind: kind, runner: runner, schtasks: 'C:\\Windows\\System32\\schtasks.exe') }
|
|
94
|
+
|
|
95
|
+
def rows(*names) = names.map { |name| %("#{name}","11/10/2026 8:00:00 AM","Ready"\r\n) }.join
|
|
96
|
+
|
|
97
|
+
it 'lists the tasks under \\RubyRaider\\ of this project and kind from one query' do
|
|
98
|
+
listing = rows("\\RubyRaider\\#{prefix}smoke", "\\RubyRaider\\#{prefix}nightly", "\\Other\\#{prefix}elsewhere",
|
|
99
|
+
"\\RubyRaider\\sub\\#{prefix}deeper", "\\#{prefix}rootlevel", "\\RubyRaider\\#{prefix}smoke",
|
|
100
|
+
*noise.map { |id| "\\RubyRaider\\#{id}" })
|
|
101
|
+
listing = "#{listing}garbage line\r\n\"unterminated\r\n"
|
|
102
|
+
runner.answers['/Query'] = [listing.b, 0]
|
|
103
|
+
|
|
104
|
+
expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
|
|
105
|
+
expect(runner.calls.map(&:first)).to eq([['C:\\Windows\\System32\\schtasks.exe', '/Query', '/FO', 'CSV', '/NH']])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'raises an Error when the task list can not be read' do
|
|
109
|
+
runner.answers['/Query'] = ['', 1, "ERROR: \e[2Jdenied"]
|
|
110
|
+
|
|
111
|
+
expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, 'Task Scheduler could not list tasks: ERROR: ?[2Jdenied')
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe RubyRaider::Schedules::Os::Systemd do
|
|
116
|
+
let(:adapter) { described_class.new(kind: kind, runner: runner, home: home, config_home: nil) }
|
|
117
|
+
let(:units) { File.join(home, '.config', 'systemd', 'user') }
|
|
118
|
+
|
|
119
|
+
it 'lists the unit file names of this project and kind, without asking systemctl' do
|
|
120
|
+
touch(units, "#{prefix}smoke.timer", "#{prefix}smoke.service", "#{prefix}nightly.service", "#{prefix}other.socket",
|
|
121
|
+
".#{prefix}x.timer.ab12.tmp", *noise.map { |id| "#{id}.timer" })
|
|
122
|
+
|
|
123
|
+
expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
|
|
124
|
+
expect(runner.calls).to be_empty
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'reads the units folder under XDG_CONFIG_HOME when that is absolute' do
|
|
128
|
+
config = File.join(tmp, 'xdg')
|
|
129
|
+
touch(File.join(config, 'systemd', 'user'), "#{prefix}smoke.timer")
|
|
130
|
+
|
|
131
|
+
expect(described_class.new(kind: kind, runner: runner, home: home, config_home: config).list_ids(prefix)).to eq(["#{prefix}smoke"])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'is empty when there is no units folder' do
|
|
135
|
+
expect(adapter.list_ids(prefix)).to eq([])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe RubyRaider::Schedules::Os::Cron do
|
|
140
|
+
let(:adapter) { described_class.new(kind: kind, runner: runner) }
|
|
141
|
+
|
|
142
|
+
it 'lists the ids on our tag lines only, keeping the crontab as it is' do
|
|
143
|
+
table = "MAILTO=x\n# raider-schedule #{prefix}smoke\n0 8 * * * cd /p && run\n# raider-schedule #{prefix}nightly\r\n" \
|
|
144
|
+
"# raider-schedule #{prefix}spaced\n# raider-schedule #{prefix}trailing \n" \
|
|
145
|
+
"0 1 * * * echo '# raider-schedule #{prefix}inline'\n" \
|
|
146
|
+
"#{noise.map { |id| "# raider-schedule #{id}\n" }.join}# raider-flow #{flow_prefix}legacy\n"
|
|
147
|
+
runner.answers['crontab -l'] = [table.b, 0]
|
|
148
|
+
|
|
149
|
+
expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
|
|
150
|
+
expect(runner.calls.map(&:first)).to eq([%w[crontab -l]])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'is empty with no crontab, and raises an Error when crontab -l fails otherwise' do
|
|
154
|
+
runner.answers['crontab -l'] = ['', 1, 'no crontab for raider']
|
|
155
|
+
expect(adapter.list_ids(prefix)).to eq([])
|
|
156
|
+
|
|
157
|
+
runner.answers['crontab -l'] = ['', 1, 'permission denied']
|
|
158
|
+
expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, /could not read the crontab/)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'raises an Error, not an encoding error, when crontab -l fails with bytes that are not UTF-8' do
|
|
162
|
+
runner.answers['crontab -l'] = ['', 1, "refus\xE9\e[2J".dup.force_encoding(Encoding::UTF_8)]
|
|
163
|
+
|
|
164
|
+
expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, 'cron could not read the crontab: refus??[2J')
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe RubyRaider::Schedules::Os::Linux do
|
|
169
|
+
let(:systemd) { RubyRaider::Schedules::Os::Systemd.new(kind: kind, runner: runner, home: home, config_home: nil) }
|
|
170
|
+
let(:cron) { RubyRaider::Schedules::Os::Cron.new(kind: kind, runner: runner) }
|
|
171
|
+
let(:units) { File.join(home, '.config', 'systemd', 'user') }
|
|
172
|
+
|
|
173
|
+
before do
|
|
174
|
+
touch(units, "#{prefix}timer_one.timer")
|
|
175
|
+
runner.answers['crontab -l'] = ["# raider-schedule #{prefix}cron_one\n", 0]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'with systemd answering: the unit files plus the cron tags' do
|
|
179
|
+
adapter = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
|
|
180
|
+
|
|
181
|
+
expect(adapter.list_ids(prefix)).to eq(["#{prefix}cron_one", "#{prefix}timer_one"])
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'with no user bus: the cron tags plus the stranded unit files' do
|
|
185
|
+
runner.answers['systemctl --user show-environment'] = ['', 1, 'Failed to connect to bus']
|
|
186
|
+
adapter = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
|
|
187
|
+
|
|
188
|
+
expect(adapter.list_ids(prefix)).to eq(["#{prefix}cron_one", "#{prefix}timer_one"])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'leaves out the other scheduler when it fails, and raises when the primary one does' do
|
|
192
|
+
runner.answers['crontab -l'] = ['', 1, 'permission denied']
|
|
193
|
+
with_systemd = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
|
|
194
|
+
expect(with_systemd.list_ids(prefix)).to eq(["#{prefix}timer_one"])
|
|
195
|
+
|
|
196
|
+
runner.answers['systemctl --user show-environment'] = ['', 1, 'Failed to connect to bus']
|
|
197
|
+
with_cron = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
|
|
198
|
+
expect { with_cron.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, /could not read the crontab/)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
# rubocop:enable RSpec/MultipleMemoizedHelpers
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tmpdir'
|
|
4
|
-
require_relative '../../../../lib/utilities/
|
|
4
|
+
require_relative '../../../../lib/utilities/schedules/os/systemd'
|
|
5
|
+
require_relative '../../../../lib/utilities/schedules/schedule'
|
|
5
6
|
|
|
6
|
-
# Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
|
|
7
7
|
# The fake runner, temp home and project folders sit next to the adapter under test
|
|
8
8
|
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
9
|
-
RSpec.describe RubyRaider::
|
|
9
|
+
RSpec.describe RubyRaider::Schedules::Os::Systemd do # rubocop:disable RSpec/FilePath
|
|
10
10
|
# Records every command and answers from a table keyed by the systemctl verb; never runs anything
|
|
11
11
|
let(:fake_runner_class) do
|
|
12
12
|
Class.new do
|
|
@@ -21,7 +21,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
21
21
|
@calls << argv
|
|
22
22
|
@seen&.call(argv, stdin_data)
|
|
23
23
|
stderr, status = answers.fetch(argv[2], ['', 0])
|
|
24
|
-
RubyRaider::
|
|
24
|
+
RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: '', stderr: stderr, exitstatus: status)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def on_call(&block) = @seen = block
|
|
@@ -29,18 +29,20 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
let(:hostile) { %q('; rm -rf ~; ' </string> %h $(id) " \\ &) }
|
|
32
|
-
let(:tmp) { File.realpath(Dir.mktmpdir('
|
|
32
|
+
let(:tmp) { File.realpath(Dir.mktmpdir('schedules-systemd')) }
|
|
33
33
|
let(:home) { File.join(tmp, 'home') }
|
|
34
34
|
let(:project_dir) { File.join(tmp, 'plain') }
|
|
35
35
|
let(:runner) { fake_runner_class.new }
|
|
36
|
-
let(:systemd) { described_class.new(runner: runner, home: home, config_home: nil) }
|
|
36
|
+
let(:systemd) { described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, config_home: nil) }
|
|
37
37
|
let(:now) { Time.utc(2026, 11, 9, 10, 0, 0) }
|
|
38
38
|
let(:units) { File.join(home, '.config', 'systemd', 'user') }
|
|
39
39
|
|
|
40
|
-
def schedule(dir: project_dir, **options)
|
|
40
|
+
def schedule(dir: project_dir, gem_env: {}, bundle: '/opt/ruby/bin/bundle', **options)
|
|
41
41
|
FileUtils.mkdir_p(dir)
|
|
42
|
-
|
|
43
|
-
RubyRaider::
|
|
42
|
+
# Hermetic: the GEM_HOME/GEM_PATH and bundle this spec runs with never reach the entry; a spec passes its own
|
|
43
|
+
allow(RubyRaider::Schedules::Schedule).to receive_messages(captured_gem_env: gem_env, bundle_path: bundle)
|
|
44
|
+
project = RubyRaider::Schedules::Project.new(File.realpath(dir))
|
|
45
|
+
RubyRaider::Schedules::Schedule.build(project: project, name: 'report', every: 'day', at: '08:00', now: now, **options)
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# systemd's specifier rule for a raw value: %% is %, any other % would be a specifier
|
|
@@ -110,7 +112,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
110
112
|
|
|
111
113
|
expect(systemd.service(item)).to eq(<<~UNIT)
|
|
112
114
|
[Unit]
|
|
113
|
-
Description=raider
|
|
115
|
+
Description=raider schedule report
|
|
114
116
|
|
|
115
117
|
[Service]
|
|
116
118
|
Type=oneshot
|
|
@@ -123,22 +125,24 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
it 'keeps GEM_HOME and GEM_PATH in the environment when they were captured' do
|
|
126
|
-
|
|
128
|
+
item = schedule(gem_env: { 'GEM_HOME' => '/g/%h$x', 'GEM_PATH' => '/g/path' })
|
|
127
129
|
|
|
128
|
-
lines = value_of(systemd.service(
|
|
130
|
+
lines = value_of(systemd.service(item), 'Environment').flat_map { |value| systemd_words(value, dollars: false) }
|
|
129
131
|
|
|
130
132
|
expect(lines.last(3)).to eq(['LANG=en_US.UTF-8', 'GEM_HOME=/g/%h$x', 'GEM_PATH=/g/path'])
|
|
131
133
|
end
|
|
132
134
|
|
|
133
|
-
describe 'with hostile values in
|
|
134
|
-
let(:item)
|
|
135
|
+
describe 'with hostile values in the bundle path, GEM_HOME and the project path' do
|
|
136
|
+
let(:item) do
|
|
137
|
+
schedule(dir: File.join(tmp, "p#{hostile}<", "string> #{hostile}"), bundle: "/opt/#{hostile}/bundle", gem_env: { 'GEM_HOME' => "/g/#{hostile}" })
|
|
138
|
+
end
|
|
135
139
|
let(:text) { systemd.service(item) }
|
|
136
140
|
|
|
137
141
|
it 'gives back the exact argv from ExecStart' do
|
|
138
142
|
argv = systemd_words(value_of(text, 'ExecStart').first, dollars: true)
|
|
139
143
|
|
|
140
144
|
expect(argv).to eq(item.argv)
|
|
141
|
-
expect(argv
|
|
145
|
+
expect(argv[1]).to eq("/opt/#{hostile}/bundle")
|
|
142
146
|
expect(item.working_directory).to include('</string>', "'; rm -rf ~; '", '$(id)', '%h', '"', '\\', '&', ' ')
|
|
143
147
|
end
|
|
144
148
|
|
|
@@ -153,7 +157,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
153
157
|
it 'writes them only in escaped form' do
|
|
154
158
|
expect(text).to include('%%h', '$$(id)', '\\" \\\\ &')
|
|
155
159
|
expect(text.scan(/(?<!%)%(?!%)/)).to be_empty
|
|
156
|
-
expect(text.lines.length).to eq(systemd.service(schedule).lines.length)
|
|
160
|
+
expect(text.lines.length).to eq(systemd.service(schedule(gem_env: { 'GEM_HOME' => '/g' })).lines.length)
|
|
157
161
|
end
|
|
158
162
|
end
|
|
159
163
|
end
|
|
@@ -162,7 +166,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
162
166
|
it 'writes the exact timer for a daily schedule' do
|
|
163
167
|
expect(systemd.timer(schedule)).to eq(<<~UNIT)
|
|
164
168
|
[Unit]
|
|
165
|
-
Description=raider
|
|
169
|
+
Description=raider schedule report schedule
|
|
166
170
|
|
|
167
171
|
[Timer]
|
|
168
172
|
OnCalendar=*-*-* 08:00:00
|
|
@@ -196,8 +200,8 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
196
200
|
it 'uses an absolute XDG_CONFIG_HOME and ignores a relative one' do
|
|
197
201
|
item = schedule
|
|
198
202
|
config = File.join(tmp, 'xdg')
|
|
199
|
-
described_class.new(runner: runner, home: home, config_home: config).install(item)
|
|
200
|
-
described_class.new(runner: runner, home: home, config_home: 'relative').install(item)
|
|
203
|
+
described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, config_home: config).install(item)
|
|
204
|
+
described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, config_home: 'relative').install(item)
|
|
201
205
|
|
|
202
206
|
expect([File.exist?(File.join(config, 'systemd', 'user', "#{item.os_id}.timer")), File.exist?(timer_path(item))]).to eq([true, true])
|
|
203
207
|
end
|
|
@@ -217,7 +221,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
217
221
|
item = schedule
|
|
218
222
|
runner.answers['enable'] = ["Failed to enable unit\e[2J\n", 1]
|
|
219
223
|
|
|
220
|
-
expect { systemd.install(item) }.to raise_error(RubyRaider::
|
|
224
|
+
expect { systemd.install(item) }.to raise_error(RubyRaider::Schedules::Error, /systemd could not enable .*Failed to enable unit\?\[2J/)
|
|
221
225
|
expect(runner.calls.map { |argv| argv[2] }).to eq(%w[daemon-reload enable disable daemon-reload])
|
|
222
226
|
expect(runner.calls[2]).to eq(['systemctl', '--user', 'disable', "#{item.os_id}.timer"])
|
|
223
227
|
expect(Dir.children(units)).to be_empty
|
|
@@ -227,7 +231,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
227
231
|
item = schedule
|
|
228
232
|
runner.answers['daemon-reload'] = ['Failed to connect to bus', 1]
|
|
229
233
|
|
|
230
|
-
expect { systemd.install(item) }.to raise_error(RubyRaider::
|
|
234
|
+
expect { systemd.install(item) }.to raise_error(RubyRaider::Schedules::Error, /Failed to connect to bus/)
|
|
231
235
|
expect(Dir.children(units)).to be_empty
|
|
232
236
|
end
|
|
233
237
|
|
|
@@ -235,7 +239,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
235
239
|
['trailing ', 'tab\\'].each do |name|
|
|
236
240
|
item = schedule(dir: File.join(tmp, name))
|
|
237
241
|
|
|
238
|
-
expect { systemd.install(item) }.to raise_error(RubyRaider::
|
|
242
|
+
expect { systemd.install(item) }.to raise_error(RubyRaider::Schedules::Error, /systemd can not use the path/)
|
|
239
243
|
end
|
|
240
244
|
expect([runner.calls, File.exist?(units)]).to eq([[], false])
|
|
241
245
|
end
|
|
@@ -246,15 +250,15 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
246
250
|
original.call(path, *rest, **options) { raise Errno::ENOSPC }
|
|
247
251
|
end
|
|
248
252
|
|
|
249
|
-
expect { systemd.install(schedule) }.to raise_error(RubyRaider::
|
|
253
|
+
expect { systemd.install(schedule) }.to raise_error(RubyRaider::Schedules::Error, /Could not write .*No space left/)
|
|
250
254
|
expect([runner.calls, Dir.children(units)]).to eq([[], []])
|
|
251
255
|
end
|
|
252
256
|
|
|
253
|
-
it 'raises a
|
|
257
|
+
it 'raises a schedule error and runs nothing when it can not write the units' do
|
|
254
258
|
FileUtils.mkdir_p(File.dirname(units))
|
|
255
259
|
File.write(units, 'not a folder')
|
|
256
260
|
|
|
257
|
-
expect { systemd.install(schedule) }.to raise_error(RubyRaider::
|
|
261
|
+
expect { systemd.install(schedule) }.to raise_error(RubyRaider::Schedules::Error, /Could not write .*systemd/)
|
|
258
262
|
expect([runner.calls, File.read(units)]).to eq([[], 'not a folder'])
|
|
259
263
|
end
|
|
260
264
|
end
|
|
@@ -282,17 +286,17 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
282
286
|
item = schedule
|
|
283
287
|
allow(File).to receive(:unlink).and_raise(Errno::EACCES)
|
|
284
288
|
|
|
285
|
-
expect { systemd.remove(item.os_id) }.to raise_error(RubyRaider::
|
|
289
|
+
expect { systemd.remove(item.os_id) }.to raise_error(RubyRaider::Schedules::Error, /Could not remove .*Permission denied/)
|
|
286
290
|
end
|
|
287
291
|
|
|
288
292
|
it 'raises when the reload fails' do
|
|
289
293
|
runner.answers['daemon-reload'] = ['Failed to connect to bus', 1]
|
|
290
294
|
|
|
291
|
-
expect { systemd.remove(schedule.os_id) }.to raise_error(RubyRaider::
|
|
295
|
+
expect { systemd.remove(schedule.os_id) }.to raise_error(RubyRaider::Schedules::Error, /Failed to connect to bus/)
|
|
292
296
|
end
|
|
293
297
|
|
|
294
|
-
it 'refuses an os_id that is not a
|
|
295
|
-
expect { systemd.remove('../../evil') }.to raise_error(RubyRaider::
|
|
298
|
+
it 'refuses an os_id that is not a schedule id' do
|
|
299
|
+
expect { systemd.remove('../../evil') }.to raise_error(RubyRaider::Schedules::Error, /Invalid os_id/)
|
|
296
300
|
expect(runner.calls).to be_empty
|
|
297
301
|
end
|
|
298
302
|
end
|
|
@@ -320,8 +324,8 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
|
|
|
320
324
|
expect(systemd.present?(schedule.os_id)).to be(false)
|
|
321
325
|
end
|
|
322
326
|
|
|
323
|
-
it 'refuses an os_id that is not a
|
|
324
|
-
expect { systemd.present?('x; rm') }.to raise_error(RubyRaider::
|
|
327
|
+
it 'refuses an os_id that is not a schedule id and runs nothing' do
|
|
328
|
+
expect { systemd.present?('x; rm') }.to raise_error(RubyRaider::Schedules::Error, /Invalid os_id/)
|
|
325
329
|
expect(runner.calls).to be_empty
|
|
326
330
|
end
|
|
327
331
|
end
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'tmpdir'
|
|
4
4
|
require 'rexml/document'
|
|
5
|
-
require_relative '../../../../lib/utilities/
|
|
5
|
+
require_relative '../../../../lib/utilities/schedules/os/task_scheduler'
|
|
6
|
+
require_relative '../../../../lib/utilities/schedules/schedule'
|
|
6
7
|
|
|
7
|
-
# Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
|
|
8
8
|
# The fake runner, fixed clock and project folders sit next to the adapter under test
|
|
9
9
|
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
10
|
-
RSpec.describe RubyRaider::
|
|
10
|
+
RSpec.describe RubyRaider::Schedules::Os::TaskScheduler do # rubocop:disable RSpec/FilePath
|
|
11
11
|
# Records every command and answers from a table keyed by the schtasks verb; never runs anything
|
|
12
12
|
let(:fake_runner_class) do
|
|
13
13
|
Class.new do
|
|
@@ -22,7 +22,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
22
22
|
@calls << argv
|
|
23
23
|
@seen&.call(argv, stdin_data)
|
|
24
24
|
stderr, status, stdout = answers.fetch(argv[1], ['', 0])
|
|
25
|
-
RubyRaider::
|
|
25
|
+
RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: stdout.to_s, stderr: stderr, exitstatus: status)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def on_call(&block) = @seen = block
|
|
@@ -30,18 +30,20 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
let(:hostile) { %q('; rm -rf ~; ' </string> %h $(id) " \\ &) }
|
|
33
|
-
let(:tmp) { File.realpath(Dir.mktmpdir('
|
|
33
|
+
let(:tmp) { File.realpath(Dir.mktmpdir('schedules-schtasks')) }
|
|
34
34
|
let(:project_dir) { File.join(tmp, 'plain') }
|
|
35
35
|
let(:runner) { fake_runner_class.new }
|
|
36
36
|
let(:clock) { Struct.new(:now).new(Time.new(2026, 11, 9, 10, 30, 0)) }
|
|
37
37
|
let(:schtasks) { 'C:\\Windows\\System32\\schtasks.exe' }
|
|
38
|
-
let(:scheduler) { described_class.new(runner: runner, clock: clock, schtasks: schtasks) }
|
|
38
|
+
let(:scheduler) { described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, clock: clock, schtasks: schtasks) }
|
|
39
39
|
let(:now) { Time.utc(2026, 11, 9, 10, 0, 0) }
|
|
40
40
|
|
|
41
|
-
def schedule(dir: project_dir, **options)
|
|
41
|
+
def schedule(dir: project_dir, gem_env: {}, bundle: '/opt/ruby/bin/bundle', **options)
|
|
42
42
|
FileUtils.mkdir_p(dir)
|
|
43
|
-
|
|
44
|
-
RubyRaider::
|
|
43
|
+
# Hermetic: the GEM_HOME/GEM_PATH and bundle this spec runs with never reach the entry; a spec passes its own
|
|
44
|
+
allow(RubyRaider::Schedules::Schedule).to receive_messages(captured_gem_env: gem_env, bundle_path: bundle)
|
|
45
|
+
project = RubyRaider::Schedules::Project.new(File.realpath(dir))
|
|
46
|
+
RubyRaider::Schedules::Schedule.build(project: project, name: 'report', every: 'day', at: '08:00', now: now, **options)
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def task_name(item) = "\\RubyRaider\\#{item.os_id}"
|
|
@@ -193,7 +195,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
193
195
|
<?xml version="1.0" encoding="UTF-16"?>
|
|
194
196
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
195
197
|
<RegistrationInfo>
|
|
196
|
-
<Description>Ruby Raider
|
|
198
|
+
<Description>Ruby Raider schedule report (every day at 08:00)</Description>
|
|
197
199
|
<URI>\\RubyRaider\\#{item.os_id}</URI>
|
|
198
200
|
</RegistrationInfo>
|
|
199
201
|
<Triggers>
|
|
@@ -279,7 +281,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
279
281
|
end
|
|
280
282
|
|
|
281
283
|
it "refuses an argument with both ' and a glob character" do
|
|
282
|
-
expect { described_class.quote("a'*") }.to raise_error(RubyRaider::
|
|
284
|
+
expect { described_class.quote("a'*") }.to raise_error(RubyRaider::Schedules::Error, /can not pass "a'\*" to ruby\.exe/)
|
|
283
285
|
end
|
|
284
286
|
end
|
|
285
287
|
|
|
@@ -290,13 +292,14 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
290
292
|
end
|
|
291
293
|
|
|
292
294
|
it 'carries no environment and no log file' do
|
|
293
|
-
|
|
295
|
+
# Without the random temp folder's name, which may hold any of these words
|
|
296
|
+
text = scheduler.task_xml(schedule).gsub(tmp, '')
|
|
294
297
|
|
|
295
298
|
expect(text).not_to include('scheduler.log', 'Environment', 'GEM_HOME', 'cmd', 'powershell')
|
|
296
299
|
end
|
|
297
300
|
|
|
298
|
-
it 'runs ruby with the bundle path and the
|
|
299
|
-
item = schedule(retries: 2,
|
|
301
|
+
it 'runs ruby with the bundle path and the schedule run arguments' do
|
|
302
|
+
item = schedule(retries: 2, selection: RubyRaider::Schedules::Selection.new(kind: 'tag', tag: 'smoke'))
|
|
300
303
|
text = scheduler.task_xml(item)
|
|
301
304
|
|
|
302
305
|
expect(field(text, 'Task/Actions/Exec/Command')).to eq(item.ruby)
|
|
@@ -338,7 +341,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
338
341
|
|
|
339
342
|
# One at a time: a lone % in both would read as %...% across the joined Arguments and be refused
|
|
340
343
|
{
|
|
341
|
-
'
|
|
344
|
+
'the bundle path' => ->(spec) { spec.schedule(bundle: "/opt/#{spec.hostile}/bundle") },
|
|
342
345
|
'the project path' => ->(spec) { spec.schedule(dir: File.join(spec.tmp, 'p<', "string> #{spec.hostile}")) }
|
|
343
346
|
}.each do |where, build|
|
|
344
347
|
describe "with hostile values in #{where}" do
|
|
@@ -363,46 +366,49 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
363
366
|
end
|
|
364
367
|
|
|
365
368
|
describe 'with quotes and glob characters but no spaces' do
|
|
366
|
-
let(:item) { schedule(dir: File.join(tmp, 'Users', "O'Brien",
|
|
369
|
+
let(:item) { schedule(dir: File.join(tmp, 'Users', "O'Brien", "proj'"), bundle: '/opt/p[1]/a*b/bundle') }
|
|
367
370
|
|
|
368
371
|
it "quotes them so ruby.exe reads the exact argv back and globs nothing ('...' around a glob)" do
|
|
369
372
|
line = field(scheduler.task_xml(item), 'Task/Actions/Exec/Arguments')
|
|
370
373
|
|
|
371
374
|
expect(ruby_split_arguments(line)).to eq(item.argv.drop(1))
|
|
372
|
-
expect(line).to include(%("#{item.project_root}"),
|
|
375
|
+
expect(line).to include(%("#{item.project_root}"), "'/opt/p[1]/a*b/bundle'")
|
|
373
376
|
end
|
|
374
377
|
|
|
375
|
-
it "refuses an
|
|
376
|
-
expect { scheduler.install(schedule(
|
|
378
|
+
it "refuses an argument with both ' and a glob character before any schtasks call" do
|
|
379
|
+
expect { scheduler.install(schedule(bundle: "/opt/a'*/bundle")) }.to raise_error(RubyRaider::Schedules::Error, /ruby\.exe/)
|
|
377
380
|
expect(runner.calls).to be_empty
|
|
378
381
|
end
|
|
379
382
|
end
|
|
380
383
|
|
|
381
384
|
describe 'with %NAME% that Task Scheduler would expand' do
|
|
382
|
-
it 'refuses it in
|
|
383
|
-
item = schedule(
|
|
385
|
+
it 'refuses it in the bundle path before any schtasks call' do
|
|
386
|
+
item = schedule(bundle: '/opt/%PATH%/bundle')
|
|
384
387
|
|
|
385
|
-
expect { scheduler.install(item) }.to raise_error(RubyRaider::
|
|
388
|
+
expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, /Task Scheduler would expand %PATH% as an environment variable/)
|
|
386
389
|
expect(runner.calls).to be_empty
|
|
387
390
|
end
|
|
388
391
|
|
|
389
392
|
it 'refuses it in the project path before any schtasks call' do
|
|
390
393
|
item = schedule(dir: File.join(tmp, '%PATH%', 'proj'))
|
|
391
394
|
|
|
392
|
-
expect { scheduler.install(item) }.to raise_error(RubyRaider::
|
|
393
|
-
expect { scheduler.task_xml(item) }.to raise_error(RubyRaider::
|
|
395
|
+
expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, /would expand %PATH%/)
|
|
396
|
+
expect { scheduler.task_xml(item) }.to raise_error(RubyRaider::Schedules::Error, /would expand %PATH%/)
|
|
394
397
|
expect(runner.calls).to be_empty
|
|
395
398
|
end
|
|
396
399
|
|
|
397
400
|
it 'refuses a %...% formed across two arguments in the joined Arguments' do
|
|
398
|
-
|
|
401
|
+
item = schedule(dir: File.join(tmp, 'y%'), bundle: '/opt/%x/bundle')
|
|
402
|
+
|
|
403
|
+
expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, %r{would expand %x/bundle exec raider schedule run report --project .*/y%})
|
|
399
404
|
expect(runner.calls).to be_empty
|
|
400
405
|
end
|
|
401
406
|
|
|
402
407
|
it 'keeps a lone %' do
|
|
403
|
-
item = schedule(
|
|
408
|
+
item = schedule(dir: File.join(tmp, '100% sure'))
|
|
404
409
|
|
|
405
|
-
expect(split_arguments(field(scheduler.task_xml(item), 'Task/Actions/Exec/Arguments'))
|
|
410
|
+
expect(split_arguments(field(scheduler.task_xml(item), 'Task/Actions/Exec/Arguments'))).to eq(item.argv.drop(1))
|
|
411
|
+
expect(item.project_root).to end_with('/100% sure')
|
|
406
412
|
end
|
|
407
413
|
end
|
|
408
414
|
|
|
@@ -426,7 +432,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
426
432
|
runner.on_call { |argv, _| path = argv[5] }
|
|
427
433
|
runner.answers['/Create'] = ["ERROR: Access is denied.\e[2J\n", 1]
|
|
428
434
|
|
|
429
|
-
expect { scheduler.install(item) }.to raise_error(RubyRaider::
|
|
435
|
+
expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, /Task Scheduler.*#{Regexp.escape(item.os_id)}.*Access is denied\.\?\[2J/)
|
|
430
436
|
expect(File.exist?(path)).to be(false)
|
|
431
437
|
end
|
|
432
438
|
end
|
|
@@ -453,18 +459,18 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
453
459
|
runner.answers['/Delete'] = ["ERROR: Access\e is denied.\n", 1]
|
|
454
460
|
runner.answers['/Query'] = ['', 0, csv_rows('\\Other', task_name(item))]
|
|
455
461
|
|
|
456
|
-
expect { scheduler.remove(item.os_id) }.to raise_error(RubyRaider::
|
|
462
|
+
expect { scheduler.remove(item.os_id) }.to raise_error(RubyRaider::Schedules::Error, /Task Scheduler could not delete .*Access\? is denied\.\z/)
|
|
457
463
|
end
|
|
458
464
|
|
|
459
465
|
it 'raises with the delete error when the task list can not be read' do
|
|
460
466
|
runner.answers['/Delete'] = ['ERROR: Access is denied.', 1]
|
|
461
467
|
runner.answers['/Query'] = ['ERROR: list failed', 1]
|
|
462
468
|
|
|
463
|
-
expect { scheduler.remove(schedule.os_id) }.to raise_error(RubyRaider::
|
|
469
|
+
expect { scheduler.remove(schedule.os_id) }.to raise_error(RubyRaider::Schedules::Error, /could not delete .*Access is denied/)
|
|
464
470
|
end
|
|
465
471
|
|
|
466
|
-
it 'refuses an os_id that is not a
|
|
467
|
-
expect { scheduler.remove('..\\..\\evil') }.to raise_error(RubyRaider::
|
|
472
|
+
it 'refuses an os_id that is not a schedule id' do
|
|
473
|
+
expect { scheduler.remove('..\\..\\evil') }.to raise_error(RubyRaider::Schedules::Error, /Invalid os_id/)
|
|
468
474
|
expect(runner.calls).to be_empty
|
|
469
475
|
end
|
|
470
476
|
end
|
|
@@ -488,7 +494,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
488
494
|
it 'raises when the task list can not be read' do
|
|
489
495
|
runner.answers['/Query'] = ["ERROR: no\e list", 1]
|
|
490
496
|
|
|
491
|
-
expect { scheduler.present?(schedule.os_id) }.to raise_error(RubyRaider::
|
|
497
|
+
expect { scheduler.present?(schedule.os_id) }.to raise_error(RubyRaider::Schedules::Error, /could not list tasks: ERROR: no\? list/)
|
|
492
498
|
end
|
|
493
499
|
end
|
|
494
500
|
|
|
@@ -497,7 +503,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
|
|
|
497
503
|
allow(ENV).to receive(:fetch).with('SystemRoot', anything).and_return('D:\\Win')
|
|
498
504
|
item = schedule
|
|
499
505
|
|
|
500
|
-
described_class.new(runner: runner, clock: clock).remove(item.os_id)
|
|
506
|
+
described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, clock: clock).remove(item.os_id)
|
|
501
507
|
|
|
502
508
|
expect(runner.calls.first.first).to eq(File.join('D:\\Win', 'System32', 'schtasks.exe'))
|
|
503
509
|
end
|