moxml 0.1.13 → 0.1.15
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/.rubocop_todo.yml +117 -66
- data/Gemfile +1 -0
- data/README.adoc +11 -9
- data/Rakefile +3 -1
- data/docs/_pages/configuration.adoc +22 -19
- data/docs/_tutorials/namespace-handling.adoc +5 -5
- data/lib/moxml/adapter/base.rb +8 -3
- data/lib/moxml/adapter/customized_libxml/entity_reference.rb +23 -0
- data/lib/moxml/adapter/customized_libxml.rb +18 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +2 -2
- data/lib/moxml/adapter/customized_oga.rb +10 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +25 -0
- data/lib/moxml/adapter/customized_ox.rb +12 -0
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +19 -0
- data/lib/moxml/adapter/customized_rexml/formatter.rb +2 -0
- data/lib/moxml/adapter/customized_rexml.rb +11 -0
- data/lib/moxml/adapter/headed_ox.rb +9 -3
- data/lib/moxml/adapter/libxml.rb +76 -62
- data/lib/moxml/adapter/nokogiri.rb +4 -5
- data/lib/moxml/adapter/oga.rb +50 -26
- data/lib/moxml/adapter/ox.rb +189 -41
- data/lib/moxml/adapter/rexml.rb +27 -8
- data/lib/moxml/attribute.rb +3 -0
- data/lib/moxml/builder.rb +1 -0
- data/lib/moxml/config.rb +7 -7
- data/lib/moxml/document.rb +5 -1
- data/lib/moxml/document_builder.rb +37 -31
- data/lib/moxml/element.rb +13 -5
- data/lib/moxml/entity_registry.rb +36 -0
- data/lib/moxml/node.rb +23 -2
- data/lib/moxml/node_set.rb +43 -15
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +1 -1
- data/spec/integration/shared_examples/edge_cases.rb +3 -0
- data/spec/moxml/adapter/oga_spec.rb +62 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +1 -12
- data/spec/moxml/allocation_benchmark_spec.rb +96 -0
- data/spec/moxml/allocation_guard_spec.rb +282 -0
- data/spec/moxml/builder_spec.rb +22 -0
- data/spec/moxml/config_spec.rb +11 -11
- data/spec/moxml/doctype_spec.rb +41 -0
- data/spec/moxml/lazy_parse_spec.rb +115 -0
- data/spec/moxml/namespace_uri_validation_spec.rb +11 -3
- data/spec/moxml/node_cache_spec.rb +110 -0
- data/spec/moxml/node_set_cache_spec.rb +90 -0
- data/spec/moxml/xml_utils_spec.rb +32 -0
- data/spec/support/allocation_helper.rb +165 -0
- data/spec/support/w3c_namespace_helpers.rb +2 -1
- metadata +15 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/allocation_helper"
|
|
5
|
+
|
|
6
|
+
# Allocation guard specs — these run in CI by default (no :performance tag).
|
|
7
|
+
#
|
|
8
|
+
# These specs enforce allocation budgets per adapter per operation.
|
|
9
|
+
# If an adapter exceeds its threshold, the spec fails with a diagnostic
|
|
10
|
+
# message showing the actual vs expected allocation count.
|
|
11
|
+
#
|
|
12
|
+
# Thresholds are in AllocationHelper::THRESHOLDS and should be tightened
|
|
13
|
+
# as optimizations are confirmed.
|
|
14
|
+
RSpec.describe "Allocation guards", order: :defined do
|
|
15
|
+
def threshold_for(adapter_name, operation)
|
|
16
|
+
AllocationHelper.threshold(adapter_name, operation)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def guard_allocations(adapter_name, operation, &block)
|
|
20
|
+
allocs = AllocationHelper.count_allocations(&block)
|
|
21
|
+
limit = threshold_for(adapter_name, operation)
|
|
22
|
+
|
|
23
|
+
if allocs > limit
|
|
24
|
+
# Generate StackProf diagnostic on failure
|
|
25
|
+
profile = AllocationHelper.profile_allocations(&block)
|
|
26
|
+
raise(<<~MSG)
|
|
27
|
+
#{adapter_name}/#{operation}: #{allocs} allocations exceeds limit of #{limit}
|
|
28
|
+
|
|
29
|
+
#{profile}
|
|
30
|
+
MSG
|
|
31
|
+
end
|
|
32
|
+
allocs
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
shared_examples "allocation guard" do |adapter_name|
|
|
36
|
+
let(:ctx) { Moxml::Context.new(adapter_name) }
|
|
37
|
+
|
|
38
|
+
# ----------------------------------------------------------------
|
|
39
|
+
# Parse allocation guard
|
|
40
|
+
# ----------------------------------------------------------------
|
|
41
|
+
describe "parse allocations" do
|
|
42
|
+
it "parses 100-element document within allocation budget" do
|
|
43
|
+
xml = generate_xml(100)
|
|
44
|
+
allocs = guard_allocations(adapter_name, :parse_100) { ctx.parse(xml) }
|
|
45
|
+
expect(allocs).to be <= threshold_for(adapter_name, :parse_100)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "parses 50-element document within allocation budget" do
|
|
49
|
+
xml = generate_xml(50)
|
|
50
|
+
allocs = guard_allocations(adapter_name, :parse_50) { ctx.parse(xml) }
|
|
51
|
+
expect(allocs).to be <= threshold_for(adapter_name, :parse_50)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "parse + root.name is allocation-efficient" do
|
|
55
|
+
xml = generate_xml(100)
|
|
56
|
+
allocs = guard_allocations(adapter_name, :parse_and_root) do
|
|
57
|
+
doc = ctx.parse(xml)
|
|
58
|
+
doc.root.name
|
|
59
|
+
end
|
|
60
|
+
expect(allocs).to be <= threshold_for(adapter_name, :parse_and_root)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------
|
|
65
|
+
# Cache hit guards — second access should allocate near-zero objects
|
|
66
|
+
# ----------------------------------------------------------------
|
|
67
|
+
describe "cache hit guards" do
|
|
68
|
+
let(:simple_xml) { "<root><a/><b/><c/></root>" }
|
|
69
|
+
let(:attr_xml) { '<root x="1" y="2" z="3"><child k="4"/></root>' }
|
|
70
|
+
|
|
71
|
+
it "children access is cached after first call" do
|
|
72
|
+
doc = ctx.parse(simple_xml)
|
|
73
|
+
root = doc.root
|
|
74
|
+
# Warm the cache
|
|
75
|
+
root.children.to_a
|
|
76
|
+
|
|
77
|
+
allocs = AllocationHelper.count_allocations { root.children.to_a }
|
|
78
|
+
expect(allocs).to be <= threshold_for(adapter_name, :cached_children_access),
|
|
79
|
+
"Second children access (#{allocs}) should allocate <= #{threshold_for(
|
|
80
|
+
adapter_name, :cached_children_access
|
|
81
|
+
)}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "attributes access is cached after first call" do
|
|
85
|
+
doc = ctx.parse(attr_xml)
|
|
86
|
+
root = doc.root
|
|
87
|
+
# Warm the cache
|
|
88
|
+
root.attributes
|
|
89
|
+
|
|
90
|
+
allocs = AllocationHelper.count_allocations { root.attributes }
|
|
91
|
+
expect(allocs).to be <= threshold_for(adapter_name, :cached_attributes_access),
|
|
92
|
+
"Second attributes access (#{allocs}) should allocate <= #{threshold_for(
|
|
93
|
+
adapter_name, :cached_attributes_access
|
|
94
|
+
)}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "NodeSet iteration is cached on second pass" do
|
|
98
|
+
xml = generate_xml(20)
|
|
99
|
+
doc = ctx.parse(xml)
|
|
100
|
+
root = doc.root
|
|
101
|
+
# Warm the cache
|
|
102
|
+
root.children.each { |_| nil }
|
|
103
|
+
|
|
104
|
+
allocs = AllocationHelper.count_allocations do
|
|
105
|
+
root.children.each do |_|
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
expect(allocs).to be <= threshold_for(adapter_name, :cached_iteration),
|
|
110
|
+
"Second NodeSet iteration (#{allocs}) should allocate <= #{threshold_for(
|
|
111
|
+
adapter_name, :cached_iteration
|
|
112
|
+
)}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ----------------------------------------------------------------
|
|
117
|
+
# Round-trip allocation guard
|
|
118
|
+
# ----------------------------------------------------------------
|
|
119
|
+
describe "round-trip allocations" do
|
|
120
|
+
it "parse → serialize → parse stays within budget" do
|
|
121
|
+
xml = generate_xml(50)
|
|
122
|
+
allocs = guard_allocations(adapter_name, :round_trip) do
|
|
123
|
+
doc = ctx.parse(xml)
|
|
124
|
+
serialized = doc.to_xml
|
|
125
|
+
ctx.parse(serialized)
|
|
126
|
+
end
|
|
127
|
+
expect(allocs).to be <= threshold_for(adapter_name, :round_trip)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ----------------------------------------------------------------
|
|
132
|
+
# Scalability guard — allocations grow linearly, not quadratically
|
|
133
|
+
# ----------------------------------------------------------------
|
|
134
|
+
describe "scalability" do
|
|
135
|
+
it "allocation growth is linear with document size" do
|
|
136
|
+
xml_100 = generate_xml(100)
|
|
137
|
+
xml_200 = generate_xml(200)
|
|
138
|
+
|
|
139
|
+
allocs_100 = AllocationHelper.count_allocations { ctx.parse(xml_100) }
|
|
140
|
+
allocs_200 = AllocationHelper.count_allocations { ctx.parse(xml_200) }
|
|
141
|
+
|
|
142
|
+
ratio = allocs_200.to_f / allocs_100
|
|
143
|
+
max_ratio = threshold_for(adapter_name, :scalability_ratio)
|
|
144
|
+
|
|
145
|
+
expect(ratio).to be <= max_ratio,
|
|
146
|
+
"200-element parse (#{allocs_200}) vs 100-element (#{allocs_100}) = #{ratio.round(2)}x, " \
|
|
147
|
+
"expected <= #{max_ratio}x (linear growth)"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ----------------------------------------------------------------
|
|
152
|
+
# Cache invalidation guards — mutation breaks cache
|
|
153
|
+
# ----------------------------------------------------------------
|
|
154
|
+
describe "cache invalidation" do
|
|
155
|
+
it "adding a child invalidates children cache" do
|
|
156
|
+
xml = "<root><a/></root>"
|
|
157
|
+
doc = ctx.parse(xml)
|
|
158
|
+
root = doc.root
|
|
159
|
+
children_before = root.children
|
|
160
|
+
|
|
161
|
+
new_elem = ctx.parse("<b/>").root
|
|
162
|
+
root.add_child(new_elem)
|
|
163
|
+
|
|
164
|
+
children_after = root.children
|
|
165
|
+
expect(children_before).not_to equal(children_after),
|
|
166
|
+
"Children cache should be invalidated after add_child"
|
|
167
|
+
expect(children_after.size).to eq(2)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "setting text invalidates children cache" do
|
|
171
|
+
xml = "<root><a/></root>"
|
|
172
|
+
doc = ctx.parse(xml)
|
|
173
|
+
root = doc.root
|
|
174
|
+
children_before = root.children
|
|
175
|
+
|
|
176
|
+
root.text = "replaced"
|
|
177
|
+
|
|
178
|
+
children_after = root.children
|
|
179
|
+
expect(children_before).not_to equal(children_after),
|
|
180
|
+
"Children cache should be invalidated after text="
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "setting attribute invalidates attributes cache" do
|
|
184
|
+
xml = '<root a="1"/>'
|
|
185
|
+
doc = ctx.parse(xml)
|
|
186
|
+
root = doc.root
|
|
187
|
+
attrs_before = root.attributes
|
|
188
|
+
|
|
189
|
+
root["b"] = "2"
|
|
190
|
+
|
|
191
|
+
attrs_after = root.attributes
|
|
192
|
+
expect(attrs_before).not_to equal(attrs_after),
|
|
193
|
+
"Attributes cache should be invalidated after []="
|
|
194
|
+
expect(attrs_after.size).to eq(2)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "removing attribute invalidates attributes cache" do
|
|
198
|
+
xml = '<root a="1" b="2"/>'
|
|
199
|
+
doc = ctx.parse(xml)
|
|
200
|
+
root = doc.root
|
|
201
|
+
attrs_before = root.attributes
|
|
202
|
+
|
|
203
|
+
root.remove_attribute("a")
|
|
204
|
+
|
|
205
|
+
attrs_after = root.attributes
|
|
206
|
+
expect(attrs_before).not_to equal(attrs_after),
|
|
207
|
+
"Attributes cache should be invalidated after remove_attribute"
|
|
208
|
+
expect(attrs_after.size).to eq(1)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "removing a child invalidates parent's children cache" do
|
|
212
|
+
xml = "<root><a/><b/></root>"
|
|
213
|
+
doc = ctx.parse(xml)
|
|
214
|
+
root = doc.root
|
|
215
|
+
children_before = root.children
|
|
216
|
+
child_a = root.children.first
|
|
217
|
+
|
|
218
|
+
child_a.remove
|
|
219
|
+
|
|
220
|
+
children_after = root.children
|
|
221
|
+
expect(children_before).not_to equal(children_after),
|
|
222
|
+
"Parent's children cache should be invalidated after child.remove"
|
|
223
|
+
expect(children_after.size).to eq(1)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ----------------------------------------------------------------
|
|
228
|
+
# NodeSet wrap cache guards
|
|
229
|
+
# ----------------------------------------------------------------
|
|
230
|
+
describe "NodeSet wrap cache" do
|
|
231
|
+
it "returns the same wrapper for same index" do
|
|
232
|
+
doc = ctx.parse("<root><a/><b/><c/></root>")
|
|
233
|
+
children = doc.root.children
|
|
234
|
+
|
|
235
|
+
first_access = children[0]
|
|
236
|
+
second_access = children[0]
|
|
237
|
+
expect(first_access).to equal(second_access),
|
|
238
|
+
"Same index should return identical wrapper object"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "returns the same wrapper from each as from []" do
|
|
242
|
+
doc = ctx.parse("<root><a/><b/><c/></root>")
|
|
243
|
+
children = doc.root.children
|
|
244
|
+
|
|
245
|
+
from_each = nil
|
|
246
|
+
children.each { |c| from_each = c if c.name == "b" }
|
|
247
|
+
from_index = children[1]
|
|
248
|
+
|
|
249
|
+
expect(from_each).to equal(from_index),
|
|
250
|
+
"Node from #each should be identical to same index from #[]"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "preserves cache across multiple iterations" do
|
|
254
|
+
doc = ctx.parse("<root><a/><b/><c/></root>")
|
|
255
|
+
children = doc.root.children
|
|
256
|
+
|
|
257
|
+
pass1 = children.map(&:name)
|
|
258
|
+
pass2 = children.map(&:name)
|
|
259
|
+
|
|
260
|
+
expect(pass1).to eq(pass2)
|
|
261
|
+
# Also verify object identity between passes
|
|
262
|
+
pass1_nodes = children.to_a
|
|
263
|
+
pass2_nodes = children.to_a
|
|
264
|
+
pass1_nodes.each_with_index do |node, i|
|
|
265
|
+
expect(node).to equal(pass2_nodes[i]),
|
|
266
|
+
"Node #{i} should be identical across iterations"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Run guards for each adapter
|
|
273
|
+
AllocationHelper::GUARDED_ADAPTERS.each do |adapter_name|
|
|
274
|
+
describe "#{adapter_name} adapter" do
|
|
275
|
+
before(:all) do
|
|
276
|
+
skip("#{adapter_name} adapter not available") unless AllocationHelper.adapter_available?(adapter_name)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it_behaves_like "allocation guard", adapter_name
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
data/spec/moxml/builder_spec.rb
CHANGED
|
@@ -5,6 +5,28 @@ require "spec_helper"
|
|
|
5
5
|
RSpec.describe Moxml::Builder do
|
|
6
6
|
let(:context) { Moxml.new }
|
|
7
7
|
|
|
8
|
+
describe "#document" do
|
|
9
|
+
it "exposes the underlying document via document accessor" do
|
|
10
|
+
builder = described_class.new(context)
|
|
11
|
+
expect(builder.document).to be_a(Moxml::Document)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "exposes the underlying document via doc alias" do
|
|
15
|
+
builder = described_class.new(context)
|
|
16
|
+
expect(builder.doc).to be_a(Moxml::Document)
|
|
17
|
+
expect(builder.doc).to equal(builder.document)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "returns the same document before and after build" do
|
|
21
|
+
builder = described_class.new(context)
|
|
22
|
+
doc_before = builder.document
|
|
23
|
+
builder.build do
|
|
24
|
+
element "root"
|
|
25
|
+
end
|
|
26
|
+
expect(builder.document).to equal(doc_before)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
8
30
|
describe "#build" do
|
|
9
31
|
it "builds a document with DSL" do
|
|
10
32
|
doc = described_class.new(context).build do
|
data/spec/moxml/config_spec.rb
CHANGED
|
@@ -17,8 +17,8 @@ RSpec.describe Moxml::Config do
|
|
|
17
17
|
expect(config.entity_load_mode).to eq(:required)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
it "sets default
|
|
21
|
-
expect(config.
|
|
20
|
+
it "sets default namespace_validation_mode to :strict" do
|
|
21
|
+
expect(config.namespace_validation_mode).to eq(:strict)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -66,26 +66,26 @@ RSpec.describe Moxml::Config do
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
describe "#
|
|
69
|
+
describe "#namespace_validation_mode=" do
|
|
70
70
|
it "accepts :strict" do
|
|
71
|
-
config.
|
|
72
|
-
expect(config.
|
|
71
|
+
config.namespace_validation_mode = :strict
|
|
72
|
+
expect(config.namespace_validation_mode).to eq(:strict)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
it "accepts :lenient" do
|
|
76
|
-
config.
|
|
77
|
-
expect(config.
|
|
76
|
+
config.namespace_validation_mode = :lenient
|
|
77
|
+
expect(config.namespace_validation_mode).to eq(:lenient)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
it "accepts string values" do
|
|
81
|
-
config.
|
|
82
|
-
expect(config.
|
|
81
|
+
config.namespace_validation_mode = "lenient"
|
|
82
|
+
expect(config.namespace_validation_mode).to eq(:lenient)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
it "raises error for invalid mode" do
|
|
86
86
|
expect do
|
|
87
|
-
config.
|
|
88
|
-
end.to raise_error(ArgumentError, /Invalid
|
|
87
|
+
config.namespace_validation_mode = :invalid
|
|
88
|
+
end.to raise_error(ArgumentError, /Invalid namespace_validation_mode/)
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
data/spec/moxml/doctype_spec.rb
CHANGED
|
@@ -46,4 +46,45 @@ RSpec.describe Moxml::Doctype do
|
|
|
46
46
|
expect(doctype.name).to eq("html")
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
describe "parsing" do
|
|
51
|
+
%i[nokogiri oga rexml ox].each do |adapter_name|
|
|
52
|
+
context "with #{adapter_name} adapter" do
|
|
53
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
54
|
+
|
|
55
|
+
it "parses PUBLIC doctype with external and system identifiers" do
|
|
56
|
+
xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html/>'
|
|
57
|
+
doc = ctx.parse(xml)
|
|
58
|
+
doctype = doc.children.find { |c| c.is_a?(described_class) }
|
|
59
|
+
|
|
60
|
+
expect(doctype).not_to be_nil
|
|
61
|
+
expect(doctype.name).to eq("html")
|
|
62
|
+
expect(doctype.external_id).to eq("-//W3C//DTD XHTML 1.0 Strict//EN")
|
|
63
|
+
expect(doctype.system_id).to eq("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "parses SYSTEM doctype with system identifier only" do
|
|
67
|
+
xml = '<!DOCTYPE config SYSTEM "config.dtd"><config/>'
|
|
68
|
+
doc = ctx.parse(xml)
|
|
69
|
+
doctype = doc.children.find { |c| c.is_a?(described_class) }
|
|
70
|
+
|
|
71
|
+
expect(doctype).not_to be_nil
|
|
72
|
+
expect(doctype.name).to eq("config")
|
|
73
|
+
expect(doctype.external_id).to be_nil
|
|
74
|
+
expect(doctype.system_id).to eq("config.dtd")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "parses simple doctype without identifiers" do
|
|
78
|
+
xml = "<!DOCTYPE html><html/>"
|
|
79
|
+
doc = ctx.parse(xml)
|
|
80
|
+
doctype = doc.children.find { |c| c.is_a?(described_class) }
|
|
81
|
+
|
|
82
|
+
expect(doctype).not_to be_nil
|
|
83
|
+
expect(doctype.name).to eq("html")
|
|
84
|
+
expect(doctype.external_id).to be_nil
|
|
85
|
+
expect(doctype.system_id).to be_nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
49
90
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/allocation_helper"
|
|
5
|
+
|
|
6
|
+
# Lazy parse correctness tests — these run in CI by default.
|
|
7
|
+
# Verifies that lazy parse produces correct document structure
|
|
8
|
+
# across all adapters without eager wrapper construction.
|
|
9
|
+
RSpec.describe "Moxml lazy parse" do
|
|
10
|
+
let(:xml) do
|
|
11
|
+
"<root><child><nested>text</nested></child><sibling>more</sibling></root>"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
shared_examples "lazy parse behavior" do |adapter_name|
|
|
15
|
+
let(:ctx) { Moxml::Context.new(adapter_name) }
|
|
16
|
+
|
|
17
|
+
describe "#parse" do
|
|
18
|
+
it "returns a Document without eagerly building wrapper tree" do
|
|
19
|
+
doc = ctx.parse(xml)
|
|
20
|
+
expect(doc).to be_a(Moxml::Document)
|
|
21
|
+
expect(doc.root).to be_a(Moxml::Element)
|
|
22
|
+
expect(doc.root.name).to eq("root")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "provides correct children via lazy access" do
|
|
26
|
+
doc = ctx.parse(xml)
|
|
27
|
+
root = doc.root
|
|
28
|
+
children = root.children.to_a
|
|
29
|
+
expect(children.size).to eq(2)
|
|
30
|
+
expect(children[0]).to be_a(Moxml::Element)
|
|
31
|
+
expect(children[0].name).to eq("child")
|
|
32
|
+
expect(children[1].name).to eq("sibling")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "provides correct nested children" do
|
|
36
|
+
doc = ctx.parse(xml)
|
|
37
|
+
nested = doc.root.children[0].children[0]
|
|
38
|
+
expect(nested.name).to eq("nested")
|
|
39
|
+
expect(nested.text).to eq("text")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "preserves attributes" do
|
|
43
|
+
xml_with_attrs = '<root a="1" b="2"><child c="3"/></root>'
|
|
44
|
+
doc = ctx.parse(xml_with_attrs)
|
|
45
|
+
root = doc.root
|
|
46
|
+
attrs = root.attributes
|
|
47
|
+
expect(attrs.size).to eq(2)
|
|
48
|
+
expect(root["a"]).to eq("1")
|
|
49
|
+
expect(root["b"]).to eq("2")
|
|
50
|
+
expect(root.children[0]["c"]).to eq("3")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "preserves text content" do
|
|
54
|
+
doc = ctx.parse("<root>hello world</root>")
|
|
55
|
+
expect(doc.root.text).to eq("hello world")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "preserves mixed content" do
|
|
59
|
+
mixed_xml = "<root>before<child/>after</root>"
|
|
60
|
+
doc = ctx.parse(mixed_xml)
|
|
61
|
+
expect(doc.root.text).to eq("beforeafter")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "handles comments" do
|
|
65
|
+
comment_xml = "<root><!-- a comment --><child/></root>"
|
|
66
|
+
doc = ctx.parse(comment_xml)
|
|
67
|
+
children = doc.root.children.to_a
|
|
68
|
+
comment = children.find(&:comment?)
|
|
69
|
+
expect(comment).not_to be_nil
|
|
70
|
+
expect(comment.content).to include("a comment")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "handles processing instructions" do
|
|
74
|
+
pi_xml = "<?pi-target pi-content?><root/>"
|
|
75
|
+
doc = ctx.parse(pi_xml)
|
|
76
|
+
expect(doc.root.name).to eq("root")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "handles namespace declarations" do
|
|
80
|
+
ns_xml = '<root xmlns:ns="http://example.com"><ns:child/></root>'
|
|
81
|
+
doc = ctx.parse(ns_xml)
|
|
82
|
+
root = doc.root
|
|
83
|
+
nss = root.namespaces
|
|
84
|
+
expect(nss.size).to be >= 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "round-trips through serialize" do
|
|
88
|
+
doc = ctx.parse(xml)
|
|
89
|
+
serialized = doc.to_xml
|
|
90
|
+
doc2 = ctx.parse(serialized)
|
|
91
|
+
expect(doc2.root.name).to eq("root")
|
|
92
|
+
expect(doc2.root.children.to_a.size).to eq(2)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Run for all guarded adapters
|
|
98
|
+
AllocationHelper::GUARDED_ADAPTERS.each do |adapter_name|
|
|
99
|
+
describe "#{adapter_name} adapter" do
|
|
100
|
+
before(:all) do
|
|
101
|
+
skip("#{adapter_name} adapter not available") unless AllocationHelper.adapter_available?(adapter_name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it_behaves_like "lazy parse behavior", adapter_name
|
|
105
|
+
|
|
106
|
+
# CDATA behavior differs between adapters
|
|
107
|
+
it "handles CDATA sections" do
|
|
108
|
+
ctx = Moxml::Context.new(adapter_name)
|
|
109
|
+
cdata_xml = "<root><![CDATA[<not xml>]]></root>"
|
|
110
|
+
doc = ctx.parse(cdata_xml)
|
|
111
|
+
expect(doc.root).not_to be_nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -92,10 +92,10 @@ RSpec.describe "Namespace URI validation" do
|
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
describe "lenient
|
|
95
|
+
describe "lenient namespace_validation_mode" do
|
|
96
96
|
let(:context) do
|
|
97
97
|
Moxml.new do |config|
|
|
98
|
-
config.
|
|
98
|
+
config.namespace_validation_mode = :lenient
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
@@ -134,7 +134,15 @@ RSpec.describe "Namespace URI validation" do
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
it "still accepts valid URIs" do
|
|
137
|
-
expect
|
|
137
|
+
expect do
|
|
138
|
+
element.add_namespace("ns", "http://example.com")
|
|
139
|
+
end.not_to raise_error
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "accepts prefixes with dots in lenient mode" do
|
|
143
|
+
expect do
|
|
144
|
+
element.add_namespace("xmlns_1.0", "http://example.com")
|
|
145
|
+
end.not_to raise_error
|
|
138
146
|
end
|
|
139
147
|
end
|
|
140
148
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/allocation_helper"
|
|
5
|
+
|
|
6
|
+
# Node caching correctness tests — these run in CI by default.
|
|
7
|
+
# Verifies that cache semantics (identity, invalidation) work correctly
|
|
8
|
+
# across all adapters.
|
|
9
|
+
RSpec.describe "Moxml node caching" do
|
|
10
|
+
shared_examples "cached children" do |adapter_name|
|
|
11
|
+
let(:ctx) { Moxml::Context.new(adapter_name) }
|
|
12
|
+
let(:xml) { "<root><a/><b/><c/></root>" }
|
|
13
|
+
let(:doc) { ctx.parse(xml) }
|
|
14
|
+
let(:root) { doc.root }
|
|
15
|
+
|
|
16
|
+
describe "Node#children caching" do
|
|
17
|
+
it "returns the same NodeSet object on repeated calls" do
|
|
18
|
+
children1 = root.children
|
|
19
|
+
children2 = root.children
|
|
20
|
+
expect(children1).to equal(children2)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns consistent child elements across calls" do
|
|
24
|
+
children1 = root.children.to_a
|
|
25
|
+
children2 = root.children.to_a
|
|
26
|
+
expect(children1.map(&:name)).to eq(children2.map(&:name))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "invalidates cache when a child is added" do
|
|
30
|
+
children_before = root.children
|
|
31
|
+
root.add_child(ctx.parse("<d/>").root)
|
|
32
|
+
children_after = root.children
|
|
33
|
+
expect(children_before).not_to equal(children_after)
|
|
34
|
+
expect(children_after.to_a.size).to eq(4)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "invalidates cache when text is set" do
|
|
38
|
+
children_before = root.children
|
|
39
|
+
root.text = "new text"
|
|
40
|
+
children_after = root.children
|
|
41
|
+
expect(children_before).not_to equal(children_after)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "Element#attributes caching" do
|
|
46
|
+
let(:attr_xml) { '<root a="1" b="2"><child c="3"/></root>' }
|
|
47
|
+
let(:attr_doc) { ctx.parse(attr_xml) }
|
|
48
|
+
let(:attr_root) { attr_doc.root }
|
|
49
|
+
|
|
50
|
+
it "returns the same array on repeated calls" do
|
|
51
|
+
attrs1 = attr_root.attributes
|
|
52
|
+
attrs2 = attr_root.attributes
|
|
53
|
+
expect(attrs1).to equal(attrs2)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "returns consistent attribute values" do
|
|
57
|
+
attrs = attr_root.attributes
|
|
58
|
+
expect(attrs.to_h do |a|
|
|
59
|
+
[a.name, a.value]
|
|
60
|
+
end).to eq({ "a" => "1", "b" => "2" })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "invalidates cache when an attribute is set" do
|
|
64
|
+
attrs_before = attr_root.attributes
|
|
65
|
+
attr_root["c"] = "3"
|
|
66
|
+
attrs_after = attr_root.attributes
|
|
67
|
+
expect(attrs_before).not_to equal(attrs_after)
|
|
68
|
+
expect(attrs_after.size).to eq(3)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "invalidates cache when an attribute is removed" do
|
|
72
|
+
attrs_before = attr_root.attributes
|
|
73
|
+
attr_root.remove_attribute("a")
|
|
74
|
+
attrs_after = attr_root.attributes
|
|
75
|
+
expect(attrs_before).not_to equal(attrs_after)
|
|
76
|
+
expect(attrs_after.size).to eq(1)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "Element#namespaces caching" do
|
|
81
|
+
let(:ns_xml) { '<root xmlns:a="http://a.com" xmlns:b="http://b.com"><a:child/></root>' }
|
|
82
|
+
let(:ns_doc) { ctx.parse(ns_xml) }
|
|
83
|
+
let(:ns_root) { ns_doc.root }
|
|
84
|
+
|
|
85
|
+
it "returns the same array on repeated calls" do
|
|
86
|
+
nss1 = ns_root.namespaces
|
|
87
|
+
nss2 = ns_root.namespaces
|
|
88
|
+
expect(nss1).to equal(nss2)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "invalidates cache when a namespace is added" do
|
|
92
|
+
nss_before = ns_root.namespaces
|
|
93
|
+
ns_root.add_namespace("c", "http://c.com")
|
|
94
|
+
nss_after = ns_root.namespaces
|
|
95
|
+
expect(nss_before).not_to equal(nss_after)
|
|
96
|
+
expect(nss_after.size).to eq(3)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
AllocationHelper::GUARDED_ADAPTERS.each do |adapter_name|
|
|
102
|
+
describe "#{adapter_name} adapter" do
|
|
103
|
+
before(:all) do
|
|
104
|
+
skip("#{adapter_name} adapter not available") unless AllocationHelper.adapter_available?(adapter_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it_behaves_like "cached children", adapter_name
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|