diff --git a/.circleci/config.yml b/.circleci/config.yml
index 55b58ef8..57569fb2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -18,8 +18,8 @@ jobs:
- run:
name: Install testing prerequisites
command: |
- # sudo apt-get update
- # sudo apt-get install -y xsltproc
+ sudo apt-get update
+ sudo apt-get install -y xsltproc
mkdir /tmp/saxon/
mkdir /tmp/saxon/lib/
@@ -46,6 +46,9 @@ jobs:
- run:
name: Run LoC-specific test suite
command: "$XSPEC test/ConvSpec-DLC.xspec"
+ - run:
+ name: Compare libxslt and Saxon output
+ command: SAXON_CP=${SAXON_CP:-/tmp/saxon/saxon-he-12.4.jar} ./test/compare-processors.sh
- store_artifacts:
path: test/xspec/marc2bibframe2-result.html
destination: xspec-tests/marc2bibframe2-result.html
diff --git a/README.md b/README.md
index 9bfde94e..06d6375e 100644
--- a/README.md
+++ b/README.md
@@ -185,6 +185,22 @@ LoC-specific conversions, run only the ConvSpec-DLC.xspec test suite:
xspec.sh test/ConvSpec-DLC.xspec
+### Testing against libxslt
+
+The XSpec suite runs under Saxon. A construct that Saxon evaluates correctly
+and libxslt does not will pass it and still produce wrong output for the many
+downstream users on libxslt -- lxml, Nokogiri, PHP's XSL extension,
+XML::LibXSLT and `xsltproc` itself. To catch that, `test/compare-processors.sh`
+converts the same records with both processors and requires the results to
+match:
+
+ SAXON_CP=/tmp/saxon/saxon-he-12.4.jar ./test/compare-processors.sh
+
+Both results are rendered through `test/normalize.xsl` first, by the same
+processor, so indentation, attribute order and where namespace declarations
+land are not treated as differences. With no arguments the script checks the
+records named in its `DEFAULT_FILES`; pass paths to check others.
+
## Active record conversion
Active conversion of records - resolving URIs for elements of the
diff --git a/test/ConvSpec-Process6-Series.xspec b/test/ConvSpec-Process6-Series.xspec
index 3e4496fe..c7fd2a54 100644
--- a/test/ConvSpec-Process6-Series.xspec
+++ b/test/ConvSpec-Process6-Series.xspec
@@ -27,6 +27,15 @@
-->
+
+
+
+
+
+
+
+
+
diff --git a/test/compare-processors.sh b/test/compare-processors.sh
new file mode 100755
index 00000000..ba9fb312
--- /dev/null
+++ b/test/compare-processors.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env bash
+#
+# Convert every test record with both Saxon and libxslt and require the two
+# results to agree.
+#
+# The XSpec suite runs under Saxon only, so a construct that Saxon evaluates
+# correctly and libxslt does not passes CI and still produces wrong output for
+# the many downstream users on libxslt -- lxml (Python), Nokogiri (Ruby), PHP's
+# XSL extension, XML::LibXSLT (Perl) and xsltproc itself.
+#
+# Both results are rendered through test/normalize.xsl before comparison, by
+# the same processor, so that indentation, attribute order and the placement of
+# namespace declarations do not count as differences.
+#
+# Usage:
+# SAXON_CP=/path/to/saxon-he-12.4.jar ./test/compare-processors.sh [file...]
+#
+# With no arguments it checks the records listed in DEFAULT_FILES below. Pass
+# paths to check anything else -- pointing it at all of test/data is a quick
+# way to see where else the two processors part company.
+
+set -u
+
+cd "$(dirname "$0")/.."
+
+: "${SAXON_CP:?set SAXON_CP to the Saxon jar, e.g. /tmp/saxon/saxon-he-12.4.jar}"
+
+command -v xsltproc >/dev/null || { echo "xsltproc not found"; exit 2; }
+
+STYLESHEET=xsl/marc2bibframe2.xsl
+NORMALIZE=test/normalize.xsl
+
+# The transform stamps the current time into the work's admin metadata, which
+# would differ between the two runs.
+DATESTAMP=2020-01-01T00:00:00
+
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT
+
+# The records this check covers. Deliberately a subset for now: it starts with
+# the series data, and should grow as other processor divergences are found and
+# resolved.
+DEFAULT_FILES=(test/data/ConvSpec-Process6-Series/*.xml)
+
+if [ "$#" -gt 0 ]; then
+ files=("$@")
+else
+ files=("${DEFAULT_FILES[@]}")
+fi
+
+failed=0
+checked=0
+
+for file in "${files[@]}"; do
+ checked=$((checked + 1))
+
+ if ! xsltproc --stringparam pGenerationDatestamp "$DATESTAMP" \
+ "$STYLESHEET" "$file" > "$work/libxslt.xml" 2> "$work/libxslt.err"; then
+ echo "FAIL $file"
+ echo " libxslt could not convert it:"
+ sed 's/^/ /' "$work/libxslt.err"
+ failed=$((failed + 1))
+ continue
+ fi
+
+ if [ ! -s "$work/libxslt.xml" ]; then
+ echo "FAIL $file"
+ echo " libxslt produced no output"
+ sed 's/^/ /' "$work/libxslt.err"
+ failed=$((failed + 1))
+ continue
+ fi
+
+ java -cp "$SAXON_CP" net.sf.saxon.Transform \
+ -s:"$file" -xsl:"$STYLESHEET" "pGenerationDatestamp=$DATESTAMP" \
+ -o:"$work/saxon.xml" 2> "$work/saxon.err" || {
+ echo "FAIL $file"
+ echo " Saxon could not convert it:"
+ sed 's/^/ /' "$work/saxon.err"
+ failed=$((failed + 1))
+ continue
+ }
+
+ xsltproc "$NORMALIZE" "$work/libxslt.xml" > "$work/libxslt.txt"
+ xsltproc "$NORMALIZE" "$work/saxon.xml" > "$work/saxon.txt"
+
+ if ! diff -q "$work/libxslt.txt" "$work/saxon.txt" > /dev/null; then
+ echo "FAIL $file"
+ echo " libxslt and Saxon disagree (< libxslt, > Saxon), first 20 lines:"
+ diff "$work/libxslt.txt" "$work/saxon.txt" | head -20 | sed 's/^/ /'
+ failed=$((failed + 1))
+ continue
+ fi
+
+ echo "ok $file"
+done
+
+echo
+if [ "$failed" -gt 0 ]; then
+ echo "$failed of $checked records differ between processors"
+ exit 1
+fi
+echo "$checked records convert identically under libxslt and Saxon"
diff --git a/test/data/ConvSpec-Process6-Series/dup-relations.xml b/test/data/ConvSpec-Process6-Series/dup-relations.xml
new file mode 100644
index 00000000..ab4c621c
--- /dev/null
+++ b/test/data/ConvSpec-Process6-Series/dup-relations.xml
@@ -0,0 +1,22 @@
+
+
+ 1
+
+ Lund studies in geography
+
+
+
+ 2
+
+ Lund studies in geography ;
+ no. 32
+
+
+
+ 3
+
+ Lund studies in geography ;
+ 1234-5678
+
+
+
diff --git a/test/normalize.xsl b/test/normalize.xsl
new file mode 100644
index 00000000..958f8aae
--- /dev/null
+++ b/test/normalize.xsl
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xsl/ConvSpec-Process6-Series.xsl b/xsl/ConvSpec-Process6-Series.xsl
index 7c5ee209..57d05ed7 100644
--- a/xsl/ConvSpec-Process6-Series.xsl
+++ b/xsl/ConvSpec-Process6-Series.xsl
@@ -63,7 +63,7 @@
-
+