haveapi 0.30.2 → 0.31.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.
@@ -3,6 +3,7 @@ require 'open3'
3
3
  require 'tmpdir'
4
4
  require 'fileutils'
5
5
  require_relative '../../benchmarks/endpoints'
6
+ require_relative '../../../../utils/performance_ci'
6
7
  require_relative 'support/samples'
7
8
  require_relative '../../test_support/performance_endpoint_fixture'
8
9
  require_relative '../../test_support/documentation_performance_fixture'
@@ -109,6 +110,70 @@ describe HaveAPI::Performance::Worker do
109
110
  end
110
111
  end
111
112
 
113
+ it 'retains the single benchmark bundle and raw unused contributor flakes without library locks' do
114
+ root = File.expand_path('../../../..', __dir__)
115
+ inputs = JSON.parse(JSON.generate(described_class.inputs(root, 'quick', benchmark: true)))
116
+ tool = inputs.fetch('benchmark')
117
+ expect(tool.keys).to match_array(HaveAPI::Performance::BENCHMARK_INPUTS)
118
+ expect(inputs.fetch('locks')).to eq('servers/ruby/benchmarks/Gemfile.lock' => tool.fetch('servers/ruby/benchmarks/Gemfile.lock').fetch('sha256'))
119
+ expect(inputs.fetch('manifests')).to have_key('.ruby-version')
120
+ expect(inputs.fetch('manifests')).not_to have_key('flake.lock')
121
+ expect(inputs.fetch('contributor_flakes').keys).to match_array(%w[flake.nix flake.lock])
122
+ tool.each do |path, input|
123
+ expect(input.fetch('content')).to eq(File.read(File.join(root, path)))
124
+ expect(input.fetch('sha256')).to eq(Digest::SHA256.hexdigest(input.fetch('content')))
125
+ end
126
+ end
127
+
128
+ it 'rejects changed Ruby and unavailable calibrated dependencies before starting workers' do
129
+ samples, json = PerformanceSamples.fixture
130
+ Dir.mktmpdir('haveapi-runtime-guard-') do |directory|
131
+ budget = File.join(directory, 'budgets.json')
132
+ cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', samples['baseline_sha'],
133
+ '--candidate-root', directory, '--candidate-sha', samples['candidate_sha'],
134
+ '--budgets', budget, '--output', File.join(directory, 'result')])
135
+ cli.send(:parse)
136
+ File.write(budget, json)
137
+ allow(cli).to receive(:child)
138
+ expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError, 'Calibration Ruby mismatch')
139
+ budgets = JSON.parse(json)
140
+ budgets['environment']['weekly']['ruby'] = JSON.parse(JSON.generate(described_class.new({}).environment.fetch(:ruby)))
141
+ budgets['environment']['weekly']['gems'] = { 'haveapi-missing-calibrated-gem' => { 'version' => '99.0', 'platform' => 'ruby' } }
142
+ File.write(budget, JSON.generate(budgets))
143
+ expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError, /Missing calibrated dependency/)
144
+ expect(cli).not_to have_received(:child)
145
+ end
146
+ end
147
+
148
+ it 'limits the nonancestor runner exception to an exact quick push event pair' do
149
+ samples, = PerformanceSamples.fixture
150
+ Dir.mktmpdir('haveapi-push-guard-') do |directory|
151
+ event = File.join(directory, 'selection.json')
152
+ selection = samples.slice('baseline_sha', 'candidate_sha').merge('event_name' => 'push', 'profile' => 'quick',
153
+ 'push_pair' => { 'event_name' => 'push', 'before' => samples['baseline_sha'], 'after' => samples['candidate_sha'] })
154
+ %w[valid changed missing weekly].each do |control|
155
+ value = Marshal.load(Marshal.dump(selection))
156
+ value['push_pair']['before'] = 'c' * 40 if control == 'changed'
157
+ value.delete('push_pair') if control == 'missing'
158
+ File.write(event, JSON.generate(value))
159
+ cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', samples['baseline_sha'],
160
+ '--candidate-root', directory, '--candidate-sha', samples['candidate_sha'],
161
+ '--profile', control == 'weekly' ? 'weekly' : 'quick', '--push-event', event,
162
+ '--budgets', '/unused', '--output', '/unused'])
163
+ cli.send(:parse)
164
+ allow(cli).to receive(:verify_runtime)
165
+ allow(HaveAPI::Performance).to receive(:gemspec)
166
+ allow(cli).to receive(:verify_source) { |role| samples['sources'].fetch(role) }
167
+ allow(cli).to receive(:ancestor?).and_return(false)
168
+ allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
169
+ allow(cli).to receive(:child).and_raise(HaveAPI::Performance::ContractError, 'pilot boundary')
170
+ expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError,
171
+ control == 'valid' ? 'pilot boundary' : /push|Push|Weekly/)
172
+ expect(cli).not_to have_received(:child) unless control == 'valid'
173
+ end
174
+ end
175
+ end
176
+
112
177
  it 'validates every response after the timed loop' do
113
178
  scenario = Class.new do
114
179
  attr_reader :calls, :validations
@@ -252,6 +317,8 @@ describe HaveAPI::Performance::Worker do
252
317
  '--candidate-root', '/controlled/candidate', '--candidate-sha', samples['candidate_sha'],
253
318
  '--budgets', '/unused', '--output', '/unused'])
254
319
  cli.send(:parse)
320
+ allow(cli).to receive(:verify_runtime)
321
+ allow(HaveAPI::Performance).to receive(:gemspec)
255
322
  allow(cli).to receive(:verify_source) { |role| samples['sources'].fetch(role) }
256
323
  allow(cli).to receive(:ancestor?).and_return(true)
257
324
  allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
@@ -354,6 +421,8 @@ describe HaveAPI::Performance::Worker do
354
421
  '--candidate-sha', samples['candidate_sha'], '--budgets', policy, '--output', output])
355
422
  # Control source verification and worker replies, retaining the real run
356
423
  # assembly, child protocol validation, direct comparator and report path.
424
+ allow(cli).to receive(:verify_runtime)
425
+ allow(HaveAPI::Performance).to receive(:gemspec)
357
426
  allow(cli).to receive_messages(git: '', ancestor?: true)
358
427
  allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
359
428
  allow(cli).to receive(:verify_source) { |role| samples['sources'].fetch(role).transform_keys(&:to_sym) }
@@ -387,6 +456,9 @@ describe HaveAPI::Performance::Worker do
387
456
  expect(cli.execute).to eq(0)
388
457
  retained = JSON.parse(File.read(File.join(output, 'samples.json')))
389
458
  expect(retained['sources']).to eq(samples['sources'])
459
+ saved_report = JSON.parse(File.read(File.join(output, 'report.json')))
460
+ expect(saved_report.dig('comparison', 'samples_sha256')).to eq(Digest::SHA256.file(File.join(output, 'samples.json')).hexdigest)
461
+ expect(saved_report['harness_sha256']).to eq(retained['harness_sha256'])
390
462
  report = HaveAPI::Performance::Comparator.new(retained, budgets, budget_sha256: Digest::SHA256.hexdigest(budget_json)).compare
391
463
  expect(report['status']).to eq('pass')
392
464
  expect(requests.map(&:first).count('calibration-adjusted')).to eq(expensive ? 0 : 1)
@@ -463,14 +535,76 @@ describe HaveAPI::Performance::Worker do
463
535
  cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', '0' * 40,
464
536
  '--candidate-root', directory, '--candidate-sha', '1' * 40,
465
537
  '--budgets', budget, '--output', missing])
538
+ allow(cli).to receive(:verify_runtime)
466
539
  expect(cli.execute).to eq(3)
467
540
  report = JSON.parse(File.read(File.join(missing, 'report.json')))
468
541
  expect(report['error']['message']).to include('Source SHA must be full immutable SHA')
469
542
  expect(report.values_at('baseline_sha', 'candidate_sha')).to eq(['0' * 40, '1' * 40])
543
+ expect(report).not_to have_key('harness_sha256')
544
+ expect(report.fetch('comparison')).not_to have_key('samples_sha256')
470
545
  equal = HaveAPI::Performance::CLI.new(['run', '--baseline-root', directory, '--baseline-sha', 'a' * 40,
471
546
  '--candidate-root', directory, '--candidate-sha', 'a' * 40,
472
547
  '--budgets', budget, '--output', File.join(directory, 'equal')])
473
548
  expect(equal.execute).to eq(3)
474
549
  end
475
550
  end
551
+
552
+ it 'checks declared Ruby and runtime requirements against installed tools including unloaded rake' do
553
+ campaign = HaveAPI::PerformanceCI::Campaign.new(repository: instance_double(HaveAPI::PerformanceCI::Repository),
554
+ selection: {}, evidence: '/unused', sources: '/unused')
555
+ spec = { 'ruby' => '>= 3.2', 'dependencies' => [['rake', 'runtime', '>= 13.0'], ['haveapi-client', 'runtime', '~> 0.30.0']] }
556
+ status = instance_double(Process::Status, success?: true)
557
+ allow(Open3).to receive(:capture3).and_return([JSON.generate(spec), '', status])
558
+ expect(campaign.send(:gemspec, '/source', 'servers/ruby/haveapi.gemspec')).to eq(spec)
559
+ changed = spec.merge('ruby' => '>= 99')
560
+ allow(Open3).to receive(:capture3).and_return([JSON.generate(changed), '', status])
561
+ expect { campaign.send(:gemspec, '/source', 'servers/ruby/haveapi.gemspec') }.to raise_error(HaveAPI::PerformanceCI::Error, /Benchmark Ruby/)
562
+ changed = spec.merge('dependencies' => [['rake', 'runtime', '>= 99']])
563
+ allow(Open3).to receive(:capture3).and_return([JSON.generate(changed), '', status])
564
+ expect { campaign.send(:gemspec, '/source', 'servers/ruby/haveapi.gemspec') }.to raise_error(HaveAPI::PerformanceCI::Error, /Benchmark dependencies/)
565
+ end
566
+
567
+ it 'rejects unsupported Ruby or gem requirements in either direct source before the first child' do
568
+ samples, = PerformanceSamples.fixture
569
+ Dir.mktmpdir('haveapi-source-requirements-') do |directory|
570
+ roots = %w[baseline candidate].to_h { |role| [role, File.join(directory, role)] }
571
+ paths = HaveAPI::Performance::Release::VERSIONS.map { |path, name| "#{path.split('/lib/').first}/#{name}.gemspec" }
572
+ valid = <<~RUBY
573
+ Gem::Specification.new do |spec|
574
+ spec.name = 'haveapi-fixture'
575
+ spec.version = '1.0.0'
576
+ spec.required_ruby_version = '>= 3.2'
577
+ spec.add_dependency 'rake', '>= 13.0'
578
+ end
579
+ RUBY
580
+ roots.each_value do |root|
581
+ paths.each do |path|
582
+ FileUtils.mkdir_p(File.dirname(File.join(root, path)))
583
+ File.write(File.join(root, path), valid)
584
+ end
585
+ end
586
+ %w[baseline candidate].product(%w[valid ruby dependency]).each do |role, fault|
587
+ path = File.join(roots.fetch(role), paths.last)
588
+ text = case fault
589
+ when 'ruby' then valid.sub('>= 3.2', '>= 99')
590
+ when 'dependency' then valid.sub('>= 13.0', '>= 99')
591
+ else valid
592
+ end
593
+ File.write(path, text)
594
+ cli = HaveAPI::Performance::CLI.new(['run', '--baseline-root', roots['baseline'], '--baseline-sha', samples['baseline_sha'],
595
+ '--candidate-root', roots['candidate'], '--candidate-sha', samples['candidate_sha'],
596
+ '--budgets', '/unused', '--output', '/unused'])
597
+ cli.send(:parse)
598
+ allow(cli).to receive(:verify_runtime)
599
+ allow(cli).to receive(:verify_source) { |name| { root: roots.fetch(name), sha: samples.fetch("#{name}_sha") } }
600
+ allow(cli).to receive(:ancestor?).and_return(true)
601
+ allow(HaveAPI::Performance::Release).to receive(:lineage).and_return(nil)
602
+ allow(cli).to receive(:child).and_raise(HaveAPI::Performance::ContractError, 'pilot boundary')
603
+ expect { cli.send(:run_samples, 'f' * 64) }.to raise_error(HaveAPI::Performance::ContractError,
604
+ fault == 'valid' ? 'pilot boundary' : /Benchmark Ruby|Benchmark dependencies/)
605
+ expect(cli).not_to have_received(:child) unless fault == 'valid'
606
+ File.write(path, valid)
607
+ end
608
+ end
609
+ end
476
610
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.2
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.30.2
32
+ version: 0.31.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.30.2
39
+ version: 0.31.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: i18n
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -210,10 +210,13 @@ files:
210
210
  - LICENSE.txt
211
211
  - README.md
212
212
  - Rakefile
213
+ - benchmarks/Gemfile
214
+ - benchmarks/Gemfile.lock
213
215
  - benchmarks/documentation.rb
214
216
  - benchmarks/endpoints.rb
215
217
  - benchmarks/performance_budgets.json
216
218
  - benchmarks/performance_compare.rb
219
+ - benchmarks/shell.nix
217
220
  - doc/client-examples.md
218
221
  - doc/create-client.md
219
222
  - doc/hooks.erb
@@ -259,6 +262,7 @@ files:
259
262
  - lib/haveapi/client_examples/rust_client.rb
260
263
  - lib/haveapi/client_examples/ts_async_client.rb
261
264
  - lib/haveapi/client_examples/ts_client.rb
265
+ - lib/haveapi/client_examples/ts_generated_client.rb
262
266
  - lib/haveapi/common.rb
263
267
  - lib/haveapi/context.rb
264
268
  - lib/haveapi/documentation_clients.rb
@@ -366,6 +370,8 @@ files:
366
370
  - spec/client_examples/rust_client_spec.rb
367
371
  - spec/client_examples/structural_schema_spec.rb
368
372
  - spec/client_examples/syntax_spec.rb
373
+ - spec/client_examples/ts_generated_client_spec.rb
374
+ - spec/client_examples/typescript_profiles_spec.rb
369
375
  - spec/common_spec.rb
370
376
  - spec/documentation/auth_filtering_spec.rb
371
377
  - spec/documentation/client_examples_spec.rb