moxml 0.5.149 → 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/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 +32 -41
- 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 +42 -3
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +183 -4
- data/lib/moxml/adapter/libxml.rb +34 -2
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +39 -7
- data/lib/moxml/adapter/ox.rb +8 -0
- data/lib/moxml/adapter/rexml.rb +17 -2
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +28 -0
- data/lib/moxml/document.rb +17 -3
- data/lib/moxml/node.rb +56 -5
- data/lib/moxml/node_set.rb +31 -1
- 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.rb +28 -0
- data/lib/moxml.rb +1 -0
- data/spec/consistency/adapter_parity_spec.rb +22 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/model_coverage_audit_spec.rb +61 -86
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/xpath_idioms_spec.rb +23 -5
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +33 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -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/element_indent_spec.rb +34 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +45 -74
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/support/input_taxonomy.rb +7 -0
- data/spec/support/model_coverage.rb +159 -0
- metadata +17 -2
data/lib/moxml/node.rb
CHANGED
|
@@ -36,6 +36,11 @@ module Moxml
|
|
|
36
36
|
self
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Document-derived wrapper state (the entity-marker verdict) flows
|
|
40
|
+
# parent -> child during prewrapped traversal; adapters that keep
|
|
41
|
+
# no per-wrapper document state carry nothing.
|
|
42
|
+
def inherit_document_state(_parent); end
|
|
43
|
+
|
|
39
44
|
def refresh_native!(new_native)
|
|
40
45
|
unless new_native.equal?(@native)
|
|
41
46
|
# Cache-stable binding nodes carry the wrapper in an ivar
|
|
@@ -226,7 +231,8 @@ module Moxml
|
|
|
226
231
|
def attach_previous_sibling(node)
|
|
227
232
|
context.bump_children_generation
|
|
228
233
|
node = prepare_node(node)
|
|
229
|
-
adapter.add_previous_sibling(@native, node.native)
|
|
234
|
+
inserted = adapter.add_previous_sibling(@native, node.native)
|
|
235
|
+
refresh_inserted_native(node, inserted)
|
|
230
236
|
# Invalidate the parent's memoized children list. The wrapper-side
|
|
231
237
|
# @parent_node link is only set when this node was yielded through a
|
|
232
238
|
# parent-aware NodeSet; nodes obtained via at()/xpath() carry no such
|
|
@@ -239,12 +245,20 @@ module Moxml
|
|
|
239
245
|
def attach_next_sibling_to(anchor, node)
|
|
240
246
|
context.bump_children_generation
|
|
241
247
|
node = prepare_node(node)
|
|
242
|
-
adapter.add_next_sibling(anchor.native, node.native)
|
|
248
|
+
inserted = adapter.add_next_sibling(anchor.native, node.native)
|
|
249
|
+
refresh_inserted_native(node, inserted)
|
|
243
250
|
anchor.parent&.invalidate_children_cache!
|
|
244
251
|
anchor.invalidate_parent_children_cache!
|
|
245
252
|
self
|
|
246
253
|
end
|
|
247
254
|
|
|
255
|
+
def refresh_inserted_native(node, inserted)
|
|
256
|
+
return unless adapter.add_sibling_returns_inserted?
|
|
257
|
+
return if inserted.nil? || inserted.equal?(node.native)
|
|
258
|
+
|
|
259
|
+
node.refresh_native!(inserted)
|
|
260
|
+
end
|
|
261
|
+
|
|
248
262
|
def remove
|
|
249
263
|
# Nokogiri parity: unlinking an already-detached node is a
|
|
250
264
|
# no-op returning self. Re-attach idioms like
|
|
@@ -296,6 +310,21 @@ module Moxml
|
|
|
296
310
|
end
|
|
297
311
|
|
|
298
312
|
def to_xml(options = {})
|
|
313
|
+
# Element-level indentation is a declared capability: engines
|
|
314
|
+
# that ignore it must not answer unindented output silently
|
|
315
|
+
# (D-09). indent: 0 and the no-option path stay common.
|
|
316
|
+
indent = options[:indent]
|
|
317
|
+
if !is_a?(Document) && indent.is_a?(Integer) && indent.positive? &&
|
|
318
|
+
!adapter.element_indent_supported?
|
|
319
|
+
raise Moxml::AdapterError.new(
|
|
320
|
+
"element-level indentation is not supported by the " \
|
|
321
|
+
"#{adapter.name.split('::').last} adapter; serialize the " \
|
|
322
|
+
"document or use indent: 0",
|
|
323
|
+
adapter: adapter.name,
|
|
324
|
+
operation: "serialize",
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
299
328
|
# Determine if we should include XML declaration
|
|
300
329
|
# For Document nodes: check native then wrapper, unless explicitly overridden
|
|
301
330
|
# For other nodes: default to no declaration unless explicitly set
|
|
@@ -367,14 +396,26 @@ module Moxml
|
|
|
367
396
|
Materializer.materialize_fields(self, &block)
|
|
368
397
|
end
|
|
369
398
|
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
399
|
+
# The first node of a node-set; scalar results (count(), string(),
|
|
400
|
+
# boolean()) pass through unwrapped and uniform across adapters.
|
|
401
|
+
# Adapters with a native first-only face use it (see
|
|
402
|
+
# Document#at_xpath).
|
|
373
403
|
def at_xpath(expression, namespaces = {})
|
|
404
|
+
if adapter.native_at_xpath?
|
|
405
|
+
result = adapter.at_xpath(@native, expression, namespaces)
|
|
406
|
+
return result if scalar_result?(result)
|
|
407
|
+
|
|
408
|
+
return Moxml::Node.wrap(result, context)
|
|
409
|
+
end
|
|
374
410
|
result = xpath(expression, namespaces)
|
|
375
411
|
result.is_a?(NodeSet) ? result.first : result
|
|
376
412
|
end
|
|
377
413
|
|
|
414
|
+
def scalar_result?(result)
|
|
415
|
+
result.is_a?(Numeric) || result.is_a?(String) ||
|
|
416
|
+
result == true || result == false || result.nil?
|
|
417
|
+
end
|
|
418
|
+
|
|
378
419
|
# Nokogiri-compatible sugar: at/search/at_css/css. Nokogiri's
|
|
379
420
|
# at/search auto-detect XPath vs CSS; the XPath-first fallback
|
|
380
421
|
# covers the dominant caller shape (XPath expression strings)
|
|
@@ -906,6 +947,16 @@ module Moxml
|
|
|
906
947
|
# input native keeps its registration so later wraps of the
|
|
907
948
|
# same base object hit the cache instead of re-minting.
|
|
908
949
|
if (extended = adapter.wrap_native(node, type, context))
|
|
950
|
+
# A native that already carries the contract IS the wrapper
|
|
951
|
+
# (leptris C mint): prime it in place and skip the WeakMap
|
|
952
|
+
# round trip — the primed @native-identity answers every
|
|
953
|
+
# later wrap through the fast path above.
|
|
954
|
+
if extended.equal?(node) && extended.is_a?(Node)
|
|
955
|
+
extended.prime_contract(extended, context, adapter, type) unless
|
|
956
|
+
extended.instance_variable_defined?(:@context)
|
|
957
|
+
return extended
|
|
958
|
+
end
|
|
959
|
+
|
|
909
960
|
extended.prime_contract(extended, context, adapter, type)
|
|
910
961
|
context.register_wrapper(node, extended)
|
|
911
962
|
context.register_wrapper(extended, extended) unless extended.equal?(node)
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -39,12 +39,18 @@ module Moxml
|
|
|
39
39
|
# nodes: Array of natives, or an adapter's LazyNodeSet — the
|
|
40
40
|
# native set is held unmaterialized until an operation needs an
|
|
41
41
|
# Array (#+, #<<, #delete, Range slices).
|
|
42
|
-
|
|
42
|
+
#
|
|
43
|
+
# prewrapped: the adapter vouches the entries are already
|
|
44
|
+
# contract-carrying wrappers (the leptris C-mint children face),
|
|
45
|
+
# so the walk path skips the wrapped-buffer allocation and the
|
|
46
|
+
# per-child wrap dispatch.
|
|
47
|
+
def initialize(nodes, context, parent_node = nil, prewrapped: false)
|
|
43
48
|
@nodes = nodes.is_a?(Array) ? nodes : nil
|
|
44
49
|
@lazy = @nodes ? nil : nodes
|
|
45
50
|
@context = context
|
|
46
51
|
@wrapped = nil
|
|
47
52
|
@parent_node = parent_node
|
|
53
|
+
@prewrapped = prewrapped
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
# The raw native nodes — adapter-internal work only (issue #26).
|
|
@@ -59,6 +65,13 @@ module Moxml
|
|
|
59
65
|
def each
|
|
60
66
|
return to_enum(:each) unless block_given?
|
|
61
67
|
|
|
68
|
+
if @prewrapped
|
|
69
|
+
(@nodes || @lazy).each do |node|
|
|
70
|
+
yield adopt_prewrapped(node)
|
|
71
|
+
end
|
|
72
|
+
return self
|
|
73
|
+
end
|
|
74
|
+
|
|
62
75
|
wrapped = wrapped_buffer
|
|
63
76
|
index = 0
|
|
64
77
|
(@nodes || @lazy).each do |node|
|
|
@@ -227,5 +240,22 @@ module Moxml
|
|
|
227
240
|
end
|
|
228
241
|
wrapped
|
|
229
242
|
end
|
|
243
|
+
|
|
244
|
+
# The prewrapped entry path: prime a not-yet-primed C-minted
|
|
245
|
+
# wrapper in place (one ivar check), attach the traversal parent,
|
|
246
|
+
# and hand the document's marker verdict down the tree.
|
|
247
|
+
def adopt_prewrapped(node)
|
|
248
|
+
unless node.is_a?(Moxml::Node)
|
|
249
|
+
node = Moxml::Node.wrap(node, @context)
|
|
250
|
+
end
|
|
251
|
+
unless node.instance_variable_defined?(:@context)
|
|
252
|
+
node.prime_contract(node, @context, Moxml::Node.adapter(@context))
|
|
253
|
+
end
|
|
254
|
+
if @parent_node
|
|
255
|
+
node.parent_node = @parent_node
|
|
256
|
+
node.inherit_document_state(@parent_node)
|
|
257
|
+
end
|
|
258
|
+
node
|
|
259
|
+
end
|
|
230
260
|
end
|
|
231
261
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# RELAX NG schema validation (TODO.sota/05): a facade for engines
|
|
5
|
+
# that ship an RNG validator. Engines without one raise the named
|
|
6
|
+
# Moxml::NotImplementedError — the face is never silently absent.
|
|
7
|
+
#
|
|
8
|
+
# ctx = Moxml.new(:leptris)
|
|
9
|
+
# schema = ctx.relaxng_file("schema.rng")
|
|
10
|
+
# schema.valid?(doc) # => true/false
|
|
11
|
+
# schema.validate_errors(doc) # => [] or [{line:, column:, message:}]
|
|
12
|
+
# schema.validate(doc) # => [] or ["line:col: error: message"]
|
|
13
|
+
#
|
|
14
|
+
# Adapters without an engine raise at compile; RelaxNG.supported?
|
|
15
|
+
# answers without raising (the XSD facade's pattern).
|
|
16
|
+
module RelaxNG
|
|
17
|
+
class << self
|
|
18
|
+
# Whether the context's adapter ships a RELAX NG validator.
|
|
19
|
+
def supported?(context)
|
|
20
|
+
adapter = context.config.adapter
|
|
21
|
+
adapter.const_defined?(:RELAXNG_SUPPORTED) &&
|
|
22
|
+
adapter.const_get(:RELAXNG_SUPPORTED)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A compiled schema (adapter-owned handle).
|
|
27
|
+
class Schema
|
|
28
|
+
def initialize(handle, context)
|
|
29
|
+
@handle = handle
|
|
30
|
+
@context = context
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid?(document)
|
|
34
|
+
@context.config.adapter.relaxng_valid?(@handle, native(document))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Structured report: [{line:, column:, message:}] (empty = valid).
|
|
38
|
+
def validate_errors(document)
|
|
39
|
+
@context.config.adapter
|
|
40
|
+
.relaxng_validate_errors(@handle, native(document))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Nokogiri shape: error strings (empty = valid).
|
|
44
|
+
def validate(document)
|
|
45
|
+
validate_errors(document).map do |error|
|
|
46
|
+
line = error[:line]
|
|
47
|
+
if line&.to_i&.positive?
|
|
48
|
+
"#{line}:#{error[:column]}: error: #{error[:message]}"
|
|
49
|
+
else
|
|
50
|
+
error[:message]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def native(document)
|
|
58
|
+
document.is_a?(Moxml::Document) ? document.native : document
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -191,6 +191,13 @@ module Moxml
|
|
|
191
191
|
def on_warning(message)
|
|
192
192
|
@handlers[:warning]&.call(message)
|
|
193
193
|
end
|
|
194
|
+
|
|
195
|
+
# A block handler subscribes to a family only when its DSL block
|
|
196
|
+
# was registered — it overrides every callback, so method
|
|
197
|
+
# ownership cannot answer interest here.
|
|
198
|
+
def handles?(family)
|
|
199
|
+
@handlers.key?(family)
|
|
200
|
+
end
|
|
194
201
|
end
|
|
195
202
|
end
|
|
196
203
|
end
|
data/lib/moxml/sax/handler.rb
CHANGED
|
@@ -98,6 +98,26 @@ module Moxml
|
|
|
98
98
|
raise error
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# SAX event families that engines may or may not report as
|
|
102
|
+
# distinct events, with their handler methods. An engine that
|
|
103
|
+
# cannot report a family must raise the named error when the
|
|
104
|
+
# handler subscribes — never fold the events silently.
|
|
105
|
+
EVENT_METHODS = {
|
|
106
|
+
cdata: :on_cdata,
|
|
107
|
+
comment: :on_comment,
|
|
108
|
+
processing_instruction: :on_processing_instruction,
|
|
109
|
+
}.freeze
|
|
110
|
+
|
|
111
|
+
# Whether this handler subscribes to an event family: the
|
|
112
|
+
# callback was overridden (class handlers) or registered (block
|
|
113
|
+
# handlers).
|
|
114
|
+
#
|
|
115
|
+
# @param family [Symbol] one of EVENT_METHODS keys
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def handles?(family)
|
|
118
|
+
method(EVENT_METHODS.fetch(family)).owner != Moxml::SAX::Handler
|
|
119
|
+
end
|
|
120
|
+
|
|
101
121
|
# Called when a non-fatal warning occurs
|
|
102
122
|
#
|
|
103
123
|
# Default implementation ignores warnings.
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath.rb
CHANGED
|
@@ -26,5 +26,33 @@ module Moxml
|
|
|
26
26
|
autoload :Node, "moxml/xpath/ruby/node"
|
|
27
27
|
autoload :Generator, "moxml/xpath/ruby/generator"
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Evaluate an expression through the pure-Ruby XPath 1.0 engine —
|
|
33
|
+
# the model's reference lane. Adapters route shapes their native
|
|
34
|
+
# engine cannot answer here (the `xmlns:` idiom, `text()` node
|
|
35
|
+
# tests, ...). Returns the engine's answer: an Array of wrapped
|
|
36
|
+
# nodes in document order, a NodeSet, or a scalar.
|
|
37
|
+
def evaluate(node, expression, namespaces = {}, adapter: nil)
|
|
38
|
+
context = node.is_a?(Moxml::Node) ? node.context : ::Moxml::Context.new(adapter)
|
|
39
|
+
wrapped = node.is_a?(Moxml::Node) ? node : Moxml::Node.wrap(node, context)
|
|
40
|
+
compiled = Compiler.compile_with_cache(Parser.parse(expression),
|
|
41
|
+
namespaces: namespaces)
|
|
42
|
+
compiled.call(wrapped)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The adapter-contract form of #evaluate: Array<native> | scalar.
|
|
46
|
+
def evaluate_natives(node, expression, namespaces = {}, adapter: nil)
|
|
47
|
+
result = evaluate(node, expression, namespaces, adapter: adapter)
|
|
48
|
+
case result
|
|
49
|
+
when Array
|
|
50
|
+
result.map { |n| n.is_a?(Moxml::Node) ? n.native : n }
|
|
51
|
+
when NodeSet
|
|
52
|
+
result.to_a.map(&:native)
|
|
53
|
+
else
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
end
|
|
29
57
|
end
|
|
30
58
|
end
|
data/lib/moxml.rb
CHANGED
|
@@ -105,6 +105,7 @@ module Moxml
|
|
|
105
105
|
autoload :Reference, "moxml/reference"
|
|
106
106
|
autoload :XSLT, "moxml/xslt"
|
|
107
107
|
autoload :XSD, "moxml/xsd"
|
|
108
|
+
autoload :RelaxNG, "moxml/relaxng"
|
|
108
109
|
|
|
109
110
|
# Error hierarchy — each subclass autoloads from the same file
|
|
110
111
|
autoload :Error, "moxml/error"
|
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
RSpec.describe "Adapter Examples" do
|
|
6
6
|
let(:xml) { "<root><child>text</child></root>" }
|
|
7
7
|
|
|
8
|
+
describe "Document children listing" do
|
|
9
|
+
it "never lists the XML declaration as a child" do
|
|
10
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter|
|
|
11
|
+
doc = Moxml.new(adapter)
|
|
12
|
+
.parse(%(<?xml version="1.0"?><r/>))
|
|
13
|
+
expect(doc.children.map(&:node_type)).to eq([:element]),
|
|
14
|
+
"#{adapter}: declaration leaked " \
|
|
15
|
+
"into children (model: state, not a node)"
|
|
16
|
+
expect(doc.has_xml_declaration).to be(true),
|
|
17
|
+
"#{adapter}: declaration state lost"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "lists the doctype per the Nokogiri contract" do
|
|
22
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter|
|
|
23
|
+
doc = Moxml.new(adapter).parse(%(<!DOCTYPE r><r/>))
|
|
24
|
+
expect(doc.children.map(&:node_type)).to eq(%i[doctype element]),
|
|
25
|
+
"#{adapter}: doctype listing"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
8
30
|
describe "Serialization consistency" do
|
|
9
31
|
it "produces equivalent XML across adapters",
|
|
10
32
|
skip: "No easy way to exclude the declaration from Nokogiri documents" do
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Citation hygiene (TODO.sota/07): a spec or doc that calls a
|
|
6
|
+
# divergence "pre-existing" or "documented" must resolve it to a
|
|
7
|
+
# register ID (D-NN) on the same line. The register is the only place
|
|
8
|
+
# such a claim is allowed to stand on its own.
|
|
9
|
+
CITATION_PHRASE = /(?:\bpre-existing\b|\bpreexisting\b|\bdocumented (?:divergence|limitation)\b)/i
|
|
10
|
+
|
|
11
|
+
RSpec.describe "divergence citations" do
|
|
12
|
+
def offenses(paths)
|
|
13
|
+
paths.flat_map do |path|
|
|
14
|
+
File.readlines(path).each_with_index.filter_map do |line, idx|
|
|
15
|
+
claim = line.match?(CITATION_PHRASE)
|
|
16
|
+
cited = line.match?(/D-\d+/)
|
|
17
|
+
"#{path}:#{idx + 1}: #{line.strip}" if claim && !cited
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "resolves every pre-existing/documented divergence claim to a register ID" do
|
|
23
|
+
paths = (Dir.glob("spec/**/*.rb") + Dir.glob("docs/**/*.adoc"))
|
|
24
|
+
.reject { |path| path.end_with?("citation_hygiene_spec.rb") }
|
|
25
|
+
found = offenses(paths)
|
|
26
|
+
expect(found).to be_empty,
|
|
27
|
+
"name a D-NN (docs/_pages/divergences.adoc) or drop " \
|
|
28
|
+
"the claim:\n#{found.join("\n")}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
+
require "support/model_coverage"
|
|
4
5
|
|
|
5
6
|
# Model coverage matrix (TODO.model/03): every face family of the
|
|
6
7
|
# processing model is governed for every adapter — covered by a pin
|
|
@@ -8,84 +9,13 @@ require "spec_helper"
|
|
|
8
9
|
# a registered divergence (adapter => D-ID), or loud (adapter =>
|
|
9
10
|
# the named error class). An unpinned family is an ungoverned cell:
|
|
10
11
|
# this audit fails until it has one of the three.
|
|
11
|
-
#
|
|
12
|
+
#
|
|
13
|
+
# The matrix itself lives in spec/support/model_coverage.rb: the same
|
|
14
|
+
# data drives this audit, the model document's pin lists, and the
|
|
15
|
+
# published coverage page. See docs/_references/processing-model.adoc
|
|
16
|
+
# for the model itself.
|
|
12
17
|
RSpec.describe "model coverage matrix" do
|
|
13
|
-
MODEL_MATRIX =
|
|
14
|
-
"document order and children listing" => {
|
|
15
|
-
section: "Document order and traversal",
|
|
16
|
-
pin: "spec/consistency/adapter_parity_spec.rb",
|
|
17
|
-
},
|
|
18
|
-
"name resolution" => {
|
|
19
|
-
section: "Names and namespaces",
|
|
20
|
-
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
21
|
-
},
|
|
22
|
-
"namespace resolution and bindings normalization" => {
|
|
23
|
-
section: "Names and namespaces",
|
|
24
|
-
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
25
|
-
},
|
|
26
|
-
"attribute access" => {
|
|
27
|
-
section: "Access semantics",
|
|
28
|
-
pin: "spec/moxml/attribute_resolver_spec.rb",
|
|
29
|
-
},
|
|
30
|
-
"text/content/raw_content" => {
|
|
31
|
-
section: "Access semantics",
|
|
32
|
-
pin: "spec/moxml/text_spec.rb",
|
|
33
|
-
},
|
|
34
|
-
"parsing modes and diagnostics" => {
|
|
35
|
-
section: "Parsing",
|
|
36
|
-
pin: "spec/moxml/document_parse_diagnostics_spec.rb",
|
|
37
|
-
},
|
|
38
|
-
"EOL normalization" => {
|
|
39
|
-
section: "Parsing",
|
|
40
|
-
pin: "spec/consistency/eol_normalization_spec.rb",
|
|
41
|
-
exempt: { oga: "D-02", ox: "D-03" },
|
|
42
|
-
},
|
|
43
|
-
"entity modes" => {
|
|
44
|
-
section: "Parsing",
|
|
45
|
-
pin: "spec/moxml/entity_spec.rb",
|
|
46
|
-
},
|
|
47
|
-
"XPath evaluation and declared idioms" => {
|
|
48
|
-
section: "XPath evaluation",
|
|
49
|
-
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
50
|
-
exempt: { oga: "D-01" },
|
|
51
|
-
},
|
|
52
|
-
"serialization shapes (xml/html/xhtml)" => {
|
|
53
|
-
section: "Serialization",
|
|
54
|
-
pin: "spec/moxml/xhtml_output_spec.rb",
|
|
55
|
-
},
|
|
56
|
-
"indentation knobs" => {
|
|
57
|
-
section: "Serialization",
|
|
58
|
-
pin: "spec/moxml/to_html_spec.rb",
|
|
59
|
-
exempt: { oga: "D-09", ox: "D-09", headed_ox: "D-09",
|
|
60
|
-
libxml: "D-09", leptris: "D-09" },
|
|
61
|
-
},
|
|
62
|
-
"canonicalization" => {
|
|
63
|
-
section: "Canonicalization",
|
|
64
|
-
pin: "spec/moxml/c14n/adapter_delegation_spec.rb",
|
|
65
|
-
exempt: { oga: "D-04", ox: "D-05", headed_ox: "D-05" },
|
|
66
|
-
},
|
|
67
|
-
"XSLT" => {
|
|
68
|
-
section: "XSLT",
|
|
69
|
-
pin: "spec/consistency/xslt_spec.rb",
|
|
70
|
-
loud: { oga: "Moxml::NotImplementedError",
|
|
71
|
-
ox: "Moxml::NotImplementedError",
|
|
72
|
-
headed_ox: "Moxml::NotImplementedError",
|
|
73
|
-
rexml: "Moxml::NotImplementedError",
|
|
74
|
-
libxml: "Moxml::NotImplementedError" },
|
|
75
|
-
},
|
|
76
|
-
"SAX" => {
|
|
77
|
-
section: "SAX",
|
|
78
|
-
pin: "spec/moxml/sax_spec.rb",
|
|
79
|
-
},
|
|
80
|
-
"materialization" => {
|
|
81
|
-
section: "Materialization",
|
|
82
|
-
pin: "spec/moxml/plan_conformance_spec.rb",
|
|
83
|
-
},
|
|
84
|
-
"loud capability contracts" => {
|
|
85
|
-
section: "Errors and loud degradation",
|
|
86
|
-
pin: "spec/consistency/loud_degradation_spec.rb",
|
|
87
|
-
},
|
|
88
|
-
}.freeze
|
|
18
|
+
MODEL_MATRIX = ModelCoverage::MODEL_COVERAGE_MATRIX # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
89
19
|
|
|
90
20
|
def register_ids
|
|
91
21
|
File.readlines("docs/_pages/divergences.adoc")
|
|
@@ -134,12 +64,7 @@ RSpec.describe "model coverage matrix" do
|
|
|
134
64
|
# section vanished fails too.
|
|
135
65
|
MODEL_DOC = "docs/_references/processing-model.adoc" # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
136
66
|
|
|
137
|
-
|
|
138
|
-
"Status and precedence",
|
|
139
|
-
"Mission and invariant",
|
|
140
|
-
"Object model",
|
|
141
|
-
"Performance: a declared, non-semantic channel",
|
|
142
|
-
].freeze
|
|
67
|
+
NON_FACE = ModelCoverage::MODEL_COVERAGE_NON_FACE # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
143
68
|
|
|
144
69
|
def model_sections
|
|
145
70
|
File.readlines(MODEL_DOC).filter_map do |line|
|
|
@@ -147,6 +72,24 @@ RSpec.describe "model coverage matrix" do
|
|
|
147
72
|
end.uniq
|
|
148
73
|
end
|
|
149
74
|
|
|
75
|
+
# Document-side pin lists (TODO.sota/07): every section carries a
|
|
76
|
+
# `Pins::` line naming the specs that enforce it (`Pins:: none` for
|
|
77
|
+
# non-face sections). Parsed as section => [paths] or nil when the
|
|
78
|
+
# line is missing.
|
|
79
|
+
def model_pin_lists
|
|
80
|
+
lists = {}
|
|
81
|
+
current = nil
|
|
82
|
+
File.readlines(MODEL_DOC).each do |line|
|
|
83
|
+
if (heading = line[/\A=== (\S.*)$/, 1])
|
|
84
|
+
current = heading.strip
|
|
85
|
+
lists[current] = nil
|
|
86
|
+
elsif current && (entry = line[/\APins:: (.*)$/, 1])
|
|
87
|
+
lists[current] = entry.scan(/`([^`]+)`/).flatten
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
lists
|
|
91
|
+
end
|
|
92
|
+
|
|
150
93
|
it "cites an existing model section for every face family" do
|
|
151
94
|
sections = model_sections
|
|
152
95
|
unknown = MODEL_MATRIX.filter_map do |family, cell|
|
|
@@ -162,7 +105,7 @@ RSpec.describe "model coverage matrix" do
|
|
|
162
105
|
it "covers or declares every model section" do
|
|
163
106
|
cited = MODEL_MATRIX.values.filter_map { |cell| cell[:section] }
|
|
164
107
|
uncovered = model_sections.reject do |section|
|
|
165
|
-
cited.include?(section) ||
|
|
108
|
+
cited.include?(section) || NON_FACE.include?(section)
|
|
166
109
|
end
|
|
167
110
|
expect(uncovered).to be_empty,
|
|
168
111
|
"model sections with no coverage matrix family " \
|
|
@@ -170,11 +113,43 @@ RSpec.describe "model coverage matrix" do
|
|
|
170
113
|
end
|
|
171
114
|
|
|
172
115
|
it "declares non-face sections that actually exist in the model doc" do
|
|
173
|
-
stale =
|
|
116
|
+
stale = NON_FACE.reject { |s| model_sections.include?(s) }
|
|
174
117
|
expect(stale).to be_empty,
|
|
175
118
|
"non-face declarations citing missing sections: #{stale.join('; ')}"
|
|
176
119
|
end
|
|
177
120
|
|
|
121
|
+
it "gives every model section a Pins line" do
|
|
122
|
+
missing = model_pin_lists.select { |_, pins| pins.nil? }.keys
|
|
123
|
+
expect(missing).to be_empty,
|
|
124
|
+
"model sections without a Pins line: #{missing.join('; ')}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "matches the model doc's face-section pin lists to the matrix" do
|
|
128
|
+
lists = model_pin_lists
|
|
129
|
+
drift = ModelCoverage.sections.filter_map do |section|
|
|
130
|
+
expected = ModelCoverage.pins_for(section)
|
|
131
|
+
got = lists[section]
|
|
132
|
+
"#{section}: doc #{got.inspect} vs matrix #{expected.inspect}" if got != expected
|
|
133
|
+
end
|
|
134
|
+
expect(drift).to be_empty,
|
|
135
|
+
"model-doc pin lists drift from the matrix: #{drift.join('; ')}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "leaves non-face sections without pins" do
|
|
139
|
+
lists = model_pin_lists
|
|
140
|
+
pinned = NON_FACE.select { |section| lists[section]&.any? }
|
|
141
|
+
expect(pinned).to be_empty,
|
|
142
|
+
"non-face sections must declare Pins:: none: #{pinned.join('; ')}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "points every model-doc pin at an existing spec" do
|
|
146
|
+
missing = model_pin_lists.values.compact.flatten.uniq.reject do |path|
|
|
147
|
+
File.exist?(path)
|
|
148
|
+
end
|
|
149
|
+
expect(missing).to be_empty,
|
|
150
|
+
"model-doc pins missing on disk: #{missing.join(', ')}"
|
|
151
|
+
end
|
|
152
|
+
|
|
178
153
|
# Bidirectional linkage (TODO.sota/03): the model doc, the pins, and
|
|
179
154
|
# the reference form one checkable triangle. Every pin cites the
|
|
180
155
|
# section it enforces; every citation names a section that exists.
|
|
@@ -213,7 +188,7 @@ RSpec.describe "model coverage matrix" do
|
|
|
213
188
|
File.exist?(path) ? pin_citations(path) : []
|
|
214
189
|
end
|
|
215
190
|
uncovered = model_sections.reject do |section|
|
|
216
|
-
cited.include?(section) ||
|
|
191
|
+
cited.include?(section) || NON_FACE.include?(section)
|
|
217
192
|
end
|
|
218
193
|
expect(uncovered).to be_empty,
|
|
219
194
|
"model sections no pin cites: #{uncovered.join('; ')}"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/model_coverage"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
# Published documentation is part of the specification apparatus
|
|
8
|
+
# (TODO.sota/07): generated blocks must be views of committed data. A
|
|
9
|
+
# data change without regeneration fails here, so the published
|
|
10
|
+
# coverage proof and scoreboard can never claim more than the tests do.
|
|
11
|
+
RSpec.describe "published documentation" do
|
|
12
|
+
it "keeps the model-coverage page in sync with the matrix" do
|
|
13
|
+
page = File.read("docs/_pages/model-coverage.adoc")
|
|
14
|
+
block = page[/MODEL-COVERAGE:BEGIN\n(.*?)\n?\/\/ MODEL-COVERAGE:END/m, 1]
|
|
15
|
+
expect(block).not_to be_nil, "coverage page is missing its markers"
|
|
16
|
+
|
|
17
|
+
expect(block).to eq(ModelCoverage.render_page_block),
|
|
18
|
+
"model-coverage page is stale: run rake docs:coverage"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "keeps every published scoreboard row in sync with the JSON data" do
|
|
22
|
+
data = JSON.parse(File.read("benchmark/scoreboard.json"))
|
|
23
|
+
page = File.read("docs/_pages/performance.adoc")
|
|
24
|
+
block = page[/SCOREBOARD:BEGIN(.*?)SCOREBOARD:END/m, 1]
|
|
25
|
+
expect(block).not_to be_nil, "performance page is missing its markers"
|
|
26
|
+
|
|
27
|
+
data["rows"].each do |row|
|
|
28
|
+
expect(block).to include(
|
|
29
|
+
"| `#{row['row']}` | #{format('%.2f', row['ratio'])}x | " \
|
|
30
|
+
"#{row['kind']} |",
|
|
31
|
+
), "scoreboard row #{row['row']} is stale against scoreboard.json"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
expect(block.scan(/\| `[^`]+` \| \d+\.\d\dx \|/).size)
|
|
35
|
+
.to eq(data["rows"].size), "scoreboard row count differs"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "names the measured engine version in the scoreboard metadata" do
|
|
39
|
+
data = JSON.parse(File.read("benchmark/scoreboard.json"))
|
|
40
|
+
protocol = data["protocol"]
|
|
41
|
+
page = File.read("docs/_pages/performance.adoc")
|
|
42
|
+
block = page[/SCOREBOARD:BEGIN(.*?)SCOREBOARD:END/m, 1]
|
|
43
|
+
|
|
44
|
+
expect(block).to include("#{protocol['adapter']} " \
|
|
45
|
+
"#{protocol['adapter_version']}"),
|
|
46
|
+
"scoreboard metadata does not name the measured engine"
|
|
47
|
+
expect(block).to include("nokogiri #{protocol['nokogiri']}")
|
|
48
|
+
end
|
|
49
|
+
end
|