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,445 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
require_relative '../../../lib/utilities/schedules/scheduler'
|
|
9
|
+
|
|
10
|
+
# The fake adapter, store and streams sit next to the project and scheduler
|
|
11
|
+
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
12
|
+
RSpec.describe RubyRaider::Schedules::Scheduler do # rubocop:disable RSpec/FilePath
|
|
13
|
+
# Stands in for an OS scheduler: records every call, keeps the installed os_ids; list_ids returns them plus
|
|
14
|
+
# whatever `extra` holds, unfiltered, so the scheduler's own checks are what keeps noise out
|
|
15
|
+
let(:fake_adapter) do
|
|
16
|
+
Class.new do
|
|
17
|
+
attr_reader :calls, :installed, :schedules, :extra
|
|
18
|
+
attr_accessor :fail_present, :fail_list, :fail_remove, :note
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@calls = []
|
|
22
|
+
@installed = []
|
|
23
|
+
@schedules = []
|
|
24
|
+
@extra = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def label = 'fakeos'
|
|
28
|
+
|
|
29
|
+
def install(schedule)
|
|
30
|
+
@calls << [:install, schedule.os_id]
|
|
31
|
+
@schedules << schedule
|
|
32
|
+
@installed |= [schedule.os_id]
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove(os_id)
|
|
37
|
+
@calls << [:remove, os_id]
|
|
38
|
+
raise RubyRaider::Schedules::Error, 'launchctl refused' if fail_remove
|
|
39
|
+
|
|
40
|
+
!@installed.delete(os_id).nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def present?(os_id)
|
|
44
|
+
raise RubyRaider::Schedules::Error, 'task list unreadable' if fail_present
|
|
45
|
+
|
|
46
|
+
@installed.include?(os_id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def list_ids(_prefix)
|
|
50
|
+
raise RubyRaider::Schedules::Error, 'task list unreadable' if fail_list
|
|
51
|
+
|
|
52
|
+
@installed + @extra
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def missed_runs_note = note
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
let(:dir) { File.realpath(Dir.mktmpdir('schedules-scheduler')) }
|
|
60
|
+
let(:project) { RubyRaider::Schedules::Project.new(dir) }
|
|
61
|
+
let(:adapter) { fake_adapter.new }
|
|
62
|
+
let(:store) { RubyRaider::Schedules::ScheduleStore.new(project) }
|
|
63
|
+
let(:out) { StringIO.new }
|
|
64
|
+
let(:err) { StringIO.new }
|
|
65
|
+
# Sun 8 Nov 2026 12:00 local
|
|
66
|
+
let(:now) { Time.local(2026, 11, 8, 12, 0) }
|
|
67
|
+
let(:host_os) { 'darwin25' }
|
|
68
|
+
let(:scheduler) { described_class.new(project, adapter: adapter, store: store, out: out, err: err, now: -> { now }, host_os: host_os) }
|
|
69
|
+
let(:os_id) { RubyRaider::Schedules::Schedule.os_id_for(dir, 'nightly') }
|
|
70
|
+
let(:yml) { File.join(dir, '.raider/schedules/schedules.yml') }
|
|
71
|
+
|
|
72
|
+
before do
|
|
73
|
+
%w[spec/a_spec.rb spec/b_spec.rb].each do |file|
|
|
74
|
+
FileUtils.mkdir_p(File.dirname(File.join(dir, file)))
|
|
75
|
+
File.write(File.join(dir, file), '')
|
|
76
|
+
end
|
|
77
|
+
# add checks the project's bundle has `raider schedule`
|
|
78
|
+
File.write(File.join(dir, 'Gemfile'), "gem 'ruby_raider'\n")
|
|
79
|
+
File.write(File.join(dir, 'Gemfile.lock'), "GEM\n specs:\n ruby_raider (#{RubyRaider::Schedules::Project::OWN_VERSION})\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
after { FileUtils.rm_rf(dir) }
|
|
83
|
+
|
|
84
|
+
def add(name = 'nightly', every: 'day', at: '08:00', **options) = scheduler.add(name, every: every, at: at, **options)
|
|
85
|
+
|
|
86
|
+
def log_run(name, *lines)
|
|
87
|
+
file = File.join(dir, ".raider/schedules/#{name}/runs.jsonl")
|
|
88
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
89
|
+
File.write(file, lines.map { |line| "#{JSON.generate(line)}\n" }.join, mode: 'a')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def edit_yml
|
|
93
|
+
data = YAML.safe_load_file(yml)
|
|
94
|
+
yield data['schedules']
|
|
95
|
+
File.write(yml, YAML.dump(data))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def mode(relative) = File.stat(File.join(dir, relative)).mode & 0o777
|
|
99
|
+
|
|
100
|
+
# What list prints, without what add printed before it
|
|
101
|
+
def listed(json: true)
|
|
102
|
+
out.truncate(0)
|
|
103
|
+
out.rewind
|
|
104
|
+
scheduler.list(json: json)
|
|
105
|
+
json ? JSON.parse(out.string) : out.string
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '.adapter' do
|
|
109
|
+
let(:runner) { instance_double(RubyRaider::Schedules::Os::CommandRunner) }
|
|
110
|
+
|
|
111
|
+
it 'picks launchd on macOS and Task Scheduler on Windows for test schedules, without calling them' do
|
|
112
|
+
picked = %w[darwin25 mingw32].map { |host| described_class.adapter(runner: runner, host_os: host) }
|
|
113
|
+
|
|
114
|
+
expect(picked.map(&:class)).to eq([RubyRaider::Schedules::Os::Launchd, RubyRaider::Schedules::Os::TaskScheduler])
|
|
115
|
+
expect(picked.map { |item| item.instance_variable_get(:@kind) }).to all(be(RubyRaider::Schedules::Kind::TESTS))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'refuses another OS, also when add picks it lazily, before any folder is made' do
|
|
119
|
+
expect { described_class.adapter(runner: runner, host_os: 'solaris2') }
|
|
120
|
+
.to raise_error(RubyRaider::Schedules::Error, 'Scheduling tests is not supported on solaris2')
|
|
121
|
+
|
|
122
|
+
lazy = described_class.new(project, out: out, err: err, now: -> { now }, host_os: 'solaris2')
|
|
123
|
+
expect { lazy.add('nightly', every: 'day', at: '08:00') }
|
|
124
|
+
.to raise_error(RubyRaider::Schedules::Error, 'Scheduling tests is not supported on solaris2')
|
|
125
|
+
expect(File.exist?(File.join(dir, '.raider'))).to be(false)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#add' do
|
|
130
|
+
it 'installs the OS entry, then writes the yml, ignores the folder in git and says when it next runs' do
|
|
131
|
+
allow(store).to receive(:save).and_wrap_original do |original, schedule|
|
|
132
|
+
expect(adapter.installed).to include(schedule.os_id)
|
|
133
|
+
original.call(schedule)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
add(every: 'weekday')
|
|
137
|
+
|
|
138
|
+
expect(out.string).to eq("Scheduled nightly: every weekday at 08:00, next run Mon 08:00\n")
|
|
139
|
+
expect([adapter.calls, store.find('nightly').selection.to_h]).to eq([[[:install, os_id]], { 'kind' => 'all' }])
|
|
140
|
+
expect([File.read(File.join(dir, '.gitignore')), err.string]).to eq([".raider/schedules/\n", ''])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'says Replaced when the name already had a schedule, and keeps one entry' do
|
|
144
|
+
add
|
|
145
|
+
add(every: 'week', at: '09:30', on: 'wed', tag: 'smoke')
|
|
146
|
+
|
|
147
|
+
expect(out.string.lines.last).to eq("Replaced nightly: every week on wed at 09:30, next run Wed 09:30\n")
|
|
148
|
+
expect(store.all.map { |item| [item.rule_text, item.selection.to_h] }).to eq([['every week on wed at 09:30', { 'kind' => 'tag', 'tag' => 'smoke' }]])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'stores spec files, a tag, retries, notify and show_browser as given' do
|
|
152
|
+
File.write(File.join(dir, 'helpers/driver_helper.rb').tap { |file| FileUtils.mkdir_p(File.dirname(file)) }, "require 'ruby_raider/runtime'\n")
|
|
153
|
+
add('files', files: %w[spec/b_spec.rb spec/a_spec.rb], retries: 2, notify: true, show_browser: true)
|
|
154
|
+
add('tagged', every: 'hour', at: ':15', tag: 'smoke')
|
|
155
|
+
|
|
156
|
+
expect(store.all.map { |item| [item.name, item.selection.to_h, item.retries, item.notify, item.show_browser, item.at] })
|
|
157
|
+
.to eq([['files', { 'kind' => 'files', 'files' => %w[spec/b_spec.rb spec/a_spec.rb] }, 2, true, true, '08:00'],
|
|
158
|
+
['tagged', { 'kind' => 'tag', 'tag' => 'smoke' }, 0, false, false, '00:15']])
|
|
159
|
+
expect(out.string.lines.last).to eq("Scheduled tagged: every hour at :15, next run Sun 12:15\n")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'installs from a freshly built schedule, never from the ruby, bundle or gem env of the yml it replaces' do
|
|
163
|
+
add
|
|
164
|
+
edit_yml { |entries| entries.first.merge!('ruby' => '/evil/ruby', 'bundle' => '/evil/bundle', 'gem_env' => { 'GEM_HOME' => '/evil' }) }
|
|
165
|
+
add
|
|
166
|
+
|
|
167
|
+
installed = adapter.schedules.last
|
|
168
|
+
expect([installed.ruby, installed.bundle, installed.gem_env]).to eq([RbConfig.ruby, RubyRaider::Schedules::Schedule.bundle_path,
|
|
169
|
+
RubyRaider::Schedules::Schedule.captured_gem_env])
|
|
170
|
+
expect(store.find('nightly').ruby).to eq(RbConfig.ruby)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'makes .raider/schedules and .raider/schedules/<name> 0700 folders' do
|
|
174
|
+
add
|
|
175
|
+
|
|
176
|
+
expect([mode('.raider/schedules'), mode('.raider/schedules/nightly'), mode('.raider/schedules/schedules.yml')]).to eq([0o700, 0o700, 0o600])
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'removes the OS entry again when the yml cannot be written' do
|
|
180
|
+
allow(store).to receive(:save).and_raise(RubyRaider::Schedules::Error, 'disk full')
|
|
181
|
+
|
|
182
|
+
expect { add }.to raise_error(RubyRaider::Schedules::Error, 'disk full: the fakeos entry was removed too, so nightly is not scheduled')
|
|
183
|
+
expect([adapter.calls, adapter.installed]).to eq([[[:install, os_id], [:remove, os_id]], []])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'says so when the rollback fails too' do
|
|
187
|
+
allow(store).to receive(:save).and_raise(Errno::ENOSPC)
|
|
188
|
+
adapter.fail_remove = true
|
|
189
|
+
|
|
190
|
+
expect { add }.to raise_error(RubyRaider::Schedules::Error, /No space left on device.*\(and removing the fakeos entry failed: launchctl refused\)/)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
context 'when refused before the OS scheduler or any file is touched' do
|
|
194
|
+
def refused(message, **options)
|
|
195
|
+
expect { scheduler.add(options.delete(:name) || 'nightly', every: 'day', at: '08:00', **options) }
|
|
196
|
+
.to raise_error(RubyRaider::Schedules::Error, message)
|
|
197
|
+
expect([adapter.calls, File.exist?(File.join(dir, '.raider')), File.exist?(File.join(dir, '.gitignore'))]).to eq([[], false, false])
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'refuses a bad name' do
|
|
201
|
+
['Nightly', 'a' * 61, '../x', '', "a\nb"].each { |name| refused(/Invalid schedule name/, name: name) }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'refuses a missing, hostile or outside spec file, and a tag with files' do
|
|
205
|
+
refused('spec/c_spec.rb not found', files: %w[spec/c_spec.rb])
|
|
206
|
+
refused(/Invalid spec file/, files: ['../x_spec.rb'])
|
|
207
|
+
refused(/Invalid spec file/, files: ['-rf_spec.rb'])
|
|
208
|
+
refused('Use --tag or spec files, not both', files: %w[spec/a_spec.rb], tag: 'smoke')
|
|
209
|
+
refused(/Invalid tag/, tag: '$(id)')
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'refuses a project whose bundle has no raider schedule, after the other checks' do
|
|
213
|
+
File.write(File.join(dir, 'Gemfile.lock'), "GEM\n specs:\n ruby_raider (4.0.1)\n")
|
|
214
|
+
refused(/\AScheduling needs ruby_raider \S+ or later in this project: bundle update ruby_raider\z/)
|
|
215
|
+
File.delete(File.join(dir, 'Gemfile.lock'))
|
|
216
|
+
refused(/: run bundle install first\z/)
|
|
217
|
+
refused(/Invalid every/, every: 'yearly')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'refuses a bad rule' do
|
|
221
|
+
refused(/Invalid every/, every: 'yearly')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'refuses show_browser on Linux, before checking the project' do
|
|
225
|
+
allow(project).to receive(:check_show_browser!)
|
|
226
|
+
linux = described_class.new(project, adapter: adapter, store: store, out: out, err: err, now: -> { now }, host_os: 'linux-gnu')
|
|
227
|
+
|
|
228
|
+
expect { linux.add('nightly', every: 'day', at: '08:00', show_browser: true) }
|
|
229
|
+
.to raise_error(RubyRaider::Schedules::Error, "Show the browser isn't available for scheduled runs on Linux")
|
|
230
|
+
expect([adapter.calls, File.exist?(File.join(dir, '.raider'))]).to eq([[], false])
|
|
231
|
+
expect(project).not_to have_received(:check_show_browser!)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'refuses show_browser for a project made before 4.0' do
|
|
235
|
+
refused('Show the browser needs a project made with Ruby Raider 4.0 or later', show_browser: true)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'refuses Windows-expanding file names on Windows' do
|
|
239
|
+
File.write(File.join(dir, 'spec/a%PATH%_spec.rb'), '')
|
|
240
|
+
windows = described_class.new(project, adapter: adapter, store: store, out: out, err: err, now: -> { now }, host_os: 'mingw32')
|
|
241
|
+
|
|
242
|
+
expect { windows.add('nightly', every: 'day', at: '08:00', files: ['spec/a%PATH%_spec.rb']) }.to raise_error(RubyRaider::Schedules::Error)
|
|
243
|
+
expect(adapter.calls).to eq([])
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'refuses a linked schedule folder before the OS scheduler is touched' do
|
|
248
|
+
FileUtils.mkdir_p(File.join(dir, '.raider/schedules'))
|
|
249
|
+
File.symlink(FileUtils.mkdir_p(File.join(dir, 'elsewhere')).first, File.join(dir, '.raider/schedules/nightly'))
|
|
250
|
+
|
|
251
|
+
expect { add }.to raise_error(RubyRaider::Schedules::Error, '.raider/schedules/nightly must be a folder, not a link')
|
|
252
|
+
expect(adapter.calls).to eq([])
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'refuses a schedules.yml it can not rewrite before the OS scheduler is touched' do
|
|
256
|
+
FileUtils.mkdir_p(File.dirname(yml))
|
|
257
|
+
File.write(yml, "schedules: nope\n")
|
|
258
|
+
|
|
259
|
+
expect { add }.to raise_error(RubyRaider::Schedules::Error, /not a schedules file/)
|
|
260
|
+
expect(adapter.calls).to eq([])
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'still schedules, with a warning, when .gitignore can not take the line' do
|
|
264
|
+
target = File.join(dir, 'elsewhere_ignore')
|
|
265
|
+
File.write(target, "keep\n")
|
|
266
|
+
File.symlink(target, File.join(dir, '.gitignore'))
|
|
267
|
+
|
|
268
|
+
add
|
|
269
|
+
|
|
270
|
+
expect(out.string).to start_with('Scheduled nightly')
|
|
271
|
+
expect(err.string).to eq("Could not add .raider/schedules/ to .gitignore: .gitignore is a link: add .raider/schedules/ to it yourself\n")
|
|
272
|
+
expect([File.read(target), store.find('nightly').name]).to eq(%W[keep\n nightly])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'warns, and still schedules, when .gitignore can not be written' do
|
|
276
|
+
allow(store).to receive(:ignore_in_git).and_raise(Errno::EACCES, '.gitignore')
|
|
277
|
+
|
|
278
|
+
add
|
|
279
|
+
|
|
280
|
+
expect([out.string.lines.size, err.string]).to eq([1, "Could not add .raider/schedules/ to .gitignore: Permission denied - .gitignore\n"])
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
describe '#remove' do
|
|
285
|
+
it 'removes the OS entry first, then the yml entry' do
|
|
286
|
+
add
|
|
287
|
+
allow(store).to receive(:delete).and_wrap_original do |original, name|
|
|
288
|
+
expect(adapter.installed).to be_empty
|
|
289
|
+
original.call(name)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
scheduler.remove('nightly')
|
|
293
|
+
|
|
294
|
+
expect([out.string.lines.last, store.find('nightly'), adapter.calls.last]).to eq(["Removed nightly\n", nil, [:remove, os_id]])
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it 'says when the OS entry was already gone, and when only the OS entry was left' do
|
|
298
|
+
add
|
|
299
|
+
adapter.installed.clear
|
|
300
|
+
scheduler.remove('nightly')
|
|
301
|
+
adapter.installed << os_id
|
|
302
|
+
scheduler.remove('nightly')
|
|
303
|
+
|
|
304
|
+
expect(out.string.lines.last(2)).to eq(["Removed nightly (it was already gone from fakeos)\n", "Removed nightly\n"])
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'refuses a name with no schedule, and a bad name without asking the OS' do
|
|
308
|
+
expect { scheduler.remove('nightly') }.to raise_error(RubyRaider::Schedules::Error, 'No schedule nightly')
|
|
309
|
+
expect { scheduler.remove('../x') }.to raise_error(RubyRaider::Schedules::Error, /Invalid schedule name/)
|
|
310
|
+
expect(adapter.calls).to eq([[:remove, os_id]])
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'keeps the yml entry when the OS scheduler refuses' do
|
|
314
|
+
add
|
|
315
|
+
adapter.fail_remove = true
|
|
316
|
+
|
|
317
|
+
expect { scheduler.remove('nightly') }.to raise_error(RubyRaider::Schedules::Error, 'launchctl refused')
|
|
318
|
+
expect(store.find('nightly')).not_to be_nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it 'says the OS entry is gone when the yml can not be rewritten' do
|
|
322
|
+
add
|
|
323
|
+
allow(store).to receive(:delete).and_raise(RubyRaider::Schedules::Error, 'disk full')
|
|
324
|
+
|
|
325
|
+
expect { scheduler.remove('nightly') }
|
|
326
|
+
.to raise_error(RubyRaider::Schedules::Error, 'disk full: the fakeos entry was already removed, but schedules.yml still lists nightly')
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it 'warns when the yml entry it removed could not be read' do
|
|
330
|
+
add
|
|
331
|
+
edit_yml { |entries| entries.first['every'] = 'yearly' }
|
|
332
|
+
|
|
333
|
+
scheduler.remove('nightly')
|
|
334
|
+
|
|
335
|
+
expect(err.string).to include('could not be read', 'fakeos entry made from a different project path may remain')
|
|
336
|
+
expect(File.read(yml)).not_to include('nightly')
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
describe '#list' do
|
|
341
|
+
let(:zone) { Time.local(2026, 11, 9, 8, 0).strftime('%:z') }
|
|
342
|
+
|
|
343
|
+
it 'is the JSON object with notice null and one row per schedule, in the spec shape' do
|
|
344
|
+
add('all_tests', every: 'weekday')
|
|
345
|
+
add('files', every: 'hour', at: ':15', files: %w[spec/a_spec.rb], retries: 1, notify: true)
|
|
346
|
+
log_run('all_tests', { 'status' => 'failed', 'finished_at' => '2026-11-06T08:01:00Z', 'trigger' => 'schedule',
|
|
347
|
+
'counts' => { 'total' => 3, 'passed' => 2, 'failed' => 1, 'skipped' => 0 }, 'run_id' => 'x' },
|
|
348
|
+
{ 'status' => 'busy', 'finished_at' => '2026-11-07T08:00:00Z', 'trigger' => 'schedule' })
|
|
349
|
+
|
|
350
|
+
expect(listed).to eq(
|
|
351
|
+
'notice' => nil,
|
|
352
|
+
'schedules' => [
|
|
353
|
+
{ 'name' => 'all_tests', 'every' => 'weekday', 'at' => '08:00', 'on' => nil, 'retries' => 0, 'notify' => false, 'show_browser' => false,
|
|
354
|
+
'selection' => { 'kind' => 'all' }, 'next_run_at' => "2026-11-09T08:00:00#{zone}", 'os_state' => 'ok',
|
|
355
|
+
'last' => { 'status' => 'failed', 'finished_at' => '2026-11-06T08:01:00Z', 'trigger' => 'schedule',
|
|
356
|
+
'counts' => { 'total' => 3, 'passed' => 2, 'failed' => 1, 'skipped' => 0 } } },
|
|
357
|
+
{ 'name' => 'files', 'every' => 'hour', 'at' => '00:15', 'on' => nil, 'retries' => 1, 'notify' => true, 'show_browser' => false,
|
|
358
|
+
'selection' => { 'kind' => 'files', 'files' => %w[spec/a_spec.rb] }, 'next_run_at' => Time.local(2026, 11, 8, 12, 15).iso8601,
|
|
359
|
+
'os_state' => 'ok', 'last' => nil }
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
it 'gives each os_state, the file check before asking the OS' do
|
|
365
|
+
%w[ok gone ruby file unknown].each { |name| add(name, files: %w[spec/a_spec.rb spec/b_spec.rb]) }
|
|
366
|
+
adapter.installed.delete(RubyRaider::Schedules::Schedule.os_id_for(dir, 'gone'))
|
|
367
|
+
edit_yml do |entries|
|
|
368
|
+
entries.find { |entry| entry['name'] == 'ruby' }['ruby'] = File.join(dir, 'no_ruby')
|
|
369
|
+
entries.find { |entry| entry['name'] == 'file' }['selection'] = { 'kind' => 'files', 'files' => %w[spec/a_spec.rb spec/gone_spec.rb spec/x_spec.rb] }
|
|
370
|
+
end
|
|
371
|
+
allow(adapter).to receive(:present?).and_wrap_original do |original, id|
|
|
372
|
+
raise RubyRaider::Schedules::Error, "task list \xFFunreadable\e".dup.force_encoding(Encoding::UTF_8) if id.end_with?('.unknown')
|
|
373
|
+
raise 'asked the OS for a schedule whose file is gone' if id.end_with?('.file')
|
|
374
|
+
|
|
375
|
+
original.call(id)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
expect(listed['schedules'].to_h { |row| [row['name'], row['os_state']] })
|
|
379
|
+
.to eq('file' => 'test file not found: spec/gone_spec.rb', 'gone' => 'missing from fakeos', 'ok' => 'ok',
|
|
380
|
+
'ruby' => 'Ruby not found: re-schedule', 'unknown' => 'unknown (task list ?unreadable?)')
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it "adds a row for each of this project's OS entries that schedules.yml lacks, and ignores every other id" do
|
|
384
|
+
add
|
|
385
|
+
prefix = RubyRaider::Schedules::Schedule.os_id_prefix_for(dir)
|
|
386
|
+
log_run('ghost', { 'status' => 'passed', 'finished_at' => 'f', 'trigger' => 'manual', 'counts' => {}, 'report_copy' => 'x' })
|
|
387
|
+
adapter.extra.push("#{prefix}ghost", "#{prefix}ghost", RubyRaider::Schedules::Schedule.os_id_for('/elsewhere', 'stranger'),
|
|
388
|
+
Class.new(RubyRaider::Schedules::Rule) { def self.kind = RubyRaider::Schedules::Kind::FLOW_LEGACY }.os_id_for(dir, 'legacy'), "#{prefix}../../etc", "#{prefix}a;id", "#{prefix}Caps",
|
|
389
|
+
"#{prefix}a\xFF".dup.force_encoding(Encoding::UTF_8), nil)
|
|
390
|
+
|
|
391
|
+
rows = listed['schedules']
|
|
392
|
+
|
|
393
|
+
expect(rows.map { |row| row['name'] }).to eq(%w[nightly ghost])
|
|
394
|
+
expect(rows.last).to eq('name' => 'ghost', 'every' => nil, 'at' => nil, 'on' => nil, 'retries' => nil, 'notify' => nil, 'show_browser' => nil,
|
|
395
|
+
'selection' => nil, 'next_run_at' => nil, 'os_state' => 'missing from schedules.yml',
|
|
396
|
+
'last' => { 'status' => 'passed', 'finished_at' => 'f', 'trigger' => 'manual', 'counts' => {} })
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it 'lists only schedules.yml rows when the OS scheduler can not be enumerated' do
|
|
400
|
+
add
|
|
401
|
+
adapter.fail_list = true
|
|
402
|
+
|
|
403
|
+
expect(listed['schedules'].map { |row| row['name'] }).to eq(%w[nightly])
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it 'still lists, with unknown states, where there is no OS scheduler to ask' do
|
|
407
|
+
add
|
|
408
|
+
lazy = described_class.new(project, out: out, err: err, now: -> { now }, host_os: 'solaris2')
|
|
409
|
+
out.truncate(0)
|
|
410
|
+
out.rewind
|
|
411
|
+
|
|
412
|
+
lazy.list
|
|
413
|
+
|
|
414
|
+
expect(out.string).to end_with("never run unknown (Scheduling tests is not supported on solaris2)\n")
|
|
415
|
+
expect(out.string.lines.size).to eq(1)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it 'is an empty list, or the add hint as text, with no schedules' do
|
|
419
|
+
expect(listed).to eq('notice' => nil, 'schedules' => [])
|
|
420
|
+
expect(listed(json: false)).to eq("No schedules yet: raider schedule add <name> --every day --at 08:00\n")
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it 'passes a notice through in JSON, and leaves it out of the text (it is on stderr already)' do
|
|
424
|
+
scheduler.list(json: true, notice: 'Removed 1 old schedule')
|
|
425
|
+
scheduler.list(notice: 'Removed 1 old schedule')
|
|
426
|
+
|
|
427
|
+
expect(JSON.parse(out.string.lines.first)['notice']).to eq('Removed 1 old schedule')
|
|
428
|
+
expect([out.string.lines.drop(1), err.string]).to eq([["No schedules yet: raider schedule add <name> --every day --at 08:00\n"], ''])
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it 'prints one printable line per row, then the missed runs note' do
|
|
432
|
+
add('nightly', tag: 'smoke', notify: true)
|
|
433
|
+
log_run('nightly', { 'status' => "passed\e[2J", 'finished_at' => '2026-11-07T08:01:00Z' })
|
|
434
|
+
adapter.extra << RubyRaider::Schedules::Schedule.os_id_for(dir, 'ghost')
|
|
435
|
+
adapter.note = '(cron: missed runs are skipped)'
|
|
436
|
+
|
|
437
|
+
lines = listed(json: false).lines
|
|
438
|
+
|
|
439
|
+
expect(lines[0]).to eq("#{'nightly'.ljust(30)} tag smoke, every day at 08:00 (notify) next Mon 9 Nov 08:00 passed?[2J 2026-11-07T08:01:00Z ok\n")
|
|
440
|
+
expect(lines[1]).to eq("#{'ghost'.ljust(30)} - - never run missing from schedules.yml\n")
|
|
441
|
+
expect(lines[2..]).to eq(["(cron: missed runs are skipped)\n"])
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
# rubocop:enable RSpec/MultipleMemoizedHelpers
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../../../lib/utilities/schedules/selection'
|
|
5
|
+
|
|
6
|
+
RSpec.describe RubyRaider::Schedules::Selection do # rubocop:disable RSpec/FilePath
|
|
7
|
+
let(:tmp) { File.realpath(Dir.mktmpdir('schedules-selection')) }
|
|
8
|
+
let(:root) { File.join(tmp, 'project') }
|
|
9
|
+
let(:mac) { 'darwin24' }
|
|
10
|
+
let(:windows) { 'mingw32' }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
FileUtils.mkdir_p(File.join(root, 'spec/login'))
|
|
14
|
+
%w[spec/a_spec.rb spec/b_spec.rb spec/login/c_spec.rb].each { |file| File.write(File.join(root, file), '') }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
after { FileUtils.rm_rf(tmp) }
|
|
18
|
+
|
|
19
|
+
def parse(files: [], tag: nil, host_os: mac) = described_class.parse(files: files, tag: tag, root: root, host_os: host_os)
|
|
20
|
+
|
|
21
|
+
def error_for(...)
|
|
22
|
+
parse(...)
|
|
23
|
+
nil
|
|
24
|
+
rescue RubyRaider::Schedules::Error => e
|
|
25
|
+
e.message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def load_error(hash, host_os: mac)
|
|
29
|
+
described_class.from_h(hash, root: root, host_os: host_os)
|
|
30
|
+
nil
|
|
31
|
+
rescue RubyRaider::Schedules::Error => e
|
|
32
|
+
e.message
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '.parse' do
|
|
36
|
+
it 'selects all tests with no files and no tag' do
|
|
37
|
+
expect([parse.kind, parse.to_h]).to eq(['all', { 'kind' => 'all' }])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'selects one tag' do
|
|
41
|
+
expect(parse(tag: 'smoke:fast-1_x').to_h).to eq({ 'kind' => 'tag', 'tag' => 'smoke:fast-1_x' })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'selects files, removing duplicates and keeping their order' do
|
|
45
|
+
selection = parse(files: %w[spec/b_spec.rb spec/a_spec.rb spec/b_spec.rb spec/login/c_spec.rb])
|
|
46
|
+
|
|
47
|
+
expect(selection.to_h).to eq({ 'kind' => 'files', 'files' => %w[spec/b_spec.rb spec/a_spec.rb spec/login/c_spec.rb] })
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'refuses a tag together with files' do
|
|
51
|
+
expect(error_for(files: %w[spec/a_spec.rb], tag: 'smoke')).to eq('Use --tag or spec files, not both')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'refuses a tag with a space, a semicolon, a newline or nothing in it' do
|
|
55
|
+
errors = ['smoke test', 'smoke;rm', "smoke\n;x", '', 'smoke$(id)', 1, "\xFF".dup.force_encoding('UTF-8')].map { |tag| error_for(tag: tag) }
|
|
56
|
+
|
|
57
|
+
expect(errors).to all(start_with('Invalid tag'))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'refuses paths that climb out, look like options or are absolute' do
|
|
61
|
+
hostile = ['../x_spec.rb', 'spec/../../x_spec.rb', '-rf_spec.rb', '/abs_spec.rb', "#{root}/spec/a_spec.rb"]
|
|
62
|
+
|
|
63
|
+
expect(hostile.map { |file| error_for(files: [file]) }).to all(start_with('Invalid spec file'))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'refuses a file that is not a _spec.rb, has a control character or is not text' do
|
|
67
|
+
bad = ['spec/a.rb', 'spec/a_spec.rb.bak', "spec/a\n_spec.rb", "spec/a\0_spec.rb", '', nil, 1, "spec/\xFF_spec.rb".b.force_encoding('UTF-8')]
|
|
68
|
+
|
|
69
|
+
expect(bad.map { |file| error_for(files: [file]) }).to all(start_with('Invalid spec file'))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'refuses a link that points outside the project' do
|
|
73
|
+
outside = File.join(tmp, 'outside_spec.rb')
|
|
74
|
+
File.write(outside, '')
|
|
75
|
+
File.symlink(outside, File.join(root, 'spec/link_spec.rb'))
|
|
76
|
+
|
|
77
|
+
expect(error_for(files: %w[spec/link_spec.rb])).to eq('spec/link_spec.rb points outside the project')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'refuses a folder link that points outside the project' do
|
|
81
|
+
FileUtils.mkdir_p(File.join(tmp, 'elsewhere'))
|
|
82
|
+
File.write(File.join(tmp, 'elsewhere/x_spec.rb'), '')
|
|
83
|
+
File.symlink(File.join(tmp, 'elsewhere'), File.join(root, 'spec/away'))
|
|
84
|
+
|
|
85
|
+
expect(error_for(files: %w[spec/away/x_spec.rb])).to eq('spec/away/x_spec.rb points outside the project')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'takes a link that stays inside the project' do
|
|
89
|
+
File.symlink(File.join(root, 'spec/a_spec.rb'), File.join(root, 'spec/alias_spec.rb'))
|
|
90
|
+
|
|
91
|
+
expect(parse(files: %w[spec/alias_spec.rb]).files).to eq(%w[spec/alias_spec.rb])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'refuses a file that is not there' do
|
|
95
|
+
expect(error_for(files: %w[spec/a_spec.rb spec/gone_spec.rb])).to eq('spec/gone_spec.rb not found')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'takes 200 files and refuses 201' do
|
|
99
|
+
files = Array.new(201) { |index| "spec/many/f#{index}_spec.rb" }
|
|
100
|
+
FileUtils.mkdir_p(File.join(root, 'spec/many'))
|
|
101
|
+
files.each { |file| File.write(File.join(root, file), '') }
|
|
102
|
+
|
|
103
|
+
expect([parse(files: files.first(200)).files.size, error_for(files: files)])
|
|
104
|
+
.to eq([200, 'Too many spec files (201): select at most 200, or use a tag'])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'counts files after removing duplicates' do
|
|
108
|
+
expect(parse(files: %w[spec/a_spec.rb] * 300).files).to eq(%w[spec/a_spec.rb])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'refuses %NAME% and glob characters in a file on Windows only' do
|
|
112
|
+
%w[a%PATH%_spec.rb a*_spec.rb a?_spec.rb a[1]_spec.rb a{b}_spec.rb].each { |file| File.write(File.join(root, 'spec', file), '') }
|
|
113
|
+
files = %w[spec/a%PATH%_spec.rb spec/a*_spec.rb spec/a?_spec.rb spec/a[1]_spec.rb spec/a{b}_spec.rb]
|
|
114
|
+
|
|
115
|
+
expect([files.map { |file| error_for(files: [file], host_os: windows) }, files.map { |file| error_for(files: [file]) }])
|
|
116
|
+
.to match([all(start_with('Invalid spec file')), all(be_nil)])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'refuses backslash climbs and drive paths on Windows' do
|
|
120
|
+
errors = ['spec\\..\\..\\x_spec.rb', 'C:/x_spec.rb', '\\\\host\\x_spec.rb'].map { |file| error_for(files: [file], host_os: windows) }
|
|
121
|
+
|
|
122
|
+
expect(errors).to all(start_with('Invalid spec file'))
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe '.from_h' do
|
|
127
|
+
it 'reads back what to_h wrote' do
|
|
128
|
+
[parse, parse(tag: 'smoke'), parse(files: %w[spec/a_spec.rb spec/login/c_spec.rb])].each do |selection|
|
|
129
|
+
expect(described_class.from_h(selection.to_h, root: root, host_os: mac).to_h).to eq(selection.to_h)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'loads a selection whose file is gone and names the missing file' do
|
|
134
|
+
selection = described_class.from_h({ 'kind' => 'files', 'files' => %w[spec/a_spec.rb spec/gone_spec.rb] }, root: root, host_os: mac)
|
|
135
|
+
|
|
136
|
+
expect([selection.missing_file(root), parse(files: %w[spec/a_spec.rb]).missing_file(root)]).to eq(['spec/gone_spec.rb', nil])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'refuses on load a file that exists but resolves outside the project' do
|
|
140
|
+
File.write(File.join(tmp, 'outside_spec.rb'), '')
|
|
141
|
+
File.symlink(File.join(tmp, 'outside_spec.rb'), File.join(root, 'spec/link_spec.rb'))
|
|
142
|
+
|
|
143
|
+
expect(load_error({ 'kind' => 'files', 'files' => %w[spec/link_spec.rb] })).to eq('spec/link_spec.rb points outside the project')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'refuses broken shapes as errors, never anything else' do
|
|
147
|
+
broken = [nil, 'all', [], {}, { 'kind' => 'some' }, { 'kind' => 'tag' }, { 'kind' => 'tag', 'tag' => 'a b' },
|
|
148
|
+
{ 'kind' => 'files' }, { 'kind' => 'files', 'files' => [] }, { 'kind' => 'files', 'files' => 'spec/a_spec.rb' },
|
|
149
|
+
{ 'kind' => 'files', 'files' => [1] }, { 'kind' => 'files', 'files' => ['../x_spec.rb'] },
|
|
150
|
+
{ 'kind' => 'all', 'tag' => 'smoke' }, { 'kind' => 'tag', 'tag' => 'smoke', 'files' => %w[spec/a_spec.rb] },
|
|
151
|
+
{ 'kind' => 'files', 'files' => Array.new(201) { |index| "spec/f#{index}_spec.rb" } }]
|
|
152
|
+
|
|
153
|
+
expect(broken.map { |hash| load_error(hash) }).to all(be_a(String))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'applies the Windows rules on load' do
|
|
157
|
+
expect(load_error({ 'kind' => 'files', 'files' => %w[spec/a%PATH%_spec.rb] }, host_os: windows)).to start_with('Invalid spec file')
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe '#check!' do
|
|
162
|
+
it 'raises for a missing file and returns itself otherwise' do
|
|
163
|
+
gone = described_class.from_h({ 'kind' => 'files', 'files' => %w[spec/gone_spec.rb] }, root: root, host_os: mac)
|
|
164
|
+
present = parse(files: %w[spec/a_spec.rb])
|
|
165
|
+
|
|
166
|
+
expect(present.check!(root)).to be(present)
|
|
167
|
+
expect { gone.check!(root) }.to raise_error(RubyRaider::Schedules::Error, 'spec/gone_spec.rb not found')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'is frozen' do
|
|
172
|
+
selection = parse(files: %w[spec/a_spec.rb])
|
|
173
|
+
|
|
174
|
+
expect([selection, selection.files, selection.files.first]).to all(be_frozen)
|
|
175
|
+
end
|
|
176
|
+
end
|