haveapi 0.30.2 → 0.31.1
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/benchmarks/Gemfile +30 -0
- data/benchmarks/Gemfile.lock +296 -0
- data/benchmarks/endpoints.rb +77 -21
- data/benchmarks/performance_compare.rb +204 -19
- data/benchmarks/shell.nix +21 -0
- data/benchmarks/spec/client_examples_spec.rb +216 -0
- data/doc/client-examples.md +88 -35
- data/doc/performance.md +163 -61
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/client_examples/ts_generated_client.rb +36 -0
- data/lib/haveapi/documentation_clients.rb +49 -6
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/spec/client_examples/profile_mount_spec.rb +45 -0
- data/spec/client_examples/ts_generated_client_spec.rb +53 -0
- data/spec/client_examples/typescript_profiles_spec.rb +113 -0
- data/spec/documentation/client_examples_spec.rb +2 -2
- data/spec/performance/comparator_spec.rb +136 -6
- data/spec/performance/event_baseline_spec.rb +185 -30
- data/spec/performance/release_lineage_spec.rb +53 -14
- data/spec/performance/runner_spec.rb +136 -0
- data/spec/performance/source_versions_spec.rb +168 -0
- data/test_support/all_client_performance_fixture.rb +84 -0
- metadata +12 -3
|
@@ -3,6 +3,7 @@ require 'open3'
|
|
|
3
3
|
require 'tmpdir'
|
|
4
4
|
require 'yaml'
|
|
5
5
|
require_relative '../../../../utils/performance_ci'
|
|
6
|
+
require_relative 'support/samples'
|
|
6
7
|
|
|
7
8
|
RSpec.describe HaveAPI::PerformanceCI do
|
|
8
9
|
let(:candidate) { sha('c') }
|
|
@@ -43,27 +44,111 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
43
44
|
expect(result).to include(candidate_sha: candidate, baseline_sha: base, profile: 'quick', relevant: true)
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
%w[master feature].each do |branch|
|
|
48
|
+
it "compares the exact #{branch} push batch against event before" do
|
|
49
|
+
event.merge!('ref' => "refs/heads/#{branch}", 'after' => candidate, 'before' => base)
|
|
50
|
+
allow(repository).to receive(:merge_base)
|
|
51
|
+
|
|
52
|
+
expect(resolve('push', event)).to include(baseline_sha: base, candidate_sha: candidate,
|
|
53
|
+
reason: 'push event before/after batch',
|
|
54
|
+
push_pair: { 'event_name' => 'push', 'before' => base, 'after' => candidate })
|
|
55
|
+
expect(repository).to have_received(:paths).with(base, candidate)
|
|
56
|
+
expect(repository).not_to have_received(:merge_base)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "permits a nonancestor #{branch} rewrite only for the exact pushed pair" do
|
|
60
|
+
event.merge!('ref' => "refs/heads/#{branch}", 'after' => candidate, 'before' => base, 'forced' => true)
|
|
61
|
+
allow(repository).to receive(:ancestor?).with(base, candidate).and_return(false)
|
|
62
|
+
allow(repository).to receive(:merge_base)
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
result = resolver.resolve(event_name: 'push', event:, event_sha: sha('e'))
|
|
65
|
+
expect(result).to include(baseline_sha: base, candidate_sha: candidate,
|
|
66
|
+
push_pair: { 'event_name' => 'push', 'before' => base, 'after' => candidate })
|
|
67
|
+
expect(repository).to have_received(:paths).with(base, candidate)
|
|
68
|
+
expect(repository).not_to have_received(:merge_base)
|
|
69
|
+
end
|
|
52
70
|
end
|
|
53
71
|
|
|
54
|
-
[false, true].each do |
|
|
55
|
-
it "uses the
|
|
72
|
+
[false, true].each do |created|
|
|
73
|
+
it "uses the captured default merge-base for a zero-before push with created=#{created}" do
|
|
56
74
|
event.merge!('ref' => 'refs/heads/feature', 'after' => candidate,
|
|
57
|
-
'before' =>
|
|
75
|
+
'before' => '0' * 40, 'created' => created)
|
|
58
76
|
allow(repository).to receive(:merge_base).with(candidate, default).and_return(base)
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
result = resolve('push', event, default_sha: default)
|
|
79
|
+
expect(result).to include(baseline_sha: base, captured_default_sha: default,
|
|
80
|
+
reason: 'new-branch merge-base against captured default')
|
|
81
|
+
expect(result[:push_pair]).to be_nil
|
|
82
|
+
expect(repository).to have_received(:paths).with(base, candidate)
|
|
61
83
|
end
|
|
62
84
|
end
|
|
63
85
|
|
|
64
|
-
it '
|
|
86
|
+
it 'does not guess a fallback when the captured default commit is unavailable' do
|
|
87
|
+
event.merge!('ref' => 'refs/heads/feature', 'after' => candidate, 'before' => '0' * 40)
|
|
88
|
+
allow(repository).to receive(:commit!).with(default).and_raise(described_class::Error, 'unavailable default')
|
|
89
|
+
expect { resolve('push', event, default_sha: default) }.to raise_error(described_class::Error, /unavailable default/)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'rejects a new branch whose captured-default merge-base equals its head' do
|
|
65
93
|
event.merge!('ref' => 'refs/heads/master', 'after' => candidate, 'before' => '0' * 40)
|
|
94
|
+
allow(repository).to receive(:merge_base).with(candidate, default).and_return(candidate)
|
|
95
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error, /differ/)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'does not replace an unavailable new-branch merge-base with another commit' do
|
|
99
|
+
event.merge!('ref' => 'refs/heads/feature', 'after' => candidate, 'before' => '0' * 40)
|
|
100
|
+
allow(repository).to receive(:merge_base).with(candidate, default).and_raise(described_class::Error, 'no merge-base')
|
|
101
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error, /no merge-base/)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'requires ancestry for the new-branch fallback instead of granting a push-pair exception' do
|
|
105
|
+
event.merge!('ref' => 'refs/heads/feature', 'after' => candidate, 'before' => '0' * 40)
|
|
106
|
+
allow(repository).to receive(:merge_base).with(candidate, default).and_return(base)
|
|
107
|
+
allow(repository).to receive(:ancestor?).with(base, candidate).and_return(false)
|
|
108
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error, /ancestor/)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'does not grant the quick push exception to a weekly push profile' do
|
|
112
|
+
event.merge!('ref' => 'refs/heads/feature', 'after' => candidate, 'before' => described_class::REFERENCE,
|
|
113
|
+
'inputs' => { 'profile' => 'weekly' })
|
|
114
|
+
allow(repository).to receive(:ancestor?).with(described_class::REFERENCE, candidate).and_return(false)
|
|
115
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error, /ancestor/)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
%w[before after].each do |endpoint|
|
|
119
|
+
[nil, '', 'short', 'A' * 40].each do |invalid|
|
|
120
|
+
it "rejects an invalid push #{endpoint} #{invalid.inspect}" do
|
|
121
|
+
event.merge!('ref' => 'refs/heads/feature', 'before' => base, 'after' => candidate, endpoint => invalid)
|
|
122
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error)
|
|
123
|
+
expect(repository).not_to have_received(:paths)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "rejects a missing push #{endpoint}" do
|
|
128
|
+
event.merge!('ref' => 'refs/heads/feature', 'before' => base, 'after' => candidate)
|
|
129
|
+
event.delete(endpoint)
|
|
130
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error)
|
|
131
|
+
expect(repository).not_to have_received(:paths)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
%w[noncommit unavailable].each do |failure|
|
|
135
|
+
it "rejects a #{failure} push #{endpoint} without substituting a merge-base" do
|
|
136
|
+
event.merge!('ref' => 'refs/heads/feature', 'before' => base, 'after' => candidate)
|
|
137
|
+
allow(repository).to receive(:commit!).with(event.fetch(endpoint)).and_raise(described_class::Error, failure)
|
|
138
|
+
allow(repository).to receive(:merge_base)
|
|
139
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error, failure)
|
|
140
|
+
expect(repository).not_to have_received(:merge_base)
|
|
141
|
+
expect(repository).not_to have_received(:paths)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'rejects zero after and equal push endpoints' do
|
|
147
|
+
event.merge!('ref' => 'refs/heads/feature', 'before' => base, 'after' => '0' * 40)
|
|
66
148
|
expect { resolve('push', event) }.to raise_error(described_class::Error)
|
|
149
|
+
event['after'] = base
|
|
150
|
+
expect { resolve('push', event) }.to raise_error(described_class::Error, /differ/)
|
|
151
|
+
expect(repository).not_to have_received(:paths)
|
|
67
152
|
end
|
|
68
153
|
|
|
69
154
|
context 'with the verified maintenance lineage' do
|
|
@@ -83,22 +168,28 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
83
168
|
it 'compares the complete maintenance push batch using event before' do
|
|
84
169
|
event.merge!('ref' => 'refs/heads/haveapi-0.30', 'before' => base, 'after' => candidate)
|
|
85
170
|
expect(resolve('push', event)).to include(baseline_sha: base, candidate_sha: candidate,
|
|
86
|
-
release_lineage: lineage, reason: '
|
|
171
|
+
release_lineage: lineage, reason: 'push event before/after batch',
|
|
172
|
+
push_pair: { 'event_name' => 'push', 'before' => base, 'after' => candidate })
|
|
87
173
|
end
|
|
88
174
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
captured_default_sha: default, release_lineage: lineage)
|
|
95
|
-
end
|
|
175
|
+
it 'permits a proven maintenance rewrite with the exact push pair' do
|
|
176
|
+
event.merge!('ref' => 'refs/heads/haveapi-0.30', 'before' => base, 'after' => candidate, 'forced' => true)
|
|
177
|
+
allow(repository).to receive(:ancestor?).with(base, candidate).and_return(false)
|
|
178
|
+
expect(resolve('push', event)).to include(baseline_sha: base, candidate_sha: candidate, release_lineage: lineage,
|
|
179
|
+
push_pair: { 'event_name' => 'push', 'before' => base, 'after' => candidate })
|
|
96
180
|
end
|
|
97
181
|
|
|
98
|
-
it '
|
|
99
|
-
event.merge!('ref' => 'refs/heads/feature', 'after' => candidate)
|
|
100
|
-
|
|
101
|
-
expect
|
|
182
|
+
it 'uses exact before for an existing maintenance feature without looking up a release head' do
|
|
183
|
+
event.merge!('ref' => 'refs/heads/feature', 'before' => base, 'after' => candidate)
|
|
184
|
+
expect(resolve('push', event)).to include(baseline_sha: base, candidate_sha: candidate, release_lineage: lineage)
|
|
185
|
+
expect(repository).not_to have_received(:default_sha).with('haveapi-0.30')
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'uses the captured default merge-base for a new maintenance feature' do
|
|
189
|
+
event.merge!('ref' => 'refs/heads/feature', 'before' => '0' * 40, 'after' => candidate)
|
|
190
|
+
allow(repository).to receive(:merge_base).with(candidate, default).and_return(base)
|
|
191
|
+
expect(resolve('push', event)).to include(baseline_sha: base, captured_default_sha: default, release_lineage: lineage)
|
|
192
|
+
expect(repository).not_to have_received(:default_sha).with('haveapi-0.30')
|
|
102
193
|
end
|
|
103
194
|
|
|
104
195
|
it 'accepts only the original weekly reference through the proven mapping' do
|
|
@@ -137,6 +228,15 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
137
228
|
expect { resolve('workflow_dispatch', event) }.to raise_error(described_class::Error, /differ/)
|
|
138
229
|
end
|
|
139
230
|
|
|
231
|
+
it 'does not extend the push exception to pull requests or schedules' do
|
|
232
|
+
event['pull_request'] = { 'head' => { 'sha' => candidate }, 'base' => { 'sha' => default } }
|
|
233
|
+
allow(repository).to receive(:merge_base).with(candidate, default).and_return(base)
|
|
234
|
+
allow(repository).to receive(:ancestor?).with(base, candidate).and_return(false)
|
|
235
|
+
expect { resolve('pull_request', event) }.to raise_error(described_class::Error, /ancestor/)
|
|
236
|
+
allow(repository).to receive(:ancestor?).with(described_class::REFERENCE, candidate).and_return(false)
|
|
237
|
+
expect { resolve('schedule', event) }.to raise_error(described_class::Error, /ancestor/)
|
|
238
|
+
end
|
|
239
|
+
|
|
140
240
|
it 'uses the exact scheduled event SHA and preserves fixed reference ancestry' do
|
|
141
241
|
expect(resolve('schedule', event)).to include(candidate_sha: candidate, baseline_sha: described_class::REFERENCE, profile: 'weekly')
|
|
142
242
|
expect(repository).to have_received(:ancestor?).with(described_class::REFERENCE, candidate)
|
|
@@ -169,7 +269,7 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
169
269
|
end
|
|
170
270
|
|
|
171
271
|
it 'skips only irrelevant valid push/PR changes and still forces scheduled/manual work' do
|
|
172
|
-
allow(repository).to receive(:paths).and_return(['
|
|
272
|
+
allow(repository).to receive(:paths).and_return(['LICENSE.txt'])
|
|
173
273
|
event.merge!('ref' => 'refs/heads/master', 'after' => candidate, 'before' => base)
|
|
174
274
|
expect(resolve('push', event)).to include(relevant: false)
|
|
175
275
|
expect(resolve('schedule', event)).to include(relevant: true)
|
|
@@ -178,12 +278,40 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
178
278
|
|
|
179
279
|
it 'detects configuration, dependencies, harness and workflow changes' do
|
|
180
280
|
%w[flake.lock Gemfile Makefile .ruby-version servers/ruby/benchmarks/endpoints.rb
|
|
281
|
+
servers/ruby/benchmarks/Gemfile servers/ruby/benchmarks/Gemfile.lock servers/ruby/benchmarks/shell.nix
|
|
181
282
|
servers/ruby/test_support/performance_sqlite_fixture.rb clients/ruby/Gemfile clients/rust/Gemfile
|
|
182
|
-
doc/performance.md utils/performance_ci.rb .github/workflows/servers-ruby-performance.yml
|
|
283
|
+
doc/performance.md utils/performance_ci.rb .github/workflows/servers-ruby-performance.yml
|
|
284
|
+
servers/ruby/benchmarks/spec/client_examples_spec.rb servers/ruby/test_support/all_client_performance_fixture.rb
|
|
285
|
+
clients/go/lib/haveapi/go_client/documentation.rb clients/typescript/lib/haveapi/typescript_client/documentation.rb
|
|
286
|
+
clients/js/src/client.ts clients/python/haveapi/client.py clients/php/src/Client.php clients/new-client/renderer.rb].each do |path|
|
|
183
287
|
expect(described_class::RELEVANT).to match(path)
|
|
184
288
|
end
|
|
185
289
|
end
|
|
186
290
|
|
|
291
|
+
it 'requires the current-client contract before comparison and rejects a missing contract outcome' do
|
|
292
|
+
path = File.expand_path('../../../../.github/workflows/servers-ruby-performance.yml', __dir__)
|
|
293
|
+
steps = YAML.load_file(path).fetch('jobs').fetch('performance').fetch('steps')
|
|
294
|
+
contract = steps.find { |step| step['id'] == 'clients' }
|
|
295
|
+
comparison = steps.find { |step| step['name'] == 'Run the selected pair outside Bundler' }
|
|
296
|
+
expect(steps.index(contract)).to be < steps.index(comparison)
|
|
297
|
+
expect(contract.fetch('if')).to eq(comparison.fetch('if'))
|
|
298
|
+
expect(contract.fetch('run')).to include('make test-performance-clients', "echo 'outcome=passed'")
|
|
299
|
+
completion = steps.find { |step| step['id'] == 'completion' }
|
|
300
|
+
Dir.mktmpdir('haveapi-client-contract-gate-') do |directory|
|
|
301
|
+
File.write(File.join(directory, 'selection.json'), '{}')
|
|
302
|
+
File.write(File.join(directory, 'runner.exit'), "0\n")
|
|
303
|
+
FileUtils.mkdir_p(File.join(directory, 'results'))
|
|
304
|
+
File.write(File.join(directory, 'results/report.json'), '{}')
|
|
305
|
+
File.write(File.join(directory, 'all-client-contract.log'), 'passed')
|
|
306
|
+
[nil, '', 'failed', 'passed'].each do |outcome|
|
|
307
|
+
environment = { 'RELEVANT' => 'true', 'CLIENT_CONTRACT' => outcome,
|
|
308
|
+
'PERFORMANCE_EVIDENCE' => directory, 'GITHUB_OUTPUT' => File.join(directory, 'output') }
|
|
309
|
+
_output, _error, status = Open3.capture3(environment, 'bash', '-euo', 'pipefail', '-c', completion.fetch('run'))
|
|
310
|
+
expect(status.success?).to eq(outcome == 'passed')
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
187
315
|
it 'only accepts successful job completion in the aggregate gate' do
|
|
188
316
|
expect(described_class.aggregate!('success', 'compared')).to be(true)
|
|
189
317
|
expect(described_class.aggregate!('success', 'no-work')).to be(true)
|
|
@@ -206,20 +334,30 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
206
334
|
end
|
|
207
335
|
end
|
|
208
336
|
|
|
209
|
-
[0, 1, 3].each do |code|
|
|
210
|
-
it "preserves actual runner exit #{code}
|
|
337
|
+
[[0, 'pass', false], [0, 'inconclusive', false], [1, 'regression', false], [3, 'error', false], [0, 'pass', true]].each do |code, outcome, rewrite|
|
|
338
|
+
it "preserves actual runner exit #{code}/#{outcome} and writes its summary#{' for a nonancestor push' if rewrite}" do
|
|
211
339
|
Dir.mktmpdir('haveapi-runner-wrapper-') do |directory|
|
|
212
340
|
checkout = File.join(directory, 'checkout')
|
|
213
341
|
FileUtils.mkdir_p(File.join(checkout, 'servers/ruby/benchmarks'))
|
|
342
|
+
samples, budget_json = PerformanceSamples.fixture
|
|
343
|
+
report = HaveAPI::Performance::Comparator.new(samples, JSON.parse(budget_json), budget_sha256: samples['budget_sha256']).compare
|
|
344
|
+
report.merge!('status' => outcome, 'baseline_sha' => base, 'candidate_sha' => candidate)
|
|
345
|
+
item = report['cases'].first
|
|
346
|
+
item['classification'] = outcome
|
|
347
|
+
item['timing']['wall_seconds']['classification'] = outcome
|
|
348
|
+
item['responses']['raw_changed'] = true
|
|
349
|
+
item['responses']['bytes_per_request'].merge!('baseline' => 100.0, 'candidate' => 112.0, 'delta' => 12.0, 'delta_percent' => 12.0)
|
|
350
|
+
summary_path = File.join(directory, 'summary.md')
|
|
351
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
352
|
+
allow(ENV).to receive(:fetch).and_call_original
|
|
353
|
+
allow(ENV).to receive(:[]).with('GITHUB_STEP_SUMMARY').and_return(summary_path)
|
|
354
|
+
allow(ENV).to receive(:fetch).with('GITHUB_STEP_SUMMARY').and_return(summary_path)
|
|
214
355
|
script = <<~RUBY
|
|
215
356
|
require 'json'
|
|
216
357
|
require 'fileutils'
|
|
217
358
|
output = ARGV.fetch(ARGV.index('--output') + 1)
|
|
218
359
|
FileUtils.mkdir_p(output)
|
|
219
|
-
File.write(File.join(output, 'report.json'), JSON.generate(
|
|
220
|
-
schema_version: #{HaveAPI::Performance::SAMPLE_SCHEMA}, status: '#{{ 0 => 'pass', 1 => 'regression', 2 => 'inconclusive', 3 => 'error' }.fetch(code)}',
|
|
221
|
-
baseline_sha: '#{base}', candidate_sha: '#{candidate}'
|
|
222
|
-
))
|
|
360
|
+
File.write(File.join(output, 'report.json'), #{JSON.generate(report).dump})
|
|
223
361
|
exit #{code}
|
|
224
362
|
RUBY
|
|
225
363
|
File.write(File.join(checkout, 'servers/ruby/benchmarks/endpoints.rb'), script)
|
|
@@ -228,6 +366,10 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
228
366
|
allow(repository).to receive(:git).with('diff', 'HEAD', '--').and_return('')
|
|
229
367
|
selection = { 'schema_version' => 1, 'relevant' => true, 'profile' => 'quick',
|
|
230
368
|
'baseline_sha' => base, 'candidate_sha' => candidate }
|
|
369
|
+
if rewrite
|
|
370
|
+
selection.merge!('event_name' => 'push', 'push_pair' => { 'event_name' => 'push', 'before' => base, 'after' => candidate })
|
|
371
|
+
allow(repository).to receive(:ancestor?).with(base, candidate).and_return(false)
|
|
372
|
+
end
|
|
231
373
|
campaign = described_class::Campaign.new(repository:, selection:,
|
|
232
374
|
evidence: File.join(directory, 'evidence'), sources: File.join(directory, 'sources'))
|
|
233
375
|
allow(campaign).to receive(:export).with(:baseline, base).and_return(directory)
|
|
@@ -235,6 +377,19 @@ RSpec.describe HaveAPI::PerformanceCI do
|
|
|
235
377
|
allow(campaign).to receive_messages(verify_dependencies: nil, retain_inputs: nil, command!: nil)
|
|
236
378
|
expect(campaign.run).to eq(code)
|
|
237
379
|
expect(File.read(File.join(directory, 'evidence/runner.exit')).strip).to eq(code.to_s)
|
|
380
|
+
summary = File.read(summary_path)
|
|
381
|
+
expect(summary).to include("Performance comparison: #{outcome}", base, candidate)
|
|
382
|
+
if outcome == 'error'
|
|
383
|
+
expect(summary).not_to include('Bytes/request')
|
|
384
|
+
else
|
|
385
|
+
expect(summary).to include("| api.public_get {} | #{outcome} | #{outcome} | pass | pass | true | false | 100.00 → 112.00 | +12.00 (+12.00%) |")
|
|
386
|
+
end
|
|
387
|
+
if rewrite
|
|
388
|
+
push_event = File.join(directory, 'evidence/push-event.json')
|
|
389
|
+
expect(JSON.parse(File.read(push_event))).to eq(selection)
|
|
390
|
+
command = JSON.parse(File.read(File.join(directory, 'evidence/runner-command.json')))
|
|
391
|
+
expect(command.last(2)).to eq(['--push-event', push_event])
|
|
392
|
+
end
|
|
238
393
|
end
|
|
239
394
|
end
|
|
240
395
|
end
|
|
@@ -76,7 +76,7 @@ describe HaveAPI::Performance::Release do
|
|
|
76
76
|
|
|
77
77
|
def contents(version, profile: 'quick')
|
|
78
78
|
original = described_class.paths(profile).to_h { |path| [path, File.read(File.join(root, path))] }
|
|
79
|
-
own =
|
|
79
|
+
own = original.fetch('VERSION').strip
|
|
80
80
|
original.to_h do |path, value|
|
|
81
81
|
value = "#{version}\n" if path == 'VERSION'
|
|
82
82
|
value = value.sub("VERSION = '#{own}'", "VERSION = '#{version}'") if described_class::VERSIONS.has_key?(path)
|
|
@@ -85,6 +85,16 @@ describe HaveAPI::Performance::Release do
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
def with_maintenance_source
|
|
89
|
+
Dir.mktmpdir('haveapi-maintenance-source-') do |source|
|
|
90
|
+
contents('0.30.2').each do |path, value|
|
|
91
|
+
FileUtils.mkdir_p(File.dirname(File.join(source, path)))
|
|
92
|
+
File.write(File.join(source, path), value)
|
|
93
|
+
end
|
|
94
|
+
yield source
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
it 'verifies the complete published binary delta and all four exact trees in an owned history' do
|
|
89
99
|
expect(proof).to include('reference_sha' => HaveAPI::Performance::REFERENCE, 'mapped_sha' => described_class::MAPPED,
|
|
90
100
|
'metadata_sha' => described_class::METADATA, 'trees' => described_class::TREES)
|
|
@@ -150,6 +160,8 @@ describe HaveAPI::Performance::Release do
|
|
|
150
160
|
'--baseline-root', root, '--candidate-root', root,
|
|
151
161
|
'--budgets', 'unused', '--output', 'unused'])
|
|
152
162
|
cli.send(:parse)
|
|
163
|
+
allow(cli).to receive(:verify_runtime)
|
|
164
|
+
allow(HaveAPI::Performance).to receive(:gemspec)
|
|
153
165
|
allow(cli).to receive(:verify_source) { |role| { root: "/controlled/#{role}", sha: head } }
|
|
154
166
|
allow(cli).to receive(:git) { |*arguments| repository.git(*arguments) }
|
|
155
167
|
allow(cli).to receive(:ancestor?) { |left, right| repository.ancestor?(left, right) }
|
|
@@ -190,7 +202,20 @@ describe HaveAPI::Performance::Release do
|
|
|
190
202
|
end
|
|
191
203
|
end
|
|
192
204
|
|
|
205
|
+
it 'builds maintenance fixtures from a later minor source template without broadening the release guard' do
|
|
206
|
+
Dir.mktmpdir('haveapi-later-minor-template-') do |template|
|
|
207
|
+
contents('0.32.0').each do |path, value|
|
|
208
|
+
FileUtils.mkdir_p(File.dirname(File.join(template, path)))
|
|
209
|
+
File.write(File.join(template, path), value)
|
|
210
|
+
end
|
|
211
|
+
allow(self).to receive(:root).and_return(template)
|
|
212
|
+
expect(described_class.capture(contents('0.30.2')).fetch('own_version')).to eq('0.30.2')
|
|
213
|
+
expect { described_class.capture(contents('0.32.0')) }.to raise_error(error, /Expected own 0\.30 patch version/)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
193
217
|
it 'rejects mismatched own constants, package names, constraints and minor versions' do
|
|
218
|
+
expect { described_class.capture(contents('0.31.0')) }.to raise_error(error, /Expected own 0\.30 patch version/)
|
|
194
219
|
[
|
|
195
220
|
['VERSION', "0.31.2\n"],
|
|
196
221
|
['VERSION', " 0.30.2\n"],
|
|
@@ -211,7 +236,6 @@ describe HaveAPI::Performance::Release do
|
|
|
211
236
|
File.write(File.join(harness, 'servers/ruby/Gemfile.lock'), "controlled server lock\n")
|
|
212
237
|
FileUtils.mkdir_p(File.join(harness, 'clients/rust'))
|
|
213
238
|
File.write(File.join(harness, 'clients/rust/Gemfile.lock'), "controlled Rust lock\n")
|
|
214
|
-
stub_const('HaveAPI::RustClient::VERSION', HaveAPI::VERSION) unless defined?(HaveAPI::RustClient::VERSION)
|
|
215
239
|
stub_const('HaveAPI::Performance::ROOT', harness)
|
|
216
240
|
ordinary = JSON.parse(JSON.generate(HaveAPI::Performance::Worker.inputs(root, 'quick')))
|
|
217
241
|
preparation = ordinary.fetch('json_preparation')
|
|
@@ -220,10 +244,23 @@ describe HaveAPI::Performance::Release do
|
|
|
220
244
|
expect do
|
|
221
245
|
described_class.validate_json_preparation(ordinary, 'files_sha256' => preparation.fetch('raw_sha256'))
|
|
222
246
|
end.not_to raise_error
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
%w[HaveAPI::VERSION HaveAPI::Client::VERSION HaveAPI::RustClient::VERSION].each do |name|
|
|
248
|
+
stub_const(name, '0.30.2')
|
|
249
|
+
end
|
|
250
|
+
HaveAPI::Performance::BENCHMARK_INPUTS.each do |path|
|
|
251
|
+
FileUtils.mkdir_p(File.dirname(File.join(harness, path)))
|
|
252
|
+
FileUtils.cp(File.join(root, path), File.join(harness, path))
|
|
253
|
+
end
|
|
254
|
+
with_maintenance_source do |source|
|
|
255
|
+
captured = HaveAPI::Performance::Worker.inputs(source, 'quick', release: true)
|
|
256
|
+
expect(captured[:locks]).to have_key('clients/rust/Gemfile.lock')
|
|
257
|
+
expect(captured[:manifests]).to have_key('clients/rust/Gemfile')
|
|
258
|
+
expect(captured.fetch(:release_versions)).to eq(described_class.capture(contents('0.30.2')))
|
|
259
|
+
benchmark = JSON.parse(JSON.generate(HaveAPI::Performance::Worker.inputs(source, 'quick', release: true, benchmark: true)))
|
|
260
|
+
expect(benchmark.fetch('manifests')).not_to have_key('flake.lock')
|
|
261
|
+
expect(benchmark.fetch('contributor_flakes')).to have_key('flake.lock')
|
|
262
|
+
expect(described_class.validate_inputs(benchmark, { 'files_sha256' => benchmark.fetch('release_versions').fetch('raw_sha256') }, 'quick')).to eq('0.30.2')
|
|
263
|
+
end
|
|
227
264
|
Dir.mktmpdir('haveapi-release-inputs-') do |source|
|
|
228
265
|
contents('0.30.99').each do |path, value|
|
|
229
266
|
FileUtils.mkdir_p(File.dirname(File.join(source, path)))
|
|
@@ -340,7 +377,7 @@ describe HaveAPI::Performance::Release do
|
|
|
340
377
|
end
|
|
341
378
|
|
|
342
379
|
it 'normalizes the actual worker HTML outside timing and retains its raw bytes' do
|
|
343
|
-
own =
|
|
380
|
+
own = '0.30.2'
|
|
344
381
|
body = "<a href=\"https://webui.haveapi.org/v#{own}/#api\">Connect</a>#{framework_footer(own)}"
|
|
345
382
|
scenario = HaveAPI::Performance::DocumentationScenario.allocate
|
|
346
383
|
allow(scenario).to receive(:reset!)
|
|
@@ -348,13 +385,15 @@ describe HaveAPI::Performance::Release do
|
|
|
348
385
|
validate!: { http_status: 200, envelope_status: nil, body_bytes: body.bytesize,
|
|
349
386
|
body_sha256: Digest::SHA256.hexdigest(body), canonical_sha256: Digest::SHA256.hexdigest(body) },
|
|
350
387
|
counters: { operations: [{ queries: 0, authorizations: 0, executions: 0 }] })
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
388
|
+
with_maintenance_source do |source|
|
|
389
|
+
worker = HaveAPI::Performance::Worker.new('source_root' => source, 'release_versions' => true)
|
|
390
|
+
worker.instance_variable_set(:@current_workload, 'docs.html_resources')
|
|
391
|
+
result = worker.measure(scenario, 1)
|
|
392
|
+
output = result.fetch(:outputs).first
|
|
393
|
+
expect(output.fetch('comparison')).to include('own_version' => own)
|
|
394
|
+
expect(result.fetch(:bodies).values.first.fetch(:body)).to eq(body)
|
|
395
|
+
expect(output.fetch('canonical_sha256')).to eq(Digest::SHA256.hexdigest(described_class.html(body, output['comparison']['own_version'])))
|
|
396
|
+
end
|
|
358
397
|
end
|
|
359
398
|
|
|
360
399
|
it 'leaves lookalike attributes, other URLs, JSON and example code byte exact' do
|
|
@@ -3,6 +3,9 @@ require 'open3'
|
|
|
3
3
|
require 'tmpdir'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require_relative '../../benchmarks/endpoints'
|
|
6
|
+
require_relative '../../../../utils/performance_ci'
|
|
7
|
+
require_relative '../../../../clients/ruby/lib/haveapi/client/version'
|
|
8
|
+
require_relative '../../../../clients/rust/lib/haveapi/rust_client/version'
|
|
6
9
|
require_relative 'support/samples'
|
|
7
10
|
require_relative '../../test_support/performance_endpoint_fixture'
|
|
8
11
|
require_relative '../../test_support/documentation_performance_fixture'
|
|
@@ -109,6 +112,70 @@ describe HaveAPI::Performance::Worker do
|
|
|
109
112
|
end
|
|
110
113
|
end
|
|
111
114
|
|
|
115
|
+
it 'retains the single benchmark bundle and raw unused contributor flakes without library locks' do
|
|
116
|
+
root = File.expand_path('../../../..', __dir__)
|
|
117
|
+
inputs = JSON.parse(JSON.generate(described_class.inputs(root, 'quick', benchmark: true)))
|
|
118
|
+
tool = inputs.fetch('benchmark')
|
|
119
|
+
expect(tool.keys).to match_array(HaveAPI::Performance::BENCHMARK_INPUTS)
|
|
120
|
+
expect(inputs.fetch('locks')).to eq('servers/ruby/benchmarks/Gemfile.lock' => tool.fetch('servers/ruby/benchmarks/Gemfile.lock').fetch('sha256'))
|
|
121
|
+
expect(inputs.fetch('manifests')).to have_key('.ruby-version')
|
|
122
|
+
expect(inputs.fetch('manifests')).not_to have_key('flake.lock')
|
|
123
|
+
expect(inputs.fetch('contributor_flakes').keys).to match_array(%w[flake.nix flake.lock])
|
|
124
|
+
tool.each do |path, input|
|
|
125
|
+
expect(input.fetch('content')).to eq(File.read(File.join(root, path)))
|
|
126
|
+
expect(input.fetch('sha256')).to eq(Digest::SHA256.hexdigest(input.fetch('content')))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'rejects changed Ruby and unavailable calibrated dependencies before starting workers' do
|
|
131
|
+
samples, json = PerformanceSamples.fixture
|
|
132
|
+
Dir.mktmpdir('haveapi-runtime-guard-') do |directory|
|
|
133
|
+
budget = File.join(directory, 'budgets.json')
|
|
134
|
+
cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', samples['baseline_sha'],
|
|
135
|
+
'--candidate-root', directory, '--candidate-sha', samples['candidate_sha'],
|
|
136
|
+
'--budgets', budget, '--output', File.join(directory, 'result')])
|
|
137
|
+
cli.send(:parse)
|
|
138
|
+
File.write(budget, json)
|
|
139
|
+
allow(cli).to receive(:child)
|
|
140
|
+
expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError, 'Calibration Ruby mismatch')
|
|
141
|
+
budgets = JSON.parse(json)
|
|
142
|
+
budgets['environment']['weekly']['ruby'] = JSON.parse(JSON.generate(described_class.new({}).environment.fetch(:ruby)))
|
|
143
|
+
budgets['environment']['weekly']['gems'] = { 'haveapi-missing-calibrated-gem' => { 'version' => '99.0', 'platform' => 'ruby' } }
|
|
144
|
+
File.write(budget, JSON.generate(budgets))
|
|
145
|
+
expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError, /Missing calibrated dependency/)
|
|
146
|
+
expect(cli).not_to have_received(:child)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'limits the nonancestor runner exception to an exact quick push event pair' do
|
|
151
|
+
samples, = PerformanceSamples.fixture
|
|
152
|
+
Dir.mktmpdir('haveapi-push-guard-') do |directory|
|
|
153
|
+
event = File.join(directory, 'selection.json')
|
|
154
|
+
selection = samples.slice('baseline_sha', 'candidate_sha').merge('event_name' => 'push', 'profile' => 'quick',
|
|
155
|
+
'push_pair' => { 'event_name' => 'push', 'before' => samples['baseline_sha'], 'after' => samples['candidate_sha'] })
|
|
156
|
+
%w[valid changed missing weekly].each do |control|
|
|
157
|
+
value = Marshal.load(Marshal.dump(selection))
|
|
158
|
+
value['push_pair']['before'] = 'c' * 40 if control == 'changed'
|
|
159
|
+
value.delete('push_pair') if control == 'missing'
|
|
160
|
+
File.write(event, JSON.generate(value))
|
|
161
|
+
cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', samples['baseline_sha'],
|
|
162
|
+
'--candidate-root', directory, '--candidate-sha', samples['candidate_sha'],
|
|
163
|
+
'--profile', control == 'weekly' ? 'weekly' : 'quick', '--push-event', event,
|
|
164
|
+
'--budgets', '/unused', '--output', '/unused'])
|
|
165
|
+
cli.send(:parse)
|
|
166
|
+
allow(cli).to receive(:verify_runtime)
|
|
167
|
+
allow(HaveAPI::Performance).to receive(:gemspec)
|
|
168
|
+
allow(cli).to receive(:verify_source) { |role| samples['sources'].fetch(role) }
|
|
169
|
+
allow(cli).to receive(:ancestor?).and_return(false)
|
|
170
|
+
allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
|
|
171
|
+
allow(cli).to receive(:child).and_raise(HaveAPI::Performance::ContractError, 'pilot boundary')
|
|
172
|
+
expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError,
|
|
173
|
+
control == 'valid' ? 'pilot boundary' : /push|Push|Weekly/)
|
|
174
|
+
expect(cli).not_to have_received(:child) unless control == 'valid'
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
112
179
|
it 'validates every response after the timed loop' do
|
|
113
180
|
scenario = Class.new do
|
|
114
181
|
attr_reader :calls, :validations
|
|
@@ -252,6 +319,8 @@ describe HaveAPI::Performance::Worker do
|
|
|
252
319
|
'--candidate-root', '/controlled/candidate', '--candidate-sha', samples['candidate_sha'],
|
|
253
320
|
'--budgets', '/unused', '--output', '/unused'])
|
|
254
321
|
cli.send(:parse)
|
|
322
|
+
allow(cli).to receive(:verify_runtime)
|
|
323
|
+
allow(HaveAPI::Performance).to receive(:gemspec)
|
|
255
324
|
allow(cli).to receive(:verify_source) { |role| samples['sources'].fetch(role) }
|
|
256
325
|
allow(cli).to receive(:ancestor?).and_return(true)
|
|
257
326
|
allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
|
|
@@ -354,6 +423,8 @@ describe HaveAPI::Performance::Worker do
|
|
|
354
423
|
'--candidate-sha', samples['candidate_sha'], '--budgets', policy, '--output', output])
|
|
355
424
|
# Control source verification and worker replies, retaining the real run
|
|
356
425
|
# assembly, child protocol validation, direct comparator and report path.
|
|
426
|
+
allow(cli).to receive(:verify_runtime)
|
|
427
|
+
allow(HaveAPI::Performance).to receive(:gemspec)
|
|
357
428
|
allow(cli).to receive_messages(git: '', ancestor?: true)
|
|
358
429
|
allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
|
|
359
430
|
allow(cli).to receive(:verify_source) { |role| samples['sources'].fetch(role).transform_keys(&:to_sym) }
|
|
@@ -387,6 +458,9 @@ describe HaveAPI::Performance::Worker do
|
|
|
387
458
|
expect(cli.execute).to eq(0)
|
|
388
459
|
retained = JSON.parse(File.read(File.join(output, 'samples.json')))
|
|
389
460
|
expect(retained['sources']).to eq(samples['sources'])
|
|
461
|
+
saved_report = JSON.parse(File.read(File.join(output, 'report.json')))
|
|
462
|
+
expect(saved_report.dig('comparison', 'samples_sha256')).to eq(Digest::SHA256.file(File.join(output, 'samples.json')).hexdigest)
|
|
463
|
+
expect(saved_report['harness_sha256']).to eq(retained['harness_sha256'])
|
|
390
464
|
report = HaveAPI::Performance::Comparator.new(retained, budgets, budget_sha256: Digest::SHA256.hexdigest(budget_json)).compare
|
|
391
465
|
expect(report['status']).to eq('pass')
|
|
392
466
|
expect(requests.map(&:first).count('calibration-adjusted')).to eq(expensive ? 0 : 1)
|
|
@@ -463,14 +537,76 @@ describe HaveAPI::Performance::Worker do
|
|
|
463
537
|
cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', '0' * 40,
|
|
464
538
|
'--candidate-root', directory, '--candidate-sha', '1' * 40,
|
|
465
539
|
'--budgets', budget, '--output', missing])
|
|
540
|
+
allow(cli).to receive(:verify_runtime)
|
|
466
541
|
expect(cli.execute).to eq(3)
|
|
467
542
|
report = JSON.parse(File.read(File.join(missing, 'report.json')))
|
|
468
543
|
expect(report['error']['message']).to include('Source SHA must be full immutable SHA')
|
|
469
544
|
expect(report.values_at('baseline_sha', 'candidate_sha')).to eq(['0' * 40, '1' * 40])
|
|
545
|
+
expect(report).not_to have_key('harness_sha256')
|
|
546
|
+
expect(report.fetch('comparison')).not_to have_key('samples_sha256')
|
|
470
547
|
equal = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', 'a' * 40,
|
|
471
548
|
'--candidate-root', directory, '--candidate-sha', 'a' * 40,
|
|
472
549
|
'--budgets', budget, '--output', File.join(directory, 'equal')])
|
|
473
550
|
expect(equal.execute).to eq(3)
|
|
474
551
|
end
|
|
475
552
|
end
|
|
553
|
+
|
|
554
|
+
it 'checks declared Ruby and runtime requirements against installed tools including unloaded rake' do
|
|
555
|
+
campaign = HaveAPI::PerformanceCI::Campaign.new(repository: instance_double(HaveAPI::PerformanceCI::Repository),
|
|
556
|
+
selection: {}, evidence: '/unused', sources: '/unused')
|
|
557
|
+
spec = { 'ruby' => '>= 3.2', 'dependencies' => [['rake', 'runtime', '>= 13.0'], ['haveapi-client', 'runtime', '~> 0.30.0']] }
|
|
558
|
+
status = instance_double(Process::Status, success?: true)
|
|
559
|
+
allow(Open3).to receive(:capture3).and_return([JSON.generate(spec), '', status])
|
|
560
|
+
expect(campaign.send(:gemspec, '/source', 'servers/ruby/haveapi.gemspec')).to eq(spec)
|
|
561
|
+
changed = spec.merge('ruby' => '>= 99')
|
|
562
|
+
allow(Open3).to receive(:capture3).and_return([JSON.generate(changed), '', status])
|
|
563
|
+
expect { campaign.send(:gemspec, '/source', 'servers/ruby/haveapi.gemspec') }.to raise_error(HaveAPI::PerformanceCI::Error, /Benchmark Ruby/)
|
|
564
|
+
changed = spec.merge('dependencies' => [['rake', 'runtime', '>= 99']])
|
|
565
|
+
allow(Open3).to receive(:capture3).and_return([JSON.generate(changed), '', status])
|
|
566
|
+
expect { campaign.send(:gemspec, '/source', 'servers/ruby/haveapi.gemspec') }.to raise_error(HaveAPI::PerformanceCI::Error, /Benchmark dependencies/)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it 'rejects unsupported Ruby or gem requirements in either direct source before the first child' do
|
|
570
|
+
samples, = PerformanceSamples.fixture
|
|
571
|
+
Dir.mktmpdir('haveapi-source-requirements-') do |directory|
|
|
572
|
+
roots = %w[baseline candidate].to_h { |role| [role, File.join(directory, role)] }
|
|
573
|
+
paths = HaveAPI::Performance::Release::VERSIONS.map { |path, name| "#{path.split('/lib/').first}/#{name}.gemspec" }
|
|
574
|
+
valid = <<~RUBY
|
|
575
|
+
Gem::Specification.new do |spec|
|
|
576
|
+
spec.name = 'haveapi-fixture'
|
|
577
|
+
spec.version = '1.0.0'
|
|
578
|
+
spec.required_ruby_version = '>= 3.2'
|
|
579
|
+
spec.add_dependency 'rake', '>= 13.0'
|
|
580
|
+
end
|
|
581
|
+
RUBY
|
|
582
|
+
roots.each_value do |root|
|
|
583
|
+
paths.each do |path|
|
|
584
|
+
FileUtils.mkdir_p(File.dirname(File.join(root, path)))
|
|
585
|
+
File.write(File.join(root, path), valid)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
%w[baseline candidate].product(%w[valid ruby dependency]).each do |role, fault|
|
|
589
|
+
path = File.join(roots.fetch(role), paths.last)
|
|
590
|
+
text = case fault
|
|
591
|
+
when 'ruby' then valid.sub('>= 3.2', '>= 99')
|
|
592
|
+
when 'dependency' then valid.sub('>= 13.0', '>= 99')
|
|
593
|
+
else valid
|
|
594
|
+
end
|
|
595
|
+
File.write(path, text)
|
|
596
|
+
cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', roots['baseline'], '--baseline-sha', samples['baseline_sha'],
|
|
597
|
+
'--candidate-root', roots['candidate'], '--candidate-sha', samples['candidate_sha'],
|
|
598
|
+
'--budgets', '/unused', '--output', '/unused'])
|
|
599
|
+
cli.send(:parse)
|
|
600
|
+
allow(cli).to receive(:verify_runtime)
|
|
601
|
+
allow(cli).to receive(:verify_source) { |name| { root: roots.fetch(name), sha: samples.fetch("#{name}_sha") } }
|
|
602
|
+
allow(cli).to receive(:ancestor?).and_return(true)
|
|
603
|
+
allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
|
|
604
|
+
allow(cli).to receive(:child).and_raise(HaveAPI::Performance::ContractError, 'pilot boundary')
|
|
605
|
+
expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError,
|
|
606
|
+
fault == 'valid' ? 'pilot boundary' : /Benchmark Ruby|Benchmark dependencies/)
|
|
607
|
+
expect(cli).not_to have_received(:child) unless fault == 'valid'
|
|
608
|
+
File.write(path, valid)
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
end
|
|
476
612
|
end
|