From 05f525b14730f8d1f84eee3163ef91240fe7d7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 16:14:24 +0200 Subject: [PATCH 01/13] Add reproduction for uniqueness regression --- .../should-fail/duplicate-GroupOfLinkSequences.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 examples/should-fail/duplicate-GroupOfLinkSequences.xml diff --git a/examples/should-fail/duplicate-GroupOfLinkSequences.xml b/examples/should-fail/duplicate-GroupOfLinkSequences.xml new file mode 100644 index 000000000..40bb4cb59 --- /dev/null +++ b/examples/should-fail/duplicate-GroupOfLinkSequences.xml @@ -0,0 +1,13 @@ + + + 2020-01-01T12:00:00Z + TEST + + + + + + + + + From 23a70f15a18381b9797d8eb5f6e05bc6f15066d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 16:14:35 +0200 Subject: [PATCH 02/13] Add quick readme on should-fail folder --- examples/should-fail/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 examples/should-fail/README.md diff --git a/examples/should-fail/README.md b/examples/should-fail/README.md new file mode 100644 index 000000000..35110f047 --- /dev/null +++ b/examples/should-fail/README.md @@ -0,0 +1,7 @@ +# Negative examples (must-fail) + +Each file carries one deliberate defect and must therefore be **rejected** by the +schema for a specific, expected reason. The cases — file, schema and expected +constraint — are declared in `.github/scripts/validate-should-fail.sh` (run in CI). + +Run locally: `XMLLINT_BIN=$(which xmllint) ./.github/scripts/validate-should-fail.sh` From 4ae5c3b794e7d90dddbf146715c80b161db70041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 16:29:16 +0200 Subject: [PATCH 03/13] Create minimalistic "should fail" harness on CI --- .github/scripts/validate-should-fail.sh | 28 +++++++++++++++++++++++++ .github/workflows/ci.yml | 3 +++ 2 files changed, 31 insertions(+) create mode 100755 .github/scripts/validate-should-fail.sh diff --git a/.github/scripts/validate-should-fail.sh b/.github/scripts/validate-should-fail.sh new file mode 100755 index 000000000..c76ff92a4 --- /dev/null +++ b/.github/scripts/validate-should-fail.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Negative examples: documents that MUST be rejected by the schema. +# Each case greps the EXACT expected constraint name, so dropping that constraint +# (document wrongly accepted) fails the build and can't be masked by another error. + +set -u +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$( cd -- "${SCRIPT_DIR}/../.." &> /dev/null && pwd ) +# CI uses the vendored 2025 Linux x86-64 xmllint ("Temporary xmllint master", +# https://github.com/TransmodelEcosystem/NeTEx/pull/915); set XMLLINT_BIN to a local +# xmllint to run this on any other OS or CPU architecture (macOS, Windows, ARM, ...). +XMLLINT="${XMLLINT_BIN:-${SCRIPT_DIR}/xmllint}" +cd "${ROOT_DIR}" + +fail=0 +assert_rejected() { # + if "${XMLLINT}" --noout --schema "$2" "$1" 2>&1 | grep -q "$3"; then + echo "OK $1" + else + echo "SHOULD HAVE FAILED $1 — not rejected by '$3' (constraint missing?)" + fail=1 + fi +} + +echo "Checking NeTEx 'should-fail' negative examples ..." +assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd GroupOfLinkSequences_UniqueBy_Id_Version + +exit "${fail}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ec17fa42..f2faaad6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: - name: Validate NeTEx XML examples run: ./.github/scripts/validate-examples.sh + - name: Validate negative examples (must-fail) + run: ./.github/scripts/validate-should-fail.sh + - name: Commit changes uses: EndBug/add-and-commit@v9 # https://github.com/marketplace/actions/add-commit with: From a2bc91fa38f918d945e39957122c68010fd8749c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 16:47:44 +0200 Subject: [PATCH 04/13] Restore GroupOfLinkSequences uniqueness (revert of 51b79ea for this constraint) --- xsd/NeTEx_publication.xsd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xsd/NeTEx_publication.xsd b/xsd/NeTEx_publication.xsd index d39608582..5e9bca54d 100644 --- a/xsd/NeTEx_publication.xsd +++ b/xsd/NeTEx_publication.xsd @@ -979,6 +979,15 @@ + + + + Every [GroupOfLinkSequences Id + Version] must be unique within document. + + + + + From 6b77671eae15d6639c39ebc2dc83c781b6570b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 18:12:23 +0200 Subject: [PATCH 05/13] Make "should-fail" checks assert behaviour, not the constraint name Also, assert that the file cannot be considered valid, first and foremost. --- .github/scripts/validate-should-fail.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/scripts/validate-should-fail.sh b/.github/scripts/validate-should-fail.sh index c76ff92a4..4b09e7af3 100755 --- a/.github/scripts/validate-should-fail.sh +++ b/.github/scripts/validate-should-fail.sh @@ -13,16 +13,25 @@ XMLLINT="${XMLLINT_BIN:-${SCRIPT_DIR}/xmllint}" cd "${ROOT_DIR}" fail=0 -assert_rejected() { # - if "${XMLLINT}" --noout --schema "$2" "$1" 2>&1 | grep -q "$3"; then - echo "OK $1" +# Behaviour assertion, parameterised by the caller: the document must be REJECTED, and +# the rejection output must contain . We test the outcome, not how it is +# enforced - passing "Duplicate key-sequence" accepts a unique, a key or a rename alike, +# as long as the duplicate is caught. Each future test declares its own . +# Fails closed: accepted, wrong reason, or xmllint not running all count as failures. +assert_rejected() { # + out=$("${XMLLINT}" --noout --schema "$2" "$1" 2>&1); st=$? + if [ "${st}" -eq 0 ]; then + echo "SHOULD HAVE FAILED $1 — accepted (must be rejected)" + fail=1 + elif printf '%s\n' "${out}" | grep -q "$3"; then + echo "OK $1" else - echo "SHOULD HAVE FAILED $1 — not rejected by '$3' (constraint missing?)" + echo "ERROR $1 — rejected, but not matching '$3' (unrelated error / xmllint failure?)" fail=1 fi } echo "Checking NeTEx 'should-fail' negative examples ..." -assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd GroupOfLinkSequences_UniqueBy_Id_Version +assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" exit "${fail}" From 4d8f7d08b0f6132ca4e1820b108c4215c57aa037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 18:14:04 +0200 Subject: [PATCH 06/13] Add failing example for duplicate "OperatingDay by CalendarDate" --- .github/scripts/validate-should-fail.sh | 1 + examples/should-fail/duplicate-CalendarDate.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 examples/should-fail/duplicate-CalendarDate.xml diff --git a/.github/scripts/validate-should-fail.sh b/.github/scripts/validate-should-fail.sh index 4b09e7af3..af21f1355 100755 --- a/.github/scripts/validate-should-fail.sh +++ b/.github/scripts/validate-should-fail.sh @@ -33,5 +33,6 @@ assert_rejected() { # echo "Checking NeTEx 'should-fail' negative examples ..." assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" +assert_rejected examples/should-fail/duplicate-CalendarDate.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" exit "${fail}" diff --git a/examples/should-fail/duplicate-CalendarDate.xml b/examples/should-fail/duplicate-CalendarDate.xml new file mode 100644 index 000000000..e35c41790 --- /dev/null +++ b/examples/should-fail/duplicate-CalendarDate.xml @@ -0,0 +1,13 @@ + + + 2020-01-01T12:00:00Z + TEST + + + + 2020-01-01 + 2020-01-01 + + + + From 7c8f51640574ceba5ead3dc77650a2dae42aaca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 18:22:01 +0200 Subject: [PATCH 07/13] We do not want duplicate ValidBetween (same id + version) --- .github/scripts/validate-should-fail.sh | 1 + examples/should-fail/duplicate-ValidBetween.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 examples/should-fail/duplicate-ValidBetween.xml diff --git a/.github/scripts/validate-should-fail.sh b/.github/scripts/validate-should-fail.sh index af21f1355..6dee5e3d6 100755 --- a/.github/scripts/validate-should-fail.sh +++ b/.github/scripts/validate-should-fail.sh @@ -34,5 +34,6 @@ assert_rejected() { # echo "Checking NeTEx 'should-fail' negative examples ..." assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" assert_rejected examples/should-fail/duplicate-CalendarDate.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" +assert_rejected examples/should-fail/duplicate-ValidBetween.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" exit "${fail}" diff --git a/examples/should-fail/duplicate-ValidBetween.xml b/examples/should-fail/duplicate-ValidBetween.xml new file mode 100644 index 000000000..2620b1b36 --- /dev/null +++ b/examples/should-fail/duplicate-ValidBetween.xml @@ -0,0 +1,13 @@ + + + 2020-01-01T12:00:00Z + TEST + + + 2020-01-01T00:00:00Z + + + 2020-01-01T00:00:00Z + + + From 0884659be0e9bcc49a559ae2173cceebed49b4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 18:27:39 +0200 Subject: [PATCH 08/13] Add test-case for duplicate ValidityPeriod --- .github/scripts/validate-should-fail.sh | 1 + .../should-fail/duplicate-ValidityPeriod.xml | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 examples/should-fail/duplicate-ValidityPeriod.xml diff --git a/.github/scripts/validate-should-fail.sh b/.github/scripts/validate-should-fail.sh index 6dee5e3d6..bec21881f 100755 --- a/.github/scripts/validate-should-fail.sh +++ b/.github/scripts/validate-should-fail.sh @@ -35,5 +35,6 @@ echo "Checking NeTEx 'should-fail' negative examples ..." assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" assert_rejected examples/should-fail/duplicate-CalendarDate.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" assert_rejected examples/should-fail/duplicate-ValidBetween.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" +assert_rejected examples/should-fail/duplicate-ValidityPeriod.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" exit "${fail}" diff --git a/examples/should-fail/duplicate-ValidityPeriod.xml b/examples/should-fail/duplicate-ValidityPeriod.xml new file mode 100644 index 000000000..e1924a15c --- /dev/null +++ b/examples/should-fail/duplicate-ValidityPeriod.xml @@ -0,0 +1,19 @@ + + + 2020-01-01T12:00:00Z + TEST + + + + + A + 2020-01-01T00:00:00Z + + + B + 2020-01-01T00:00:00Z + + + + + From 6819d84eef12c6d5c9080aa8e385d45dd07166a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 18:55:10 +0200 Subject: [PATCH 09/13] Restore OperatingDay CalendarDate uniqueness --- xsd/NeTEx_publication.xsd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xsd/NeTEx_publication.xsd b/xsd/NeTEx_publication.xsd index 5e9bca54d..b70a186b4 100644 --- a/xsd/NeTEx_publication.xsd +++ b/xsd/NeTEx_publication.xsd @@ -2200,6 +2200,15 @@ + + + + Each date is only allowed once per calendar. + + + + + From 139a64317a2db0c7e8d1416a8b3888cf4492b408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 19:04:26 +0200 Subject: [PATCH 10/13] Restore ValidBetween uniqueness ValidBetween lost (afaik) id+version uniqueness in 51b79ea and no xsd:key covers it (guarded by examples/should-fail/duplicate-ValidBetween.xml). Exact verbatim revert, byte-identical to v2.0. Its selector also covers ValidityCondition/ValidityTrigger/ValidityRuleParameter, which keys already cover - kept verbatim to stay an exact revert, not trimmed (but this can be covered by a later optimisation). --- xsd/NeTEx_publication.xsd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xsd/NeTEx_publication.xsd b/xsd/NeTEx_publication.xsd index b70a186b4..035b0cd8a 100644 --- a/xsd/NeTEx_publication.xsd +++ b/xsd/NeTEx_publication.xsd @@ -2257,6 +2257,15 @@ + + + + Every [ValidityCondition Id + Version] must be unique within document. + + + + + From 39c5883baf0806eca42809c96a88dffaf2ab16ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Sat, 20 Jun 2026 19:08:27 +0200 Subject: [PATCH 11/13] Restore ValidityPeriod uniqueness ValidityPeriod lost id+version uniqueness in 51b79ea and no xsd:key covers it (guarded by examples/should-fail/duplicate-ValidityPeriod.xml). Should be verbatim revert (compared to v2.0). Its selector also covers UsageValidityPeriod, which a key already covers - kept verbatim to stay an exact revert, not trimmed. --- xsd/NeTEx_publication.xsd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xsd/NeTEx_publication.xsd b/xsd/NeTEx_publication.xsd index 035b0cd8a..fe127fa5d 100644 --- a/xsd/NeTEx_publication.xsd +++ b/xsd/NeTEx_publication.xsd @@ -6515,6 +6515,15 @@ + + + + Every [UsageValidityPeriod Id + Version] must be unique within document. + + + + + From 70f4d9300935e570d7b925d671d95e266d52556d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:11:51 +0000 Subject: [PATCH 12/13] Lint and update documentation tables --- examples/should-fail/duplicate-CalendarDate.xml | 8 ++++++-- examples/should-fail/duplicate-ValidBetween.xml | 8 ++++++-- examples/should-fail/duplicate-ValidityPeriod.xml | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/should-fail/duplicate-CalendarDate.xml b/examples/should-fail/duplicate-CalendarDate.xml index e35c41790..ac82ae569 100644 --- a/examples/should-fail/duplicate-CalendarDate.xml +++ b/examples/should-fail/duplicate-CalendarDate.xml @@ -5,8 +5,12 @@ - 2020-01-01 - 2020-01-01 + + 2020-01-01 + + + 2020-01-01 + diff --git a/examples/should-fail/duplicate-ValidBetween.xml b/examples/should-fail/duplicate-ValidBetween.xml index 2620b1b36..44eb0c8f2 100644 --- a/examples/should-fail/duplicate-ValidBetween.xml +++ b/examples/should-fail/duplicate-ValidBetween.xml @@ -4,10 +4,14 @@ TEST - 2020-01-01T00:00:00Z + + 2020-01-01T00:00:00Z + - 2020-01-01T00:00:00Z + + 2020-01-01T00:00:00Z + diff --git a/examples/should-fail/duplicate-ValidityPeriod.xml b/examples/should-fail/duplicate-ValidityPeriod.xml index e1924a15c..e7f49a0d9 100644 --- a/examples/should-fail/duplicate-ValidityPeriod.xml +++ b/examples/should-fail/duplicate-ValidityPeriod.xml @@ -7,11 +7,15 @@ A - 2020-01-01T00:00:00Z + + 2020-01-01T00:00:00Z + B - 2020-01-01T00:00:00Z + + 2020-01-01T00:00:00Z + From 430e8af2396bc9d72589b6fceeeafea5fea05006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20Barr=C3=A8re?= Date: Mon, 22 Jun 2026 15:31:18 +0200 Subject: [PATCH 13/13] Batch verifications (much shorter time) like for regular validation script --- .github/scripts/validate-should-fail.sh | 61 ++++++++++++++----------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/.github/scripts/validate-should-fail.sh b/.github/scripts/validate-should-fail.sh index bec21881f..c66b282b3 100755 --- a/.github/scripts/validate-should-fail.sh +++ b/.github/scripts/validate-should-fail.sh @@ -1,40 +1,49 @@ #!/bin/bash # Negative examples: documents that MUST be rejected by the schema. -# Each case greps the EXACT expected constraint name, so dropping that constraint -# (document wrongly accepted) fails the build and can't be masked by another error. +# +# Each case is "|". We assert BEHAVIOUR (the +# document is rejected, and its output contains the declared substring) - not a +# specific constraint name, and each case carries its own clause. +# +# All cases share ONE schema and are validated in a SINGLE xmllint call (one +# compile for many files) to stay fast. Fails closed: accepted, wrong reason, or +# xmllint not running all count as failures. +# +# CI uses the vendored 2025 Linux x86-64 xmllint ("Temporary xmllint master", +# https://github.com/TransmodelEcosystem/NeTEx/pull/915); set XMLLINT_BIN to a local +# xmllint to run this on any other OS or CPU architecture (macOS, Windows, ARM, ...). set -u SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ROOT_DIR=$( cd -- "${SCRIPT_DIR}/../.." &> /dev/null && pwd ) -# CI uses the vendored 2025 Linux x86-64 xmllint ("Temporary xmllint master", -# https://github.com/TransmodelEcosystem/NeTEx/pull/915); set XMLLINT_BIN to a local -# xmllint to run this on any other OS or CPU architecture (macOS, Windows, ARM, ...). XMLLINT="${XMLLINT_BIN:-${SCRIPT_DIR}/xmllint}" cd "${ROOT_DIR}" +SCHEMA="xsd/NeTEx_publication.xsd" + +# "|" +CASES=( + "examples/should-fail/duplicate-GroupOfLinkSequences.xml|Duplicate key-sequence" + "examples/should-fail/duplicate-CalendarDate.xml|Duplicate key-sequence" + "examples/should-fail/duplicate-ValidBetween.xml|Duplicate key-sequence" + "examples/should-fail/duplicate-ValidityPeriod.xml|Duplicate key-sequence" +) + +files=() +for c in "${CASES[@]}"; do files+=("${c%%|*}"); done +out=$("${XMLLINT}" --noout --schema "${SCHEMA}" "${files[@]}" 2>&1) + fail=0 -# Behaviour assertion, parameterised by the caller: the document must be REJECTED, and -# the rejection output must contain . We test the outcome, not how it is -# enforced - passing "Duplicate key-sequence" accepts a unique, a key or a rename alike, -# as long as the duplicate is caught. Each future test declares its own . -# Fails closed: accepted, wrong reason, or xmllint not running all count as failures. -assert_rejected() { # - out=$("${XMLLINT}" --noout --schema "$2" "$1" 2>&1); st=$? - if [ "${st}" -eq 0 ]; then - echo "SHOULD HAVE FAILED $1 — accepted (must be rejected)" - fail=1 - elif printf '%s\n' "${out}" | grep -q "$3"; then - echo "OK $1" +echo "Checking NeTEx 'should-fail' negative examples ..." +for c in "${CASES[@]}"; do + f="${c%%|*}"; expected="${c#*|}" + if printf '%s\n' "${out}" | grep -Fqx "${f} validates"; then + echo "SHOULD HAVE FAILED ${f} — accepted (must be rejected)"; fail=1 + elif printf '%s\n' "${out}" | grep -F "${f}:" | grep -q "${expected}"; then + echo "OK ${f}" else - echo "ERROR $1 — rejected, but not matching '$3' (unrelated error / xmllint failure?)" - fail=1 + echo "ERROR ${f} — rejected, but not matching '${expected}'"; fail=1 fi -} - -echo "Checking NeTEx 'should-fail' negative examples ..." -assert_rejected examples/should-fail/duplicate-GroupOfLinkSequences.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" -assert_rejected examples/should-fail/duplicate-CalendarDate.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" -assert_rejected examples/should-fail/duplicate-ValidBetween.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" -assert_rejected examples/should-fail/duplicate-ValidityPeriod.xml xsd/NeTEx_publication.xsd "Duplicate key-sequence" +done exit "${fail}"