svg_conform 0.1.10 → 0.1.11

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,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe SvgConform::ValidationContext do
4
+ let(:document) { nil }
5
+ let(:profile) { instance_double(SvgConform::Profile) }
6
+ let(:context) { described_class.new(document, profile) }
7
+
8
+ describe "#state_for" do
9
+ # Create test requirement with nested State class
10
+ let(:requirement_with_state) do
11
+ req_class = Class.new(SvgConform::Requirements::BaseRequirement) do
12
+ def id
13
+ "test_requirement"
14
+ end
15
+ end
16
+
17
+ # Define State class as a proper constant
18
+ req_class.const_set(:State, Class.new do
19
+ attr_accessor :collected_items
20
+
21
+ def initialize
22
+ @collected_items = []
23
+ end
24
+ end)
25
+
26
+ req_class.new
27
+ end
28
+
29
+ it "creates a fresh State instance on first access" do
30
+ state = context.state_for(requirement_with_state)
31
+ expect(state).to be_a(requirement_with_state.class::State)
32
+ expect(state.collected_items).to eq([])
33
+ end
34
+
35
+ it "returns the same State instance on subsequent accesses for same requirement class" do
36
+ state1 = context.state_for(requirement_with_state)
37
+ state1.collected_items << "item1"
38
+ state2 = context.state_for(requirement_with_state)
39
+ expect(state2).to be(state1)
40
+ expect(state2.collected_items).to eq(["item1"])
41
+ end
42
+
43
+ it "creates separate State instances for different requirement classes" do
44
+ req1_class = Class.new(SvgConform::Requirements::BaseRequirement) do
45
+ def id
46
+ "req1"
47
+ end
48
+ end
49
+ req1_class.const_set(:State, Class.new do
50
+ attr_accessor :data
51
+
52
+ def initialize
53
+ @data = []
54
+ end
55
+ end)
56
+
57
+ req2_class = Class.new(SvgConform::Requirements::BaseRequirement) do
58
+ def id
59
+ "req2"
60
+ end
61
+ end
62
+ req2_class.const_set(:State, Class.new do
63
+ attr_accessor :items
64
+
65
+ def initialize
66
+ @items = []
67
+ end
68
+ end)
69
+
70
+ req1 = req1_class.new
71
+ req2 = req2_class.new
72
+
73
+ state1 = context.state_for(req1)
74
+ state2 = context.state_for(req1)
75
+ state3 = context.state_for(req2)
76
+
77
+ expect(state1).to be(state2)
78
+ expect(state1).not_to be(state3)
79
+ expect(state2).not_to be(state3)
80
+ end
81
+
82
+ it "uses requirement class as key, not instance" do
83
+ req1_instance1 = requirement_with_state
84
+ req1_instance2 = requirement_with_state.class.new
85
+
86
+ state1 = context.state_for(req1_instance1)
87
+ state1.collected_items << "from_instance1"
88
+
89
+ state2 = context.state_for(req1_instance2)
90
+
91
+ # Both instances of the same requirement class should share state
92
+ expect(state1).to be(state2)
93
+ expect(state2.collected_items).to eq(["from_instance1"])
94
+ end
95
+
96
+ context "with requirement that lacks nested State class" do
97
+ let(:requirement_without_state) do
98
+ Class.new(SvgConform::Requirements::BaseRequirement) do
99
+ def id
100
+ "no_state_requirement"
101
+ end
102
+ end.new
103
+ end
104
+
105
+ it "raises an error when State class is not defined" do
106
+ expect do
107
+ context.state_for(requirement_without_state)
108
+ end.to raise_error(NameError, /uninitialized constant.*State/)
109
+ end
110
+ end
111
+ end
112
+
113
+ describe "state lifecycle" do
114
+ let(:requirement_class) do
115
+ req_class = Class.new(SvgConform::Requirements::BaseRequirement) do
116
+ def id
117
+ "counter_requirement"
118
+ end
119
+ end
120
+
121
+ req_class.const_set(:State, Class.new do
122
+ attr_accessor :counter
123
+
124
+ def initialize
125
+ @counter = 0
126
+ end
127
+ end)
128
+
129
+ req_class
130
+ end
131
+
132
+ it "creates fresh state for each validation context instance" do
133
+ requirement = requirement_class.new
134
+
135
+ context1 = described_class.new(nil, profile)
136
+ state1 = context1.state_for(requirement)
137
+ state1.counter = 42
138
+
139
+ context2 = described_class.new(nil, profile)
140
+ state2 = context2.state_for(requirement)
141
+
142
+ # Different contexts should have different state instances
143
+ expect(state1).not_to be(state2)
144
+ expect(state2.counter).to eq(0)
145
+ end
146
+
147
+ it "prevents state pollution across validations" do
148
+ requirement = requirement_class.new
149
+
150
+ # First validation
151
+ context1 = described_class.new(nil, profile)
152
+ state1 = context1.state_for(requirement)
153
+ state1.counter = 100
154
+
155
+ # Second validation with same profile
156
+ context2 = described_class.new(nil, profile)
157
+ state2 = context2.state_for(requirement)
158
+
159
+ # State should be fresh, not polluted by first validation
160
+ expect(state2.counter).to eq(0)
161
+ end
162
+ end
163
+
164
+ describe "#node_structurally_invalid?" do
165
+ it "returns false for nodes not marked as structurally invalid" do
166
+ # In SAX mode (document = nil), simple objects return nil from node_id generator
167
+ node = Object.new
168
+ expect(context.node_structurally_invalid?(node)).to be false
169
+ end
170
+ end
171
+
172
+ describe "#mark_node_structurally_invalid" do
173
+ it "marks a node as structurally invalid" do
174
+ # Use a node-like object with a stable path_id for tracking
175
+ node = Object.new
176
+ def node.path_id
177
+ "test_node_1"
178
+ end
179
+
180
+ context.mark_node_structurally_invalid(node)
181
+ # After marking, the node should be tracked as structurally invalid
182
+ expect(context.node_structurally_invalid?(node)).to be true
183
+ end
184
+ end
185
+
186
+ describe "#register_id" do
187
+ it "registers an ID in the reference manifest" do
188
+ context.register_id("test_id", element_name: "svg", line_number: 1,
189
+ column_number: 1)
190
+ expect(context.id_defined?("test_id")).to be true
191
+ end
192
+ end
193
+
194
+ describe "#register_reference" do
195
+ it "registers a reference in the manifest" do
196
+ # Use a concrete reference type
197
+ reference = SvgConform::References::InternalFragmentReference.new(
198
+ value: "#target",
199
+ element_name: "use",
200
+ attribute_name: "href",
201
+ line_number: 1,
202
+ column_number: 1,
203
+ )
204
+
205
+ context.register_reference(reference)
206
+ # Verify reference was added - the manifest should now track this reference
207
+ expect(context.reference_manifest).to be_a(SvgConform::References::ReferenceManifest)
208
+ end
209
+ end
210
+
211
+ describe "#id_defined?" do
212
+ it "returns false for undefined IDs" do
213
+ expect(context.id_defined?("nonexistent")).to be false
214
+ end
215
+
216
+ it "returns true for registered IDs" do
217
+ context.register_id("test_id", element_name: "svg")
218
+ expect(context.id_defined?("test_id")).to be true
219
+ end
220
+ end
221
+
222
+ describe "#add_error" do
223
+ it "adds an error to the context" do
224
+ node = Object.new
225
+ context.add_error(
226
+ node: node,
227
+ message: "test error",
228
+ requirement_id: "test",
229
+ )
230
+
231
+ expect(context.has_errors?).to be true
232
+ expect(context.errors.first.message).to eq("test error")
233
+ end
234
+ end
235
+
236
+ describe "#add_warning" do
237
+ it "adds a warning to the context" do
238
+ node = Object.new
239
+ context.add_warning(
240
+ rule: "test_rule",
241
+ node: node,
242
+ message: "test warning",
243
+ )
244
+
245
+ expect(context.has_warnings?).to be true
246
+ expect(context.warnings.first.message).to eq("test warning")
247
+ end
248
+ end
249
+
250
+ describe "#add_notice" do
251
+ it "adds a notice to the context" do
252
+ node = Object.new
253
+ context.add_notice(
254
+ rule: "test_rule",
255
+ node: node,
256
+ message: "test notice",
257
+ )
258
+
259
+ expect(context.infos.first.message).to eq("test notice")
260
+ end
261
+ end
262
+
263
+ describe "#has_errors?" do
264
+ it "returns false when no errors" do
265
+ expect(context.has_errors?).to be false
266
+ end
267
+
268
+ it "returns true when errors are added" do
269
+ node = Object.new
270
+ context.add_error(node: node, message: "error", requirement_id: "test")
271
+ expect(context.has_errors?).to be true
272
+ end
273
+ end
274
+
275
+ describe "#has_warnings?" do
276
+ it "returns false when no warnings" do
277
+ expect(context.has_warnings?).to be false
278
+ end
279
+
280
+ it "returns true when warnings are added" do
281
+ node = Object.new
282
+ context.add_warning(rule: "test", node: node, message: "warning")
283
+ expect(context.has_warnings?).to be true
284
+ end
285
+ end
286
+
287
+ describe "#add_fix" do
288
+ it "stores fix in fixes array" do
289
+ fix = -> { "some action" }
290
+ context.add_fix(fix)
291
+ expect(context.fixes).to eq([fix])
292
+ end
293
+ end
294
+
295
+ describe "#has_fixes?" do
296
+ it "returns false when no fixes" do
297
+ expect(context.has_fixes?).to be false
298
+ end
299
+
300
+ it "returns true when fixes are added" do
301
+ context.add_fix(-> { "action" })
302
+ expect(context.has_fixes?).to be true
303
+ end
304
+ end
305
+ end
@@ -29,4 +29,160 @@ RSpec.describe SvgConform do
29
29
  expect(css_profile).not_to be_nil
30
30
  expect(css_profile.requirements).not_to be_empty
31
31
  end
32
+
33
+ describe "profile switching without state leakage" do
34
+ let(:svg_with_invalid_ref) do
35
+ <<~SVG
36
+ <?xml version="1.0" encoding="UTF-8"?>
37
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 100 100">
38
+ <defs>
39
+ <rect id="valid-rect" width="10" height="10"/>
40
+ </defs>
41
+ <use href="#valid-rect" x="10" y="10"/>
42
+ <use href="#invalid-rect" x="20" y="20"/>
43
+ </svg>
44
+ SVG
45
+ end
46
+
47
+ it "maintains consistent validation results when switching between profiles" do
48
+ validator = SvgConform::Validator.new(mode: :sax)
49
+
50
+ # First validation with metanorma profile
51
+ profile_meta = SvgConform::Profiles.get("metanorma")
52
+ result_meta1 = validator.validate(svg_with_invalid_ref,
53
+ profile: profile_meta)
54
+ first_meta_count = result_meta1.errors.count
55
+
56
+ # Validation with svg_1_2_rfc profile
57
+ profile_rfc = SvgConform::Profiles.get("svg_1_2_rfc")
58
+ result_rfc1 = validator.validate(svg_with_invalid_ref,
59
+ profile: profile_rfc)
60
+ first_rfc_count = result_rfc1.errors.count
61
+
62
+ # Second validation with metanorma profile (should match first)
63
+ result_meta2 = validator.validate(svg_with_invalid_ref,
64
+ profile: profile_meta)
65
+ second_meta_count = result_meta2.errors.count
66
+
67
+ # Second validation with svg_1_2_rfc profile (should match first)
68
+ result_rfc2 = validator.validate(svg_with_invalid_ref,
69
+ profile: profile_rfc)
70
+ second_rfc_count = result_rfc2.errors.count
71
+
72
+ # Verify consistency - no state leakage between profiles
73
+ expect(first_meta_count).to eq(second_meta_count),
74
+ "metanorma profile should return consistent results (#{first_meta_count} vs #{second_meta_count})"
75
+
76
+ expect(first_rfc_count).to eq(second_rfc_count),
77
+ "svg_1_2_rfc profile should return consistent results (#{first_rfc_count} vs #{second_rfc_count})"
78
+ end
79
+
80
+ it "properly resets requirement state between validations" do
81
+ # This test specifically verifies that requirement state is reset
82
+ # by validating the same content multiple times
83
+ validator = SvgConform::Validator.new(mode: :sax)
84
+ profile = SvgConform::Profiles.get("svg_1_2_rfc")
85
+
86
+ results = []
87
+ 3.times do
88
+ result = validator.validate(svg_with_invalid_ref, profile: profile)
89
+ results << result.errors.count
90
+ end
91
+
92
+ # All runs should produce the same error count
93
+ expect(results.uniq.size).to eq(1),
94
+ "All validation runs should produce identical results: #{results.inspect}"
95
+ end
96
+
97
+ it "handles interleaved profile validations correctly" do
98
+ validator = SvgConform::Validator.new(mode: :sax)
99
+
100
+ profile_meta = SvgConform::Profiles.get("metanorma")
101
+ profile_rfc = SvgConform::Profiles.get("svg_1_2_rfc")
102
+
103
+ # Interleave validations
104
+ results = []
105
+ results << [:meta,
106
+ validator.validate(svg_with_invalid_ref,
107
+ profile: profile_meta).errors.count]
108
+ results << [:rfc,
109
+ validator.validate(svg_with_invalid_ref,
110
+ profile: profile_rfc).errors.count]
111
+ results << [:meta,
112
+ validator.validate(svg_with_invalid_ref,
113
+ profile: profile_meta).errors.count]
114
+ results << [:rfc,
115
+ validator.validate(svg_with_invalid_ref,
116
+ profile: profile_rfc).errors.count]
117
+ results << [:meta,
118
+ validator.validate(svg_with_invalid_ref,
119
+ profile: profile_meta).errors.count]
120
+
121
+ # Extract counts by profile
122
+ meta_counts = results.select { |type, _| type == :meta }.map(&:last)
123
+ rfc_counts = results.select { |type, _| type == :rfc }.map(&:last)
124
+
125
+ # Each profile should produce consistent results
126
+ expect(meta_counts.uniq.size).to eq(1),
127
+ "metanorma validations should be consistent: #{meta_counts.inspect}"
128
+ expect(rfc_counts.uniq.size).to eq(1),
129
+ "svg_1_2_rfc validations should be consistent: #{rfc_counts.inspect}"
130
+ end
131
+
132
+ it "returns different error counts for profiles with different requirements" do
133
+ # Real-world SVG with style attributes that violate svg_1_2_rfc but not metanorma
134
+ svg_with_styles = <<~SVG
135
+ <svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 28000 21000">
136
+ <g class="Drawing" id="Straight_Connector_42">
137
+ <g>
138
+ <g style="stroke:rgb(0,0,0);stroke-width:88;fill:none">
139
+ <path d="M 4264,13886 L 4264,17273" style="fill:none" />
140
+ </g></g></g>
141
+ <g class="Drawing" id="Straight_Connector_33">
142
+ <g>
143
+ <g style="stroke:rgb(0,0,0);stroke-width:88;fill:none">
144
+ <path d="M 20355,10711 L 20351,13886" style="fill:none" />
145
+ </g></g></g>
146
+ <g class="Drawing">
147
+ <g>
148
+ <g style="stroke:none;fill:none">
149
+ <rect height="3490" width="25184" x="1512" y="340" />
150
+ </g>
151
+ <g style="font-family:Arial embedded;font-size:1552px;font-weight:400">
152
+ <g style="stroke:none;fill:rgb(0,0,0)">
153
+ <text>
154
+ <tspan x="4733 5855 6718 7582 8446 8877 9741 10605 11036 12074 12853 13716 14580 15443 15960 16307 17171 17602 18724 19071 19935 20799 21315 22179 " y="2482">
155
+ Updated Scenario Diagram
156
+ </tspan>
157
+ </text>
158
+ </g>
159
+ </g>
160
+ </g>
161
+ </g>
162
+ </svg>
163
+ SVG
164
+
165
+ validator = SvgConform::Validator.new(mode: :sax)
166
+
167
+ # Validate with metanorma (should have 0 errors - more permissive)
168
+ profile_meta = SvgConform::Profiles.get("metanorma")
169
+ result_meta = validator.validate(svg_with_styles, profile: profile_meta)
170
+ meta_errors = result_meta.errors.count
171
+
172
+ # Validate with svg_1_2_rfc (should have errors - stricter requirements)
173
+ profile_rfc = SvgConform::Profiles.get("svg_1_2_rfc")
174
+ result_rfc = validator.validate(svg_with_styles, profile: profile_rfc)
175
+ rfc_errors = result_rfc.errors.count
176
+
177
+ # Profiles should return different results
178
+ expect(meta_errors).to eq(0), "metanorma profile should allow this SVG"
179
+ expect(rfc_errors).to be > 0,
180
+ "svg_1_2_rfc profile should detect violations"
181
+
182
+ # Verify consistency when repeating
183
+ result_rfc2 = validator.validate(svg_with_styles, profile: profile_rfc)
184
+ expect(result_rfc2.errors.count).to eq(rfc_errors),
185
+ "svg_1_2_rfc should return same error count on repeat (no state leakage)"
186
+ end
187
+ end
32
188
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: svg_conform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-23 00:00:00.000000000 Z
11
+ date: 2026-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -115,6 +115,7 @@ files:
115
115
  - exe/svg_conform
116
116
  - lib/svg_conform.rb
117
117
  - lib/svg_conform/batch_report.rb
118
+ - lib/svg_conform/classification_cache.rb
118
119
  - lib/svg_conform/cli.rb
119
120
  - lib/svg_conform/commands/check.rb
120
121
  - lib/svg_conform/commands/profiles.rb
@@ -241,8 +242,10 @@ files:
241
242
  - spec/fixtures/style/repair/basic_violations.svg
242
243
  - spec/fixtures/style_promotion/inputs/basic_test.svg
243
244
  - spec/fixtures/style_promotion/repair/basic_test.svg
245
+ - spec/fixtures/svg_1_2_rfc/expected_errors/ietf_test_violations.yml
244
246
  - spec/fixtures/svg_1_2_rfc/inputs/allowed_elements_violations.svg
245
247
  - spec/fixtures/svg_1_2_rfc/inputs/color_restrictions_violations.svg
248
+ - spec/fixtures/svg_1_2_rfc/inputs/ietf_test_violations.svg
246
249
  - spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.code
247
250
  - spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.err
248
251
  - spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.out
@@ -445,6 +448,7 @@ files:
445
448
  - spec/svg_conform/requirements/style_promotion_requirement_spec.rb
446
449
  - spec/svg_conform/requirements/style_requirement_spec.rb
447
450
  - spec/svg_conform/requirements/viewbox_required_requirement_spec.rb
451
+ - spec/svg_conform/validation_context_spec.rb
448
452
  - spec/svg_conform/validator_input_types_spec.rb
449
453
  - spec/svg_conform_spec.rb
450
454
  - spec/svgcheck_compatibility_spec.rb