jsonstructure 0.5.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 +7 -0
- data/Gemfile +12 -0
- data/README.md +252 -0
- data/Rakefile +82 -0
- data/lib/jsonstructure/binary_installer.rb +134 -0
- data/lib/jsonstructure/ffi.rb +149 -0
- data/lib/jsonstructure/instance_validator.rb +76 -0
- data/lib/jsonstructure/schema_validator.rb +72 -0
- data/lib/jsonstructure/validation_result.rb +96 -0
- data/lib/jsonstructure/version.rb +6 -0
- data/lib/jsonstructure.rb +114 -0
- data/spec/instance_validator_spec.rb +124 -0
- data/spec/schema_validator_spec.rb +90 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/test_assets_spec.rb +205 -0
- data/spec/thread_safety_spec.rb +257 -0
- data/spec/validation_result_spec.rb +155 -0
- metadata +112 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
# Test against the shared test-assets to ensure consistency with other SDKs
|
|
7
|
+
RSpec.describe 'Test Assets Conformance' do
|
|
8
|
+
# Find test-assets directory relative to this file
|
|
9
|
+
def find_test_assets_path
|
|
10
|
+
current = File.dirname(__FILE__)
|
|
11
|
+
|
|
12
|
+
# Try different relative paths
|
|
13
|
+
candidates = [
|
|
14
|
+
File.join(current, '..', '..', 'test-assets'),
|
|
15
|
+
File.join(current, '..', '..', '..', 'test-assets'),
|
|
16
|
+
File.join(current, '..', '..', 'sdk', 'test-assets')
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
candidates.each do |path|
|
|
20
|
+
expanded = File.expand_path(path)
|
|
21
|
+
return expanded if File.directory?(expanded)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:test_assets_path) { find_test_assets_path }
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
skip 'test-assets directory not found' unless test_assets_path
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe 'Invalid Schemas' do
|
|
34
|
+
let(:invalid_schemas_path) { File.join(test_assets_path, 'schemas', 'invalid') }
|
|
35
|
+
|
|
36
|
+
it 'rejects all invalid schemas' do
|
|
37
|
+
skip 'invalid schemas directory not found' unless File.directory?(invalid_schemas_path)
|
|
38
|
+
|
|
39
|
+
schema_files = Dir.glob(File.join(invalid_schemas_path, '*.struct.json'))
|
|
40
|
+
skip 'no invalid schema files found' if schema_files.empty?
|
|
41
|
+
|
|
42
|
+
passed = 0
|
|
43
|
+
failed = []
|
|
44
|
+
|
|
45
|
+
schema_files.each do |file|
|
|
46
|
+
schema_content = File.read(file)
|
|
47
|
+
result = JsonStructure::SchemaValidator.validate(schema_content)
|
|
48
|
+
|
|
49
|
+
if result.invalid?
|
|
50
|
+
passed += 1
|
|
51
|
+
else
|
|
52
|
+
failed << File.basename(file)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
expect(failed).to be_empty,
|
|
57
|
+
"Expected these schemas to be invalid but they were valid: #{failed.join(', ')}"
|
|
58
|
+
|
|
59
|
+
puts " Tested #{passed} invalid schemas - all correctly rejected"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe 'Valid Schemas (validation extension)' do
|
|
64
|
+
let(:valid_schemas_path) { File.join(test_assets_path, 'schemas', 'validation') }
|
|
65
|
+
|
|
66
|
+
it 'accepts all validation extension schemas' do
|
|
67
|
+
skip 'validation schemas directory not found' unless File.directory?(valid_schemas_path)
|
|
68
|
+
|
|
69
|
+
schema_files = Dir.glob(File.join(valid_schemas_path, '*.struct.json'))
|
|
70
|
+
skip 'no validation schema files found' if schema_files.empty?
|
|
71
|
+
|
|
72
|
+
passed = 0
|
|
73
|
+
failed = []
|
|
74
|
+
|
|
75
|
+
schema_files.each do |file|
|
|
76
|
+
schema_content = File.read(file)
|
|
77
|
+
result = JsonStructure::SchemaValidator.validate(schema_content)
|
|
78
|
+
|
|
79
|
+
# Check for errors only, warnings are acceptable
|
|
80
|
+
errors = result.errors.select { |e| e.severity == :error }
|
|
81
|
+
if errors.empty?
|
|
82
|
+
passed += 1
|
|
83
|
+
else
|
|
84
|
+
failed << "#{File.basename(file)}: #{result.error_messages.join(', ')}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
expect(failed).to be_empty,
|
|
89
|
+
"Expected these schemas to be valid but got errors:\n #{failed.join("\n ")}"
|
|
90
|
+
|
|
91
|
+
puts " Tested #{passed} validation schemas - all accepted"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe 'Instance Validation against test schemas' do
|
|
96
|
+
let(:validation_instances_path) { File.join(test_assets_path, 'instances', 'validation') }
|
|
97
|
+
let(:validation_schemas_path) { File.join(test_assets_path, 'schemas', 'validation') }
|
|
98
|
+
|
|
99
|
+
# Helper to extract the actual value from a test instance
|
|
100
|
+
# Test instances may have metadata fields like _description, _expectedError, _comment
|
|
101
|
+
# These need to be stripped before validation, or use the "value" field if present
|
|
102
|
+
def extract_instance_value(instance_json)
|
|
103
|
+
instance = JSON.parse(instance_json)
|
|
104
|
+
|
|
105
|
+
# If there's a "value" field, use that
|
|
106
|
+
return JSON.generate(instance['value']) if instance.key?('value')
|
|
107
|
+
|
|
108
|
+
# Otherwise, remove metadata fields and use the rest
|
|
109
|
+
instance.delete('_description')
|
|
110
|
+
instance.delete('_expectedError')
|
|
111
|
+
instance.delete('_comment')
|
|
112
|
+
JSON.generate(instance)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'rejects invalid instances against their schemas' do
|
|
116
|
+
skip 'validation instances directory not found' unless File.directory?(validation_instances_path)
|
|
117
|
+
|
|
118
|
+
# Each schema has a corresponding directory with invalid instances
|
|
119
|
+
schema_dirs = Dir.glob(File.join(validation_instances_path, '*')).select { |f| File.directory?(f) }
|
|
120
|
+
skip 'no instance directories found' if schema_dirs.empty?
|
|
121
|
+
|
|
122
|
+
passed = 0
|
|
123
|
+
failed = []
|
|
124
|
+
|
|
125
|
+
schema_dirs.each do |instance_dir|
|
|
126
|
+
schema_name = File.basename(instance_dir)
|
|
127
|
+
schema_file = File.join(validation_schemas_path, "#{schema_name}.struct.json")
|
|
128
|
+
|
|
129
|
+
next unless File.exist?(schema_file)
|
|
130
|
+
|
|
131
|
+
schema_content = File.read(schema_file)
|
|
132
|
+
|
|
133
|
+
# Test invalid instances (files directly in the schema's instance directory)
|
|
134
|
+
instance_files = Dir.glob(File.join(instance_dir, '*.json'))
|
|
135
|
+
instance_files.each do |instance_file|
|
|
136
|
+
instance_content = File.read(instance_file)
|
|
137
|
+
# Extract the actual value, stripping metadata fields
|
|
138
|
+
instance_value = extract_instance_value(instance_content)
|
|
139
|
+
result = JsonStructure::InstanceValidator.validate(instance_value, schema_content)
|
|
140
|
+
|
|
141
|
+
if result.invalid?
|
|
142
|
+
passed += 1
|
|
143
|
+
else
|
|
144
|
+
failed << "#{schema_name}/#{File.basename(instance_file)}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
expect(failed).to be_empty,
|
|
150
|
+
"Expected these instances to be invalid but they were valid:\n #{failed.join("\n ")}"
|
|
151
|
+
|
|
152
|
+
puts " Tested #{passed} invalid instances - all correctly rejected"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe 'Primer sample schemas' do
|
|
157
|
+
def find_primer_samples_path
|
|
158
|
+
current = File.dirname(__FILE__)
|
|
159
|
+
|
|
160
|
+
candidates = [
|
|
161
|
+
File.join(current, '..', '..', 'primer-and-samples', 'samples', 'core'),
|
|
162
|
+
File.join(current, '..', '..', '..', 'primer-and-samples', 'samples', 'core')
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
candidates.each do |path|
|
|
166
|
+
expanded = File.expand_path(path)
|
|
167
|
+
return expanded if File.directory?(expanded)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
let(:primer_path) { find_primer_samples_path }
|
|
174
|
+
|
|
175
|
+
it 'accepts all primer sample schemas' do
|
|
176
|
+
skip 'primer samples directory not found' unless primer_path
|
|
177
|
+
|
|
178
|
+
# Find all .struct.json files recursively
|
|
179
|
+
schema_files = Dir.glob(File.join(primer_path, '**', '*.struct.json'))
|
|
180
|
+
skip 'no primer schema files found' if schema_files.empty?
|
|
181
|
+
|
|
182
|
+
passed = 0
|
|
183
|
+
failed = []
|
|
184
|
+
|
|
185
|
+
schema_files.each do |file|
|
|
186
|
+
schema_content = File.read(file)
|
|
187
|
+
result = JsonStructure::SchemaValidator.validate(schema_content)
|
|
188
|
+
|
|
189
|
+
# Check for errors only
|
|
190
|
+
errors = result.errors.select { |e| e.severity == :error }
|
|
191
|
+
if errors.empty?
|
|
192
|
+
passed += 1
|
|
193
|
+
else
|
|
194
|
+
relative_path = file.sub(primer_path + '/', '')
|
|
195
|
+
failed << "#{relative_path}: #{result.error_messages.join(', ')}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
expect(failed).to be_empty,
|
|
200
|
+
"Expected these primer schemas to be valid but got errors:\n #{failed.join("\n ")}"
|
|
201
|
+
|
|
202
|
+
puts " Tested #{passed} primer sample schemas - all accepted"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Thread Safety' do
|
|
6
|
+
let(:valid_schema) { '{"type": "object", "properties": {"name": {"type": "string"}}}' }
|
|
7
|
+
let(:valid_instance) { '{"name": "test"}' }
|
|
8
|
+
let(:invalid_instance) { '{"name": 123}' }
|
|
9
|
+
|
|
10
|
+
describe 'concurrent schema validation' do
|
|
11
|
+
it 'handles multiple threads validating schemas concurrently' do
|
|
12
|
+
thread_count = 10
|
|
13
|
+
iterations = 50
|
|
14
|
+
errors = []
|
|
15
|
+
mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
threads = thread_count.times.map do |thread_id|
|
|
18
|
+
Thread.new do
|
|
19
|
+
iterations.times do |i|
|
|
20
|
+
begin
|
|
21
|
+
result = JsonStructure::SchemaValidator.validate(valid_schema)
|
|
22
|
+
unless result.valid?
|
|
23
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: expected valid, got invalid" }
|
|
24
|
+
end
|
|
25
|
+
rescue => e
|
|
26
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: #{e.class} - #{e.message}" }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
threads.each(&:join)
|
|
33
|
+
expect(errors).to be_empty, "Errors occurred during concurrent validation:\n#{errors.join("\n")}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'handles mixed valid and invalid schemas concurrently' do
|
|
37
|
+
thread_count = 10
|
|
38
|
+
iterations = 20
|
|
39
|
+
errors = []
|
|
40
|
+
mutex = Mutex.new
|
|
41
|
+
|
|
42
|
+
schemas = [
|
|
43
|
+
{ json: '{"type": "string"}', valid: true },
|
|
44
|
+
{ json: '{"type": "invalid_type"}', valid: false },
|
|
45
|
+
{ json: '{"type": "object", "properties": {"a": {"type": "integer"}}}', valid: true },
|
|
46
|
+
{ json: '{"minimum": "not_a_number"}', valid: false }
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
threads = thread_count.times.map do |thread_id|
|
|
50
|
+
Thread.new do
|
|
51
|
+
iterations.times do |i|
|
|
52
|
+
schema_info = schemas[(thread_id + i) % schemas.length]
|
|
53
|
+
begin
|
|
54
|
+
result = JsonStructure::SchemaValidator.validate(schema_info[:json])
|
|
55
|
+
if result.valid? != schema_info[:valid]
|
|
56
|
+
mutex.synchronize do
|
|
57
|
+
errors << "Thread #{thread_id}, iteration #{i}: expected #{schema_info[:valid]}, got #{result.valid?}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
rescue => e
|
|
61
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: #{e.class} - #{e.message}" }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
threads.each(&:join)
|
|
68
|
+
expect(errors).to be_empty, "Errors occurred during concurrent validation:\n#{errors.join("\n")}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe 'concurrent instance validation' do
|
|
73
|
+
it 'handles multiple threads validating instances concurrently' do
|
|
74
|
+
thread_count = 10
|
|
75
|
+
iterations = 50
|
|
76
|
+
errors = []
|
|
77
|
+
mutex = Mutex.new
|
|
78
|
+
|
|
79
|
+
threads = thread_count.times.map do |thread_id|
|
|
80
|
+
Thread.new do
|
|
81
|
+
iterations.times do |i|
|
|
82
|
+
begin
|
|
83
|
+
result = JsonStructure::InstanceValidator.validate(valid_instance, valid_schema)
|
|
84
|
+
unless result.valid?
|
|
85
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: expected valid, got invalid" }
|
|
86
|
+
end
|
|
87
|
+
rescue => e
|
|
88
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: #{e.class} - #{e.message}" }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
threads.each(&:join)
|
|
95
|
+
expect(errors).to be_empty, "Errors occurred during concurrent validation:\n#{errors.join("\n")}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'handles mixed valid and invalid instances concurrently' do
|
|
99
|
+
thread_count = 10
|
|
100
|
+
iterations = 20
|
|
101
|
+
errors = []
|
|
102
|
+
mutex = Mutex.new
|
|
103
|
+
|
|
104
|
+
test_cases = [
|
|
105
|
+
{ instance: '{"name": "Alice"}', valid: true },
|
|
106
|
+
{ instance: '{"name": 123}', valid: false },
|
|
107
|
+
{ instance: '{"name": "Bob", "age": 30}', valid: true },
|
|
108
|
+
{ instance: '{}', valid: true }
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
threads = thread_count.times.map do |thread_id|
|
|
112
|
+
Thread.new do
|
|
113
|
+
iterations.times do |i|
|
|
114
|
+
test_case = test_cases[(thread_id + i) % test_cases.length]
|
|
115
|
+
begin
|
|
116
|
+
result = JsonStructure::InstanceValidator.validate(test_case[:instance], valid_schema)
|
|
117
|
+
if result.valid? != test_case[:valid]
|
|
118
|
+
mutex.synchronize do
|
|
119
|
+
errors << "Thread #{thread_id}, iteration #{i}: expected #{test_case[:valid]}, got #{result.valid?}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
rescue => e
|
|
123
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: #{e.class} - #{e.message}" }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
threads.each(&:join)
|
|
130
|
+
expect(errors).to be_empty, "Errors occurred during concurrent validation:\n#{errors.join("\n")}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe 'mixed concurrent operations' do
|
|
135
|
+
it 'handles schema and instance validations running concurrently' do
|
|
136
|
+
thread_count = 10
|
|
137
|
+
iterations = 30
|
|
138
|
+
errors = []
|
|
139
|
+
mutex = Mutex.new
|
|
140
|
+
|
|
141
|
+
threads = thread_count.times.map do |thread_id|
|
|
142
|
+
Thread.new do
|
|
143
|
+
iterations.times do |i|
|
|
144
|
+
begin
|
|
145
|
+
if i.even?
|
|
146
|
+
# Schema validation
|
|
147
|
+
result = JsonStructure::SchemaValidator.validate(valid_schema)
|
|
148
|
+
unless result.valid?
|
|
149
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i} (schema): expected valid" }
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
# Instance validation
|
|
153
|
+
result = JsonStructure::InstanceValidator.validate(valid_instance, valid_schema)
|
|
154
|
+
unless result.valid?
|
|
155
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i} (instance): expected valid" }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
rescue => e
|
|
159
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: #{e.class} - #{e.message}" }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
threads.each(&:join)
|
|
166
|
+
expect(errors).to be_empty, "Errors occurred during concurrent validation:\n#{errors.join("\n")}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
describe 'validation tracking' do
|
|
171
|
+
it 'properly tracks active validations' do
|
|
172
|
+
# Initially no validations should be active
|
|
173
|
+
expect(JsonStructure.validations_active?).to be false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'handles validation_started and validation_completed correctly' do
|
|
177
|
+
# Start a validation
|
|
178
|
+
JsonStructure.validation_started
|
|
179
|
+
expect(JsonStructure.validations_active?).to be true
|
|
180
|
+
|
|
181
|
+
# Complete the validation
|
|
182
|
+
JsonStructure.validation_completed
|
|
183
|
+
expect(JsonStructure.validations_active?).to be false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'handles multiple concurrent validation tracking' do
|
|
187
|
+
# Start multiple validations
|
|
188
|
+
5.times { JsonStructure.validation_started }
|
|
189
|
+
expect(JsonStructure.validations_active?).to be true
|
|
190
|
+
|
|
191
|
+
# Complete them all
|
|
192
|
+
5.times { JsonStructure.validation_completed }
|
|
193
|
+
expect(JsonStructure.validations_active?).to be false
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
describe 'stress test' do
|
|
198
|
+
it 'survives high-concurrency stress test' do
|
|
199
|
+
thread_count = 20
|
|
200
|
+
iterations = 100
|
|
201
|
+
errors = []
|
|
202
|
+
mutex = Mutex.new
|
|
203
|
+
|
|
204
|
+
schemas = [
|
|
205
|
+
'{"type": "string"}',
|
|
206
|
+
'{"type": "integer", "minimum": 0}',
|
|
207
|
+
'{"type": "object", "properties": {"id": {"type": "integer"}}}',
|
|
208
|
+
'{"type": "array", "items": {"type": "string"}}'
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
instances = [
|
|
212
|
+
['"hello"', '{"type": "string"}', true],
|
|
213
|
+
['42', '{"type": "integer", "minimum": 0}', true],
|
|
214
|
+
['{"id": 123}', '{"type": "object", "properties": {"id": {"type": "integer"}}}', true],
|
|
215
|
+
['["a", "b", "c"]', '{"type": "array", "items": {"type": "string"}}', true],
|
|
216
|
+
['"hello"', '{"type": "integer"}', false],
|
|
217
|
+
['-5', '{"type": "integer", "minimum": 0}', false]
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
threads = thread_count.times.map do |thread_id|
|
|
221
|
+
Thread.new do
|
|
222
|
+
iterations.times do |i|
|
|
223
|
+
begin
|
|
224
|
+
case i % 3
|
|
225
|
+
when 0
|
|
226
|
+
# Schema validation
|
|
227
|
+
schema = schemas[rand(schemas.length)]
|
|
228
|
+
JsonStructure::SchemaValidator.validate(schema)
|
|
229
|
+
when 1
|
|
230
|
+
# Instance validation with expected result
|
|
231
|
+
instance_info = instances[rand(instances.length)]
|
|
232
|
+
result = JsonStructure::InstanceValidator.validate(instance_info[0], instance_info[1])
|
|
233
|
+
if result.valid? != instance_info[2]
|
|
234
|
+
mutex.synchronize do
|
|
235
|
+
errors << "Thread #{thread_id}, iteration #{i}: unexpected result for #{instance_info[0]}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
when 2
|
|
239
|
+
# validate! with exception handling
|
|
240
|
+
begin
|
|
241
|
+
JsonStructure::SchemaValidator.validate!('{"type": "string"}')
|
|
242
|
+
rescue JsonStructure::SchemaValidationError
|
|
243
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: unexpected exception" }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
rescue => e
|
|
247
|
+
mutex.synchronize { errors << "Thread #{thread_id}, iteration #{i}: #{e.class} - #{e.message}" }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
threads.each(&:join)
|
|
254
|
+
expect(errors).to be_empty, "Errors occurred during stress test:\n#{errors.first(10).join("\n")}"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe JsonStructure::ValidationResult do
|
|
6
|
+
describe '#valid?' do
|
|
7
|
+
it 'returns true for valid result' do
|
|
8
|
+
result = described_class.new(true, [])
|
|
9
|
+
expect(result).to be_valid
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'returns false for invalid result' do
|
|
13
|
+
result = described_class.new(false, [])
|
|
14
|
+
expect(result).to be_invalid
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#errors' do
|
|
19
|
+
it 'returns empty array for valid result' do
|
|
20
|
+
result = described_class.new(true, [])
|
|
21
|
+
expect(result.errors).to be_empty
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns error array for invalid result' do
|
|
25
|
+
error = JsonStructure::ValidationError.new(
|
|
26
|
+
code: 1,
|
|
27
|
+
severity: JsonStructure::FFI::JS_SEVERITY_ERROR,
|
|
28
|
+
path: '/test',
|
|
29
|
+
message: 'Test error',
|
|
30
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
31
|
+
)
|
|
32
|
+
result = described_class.new(false, [error])
|
|
33
|
+
|
|
34
|
+
expect(result.errors).not_to be_empty
|
|
35
|
+
expect(result.errors.first).to be_a(JsonStructure::ValidationError)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '#error_messages' do
|
|
40
|
+
it 'returns only error-level messages' do
|
|
41
|
+
errors = [
|
|
42
|
+
JsonStructure::ValidationError.new(
|
|
43
|
+
code: 1,
|
|
44
|
+
severity: JsonStructure::FFI::JS_SEVERITY_ERROR,
|
|
45
|
+
path: '/test',
|
|
46
|
+
message: 'Error message',
|
|
47
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
48
|
+
),
|
|
49
|
+
JsonStructure::ValidationError.new(
|
|
50
|
+
code: 2,
|
|
51
|
+
severity: JsonStructure::FFI::JS_SEVERITY_WARNING,
|
|
52
|
+
path: '/test',
|
|
53
|
+
message: 'Warning message',
|
|
54
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
55
|
+
)
|
|
56
|
+
]
|
|
57
|
+
result = described_class.new(false, errors)
|
|
58
|
+
|
|
59
|
+
expect(result.error_messages).to eq(['Error message'])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#warning_messages' do
|
|
64
|
+
it 'returns only warning-level messages' do
|
|
65
|
+
errors = [
|
|
66
|
+
JsonStructure::ValidationError.new(
|
|
67
|
+
code: 1,
|
|
68
|
+
severity: JsonStructure::FFI::JS_SEVERITY_ERROR,
|
|
69
|
+
path: '/test',
|
|
70
|
+
message: 'Error message',
|
|
71
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
72
|
+
),
|
|
73
|
+
JsonStructure::ValidationError.new(
|
|
74
|
+
code: 2,
|
|
75
|
+
severity: JsonStructure::FFI::JS_SEVERITY_WARNING,
|
|
76
|
+
path: '/test',
|
|
77
|
+
message: 'Warning message',
|
|
78
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
result = described_class.new(false, errors)
|
|
82
|
+
|
|
83
|
+
expect(result.warning_messages).to eq(['Warning message'])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
RSpec.describe JsonStructure::ValidationError do
|
|
89
|
+
describe '#error?' do
|
|
90
|
+
it 'returns true for error severity' do
|
|
91
|
+
error = described_class.new(
|
|
92
|
+
code: 1,
|
|
93
|
+
severity: JsonStructure::FFI::JS_SEVERITY_ERROR,
|
|
94
|
+
path: '/test',
|
|
95
|
+
message: 'Test',
|
|
96
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(error).to be_error
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns false for non-error severity' do
|
|
103
|
+
warning = described_class.new(
|
|
104
|
+
code: 1,
|
|
105
|
+
severity: JsonStructure::FFI::JS_SEVERITY_WARNING,
|
|
106
|
+
path: '/test',
|
|
107
|
+
message: 'Test',
|
|
108
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(warning).not_to be_error
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe '#warning?' do
|
|
116
|
+
it 'returns true for warning severity' do
|
|
117
|
+
warning = described_class.new(
|
|
118
|
+
code: 1,
|
|
119
|
+
severity: JsonStructure::FFI::JS_SEVERITY_WARNING,
|
|
120
|
+
path: '/test',
|
|
121
|
+
message: 'Test',
|
|
122
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
expect(warning).to be_warning
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#to_s' do
|
|
130
|
+
it 'includes path when present' do
|
|
131
|
+
error = described_class.new(
|
|
132
|
+
code: 1,
|
|
133
|
+
severity: JsonStructure::FFI::JS_SEVERITY_ERROR,
|
|
134
|
+
path: '/test/path',
|
|
135
|
+
message: 'Test error',
|
|
136
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
expect(error.to_s).to include('Test error')
|
|
140
|
+
expect(error.to_s).to include('/test/path')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'works without path' do
|
|
144
|
+
error = described_class.new(
|
|
145
|
+
code: 1,
|
|
146
|
+
severity: JsonStructure::FFI::JS_SEVERITY_ERROR,
|
|
147
|
+
path: nil,
|
|
148
|
+
message: 'Test error',
|
|
149
|
+
location: { line: 1, column: 1, offset: 0 }
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
expect(error.to_s).to eq('Test error')
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|