haveapi 0.31.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92be099fb88d7c03896cb0d40205830260f93959987227c2d793a31012e6917f
4
- data.tar.gz: 06c2e32b9c92d68ff32cfbe5f43ae9c22fb360a0d8bfb365ead9a48080001348
3
+ metadata.gz: 61839695eb2b83a98b003c909eeee8de4dd571cdf4b17a519f3ae4519dfcd4cf
4
+ data.tar.gz: 98f3dbf9483db43feba421bd33a24a3efe1951228dd3cad41b1219e9e881661a
5
5
  SHA512:
6
- metadata.gz: d3cb01c43beac90a8a095eded1edb417a32090ef7149fcd65da86d6d31da0cb54a2eb79f57e2ed2a456f5fd9f1d5bc7916551e88d0856b5d09fb5a1b0e0bba95
7
- data.tar.gz: 610e4b8bc84dc86b6a03db3bcfac4bdedf8512482e76cfe2b3b8013cfc33fdb4367055cc546ada82ac1153342c742cbfe1f42b7efb466a3239aab5595352f5b3
6
+ metadata.gz: d01b884188cbed69ce4078ba790912ec7de535d30fc9647515288f17447375fb46778c142b0d3546ebd078d9c169a17a352668dec0a021e399c46cefecd0e3b7
7
+ data.tar.gz: 9587c018e899a4d0fd3a320f38b1fddf02f2cdde37c845bf16e6e6052ad88f67e0d4020c5267f9a940f92f25bf941ac9225579c327d509b24e768336a4ba868a
@@ -386,6 +386,7 @@ module HaveAPI
386
386
  paths = %w[Gemfile flake.lock servers/ruby/Gemfile servers/ruby/haveapi.gemspec clients/ruby/Gemfile clients/ruby/haveapi-client.gemspec]
387
387
  paths += %w[clients/rust/Gemfile clients/rust/haveapi-rust-client.gemspec]
388
388
  paths += %w[.ruby-version flake.nix] if benchmark
389
+ paths |= SourceVersions::PATHS unless release
389
390
  raw_manifests = paths.to_h do |path|
390
391
  target = File.join(source, path)
391
392
  raise ContractError, "Missing dependency input #{target}" unless File.file?(target)
@@ -410,7 +411,12 @@ module HaveAPI
410
411
 
411
412
  result = { manifests: receipt.fetch('normalized_sha256'), locks:, release_versions: receipt }
412
413
  else
413
- result = { manifests:, locks:, json_preparation: preparation }
414
+ versions = SourceVersions.capture(raw_manifests.slice(*SourceVersions::PATHS))
415
+ loaded = [HaveAPI::VERSION, HaveAPI::Client::VERSION, HaveAPI::RustClient::VERSION]
416
+ raise ContractError, 'Wrong loaded local package version' unless loaded.all? { |version| version == versions.fetch('own_version') }
417
+
418
+ result = { manifests: manifests.merge(versions.fetch('normalized_sha256')), locks:,
419
+ json_preparation: preparation, source_versions: versions }
414
420
  end
415
421
  if benchmark
416
422
  result[:manifests] = result.fetch(:manifests).except('flake.nix', 'flake.lock')
@@ -697,13 +703,7 @@ module HaveAPI
697
703
  end
698
704
 
699
705
  inputs = result.fetch('inputs')
700
- if @release_lineage
701
- Release.validate_inputs(inputs, @sources.fetch(role), @options.fetch(:profile))
702
- elsif inputs.has_key?('release_versions')
703
- raise ContractError, 'Unproven own-version normalization'
704
- else
705
- Release.validate_json_preparation(inputs, @sources.fetch(role))
706
- end
706
+ Performance.validate_input_receipts(inputs, @sources.fetch(role), profile: @options.fetch(:profile), lineage: @release_lineage)
707
707
  expected = Identity.parse(File.read(@options.fetch(:budgets))).fetch('environment').fetch('weekly')
708
708
  raise ContractError, 'Calibration environment mismatch' unless result.fetch('environment') == expected
709
709
 
@@ -38,7 +38,7 @@ module HaveAPI
38
38
  require 'json'
39
39
  spec = Gem::Specification.load(File.basename(ARGV.fetch(0)))
40
40
  abort 'Cannot load gemspec' unless spec
41
- puts JSON.generate(ruby: spec.required_ruby_version.to_s,
41
+ puts JSON.generate(name: spec.name, version: spec.version.to_s, ruby: spec.required_ruby_version.to_s,
42
42
  dependencies: spec.dependencies.map { |d| [d.name, d.type.to_s, d.requirement.to_s] }.sort)
43
43
  RUBY
44
44
  environment = (ENV.keys.grep(/\ABUNDLE/) + %w[RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS]).to_h { |key| [key, nil] }
@@ -209,7 +209,7 @@ module HaveAPI
209
209
  end
210
210
 
211
211
  def self.graph(inputs)
212
- inputs.except('release_versions', 'json_preparation', 'contributor_flakes')
212
+ inputs.except('release_versions', 'source_versions', 'json_preparation', 'contributor_flakes')
213
213
  end
214
214
 
215
215
  def self.html(body, own)
@@ -238,6 +238,103 @@ module HaveAPI
238
238
  end
239
239
  end
240
240
 
241
+ # Ordinary source ancestry needs no historical maintenance certificate.
242
+ # Only synchronized package metadata is omitted from input equality.
243
+ module SourceVersions
244
+ PLACEHOLDER = 'OWN.VERSION'.freeze
245
+ COMPONENTS = Release::VERSIONS
246
+ SPECS = COMPONENTS.to_h { |path, name| ["#{path.split('/lib/').first}/#{name}.gemspec", name] }.freeze
247
+ PATHS = (['VERSION'] + COMPONENTS.keys + SPECS.keys).freeze
248
+
249
+ def self.capture(contents)
250
+ raise ContractError, 'Incomplete own source metadata' unless contents.keys.sort == PATHS.sort
251
+
252
+ own = contents.fetch('VERSION').delete_suffix("\n")
253
+ unless own.match?(/\A(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\z/) && contents['VERSION'] == "#{own}\n"
254
+ raise ContractError, 'Invalid own source VERSION'
255
+ end
256
+
257
+ normalized = contents.merge('VERSION' => "#{PLACEHOLDER}\n")
258
+ COMPONENTS.each do |path, name|
259
+ text = contents.fetch(path)
260
+ declaration = /^(\s*VERSION = )'#{Regexp.escape(own)}'(?=\.freeze$)/
261
+ unless text.scan(/^\s*VERSION\s*=/).length == 1 && text.scan(declaration).length == 1
262
+ raise ContractError, "Unsynchronized own version: #{name}"
263
+ end
264
+
265
+ normalized[path] = text.sub(declaration, "\\1'#{PLACEHOLDER}'")
266
+ end
267
+ SPECS.each do |path, name|
268
+ text = contents.fetch(path)
269
+ constant = { 'haveapi' => 'HaveAPI', 'haveapi-client' => 'HaveAPI::Client', 'haveapi-rust-client' => 'HaveAPI::RustClient' }.fetch(name)
270
+ unless text.scan(/\.name\s*=/).length == 1 && text.scan(/\.name\s*= '#{Regexp.escape(name)}'$/).length == 1 &&
271
+ text.scan(/\.version\s*=/).length == 1 && text.scan(/\.version\s*= #{Regexp.escape(constant)}::VERSION$/).length == 1
272
+ raise ContractError, 'Mismatched own package identity'
273
+ end
274
+
275
+ dependencies = text.scan(/\.add_(?:runtime_|development_)?dependency\s+['"]haveapi-client['"]/)
276
+ requirement = /(?<=\.add_dependency )'haveapi-client', '~> #{Regexp.escape(own)}'(?=$)/
277
+ if name == 'haveapi-client'
278
+ raise ContractError, 'Unexpected own client dependency' unless dependencies.empty?
279
+ else
280
+ unless dependencies.length == 1 && text.scan(requirement).length == 1
281
+ raise ContractError, 'Unsynchronized own client constraint'
282
+ end
283
+
284
+ normalized[path] = text.sub(requirement, "'haveapi-client', '~> #{PLACEHOLDER}'")
285
+ end
286
+ end
287
+ { 'contents' => contents, 'own_version' => own,
288
+ 'raw_sha256' => contents.transform_values { |text| Digest::SHA256.hexdigest(text) },
289
+ 'normalized_sha256' => normalized.transform_values { |text| Digest::SHA256.hexdigest(text) } }
290
+ end
291
+
292
+ def self.gemspec(root, path, receipt)
293
+ spec = Performance.gemspec(root, path)
294
+ own = receipt.fetch('own_version')
295
+ unless spec.fetch('name') == SPECS.fetch(path) && spec.fetch('version') == own
296
+ raise ContractError, 'Evaluated own package identity mismatch'
297
+ end
298
+
299
+ dependencies = spec.fetch('dependencies').select { |name, _type, _requirement| name == 'haveapi-client' }
300
+ expected = SPECS.fetch(path) == 'haveapi-client' ? [] : [['haveapi-client', 'runtime', "~> #{own}"]]
301
+ raise ContractError, 'Evaluated own client constraint mismatch' unless dependencies == expected
302
+
303
+ spec.merge('version' => PLACEHOLDER, 'dependencies' => spec.fetch('dependencies').map do |name, type, requirement|
304
+ [name, type, name == 'haveapi-client' ? "~> #{PLACEHOLDER}" : requirement]
305
+ end)
306
+ end
307
+
308
+ def self.validate_inputs(inputs, source)
309
+ receipt = inputs.fetch('source_versions')
310
+ raise ContractError, 'Own source metadata normalization mismatch' unless receipt == capture(receipt.fetch('contents'))
311
+
312
+ receipt.fetch('raw_sha256').each do |path, digest|
313
+ unless source.fetch('files_sha256').fetch(path) == digest && inputs.fetch('manifests').fetch(path) == receipt.fetch('normalized_sha256').fetch(path)
314
+ raise ContractError, 'Own source metadata escaped verified source'
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ def self.validate_input_receipts(inputs, source, profile:, lineage:)
321
+ if lineage
322
+ raise ContractError, 'Conflicting own-version receipts' if inputs.has_key?('source_versions')
323
+
324
+ Release.validate_inputs(inputs, source, profile)
325
+ else
326
+ raise ContractError, 'Unproven own-version normalization' if inputs.has_key?('release_versions')
327
+
328
+ Release.validate_json_preparation(inputs, source)
329
+ SourceVersions.validate_inputs(inputs, source) if inputs.has_key?('source_versions')
330
+ inputs.fetch('manifests').each do |path, digest|
331
+ next if Release::JSON_GEMFILES.include?(path) || (inputs.has_key?('source_versions') && SourceVersions::PATHS.include?(path))
332
+
333
+ raise ContractError, 'Dependency manifest escaped source' unless source.fetch('files_sha256').fetch(path) == digest
334
+ end
335
+ end
336
+ end
337
+
241
338
  def self.validate_source_pair(profile:, baseline_sha:, candidate_sha:)
242
339
  raise ContractError, 'Baseline and candidate must differ' if baseline_sha == candidate_sha
243
340
  return unless profile == 'weekly' && baseline_sha != REFERENCE
@@ -430,21 +527,10 @@ module HaveAPI
430
527
  raise ContractError, 'Missing contributor flake provenance'
431
528
  end
432
529
 
433
- inputs.fetch('manifests').each do |path, digest|
434
- next if Release::JSON_GEMFILES.include?(path) || lineage
435
-
436
- raise ContractError, 'Dependency manifest escaped source' unless selected.fetch('files_sha256').fetch(path) == digest
437
- end
438
530
  elsif inputs.has_key?('contributor_flakes')
439
531
  raise ContractError, 'Contributor flake separation without benchmark inputs'
440
532
  end
441
- if lineage
442
- Release.validate_inputs(inputs, selected, @samples.fetch('profile'))
443
- elsif inputs.has_key?('release_versions')
444
- raise ContractError, 'Unproven own-version normalization'
445
- else
446
- Release.validate_json_preparation(inputs, selected)
447
- end
533
+ Performance.validate_input_receipts(inputs, selected, profile: @samples.fetch('profile'), lineage:)
448
534
  unless observation.fetch('environment') == @samples['environment'] && Release.graph(inputs) == @samples.fetch('inputs') &&
449
535
  observation.fetch('fixture_sha256') == @samples['fixture_sha256']
450
536
  raise ContractError, 'Worker provenance mismatch'
@@ -0,0 +1,216 @@
1
+ # Run unbundled in the locked benchmark shell; optional generators are required.
2
+ # One shared mounted fixture excludes setup from the per-render measurements.
3
+ # rubocop:disable RSpec/InstanceVariable, RSpec/LeakyLocalVariable
4
+ require_relative '../performance_compare'
5
+ root = File.expand_path('../../../..', __dir__)
6
+ %w[servers/ruby clients/ruby clients/go clients/rust clients/typescript].each do |component|
7
+ $:.unshift(File.join(root, component, 'lib'))
8
+ end
9
+ require 'haveapi'
10
+ require 'rack/mock'
11
+ require 'base64'
12
+ require 'cgi'
13
+ require_relative '../../test_support/all_client_performance_fixture'
14
+
15
+ RSpec.configure { |config| config.fail_if_no_examples = true }
16
+
17
+ RSpec.describe HaveAPI::ClientExample, '.render' do
18
+ # Each mode has its own registry entry. New entries need reviewed expectations
19
+ # and a real supported profile here, rather than silently missing measurement.
20
+ profiles = {
21
+ '0' => nil, '2' => nil, '3' => nil, '5' => nil, '6' => nil,
22
+ 'javascript-callback' => nil, 'javascript-async' => nil,
23
+ 'typescript-callback' => nil, 'typescript-async' => nil,
24
+ 'python-sync' => nil, 'python-async' => nil,
25
+ 'go' => :go, 'rust-async' => :rust, 'rust-blocking' => :rust,
26
+ 'typescript-generated' => :typescript
27
+ }.freeze
28
+ lanes = %i[init basic token oauth2 public protected failure].freeze
29
+ let(:matrix) { profiles }
30
+
31
+ before(:all) do
32
+ @server = HaveAPI::AllClientPerformanceFixture.build(resources: 2)
33
+ @profiles = @server.documentation_client_profiles(1)
34
+ @description = description(@server, 'admin')
35
+ @large_description = Marshal.load(Marshal.dump(@description))
36
+ unrelated = @large_description.fetch(:resources).fetch('resource1')
37
+ unrelated[:resources] = 128.times.to_h { |i| ["unrelated#{i}", Marshal.load(Marshal.dump(@description[:resources]['resource1']))] }
38
+ @measurements = []
39
+ end
40
+
41
+ after(:all) do
42
+ RSpec.configuration.reporter.message("ALL_CLIENT_PERFORMANCE #{JSON.generate(registry: profiles.keys.sort, lanes:, measurements: @measurements)}")
43
+ end
44
+
45
+ def headers(user)
46
+ { 'HTTP_HOST' => 'example.test', 'HTTP_ACCEPT_LANGUAGE' => 'en',
47
+ 'HTTP_AUTHORIZATION' => "Basic #{Base64.strict_encode64("#{user}:fixture-only")}" }
48
+ end
49
+
50
+ def description(server, user)
51
+ server.describe_version(HaveAPI::Context.new(server, version: 1,
52
+ user: HaveAPI::DocumentationPerformanceFixture::User.new(user), params: {}))
53
+ end
54
+
55
+ def context(description, profile, action_name: 'protected', sample_index: 0, resource_name: 'resource0')
56
+ resource = description.fetch(:resources).fetch(resource_name)
57
+ action = resource.fetch(:actions).fetch(action_name)
58
+ HaveAPI::ClientExample::Context.new(version: 1, host: 'example.test',
59
+ base_url: 'http://example.test/benchmark/api/',
60
+ version_url: 'http://example.test/benchmark/api/v1/',
61
+ description:, profile:, resource_path: [resource_name.to_s],
62
+ resource:, action_name:, action:, sample: action.fetch(:examples).fetch(sample_index))
63
+ end
64
+
65
+ def render(client, lane, ctx)
66
+ if %i[basic token oauth2].include?(lane)
67
+ client.render(:auth, context: ctx, auth_method: lane, auth_description: ctx.description.fetch(:authentication).fetch(lane))
68
+ else
69
+ client.render(lane == :init ? :init : :example, context: ctx)
70
+ end
71
+ end
72
+
73
+ def expect_result(result, id, lane)
74
+ expect(result).to be_a(Hash), "#{id}/#{lane} returned no panel"
75
+ if id == 'typescript-generated' && lane == :oauth2
76
+ expect(result[:status]).to eq(:limitation)
77
+ expect(result[:reason]).to be_a(String)
78
+ expect(result[:reason]).not_to be_empty
79
+ else
80
+ expect(result[:status]).to eq(:code), "#{id}/#{lane}: #{result.inspect}"
81
+ expect(result[:code]).to be_a(String)
82
+ expect(result[:code]).not_to be_empty
83
+ if %w[0 3].include?(id) && lane == :oauth2
84
+ expect(result[:code]).to include('OAuth2 is not supported')
85
+ end
86
+ end
87
+ end
88
+
89
+ def allocations(count: 8, &block)
90
+ 2.times(&block)
91
+ Array.new(3) do
92
+ before = GC.stat(:total_allocated_objects)
93
+ count.times(&block)
94
+ (GC.stat(:total_allocated_objects) - before) / count.to_f
95
+ end.min
96
+ end
97
+
98
+ def calls(&block)
99
+ count = 0
100
+ trace = TracePoint.new(:call) { count += 1 }
101
+ trace.enable(&block)
102
+ count
103
+ ensure
104
+ trace&.disable
105
+ end
106
+
107
+ it 'requires exactly the reviewed registry and all real profiles' do
108
+ ids = described_class.clients.map(&:id)
109
+ expect(ids.sort).to eq(profiles.keys.sort)
110
+ expect(ids.uniq).to eq(ids)
111
+ expect(@profiles.keys.sort).to eq(%i[go rust typescript])
112
+ expect(@description.fetch(:authentication).keys.sort).to eq(%i[basic oauth2 token])
113
+ end
114
+
115
+ profiles.each do |id, language|
116
+ lanes.each do |lane|
117
+ it "bounds #{id}/#{lane} work and allocations with unrelated descendants" do
118
+ client = described_class.clients.find { |item| item.id == id }
119
+ expect(client).not_to be_nil
120
+ profile = language && @profiles.fetch(language)
121
+ options = { action_name: lane == :public ? 'public' : 'protected', sample_index: lane == :failure ? 1 : 0 }
122
+ small = context(@description, profile, **options)
123
+ large = context(@large_description, profile, **options)
124
+ expect(client.available?(small)).to be(true)
125
+ expect(client.available?(large)).to be(true)
126
+ before = Marshal.dump([small.description, large.description])
127
+ expected = render(client, lane, small)
128
+ expect_result(expected, id, lane)
129
+ expect(render(client, lane, large)).to eq(expected)
130
+ small_calls = calls { render(client, lane, small) }
131
+ large_calls = calls { render(client, lane, large) }
132
+ small_allocations = allocations { render(client, lane, small) }
133
+ large_allocations = allocations { render(client, lane, large) }
134
+ @measurements << { id:, lane:, allocations: [small_allocations, large_allocations], calls: [small_calls, large_calls] }
135
+ # Selected action/provider/sample is unchanged. Allow fixed VM/lookup
136
+ # slack, while recursive walks of the 128 unrelated subtrees must fail.
137
+ expect(large_calls).to be <= small_calls + 32
138
+ expect(large_allocations).to be <= small_allocations + 32
139
+ expect(Marshal.dump([small.description, large.description])).to eq(before)
140
+ end
141
+ end
142
+ end
143
+
144
+ def expect_panel(body, anchor, result)
145
+ panel = body.match(%r{<div id="#{Regexp.escape(anchor)}">\s*(.*?)</div>}m)
146
+ expect(panel).not_to be_nil, "Missing #{anchor}"
147
+ text = result.fetch(result[:status] == :code ? :code : :reason)
148
+ expect(panel[1]).to include(CGI.escapeHTML(text))
149
+ expect(panel[1]).to include(result[:status] == :code ? '<pre><code' : 'client-limitation')
150
+ end
151
+
152
+ def expect_complete_page(server, user, resources)
153
+ desc = description(server, user)
154
+ response = Rack::MockRequest.new(server.app).get('/benchmark/api/v1/', **headers(user))
155
+ expect(response.status).to eq(200)
156
+ expect(response['content-type']).to start_with('text/html')
157
+ expect(response.body).to include('PUBLIC_SAMPLE')
158
+ %w[ADMIN_ONLY_ACTION ADMIN_ONLY_DEFAULT ADMIN_ONLY_REQUEST ADMIN_ONLY_RESPONSE ADMIN_ONLY_ACTION_SAMPLE admin_field admin_only].each do |marker|
159
+ expect(response.body.include?(marker)).to eq(user == 'admin'), marker
160
+ end
161
+ example_count = 0
162
+ HaveAPI::ClientExample.clients.each do |client|
163
+ language = matrix.fetch(client.id)
164
+ profile = language && server.documentation_client_profiles(1).fetch(language)
165
+ ctx = context(desc, profile)
166
+ expect(client.available?(ctx)).to be(true)
167
+ expect_panel(response.body, "init-#{client.id}", render(client, :init, ctx))
168
+ desc.fetch(:authentication).each_key do |method|
169
+ result = render(client, method, ctx)
170
+ expect_result(result, client.id, method)
171
+ expect_panel(response.body, "auth-#{method}-#{client.id}", result)
172
+ end
173
+ resources.times do |index|
174
+ resource_name = "resource#{index}"
175
+ desc.fetch(:resources).fetch(resource_name).fetch(:actions).each do |action_name, action|
176
+ action.fetch(:examples).each_index do |sample_index|
177
+ selected = context(desc, profile, action_name: action_name.to_s, sample_index:, resource_name:)
178
+ result = client.render(:example, context: selected)
179
+ expect_result(result, client.id, :example)
180
+ expect_panel(response.body, "example-#{resource_name}-#{action_name}-#{sample_index}-#{client.id}", result)
181
+ example_count += 1
182
+ end
183
+ end
184
+ end
185
+ end
186
+ expected_samples = (resources * 2 * (user == 'admin' ? 3 : 2)) + (user == 'admin' ? 1 : 0)
187
+ expect(example_count).to eq(expected_samples * matrix.length)
188
+ response
189
+ end
190
+
191
+ it 'renders every real panel and isolates admin/member/admin pages through the same adapters' do
192
+ first = expect_complete_page(@server, 'admin', 2)
193
+ expect_complete_page(@server, 'member', 2)
194
+ last = expect_complete_page(@server, 'admin', 2)
195
+ expect(last.body).to eq(first.body)
196
+ end
197
+
198
+ it 'bounds complete all-profile page allocations by linear output growth' do
199
+ measurements = [2, 4, 8].map do |resources|
200
+ server = HaveAPI::AllClientPerformanceFixture.build(resources:)
201
+ response = expect_complete_page(server, 'admin', resources)
202
+ request = Rack::MockRequest.new(server.app)
203
+ allocated = allocations(count: 1) { request.get('/benchmark/api/v1/', **headers('admin')) }
204
+ { resources:, bytes: response.body.bytesize, allocations: allocated }
205
+ end
206
+ @measurements << { pages: measurements }
207
+ small, medium, large = measurements
208
+ expect(large[:bytes]).to be > medium[:bytes]
209
+ expect(medium[:bytes]).to be > small[:bytes]
210
+ expect(medium[:allocations]).to be > small[:allocations]
211
+ # Doubling the added resources doubles actual panels/output. A 2.5x
212
+ # incremental allowance covers bookkeeping; quadratic copying approaches 4x.
213
+ expect(large[:allocations] - medium[:allocations]).to be <= 2.5 * (medium[:allocations] - small[:allocations])
214
+ end
215
+ end
216
+ # rubocop:enable RSpec/InstanceVariable, RSpec/LeakyLocalVariable
@@ -247,6 +247,13 @@ They check description sharing, allocation growth and isolation between users
247
247
  without a wall-clock threshold. The Rust adapter suite separately checks that
248
248
  normalization avoids unrelated description descendants.
249
249
 
250
+ The additional required `make test-performance-clients` contract checks every
251
+ registered renderer and mode with real generated profiles, bounded per-render
252
+ work and allocations, and complete-page allocation scaling. It runs in the
253
+ locked, unbundled benchmark environment, separately from the ordinary server
254
+ bundle. See [performance testing](performance.md#all-client-documentation-contract)
255
+ for preparation, exact coverage and CI behavior.
256
+
250
257
  ### Documentation benchmark
251
258
 
252
259
  Prepare the server bundle in its declared Nix environment, then run the opt-in
data/doc/performance.md CHANGED
@@ -13,6 +13,56 @@ query behavior; they do not measure production database, network server or
13
13
  vpsAdmin latency. The checks detect specific regressions and do not guarantee
14
14
  every future performance characteristic.
15
15
 
16
+ ## All-client documentation contract
17
+
18
+ The required performance workflow first runs `make test-performance-clients`
19
+ against the selected candidate in the prepared, unbundled benchmark shell:
20
+
21
+ ```sh
22
+ export HAVEAPI_BENCHMARK_GEM_HOME="$PWD/servers/ruby/benchmarks/.gems"
23
+ nix-shell servers/ruby/benchmarks/shell.nix --run 'make test-performance-clients'
24
+ ```
25
+
26
+ Prepare the frozen benchmark bundle as described below before running this
27
+ command. It loads the actual Go, Rust and TypeScript generator sources and fails
28
+ if a dependency or profile is missing. It needs no Go, Rust or TypeScript
29
+ compiler. The ordinary `make test-performance` remains a separate server-bundle
30
+ check; both checks are required.
31
+
32
+ The explicit coverage matrix must equal the live renderer registry. Its 15
33
+ entries include every callback, async, blocking and generated variant. Each
34
+ entry renders initialization, Basic, token and OAuth2 authentication, public and
35
+ protected actions, and a failed action sample. Basic authentication and action
36
+ samples must produce code for every renderer. Generated TypeScript's OAuth2
37
+ limitation and the Ruby library/CLI's existing unsupported-OAuth2 comments have
38
+ explicit expectations. New renderer IDs or providers require reviewed coverage;
39
+ they cannot silently disappear from measurement.
40
+
41
+ Direct checks hold the selected action, sample and provider constant while adding
42
+ 128 unrelated resource subtrees. Setup and context ownership copying happen
43
+ outside measurement. Three batches of eight renders check allocations, and a
44
+ separate Ruby-call count checks traversal growth; each allows 32 additional
45
+ objects or calls per render. Output must remain identical and inputs unchanged.
46
+
47
+ Complete Rack HTML requests enable all three generated profiles. Checks compare
48
+ every registered initialization/authentication/action panel with real renderer
49
+ output, including successful and failed samples, and exercise admin/member/admin
50
+ isolation through the same adapters. Allocation measurements use pages with
51
+ 2, 4 and 8 resources, each with two actions and three admin-visible samples per
52
+ action, plus one admin-only action. Member pages hide the admin sample and action.
53
+ The added-resource allocation increment may grow by at most 2.5 times when the
54
+ resource increment doubles. This permits linear panel/output growth while
55
+ rejecting the measured whole-description-per-example copy regression. The
56
+ fixture uses scalar fields so Go action panels exercise supported code paths.
57
+
58
+ The log retains the named registry, kinds, allocations, Ruby-call counts and
59
+ page byte counts. These deterministic checks measure the selected candidate.
60
+ They use no wall-clock threshold and provide no historical timing result. The
61
+ paired timing suite below retains its original workloads, fixed reference and budgets; its
62
+ reference predates generated TypeScript support. Relevant pushes and pull
63
+ requests, manual runs and scheduled runs must pass the all-client contract before
64
+ the paired comparison. Changes under any client directory trigger the gate.
65
+
16
66
  ## Local commands
17
67
 
18
68
  The benchmark has its own third-party tool bundle and pinned Nix shell in
@@ -200,6 +250,20 @@ the original manifest contents, hashes and declaration evidence, then compare
200
250
  normalized contents. Replay verifies the transformation from the retained raw
201
251
  inputs. Other dependency requirements and manifest contents must match.
202
252
 
253
+ Comparisons outside the historical 0.30 maintenance mapping retain a
254
+ `source_versions` receipt for each source. It validates the numeric source
255
+ `VERSION`, matching Ruby server/client
256
+ and Rust generator version constants, package identities and exact synchronized
257
+ internal `haveapi-client` requirements before normalizing those version fields.
258
+ Preparation, live workers and replay bind the raw hashes to the verified source
259
+ and compare the normalized manifests. Missing, duplicate, inconsistent or tampered
260
+ metadata fails. All other manifest bytes, external dependencies, runtime and
261
+ benchmark locks remain part of the strict comparison. Older raw-manifest samples
262
+ still replay; removing a receipt from normalized samples fails source validation.
263
+ This rule permits coordinated package-version changes without changing baseline
264
+ selection or the separate historical 0.30 maintenance-lineage proof. Response
265
+ changes remain diagnostics under the existing per-revision stability policy.
266
+
203
267
  ## GitHub workflow and baseline selection
204
268
 
205
269
  The `servers/ruby performance` workflow runs on pushes, ordinary pull requests,
data/haveapi.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.31.0'
18
+ s.add_dependency 'haveapi-client', '~> 0.31.1'
19
19
  s.add_dependency 'i18n', '>= 1.6', '< 2'
20
20
  s.add_dependency 'json'
21
21
  s.add_dependency 'mail'
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
2
  PROTOCOL_VERSION = '2.0'.freeze
3
- VERSION = '0.31.0'.freeze
3
+ VERSION = '0.31.1'.freeze
4
4
  end
@@ -269,7 +269,7 @@ RSpec.describe HaveAPI::PerformanceCI do
269
269
  end
270
270
 
271
271
  it 'skips only irrelevant valid push/PR changes and still forces scheduled/manual work' do
272
- allow(repository).to receive(:paths).and_return(['clients/php/README.md'])
272
+ allow(repository).to receive(:paths).and_return(['LICENSE.txt'])
273
273
  event.merge!('ref' => 'refs/heads/master', 'after' => candidate, 'before' => base)
274
274
  expect(resolve('push', event)).to include(relevant: false)
275
275
  expect(resolve('schedule', event)).to include(relevant: true)
@@ -280,11 +280,38 @@ RSpec.describe HaveAPI::PerformanceCI do
280
280
  %w[flake.lock Gemfile Makefile .ruby-version servers/ruby/benchmarks/endpoints.rb
281
281
  servers/ruby/benchmarks/Gemfile servers/ruby/benchmarks/Gemfile.lock servers/ruby/benchmarks/shell.nix
282
282
  servers/ruby/test_support/performance_sqlite_fixture.rb clients/ruby/Gemfile clients/rust/Gemfile
283
- doc/performance.md utils/performance_ci.rb .github/workflows/servers-ruby-performance.yml].each do |path|
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|
284
287
  expect(described_class::RELEVANT).to match(path)
285
288
  end
286
289
  end
287
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
+
288
315
  it 'only accepts successful job completion in the aggregate gate' do
289
316
  expect(described_class.aggregate!('success', 'compared')).to be(true)
290
317
  expect(described_class.aggregate!('success', 'no-work')).to be(true)
@@ -4,6 +4,8 @@ require 'tmpdir'
4
4
  require 'fileutils'
5
5
  require_relative '../../benchmarks/endpoints'
6
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'
7
9
  require_relative 'support/samples'
8
10
  require_relative '../../test_support/performance_endpoint_fixture'
9
11
  require_relative '../../test_support/documentation_performance_fixture'
@@ -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.31.0
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.31.0
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.31.0
39
+ version: 0.31.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: i18n
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -217,6 +217,7 @@ files:
217
217
  - benchmarks/performance_budgets.json
218
218
  - benchmarks/performance_compare.rb
219
219
  - benchmarks/shell.nix
220
+ - benchmarks/spec/client_examples_spec.rb
220
221
  - doc/client-examples.md
221
222
  - doc/create-client.md
222
223
  - doc/hooks.erb
@@ -396,6 +397,7 @@ files:
396
397
  - spec/performance/json_activation_spec.rb
397
398
  - spec/performance/release_lineage_spec.rb
398
399
  - spec/performance/runner_spec.rb
400
+ - spec/performance/source_versions_spec.rb
399
401
  - spec/performance/sqlite_spec.rb
400
402
  - spec/performance/support/live_worker.rb
401
403
  - spec/performance/support/samples.rb
@@ -412,6 +414,7 @@ files:
412
414
  - spec/validators/length_spec.rb
413
415
  - spec/validators/numericality_spec.rb
414
416
  - spec/validators/presence_spec.rb
417
+ - test_support/all_client_performance_fixture.rb
415
418
  - test_support/client_test_api.rb
416
419
  - test_support/client_test_server.rb
417
420
  - test_support/documentation_performance_fixture.rb