Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/Schematron.Tests/CompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;

namespace Schematron.Tests;

/// <summary>
/// Compatibility tests: verifies that Schematron.NET produces the same pass/fail result
/// as the ISO XSLT 1.0 reference implementation for a set of (schema, xml) pairs.
///
/// Comparison is limited to whether validation passes or fails (HasErrors). Message text
/// is intentionally not compared because the two implementations format messages differently.
///
/// ISO Schematron 2025 features that have no XSLT 1.0 counterpart (group, @visit-each,
/// @when on phase) are NOT covered here — they are tested in ValidatorTests.cs.
/// </summary>
public class CompatibilityTests
{
/// <summary>
/// Test cases: (label, schemaPath, xmlFile, expectErrors).
/// xmlFile is relative to the test output directory.
/// </summary>
public static IEnumerable<object[]> Cases()
{
// ── basic-assert ──────────────────────────────────────────────────────
yield return Case("BasicAssert/valid", "Content/compat/basic-assert.sch", "Content/compat/basic-assert-valid.xml", false);
yield return Case("BasicAssert/invalid", "Content/compat/basic-assert.sch", "Content/compat/basic-assert-invalid.xml", true);

// ── basic-report ─────────────────────────────────────────────────────
yield return Case("BasicReport/no-trigger", "Content/compat/basic-report.sch", "Content/compat/basic-report-valid.xml", false);
yield return Case("BasicReport/trigger", "Content/compat/basic-report.sch", "Content/compat/basic-report-trigger.xml", true);

// ── first-match (rule node-first-match within a pattern) ──────────────
yield return Case("FirstMatch/valid", "Content/compat/first-match.sch", "Content/compat/first-match-valid.xml", false);
yield return Case("FirstMatch/invalid", "Content/compat/first-match.sch", "Content/compat/first-match-invalid.xml", true);

// ── multi-pattern ────────────────────────────────────────────────────
yield return Case("MultiPattern/valid", "Content/compat/multi-pattern.sch", "Content/compat/multi-pattern-valid.xml", false);
yield return Case("MultiPattern/invalid", "Content/compat/multi-pattern.sch", "Content/compat/multi-pattern-invalid.xml", true);

// ── let variables ────────────────────────────────────────────────────
yield return Case("LetScopes/valid", "Content/compat/let-scopes.sch", "Content/compat/let-scopes-valid.xml", false);
yield return Case("LetScopes/invalid", "Content/compat/let-scopes.sch", "Content/compat/let-scopes-invalid.xml", true);

// ── abstract rules via <extends> ─────────────────────────────────────
yield return Case("AbstractRule/valid", "Content/compat/abstract-rule.sch", "Content/compat/abstract-rule-valid.xml", false);
yield return Case("AbstractRule/invalid", "Content/compat/abstract-rule.sch", "Content/compat/abstract-rule-invalid.xml", true);

// ── abstract patterns (is-a) — reuse existing fixture ────────────────
yield return Case("AbstractPattern/valid", "Content/abstract-pattern-schema.sch", "Content/compat/abstract-pattern-valid.xml", false);
yield return Case("AbstractPattern/invalid", "Content/abstract-pattern-schema.sch", "Content/compat/abstract-pattern-invalid.xml", true);

// ── phases ───────────────────────────────────────────────────────────
yield return Case("Phases/basic-valid", "Content/compat/phases.sch", "Content/compat/phases-basic-valid.xml", false);
yield return Case("Phases/basic-invalid", "Content/compat/phases.sch", "Content/compat/phases-basic-invalid.xml", true);

// ── namespace prefixes ───────────────────────────────────────────────
yield return Case("Namespaces/valid", "Content/compat/namespaces.sch", "Content/compat/namespaces-valid.xml", false);
yield return Case("Namespaces/invalid", "Content/compat/namespaces.sch", "Content/compat/namespaces-invalid.xml", true);

// ── value-of in message ──────────────────────────────────────────────
yield return Case("ValueOf/valid", "Content/compat/value-of.sch", "Content/compat/value-of-valid.xml", false);
yield return Case("ValueOf/invalid", "Content/compat/value-of.sch", "Content/compat/value-of-invalid.xml", true);
}

static object[] Case(string label, string schemaPath, string xmlPath, bool expectErrors)
=> [label, schemaPath, xmlPath, expectErrors];

[Theory]
[MemberData(nameof(Cases))]
public void BothImplementationsAgreeOnPassFail(string label, string schemaPath, string xmlPath, bool expectErrors)
{
string schemaFullPath = ResolvePath(schemaPath);
string xmlContent = File.ReadAllText(ResolvePath(xmlPath));

// ── Schematron.NET ───────────────────────────────────────────────────
bool netErrors = false;
var validator = new Validator();
validator.AddSchema(System.Xml.XmlReader.Create(schemaFullPath));
try
{
validator.Validate(new StringReader(xmlContent));
}
catch (ValidationException)
{
netErrors = true;
}

// ── Reference XSLT implementation ────────────────────────────────────
SvrlResult svrl = ReferenceSchematronRunner.Validate(schemaFullPath, xmlContent);

// ── Assertions ───────────────────────────────────────────────────────
Xunit.Assert.Equal(expectErrors, netErrors);
Xunit.Assert.Equal(expectErrors, svrl.HasErrors);
Xunit.Assert.Equal(netErrors, svrl.HasErrors);
_ = label; // used only for test display name
}

// -------------------------------------------------------------------------
// Abstract-pattern fixtures are inlined here (no separate file needed for
// the reused schema) but we do need the XML files.
// -------------------------------------------------------------------------

static string ResolvePath(string relative)
{
// When tests run, the working directory is the test output directory.
if (File.Exists(relative)) return relative;
// Fallback: relative to Content/ for schemas shared with ValidatorTests.
string alt = Path.Combine(".", relative);
return alt;
}
}
23 changes: 23 additions & 0 deletions src/Schematron.Tests/Content/abstract-pattern-schema.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<title>Abstract pattern test schema</title>

<!-- Abstract pattern template: $elementName and $minCount are placeholders -->
<pattern abstract="true" id="required-child">
<rule context="$parentElement">
<assert test="count($childElement) &gt;= 1">A <name/> must have at least one <value-of select="'$childElement'"/>.</assert>
</rule>
</pattern>

<!-- Concrete instantiation 1: order must have at least one item -->
<pattern id="order-items" is-a="required-child">
<param name="parentElement" value="order"/>
<param name="childElement" value="item"/>
</pattern>

<!-- Concrete instantiation 2: invoice must have at least one line -->
<pattern id="invoice-lines" is-a="required-child">
<param name="parentElement" value="invoice"/>
<param name="childElement" value="line"/>
</pattern>
</schema>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- order has no items: violates the abstract pattern instantiation -->
<root><order/><invoice><line/></invoice></root>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<root><order><item/></order><invoice><line/></invoice></root>
5 changes: 5 additions & 0 deletions src/Schematron.Tests/Content/compat/abstract-rule-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<root>
<!-- customer missing email and id -->
<customer name="Bob"/>
<supplier id="s1" name="Acme"/>
</root>
4 changes: 4 additions & 0 deletions src/Schematron.Tests/Content/compat/abstract-rule-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<root>
<customer id="c1" name="Alice" email="alice@example.com"/>
<supplier id="s1" name="Acme"/>
</root>
19 changes: 19 additions & 0 deletions src/Schematron.Tests/Content/compat/abstract-rule.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Abstract rule via <extends rule="..."/> -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Abstract Rule Extends</sch:title>
<!-- Abstract rule: common checks for any named entity -->
<sch:rule abstract="true" id="entity-checks">
<sch:assert test="@id">An entity must have an id.</sch:assert>
<sch:assert test="@name">An entity must have a name.</sch:assert>
</sch:rule>
<sch:pattern>
<sch:rule context="customer">
<sch:extends rule="entity-checks"/>
<sch:assert test="@email">A customer must have an email.</sch:assert>
</sch:rule>
<sch:rule context="supplier">
<sch:extends rule="entity-checks"/>
</sch:rule>
</sch:pattern>
</sch:schema>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<person age="30"/>
1 change: 1 addition & 0 deletions src/Schematron.Tests/Content/compat/basic-assert-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<person name="Alice" age="30"/>
10 changes: 10 additions & 0 deletions src/Schematron.Tests/Content/compat/basic-assert.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Basic Assert</sch:title>
<sch:pattern>
<sch:rule context="person">
<sch:assert test="@name">A person must have a name attribute.</sch:assert>
<sch:assert test="@age > 0">Age must be positive.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<order status="cancelled"/>
1 change: 1 addition & 0 deletions src/Schematron.Tests/Content/compat/basic-report-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<order status="active"/>
9 changes: 9 additions & 0 deletions src/Schematron.Tests/Content/compat/basic-report.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Basic Report</sch:title>
<sch:pattern>
<sch:rule context="order">
<sch:report test="@status = 'cancelled'">Order is cancelled.</sch:report>
</sch:rule>
</sch:pattern>
</sch:schema>
4 changes: 4 additions & 0 deletions src/Schematron.Tests/Content/compat/first-match-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- special item with price=50: violates first rule (>100), but second rule must NOT also fire -->
<root>
<item type="special" price="50"/>
</root>
5 changes: 5 additions & 0 deletions src/Schematron.Tests/Content/compat/first-match-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- special item with price=150: passes first rule; ordinary item passes second rule -->
<root>
<item type="special" price="150"/>
<item price="5"/>
</root>
15 changes: 15 additions & 0 deletions src/Schematron.Tests/Content/compat/first-match.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Tests that within a pattern, each node is matched by only the first matching rule. -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>First Match</sch:title>
<sch:pattern>
<!-- More specific rule first -->
<sch:rule context="item[@type='special']">
<sch:assert test="@price > 100">Special item price must be over 100.</sch:assert>
</sch:rule>
<!-- General rule — should NOT fire for nodes already matched above -->
<sch:rule context="item">
<sch:assert test="@price > 0">Item price must be positive.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
1 change: 1 addition & 0 deletions src/Schematron.Tests/Content/compat/let-scopes-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<score value="200"/>
1 change: 1 addition & 0 deletions src/Schematron.Tests/Content/compat/let-scopes-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<score value="50"/>
14 changes: 14 additions & 0 deletions src/Schematron.Tests/Content/compat/let-scopes.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Let variables at schema, pattern, and rule scopes. -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Let Scopes</sch:title>
<sch:let name="maxScore" value="'100'"/>
<sch:pattern>
<sch:let name="minScore" value="'0'"/>
<sch:rule context="score">
<sch:let name="val" value="@value"/>
<sch:assert test="$val >= $minScore">Score must be at least <sch:value-of select="$minScore"/>.</sch:assert>
<sch:assert test="$val &lt;= $maxScore">Score must be at most <sch:value-of select="$maxScore"/>.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
2 changes: 2 additions & 0 deletions src/Schematron.Tests/Content/compat/multi-pattern-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- missing both id and price: both patterns fire -->
<catalog><product/></catalog>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<catalog><product id="p1" price="9.99"/></catalog>
15 changes: 15 additions & 0 deletions src/Schematron.Tests/Content/compat/multi-pattern.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Multiple patterns: each pattern evaluates all nodes independently (no cross-pattern first-match). -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Multi Pattern</sch:title>
<sch:pattern id="p1">
<sch:rule context="product">
<sch:assert test="@id">A product must have an id.</sch:assert>
</sch:rule>
</sch:pattern>
<sch:pattern id="p2">
<sch:rule context="product">
<sch:assert test="@price">A product must have a price.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
2 changes: 2 additions & 0 deletions src/Schematron.Tests/Content/compat/namespaces-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- missing id and no items -->
<po:order xmlns:po="http://example.com/po"/>
3 changes: 3 additions & 0 deletions src/Schematron.Tests/Content/compat/namespaces-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<po:order xmlns:po="http://example.com/po" id="o1">
<po:item qty="2"/>
</po:order>
15 changes: 15 additions & 0 deletions src/Schematron.Tests/Content/compat/namespaces.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Namespace prefix usage in rule context and test expressions. -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Namespace Prefixes</sch:title>
<sch:ns prefix="po" uri="http://example.com/po"/>
<sch:pattern>
<sch:rule context="po:order">
<sch:assert test="@id">An order must have an id.</sch:assert>
<sch:assert test="po:item">An order must have at least one item.</sch:assert>
</sch:rule>
<sch:rule context="po:item">
<sch:assert test="@qty > 0">Item quantity must be positive.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
2 changes: 2 additions & 0 deletions src/Schematron.Tests/Content/compat/phases-basic-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- no title: fails "basic" and "full" phases -->
<document author="Alice"/>
2 changes: 2 additions & 0 deletions src/Schematron.Tests/Content/compat/phases-basic-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- has title but no author: passes "basic" phase, fails "full" phase -->
<document title="My Doc"/>
22 changes: 22 additions & 0 deletions src/Schematron.Tests/Content/compat/phases.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Phases: pattern p1 = basic checks; pattern p2 = extended checks. -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" defaultPhase="basic">
<sch:title>Phases</sch:title>
<sch:phase id="basic">
<sch:active pattern="p1"/>
</sch:phase>
<sch:phase id="full">
<sch:active pattern="p1"/>
<sch:active pattern="p2"/>
</sch:phase>
<sch:pattern id="p1">
<sch:rule context="document">
<sch:assert test="@title">Document must have a title.</sch:assert>
</sch:rule>
</sch:pattern>
<sch:pattern id="p2">
<sch:rule context="document">
<sch:assert test="@author">Document must have an author.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
1 change: 1 addition & 0 deletions src/Schematron.Tests/Content/compat/value-of-invalid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<product name="Widget" price="-5"/>
1 change: 1 addition & 0 deletions src/Schematron.Tests/Content/compat/value-of-valid.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<product name="Widget" price="9.99"/>
10 changes: 10 additions & 0 deletions src/Schematron.Tests/Content/compat/value-of.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- value-of in message: verifies expansion happens and validation fails. -->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>Value-of in Message</sch:title>
<sch:pattern>
<sch:rule context="product">
<sch:assert test="@price > 0">Product <sch:value-of select="@name"/> has invalid price: <sch:value-of select="@price"/>.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
16 changes: 16 additions & 0 deletions src/Schematron.Tests/Content/diagnostics-schema.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<title>Diagnostics test schema</title>

<diagnostics>
<diagnostic id="d-name-required">The 'name' attribute is required on the 'person' element.</diagnostic>
<diagnostic id="d-age-range">Age must be between 0 and 120.</diagnostic>
</diagnostics>

<pattern>
<rule context="person">
<assert test="@name" diagnostics="d-name-required">person must have a name.</assert>
<assert test="number(@age) &gt;= 0 and number(@age) &lt;= 120" diagnostics="d-age-range">Age out of range.</assert>
</rule>
</pattern>
</schema>
12 changes: 12 additions & 0 deletions src/Schematron.Tests/Content/group-schema.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron" schematronEdition="2025">
<title>Group Test Schema</title>
<group>
<rule context="item">
<assert test="@id">Item must have an id.</assert>
</rule>
<rule context="item">
<assert test="@name">Item must have a name.</assert>
</rule>
</group>
</schema>
15 changes: 15 additions & 0 deletions src/Schematron.Tests/Content/iso-schema.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" schematronEdition="2025">
<sch:title>ISO Namespace Test Schema</sch:title>
<sch:ns prefix="ex" uri="http://example.com/test"/>

<sch:pattern>
<sch:rule context="//order">
<sch:assert test="@id">An order must have an id attribute.</sch:assert>
<sch:report test="@status = 'cancelled'">Order <sch:value-of select="@id"/> is cancelled.</sch:report>
</sch:rule>
<sch:rule context="//item">
<sch:assert test="@price > 0">Item price must be positive.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
10 changes: 10 additions & 0 deletions src/Schematron.Tests/Content/legacy-schema.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://www.ascc.net/xml/schematron">
<sch:title>Legacy Namespace Test Schema</sch:title>

<sch:pattern name="Order checks">
<sch:rule context="//order">
<sch:assert test="@id">An order must have an id attribute.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
Loading
Loading