moxml-testsuite 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.adoc +60 -0
- data/lib/moxml/testsuite/dataset.rb +43 -0
- data/lib/moxml/testsuite/families/c14n.rb +42 -0
- data/lib/moxml/testsuite/families/decisions.rb +120 -0
- data/lib/moxml/testsuite/families/entity_registry.rb +77 -0
- data/lib/moxml/testsuite/families/serialization.rb +40 -0
- data/lib/moxml/testsuite/families/xsd_structural.rb +45 -0
- data/lib/moxml/testsuite/reference.rb +129 -0
- data/lib/moxml/testsuite/report.rb +55 -0
- data/lib/moxml/testsuite/version.rb +7 -0
- data/lib/moxml/testsuite.rb +63 -0
- data/testdata/moxml-conformance.json +1250 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c83c47e9b732b9b23aa8923894275f0b5e9820c050b56363a1713a4305bd0d77
|
|
4
|
+
data.tar.gz: 56443d6aaae65d2934e47c96903551ca474270bf5682cd23214e52908c1e6350
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9b921670e6cd7d478782efd840b11167ff9c9130a39635313fea482383569606c3a108ebe7be19f1edcd69de9930e7e2ba61ecd29460af0fb3fbe30d646bf865
|
|
7
|
+
data.tar.gz: cf51812479aa486ae07c7ce65fa1407833ec1ac1c294c558a59997d72cc2b6f9cf5d89bf9fe2c36ab8f25c7a9588cac105af6bda9d8fb861c96994c131c34701
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ribose Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.adoc
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
= moxml-testsuite
|
|
2
|
+
|
|
3
|
+
image:https://img.shields.io/gem/v/moxml-testsuite.svg["Gem Version", link="https://rubygems.org/gems/moxml-testsuite"]
|
|
4
|
+
|
|
5
|
+
The moxml conformance test suite: certify any XML engine against the moxml model.
|
|
6
|
+
|
|
7
|
+
The moxml processing model (https://github.com/lutaml/moxml — `docs/_references/processing-model.adoc`) is a set of committed answers: one object model, one document order, one serialization shape, one canonicalization, one validation verdict — for every question an XML program can ask. The moxml conformance dataset (schema 5, 177 cases across seven families) IS those answers as data, and this gem is the runner that drives any engine through them.
|
|
8
|
+
|
|
9
|
+
== The seven families
|
|
10
|
+
|
|
11
|
+
[cols="1,2,3"]
|
|
12
|
+
|===
|
|
13
|
+
|Family |Cases |Facade faces
|
|
14
|
+
|
|
15
|
+
|builtin_lexical |81 |`lexical_valid?(type_name, lexical)`
|
|
16
|
+
|xsd_regex_translation |21 |`regex_translates?(pattern)`
|
|
17
|
+
|schema_wildcards |24 |`wildcard_covers?(...)`, `wildcard_exclusion?(...)`
|
|
18
|
+
|entity_registry |11 |`entity_declared?(...)` and the registry faces
|
|
19
|
+
|serialization |20 |`parse(xml)`, `serialize(doc)`
|
|
20
|
+
|c14n |14 |`canonicalize(doc)`
|
|
21
|
+
|xsd_structural |6 |`validate_xsd(schema, instance)`
|
|
22
|
+
|===
|
|
23
|
+
|
|
24
|
+
== Certifying an engine
|
|
25
|
+
|
|
26
|
+
The dataset ships vendored (`testdata/moxml-conformance.json`); the runner consumes rows and never restates them. Any object implementing the certification protocol can be certified:
|
|
27
|
+
|
|
28
|
+
[source, ruby]
|
|
29
|
+
----
|
|
30
|
+
require "moxml/testsuite"
|
|
31
|
+
|
|
32
|
+
report = Moxml::TestSuite.run(Moxml::TestSuite::Reference.new)
|
|
33
|
+
puts report
|
|
34
|
+
report.passed? # => true
|
|
35
|
+
----
|
|
36
|
+
|
|
37
|
+
`Reference` implements the whole protocol on moxml itself — pin the engine when the host carries several:
|
|
38
|
+
|
|
39
|
+
[source, ruby]
|
|
40
|
+
----
|
|
41
|
+
Moxml::TestSuite::Reference.new(adapter: :leptris)
|
|
42
|
+
Moxml::TestSuite::Reference.new(adapter: :nokogiri)
|
|
43
|
+
----
|
|
44
|
+
|
|
45
|
+
A facade that cannot answer a family's faces sees that family SKIP — loudly, in the report — never a quiet pass. A facade that answers against the dataset FAILs with the offending case named.
|
|
46
|
+
|
|
47
|
+
== The one-directional contract
|
|
48
|
+
|
|
49
|
+
The model's degradation is one-directional: an engine that cannot evaluate a construct stays silent (under-report); it never produces a different answer. The suite pins that too — unevaluatable rows expect under-reporting, and "answered wrong" and "answered differently per engine" are both failures. Equivalence is to the model, not merely engine-to-engine.
|
|
50
|
+
|
|
51
|
+
== Provenance
|
|
52
|
+
|
|
53
|
+
The dataset is generated from the moxml suite (`rake conformance:export` in lutaml/moxml) and consumed by the cross-language runners: moxml-rs (Rust) and moxml-ts (TypeScript). schema 5 is the row-shape version; `generator` names the producing moxml release.
|
|
54
|
+
|
|
55
|
+
== Development
|
|
56
|
+
|
|
57
|
+
----
|
|
58
|
+
bundle install
|
|
59
|
+
bundle exec rake
|
|
60
|
+
----
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module TestSuite
|
|
7
|
+
# The vendored dataset: the model's committed answers, seven
|
|
8
|
+
# families, schema-versioned. The JSON is the single source -
|
|
9
|
+
# runners consume rows, never restate them.
|
|
10
|
+
module Dataset
|
|
11
|
+
FAMILIES = %w[
|
|
12
|
+
builtin_lexical
|
|
13
|
+
xsd_regex_translation
|
|
14
|
+
schema_wildcards
|
|
15
|
+
entity_registry
|
|
16
|
+
serialization
|
|
17
|
+
c14n
|
|
18
|
+
xsd_structural
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
DATA_PATH = File.join(__dir__, "../../../testdata/moxml-conformance.json")
|
|
22
|
+
|
|
23
|
+
def self.load(path: DATA_PATH)
|
|
24
|
+
raw = JSON.parse(File.read(path))
|
|
25
|
+
Dataset::V1.new(raw)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class V1
|
|
29
|
+
attr_reader :schema, :generator, :cases
|
|
30
|
+
|
|
31
|
+
def initialize(raw)
|
|
32
|
+
@schema = raw["schema"]
|
|
33
|
+
@generator = raw["generator"]
|
|
34
|
+
@cases = raw["cases"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rows(family)
|
|
38
|
+
@cases.fetch(family)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
module Families
|
|
6
|
+
# Family: c14n (14 rows). Canonical XML 1.0, byte-identical:
|
|
7
|
+
# sorted attributes, expanded references, comments dropped.
|
|
8
|
+
# Facade: parse(xml) -> doc; canonicalize(doc) -> string.
|
|
9
|
+
class C14n
|
|
10
|
+
def initialize(facade, dataset)
|
|
11
|
+
@facade = facade
|
|
12
|
+
@rows = dataset.rows("c14n")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(report)
|
|
16
|
+
unless @facade.respond_to?(:canonicalize)
|
|
17
|
+
return report.skip("c14n", "facade does not implement canonicalize")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
failures = []
|
|
21
|
+
@rows.each do |row|
|
|
22
|
+
doc = @facade.parse(row["input"])
|
|
23
|
+
out = @facade.canonicalize(doc)
|
|
24
|
+
if out != row["canonical"]
|
|
25
|
+
failures << format("%s: got %s, want %s",
|
|
26
|
+
row["id"], out.inspect,
|
|
27
|
+
row["canonical"].inspect)
|
|
28
|
+
end
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
failures << format("%s: raised %s: %s", row["id"], e.class,
|
|
31
|
+
e.message[0, 120])
|
|
32
|
+
end
|
|
33
|
+
if failures.empty?
|
|
34
|
+
report.pass("c14n", @rows.length)
|
|
35
|
+
else
|
|
36
|
+
report.fail("c14n", failures, @rows.length)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
module Families
|
|
6
|
+
# Families: the pure-logic decision faces (the model's
|
|
7
|
+
# committed answers as functions).
|
|
8
|
+
#
|
|
9
|
+
# builtin_lexical (81): lexical_valid?(type_name, lexical)
|
|
10
|
+
# schema_wildcards (24): the pinned covers/exclusion protocol -
|
|
11
|
+
# wildcard_covers?(constraint:, process:, not_namespace:,
|
|
12
|
+
# not_qname:, item_namespace:) -> bool
|
|
13
|
+
# wildcard_exclusion?(constraint:, process:, not_namespace:,
|
|
14
|
+
# not_qname:, item_namespace:, item_name:,
|
|
15
|
+
# defined:, sibling_names:) -> String | nil
|
|
16
|
+
# (the dataset's committed shape: a row answers covers? plus
|
|
17
|
+
# the negative-exclusion symbol; there is no single "admits")
|
|
18
|
+
# xsd_regex_translation (21): regex_translates?(pattern) ->
|
|
19
|
+
# regexp | nil (nil = untranslatable per the model).
|
|
20
|
+
class Decisions
|
|
21
|
+
def initialize(facade, dataset)
|
|
22
|
+
@facade = facade
|
|
23
|
+
@dataset = dataset
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
DECISION_FACES = %i[
|
|
27
|
+
lexical_valid? wildcard_covers? regex_translates?
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def call(report)
|
|
31
|
+
if DECISION_FACES.none? { |face| @facade.respond_to?(face) }
|
|
32
|
+
return report.skip("decisions",
|
|
33
|
+
"facade does not implement the decision faces")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
failures = []
|
|
37
|
+
failures += run_lexical
|
|
38
|
+
failures += run_wildcards
|
|
39
|
+
failures += run_regex
|
|
40
|
+
total = @dataset.rows("builtin_lexical").length +
|
|
41
|
+
@dataset.rows("schema_wildcards").length +
|
|
42
|
+
@dataset.rows("xsd_regex_translation").length
|
|
43
|
+
if failures.empty?
|
|
44
|
+
report.pass("decisions", total)
|
|
45
|
+
else
|
|
46
|
+
report.fail("decisions", failures, total)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def run_lexical
|
|
53
|
+
return [] unless @facade.respond_to?(:lexical_valid?)
|
|
54
|
+
|
|
55
|
+
@dataset.rows("builtin_lexical").filter_map do |row|
|
|
56
|
+
next if @facade.lexical_valid?(row["type"],
|
|
57
|
+
row["lexical"]) == row["valid"]
|
|
58
|
+
|
|
59
|
+
format("lexical %s %s: got %s, want %s",
|
|
60
|
+
row["type"], row["lexical"].inspect,
|
|
61
|
+
!row["valid"], row["valid"])
|
|
62
|
+
end
|
|
63
|
+
rescue NoMethodError, ArgumentError => e
|
|
64
|
+
["lexical face unavailable: #{e.class}: #{e.message[0, 80]}"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def run_wildcards
|
|
68
|
+
return [] unless @facade.respond_to?(:wildcard_covers?)
|
|
69
|
+
|
|
70
|
+
@dataset.rows("schema_wildcards").filter_map do |row|
|
|
71
|
+
common = {
|
|
72
|
+
constraint: row["constraint"], process: row["process"],
|
|
73
|
+
not_namespace: row["not_namespace"],
|
|
74
|
+
not_qname: Array(row["not_qname"]),
|
|
75
|
+
item_namespace: row["item_namespace"]
|
|
76
|
+
}
|
|
77
|
+
covers = @facade.wildcard_covers?(**common)
|
|
78
|
+
unless covers == row["covers"]
|
|
79
|
+
next format("wildcard %s/%s covers?(%s): got %s, want %s",
|
|
80
|
+
row["constraint"], row["process"],
|
|
81
|
+
row["item_qname"], covers, row["covers"])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
exclusion = @facade.wildcard_exclusion?(
|
|
85
|
+
**common, item_name: row["item_qname"],
|
|
86
|
+
defined: row["defined"],
|
|
87
|
+
sibling_names: Array(row["sibling_names"])
|
|
88
|
+
)
|
|
89
|
+
want = row["exclusion"]
|
|
90
|
+
next if exclusion.to_s == want.to_s
|
|
91
|
+
|
|
92
|
+
format("wildcard %s/%s exclusion(%s): got %s, want %s",
|
|
93
|
+
row["constraint"], row["process"], row["item_qname"],
|
|
94
|
+
exclusion.inspect, want.inspect)
|
|
95
|
+
end
|
|
96
|
+
rescue NoMethodError, ArgumentError => e
|
|
97
|
+
["wildcard face unavailable: #{e.class}: #{e.message[0, 80]}"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run_regex
|
|
101
|
+
return [] unless @facade.respond_to?(:regex_translates?)
|
|
102
|
+
|
|
103
|
+
@dataset.rows("xsd_regex_translation").filter_map do |row|
|
|
104
|
+
compiled = @facade.regex_translates?(row["pattern"])
|
|
105
|
+
next if compiled.nil? != row["translatable"]
|
|
106
|
+
|
|
107
|
+
next unless row["translatable"] &&
|
|
108
|
+
compiled.match?(row["value"]) != row["matches"]
|
|
109
|
+
|
|
110
|
+
format("regex %s %s: matched %s, want %s",
|
|
111
|
+
row["pattern"].inspect, row["value"].inspect,
|
|
112
|
+
compiled.match?(row["value"]), row["matches"])
|
|
113
|
+
end
|
|
114
|
+
rescue NoMethodError, ArgumentError => e
|
|
115
|
+
["regex face unavailable: #{e.class}: #{e.message[0, 80]}"]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
module Families
|
|
6
|
+
# Family: entity_registry (11 rows). The registry contract:
|
|
7
|
+
# lookup (declared?, codepoint_for_name, names_for_codepoint,
|
|
8
|
+
# primary_name_for_codepoint, standard_entity?) and restoration
|
|
9
|
+
# (should_restore? under a restoration mode). Facade faces are
|
|
10
|
+
# seed-scoped - every call carries the seed map so engines stay
|
|
11
|
+
# stateless across rows:
|
|
12
|
+
#
|
|
13
|
+
# entity_declared?(seed:, name:)
|
|
14
|
+
# entity_codepoint(seed:, name:)
|
|
15
|
+
# entity_names(seed:, codepoint:)
|
|
16
|
+
# entity_primary_name(seed:, codepoint:)
|
|
17
|
+
# entity_standard?(seed:, codepoint:)
|
|
18
|
+
# entity_should_restore?(seed:, codepoint:, restore_entities:,
|
|
19
|
+
# restoration_mode:)
|
|
20
|
+
class EntityRegistry
|
|
21
|
+
def initialize(facade, dataset)
|
|
22
|
+
@facade = facade
|
|
23
|
+
@rows = dataset.rows("entity_registry")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(report)
|
|
27
|
+
unless @facade.respond_to?(:entity_declared?)
|
|
28
|
+
return report.skip("entity_registry",
|
|
29
|
+
"facade does not implement the registry faces")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
failures = []
|
|
33
|
+
@rows.each do |row|
|
|
34
|
+
failures.concat(check_row(row))
|
|
35
|
+
end
|
|
36
|
+
if failures.empty?
|
|
37
|
+
report.pass("entity_registry", @rows.length)
|
|
38
|
+
else
|
|
39
|
+
report.fail("entity_registry", failures,
|
|
40
|
+
@rows.length)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def check_row(row)
|
|
47
|
+
seed = row["seed"]
|
|
48
|
+
label = "#{row['qname'].inspect}/#{row['qcp']} seed=#{seed.inspect}"
|
|
49
|
+
[
|
|
50
|
+
[row["declared"],
|
|
51
|
+
@facade.entity_declared?(seed: seed, name: row["qname"])],
|
|
52
|
+
[row["codepoint"],
|
|
53
|
+
@facade.entity_codepoint(seed: seed, name: row["qname"])],
|
|
54
|
+
[row["names"],
|
|
55
|
+
@facade.entity_names(seed: seed, codepoint: row["qcp"])],
|
|
56
|
+
[row["primary"],
|
|
57
|
+
@facade.entity_primary_name(seed: seed, codepoint: row["qcp"])],
|
|
58
|
+
[row["standard"],
|
|
59
|
+
@facade.entity_standard?(seed: seed, codepoint: row["qcp"])],
|
|
60
|
+
[row["restore"],
|
|
61
|
+
@facade.entity_should_restore?(
|
|
62
|
+
seed: seed, codepoint: row["qcp"],
|
|
63
|
+
restore_entities: row["restore_entities"],
|
|
64
|
+
restoration_mode: row["mode"]
|
|
65
|
+
)],
|
|
66
|
+
].filter_map do |want, got|
|
|
67
|
+
next if want == got
|
|
68
|
+
|
|
69
|
+
format("%s: got %s, want %s", label, got.inspect, want.inspect)
|
|
70
|
+
end
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
[format("%s: raised %s: %s", label, e.class, e.message[0, 120])]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
module Families
|
|
6
|
+
# Family: serialization (20 rows). The emitter contract:
|
|
7
|
+
# long-form empty elements, the attribute escaping shapes
|
|
8
|
+
# (named + numeric references), namespace declarations
|
|
9
|
+
# verbatim. Facade: parse(xml) -> doc; serialize(doc) -> string.
|
|
10
|
+
class Serialization
|
|
11
|
+
def initialize(facade, dataset)
|
|
12
|
+
@facade = facade
|
|
13
|
+
@rows = dataset.rows("serialization")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(report)
|
|
17
|
+
failures = []
|
|
18
|
+
@rows.each do |row|
|
|
19
|
+
doc = @facade.parse(row["input"])
|
|
20
|
+
out = @facade.serialize(doc)
|
|
21
|
+
# A single trailing document newline is formatting, not a
|
|
22
|
+
# committed emitter shape - the dataset rows are chomped.
|
|
23
|
+
if out&.chomp != row["output"]
|
|
24
|
+
failures << format("%s: got %s, want %s",
|
|
25
|
+
row["id"], out.inspect, row["output"].inspect)
|
|
26
|
+
end
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
failures << format("%s: raised %s: %s", row["id"], e.class,
|
|
29
|
+
e.message[0, 120])
|
|
30
|
+
end
|
|
31
|
+
if failures.empty?
|
|
32
|
+
report.pass("serialization", @rows.length)
|
|
33
|
+
else
|
|
34
|
+
report.fail("serialization", failures, @rows.length)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
module Families
|
|
6
|
+
# Family: xsd_structural (6 rows). Schema + instance pairs
|
|
7
|
+
# with the committed verdict. Facade: parse(xml);
|
|
8
|
+
# validate_xsd(schema_xml, instance) -> [] (valid) | [#to_s].
|
|
9
|
+
class XsdStructural
|
|
10
|
+
def initialize(facade, dataset)
|
|
11
|
+
@facade = facade
|
|
12
|
+
@rows = dataset.rows("xsd_structural")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(report)
|
|
16
|
+
unless @facade.respond_to?(:validate_xsd)
|
|
17
|
+
return report.skip("xsd_structural",
|
|
18
|
+
"facade does not implement validate_xsd")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
failures = []
|
|
22
|
+
@rows.each do |row|
|
|
23
|
+
errs = @facade.validate_xsd(row["schema"], row["instance"])
|
|
24
|
+
valid = errs.nil? || errs.empty?
|
|
25
|
+
expected = row["valid"]
|
|
26
|
+
if valid != expected
|
|
27
|
+
failures << format("%s: expected %s, engine said %s (%s)",
|
|
28
|
+
row["id"], expected, valid,
|
|
29
|
+
Array(errs).first || "no rows")
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
failures << format("%s: raised %s: %s", row["id"], e.class,
|
|
33
|
+
e.message[0, 120])
|
|
34
|
+
end
|
|
35
|
+
if failures.empty?
|
|
36
|
+
report.pass("xsd_structural", @rows.length)
|
|
37
|
+
else
|
|
38
|
+
report.fail("xsd_structural", failures,
|
|
39
|
+
@rows.length)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
# The moxml-backed reference facade: every certification face
|
|
6
|
+
# answered through moxml itself. Any moxml install certifies its
|
|
7
|
+
# own engine with it:
|
|
8
|
+
#
|
|
9
|
+
# report = Moxml::TestSuite.run(Moxml::TestSuite::Reference.new)
|
|
10
|
+
# puts report
|
|
11
|
+
# report.passed? # => true
|
|
12
|
+
#
|
|
13
|
+
# Pin the engine explicitly when the host carries several:
|
|
14
|
+
#
|
|
15
|
+
# Moxml::TestSuite::Reference.new(adapter: :leptris)
|
|
16
|
+
#
|
|
17
|
+
# The decision faces (lexical, wildcard, regex, entity registry)
|
|
18
|
+
# are engine-free - moxml answers them in pure Ruby, the model's
|
|
19
|
+
# committed answers. The DOM faces (parse, serialize, canonicalize,
|
|
20
|
+
# validate_xsd) run on the selected adapter: that is the point -
|
|
21
|
+
# the suite certifies the ENGINE's observable behavior against
|
|
22
|
+
# the model.
|
|
23
|
+
class Reference
|
|
24
|
+
def initialize(adapter: nil)
|
|
25
|
+
require "moxml"
|
|
26
|
+
@context = adapter ? Moxml.new(adapter) : Moxml.new
|
|
27
|
+
# The dataset pins the model's committed emitter shape:
|
|
28
|
+
# compact, no inter-element indentation.
|
|
29
|
+
@context.config.default_indent = 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :context
|
|
33
|
+
|
|
34
|
+
# -- DOM faces -------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def parse(xml)
|
|
37
|
+
@context.parse(xml)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serialize(doc)
|
|
41
|
+
doc.to_xml
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def canonicalize(doc)
|
|
45
|
+
Moxml::C14n.canonicalize(doc)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_xsd(schema_xml, instance)
|
|
49
|
+
@context.xsd(schema_xml).validate(instance)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# -- decision faces (engine-free) ------------------------------
|
|
53
|
+
|
|
54
|
+
def lexical_valid?(type_name, lexical)
|
|
55
|
+
Moxml::XSD::BuiltinLexical.valid?(type_name, lexical)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def regex_translates?(pattern)
|
|
59
|
+
Moxml::XSD::SchemaRules::XsdRegex.translate(pattern)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def wildcard_covers?(constraint:, process:, not_namespace:,
|
|
63
|
+
not_qname:, item_namespace:)
|
|
64
|
+
wildcard(constraint, process, not_namespace, not_qname)
|
|
65
|
+
.covers?(item_namespace.to_sym)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def wildcard_exclusion?(constraint:, process:, not_namespace:,
|
|
69
|
+
not_qname:, item_namespace:, item_name:,
|
|
70
|
+
defined:, sibling_names:)
|
|
71
|
+
answer = wildcard(constraint, process, not_namespace, not_qname)
|
|
72
|
+
.negative_exclusion(item_name, item_namespace.to_sym,
|
|
73
|
+
defined: defined,
|
|
74
|
+
sibling_names: sibling_names)
|
|
75
|
+
answer&.to_s
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def entity_declared?(seed:, name:)
|
|
79
|
+
registry(seed).declared?(name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def entity_codepoint(seed:, name:)
|
|
83
|
+
registry(seed).codepoint_for_name(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def entity_names(seed:, codepoint:)
|
|
87
|
+
registry(seed).names_for_codepoint(codepoint)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def entity_primary_name(seed:, codepoint:)
|
|
91
|
+
registry(seed).primary_name_for_codepoint(codepoint)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def entity_standard?(seed:, codepoint:)
|
|
95
|
+
registry(seed).standard_entity?(codepoint)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def entity_should_restore?(seed:, codepoint:, restore_entities:,
|
|
99
|
+
restoration_mode:)
|
|
100
|
+
registry(seed).should_restore?(
|
|
101
|
+
codepoint, config: restoration_config(restore_entities,
|
|
102
|
+
restoration_mode)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def wildcard(constraint, process, not_namespace, not_qname)
|
|
109
|
+
Moxml::XSD::SchemaRules::Wildcard.new(
|
|
110
|
+
constraint, process,
|
|
111
|
+
not_namespace: not_namespace,
|
|
112
|
+
not_qname: Array(not_qname).join(" "),
|
|
113
|
+
element: nil
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def registry(seed)
|
|
118
|
+
Moxml::EntityRegistry.new(mode: :custom, entity_provider: -> { seed })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def restoration_config(restore_entities, restoration_mode)
|
|
122
|
+
config = Moxml::Config.new
|
|
123
|
+
config.restore_entities = restore_entities
|
|
124
|
+
config.entity_restoration_mode = restoration_mode.to_sym
|
|
125
|
+
config
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module TestSuite
|
|
5
|
+
# The certification outcome: per-family pass/fail/skip with the
|
|
6
|
+
# failing case ids. Truthy: report.passed? after run.
|
|
7
|
+
class Report
|
|
8
|
+
attr_reader :facade_class, :results
|
|
9
|
+
|
|
10
|
+
def initialize(facade_class:)
|
|
11
|
+
@facade_class = facade_class
|
|
12
|
+
@results = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def pass(family, count)
|
|
16
|
+
@results[family] = { status: :pass, count: count, failures: [] }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fail(family, failures, count)
|
|
20
|
+
@results[family] = { status: :fail, count: count, failures: failures }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def skip(family, reason)
|
|
24
|
+
@results[family] = { status: :skip, reason: reason }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def passed?
|
|
28
|
+
@results.values.all? { |r| r[:status] != :fail }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failures
|
|
32
|
+
@results.select { |_, r| r[:status] == :fail }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def summary
|
|
36
|
+
lines = ["moxml-testsuite (facade: #{@facade_class})"]
|
|
37
|
+
@results.each do |family, r|
|
|
38
|
+
case r[:status]
|
|
39
|
+
when :pass then lines << " PASS #{family} (#{r[:count]} cases)"
|
|
40
|
+
when :fail
|
|
41
|
+
lines << " FAIL #{family} " \
|
|
42
|
+
"(#{r[:failures].length}/#{r[:count]} cases):"
|
|
43
|
+
r[:failures].each { |f| lines << " #{f}" }
|
|
44
|
+
when :skip then lines << " SKIP #{family} (#{r[:reason]})"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
lines.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
summary
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|