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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +117 -66
  3. data/Gemfile +1 -0
  4. data/README.adoc +11 -9
  5. data/Rakefile +34 -1
  6. data/TODO.remaining/1-entity-reference-adapter-support.md +157 -0
  7. data/TODO.remaining/2-entity-restoration-model-driven.md +169 -0
  8. data/TODO.remaining/3-entity-reference-test-coverage.md +170 -0
  9. data/TODO.remaining/4-lenient-entities-mode.md +106 -0
  10. data/TODO.remaining/5-fixture-integrity.md +65 -0
  11. data/TODO.remaining/6-ox-element-ordering-bug.md +36 -0
  12. data/TODO.remaining/7-headed-ox-limitations.md +95 -0
  13. data/TODO.remaining/8-xpath-predicate-gaps.md +68 -0
  14. data/TODO.remaining/9-cleanup-hygiene.md +42 -0
  15. data/TODO.remaining/README.md +54 -0
  16. data/benchmarks/generate_report.rb +1 -1
  17. data/docs/_pages/configuration.adoc +22 -19
  18. data/docs/_tutorials/namespace-handling.adoc +5 -5
  19. data/lib/moxml/adapter/base.rb +22 -3
  20. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -1
  21. data/lib/moxml/adapter/customized_libxml/entity_reference.rb +23 -0
  22. data/lib/moxml/adapter/customized_libxml.rb +18 -0
  23. data/lib/moxml/adapter/customized_oga.rb +10 -0
  24. data/lib/moxml/adapter/customized_ox/entity_reference.rb +25 -0
  25. data/lib/moxml/adapter/customized_ox.rb +12 -0
  26. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +19 -0
  27. data/lib/moxml/adapter/customized_rexml/formatter.rb +44 -20
  28. data/lib/moxml/adapter/customized_rexml.rb +11 -0
  29. data/lib/moxml/adapter/headed_ox.rb +37 -14
  30. data/lib/moxml/adapter/libxml.rb +233 -119
  31. data/lib/moxml/adapter/nokogiri.rb +22 -11
  32. data/lib/moxml/adapter/oga.rb +64 -25
  33. data/lib/moxml/adapter/ox.rb +198 -42
  34. data/lib/moxml/adapter/rexml.rb +64 -13
  35. data/lib/moxml/attribute.rb +3 -0
  36. data/lib/moxml/builder.rb +78 -24
  37. data/lib/moxml/config.rb +24 -7
  38. data/lib/moxml/declaration.rb +4 -2
  39. data/lib/moxml/document.rb +8 -1
  40. data/lib/moxml/document_builder.rb +44 -37
  41. data/lib/moxml/element.rb +18 -5
  42. data/lib/moxml/entity_registry.rb +51 -1
  43. data/lib/moxml/native_attachment.rb +65 -0
  44. data/lib/moxml/node.rb +39 -50
  45. data/lib/moxml/node_set.rb +43 -15
  46. data/lib/moxml/version.rb +1 -1
  47. data/lib/moxml/xml_utils.rb +1 -1
  48. data/lib/moxml/xpath/compiler.rb +4 -1
  49. data/lib/moxml.rb +1 -0
  50. data/scripts/format_xml.rb +16 -0
  51. data/scripts/pretty_format_xml.rb +14 -0
  52. data/spec/consistency/round_trip_spec.rb +3 -30
  53. data/spec/integration/all_adapters_spec.rb +1 -0
  54. data/spec/integration/headed_ox_integration_spec.rb +0 -2
  55. data/spec/integration/shared_examples/edge_cases.rb +7 -4
  56. data/spec/integration/shared_examples/integration_workflows.rb +3 -3
  57. data/spec/integration/shared_examples/node_wrappers/cdata_behavior.rb +1 -1
  58. data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +224 -0
  59. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +1 -1
  60. data/spec/moxml/adapter/headed_ox_spec.rb +8 -8
  61. data/spec/moxml/adapter/oga_spec.rb +46 -0
  62. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +1 -12
  63. data/spec/moxml/allocation_benchmark_spec.rb +96 -0
  64. data/spec/moxml/allocation_guard_spec.rb +282 -0
  65. data/spec/moxml/builder_spec.rb +256 -0
  66. data/spec/moxml/config_spec.rb +11 -11
  67. data/spec/moxml/doctype_spec.rb +41 -0
  68. data/spec/moxml/lazy_parse_spec.rb +115 -0
  69. data/spec/moxml/namespace_uri_validation_spec.rb +11 -3
  70. data/spec/moxml/node_cache_spec.rb +110 -0
  71. data/spec/moxml/node_set_cache_spec.rb +90 -0
  72. data/spec/moxml/xml_utils_spec.rb +32 -0
  73. data/spec/moxml/xpath/axes_spec.rb +1 -1
  74. data/spec/moxml/xpath/compiler_spec.rb +2 -2
  75. data/spec/moxml/xpath/functions/position_functions_spec.rb +5 -5
  76. data/spec/moxml/xpath/functions/special_functions_spec.rb +1 -1
  77. data/spec/performance/memory_usage_spec.rb +0 -4
  78. data/spec/support/allocation_helper.rb +165 -0
  79. data/spec/support/w3c_namespace_helpers.rb +2 -1
  80. 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&amp;b</p>")
134
+ output = doc.to_xml
135
+ expect(output).to include("&amp;")
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/HEADED_OX_LIMITATIONS.md"
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(Moxml::NodeSet)
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
- expect(result.first.xpath("title").first.text).to eq("Book 2")
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
- expect(result.first.xpath("title").first.text).to eq("Book 2")
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(Moxml::Element)
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
- expect(result.first.xpath("title").first.text).to eq("Book 3")
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
- <<~XML
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