moxml 0.5.148 → 0.5.150
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/.github/workflows/dependent-repos.json +7 -2
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.github/workflows/nightly-fuzz.yml +80 -0
- data/.github/workflows/performance-release.yml +79 -0
- data/.github/workflows/performance.yml +59 -0
- data/Rakefile +23 -0
- data/benchmark/README.md +106 -0
- data/benchmark/scoreboard.json +115 -0
- data/benchmark/scoreboard.rb +556 -0
- data/bin/generate-model-coverage +21 -0
- data/docs/_pages/adapter-compatibility.adoc +3 -3
- data/docs/_pages/divergences.adoc +88 -39
- data/docs/_pages/ecosystem.adoc +62 -0
- data/docs/_pages/headed-ox-limitations.adoc +1 -1
- data/docs/_pages/headed-ox.adoc +2 -2
- data/docs/_pages/index.adoc +4 -0
- data/docs/_pages/model-coverage.adoc +57 -0
- data/docs/_pages/performance.adoc +29 -0
- data/docs/_pages/why-moxml.adoc +87 -0
- data/docs/_references/processing-model.adoc +69 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +293 -23
- data/lib/moxml/adapter/libxml.rb +179 -25
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +71 -9
- data/lib/moxml/adapter/ox.rb +34 -2
- data/lib/moxml/adapter/rexml.rb +82 -17
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +33 -1
- data/lib/moxml/document.rb +18 -4
- data/lib/moxml/node.rb +75 -8
- data/lib/moxml/node_set.rb +42 -1
- data/lib/moxml/reference/normalizer.rb +63 -0
- data/lib/moxml/reference/parser.rb +417 -0
- data/lib/moxml/reference/writer.rb +121 -0
- data/lib/moxml/reference.rb +205 -0
- data/lib/moxml/relaxng.rb +62 -0
- data/lib/moxml/sax/block_handler.rb +7 -0
- data/lib/moxml/sax/handler.rb +20 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +17 -2
- data/lib/moxml/xpath/ordering.rb +97 -0
- data/lib/moxml/xpath.rb +29 -0
- data/lib/moxml.rb +2 -0
- data/spec/consistency/adapter_parity_spec.rb +24 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/differential_taxonomy_spec.rb +117 -0
- data/spec/consistency/divergence_register_spec.rb +9 -1
- data/spec/consistency/eol_normalization_spec.rb +2 -0
- data/spec/consistency/loud_degradation_spec.rb +2 -0
- data/spec/consistency/metamorphic_properties_spec.rb +195 -0
- data/spec/consistency/metamorphic_spec.rb +138 -0
- data/spec/consistency/model_coverage_audit_spec.rb +144 -61
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +34 -5
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +35 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -6
- data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
- data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
- data/spec/moxml/c14n/subset_spec.rb +2 -12
- data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
- data/spec/moxml/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/entity_spec.rb +2 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/plan_conformance_spec.rb +2 -0
- data/spec/moxml/processing_instruction_spec.rb +9 -0
- data/spec/moxml/reference_opal_spec.rb +29 -0
- data/spec/moxml/reference_spec.rb +162 -0
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +47 -74
- data/spec/moxml/text_spec.rb +2 -0
- data/spec/moxml/to_html_spec.rb +2 -0
- data/spec/moxml/xhtml_output_spec.rb +2 -0
- data/spec/moxml/xpath/axes_spec.rb +2 -2
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/moxml/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +219 -0
- data/spec/support/model_coverage.rb +159 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +32 -2
|
@@ -81,9 +81,7 @@ RSpec.describe "XPath Capabilities" do
|
|
|
81
81
|
it "supports attribute value matching ([@attr='value'])" do
|
|
82
82
|
doc = Moxml.new.parse(simple_xml)
|
|
83
83
|
programming = doc.xpath("//book[@category='programming']")
|
|
84
|
-
expect(programming.length).to
|
|
85
|
-
rescue Moxml::XPathError
|
|
86
|
-
skip "D-12: Attribute value predicates not supported on #{adapter_name}"
|
|
84
|
+
expect(programming.length).to eq(1)
|
|
87
85
|
end
|
|
88
86
|
end
|
|
89
87
|
|
|
@@ -96,9 +94,7 @@ RSpec.describe "XPath Capabilities" do
|
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
items = doc.xpath("//book:item", namespaces)
|
|
99
|
-
expect(items.length).to
|
|
100
|
-
rescue Moxml::XPathError, NoMethodError
|
|
101
|
-
skip "D-12: Namespace XPath not supported on #{adapter_name}"
|
|
97
|
+
expect(items.length).to eq(2)
|
|
102
98
|
end
|
|
103
99
|
|
|
104
100
|
it "handles default namespaces" do
|
|
@@ -106,9 +102,7 @@ RSpec.describe "XPath Capabilities" do
|
|
|
106
102
|
namespaces = { "lib" => "http://library.org" }
|
|
107
103
|
|
|
108
104
|
library = doc.xpath("//lib:library", namespaces)
|
|
109
|
-
expect(library.length).to
|
|
110
|
-
rescue Moxml::XPathError, NoMethodError
|
|
111
|
-
skip "D-12: Default namespace XPath not supported on #{adapter_name}"
|
|
105
|
+
expect(library.length).to eq(1)
|
|
112
106
|
end
|
|
113
107
|
end
|
|
114
108
|
|
|
@@ -116,17 +110,13 @@ RSpec.describe "XPath Capabilities" do
|
|
|
116
110
|
it "supports numeric positions ([1])" do
|
|
117
111
|
doc = Moxml.new.parse(simple_xml)
|
|
118
112
|
first_book = doc.xpath("//book[1]")
|
|
119
|
-
expect(first_book.length).to
|
|
120
|
-
rescue Moxml::XPathError
|
|
121
|
-
skip "D-12: Position predicates not supported on #{adapter_name}"
|
|
113
|
+
expect(first_book.length).to eq(1)
|
|
122
114
|
end
|
|
123
115
|
|
|
124
116
|
it "supports position() function" do
|
|
125
117
|
doc = Moxml.new.parse(simple_xml)
|
|
126
118
|
first_books = doc.xpath("//book[position()=1]")
|
|
127
|
-
expect(first_books.length).to
|
|
128
|
-
rescue Moxml::XPathError
|
|
129
|
-
skip "D-12: position() function not supported on #{adapter_name}"
|
|
119
|
+
expect(first_books.length).to eq(1)
|
|
130
120
|
end
|
|
131
121
|
end
|
|
132
122
|
|
|
@@ -134,20 +124,13 @@ RSpec.describe "XPath Capabilities" do
|
|
|
134
124
|
it "supports count() function, returning the scalar unwrapped" do
|
|
135
125
|
doc = Moxml.new.parse(simple_xml)
|
|
136
126
|
result = doc.xpath("count(//book)")
|
|
137
|
-
# REXML's XPath has no function support and yields an empty set
|
|
138
|
-
skip "D-12: count() function not supported on #{adapter_name}" if result.is_a?(Moxml::NodeSet)
|
|
139
|
-
|
|
140
127
|
expect(result).to eq(2)
|
|
141
|
-
rescue Moxml::XPathError, NoMethodError
|
|
142
|
-
skip "D-12: count() function not supported on #{adapter_name}"
|
|
143
128
|
end
|
|
144
129
|
|
|
145
130
|
it "supports text() node selection" do
|
|
146
131
|
doc = Moxml.new.parse(simple_xml)
|
|
147
132
|
texts = doc.xpath("//title/text()")
|
|
148
|
-
expect(texts.length).to
|
|
149
|
-
rescue Moxml::XPathError
|
|
150
|
-
skip "D-12: text() not supported on #{adapter_name}"
|
|
133
|
+
expect(texts.length).to eq(3)
|
|
151
134
|
end
|
|
152
135
|
end
|
|
153
136
|
|
|
@@ -155,17 +138,13 @@ RSpec.describe "XPath Capabilities" do
|
|
|
155
138
|
it "supports numeric comparisons ([@attr < value])" do
|
|
156
139
|
doc = Moxml.new.parse(simple_xml)
|
|
157
140
|
cheap_books = doc.xpath("//book[@id < 2]")
|
|
158
|
-
expect(cheap_books.length).to
|
|
159
|
-
rescue Moxml::XPathError
|
|
160
|
-
skip "D-12: Numeric predicates not supported on #{adapter_name}"
|
|
141
|
+
expect(cheap_books.length).to eq(1)
|
|
161
142
|
end
|
|
162
143
|
|
|
163
144
|
it "supports boolean expressions ([cond1 and cond2])" do
|
|
164
145
|
doc = Moxml.new.parse(simple_xml)
|
|
165
146
|
result = doc.xpath("//book[@id and @category]")
|
|
166
|
-
expect(result.length).to
|
|
167
|
-
rescue Moxml::XPathError
|
|
168
|
-
skip "D-12: Boolean expressions not supported on #{adapter_name}"
|
|
147
|
+
expect(result.length).to eq(2)
|
|
169
148
|
end
|
|
170
149
|
end
|
|
171
150
|
|
|
@@ -173,17 +152,13 @@ RSpec.describe "XPath Capabilities" do
|
|
|
173
152
|
it "supports child axis" do
|
|
174
153
|
doc = Moxml.new.parse(simple_xml)
|
|
175
154
|
children = doc.xpath("/library/child::book")
|
|
176
|
-
expect(children.length).to
|
|
177
|
-
rescue Moxml::XPathError
|
|
178
|
-
skip "D-12: child:: axis not supported on #{adapter_name}"
|
|
155
|
+
expect(children.length).to eq(2)
|
|
179
156
|
end
|
|
180
157
|
|
|
181
158
|
it "supports descendant axis" do
|
|
182
159
|
doc = Moxml.new.parse(simple_xml)
|
|
183
160
|
descendants = doc.xpath("//library/descendant::title")
|
|
184
|
-
expect(descendants.length).to
|
|
185
|
-
rescue Moxml::XPathError
|
|
186
|
-
skip "D-12: descendant:: axis not supported on #{adapter_name}"
|
|
161
|
+
expect(descendants.length).to eq(3)
|
|
187
162
|
end
|
|
188
163
|
end
|
|
189
164
|
|
|
@@ -191,9 +166,7 @@ RSpec.describe "XPath Capabilities" do
|
|
|
191
166
|
it "supports union (|)" do
|
|
192
167
|
doc = Moxml.new.parse(simple_xml)
|
|
193
168
|
items = doc.xpath("//book | //magazine")
|
|
194
|
-
expect(items.length).to
|
|
195
|
-
rescue Moxml::XPathError
|
|
196
|
-
skip "D-12: Union operator not supported on #{adapter_name}"
|
|
169
|
+
expect(items.length).to eq(3)
|
|
197
170
|
end
|
|
198
171
|
end
|
|
199
172
|
end
|
data/spec/moxml/xsd_spec.rb
CHANGED
|
@@ -141,4 +141,42 @@ RSpec.describe Moxml::XSD do
|
|
|
141
141
|
.to eq(Moxml::Adapter::Leptris::NATIVE_XSD)
|
|
142
142
|
end
|
|
143
143
|
end
|
|
144
|
+
|
|
145
|
+
describe "content_valid? with C-minted identity children (moxml#410)",
|
|
146
|
+
if: Moxml::Adapter::Leptris::XSD_SUPPORTED do
|
|
147
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
148
|
+
let(:schema_text) do
|
|
149
|
+
<<~XML
|
|
150
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
151
|
+
<xs:element name="person">
|
|
152
|
+
<xs:complexType>
|
|
153
|
+
<xs:sequence>
|
|
154
|
+
<xs:element name="name" type="xs:string"/>
|
|
155
|
+
</xs:sequence>
|
|
156
|
+
</xs:complexType>
|
|
157
|
+
</xs:element>
|
|
158
|
+
</xs:schema>
|
|
159
|
+
XML
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "answers for identity wrappers minted at create (1.9.374+)" do
|
|
163
|
+
schema = ctx.xsd(schema_text)
|
|
164
|
+
doc = ctx.create_document
|
|
165
|
+
root = doc.create_element("person")
|
|
166
|
+
doc.root = root
|
|
167
|
+
name = doc.create_element("name")
|
|
168
|
+
name.text = "A"
|
|
169
|
+
root.add_child(name)
|
|
170
|
+
|
|
171
|
+
expect(root.native.class.name).to include("Identity")
|
|
172
|
+
expect(schema.content_valid?("person", root.children.to_a)).to be(true)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "answers for identity children from parse" do
|
|
176
|
+
schema = ctx.xsd(schema_text)
|
|
177
|
+
root = ctx.parse("<person><name>A</name></person>").root
|
|
178
|
+
expect(schema.content_valid?("person", root.children.to_a)).to be(true)
|
|
179
|
+
expect(schema.content_valid?("person", [])).to be(false)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
144
182
|
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Seeded XML input generator (TODO.sota/02): composes the 01
|
|
4
|
+
# taxonomy's vocabulary into fresh inputs, deterministically from a
|
|
5
|
+
# seed. CI legs run a fixed seed — identical cases every run; the
|
|
6
|
+
# nightly fuzz leg passes a random one (MOXML_FUZZ_SEED). Every case
|
|
7
|
+
# id carries its provenance ("gen <seed>/<i> <malform>"), so a
|
|
8
|
+
# failure reproduces at that seed and index.
|
|
9
|
+
module InputGenerator
|
|
10
|
+
# Generated inputs record the divergence-relevant features they use
|
|
11
|
+
# so the differential exempts exactly those cells, never silently —
|
|
12
|
+
# every value is a register ID (docs/_pages/divergences.adoc).
|
|
13
|
+
FEATURE_EXEMPT = {
|
|
14
|
+
inter_element_ws: {
|
|
15
|
+
nodes: { ox: "D-13", headed_ox: "D-13" },
|
|
16
|
+
text: { ox: "D-13", headed_ox: "D-13" },
|
|
17
|
+
},
|
|
18
|
+
pi_in_content: { text: { oga: "D-14" } },
|
|
19
|
+
pre_root_ws: { nodes: { rexml: "D-15" }, text: { rexml: "D-15" } },
|
|
20
|
+
adjacent_cdata: {
|
|
21
|
+
nodes: { oga: "D-19", rexml: "D-19", ox: "D-19", headed_ox: "D-19",
|
|
22
|
+
leptris: "D-19" },
|
|
23
|
+
},
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# malform is one of the three guaranteed-breaking structural
|
|
27
|
+
# mutations; oga's strict lane is lexer-strict (D-17) and is
|
|
28
|
+
# exempted for all of them in the strict-raise property.
|
|
29
|
+
Case = Struct.new(:id, :xml, :well_formed, :features, :voids, :malform)
|
|
30
|
+
|
|
31
|
+
ELEMENTS = %w[a b sec p].freeze
|
|
32
|
+
VOIDS = %w[br img hr].freeze
|
|
33
|
+
ATTR_VALUES = [
|
|
34
|
+
"1", "x y", "a & b", "<tag>", "AB", "taillé"
|
|
35
|
+
].freeze
|
|
36
|
+
TEXTS = [
|
|
37
|
+
"plain text", " spaced ", "a & b < c", "AB",
|
|
38
|
+
"café   NBSP", "word"
|
|
39
|
+
].freeze
|
|
40
|
+
NAMESPACES = [
|
|
41
|
+
'xmlns="urn:t"', 'xmlns:ns="urn:n"', 'xmlns="urn:r"'
|
|
42
|
+
].freeze
|
|
43
|
+
XML_ATTRS = ['xml:space="preserve"', 'xml:lang="en"'].freeze
|
|
44
|
+
MALFORMS = %i[unclosed stray attr_quote].freeze
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# mode: :fuzz mixes the three structural malformations into roughly
|
|
49
|
+
# a third of the cases (the strict-raise property consumes those);
|
|
50
|
+
# :well_formed answers pure compositions.
|
|
51
|
+
def cases(seed:, count:, mode: :fuzz)
|
|
52
|
+
rng = Random.new(seed)
|
|
53
|
+
Array.new(count) do |i|
|
|
54
|
+
body, features, voids = compose(rng)
|
|
55
|
+
id = "gen #{seed}/#{i}"
|
|
56
|
+
if mode == :fuzz && rng.rand(3).zero?
|
|
57
|
+
malform = MALFORMS[rng.rand(MALFORMS.length)]
|
|
58
|
+
Case.new("#{id} #{malform}", mutate(body, malform), false,
|
|
59
|
+
features, voids, malform)
|
|
60
|
+
else
|
|
61
|
+
Case.new(id, body, true, features, voids, nil)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def compose(rng)
|
|
67
|
+
features = []
|
|
68
|
+
voids = []
|
|
69
|
+
pre = +""
|
|
70
|
+
draw = rng.rand(6)
|
|
71
|
+
if draw.zero?
|
|
72
|
+
pre << "<!--pre-root-->"
|
|
73
|
+
elsif draw == 1
|
|
74
|
+
pre << "<?prep root?>"
|
|
75
|
+
elsif draw == 2
|
|
76
|
+
pre << " "
|
|
77
|
+
features << :pre_root_ws
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
attrs = +'<r id="root"'
|
|
81
|
+
attrs << " #{NAMESPACES[rng.rand(NAMESPACES.length)]}" if rng.rand(2).zero?
|
|
82
|
+
attrs << " #{XML_ATTRS[rng.rand(XML_ATTRS.length)]}" if rng.rand(3).zero?
|
|
83
|
+
attrs << ' ns:x="v"' if rng.rand(3).zero?
|
|
84
|
+
attrs << ">"
|
|
85
|
+
|
|
86
|
+
body = +""
|
|
87
|
+
depth = 1 + rng.rand(4)
|
|
88
|
+
(1 + rng.rand(4)).times { body << child(rng, 0, depth, features, voids) }
|
|
89
|
+
# a whitespace-only run between two constructs is a dropped-text
|
|
90
|
+
# divergence cell (D-13 on ox/headed_ox)
|
|
91
|
+
features << :inter_element_ws if body.match?(%r{>\s+<})
|
|
92
|
+
|
|
93
|
+
xml = "#{pre}#{attrs}#{body}</r>"
|
|
94
|
+
# a default-namespace declaration scopes XPath name tests away
|
|
95
|
+
# from generated a/b elements (XPath 1.0, processing model
|
|
96
|
+
# "Names and namespaces")
|
|
97
|
+
features << :default_ns if xml.include?('xmlns="')
|
|
98
|
+
# adjacent text-like leaves — CDATA next to CDATA/text — coalesce
|
|
99
|
+
# on the libxml2 lane (D-19)
|
|
100
|
+
if xml.match?(/]\]>\s*<!\[CDATA\[/) || xml.match?(/]\]>\s*[^<\s]/) ||
|
|
101
|
+
xml.match?(/[^<\s]\s*<!\[CDATA\[/)
|
|
102
|
+
features << :adjacent_cdata
|
|
103
|
+
end
|
|
104
|
+
[xml, features.uniq, voids.uniq]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def child(rng, level, max_depth, features, voids)
|
|
108
|
+
if level < max_depth && rng.rand(8) < 5
|
|
109
|
+
inner = +""
|
|
110
|
+
(1 + rng.rand(3)).times do
|
|
111
|
+
inner << child(rng, level + 1, max_depth, features, voids)
|
|
112
|
+
end
|
|
113
|
+
name = ELEMENTS[rng.rand(ELEMENTS.length)]
|
|
114
|
+
extra = +""
|
|
115
|
+
extra << " #{NAMESPACES[rng.rand(NAMESPACES.length)]}" if rng.rand(4).zero?
|
|
116
|
+
extra << " #{XML_ATTRS[rng.rand(XML_ATTRS.length)]}" if rng.rand(4).zero?
|
|
117
|
+
extra << " ns:x=\"#{ATTR_VALUES[rng.rand(ATTR_VALUES.length)]}\"" if rng.rand(3).zero?
|
|
118
|
+
"<#{name}#{extra}>#{inner}</#{name}>"
|
|
119
|
+
else
|
|
120
|
+
leaf(rng, features, voids)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def leaf(rng, features, voids)
|
|
125
|
+
case rng.rand(6)
|
|
126
|
+
when 0, 1
|
|
127
|
+
TEXTS[rng.rand(TEXTS.length)]
|
|
128
|
+
when 2
|
|
129
|
+
"<!--comment #{rng.rand(9)}-->"
|
|
130
|
+
when 3
|
|
131
|
+
features << :pi_in_content
|
|
132
|
+
"<?inst data-#{rng.rand(9)}?>"
|
|
133
|
+
when 4
|
|
134
|
+
"<![CDATA[x < #{rng.rand(9)} & more]]>"
|
|
135
|
+
else
|
|
136
|
+
v = VOIDS[rng.rand(VOIDS.length)]
|
|
137
|
+
voids << v
|
|
138
|
+
"<#{v}/>"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Each mutation is guaranteed to break well-formedness (the root
|
|
143
|
+
# close and the root id attribute always exist in composed bodies).
|
|
144
|
+
def mutate(xml, malform)
|
|
145
|
+
case malform
|
|
146
|
+
when :unclosed
|
|
147
|
+
xml.sub(%r{</[a-z]+>}, "")
|
|
148
|
+
when :stray
|
|
149
|
+
"#{xml}</r>"
|
|
150
|
+
when :attr_quote
|
|
151
|
+
xml.sub('id="root"', 'id="root')
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The canonical input-class taxonomy (TODO.sota/01-totality): every
|
|
4
|
+
# class of input the processing model touches, as shared fixture data
|
|
5
|
+
# the differential suites consume. Adding a class here automatically
|
|
6
|
+
# extends the whole-class differential; a class without a stable
|
|
7
|
+
# cross-adapter answer must cite a registered divergence (D-ID) or a
|
|
8
|
+
# named raise in the consuming suite.
|
|
9
|
+
module InputTaxonomy
|
|
10
|
+
Input = Struct.new(:id, :klass, :xml, :well_formed, :note) do
|
|
11
|
+
def well_formed? = well_formed
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
WELL_FORMED = "well-formed"
|
|
15
|
+
MALFORMED = "malformed"
|
|
16
|
+
|
|
17
|
+
def self.all
|
|
18
|
+
@all ||= inputs.freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.well_formed
|
|
22
|
+
all.select(&:well_formed?)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.malformed
|
|
26
|
+
all.reject(&:well_formed?)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.by_klass(klass)
|
|
30
|
+
all.select { |i| i.klass == klass }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
CLASSES = [
|
|
34
|
+
"empty/minimal",
|
|
35
|
+
"deep nesting",
|
|
36
|
+
"huge text/attributes",
|
|
37
|
+
"entities",
|
|
38
|
+
"namespaces",
|
|
39
|
+
"mixed content",
|
|
40
|
+
"pi/comment/cdata edges",
|
|
41
|
+
"malformed",
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
# Registered per-input exemptions for the cross-adapter
|
|
45
|
+
# differential: input id => { face => { adapter => D-NN } }. The
|
|
46
|
+
# values are register IDs (docs/_pages/divergences.adoc) enforced
|
|
47
|
+
# by the coverage audit.
|
|
48
|
+
EXEMPT = {
|
|
49
|
+
"mixed content: whitespace runs" => {
|
|
50
|
+
nodes: { ox: "D-13", headed_ox: "D-13" },
|
|
51
|
+
text: { ox: "D-13", headed_ox: "D-13" },
|
|
52
|
+
},
|
|
53
|
+
"mixed content: CRLF text normalizes to LF" => {
|
|
54
|
+
text: { oga: "D-02" },
|
|
55
|
+
},
|
|
56
|
+
"mixed content: bare CR text normalizes to LF" => {
|
|
57
|
+
text: { oga: "D-02" },
|
|
58
|
+
},
|
|
59
|
+
"mixed content: character-ref CR survives" => {
|
|
60
|
+
text: { oga: "D-02", ox: "D-03", headed_ox: "D-03" },
|
|
61
|
+
},
|
|
62
|
+
"pi/comment/cdata: pi inside content" => {
|
|
63
|
+
text: { oga: "D-14" },
|
|
64
|
+
pi_content: { ox: "D-05", headed_ox: "D-05" },
|
|
65
|
+
},
|
|
66
|
+
"pi/comment/cdata: pi multi-space separator" => {
|
|
67
|
+
text: { oga: "D-14" },
|
|
68
|
+
pi_content: { ox: "D-05", headed_ox: "D-05" },
|
|
69
|
+
},
|
|
70
|
+
"pi/comment/cdata: pi trailing whitespace" => {
|
|
71
|
+
text: { oga: "D-14" },
|
|
72
|
+
pi_content: { ox: "D-05", headed_ox: "D-05", rexml: "D-20" },
|
|
73
|
+
},
|
|
74
|
+
"huge text with entities" => {
|
|
75
|
+
text: { rexml: "D-16" },
|
|
76
|
+
serialize: { rexml: "D-16" },
|
|
77
|
+
},
|
|
78
|
+
"pi/comment/cdata: pi before root" => {
|
|
79
|
+
nodes: { rexml: "D-15" },
|
|
80
|
+
text: { rexml: "D-15" },
|
|
81
|
+
pi_content: { ox: "D-05", headed_ox: "D-05" },
|
|
82
|
+
},
|
|
83
|
+
"pi/comment/cdata: comment before and after root" => {
|
|
84
|
+
comment_content: { ox: "D-06", headed_ox: "D-06" },
|
|
85
|
+
},
|
|
86
|
+
"pi/comment/cdata: comment inside content" => {
|
|
87
|
+
comment_content: { ox: "D-06", headed_ox: "D-06" },
|
|
88
|
+
},
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
# Engines whose strict lane cannot see a class of malformedness are
|
|
92
|
+
# registered: oga's strict is lexer-strict, not well-formedness-
|
|
93
|
+
# strict (D-17). leptris raises through the facade guard (D-18,
|
|
94
|
+
# leptris-ruby#514) and is therefore verified, not exempt; entity
|
|
95
|
+
# and prefix classes are engine-variant by design (entity modes,
|
|
96
|
+
# namespace validation).
|
|
97
|
+
STRICT_EXEMPT = {
|
|
98
|
+
"malformed: bad nesting" => { oga: "D-17" },
|
|
99
|
+
"malformed: invalid utf-8 byte" => { oga: "D-18", ox: "D-18",
|
|
100
|
+
headed_ox: "D-18" },
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
def self.inputs
|
|
104
|
+
[
|
|
105
|
+
# -- empty/minimal --
|
|
106
|
+
Input.new("empty/minimal: empty string", "empty/minimal", "", false,
|
|
107
|
+
"no root: parse must fail loudly or per contract"),
|
|
108
|
+
Input.new("empty/minimal: whitespace only", "empty/minimal", " \n\t ", false,
|
|
109
|
+
"whitespace-only: no root"),
|
|
110
|
+
Input.new("empty/minimal: bare root", "empty/minimal", "<r/>", true, nil),
|
|
111
|
+
Input.new("empty/minimal: empty paired root", "empty/minimal", "<r></r>", true, nil),
|
|
112
|
+
Input.new("empty/minimal: declaration only", "empty/minimal",
|
|
113
|
+
'<?xml version="1.0"?>', false, "declaration without root"),
|
|
114
|
+
|
|
115
|
+
# -- deep nesting --
|
|
116
|
+
Input.new("deep nesting: 200 levels", "deep nesting",
|
|
117
|
+
"<r>#{'<d>' * 200}leaf#{'</d>' * 200}</r>", true, nil),
|
|
118
|
+
Input.new("deep nesting: siblings at depth", "deep nesting",
|
|
119
|
+
"<r>#{'<a><b>' * 50}#{'</b></a>' * 50}</r>", true, nil),
|
|
120
|
+
|
|
121
|
+
# -- huge text/attributes --
|
|
122
|
+
Input.new("huge text: 100k chars", "huge text/attributes",
|
|
123
|
+
"<r>#{'x' * 100_000}</r>", true, nil),
|
|
124
|
+
Input.new("huge attribute: 10k chars", "huge text/attributes",
|
|
125
|
+
%(<r a="#{'y' * 10_000}"/>), true, nil),
|
|
126
|
+
Input.new("huge text with entities", "huge text/attributes",
|
|
127
|
+
"<r>#{'&' * 20_000}</r>", true, nil),
|
|
128
|
+
|
|
129
|
+
# -- entities: built-in --
|
|
130
|
+
Input.new("entities: all five built-ins", "entities",
|
|
131
|
+
"<r>&<>"'</r>", true, nil),
|
|
132
|
+
# -- entities: numeric --
|
|
133
|
+
Input.new("entities: decimal numeric", "entities",
|
|
134
|
+
"<r>Aé</r>", true, nil),
|
|
135
|
+
Input.new("entities: hex numeric", "entities",
|
|
136
|
+
"<r>Aé</r>", true, nil),
|
|
137
|
+
# -- entities: custom (DTD-declared) --
|
|
138
|
+
Input.new("entities: custom DTD entity", "entities",
|
|
139
|
+
'<!DOCTYPE r [<!ENTITY x "expanded">]><r>&x;</r>', true, nil),
|
|
140
|
+
# -- entities: undefined (disabled context) --
|
|
141
|
+
Input.new("entities: undefined reference", "entities",
|
|
142
|
+
"<r>&undefinedEntity;</r>", false,
|
|
143
|
+
"undefined entity: malformed without a DTD"),
|
|
144
|
+
Input.new("entities: NBSP char ref", "entities",
|
|
145
|
+
"<r> </r>", true, "U+00A0 survives as content"),
|
|
146
|
+
|
|
147
|
+
# -- namespaces: default --
|
|
148
|
+
Input.new("namespaces: default", "namespaces",
|
|
149
|
+
'<r xmlns="urn:a"><c/></r>', true, nil),
|
|
150
|
+
# -- namespaces: prefixed --
|
|
151
|
+
Input.new("namespaces: prefixed", "namespaces",
|
|
152
|
+
'<r xmlns:p="urn:a"><p:c/></r>', true, nil),
|
|
153
|
+
# -- namespaces: undeclared prefix --
|
|
154
|
+
Input.new("namespaces: undeclared prefix", "namespaces",
|
|
155
|
+
"<p:c/>", false,
|
|
156
|
+
"undeclared prefix: malformed in strict parsing"),
|
|
157
|
+
# -- namespaces: redeclared --
|
|
158
|
+
Input.new("namespaces: redeclared", "namespaces",
|
|
159
|
+
'<r xmlns:p="urn:a"><s xmlns:p="urn:b"><p:c/></s></r>', true, nil),
|
|
160
|
+
# -- namespaces: reserved xml --
|
|
161
|
+
Input.new("namespaces: reserved xml prefix", "namespaces",
|
|
162
|
+
'<r xml:lang="en"/>', true, nil),
|
|
163
|
+
# -- namespaces: xmlns attribute --
|
|
164
|
+
Input.new("namespaces: xmlns declaration attribute", "namespaces",
|
|
165
|
+
'<r xmlns="urn:a" xmlns:p="urn:b"/>', true, nil),
|
|
166
|
+
|
|
167
|
+
# -- mixed content --
|
|
168
|
+
Input.new("mixed content: text and inline", "mixed content",
|
|
169
|
+
"<r>text <b>bold</b> tail</r>", true, nil),
|
|
170
|
+
Input.new("mixed content: whitespace runs", "mixed content",
|
|
171
|
+
"<r>\n <b>x</b>\n <b>y</b>\n</r>", true, nil),
|
|
172
|
+
Input.new("mixed content: attribute entities in text", "mixed content",
|
|
173
|
+
'<r a="<&"><tag></r>', true, nil),
|
|
174
|
+
Input.new("mixed content: CRLF text normalizes to LF", "mixed content",
|
|
175
|
+
"<r>a\r\nb</r>", true, "XML 1.0 §2.11"),
|
|
176
|
+
Input.new("mixed content: bare CR text normalizes to LF", "mixed content",
|
|
177
|
+
"<r>a\rb</r>", true, "XML 1.0 §2.11"),
|
|
178
|
+
Input.new("mixed content: character-ref CR survives", "mixed content",
|
|
179
|
+
"<r>a
b</r>", true, "
 is not normalized"),
|
|
180
|
+
|
|
181
|
+
# -- pi/comment/cdata edges --
|
|
182
|
+
Input.new("pi/comment/cdata: pi before root", "pi/comment/cdata edges",
|
|
183
|
+
'<?xml-stylesheet href="a"?> <r/>', true, nil),
|
|
184
|
+
Input.new("pi/comment/cdata: comment before and after root", "pi/comment/cdata edges",
|
|
185
|
+
"<!-- lead --><r/><!-- trail -->", true, nil),
|
|
186
|
+
Input.new("pi/comment/cdata: cdata with markup", "pi/comment/cdata edges",
|
|
187
|
+
"<r><![CDATA[<notmarkup> & raw]]></r>", true, nil),
|
|
188
|
+
Input.new("pi/comment/cdata: empty cdata", "pi/comment/cdata edges",
|
|
189
|
+
"<r><![CDATA[]]></r>", true, nil),
|
|
190
|
+
Input.new("pi/comment/cdata: comment inside content", "pi/comment/cdata edges",
|
|
191
|
+
"<r>a<!-- mid -->b</r>", true, nil),
|
|
192
|
+
Input.new("pi/comment/cdata: pi inside content", "pi/comment/cdata edges",
|
|
193
|
+
"<r>a<?pi data?>b</r>", true,
|
|
194
|
+
"PI content must not contribute to text (D-14 on oga)"),
|
|
195
|
+
Input.new("pi/comment/cdata: pi multi-space separator", "pi/comment/cdata edges",
|
|
196
|
+
"<r><?pi data?></r>", true,
|
|
197
|
+
"separator whitespace is not PI content (uniform)"),
|
|
198
|
+
Input.new("pi/comment/cdata: pi trailing whitespace", "pi/comment/cdata edges",
|
|
199
|
+
"<r><?pi data ?></r>", true,
|
|
200
|
+
"trailing whitespace is PI data (D-20 on rexml)"),
|
|
201
|
+
Input.new("pi/comment/cdata: empty pi", "pi/comment/cdata edges",
|
|
202
|
+
"<r><?pi?></r>", true, nil),
|
|
203
|
+
|
|
204
|
+
# -- malformed: unclosed --
|
|
205
|
+
Input.new("malformed: unclosed element", "malformed", "<r><c>", false, nil),
|
|
206
|
+
# -- malformed: bad nest --
|
|
207
|
+
Input.new("malformed: bad nesting", "malformed", "<r><a><b></a></b></r>", false, nil),
|
|
208
|
+
# -- malformed: bad attribute --
|
|
209
|
+
Input.new("malformed: unterminated attribute", "malformed",
|
|
210
|
+
'<r a="x>', false, nil),
|
|
211
|
+
# -- malformed: stray closing --
|
|
212
|
+
Input.new("malformed: stray closing tag", "malformed", "</r>", false, nil),
|
|
213
|
+
# -- malformed: invalid encoding bytes --
|
|
214
|
+
Input.new("malformed: invalid utf-8 byte", "malformed",
|
|
215
|
+
"<r>\xC3\x28</r>", false,
|
|
216
|
+
"0xC3 0x28 is not valid UTF-8"),
|
|
217
|
+
].freeze
|
|
218
|
+
end
|
|
219
|
+
end
|