moxml 0.1.14 → 0.1.16
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 +34 -1
- data/TODO.remaining/1-entity-reference-adapter-support.md +157 -0
- data/TODO.remaining/2-entity-restoration-model-driven.md +169 -0
- data/TODO.remaining/3-entity-reference-test-coverage.md +170 -0
- data/TODO.remaining/4-lenient-entities-mode.md +106 -0
- data/TODO.remaining/5-fixture-integrity.md +65 -0
- data/TODO.remaining/6-ox-element-ordering-bug.md +36 -0
- data/TODO.remaining/7-headed-ox-limitations.md +95 -0
- data/TODO.remaining/8-xpath-predicate-gaps.md +68 -0
- data/TODO.remaining/9-cleanup-hygiene.md +42 -0
- data/TODO.remaining/README.md +54 -0
- data/benchmarks/generate_report.rb +1 -1
- data/docs/_pages/configuration.adoc +22 -19
- data/docs/_tutorials/namespace-handling.adoc +5 -5
- data/lib/moxml/adapter/base.rb +22 -3
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -1
- 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.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 +44 -20
- data/lib/moxml/adapter/customized_rexml.rb +11 -0
- data/lib/moxml/adapter/headed_ox.rb +37 -14
- data/lib/moxml/adapter/libxml.rb +233 -119
- data/lib/moxml/adapter/nokogiri.rb +22 -11
- data/lib/moxml/adapter/oga.rb +64 -25
- data/lib/moxml/adapter/ox.rb +198 -42
- data/lib/moxml/adapter/rexml.rb +64 -13
- data/lib/moxml/attribute.rb +3 -0
- data/lib/moxml/builder.rb +78 -24
- data/lib/moxml/config.rb +24 -7
- data/lib/moxml/declaration.rb +4 -2
- data/lib/moxml/document.rb +8 -1
- data/lib/moxml/document_builder.rb +44 -37
- data/lib/moxml/element.rb +18 -5
- data/lib/moxml/entity_registry.rb +51 -1
- data/lib/moxml/native_attachment.rb +65 -0
- data/lib/moxml/node.rb +39 -50
- data/lib/moxml/node_set.rb +43 -15
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +4 -1
- data/lib/moxml.rb +1 -0
- data/scripts/format_xml.rb +16 -0
- data/scripts/pretty_format_xml.rb +14 -0
- data/spec/consistency/round_trip_spec.rb +3 -30
- data/spec/integration/all_adapters_spec.rb +1 -0
- data/spec/integration/headed_ox_integration_spec.rb +0 -2
- data/spec/integration/shared_examples/edge_cases.rb +7 -4
- data/spec/integration/shared_examples/integration_workflows.rb +3 -3
- data/spec/integration/shared_examples/node_wrappers/cdata_behavior.rb +1 -1
- data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +224 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +1 -1
- data/spec/moxml/adapter/headed_ox_spec.rb +8 -8
- data/spec/moxml/adapter/oga_spec.rb +46 -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 +256 -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/moxml/xpath/axes_spec.rb +1 -1
- data/spec/moxml/xpath/compiler_spec.rb +2 -2
- data/spec/moxml/xpath/functions/position_functions_spec.rb +5 -5
- data/spec/moxml/xpath/functions/special_functions_spec.rb +1 -1
- data/spec/performance/memory_usage_spec.rb +0 -4
- data/spec/support/allocation_helper.rb +165 -0
- data/spec/support/w3c_namespace_helpers.rb +2 -1
- metadata +29 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_examples "Moxml::EntityReference" do
|
|
4
|
+
let(:context) { Moxml.new }
|
|
5
|
+
|
|
6
|
+
describe "entity reference node" do
|
|
7
|
+
let(:doc) { context.create_document }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
root = doc.create_element("root")
|
|
11
|
+
doc.add_child(root)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "creation" do
|
|
15
|
+
it "creates an entity reference node" do
|
|
16
|
+
ref = doc.create_entity_reference("nbsp")
|
|
17
|
+
expect(ref).to be_a(Moxml::EntityReference)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "has empty text content" do
|
|
21
|
+
ref = doc.create_entity_reference("amp")
|
|
22
|
+
expect(ref.text).to eq("")
|
|
23
|
+
expect(ref.content).to eq("")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "exposes entity name" do
|
|
27
|
+
ref = doc.create_entity_reference("mdash")
|
|
28
|
+
expect(ref.name).to eq("mdash")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "is recognized as entity_reference type" do
|
|
32
|
+
ref = doc.create_entity_reference("copy")
|
|
33
|
+
expect(ref.entity_reference?).to be true
|
|
34
|
+
expect(ref).not_to be_element
|
|
35
|
+
expect(ref).not_to be_text
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "validates entity reference name" do
|
|
39
|
+
expect { doc.create_entity_reference("123invalid") }.to raise_error(Moxml::ValidationError)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "serialization" do
|
|
44
|
+
it "serializes to entity syntax" do
|
|
45
|
+
ref = doc.create_entity_reference("nbsp")
|
|
46
|
+
expect(ref.to_xml).to eq(" ")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "serializes standard XML entities" do
|
|
50
|
+
ref = doc.create_entity_reference("amp")
|
|
51
|
+
expect(ref.to_xml).to eq("&")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "tree integration" do
|
|
56
|
+
it "survives add_child and retrieval" do
|
|
57
|
+
root = doc.root
|
|
58
|
+
ref = doc.create_entity_reference("nbsp")
|
|
59
|
+
root.add_child(ref)
|
|
60
|
+
children = root.children
|
|
61
|
+
expect(children.size).to be >= 1
|
|
62
|
+
first = children.last
|
|
63
|
+
expect(first).to be_a(Moxml::EntityReference)
|
|
64
|
+
expect(first.name).to eq("nbsp")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "serializes within element content" do
|
|
68
|
+
root = doc.root
|
|
69
|
+
root.add_child(doc.create_text("before"))
|
|
70
|
+
root.add_child(doc.create_entity_reference("nbsp"))
|
|
71
|
+
root.add_child(doc.create_text("after"))
|
|
72
|
+
xml = doc.to_xml
|
|
73
|
+
expect(xml).to include(" ")
|
|
74
|
+
expect(xml).to include("before")
|
|
75
|
+
expect(xml).to include("after")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "supports multiple entity references" do
|
|
79
|
+
root = doc.root
|
|
80
|
+
root.add_child(doc.create_entity_reference("nbsp"))
|
|
81
|
+
root.add_child(doc.create_entity_reference("copy"))
|
|
82
|
+
root.add_child(doc.create_entity_reference("mdash"))
|
|
83
|
+
xml = doc.to_xml
|
|
84
|
+
expect(xml).to include(" ")
|
|
85
|
+
expect(xml).to include("©")
|
|
86
|
+
expect(xml).to include("—")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "equality" do
|
|
91
|
+
it "has the same name as another entity reference with same name" do
|
|
92
|
+
ref1 = doc.create_entity_reference("amp")
|
|
93
|
+
ref2 = doc.create_entity_reference("amp")
|
|
94
|
+
expect(ref1.name).to eq(ref2.name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "does not equal entity reference with different name" do
|
|
98
|
+
ref1 = doc.create_entity_reference("amp")
|
|
99
|
+
ref2 = doc.create_entity_reference("nbsp")
|
|
100
|
+
expect(ref1.name).not_to eq(ref2.name)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe "entity reference via builder DSL" do
|
|
106
|
+
it "creates entity references" do
|
|
107
|
+
built = Moxml::Builder.new(context).build do
|
|
108
|
+
element("p") { entity_reference("nbsp") }
|
|
109
|
+
end
|
|
110
|
+
expect(built.root.children.last).to be_a(Moxml::EntityReference)
|
|
111
|
+
expect(built.root.children.last.name).to eq("nbsp")
|
|
112
|
+
expect(built.to_xml).to include(" ")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "mixes text and entity references" do
|
|
116
|
+
built = Moxml::Builder.new(context).build do
|
|
117
|
+
element("p") do
|
|
118
|
+
text "Copyright"
|
|
119
|
+
entity_reference("copy")
|
|
120
|
+
text " 2024"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
xml = built.to_xml
|
|
124
|
+
expect(xml).to include("Copyright")
|
|
125
|
+
expect(xml).to include("©")
|
|
126
|
+
expect(xml).to include("2024")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "entity restoration" do
|
|
131
|
+
it "restores standard XML entities when enabled" do
|
|
132
|
+
ctx_restore = Moxml.new(context.config.adapter_name) { |c| c.restore_entities = true }
|
|
133
|
+
doc = ctx_restore.parse("<p>a&b</p>")
|
|
134
|
+
output = doc.to_xml
|
|
135
|
+
expect(output).to include("&")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "does not create entity references when disabled" do
|
|
139
|
+
ctx_no_restore = Moxml.new(context.config.adapter_name) { |c| c.restore_entities = false }
|
|
140
|
+
doc = ctx_no_restore.parse("<p>text</p>")
|
|
141
|
+
refs = doc.root.children.select { |c| c.is_a?(Moxml::EntityReference) }
|
|
142
|
+
expect(refs).to be_empty
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe "EntityRegistry#should_restore?" do
|
|
147
|
+
let(:registry) { context.entity_registry }
|
|
148
|
+
|
|
149
|
+
let(:config_on) do
|
|
150
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
151
|
+
cfg.restore_entities = true
|
|
152
|
+
cfg
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
let(:config_off) do
|
|
156
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
157
|
+
cfg.restore_entities = false
|
|
158
|
+
cfg
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "always restores the 5 standard XML entities" do
|
|
162
|
+
expect(registry.should_restore?(0x26, config: config_on)).to be true # amp
|
|
163
|
+
expect(registry.should_restore?(0x3C, config: config_on)).to be true # lt
|
|
164
|
+
expect(registry.should_restore?(0x3E, config: config_on)).to be true # gt
|
|
165
|
+
expect(registry.should_restore?(0x22, config: config_on)).to be true # quot
|
|
166
|
+
expect(registry.should_restore?(0x27, config: config_on)).to be true # apos
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "restores standard XML entities even when restore_entities is false" do
|
|
170
|
+
expect(registry.should_restore?(0x26, config: config_off)).to be true # amp
|
|
171
|
+
expect(registry.should_restore?(0x3C, config: config_off)).to be true # lt
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "restores non-standard entities only when restore_entities is true" do
|
|
175
|
+
expect(registry.should_restore?(0xA0, config: config_on)).to be true # nbsp
|
|
176
|
+
expect(registry.should_restore?(0xA0, config: config_off)).to be false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "does not restore unknown codepoints" do
|
|
180
|
+
expect(registry.should_restore?(0xDEAD, config: config_on)).to be false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe "entity_restoration_mode" do
|
|
185
|
+
let(:registry) { context.entity_registry }
|
|
186
|
+
|
|
187
|
+
it "defaults to :lenient" do
|
|
188
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
189
|
+
expect(cfg.entity_restoration_mode).to eq(:lenient)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "accepts :strict" do
|
|
193
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
194
|
+
cfg.entity_restoration_mode = :strict
|
|
195
|
+
expect(cfg.entity_restoration_mode).to eq(:strict)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "rejects invalid modes" do
|
|
199
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
200
|
+
expect { cfg.entity_restoration_mode = :bogus }.to raise_error(ArgumentError)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "restores non-standard entities in lenient mode" do
|
|
204
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
205
|
+
cfg.restore_entities = true
|
|
206
|
+
cfg.entity_restoration_mode = :lenient
|
|
207
|
+
expect(registry.should_restore?(0xA0, config: cfg)).to be true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "restores non-standard entities in strict mode (until DTD parsing)" do
|
|
211
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
212
|
+
cfg.restore_entities = true
|
|
213
|
+
cfg.entity_restoration_mode = :strict
|
|
214
|
+
expect(registry.should_restore?(0xA0, config: cfg)).to be true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "does not restore non-standard entities when restore_entities is false" do
|
|
218
|
+
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
219
|
+
cfg.restore_entities = false
|
|
220
|
+
cfg.entity_restoration_mode = :lenient
|
|
221
|
+
expect(registry.should_restore?(0xA0, config: cfg)).to be false
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -110,7 +110,7 @@ RSpec.shared_examples "Moxml::Node" do
|
|
|
110
110
|
pending "Ox doesn't have a native XPath"
|
|
111
111
|
end
|
|
112
112
|
if context.config.adapter_name == :headed_ox
|
|
113
|
-
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/
|
|
113
|
+
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/_pages/headed-ox-limitations.adoc"
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
node = doc.at_xpath("//b")
|
|
@@ -41,7 +41,7 @@ RSpec.describe Moxml::Adapter::HeadedOx do
|
|
|
41
41
|
it "executes simple XPath queries" do
|
|
42
42
|
result = adapter.xpath(doc, "/root/book")
|
|
43
43
|
|
|
44
|
-
expect(result).to be_a(
|
|
44
|
+
expect(result).to be_a(Array)
|
|
45
45
|
expect(result.size).to eq(3)
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -71,19 +71,19 @@ RSpec.describe Moxml::Adapter::HeadedOx do
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
it "supports XPath string functions in predicates" do
|
|
74
|
-
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
|
|
75
74
|
result = adapter.xpath(doc, "//book[contains(title, '2')]")
|
|
76
75
|
|
|
77
76
|
expect(result.size).to eq(1)
|
|
78
|
-
|
|
77
|
+
book = Moxml::Node.wrap(result.first, doc.context)
|
|
78
|
+
expect(book.xpath("title").first.text).to eq("Book 2")
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
it "supports XPath position functions" do
|
|
82
|
-
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
|
|
83
82
|
result = adapter.xpath(doc, "//book[position() = 2]")
|
|
84
83
|
|
|
85
84
|
expect(result.size).to eq(1)
|
|
86
|
-
|
|
85
|
+
book = Moxml::Node.wrap(result.first, doc.context)
|
|
86
|
+
expect(book.xpath("title").first.text).to eq("Book 2")
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
it "supports descendant axis" do
|
|
@@ -145,7 +145,7 @@ RSpec.describe Moxml::Adapter::HeadedOx do
|
|
|
145
145
|
it "returns first matching node" do
|
|
146
146
|
result = adapter.at_xpath(doc, "//book")
|
|
147
147
|
|
|
148
|
-
expect(result).to be_a(
|
|
148
|
+
expect(result).to be_a(::Ox::Element)
|
|
149
149
|
expect(result.name).to eq("book")
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -301,10 +301,10 @@ RSpec.describe Moxml::Adapter::HeadedOx do
|
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
it "supports last()" do
|
|
304
|
-
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
|
|
305
304
|
result = adapter.xpath(doc, "//book[position() = last()]")
|
|
306
305
|
expect(result.size).to eq(1)
|
|
307
|
-
|
|
306
|
+
book = Moxml::Node.wrap(result.first, doc.context)
|
|
307
|
+
expect(book.xpath("title").first.text).to eq("Book 3")
|
|
308
308
|
end
|
|
309
309
|
end
|
|
310
310
|
end
|
|
@@ -119,4 +119,50 @@ RSpec.describe Moxml::Adapter::Oga do
|
|
|
119
119
|
expect(serialized).not_to include("\x01")
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
|
+
|
|
123
|
+
describe "doctype handling" do
|
|
124
|
+
it "correctly parses PUBLIC doctype" do
|
|
125
|
+
xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html/>'
|
|
126
|
+
doc = described_class.parse(xml)
|
|
127
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
128
|
+
|
|
129
|
+
expect(doctype.name).to eq("html")
|
|
130
|
+
expect(doctype.external_id).to eq("-//W3C//DTD XHTML 1.0 Strict//EN")
|
|
131
|
+
expect(doctype.system_id).to eq("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "correctly parses SYSTEM doctype" do
|
|
135
|
+
xml = '<!DOCTYPE config SYSTEM "config.dtd"><config/>'
|
|
136
|
+
doc = described_class.parse(xml)
|
|
137
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
138
|
+
|
|
139
|
+
expect(doctype.name).to eq("config")
|
|
140
|
+
expect(doctype.external_id).to be_nil
|
|
141
|
+
expect(doctype.system_id).to eq("config.dtd")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "correctly parses simple doctype" do
|
|
145
|
+
xml = "<!DOCTYPE html><html/>"
|
|
146
|
+
doc = described_class.parse(xml)
|
|
147
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
148
|
+
|
|
149
|
+
expect(doctype.name).to eq("html")
|
|
150
|
+
expect(doctype.external_id).to be_nil
|
|
151
|
+
expect(doctype.system_id).to be_nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "round-trips PUBLIC doctype" do
|
|
155
|
+
xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html/>'
|
|
156
|
+
doc = described_class.parse(xml)
|
|
157
|
+
|
|
158
|
+
expect(doc.to_xml).to include('PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "round-trips SYSTEM doctype" do
|
|
162
|
+
xml = '<!DOCTYPE config SYSTEM "config.dtd"><config/>'
|
|
163
|
+
doc = described_class.parse(xml)
|
|
164
|
+
|
|
165
|
+
expect(doc.to_xml).to include('SYSTEM "config.dtd"')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
122
168
|
end
|
|
@@ -4,18 +4,7 @@
|
|
|
4
4
|
# A better way is to run it through Moxml wrappers
|
|
5
5
|
RSpec.shared_examples "xml adapter" do
|
|
6
6
|
let(:xml) do
|
|
7
|
-
|
|
8
|
-
<?xml version="1.0"?>
|
|
9
|
-
<root xmlns="http://example.org" xmlns:x="http://example.org/x">
|
|
10
|
-
<child id="1">Text</child>
|
|
11
|
-
<child id="2"/>
|
|
12
|
-
<x:special>
|
|
13
|
-
<![CDATA[Some <special> text]]>
|
|
14
|
-
<!-- A comment -->
|
|
15
|
-
<?pi target?>
|
|
16
|
-
</x:special>
|
|
17
|
-
</root>
|
|
18
|
-
XML
|
|
7
|
+
'<?xml version="1.0"?><root xmlns="http://example.org" xmlns:x="http://example.org/x"><child id="1">Text</child><child id="2"/><x:special><![CDATA[Some <special> text]]><!-- A comment --><?pi target?></x:special></root>'
|
|
19
8
|
end
|
|
20
9
|
|
|
21
10
|
describe ".parse" do
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/allocation_helper"
|
|
5
|
+
|
|
6
|
+
# Detailed allocation benchmarks — only run with RUN_PERFORMANCE=1.
|
|
7
|
+
# These measure exact allocation counts and compare across adapters.
|
|
8
|
+
RSpec.describe "Moxml allocation benchmarks", :performance do
|
|
9
|
+
shared_examples "reduced allocations" do |adapter_name|
|
|
10
|
+
let(:ctx) { Moxml::Context.new(adapter_name) }
|
|
11
|
+
|
|
12
|
+
it "parse allocates fewer objects than a 100-element baseline" do
|
|
13
|
+
xml = generate_xml(100)
|
|
14
|
+
allocs = AllocationHelper.count_allocations { ctx.parse(xml) }
|
|
15
|
+
# Before lazy parse: ~18,000 allocations for 100 elements via DocumentBuilder
|
|
16
|
+
# After lazy parse: should be dramatically less (document wrapper + root only)
|
|
17
|
+
expect(allocs).to be < 5000,
|
|
18
|
+
"Expected <5000 allocations for 100-element parse, got #{allocs}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "parse + root access is allocation-efficient" do
|
|
22
|
+
xml = generate_xml(50)
|
|
23
|
+
allocs = AllocationHelper.count_allocations do
|
|
24
|
+
doc = ctx.parse(xml)
|
|
25
|
+
doc.root.name
|
|
26
|
+
end
|
|
27
|
+
expect(allocs).to be < 2000,
|
|
28
|
+
"Expected <2000 allocations for parse + root.name, got #{allocs}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "children access is cached (repeated calls don't increase allocations)" do
|
|
32
|
+
xml = "<root><a/><b/><c/></root>"
|
|
33
|
+
doc = ctx.parse(xml)
|
|
34
|
+
root = doc.root
|
|
35
|
+
|
|
36
|
+
allocs1 = AllocationHelper.count_allocations { root.children.to_a }
|
|
37
|
+
allocs2 = AllocationHelper.count_allocations { root.children.to_a }
|
|
38
|
+
|
|
39
|
+
# Second call should allocate fewer objects because children are cached
|
|
40
|
+
expect(allocs2).to be <= allocs1,
|
|
41
|
+
"Second children.to_a (#{allocs2}) should allocate <= first (#{allocs1})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "attributes access is cached" do
|
|
45
|
+
xml = '<root a="1" b="2" c="3"><child d="4"/></root>'
|
|
46
|
+
doc = ctx.parse(xml)
|
|
47
|
+
root = doc.root
|
|
48
|
+
|
|
49
|
+
allocs1 = AllocationHelper.count_allocations { root.attributes }
|
|
50
|
+
allocs2 = AllocationHelper.count_allocations { root.attributes }
|
|
51
|
+
|
|
52
|
+
expect(allocs2).to be <= allocs1,
|
|
53
|
+
"Second attributes call (#{allocs2}) should allocate <= first (#{allocs1})"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "namespaces access is cached" do
|
|
57
|
+
xml = '<root xmlns:a="http://a.com" xmlns:b="http://b.com"><a:child/></root>'
|
|
58
|
+
doc = ctx.parse(xml)
|
|
59
|
+
root = doc.root
|
|
60
|
+
|
|
61
|
+
allocs1 = AllocationHelper.count_allocations { root.namespaces }
|
|
62
|
+
allocs2 = AllocationHelper.count_allocations { root.namespaces }
|
|
63
|
+
|
|
64
|
+
expect(allocs2).to be <= allocs1,
|
|
65
|
+
"Second namespaces call (#{allocs2}) should allocate <= first (#{allocs1})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "NodeSet iteration is cached (second iteration allocates less)" do
|
|
69
|
+
xml = generate_xml(20)
|
|
70
|
+
doc = ctx.parse(xml)
|
|
71
|
+
root = doc.root
|
|
72
|
+
|
|
73
|
+
allocs1 = AllocationHelper.count_allocations do
|
|
74
|
+
root.children.each do |_c|
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
allocs2 = AllocationHelper.count_allocations do
|
|
78
|
+
root.children.each do |_c|
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
expect(allocs2).to be <= allocs1,
|
|
83
|
+
"Second NodeSet iteration (#{allocs2}) should allocate <= first (#{allocs1})"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
AllocationHelper::GUARDED_ADAPTERS.each do |adapter_name|
|
|
88
|
+
describe "#{adapter_name} adapter" do
|
|
89
|
+
before(:all) do
|
|
90
|
+
skip("#{adapter_name} adapter not available") unless AllocationHelper.adapter_available?(adapter_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it_behaves_like "reduced allocations", adapter_name
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|