inferno_core 1.4.5 → 1.4.6
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/lib/inferno/apps/web/router.rb +1 -1
- data/lib/inferno/config/boot/db.rb +0 -5
- data/lib/inferno/dsl/must_support_assessment.rb +12 -24
- data/lib/inferno/entities/test_kit.rb +4 -2
- data/lib/inferno/jobs/execute_test_run.rb +3 -0
- data/lib/inferno/jobs/resume_test_run.rb +4 -0
- data/lib/inferno/test_runner.rb +10 -7
- data/lib/inferno/version.rb +1 -1
- metadata +2 -3
- data/lib/inferno/utils/db_concurrency_check.rb +0 -166
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ed690c6df4e1cfcb7f4ec361fb8326af28107839c20fbb9de6069ccdc3e950b
|
|
4
|
+
data.tar.gz: 1c35c4afdf97fc1f8e053d213bdc643099c79defd0b09e50fccb34977a2a497d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '048ca08826b17e50cbbd7452d600e996287167ec72d7993ed3a7686849bc6bbcd61221138df2dd5520ee9156deda55c8d577d99f9ade542d17cda31857d82837'
|
|
7
|
+
data.tar.gz: b1878c6aedf0f99732536a02fad0cbc2cf2913d21e8cae56586764a04c428346303fd9fb95c462538a96a3968fee7cac3cb0e27bd8b28998e293597aa94d7fd9
|
|
@@ -98,7 +98,7 @@ module Inferno
|
|
|
98
98
|
Inferno.routes.each do |route|
|
|
99
99
|
cleaned_id = route[:suite].id.gsub(/[^a-zA-Z\d\-._~]/, '_')
|
|
100
100
|
path = "/custom/#{cleaned_id}#{route[:path]}"
|
|
101
|
-
Application['logger'].info("Registering custom route: #{path}")
|
|
101
|
+
Application['logger'].info("Registering custom route: #{route[:method].to_s.upcase} #{path}")
|
|
102
102
|
if route[:method] == :all
|
|
103
103
|
mount route[:handler], at: path
|
|
104
104
|
else
|
|
@@ -16,11 +16,6 @@ module Inferno
|
|
|
16
16
|
# or writes from another. WAL mode lets readers proceed without blocking on a
|
|
17
17
|
# concurrent writer, and a longer busy_timeout gives a blocked writer more
|
|
18
18
|
# room to wait its turn instead of immediately raising SQLITE_BUSY.
|
|
19
|
-
#
|
|
20
|
-
# If you change this, run `bundle exec rake db:check_concurrency` to verify
|
|
21
|
-
# SQLITE_BUSY is still avoided under concurrent test-run writes and status
|
|
22
|
-
# polling (see lib/inferno/utils/db_concurrency_check.rb for why that's a
|
|
23
|
-
# rake task and not a spec).
|
|
24
19
|
def configure_sqlite_pragmas!(config)
|
|
25
20
|
return config unless config['adapter'] == 'sqlite'
|
|
26
21
|
|
|
@@ -141,7 +141,9 @@ module Inferno
|
|
|
141
141
|
|
|
142
142
|
def handle_must_support_slice_choices
|
|
143
143
|
missing_slices.delete_if do |slice|
|
|
144
|
-
choices = metadata.must_supports[:choices].find
|
|
144
|
+
choices = metadata.must_supports[:choices].find do |choice|
|
|
145
|
+
choice[:slice_names]&.any? { |slice_name| slice_identified_by?(slice, slice_name) }
|
|
146
|
+
end
|
|
145
147
|
any_choice_supported?(choices)
|
|
146
148
|
end
|
|
147
149
|
end
|
|
@@ -172,7 +174,15 @@ module Inferno
|
|
|
172
174
|
def any_slice_names_choice_supported?(choices)
|
|
173
175
|
return false unless choices[:slice_names].present?
|
|
174
176
|
|
|
175
|
-
choices[:slice_names].any?
|
|
177
|
+
choices[:slice_names].any? do |slice_name|
|
|
178
|
+
missing_slices.none? { |slice| slice_identified_by?(slice, slice_name) }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Entries in a choice's slice_names may be either the slice name (eg 'us-core') or the slice id
|
|
183
|
+
# (eg 'Condition.category:us-core'). The id is unique within a profile while the name may not be.
|
|
184
|
+
def slice_identified_by?(slice, slice_name_or_id)
|
|
185
|
+
[slice[:slice_name], slice[:slice_id]].include?(slice_name_or_id)
|
|
176
186
|
end
|
|
177
187
|
|
|
178
188
|
def any_elements_choice_supported?(choices)
|
|
@@ -303,28 +313,6 @@ module Inferno
|
|
|
303
313
|
]
|
|
304
314
|
end
|
|
305
315
|
|
|
306
|
-
def process_must_support_element_in_extension(resource, path)
|
|
307
|
-
return [resource, path] unless path.start_with?('extension:')
|
|
308
|
-
|
|
309
|
-
path_without_prefix = path.delete_prefix('extension:')
|
|
310
|
-
extension_split = path_without_prefix.split('.')
|
|
311
|
-
extension_name = extension_split.first
|
|
312
|
-
extension_path = extension_split.last
|
|
313
|
-
|
|
314
|
-
found_extension_url =
|
|
315
|
-
normalized_extension_url(must_support_extensions.find { |ex| ex[:id].include?(extension_name) }[:url])
|
|
316
|
-
ms_element_extension = resource.extension.find do |extension|
|
|
317
|
-
normalized_extension_url(extension.url) == found_extension_url
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
if ms_element_extension.present?
|
|
321
|
-
resource = ms_element_extension
|
|
322
|
-
path = extension_path
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
[resource, path]
|
|
326
|
-
end
|
|
327
|
-
|
|
328
316
|
def matching_without_extensions?(value, ms_extension_urls, fixed_value)
|
|
329
317
|
has_ms_extension = must_support_extension_present?(value, ms_extension_urls)
|
|
330
318
|
|
|
@@ -168,9 +168,11 @@ module Inferno
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
# @private
|
|
171
|
-
def copy_instance_variables
|
|
171
|
+
def copy_instance_variables(subclass)
|
|
172
|
+
# The id must be unique, and the suites and options are memoized from
|
|
173
|
+
# suite_ids, which the subclass may change.
|
|
172
174
|
instance_variables
|
|
173
|
-
.reject { |variable| [
|
|
175
|
+
.reject { |variable| [:@id, :@suites, :@options].include? variable }
|
|
174
176
|
.each { |variable| subclass.instance_variable_set(variable, instance_variable_get(variable).dup) }
|
|
175
177
|
end
|
|
176
178
|
end
|
|
@@ -3,6 +3,9 @@ module Inferno
|
|
|
3
3
|
class ExecuteTestRun
|
|
4
4
|
include Sidekiq::Worker
|
|
5
5
|
|
|
6
|
+
# Retrying would replay the whole test run, so surface failures instead.
|
|
7
|
+
sidekiq_options retry: false
|
|
8
|
+
|
|
6
9
|
def perform(test_run_id)
|
|
7
10
|
test_run = Inferno::Repositories::TestRuns.new.find(test_run_id)
|
|
8
11
|
test_session = Inferno::Repositories::TestSessions.new.find(test_run.test_session_id)
|
|
@@ -3,6 +3,10 @@ module Inferno
|
|
|
3
3
|
class ResumeTestRun
|
|
4
4
|
include Sidekiq::Worker
|
|
5
5
|
|
|
6
|
+
# Retrying would rerun the test that was in progress, resending its
|
|
7
|
+
# requests, so surface failures instead.
|
|
8
|
+
sidekiq_options retry: false
|
|
9
|
+
|
|
6
10
|
def perform(test_run_id)
|
|
7
11
|
test_run = Inferno::Repositories::TestRuns.new.find(test_run_id)
|
|
8
12
|
test_session = Inferno::Repositories::TestSessions.new.find(test_run.test_session_id)
|
data/lib/inferno/test_runner.rb
CHANGED
|
@@ -89,8 +89,12 @@ module Inferno
|
|
|
89
89
|
if result == 'wait'
|
|
90
90
|
# The waiting status and the wait result must become visible to
|
|
91
91
|
# readers atomically, so that pollers never see the run waiting
|
|
92
|
-
# without the waiting test's result being present.
|
|
93
|
-
|
|
92
|
+
# without the waiting test's result being present. Immediate mode
|
|
93
|
+
# takes the sqlite write lock up front () mark_as_waiting reads before
|
|
94
|
+
# it writes) and is used to avoid a SQLITE_BUSY error without waiting
|
|
95
|
+
# out busy_timeout when it tries to upgrade to a write lock that is
|
|
96
|
+
# already taken.
|
|
97
|
+
Inferno::Application['db.connection'].transaction(mode: :immediate) do
|
|
94
98
|
test_runs_repo.mark_as_waiting(test_run.id, test_instance.identifier, test_instance.wait_timeout)
|
|
95
99
|
persist_result(result_params)
|
|
96
100
|
end
|
|
@@ -280,11 +284,10 @@ module Inferno
|
|
|
280
284
|
# into a single lock acquisition instead of one per insert, cutting
|
|
281
285
|
# down on sqlite write-lock contention with concurrent readers/writers.
|
|
282
286
|
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
result = Inferno::Application['db.connection'].transaction do
|
|
287
|
+
# Immediate mode takes the write lock up front so that any reads inside
|
|
288
|
+
# the transaction can't leave it stuck unable to upgrade to a write lock
|
|
289
|
+
# (see the wait transaction in #run_test).
|
|
290
|
+
result = Inferno::Application['db.connection'].transaction(mode: :immediate) do
|
|
288
291
|
results_repo.create(
|
|
289
292
|
params.merge(test_run_id: test_run.id, test_session_id: test_session.id)
|
|
290
293
|
)
|
data/lib/inferno/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inferno_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen MacVicar
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-10-
|
|
13
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: activesupport
|
|
@@ -759,7 +759,6 @@ files:
|
|
|
759
759
|
- lib/inferno/route_storage.rb
|
|
760
760
|
- lib/inferno/spec_support.rb
|
|
761
761
|
- lib/inferno/test_runner.rb
|
|
762
|
-
- lib/inferno/utils/db_concurrency_check.rb
|
|
763
762
|
- lib/inferno/utils/execution_script_runner.rb
|
|
764
763
|
- lib/inferno/utils/ig_downloader.rb
|
|
765
764
|
- lib/inferno/utils/markdown_formatter.rb
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
require 'sequel'
|
|
2
|
-
require 'sqlite3'
|
|
3
|
-
require 'tmpdir'
|
|
4
|
-
require 'securerandom'
|
|
5
|
-
|
|
6
|
-
module Inferno
|
|
7
|
-
module Utils
|
|
8
|
-
# Verifies that the sqlite WAL/busy_timeout/transaction-wrapping in
|
|
9
|
-
# lib/inferno/config/boot/db.rb and TestRunner#persist_result actually
|
|
10
|
-
# prevent SQLITE_BUSY under concurrent test-run writes and status-polling
|
|
11
|
-
# reads against a shared sqlite file.
|
|
12
|
-
#
|
|
13
|
-
# This is deliberately NOT an rspec example. It's a real-time concurrency
|
|
14
|
-
# measurement, and a hardcoded load level that reliably reproduces (or
|
|
15
|
-
# avoids) SQLITE_BUSY on one machine does not transfer to others: a load
|
|
16
|
-
# level tuned to fail reliably without the fix and pass reliably with it,
|
|
17
|
-
# verified repeatedly on one machine, failed consistently in GitHub
|
|
18
|
-
# Actions and on another local machine. Rather than assert against a
|
|
19
|
-
# fixed load level, this calibrates to whatever machine it runs on: it
|
|
20
|
-
# escalates the write volume until it can demonstrate the *unprotected*
|
|
21
|
-
# configuration actually hits SQLITE_BUSY, then checks whether the
|
|
22
|
-
# *protected* configuration avoids it under that same, machine-calibrated
|
|
23
|
-
# load.
|
|
24
|
-
#
|
|
25
|
-
# Run manually with `bundle exec rake db:check_concurrency` after changing
|
|
26
|
-
# the persistence/locking behavior in the files above. It isn't part of
|
|
27
|
-
# the default test suite because it's a real-time measurement, not a
|
|
28
|
-
# deterministic assertion, and shouldn't gate CI.
|
|
29
|
-
module DbConcurrencyCheck
|
|
30
|
-
WRITER_THREADS = 5
|
|
31
|
-
READER_THREADS = 3
|
|
32
|
-
READER_POLL_INTERVAL = 0.2
|
|
33
|
-
MAX_CONNECTIONS = 15
|
|
34
|
-
MAX_CASCADES_PER_WRITER = 5000
|
|
35
|
-
|
|
36
|
-
module_function
|
|
37
|
-
|
|
38
|
-
def run
|
|
39
|
-
cascades = 10
|
|
40
|
-
baseline_errors = []
|
|
41
|
-
|
|
42
|
-
loop do
|
|
43
|
-
baseline_errors = measure(protected: false, cascades_per_writer: cascades)
|
|
44
|
-
break if baseline_errors.any? || cascades > MAX_CASCADES_PER_WRITER
|
|
45
|
-
|
|
46
|
-
cascades *= 2
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
if baseline_errors.empty?
|
|
50
|
-
puts "INCONCLUSIVE: could not reproduce SQLITE_BUSY even at #{cascades} cascades/writer on this machine."
|
|
51
|
-
return true
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
puts "Reproduced #{baseline_errors.size} SQLITE_BUSY error(s) without WAL/busy_timeout/transaction-" \
|
|
55
|
-
"wrapping at #{cascades} cascades/writer (#{WRITER_THREADS} writers, #{READER_THREADS} readers " \
|
|
56
|
-
"polling every #{READER_POLL_INTERVAL}s)."
|
|
57
|
-
|
|
58
|
-
protected_errors = measure(protected: true, cascades_per_writer: cascades)
|
|
59
|
-
|
|
60
|
-
if protected_errors.empty?
|
|
61
|
-
puts 'PASS: the current fix avoided SQLITE_BUSY under the same load that reproduced it above.'
|
|
62
|
-
true
|
|
63
|
-
else
|
|
64
|
-
puts "FAIL: the current fix still hit #{protected_errors.size} SQLITE_BUSY error(s) under the same load:"
|
|
65
|
-
puts protected_errors.first.message
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def measure(protected:, cascades_per_writer:)
|
|
71
|
-
Dir.mktmpdir do |dir|
|
|
72
|
-
db = connect(File.join(dir, 'contention.db'), protected:)
|
|
73
|
-
result_class, request_class, header_class = build_schema_and_models(db)
|
|
74
|
-
errors = []
|
|
75
|
-
mutex = Mutex.new
|
|
76
|
-
stop_reading = false
|
|
77
|
-
|
|
78
|
-
writers = Array.new(WRITER_THREADS) do
|
|
79
|
-
Thread.new do
|
|
80
|
-
cascades_per_writer.times do
|
|
81
|
-
persist_cascade(db, result_class, request_class, header_class, protected:)
|
|
82
|
-
rescue StandardError => e
|
|
83
|
-
mutex.synchronize { errors << e }
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
readers = Array.new(READER_THREADS) do
|
|
89
|
-
Thread.new { poll_until(-> { stop_reading }, result_class, request_class, errors, mutex) }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
writers.each(&:join)
|
|
93
|
-
stop_reading = true
|
|
94
|
-
readers.each(&:join)
|
|
95
|
-
db.disconnect
|
|
96
|
-
|
|
97
|
-
errors.select { |e| e.is_a?(Sequel::DatabaseError) && e.message =~ /database is locked|SQLITE_BUSY/i }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def connect(db_path, protected:)
|
|
102
|
-
connect_sqls =
|
|
103
|
-
if protected
|
|
104
|
-
["PRAGMA busy_timeout = #{ENV.fetch('DB_BUSY_TIMEOUT_MS', '15000')}", 'PRAGMA journal_mode = WAL']
|
|
105
|
-
else
|
|
106
|
-
['PRAGMA busy_timeout = 0']
|
|
107
|
-
end
|
|
108
|
-
Sequel.connect(adapter: 'sqlite', database: db_path, max_connections: MAX_CONNECTIONS, connect_sqls:)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def persist_cascade(db, result_class, request_class, header_class, protected:)
|
|
112
|
-
if protected
|
|
113
|
-
db.transaction { create_cascade(result_class, request_class, header_class) }
|
|
114
|
-
else
|
|
115
|
-
create_cascade(result_class, request_class, header_class)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def poll_until(stop, result_class, request_class, errors, mutex)
|
|
120
|
-
until stop.call
|
|
121
|
-
result_class.order(Sequel.desc(:id)).limit(1).all
|
|
122
|
-
request_class.order(Sequel.desc(:id)).limit(1).all
|
|
123
|
-
sleep(READER_POLL_INTERVAL)
|
|
124
|
-
end
|
|
125
|
-
rescue StandardError => e
|
|
126
|
-
mutex.synchronize { errors << e }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def build_schema_and_models(db)
|
|
130
|
-
db.create_table(:results) do
|
|
131
|
-
primary_key :id
|
|
132
|
-
column :test_id, String
|
|
133
|
-
column :result, String
|
|
134
|
-
end
|
|
135
|
-
db.create_table(:requests) do
|
|
136
|
-
primary_key :id
|
|
137
|
-
foreign_key :result_id, :results
|
|
138
|
-
column :verb, String
|
|
139
|
-
end
|
|
140
|
-
db.create_table(:headers) do
|
|
141
|
-
primary_key :id
|
|
142
|
-
foreign_key :request_id, :requests
|
|
143
|
-
column :name, String
|
|
144
|
-
column :value, String
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
[
|
|
148
|
-
Class.new(Sequel::Model(db[:results])),
|
|
149
|
-
Class.new(Sequel::Model(db[:requests])),
|
|
150
|
-
Class.new(Sequel::Model(db[:headers]))
|
|
151
|
-
]
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def create_cascade(result_class, request_class, header_class)
|
|
155
|
-
result = result_class.create(test_id: SecureRandom.uuid, result: 'pass')
|
|
156
|
-
3.times do
|
|
157
|
-
request = request_class.create(result_id: result.id, verb: 'GET')
|
|
158
|
-
2.times do |i|
|
|
159
|
-
header_class.create(request_id: request.id, name: "Header-#{i}", value: SecureRandom.hex(8))
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
result
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|