moxml 0.5.148 → 0.5.149
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/nightly-fuzz.yml +80 -0
- data/docs/_pages/divergences.adoc +58 -0
- data/docs/_references/processing-model.adoc +30 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/leptris.rb +110 -19
- data/lib/moxml/adapter/libxml.rb +145 -23
- data/lib/moxml/adapter/oga.rb +33 -3
- data/lib/moxml/adapter/ox.rb +26 -2
- data/lib/moxml/adapter/rexml.rb +65 -15
- data/lib/moxml/context.rb +5 -1
- data/lib/moxml/document.rb +6 -6
- data/lib/moxml/node.rb +22 -6
- data/lib/moxml/node_set.rb +11 -0
- 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/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 +1 -0
- data/lib/moxml.rb +1 -0
- data/spec/consistency/adapter_parity_spec.rb +2 -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 +108 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +11 -0
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/attribute_resolver_spec.rb +2 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +2 -0
- data/spec/moxml/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/entity_spec.rb +2 -0
- 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/sax_spec.rb +2 -0
- 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/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +212 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +16 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Processing model §Names and namespaces (normative): pinned here.
|
|
4
|
+
# Processing model §XPath evaluation (normative): pinned here.
|
|
5
|
+
|
|
3
6
|
require "spec_helper"
|
|
4
7
|
|
|
5
8
|
# Cross-adapter XPath idioms that nokogiri consumers rely on:
|
|
@@ -107,6 +110,14 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
107
110
|
expect(doc.at_xpath("/*")&.name).to eq("r")
|
|
108
111
|
expect(doc.at_xpath("/r")&.name).to eq("r")
|
|
109
112
|
end
|
|
113
|
+
|
|
114
|
+
it "answers scalar XPath functions unwrapped and uniform" do
|
|
115
|
+
doc = ctx.parse(plain_xml)
|
|
116
|
+
expect(doc.xpath("count(//a)")).to eq(1.0)
|
|
117
|
+
expect(doc.xpath("string(//a/@id)")).to eq("x")
|
|
118
|
+
expect(doc.xpath("boolean(//b)")).to be(true)
|
|
119
|
+
expect(doc.at_xpath("count(//a)")).to eq(1.0)
|
|
120
|
+
end
|
|
110
121
|
end
|
|
111
122
|
end
|
|
112
123
|
end
|
|
@@ -5,10 +5,13 @@ require "spec_helper"
|
|
|
5
5
|
# moxml#375 — element hydration through the binding's
|
|
6
6
|
# attributes_snapshot face (libleptris 1.9.353, engine
|
|
7
7
|
# leptris#1689): one crossing for every attribute instead of one
|
|
8
|
-
# per attribute. The
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# per attribute. The facade listing face is bare names (uniform with
|
|
9
|
+
# every adapter, and with Attribute#name), document order, fresh
|
|
10
|
+
# values.
|
|
11
11
|
RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
12
|
+
def bare(pairs)
|
|
13
|
+
pairs.map { |(name, value)| [name.split(":", 2).last, value] }
|
|
14
|
+
end
|
|
12
15
|
let(:ctx) { Moxml.new(:leptris) }
|
|
13
16
|
|
|
14
17
|
context "when the snapshot face is armed (>= 1.9.353.0)" do
|
|
@@ -23,11 +26,11 @@ RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
|
23
26
|
'<root a="1" xml:lang="en" xmlns:ns="urn:u" ns:p="v"/>',
|
|
24
27
|
)
|
|
25
28
|
binding_element = Moxml::Adapter::Leptris.to_binding(doc.root)
|
|
26
|
-
expected = binding_element.attribute_pairs
|
|
29
|
+
expected = bare(binding_element.attribute_pairs)
|
|
27
30
|
|
|
28
31
|
expect(doc.root.attribute_pairs).to eq(expected)
|
|
29
32
|
expect(doc.root.attribute_pairs).to eq(
|
|
30
|
-
[["a", "1"], ["
|
|
33
|
+
[["a", "1"], ["lang", "en"], ["p", "v"]],
|
|
31
34
|
)
|
|
32
35
|
end
|
|
33
36
|
|
|
@@ -36,7 +39,7 @@ RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
|
36
39
|
'<root z="9" xmlns:m="urn:m" m:x="8" k="7"/>',
|
|
37
40
|
)
|
|
38
41
|
expect(doc.root.attribute_pairs.map(&:first)).to eq(
|
|
39
|
-
%w[z
|
|
42
|
+
%w[z x k],
|
|
40
43
|
)
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -71,7 +74,7 @@ RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
|
71
74
|
)
|
|
72
75
|
binding_element = Moxml::Adapter::Leptris.to_binding(doc.root)
|
|
73
76
|
expect(doc.root.attribute_pairs).to eq(
|
|
74
|
-
binding_element.attribute_pairs,
|
|
77
|
+
bare(binding_element.attribute_pairs),
|
|
75
78
|
)
|
|
76
79
|
end
|
|
77
80
|
end
|
|
@@ -94,7 +94,7 @@ RSpec.describe Moxml::Adapter::Ox do
|
|
|
94
94
|
|
|
95
95
|
it "fails loudly on ox_mode: :hash (the adapter walks a tree, not a Hash)" do
|
|
96
96
|
expect { described_class.parse("<r><t> x </t></r>", ox_mode: :hash) }
|
|
97
|
-
.to raise_error(
|
|
97
|
+
.to raise_error(Moxml::ParseError, /must be a String or Ox::Node/)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
it "surfaces an invalid ox_skip as Moxml::ParseError" do
|
data/spec/moxml/doctype_spec.rb
CHANGED
|
@@ -84,6 +84,13 @@ RSpec.describe Moxml::Doctype do
|
|
|
84
84
|
expect(doctype.external_id).to be_nil
|
|
85
85
|
expect(doctype.system_id).to be_nil
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
it "preserves a DOCTYPE internal subset on serialization" do
|
|
89
|
+
xml = '<!DOCTYPE r [<!ENTITY x "v">]><r>&x;</r>'
|
|
90
|
+
parsed = ctx.parse(xml)
|
|
91
|
+
|
|
92
|
+
expect(parsed.to_xml(indent: 0)).to include('<!ENTITY x "v">')
|
|
93
|
+
end
|
|
87
94
|
end
|
|
88
95
|
end
|
|
89
96
|
end
|
data/spec/moxml/entity_spec.rb
CHANGED
|
@@ -21,6 +21,15 @@ RSpec.describe Moxml::ProcessingInstruction do
|
|
|
21
21
|
expect(pi.content).to include("type=")
|
|
22
22
|
expect(pi.content).to include("href=")
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
it "answers the empty string for a PI with no data on every adapter" do
|
|
26
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
|
|
27
|
+
parsed = Moxml.new(name).parse("<r><?pi?></r>")
|
|
28
|
+
pi = parsed.children.first.children
|
|
29
|
+
.find { |n| n.is_a?(described_class) }
|
|
30
|
+
expect(pi.content).to eq(""), "empty PI content on #{name}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
describe "creation" do
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# The engine-free lane (TODO.sota/03): the reference is pure Ruby and
|
|
6
|
+
# answers the model where no native engine exists. This spec proves it
|
|
7
|
+
# transpiles and runs under Opal.
|
|
8
|
+
RSpec.describe "Moxml::Reference under Opal", if: RUBY_ENGINE == "opal" do
|
|
9
|
+
it "parses, answers faces, and writes without a native engine" do
|
|
10
|
+
document = Moxml::Reference.parse(
|
|
11
|
+
'<r a="1"><b>t&u</b><![CDATA[c]]><?pi d?></r>',
|
|
12
|
+
)
|
|
13
|
+
expect(Moxml::Reference.element_names(document)).to eq(%w[r b])
|
|
14
|
+
expect(Moxml::Reference.child_element_names(document)).to eq([["b"], []])
|
|
15
|
+
element = document.root.children.find(&:element?)
|
|
16
|
+
expect(element.children.map(&:kind)).to eq([:text])
|
|
17
|
+
expect(element.children.first.data).to eq("t&u")
|
|
18
|
+
expect(document.root.text).to eq("t&uc")
|
|
19
|
+
expect(Moxml::Reference.processing_instructions(document))
|
|
20
|
+
.to eq([["pi", "d"]])
|
|
21
|
+
expect(Moxml::Reference.to_xml(document))
|
|
22
|
+
.to include("<r a=\"1\">", "<b>t&u</b>", "<![CDATA[c]]>")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "raises a named ParseError for malformed input" do
|
|
26
|
+
expect { Moxml::Reference.parse("<r><c></r>") }
|
|
27
|
+
.to raise_error(Moxml::ParseError)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/input_taxonomy"
|
|
5
|
+
require "support/xml_generator"
|
|
6
|
+
|
|
7
|
+
# The executable reference (TODO.sota/03): a pure-Ruby reader,
|
|
8
|
+
# writer, and normalizer that answer the processing model with no
|
|
9
|
+
# engine involved. The differential suite tests adapters against it;
|
|
10
|
+
# these specs pin the reference itself.
|
|
11
|
+
RSpec.describe Moxml::Reference do
|
|
12
|
+
describe "parsing" do
|
|
13
|
+
it "reads the object model, declaration, and document parts" do
|
|
14
|
+
doc = described_class.parse(
|
|
15
|
+
'<?xml version="1.0" encoding="UTF-8"?><!--lead--><r a="1"><b>t</b></r>',
|
|
16
|
+
)
|
|
17
|
+
expect(doc.declaration[:version]).to eq("1.0")
|
|
18
|
+
expect(doc.declaration[:encoding]).to eq("UTF-8")
|
|
19
|
+
expect(described_class.element_names(doc)).to eq(%w[r b])
|
|
20
|
+
expect(doc.root.attribute_pairs).to eq([["a", "1"]])
|
|
21
|
+
expect(described_class.comments(doc)).to eq(["lead"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "answers bare local names for prefixed sources" do
|
|
25
|
+
doc = described_class.parse('<r xmlns:p="urn:a"><p:c p:x="1"/></r>')
|
|
26
|
+
expect(described_class.element_names(doc)).to eq(%w[r c])
|
|
27
|
+
expect(described_class.child_element_names(doc)).to eq([["c"], []])
|
|
28
|
+
expect(doc.root.children.first.attribute_pairs).to eq([["x", "1"]])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "expands built-in and numeric references, never comments or PI data" do
|
|
32
|
+
doc = described_class.parse(
|
|
33
|
+
"<r>&AB<![CDATA[<raw>]]><!-- c --><?pi d?></r>",
|
|
34
|
+
)
|
|
35
|
+
expect(doc.root.text).to eq("&AB<raw>")
|
|
36
|
+
expect(described_class.processing_instructions(doc)).to eq([["pi", "d"]])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "preserves DTD-declared general entities lexically (uniform lane)" do
|
|
40
|
+
doc = described_class.parse(
|
|
41
|
+
'<!DOCTYPE r [<!ENTITY x "expanded">]><r>&x;</r>',
|
|
42
|
+
)
|
|
43
|
+
expect(doc.root.text).to eq("&x;")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "normalizes literal line endings but not character references" do
|
|
47
|
+
doc = described_class.parse("<r>a\r\nb\rc
d</r>")
|
|
48
|
+
expect(doc.root.text).to eq("a\nb\nc\rd")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "normalizes literal attribute whitespace" do
|
|
52
|
+
doc = described_class.parse(%(<r a="x\ty\nz"/>))
|
|
53
|
+
expect(doc.root.attribute_pairs).to eq([["a", "x y z"]])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "drops the PI separator from PI data" do
|
|
57
|
+
doc = described_class.parse("<r><?pi data ?></r>")
|
|
58
|
+
expect(described_class.processing_instructions(doc))
|
|
59
|
+
.to eq([["pi", "data "]])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "answers the empty string for PI with no data" do
|
|
63
|
+
doc = described_class.parse("<r><?pi?></r>")
|
|
64
|
+
expect(described_class.processing_instructions(doc)).to eq([["pi", ""]])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "totality over the input taxonomy" do
|
|
69
|
+
it "parses every well-formed taxonomy fixture" do
|
|
70
|
+
InputTaxonomy.well_formed.each do |input|
|
|
71
|
+
expect { described_class.parse(input.xml) }
|
|
72
|
+
.not_to raise_error, "#{input.id} must parse in the reference"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "raises a named ParseError for every malformed taxonomy fixture" do
|
|
77
|
+
InputTaxonomy.malformed.each do |input|
|
|
78
|
+
expect { described_class.parse(input.xml) }
|
|
79
|
+
.to raise_error(Moxml::ParseError),
|
|
80
|
+
"#{input.id} must raise in the reference"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "parses every generated document" do
|
|
85
|
+
(1..10).each do |seed|
|
|
86
|
+
expect { described_class.parse(XmlGenerator.generate(seed)) }
|
|
87
|
+
.not_to raise_error
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe "writers" do
|
|
93
|
+
let(:doc) do
|
|
94
|
+
described_class.parse(
|
|
95
|
+
'<r a="<&""><b/>t<!--c--><![CDATA[x<y]]></r>',
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "emits XML that re-parses to the same information set" do
|
|
100
|
+
xml = described_class.to_xml(doc)
|
|
101
|
+
again = described_class.parse(xml)
|
|
102
|
+
expect(described_class.element_names(again))
|
|
103
|
+
.to eq(described_class.element_names(doc))
|
|
104
|
+
expect(again.root.attribute_pairs).to eq(doc.root.attribute_pairs)
|
|
105
|
+
expect(again.root.text).to eq(doc.root.text)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "emits the canonical facade escaping" do
|
|
109
|
+
expect(described_class.to_xml(doc))
|
|
110
|
+
.to include('<r a="<&"">')
|
|
111
|
+
expect(described_class.to_xml(doc)).to include("<![CDATA[x<y]]>")
|
|
112
|
+
expect(described_class.to_xml(doc)).to include("<b/>")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "splits a CDATA end sequence" do
|
|
116
|
+
document = Moxml::Reference::Document.new
|
|
117
|
+
root = Moxml::Reference::Node.new(kind: :element, name: "r")
|
|
118
|
+
root.children << Moxml::Reference::Node.new(kind: :cdata, data: "a]]>b")
|
|
119
|
+
document.children << root
|
|
120
|
+
expect(described_class.to_xml(document)).to include("]]]]><![CDATA[>")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "leaves void elements unclosed in HTML and self-closed in XHTML" do
|
|
124
|
+
html_doc = described_class.parse("<html><body>a<br/>b</body></html>")
|
|
125
|
+
expect(described_class.to_html(html_doc)).to include("<br>")
|
|
126
|
+
expect(described_class.to_xhtml(html_doc)).to include("<br/>")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "emits raw text inside HTML script and style, escaped in XHTML" do
|
|
130
|
+
html_doc = described_class.parse("<html><script>a < b</script></html>")
|
|
131
|
+
expect(described_class.to_html(html_doc))
|
|
132
|
+
.to include("<script>a < b</script>")
|
|
133
|
+
expect(described_class.to_xhtml(html_doc))
|
|
134
|
+
.to include("<script>a < b</script>")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "emits no XML declaration unless the source had one" do
|
|
138
|
+
expect(described_class.to_html(described_class.parse("<r/>")))
|
|
139
|
+
.to eq("<r></r>")
|
|
140
|
+
with_declaration = described_class.parse('<?xml version="1.0"?><r/>')
|
|
141
|
+
expect(described_class.to_xml(with_declaration))
|
|
142
|
+
.to start_with('<?xml version="1.0"?>')
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe "normalizer" do
|
|
147
|
+
it "normalizes CRLF and bare CR to LF" do
|
|
148
|
+
expect(Moxml::Reference::Normalizer.normalize_eol("a\r\nb\rc"))
|
|
149
|
+
.to eq("a\nb\nc")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "expands built-ins and numeric references" do
|
|
153
|
+
expect(Moxml::Reference::Normalizer.expand_entities("<A"))
|
|
154
|
+
.to eq("<A")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "raises for an undefined reference" do
|
|
158
|
+
expect { Moxml::Reference::Normalizer.expand_entities("&nope;") }
|
|
159
|
+
.to raise_error(Moxml::ParseError)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/spec/moxml/sax_spec.rb
CHANGED
data/spec/moxml/text_spec.rb
CHANGED
data/spec/moxml/to_html_spec.rb
CHANGED
|
@@ -217,11 +217,11 @@ RSpec.describe "XPath Axes" do
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
it "selects ancestors with ancestor::" do
|
|
220
|
-
expect(select("//c/ancestor::*")).to eq(%w[parent
|
|
220
|
+
expect(select("//c/ancestor::*")).to eq(%w[root parent])
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
it "includes the context node with ancestor-or-self::" do
|
|
224
|
-
expect(select("//c/ancestor-or-self::*")).to eq(%w[
|
|
224
|
+
expect(select("//c/ancestor-or-self::*")).to eq(%w[root parent c])
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
it "selects following siblings" do
|
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
|