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.
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require_relative '../../../../utils/performance_ci'
5
+ require_relative '../../benchmarks/endpoints'
6
+ require_relative '../../../../clients/ruby/lib/haveapi/client/version'
7
+ require_relative '../../../../clients/rust/lib/haveapi/rust_client/version'
8
+ require_relative 'support/samples'
9
+
10
+ describe HaveAPI::PerformanceCI::Campaign do
11
+ let(:root) { File.expand_path('../../../..', __dir__) }
12
+ let(:error) { HaveAPI::Performance::ContractError }
13
+
14
+ before do
15
+ # These metadata fixtures evaluate real gemspecs without loading the optional
16
+ # Rust adapter. Ordinary server bundles do not include its dependencies; the
17
+ # real benchmark environment checks their installed identities separately.
18
+ allow(Gem::Specification).to receive(:find_by_name).and_call_original
19
+ { 'fiddle' => '1.1.8', 'tomlrb' => '2.0.5' }.each do |name, version|
20
+ specification = Gem::Specification.new do |spec|
21
+ spec.name = name
22
+ spec.version = version
23
+ end
24
+ allow(Gem::Specification).to receive(:find_by_name).with(name).and_return(specification)
25
+ end
26
+ end
27
+
28
+ def source_contents(version)
29
+ paths = HaveAPI::Performance::Release.paths('quick')
30
+ actual = File.read(File.join(root, 'VERSION')).strip
31
+ paths.to_h do |path|
32
+ text = File.read(File.join(root, path))
33
+ text = "#{version}\n" if path == 'VERSION'
34
+ text = text.sub("VERSION = '#{actual}'", "VERSION = '#{version}'") if HaveAPI::Performance::Release::VERSIONS.has_key?(path)
35
+ text = text.sub("'haveapi-client', '~> #{actual}'", "'haveapi-client', '~> #{version}'") if path.end_with?('.gemspec')
36
+ [path, text]
37
+ end
38
+ end
39
+
40
+ def write_source(directory, contents)
41
+ contents.each do |path, text|
42
+ FileUtils.mkdir_p(File.dirname(File.join(directory, path)))
43
+ File.write(File.join(directory, path), text)
44
+ end
45
+ end
46
+
47
+ def prepare(baseline, candidate)
48
+ Dir.mktmpdir('haveapi-own-versions-') do |directory|
49
+ roots = { baseline: File.join(directory, 'baseline'), candidate: File.join(directory, 'candidate') }
50
+ write_source(roots[:baseline], baseline)
51
+ write_source(roots[:candidate], candidate)
52
+ campaign = described_class.new(repository: nil, selection: { 'profile' => 'quick' }, evidence: directory, sources: directory)
53
+ campaign.send(:verify_dependencies, roots)
54
+ yield roots if block_given?
55
+ end
56
+ end
57
+
58
+ %w[0.30.0 0.31.0].each do |baseline|
59
+ it "accepts synchronized #{baseline} to 0.31.1 metadata during preparation" do
60
+ expect { prepare(source_contents(baseline), source_contents('0.31.1')) }.not_to raise_error
61
+ end
62
+
63
+ it "retains and validates the live worker graph for #{baseline} to 0.31.1" do
64
+ prepare(source_contents(baseline), source_contents('0.31.1')) do |roots|
65
+ graphs = roots.map do |role, directory|
66
+ version = role == :baseline ? baseline : '0.31.1'
67
+ %w[HaveAPI::VERSION HaveAPI::Client::VERSION HaveAPI::RustClient::VERSION].each { |constant| stub_const(constant, version) }
68
+ inputs = JSON.parse(JSON.generate(HaveAPI::Performance::Worker.inputs(directory, 'quick', benchmark: true)))
69
+ hashes = source_contents(version).transform_values { |text| Digest::SHA256.hexdigest(text) }
70
+ expect do
71
+ HaveAPI::Performance.validate_input_receipts(inputs, { 'files_sha256' => hashes }, profile: 'quick', lineage: nil)
72
+ end.not_to raise_error
73
+ HaveAPI::Performance::Release.graph(inputs)
74
+ end
75
+ expect(graphs.first).to eq(graphs.last)
76
+ end
77
+ end
78
+
79
+ it "replays the synchronized #{baseline} to 0.31.1 graph" do
80
+ samples, budgets = replay_samples(baseline)
81
+ expect(compare(samples, budgets).fetch('status')).to eq('pass')
82
+ end
83
+ end
84
+
85
+ def replay_samples(baseline = '0.31.0')
86
+ samples, budgets = PerformanceSamples.fixture
87
+ samples.fetch('sources').each do |role, source|
88
+ contents = source_contents(role == 'baseline' ? baseline : '0.31.1').slice(*HaveAPI::Performance::SourceVersions::PATHS)
89
+ receipt = HaveAPI::Performance::SourceVersions.capture(contents)
90
+ source['files_sha256'].merge!(receipt.fetch('raw_sha256'))
91
+ samples.fetch('observations').each_value do |observation|
92
+ next unless observation.fetch('source').fetch('root') == source.fetch('root')
93
+
94
+ inputs = Marshal.load(Marshal.dump(observation.fetch('inputs')))
95
+ inputs['manifests'].merge!(receipt.fetch('normalized_sha256'))
96
+ inputs['source_versions'] = receipt
97
+ observation['inputs'] = inputs
98
+ samples['inputs'] = HaveAPI::Performance::Release.graph(inputs)
99
+ end
100
+ end
101
+ [samples, JSON.parse(budgets)]
102
+ end
103
+
104
+ def compare(samples, budgets)
105
+ HaveAPI::Performance::Comparator.new(samples, budgets, budget_sha256: samples.fetch('budget_sha256')).compare
106
+ end
107
+
108
+ it 'rejects external dependency and other unnormalized input edits in preparation' do
109
+ %w[Gemfile .ruby-version servers/ruby/haveapi.gemspec].each do |path|
110
+ changed = source_contents('0.31.1')
111
+ changed[path] = path == '.ruby-version' ? "3.0.0\n" : "#{changed[path]}\n# unrelated change\n"
112
+ expect { prepare(source_contents('0.31.0'), changed) }.to raise_error(HaveAPI::PerformanceCI::Error, /Dependency inputs differ/)
113
+ end
114
+ changed = source_contents('0.31.1')
115
+ changed['servers/ruby/haveapi.gemspec'] = changed['servers/ruby/haveapi.gemspec'].sub("'activesupport', '>= 7.1'", "'activesupport', '>= 999.0'")
116
+ expect { prepare(source_contents('0.31.0'), changed) }.to raise_error(error, /Benchmark dependencies do not satisfy/)
117
+ end
118
+
119
+ it 'rejects malformed, missing, duplicate and unsynchronized own declarations' do
120
+ original = source_contents('0.31.1').slice(*HaveAPI::Performance::SourceVersions::PATHS)
121
+ changes = [
122
+ ['VERSION', "0.31.01\n"], ['VERSION', " 0.31.1\n"],
123
+ ['servers/ruby/lib/haveapi/version.rb', original['servers/ruby/lib/haveapi/version.rb'].sub('0.31.1', '0.31.0')],
124
+ ['servers/ruby/lib/haveapi/version.rb', "VERSION = '0.31.1'.freeze\nVERSION = '0.31.1'.freeze\n"],
125
+ ['servers/ruby/haveapi.gemspec', original['servers/ruby/haveapi.gemspec'].sub('~> 0.31.1', '~> 0.31.0')],
126
+ ['servers/ruby/haveapi.gemspec', original['servers/ruby/haveapi.gemspec'].sub("~> 0.31.1'", "~> 0.31.1', '< 0.32'")],
127
+ ['servers/ruby/haveapi.gemspec', "#{original['servers/ruby/haveapi.gemspec']}s.add_dependency 'haveapi-client', '~> 0.31.1'\n"],
128
+ ['servers/ruby/haveapi.gemspec', original['servers/ruby/haveapi.gemspec'].sub('HaveAPI::VERSION', "'0.31.0'")],
129
+ ['clients/ruby/haveapi-client.gemspec', original['clients/ruby/haveapi-client.gemspec'].sub("'haveapi-client'", "'other-client'")]
130
+ ]
131
+ changes.each do |path, text|
132
+ expect { HaveAPI::Performance::SourceVersions.capture(original.merge(path => text)) }.to raise_error(error)
133
+ end
134
+ expect { HaveAPI::Performance::SourceVersions.capture(original.except('VERSION')) }.to raise_error(error)
135
+ end
136
+
137
+ it 'rejects loaded versions that do not match the captured source metadata' do
138
+ Dir.mktmpdir('haveapi-own-worker-') do |directory|
139
+ write_source(directory, source_contents('0.31.99'))
140
+ expect { HaveAPI::Performance::Worker.inputs(directory, 'quick', benchmark: true) }.to raise_error(error, /Wrong loaded local package version/)
141
+ end
142
+ end
143
+
144
+ it 'rejects tampered, stripped, conflicting or unbound receipts and changed runtime/lock inputs on replay' do
145
+ %w[raw normalized content stripped conflicting source runtime lock].each do |fault|
146
+ samples, budgets = replay_samples
147
+ observation = samples.fetch('observations').fetch('candidate')
148
+ inputs = observation.fetch('inputs')
149
+ receipt = inputs.fetch('source_versions')
150
+ case fault
151
+ when 'raw' then receipt['raw_sha256']['VERSION'] = 'f' * 64
152
+ when 'normalized' then receipt['normalized_sha256']['VERSION'] = 'f' * 64
153
+ when 'content' then receipt['contents']['VERSION'] = "0.31.2\n"
154
+ when 'stripped' then inputs.delete('source_versions')
155
+ when 'conflicting' then inputs['release_versions'] = {}
156
+ when 'source' then samples['sources']['candidate']['files_sha256']['VERSION'] = 'f' * 64
157
+ when 'runtime' then observation['environment'] = { 'ruby' => 'changed' }
158
+ when 'lock' then inputs['locks']['Gemfile.lock'] = 'f' * 64
159
+ end
160
+ expect { compare(samples, budgets) }.to raise_error(error), fault
161
+ end
162
+ end
163
+
164
+ it 'continues to replay legacy raw manifests without own-version receipts' do
165
+ samples, budgets = PerformanceSamples.fixture
166
+ expect(compare(samples, JSON.parse(budgets)).fetch('status')).to eq('pass')
167
+ end
168
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'documentation_performance_fixture'
2
+
3
+ module HaveAPI
4
+ # Scalar samples deliberately stay inside every generator's supported subset.
5
+ # The historical timing fixture and its calibrated workloads stay separate.
6
+ module AllClientPerformanceFixture
7
+ class TokenConfig < Authentication::Token::Config
8
+ # Keep the saved JSON SDK description and live provider value identical.
9
+ query_parameter '_auth_token'
10
+ end
11
+
12
+ class OAuthConfig < Authentication::OAuth2::Config
13
+ def base_url
14
+ 'http://example.test'
15
+ end
16
+ end
17
+
18
+ def self.build(resources:)
19
+ definitions = Module.new
20
+ resources.times do |index|
21
+ resource = Class.new(HaveAPI::Resource)
22
+ definitions.const_set("Resource#{index}", resource)
23
+ resource.resource_name = "Resource#{index}"
24
+ resource.version 1
25
+ resource.route "resources#{index}"
26
+ %w[Public Protected].each { |name| add_action(resource, name) }
27
+ DocumentationPerformanceFixture.send(:add_admin_action, resource) if index == 0
28
+ end
29
+ server = HaveAPI::Server.new(definitions)
30
+ server.use_version(1)
31
+ server.default_version = 1
32
+ server.auth_chain << DocumentationPerformanceFixture::BasicProvider
33
+ server.auth_chain << Authentication::Token.with_config(TokenConfig)
34
+ server.auth_chain << Authentication::OAuth2.with_config(OAuthConfig)
35
+ server.documentation_clients(1,
36
+ go: { import_path: 'example.test/performance/client' },
37
+ rust: { crate_name: 'performance_client' },
38
+ typescript: { package_name: 'performance-client' })
39
+ server.mount('/benchmark/api/')
40
+ server
41
+ end
42
+
43
+ def self.add_action(resource, name)
44
+ resource.define_action(name) do
45
+ route name.downcase
46
+ http_method :post
47
+ auth name == 'Protected'
48
+ input(:hash) do
49
+ string :value
50
+ bool :enabled
51
+ integer :count
52
+ string :admin_field, default: 'ADMIN_ONLY_DEFAULT'
53
+ end
54
+ output(:hash) do
55
+ string :marker
56
+ string :admin_field
57
+ end
58
+ authorize do |user|
59
+ if user && user.login != 'admin'
60
+ input blacklist: [:admin_field]
61
+ output blacklist: [:admin_field]
62
+ end
63
+ allow
64
+ end
65
+ example 'Success' do
66
+ request(value: '', enabled: false, count: 0)
67
+ response(marker: 'PUBLIC_SAMPLE')
68
+ end
69
+ example 'Failure' do
70
+ request(value: '', enabled: false, count: 0)
71
+ status false
72
+ message 'Expected failure'
73
+ errors(value: ['fixture error'])
74
+ end
75
+ example 'Admin' do
76
+ authorize { |user| user.login == 'admin' }
77
+ request(admin_field: 'ADMIN_ONLY_REQUEST')
78
+ response(admin_field: 'ADMIN_ONLY_RESPONSE')
79
+ end
80
+ end
81
+ end
82
+ private_class_method :add_action
83
+ end
84
+ 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.1
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.1
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.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: i18n
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -210,10 +210,14 @@ 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
220
+ - benchmarks/spec/client_examples_spec.rb
217
221
  - doc/client-examples.md
218
222
  - doc/create-client.md
219
223
  - doc/hooks.erb
@@ -259,6 +263,7 @@ files:
259
263
  - lib/haveapi/client_examples/rust_client.rb
260
264
  - lib/haveapi/client_examples/ts_async_client.rb
261
265
  - lib/haveapi/client_examples/ts_client.rb
266
+ - lib/haveapi/client_examples/ts_generated_client.rb
262
267
  - lib/haveapi/common.rb
263
268
  - lib/haveapi/context.rb
264
269
  - lib/haveapi/documentation_clients.rb
@@ -366,6 +371,8 @@ files:
366
371
  - spec/client_examples/rust_client_spec.rb
367
372
  - spec/client_examples/structural_schema_spec.rb
368
373
  - spec/client_examples/syntax_spec.rb
374
+ - spec/client_examples/ts_generated_client_spec.rb
375
+ - spec/client_examples/typescript_profiles_spec.rb
369
376
  - spec/common_spec.rb
370
377
  - spec/documentation/auth_filtering_spec.rb
371
378
  - spec/documentation/client_examples_spec.rb
@@ -390,6 +397,7 @@ files:
390
397
  - spec/performance/json_activation_spec.rb
391
398
  - spec/performance/release_lineage_spec.rb
392
399
  - spec/performance/runner_spec.rb
400
+ - spec/performance/source_versions_spec.rb
393
401
  - spec/performance/sqlite_spec.rb
394
402
  - spec/performance/support/live_worker.rb
395
403
  - spec/performance/support/samples.rb
@@ -406,6 +414,7 @@ files:
406
414
  - spec/validators/length_spec.rb
407
415
  - spec/validators/numericality_spec.rb
408
416
  - spec/validators/presence_spec.rb
417
+ - test_support/all_client_performance_fixture.rb
409
418
  - test_support/client_test_api.rb
410
419
  - test_support/client_test_server.rb
411
420
  - test_support/documentation_performance_fixture.rb