-*- mode: org -*- #+TITLE: test shell script ssp document abstraction #+DESCRIPTION: env envrc used by make & nix #+FILETAGS: :spine:build:tools: #+AUTHOR: Ralph Amissah #+EMAIL: [[mailto:ralph.amissah@gmail.com][ralph.amissah@gmail.com]] #+COPYRIGHT: Copyright (C) 2015 (continuously updated, current 2026) Ralph Amissah #+LANGUAGE: en #+STARTUP: content hideblocks hidestars noindent entitiespretty #+PROPERTY: header-args+ :eval never-export :exports code #+PROPERTY: header-args+ :noweb yes :padline no #+PROPERTY: header-args+ :results silent :cache no #+PROPERTY: header-args+ :mkdirp yes #+OPTIONS: H:3 num:nil toc:t \n:t ::t |:t ^:nil -:t f:t *:t - magic single double-quote → " ← FIX changes hilighting behavior (occuring after it) in org document. INVESTIGATE (org-mode CONFIG?) FIND & FIX * document abstraction - ocda ** run tests #+HEADER: :tangle ../test/run-tests.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # run-tests.sh # # Runs the abstraction tests in sequence and reports one line each. # # Usage: # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/run-tests.sh ./bin/spine-ldc # # Two outside tools are needed: sqlite3 by one script and epubcheck by # another. If either is not on PATH the harness re-runs itself inside a nix # shell that has both, so this works as it stands: # SpinePOD=... ./test/run-tests.sh ./bin/spine-ldc # # Exit codes: # 0 every test ran and passed # n the number of tests that ran and failed # 2 the suite did not run in full: a set-up problem here (no $SpinePOD, # no binary, a missing tool) or a script exiting 2 for one of its own. # Reported as INCOMPLETE rather than PASS, because nothing should read # "it passed" out of a check that never ran # # Order, and what depends on what: # # 1 test-abstraction-ssp.sh the abstraction has not changed # 2 test-run-order.sh nor does it depend on the run # 3 test-abstraction-ssp-roundtrip.sh the .ssp can be read back whole # 4 test-abstraction-db.sh the two serialisations agree # 5 test-abstraction-db-roundtrip.sh the .db can be read back whole # 6 test-ocn-alignment.sh the languages agree on their ocns # 7 test-acceptance-db-to-pod.sh a database gives its document back # 8 test-epub-validity.sh the epubs a reader can actually open # # None of them calls another, and each can be run on its own. The order # matters only for reading the result: # # - 1 is first because it is the one that says whether the abstraction # itself moved. If it fails, the others are answering a different # question than you think, and its failure is the one to look at. # - 2 says that a document abstracted alone is the document abstracted in # company. 1 only ever runs the whole sample set, so an abstraction # that depended on what else was in the run passed it; this is the # check that would have caught that, and the one that says an artefact # can be verified on its own. # - 3 reads the committed reference set, so it is only a statement about # the current binary if 1 passes. Run together, they say: the abstraction # is unchanged, and this binary can read that abstraction back. # - 4 and 5 generate both artefacts themselves and depend on nothing # committed. Since the database is built from the .ssp, they are checks # on the reader rather than on two independent writers. # - 6 is about the sample documents rather than about a serialisation: it # asserts that spine reports the two known divergences in live-manual's # translations and reports nothing about the other documents. It will # fail if those translations are mended, which is the right moment to # hear about it. # - 7 is the acceptance test for the document source claim, and the widest # of them: it builds a document from its pod and again from the database # that build produced, and requires the two output trees to be identical. # Where 4 and 5 check one serialisation against another, this checks a # whole document through the whole pipeline twice. If it fails and the # others pass, what broke is the carrying or the materialising rather # than the abstraction. set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_BIN="${1:-}" if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi # the tools the scripts need, and the shell that provides them. # # Both are checked, not just sqlite3. Checking one and providing two meant # that a shell with sqlite3 already on PATH - a dev shell, or any machine # with sqlite installed - did not re-exec, and test-epub-validity.sh then # could not find epubcheck and reported a set-up problem that read as a # failing test. _missing="" command -v sqlite3 > /dev/null 2>&1 || _missing="$_missing sqlite3" command -v epubcheck > /dev/null 2>&1 || _missing="$_missing epubcheck" if [ -n "$_missing" ]; then if [ -n "$SpineTestsReExeced" ]; then echo "ERROR: still missing$_missing inside the nix shell." >&2 exit 2 fi if command -v nix > /dev/null 2>&1; then echo "not on PATH:$_missing - re-running inside nix shell (sqlite, epubcheck)" SpineTestsReExeced=1 exec nix shell "nixpkgs#sqlite" "nixpkgs#epubcheck" \ -c "$0" "$@" fi echo "ERROR: missing$_missing and nix is not available." >&2 exit 2 fi TESTS="test-abstraction-ssp.sh test-run-order.sh test-abstraction-ssp-roundtrip.sh test-abstraction-db.sh test-abstraction-db-roundtrip.sh test-ocn-alignment.sh test-acceptance-db-to-pod.sh test-epub-validity.sh" PASSED=0 FAILED=0 SETUP=0 RESULTS="" LOG="${TMPDIR:-/tmp}/spine-run-tests.$$" for t in $TESTS; do printf '%s\n' "── $t" # the status has to be the test's, not the last command of a pipeline: # "cmd | sed" reports sed's status, which is always 0, and a harness that # cannot fail is worse than no harness # "|| _rc=$?" and not a bare call: set -e is on, and a bare non-zero # would end the harness at the first failing test instead of running the # rest and summarising _rc=0 sh "$SCRIPT_DIR/$t" $SPINE_BIN > "$LOG" 2>&1 || _rc=$? sed 's/^/ /' "$LOG" # every script uses 2 for "could not run" and 1 for "ran and failed", and # the two must not read alike: "FAIL test-epub-validity.sh" says the epubs # are invalid, where exit 2 says nobody checked them case "$_rc" in 0) PASSED=$((PASSED + 1)); RESULTS="$RESULTS ok $t" ;; 2) SETUP=$((SETUP + 1)); RESULTS="$RESULTS SET-UP $t (could not run; nothing was checked)" ;; *) FAILED=$((FAILED + 1)); RESULTS="$RESULTS FAIL $t" ;; esac done rm -f "$LOG" echo "" echo "summary:$RESULTS" echo "" _total=$((PASSED + FAILED + SETUP)) if [ "$FAILED" -eq 0 ] && [ "$SETUP" -eq 0 ]; then echo "PASS: $PASSED of $_total" exit 0 elif [ "$FAILED" -eq 0 ]; then # nothing failed, but the suite did not run in full, and saying PASS here # would claim more than was checked echo "INCOMPLETE: $PASSED of $_total ran, $SETUP could not" exit 2 else echo "FAIL: $FAILED of $_total" exit "$FAILED" fi #+END_SRC ** 1-3. ssp ocda abstraction *** 1. document.ssp abstraction test/test-abstraction-ssp.sh #+HEADER: :tangle ../test/test-abstraction-ssp.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-abstraction-ssp.sh # # Regression test for spine's document abstraction. # Generates .ssp files for all sample documents and diffs # against committed reference files. # Also checks that a parallel run and a serial run of the same documents # produce identical .ssp (guards against per-document state leaking # between documents or between worker threads). # # Usage: # ./test/test-abstraction-ssp.sh # uses result/bin/spine # ./test/test-abstraction-ssp.sh ./bin/spine-ldc # specify binary # # Exit codes: # 0 all .ssp files match reference # 1 differences found (printed to stdout) # 2 missing reference directory (run with --generate first) # # To generate/update reference files: # ./test/test-abstraction-ssp.sh --generate # ./test/test-abstraction-ssp.sh --generate ./bin/spine-ldc set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # $SpinePOD names the sample pod directory, relative to the spine directory. # Unset, "$SPINE_DIR/$SpinePOD" is the spine directory itself, which exists, # so the "sample documents not found" check below would not catch it and # spine would be handed every file in the spine tree. if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " Set it to the sample pod directory, relative to $SPINE_DIR" >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi REF_DIR="$SCRIPT_DIR/reference/abstraction" TMP_DIR="$SCRIPT_DIR/current/abstraction" # parse arguments GENERATE=false SPINE_BIN="" for arg in "$@"; do case "$arg" in --generate) GENERATE=true ;; *) SPINE_BIN="$arg" ;; esac done # find spine binary if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi # check samples exist if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 exit 2 fi echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" if [ "$GENERATE" = true ]; then echo "Generating reference .ssp files..." rm -rf "$REF_DIR" mkdir -p "$REF_DIR" $SPINE_BIN --show-abstraction --skip-output --output="$SCRIPT_DIR/reference" "$SAMPLES_DIR"/* 2>&1 | tail -1 # flatten language subdirs into reference dir find "$SCRIPT_DIR/reference" -name "*.ssp" ! -path "$REF_DIR/*" -exec mv {} "$REF_DIR/" \; # clean up empty language dirs find "$SCRIPT_DIR/reference" -mindepth 1 -type d -empty -delete 2>/dev/null || true COUNT=$(ls "$REF_DIR"/*.ssp 2>/dev/null | wc -l) echo "Generated $COUNT reference .ssp files in $REF_DIR" exit 0 fi # check reference exists if [ ! -d "$REF_DIR" ] || [ -z "$(ls "$REF_DIR"/*.ssp 2>/dev/null)" ]; then echo "ERROR: no reference files found at $REF_DIR" >&2 echo "Run with --generate first to create reference files." >&2 exit 2 fi # generate current .ssp files echo "Generating current .ssp files..." rm -rf "$TMP_DIR" mkdir -p "$TMP_DIR" # this run has to be the parallel one: the serial run below is what it is # compared against, and two serial runs compare nothing. Serial is spine's # behaviour now and --parallel is the option, so it has to be asked for # here by name. Drop it and this test stops guarding the .ssp write race # without any test failing to say so. $SPINE_BIN --parallel --show-abstraction --skip-output --output="$SCRIPT_DIR/current" "$SAMPLES_DIR"/* 2>&1 | tail -1 # flatten find "$SCRIPT_DIR/current" -name "*.ssp" ! -path "$TMP_DIR/*" -exec mv {} "$TMP_DIR/" \; find "$SCRIPT_DIR/current" -mindepth 1 -type d -empty -delete 2>/dev/null || true # generate the same .ssp serially, to check that parallel and serial # processing agree (per-document state leaking between documents, or # between worker threads, shows up here) SER_DIR="$SCRIPT_DIR/current-serial/abstraction" echo "Generating current .ssp files (serial)..." rm -rf "$SCRIPT_DIR/current-serial" mkdir -p "$SER_DIR" $SPINE_BIN --show-abstraction --serial --skip-output --output="$SCRIPT_DIR/current-serial" "$SAMPLES_DIR"/* 2>&1 | tail -1 find "$SCRIPT_DIR/current-serial" -name "*.ssp" ! -path "$SER_DIR/*" -exec mv {} "$SER_DIR/" \; find "$SCRIPT_DIR/current-serial" -mindepth 1 -type d -empty -delete 2>/dev/null || true # diff echo "Comparing against reference..." FAILURES=0 for ref_file in "$REF_DIR"/*.ssp; do basename=$(basename "$ref_file") cur_file="$TMP_DIR/$basename" if [ ! -f "$cur_file" ]; then echo "MISSING: $basename (in reference but not generated)" FAILURES=$((FAILURES + 1)) continue fi if ! diff -q "$ref_file" "$cur_file" > /dev/null 2>&1; then echo "CHANGED: $basename" diff --unified=3 "$ref_file" "$cur_file" | head -30 echo " ..." FAILURES=$((FAILURES + 1)) fi done # check for new files not in reference for cur_file in "$TMP_DIR"/*.ssp; do basename=$(basename "$cur_file") ref_file="$REF_DIR/$basename" if [ ! -f "$ref_file" ]; then echo "NEW: $basename (generated but not in reference)" FAILURES=$((FAILURES + 1)) fi done # parallel vs serial determinism for cur_file in "$TMP_DIR"/*.ssp; do basename=$(basename "$cur_file") ser_file="$SER_DIR/$basename" if [ ! -f "$ser_file" ]; then echo "MISSING (serial run): $basename" FAILURES=$((FAILURES + 1)) continue fi if ! diff -q "$cur_file" "$ser_file" > /dev/null 2>&1; then echo "NON-DETERMINISTIC: $basename (parallel and serial runs differ)" diff --unified=3 "$cur_file" "$ser_file" | head -30 echo " ..." FAILURES=$((FAILURES + 1)) fi done # clean up rm -rf "$SCRIPT_DIR/current" "$SCRIPT_DIR/current-serial" if [ "$FAILURES" -eq 0 ]; then REF_COUNT=$(ls "$REF_DIR"/*.ssp | wc -l) echo "PASS: all $REF_COUNT .ssp files match reference (parallel and serial agree)" exit 0 else echo "FAIL: $FAILURES difference(s) found" exit 1 fi #+END_SRC *** 2. document.ssp abstraction test/test-run-order.sh #+HEADER: :tangle ../test/test-run-order.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-run-order.sh # # A document's abstraction must not depend on what else is in the run. # # It did. The set of heading anchor names spine has seen is seeded with the # names it keeps for its own generated sections (toc, endnotes, glossary, # bibliography, bookindex, blurb, _the_title), and the seeding was written # only in scope(exit) and at the tail of docAbstraction(): both run *after* a # document, so they seeded the next one. The first document of a run began # with the set empty and was the one document not held to the reserved names, # and a heading of its own claiming "endnotes" kept the name of the generated # section rather than being renamed out of its way. # # So the same document abstracted alone and abstracted after another differed, # and an artefact written in a collection run could not be verified on its # own: --ocda-verify re-parses one document, where it is necessarily first, # and reported a correct artefact as one whose markup no longer makes it. # # The check: abstract two documents in one run, abstract each of them alone, # and require the .ssp to be identical either way. The second document is the # one that matters (it is seeded in the pair and not when alone), so the pair # is ordered with the affected document last. # # $SpineRunOrderDocs overrides the pair, space separated, unqualified # directory names within $SpinePOD. The default pair is chosen so that the # test would fail before the fix: the_autonomous_contract has a "1~endnotes" # heading, which is what the seeding renames. # # Usage: # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/test-run-order.sh ./bin/spine-ldc # # Exit codes: # 0 a document abstracts the same alone as in company # 1 it does not (the differing files are printed) # 2 set-up problem (no $SpinePOD, no binary, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 exit 2 fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi DOCS="${SpineRunOrderDocs:-gpl2.fsf the_autonomous_contract.ralph_amissah}" for d in $DOCS; do if [ ! -d "$SAMPLES_DIR/$d" ]; then echo "ERROR: sample document not found: $SAMPLES_DIR/$d" >&2 echo " set \$SpineRunOrderDocs to a pair that is present" >&2 exit 2 fi done OUT_DIR="$SCRIPT_DIR/current-run-order" rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR" echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" echo "documents: $DOCS" FAILURES=0 fail() { echo " FAIL: $*"; FAILURES=$((FAILURES + 1)); } # the pair, in one run _pair_args="" for d in $DOCS; do _pair_args="$_pair_args $SAMPLES_DIR/$d" done # shellcheck disable=SC2086 $SPINE_BIN --show-abstraction --skip-output \ --output="$OUT_DIR/together" $_pair_args > "$OUT_DIR/together.out" 2>&1 # and each of them on its own for d in $DOCS; do $SPINE_BIN --show-abstraction --skip-output \ --output="$OUT_DIR/alone-$d" "$SAMPLES_DIR/$d" \ > "$OUT_DIR/alone-$d.out" 2>&1 done # every .ssp the pair run produced has to have a twin, identical, in the run # that produced it alone _compared=0 for f in $(find "$OUT_DIR/together" -name "*.ssp" | sort); do _rel="${f#"$OUT_DIR/together/"}" _alone="" for d in $DOCS; do if [ -f "$OUT_DIR/alone-$d/$_rel" ]; then _alone="$OUT_DIR/alone-$d/$_rel"; fi done if [ -z "$_alone" ]; then fail "$_rel was produced in the pair run and by neither document alone" continue fi _compared=$((_compared + 1)) if ! cmp -s "$f" "$_alone"; then fail "$_rel differs between the pair run and the run of its own document" diff --unified=2 "$_alone" "$f" | head -20 | sed 's/^/ /' fi done if [ "$_compared" -eq 0 ]; then echo "ERROR: no .ssp files were produced to compare" >&2 rm -rf "$OUT_DIR" exit 2 fi rm -rf "$OUT_DIR" if [ "$FAILURES" -eq 0 ]; then echo "PASS: $_compared .ssp identical whether the document ran alone or in company" exit 0 else echo "FAIL: $FAILURES check(s)" exit 1 fi #+END_SRC *** 3. document.ssp abstraction test/test-abstraction-ssp-roundtrip.sh #+HEADER: :tangle ../test/test-abstraction-ssp-roundtrip.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-abstraction-ssp-roundtrip.sh # # Reads each reference .ssp back into the abstraction and emits it again, # and requires the result to be byte identical to the file it read. # # What that proves: the reader (sisudoc.ocda.abstraction.ssp_in) restores # every field the writer emits. The check is made against the writer itself, # since both go through template sspObjectRecord, so there is no second # description of the format that could drift from the first. # # It needs no sample documents and no output directory: the committed # reference set is the input. Run test-abstraction-ssp.sh first if you want # to know that the reference set is itself current. # # Usage: # ./test/test-abstraction-ssp-roundtrip.sh # uses result/bin/spine # ./test/test-abstraction-ssp-roundtrip.sh ./bin/spine-ldc # specify binary # # Exit codes: # 0 every reference .ssp round trips byte identically # 1 one or more do not (the first differences are printed) # 2 set-up problem (no binary, no reference set) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REF_DIR="$SCRIPT_DIR/reference/abstraction" SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi if [ ! -d "$REF_DIR" ] || [ -z "$(ls "$REF_DIR"/*.ssp 2>/dev/null)" ]; then echo "ERROR: no reference files found at $REF_DIR" >&2 echo "Run ./test/test-abstraction-ssp.sh --generate first." >&2 exit 2 fi TMP="$SCRIPT_DIR/current-roundtrip" rm -rf "$TMP" mkdir -p "$TMP" echo "spine binary: $SPINE_BIN" echo "reference: $REF_DIR" echo "Reading each .ssp back and emitting it again ..." COUNT=0 FAILURES=0 for ref in "$REF_DIR"/*.ssp; do base=$(basename "$ref") COUNT=$((COUNT + 1)) if ! $SPINE_BIN --ssp-round-trip="$ref" > "$TMP/$base" 2> "$TMP/$base.err"; then echo "ERROR: $base (spine exited non-zero)" head -3 "$TMP/$base.err" FAILURES=$((FAILURES + 1)) continue fi if ! cmp -s "$ref" "$TMP/$base"; then echo "DIFFERS: $base ($(diff "$ref" "$TMP/$base" | grep -c '^[<>]') lines)" diff --unified=1 "$ref" "$TMP/$base" | head -12 echo " ..." FAILURES=$((FAILURES + 1)) fi done rm -rf "$TMP" if [ "$FAILURES" -eq 0 ]; then echo "PASS: all $COUNT .ssp files read back and re-emit byte identically" exit 0 else echo "FAIL: $FAILURES of $COUNT do not round trip" exit 1 fi #+END_SRC ** 4&5. db ocda abstraction *** 4. document db ocda test/test-abstraction-db.sh #+HEADER: :tangle ../test/test-abstraction-db.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-abstraction-db.sh # # Cross-check for spine's two serialisations of the document abstraction: # the .ssp text file and the .ocda.db sqlite database. # # It generates both for every sample document and asserts that they agree # object for object and field for field. There is no committed reference # set: the two artefacts check each other, which is what catches the thing # that went wrong before, one writer gaining a field and the other not. # # Usage: # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/test-abstraction-db.sh ./bin/spine-ldc # # Exit codes: # 0 the two agree for every document # 1 disagreements found (printed) # 2 set-up problem (no $SpinePOD, no binary, no sqlite3, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " Set it to the sample pod directory, relative to $SPINE_DIR" >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi if ! command -v sqlite3 > /dev/null 2>&1; then echo "ERROR: sqlite3 not on PATH." >&2 echo " e.g. nix shell \"nixpkgs#sqlite\" -c ./test/test-abstraction-db.sh $SPINE_BIN" >&2 exit 2 fi if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 exit 2 fi OUT_DIR="$SCRIPT_DIR/current-db" echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" echo "Generating .ssp and .ocda.db ..." rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR" $SPINE_BIN --show-abstraction --ocda-db --skip-output \ --output="$OUT_DIR" "$SAMPLES_DIR"/* 2>&1 | tail -1 FAILURES=0 DOCS=0 # ssp property -> sqlite predicate counting the same thing # "|" CHECKS=" children|children IS NOT NULL ancestors|ancestors IS NOT NULL ancestors_collapsed|ancestors_collapsed IS NOT NULL dom_status|dom_status IS NOT NULL dom_status_collapsed|dom_status_collapsed IS NOT NULL last_descendant|last_descendant_ocn != 0 parent_lev|parent_lev != 0 heading_lev_collapsed|heading_lev_collapsed IS NOT NULL section|is_of_section IS NOT NULL AND is_of_section != section part|is_of_part IS NOT NULL is_of_type|is_of_type IS NOT NULL attrib|attrib IS NOT NULL meta_lang|meta_lang IS NOT NULL meta_syntax|meta_syntax IS NOT NULL sha256|sha256 IS NOT NULL heading_ancestors_text|heading_ancestors_text IS NOT NULL segment|segment IS NOT NULL segment_prev|segment_prev IS NOT NULL segment_next|segment_next IS NOT NULL segment_html_is|segment_html_is IS NOT NULL segment_epub_is|segment_epub_is IS NOT NULL segment_epub|segment_epub IS NOT NULL anchor|anchor IS NOT NULL AND anchor != segment heading_lev_anchor|heading_lev_anchor IS NOT NULL table_cols|table_cols IS NOT NULL table_widths|table_widths IS NOT NULL table_aligns|table_aligns IS NOT NULL dummy: true|dummy_heading = 1 ocn_off: true|object_number_off = 1 bullet: true|bullet = 1 table_header: true|table_header = 1 code_linenumbers: true|code_linenumbers = 1 lang|lang IS NOT NULL indent|indent_base != 0 OR indent_hang != 0 " # repeatable ssp property -> child table LISTS=" image|object_images stow_link|object_links anchor_tag|object_anchors lev4_subtoc|object_subtoc " report() { echo " MISMATCH $1: .ssp $2, db $3"; } # the .ssp is written into the pod tree (pod//media/abstraction/), one # per language, and the database beside it (pod/.ocda.db), one per # document holding every language of it. So the .ssp name carries the # language and the database name does not: ..ssp beside # .ocda.db, and inside the database the language's rows are the ones # whose doc_id is the id documents gives that language. for ssp in $(find "$OUT_DIR/pod" -name "*.ssp" 2>/dev/null | sort); do [ -f "$ssp" ] || continue base=$(basename "$ssp" .ssp) lang="${base##*.}" stem="${base%.*}" db="$OUT_DIR/pod/$stem.ocda.db" if [ ! -f "$db" ]; then echo "MISSING db for $base" FAILURES=$((FAILURES + 1)) continue fi did=$(sqlite3 "$db" "select id from documents where lang='$lang';" 2>/dev/null || echo '') if [ -z "$did" ]; then echo "MISSING $lang in $(basename "$db"); it holds:" \ "$(sqlite3 "$db" "select group_concat(lang,' ') from documents;" 2>/dev/null)" FAILURES=$((FAILURES + 1)) continue fi # the objects of this one language, for the child tables to key on mine="object_id in (select id from objects where doc_id=$did)" DOCS=$((DOCS + 1)) doc_fail=0 # object population s_objs=$(grep -c '^\[' "$ssp" || true) d_objs=$(sqlite3 "$db" "select count(*) from objects where doc_id=$did;" 2>/dev/null || echo ERR) if [ "$s_objs" != "$d_objs" ]; then echo "$base"; report "objects" "$s_objs" "$d_objs"; doc_fail=1 fi # per section for sec in head toc body endnotes glossary bibliography bookindex blurb; do s_sec=$(awk -v s="@$sec {" '$0==s{f=1;next} /^\}$/{f=0} f&&/^\[/{n++} END{print n+0}' "$ssp") d_sec=$(sqlite3 "$db" "select count(*) from objects where doc_id=$did and section='$sec';" 2>/dev/null || echo ERR) if [ "$s_sec" != "$d_sec" ]; then [ $doc_fail -eq 0 ] && echo "$base" report "section $sec" "$s_sec" "$d_sec"; doc_fail=1 fi done # single valued properties echo "$CHECKS" | while IFS='|' read -r prop pred; do [ -z "$prop" ] && continue case "$prop" in *": true") s_n=$(grep -c "^\.$prop\$" "$ssp" || true) ;; *) s_n=$(grep -c "^\.$prop: " "$ssp" || true) ;; esac d_n=$(sqlite3 "$db" "select count(*) from objects where doc_id=$did and ($pred);" 2>/dev/null || echo ERR) if [ "$s_n" != "$d_n" ]; then echo "$base: MISMATCH .$prop: .ssp $s_n, db $d_n" fi done > "$OUT_DIR/.props.$$" if [ -s "$OUT_DIR/.props.$$" ]; then [ $doc_fail -eq 0 ] && echo "$base" sed 's/^[^:]*: / /' "$OUT_DIR/.props.$$" doc_fail=1 fi rm -f "$OUT_DIR/.props.$$" # repeatable properties against their tables echo "$LISTS" | while IFS='|' read -r prop table; do [ -z "$prop" ] && continue s_n=$(grep -c "^\.$prop: " "$ssp" || true) d_n=$(sqlite3 "$db" "select count(*) from $table where $mine;" 2>/dev/null || echo ERR) if [ "$s_n" != "$d_n" ]; then echo "$base: MISMATCH .$prop: .ssp $s_n, $table $d_n" fi done > "$OUT_DIR/.lists.$$" if [ -s "$OUT_DIR/.lists.$$" ]; then [ $doc_fail -eq 0 ] && echo "$base" sed 's/^[^:]*: / /' "$OUT_DIR/.lists.$$" doc_fail=1 fi rm -f "$OUT_DIR/.lists.$$" # every image the abstraction referred to and found is carried in the file. # the files table is shared: the languages of a document refer to the same # images and one copy serves them all, so what is counted here is the names # this language referred to and the stored files that answer them. want=$(sqlite3 "$db" "select count(distinct name) from object_images where missing = 0 and $mine;" 2>/dev/null || echo ERR) have=$(sqlite3 "$db" "select count(*) from files f where f.role='image' and exists (select 1 from object_images i where i.name = f.name and i.missing = 0 and i.$mine);" 2>/dev/null || echo ERR) if [ "$want" != "$have" ]; then [ $doc_fail -eq 0 ] && echo "$base" report "images carried" "$want referenced" "$have stored"; doc_fail=1 fi # the text itself, on the first and last citable object for pick in "min" "max"; do ocn=$(sqlite3 "$db" "select $pick(ocn) from objects where doc_id=$did and ocn > 0 and section='body';" 2>/dev/null || echo "") [ -z "$ocn" ] && continue sqlite3 "$db" "select text from objects where doc_id=$did and ocn=$ocn and section='body';" > "$OUT_DIR/.dbtxt.$$" 2>/dev/null || true awk -v o="[$ocn] " 'index($0,o)==1{f=1;next} f&&/^$/{exit} f&&/^\| /{print substr($0,3)}' \ "$ssp" > "$OUT_DIR/.ssptxt.$$" if ! diff -q "$OUT_DIR/.dbtxt.$$" "$OUT_DIR/.ssptxt.$$" > /dev/null 2>&1; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISMATCH text of ocn $ocn" doc_fail=1 fi rm -f "$OUT_DIR/.dbtxt.$$" "$OUT_DIR/.ssptxt.$$" done # the header blocks: every @source/@meta/@make/@doc_has property in the .ssp # must be in the database under its own key, with the same value. the three # excluded keys are the database's own notes about the artefact rather than # properties of the document: what it was built from (source.filename, # source.ssp_digest) and how its languages compared once they had all been # read (translation.ocn_aligned), which no single .ssp can state. this is # what catches a header property that reaches one artefact and not the # other, which a count of objects cannot see. awk ' /^@(source|meta|make|doc_has) \{$/ { blk=substr($1,2); next } /^\}$/ { blk=""; next } blk != "" && /^ [a-z_.]+: / { k=$1; sub(/:$/,"",k) v=substr($0, index($0,": ")+2) pfx = (blk=="meta") ? "" : blk "." print pfx k "\t" v } ' "$ssp" | sort > "$OUT_DIR/.sspmeta.$$" sqlite3 -separator "$(printf '\t')" "$db" \ "select key, value from metadata where doc_id=$did and key not in ('source.filename','source.ssp_digest','translation.ocn_aligned');" \ 2>/dev/null | sort > "$OUT_DIR/.dbmeta.$$" if ! diff -q "$OUT_DIR/.sspmeta.$$" "$OUT_DIR/.dbmeta.$$" > /dev/null 2>&1; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISMATCH header blocks (.ssp against metadata table):" diff "$OUT_DIR/.sspmeta.$$" "$OUT_DIR/.dbmeta.$$" | head -8 | sed 's/^/ /' doc_fail=1 fi rm -f "$OUT_DIR/.sspmeta.$$" "$OUT_DIR/.dbmeta.$$" # the markup, carried. every markup file of this language is stored in the # database, and source.digest is rebuilt from the stored bytes and held # against what the .ssp recorded. # # source.digest is the sha256 over one line per markup file, # " ", sorted by that whole line and joined with newlines # and no trailing newline. So the same value can be computed here from the # files rows alone, and if it matches, the bytes in the database are the # bytes the abstraction was built from: not merely present, but the right # ones. That is the check worth having, since a file stored under the right # name with the wrong contents would pass a mere count. # LC_ALL=C: the lines are sorted by byte, as the writer sorts them, not by # whatever collation the locale would prefer src_lines=$(sqlite3 "$db" "select sha256 || ' ' || substr(name, length('media/text/$lang/') + 1) from files where role='source' and name like 'media/text/$lang/%';" \ 2>/dev/null | LC_ALL=C sort) # printf '%s' rather than echo: the joined lines carry no trailing newline d_dig=$(printf '%s' "$src_lines" | sha256sum | cut -d' ' -f1 | tr 'a-z' 'A-Z') s_dig=$(awk '/^@source \{$/ {b=1; next} /^\}$/ {b=0} b && /^ digest: / {print $2}' "$ssp") n_src=$(sqlite3 "$db" "select count(*) from files where role='source' and name like 'media/text/$lang/%';" 2>/dev/null || echo 0) if [ "$n_src" = "0" ]; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISSING markup carried for [$lang]"; doc_fail=1 elif [ -n "$s_dig" ] && [ "$d_dig" != "$s_dig" ]; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISMATCH source.digest rebuilt from the $n_src carried file(s):" echo " .ssp $s_dig" echo " db $d_dig" doc_fail=1 fi # the source filename, and the version the two share sf=$(sqlite3 "$db" "select value from metadata where doc_id=$did and key='source.filename';" 2>/dev/null || echo '') sf_ssp=$(sed -n 's/^% Source: //p' "$ssp" | head -1) if [ "$sf" != "$sf_ssp" ]; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISMATCH source.filename: db '$sf' .ssp '$sf_ssp'"; doc_fail=1 fi sv_ssp=$(sed -n 's/^% SiSU Document Abstraction v//p' "$ssp" | head -1) # the file says what it is sv=$(sqlite3 "$db" "select value from metadata where doc_id is null and key='schema.version';" 2>/dev/null || echo '') if [ -z "$sv" ]; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISSING metadata schema.version"; doc_fail=1 elif [ "$sv" != "$sv_ssp" ]; then [ $doc_fail -eq 0 ] && echo "$base" echo " MISMATCH format version: db '$sv' .ssp '$sv_ssp'"; doc_fail=1 fi FAILURES=$((FAILURES + doc_fail)) done # one database per document, holding every language of it: the languages the # documents table names are exactly the languages there are .ssp files for. # A per language loop cannot see this - it checks the languages it finds and # would not notice one the database had gained or lost - so it is checked per # database, once, here. DBS=0 for db in $(find "$OUT_DIR/pod" -maxdepth 1 -name "*.ocda.db" 2>/dev/null | sort); do [ -f "$db" ] || continue DBS=$((DBS + 1)) stem=$(basename "$db" .ocda.db) d_langs=$(sqlite3 "$db" "select lang from documents order by lang;" 2>/dev/null \ | tr '\n' ' ' | sed 's/ $//') s_langs=$(find "$OUT_DIR/pod" -name "$stem.*.ssp" 2>/dev/null \ | sed "s|.*/$stem\.||; s|\.ssp$||" | sort | tr '\n' ' ' | sed 's/ $//') if [ "$d_langs" != "$s_langs" ]; then echo "$stem" echo " MISMATCH languages: db '$d_langs', .ssp files '$s_langs'" FAILURES=$((FAILURES + 1)) fi done rm -rf "$OUT_DIR" if [ "$FAILURES" -eq 0 ]; then echo "PASS: .ssp and .ocda.db agree for all $DOCS documents in $DBS databases" exit 0 else echo "FAIL: $FAILURES document(s) disagree" exit 1 fi #+END_SRC * *** 5. document db ocda test-abstraction-db-roundtrip.sh #+HEADER: :tangle ../test/test-abstraction-db-roundtrip.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-abstraction-db-roundtrip.sh # # Reads each .ocda.db back into the abstraction, emits it as # .ssp, and requires the result to be byte identical to the .ssp written # from the same document. A database holds every language of its document, # so each language is asked for by name (--lang) and held against its own # .ssp, and a database of more than one language is required to refuse a # round trip that names none. # # What that proves: the two serialisations really do carry the same thing. # test-abstraction-db.sh counts fields and compares them; this reconstructs # the whole document from the database and holds it against the text. # # Usage (needs the sample documents, since both artefacts are generated): # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/test-abstraction-db-roundtrip.sh ./bin/spine-ldc # # Exit codes: # 0 every database re-emits its document's .ssp exactly # 1 one or more do not # 2 set-up problem set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 exit 2 fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi OUT_DIR="$SCRIPT_DIR/current-db-roundtrip" rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR" echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" echo "Generating .ssp and .ocda.db ..." $SPINE_BIN --show-abstraction --ocda-db --skip-output \ --output="$OUT_DIR" "$SAMPLES_DIR"/* 2>&1 | tail -1 COUNT=0 DBS=0 FAILURES=0 # the database sits beside the pod (pod/.ocda.db), the .ssp files inside # it (pod//media/abstraction/..ssp). # # A database holds every language of its document, so one database answers # for as many .ssp files as the document has languages, and each has to be # asked for by name: --lang says which language to emit. With no language # named a database holding more than one refuses to choose, which is checked # too - a round trip that quietly emitted the first language would pass # against the first .ssp and say nothing about the other nine. for db in "$OUT_DIR"/pod/*.ocda.db; do [ -f "$db" ] || continue DBS=$((DBS + 1)) stem=$(basename "$db" .ocda.db) # the languages come from the .ssp files rather than from the database, so # that this script needs no sqlite3: each .ssp is a language the database # has to be able to re-emit. A language in the database with no .ssp beside # it is the other direction, and test-abstraction-db.sh checks that. langs=$(find "$OUT_DIR/pod" -name "$stem.*.ssp" 2>/dev/null \ | sed "s|.*/$stem\.||; s|\.ssp$||" | sort) if [ -z "$langs" ]; then echo "MISSING .ssp for $stem" FAILURES=$((FAILURES + 1)) continue fi n_langs=$(printf '%s\n' "$langs" | grep -c .) for lang in $langs; do base="$stem.$lang" ssp="$(find "$OUT_DIR/pod" -name "$base.ssp" 2>/dev/null | head -1)" COUNT=$((COUNT + 1)) if [ ! -f "$ssp" ]; then echo "MISSING .ssp for $base" FAILURES=$((FAILURES + 1)) continue fi if ! $SPINE_BIN --ocda-db-round-trip="$db" --lang="$lang" \ > "$OUT_DIR/from-db.ssp" 2> "$OUT_DIR/from-db.err"; then echo "ERROR: $base (spine exited non-zero)" head -3 "$OUT_DIR/from-db.err" FAILURES=$((FAILURES + 1)) continue fi if ! cmp -s "$ssp" "$OUT_DIR/from-db.ssp"; then echo "DIFFERS: $base ($(diff "$ssp" "$OUT_DIR/from-db.ssp" | grep -c '^[<>]') lines)" diff --unified=1 "$ssp" "$OUT_DIR/from-db.ssp" | head -12 echo " ..." FAILURES=$((FAILURES + 1)) fi done # more than one language and none named: it must refuse rather than pick if [ "$n_langs" -gt 1 ]; then if $SPINE_BIN --ocda-db-round-trip="$db" > /dev/null 2> /dev/null; then echo "$stem" echo " ACCEPTED a round trip with no language named, holding $n_langs" FAILURES=$((FAILURES + 1)) fi fi done rm -rf "$OUT_DIR" if [ "$FAILURES" -eq 0 ]; then echo "PASS: $DBS databases re-emit all $COUNT of their documents' .ssp exactly" exit 0 else echo "FAIL: $FAILURES of $COUNT do not" exit 1 fi #+END_SRC ** 6. test ocn alignment #+HEADER: :tangle ../test/test-ocn-alignment.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-ocn-alignment.sh # # The languages of a document share their object numbering, and spine warns # when they stop doing so. This checks both halves of that: that the sample # set is silent, and that a document whose languages really do diverge is # reported. # # Two fixtures, because one cannot say both things: # # the sample set every document aligned, so nothing is reported at # all. This is the half that would fail if a change # made the check cry wolf, and the half that changed on # 2026-09-18 when live-manual's ja and pl translations # were mended. Until then ja was two objects short of # en and pl had a stray heading at ocn 313, and this # script asserted those two by name. # # a broken copy live-manual copied out of the sample set with one # paragraph removed from one language, which is a count # divergence and must be reported. Built here rather # than relied upon, so that mending a translation # cannot quietly leave the check untested. A check that # never fires proves as little as one that always does. # # It also asserts that --strict turns the warning into a non-zero exit, and # that without --strict the run still succeeds: a document with a diverging # translation is still a document. # # Usage: # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/test-ocn-alignment.sh ./bin/spine-ldc # # Exit codes: # 0 the check reports what it should, and nothing else # 1 it does not (printed) # 2 set-up problem (no $SpinePOD, no binary, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 exit 2 fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi OUT_DIR="$SCRIPT_DIR/current-ocn-alignment" rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR" echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" FAILURES=0 fail() { echo " FAIL: $*"; FAILURES=$((FAILURES + 1)); } # the whole sample set, abstraction only: the check does not need outputs echo "Abstracting the sample set ..." set +e $SPINE_BIN --show-abstraction --skip-output \ --output="$OUT_DIR" "$SAMPLES_DIR"/* \ > "$OUT_DIR/run.out" 2> "$OUT_DIR/run.err" RUN_RC=$? set -e # without --strict a diverging document is still built if [ "$RUN_RC" -ne 0 ]; then fail "the run exited $RUN_RC without --strict; a warning must not fail a run" head -5 "$OUT_DIR/run.err" fi # it must say nothing at all: every document in the set is aligned n_docs=$(grep -c "^WARNING ocn alignment: " "$OUT_DIR/run.err" || true) if [ "$n_docs" != "0" ]; then fail "$n_docs document(s) reported over the sample set, expected none" grep -A 3 "^WARNING ocn alignment: " "$OUT_DIR/run.err" | head -12 | sed 's/^/ /' fi # --strict over the whole set therefore passes too set +e $SPINE_BIN --show-abstraction --skip-output --strict \ --output="$OUT_DIR/strict-ok" "$SAMPLES_DIR"/live-manual \ > "$OUT_DIR/strict-ok.out" 2> "$OUT_DIR/strict-ok.err" STRICT_OK_RC=$? set -e if [ "$STRICT_OK_RC" -ne 0 ]; then fail "--strict exited $STRICT_OK_RC on a document whose languages agree" head -5 "$OUT_DIR/strict-ok.err" fi # and now the other half: a document that really does diverge. # # Built here, from a copy, by removing one paragraph from one language. The # sample set no longer contains a divergence, and a check that never fires # proves as little as one that always does. echo "Breaking a copy, to see the check fire ..." BROKEN="$OUT_DIR/broken" mkdir -p "$BROKEN" cp -a "$SAMPLES_DIR/live-manual" "$BROKEN/live-manual" TARGET="$BROKEN/live-manual/media/text/de/about_manual.ssi" if [ ! -f "$TARGET" ]; then echo "ERROR: fixture not found: $TARGET" >&2 exit 2 fi # the first paragraph after the first heading, whole: a numbered object, so # removing it renumbers everything below it in this language and no other. # # The whole of it, to the blank line that ends it. Dropping a single line of # a wrapped paragraph leaves the paragraph there, one line shorter, and the # object count unchanged: the first version of this fixture did that and the # check correctly said nothing. awk ' BEGIN { state = 0 } state == 0 && /^[A-Za-z]/ && !/^[0-9A-D]~/ && !/^:[A-D]~/ && !/^%/ && !/^@/ \ { state = 1; next } state == 1 && /^[[:space:]]*$/ { state = 2; next } state == 1 { next } { print } ' "$TARGET" > "$TARGET.new" && mv "$TARGET.new" "$TARGET" set +e $SPINE_BIN --show-abstraction --skip-output \ --output="$OUT_DIR/broken-out" "$BROKEN/live-manual" \ > "$OUT_DIR/broken.out" 2> "$OUT_DIR/broken.err" BROKEN_RC=$? set -e if [ "$BROKEN_RC" -ne 0 ]; then fail "the run over the broken copy exited $BROKEN_RC; a warning must not fail a run" head -5 "$OUT_DIR/broken.err" fi if ! grep -q "^WARNING ocn alignment: live-manual$" "$OUT_DIR/broken.err"; then fail "the removed paragraph was not reported" head -5 "$OUT_DIR/broken.err" | sed 's/^/ /' fi if ! grep -q "^ \[de\] differs from \[en\]$" "$OUT_DIR/broken.err"; then fail "de not reported as differing from en" fi if ! grep -q "numbered objects: en .*, de .* (-1)" "$OUT_DIR/broken.err"; then fail "de's object count not reported as one short" grep "numbered objects" "$OUT_DIR/broken.err" | sed 's/^/ /' fi # only the language that was broken n_langs=$(grep -c "^ \[.*\] differs from " "$OUT_DIR/broken.err" || true) if [ "$n_langs" != "1" ]; then fail "$n_langs languages reported on the broken copy, expected 1 (de)" grep "^ \[.*\] differs from " "$OUT_DIR/broken.err" | sed 's/^/ /' fi # and --strict makes that a failed run set +e $SPINE_BIN --show-abstraction --skip-output --strict \ --output="$OUT_DIR/broken-strict" "$BROKEN/live-manual" \ > "$OUT_DIR/broken-strict.out" 2> "$OUT_DIR/broken-strict.err" STRICT_RC=$? set -e if [ "$STRICT_RC" -eq 0 ]; then fail "--strict exited 0 on a document whose languages diverge" fi rm -rf "$OUT_DIR" if [ "$FAILURES" -eq 0 ]; then echo "PASS: the sample set is aligned, a broken copy is reported, --strict fails" exit 0 else echo "FAIL: $FAILURES check(s)" exit 1 fi #+END_SRC ** 7. test acceptance db to pod #+HEADER: :tangle ../test/test-acceptance-db-to-pod.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-acceptance-db-to-pod.sh # # The acceptance test for the document source claim: a .ocda.db carries # everything a pod does, so a pod written back out of it builds the same # document. # # Two runs of the same spine, with the same flags, over the same document: # # A the pod -> artefacts and outputs # B A's database -> pod -> artefacts and outputs # # and `diff -r` between the two output trees must be empty. That single # comparison covers more than it appears: the written pod tree (markup, conf, # images, translation catalogues), the .ssp files, the .ocda.db itself, the # digests.txt, and the text, html and epub built from them. # # Two things the runs must share for the comparison to mean anything: # # the site configuration. The abstraction depends only on pod content, # but a document's metadata pages carry the site's url, which comes from # a .dr/config_local_site found *above* the pod. A materialised pod sits # in a directory of the run's making with no such file, so the pod for # run A is copied out of the sample set to a neutral place rather than # read where it lies. Without that, a sound comparison shows ~22 files # differing in a url and reads as corruption. # # the flags. The dom structure tags the .ssp records are computed only # when an action needs them, so two runs asking for different things # build different abstractions from the same markup. Both runs here ask # for the same set. # # Not compared: the pod archive. --pod is not asked for, so neither run # writes one. The archive is a container around bytes that are compared # here already, and its own bytes are not part of what the database claims # to carry. # # Also asserted: --ocda-verify passes on the database, which is the other # half of the claim. The comparison says the pod comes back; verify says the # abstraction it makes is the one recorded with it. # # Usage: # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/test-acceptance-db-to-pod.sh ./bin/spine-ldc # # Exit codes: # 0 every document round trips with no difference # 1 differences found (printed) # 2 set-up problem (no $SpinePOD, no binary, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: sample documents not found at $SAMPLES_DIR" >&2 exit 2 fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi # the documents to round trip. live-manual is the one that exercises every # part: ten languages, inserts, and a tools/po4a catalogue tree. The second # has images, which live-manual has none of, and is a single language. DOCS="${SpineAcceptanceDocs:-live-manual the_wealth_of_networks.yochai_benkler}" # the same flags for both runs: the pod tree, both abstraction artefacts, and # three output formats over them FLAGS="--source --show-abstraction --ocda-db --text --html --epub" OUT_DIR="$SCRIPT_DIR/current-acceptance" rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR/pods" echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" FAILURES=0 DOCS_DONE=0 for doc in $DOCS; do if [ ! -d "$SAMPLES_DIR/$doc" ]; then echo "MISSING sample: $doc" FAILURES=$((FAILURES + 1)) continue fi echo "── $doc" # the pod, copied where no .dr/ sits above it cp -a "$SAMPLES_DIR/$doc" "$OUT_DIR/pods/$doc" # A: from the pod if ! $SPINE_BIN $FLAGS --output="$OUT_DIR/a.$doc" "$OUT_DIR/pods/$doc" \ > "$OUT_DIR/a.$doc.out" 2> "$OUT_DIR/a.$doc.err"; then echo " FAIL: the run from the pod exited non-zero" tail -3 "$OUT_DIR/a.$doc.err" | sed 's/^/ /' FAILURES=$((FAILURES + 1)) continue fi db=$(find "$OUT_DIR/a.$doc/pod" -maxdepth 1 -name "*.ocda.db" | head -1) if [ ! -f "$db" ]; then echo " FAIL: the run from the pod wrote no database" FAILURES=$((FAILURES + 1)) continue fi # the database says the markup it carries still makes the abstraction it # holds. Checked before the build, because a build from it checks the same # thing and would stop; asked here on its own so that a failure says which # of the two claims broke if ! $SPINE_BIN --ocda-verify="$db" --output="$OUT_DIR/v.$doc" \ > "$OUT_DIR/v.$doc.out" 2> "$OUT_DIR/v.$doc.err"; then echo " FAIL: --ocda-verify on $(basename "$db")" grep -E "no longer|records no" "$OUT_DIR/v.$doc.err" | head -3 | sed 's/^/ /' FAILURES=$((FAILURES + 1)) continue fi verified=$(grep -E "^verify: " "$OUT_DIR/v.$doc.out" | head -1) [ -n "$verified" ] && echo " $verified" # B: from the database if ! $SPINE_BIN $FLAGS --output="$OUT_DIR/b.$doc" "$db" \ > "$OUT_DIR/b.$doc.out" 2> "$OUT_DIR/b.$doc.err"; then echo " FAIL: the run from the database exited non-zero" tail -3 "$OUT_DIR/b.$doc.err" | sed 's/^/ /' FAILURES=$((FAILURES + 1)) continue fi # and the whole of it, held against the whole of it if diff -rq "$OUT_DIR/a.$doc" "$OUT_DIR/b.$doc" > "$OUT_DIR/d.$doc" 2>&1; then n=$(find "$OUT_DIR/b.$doc" -type f | wc -l) echo " ok: $n files, no differences" DOCS_DONE=$((DOCS_DONE + 1)) else echo " FAIL: $(grep -c . "$OUT_DIR/d.$doc") difference(s)" head -8 "$OUT_DIR/d.$doc" | sed 's/^/ /' FAILURES=$((FAILURES + 1)) fi done rm -rf "$OUT_DIR" if [ "$FAILURES" -eq 0 ]; then echo "PASS: $DOCS_DONE document(s) build identically from their database" exit 0 else echo "FAIL: $FAILURES document(s) do not" exit 1 fi #+END_SRC ** 8. test epub validity #+HEADER: :tangle ../test/test-epub-validity.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # epubcheck over the sample set, as a regression guard. # # Every epub spine writes is checked with epubcheck. A *fatal* is never # acceptable: it means a reader cannot parse the file at all. Errors are # compared against a recorded allowance, so that the ones known to be # outstanding do not fail the run while any new one does. # # The allowance is deliberately a single number rather than a list of # messages: a list invites adding to it. When the number goes down, lower it # here in the same commit that lowers it in the output. # # Usage: # SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod \ # ./test/test-epub-validity.sh ./bin/spine-ldc # # Exit codes: # 0 no fatals, and errors within the allowance # 1 a fatal, or more errors than allowed # 2 set-up problem (no $SpinePOD, no binary, no epubcheck, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # What is known to be outstanding over the whole sample set. Both numbers are # a ceiling, so a regression fails and an improvement does not; lower them in # the commit that lowers the output, and say in that commit what went. # # There is no allowance for a fatal, and there must never be one. The last # one - the_public_domain, chapter_2: a bare url ending a footnote body # swallowed the "}~" close delimiter, so the note closed inside the link # markup and left an unbalanced behind - was a url boundary defect in # the abstraction, and is fixed. # # No error is allowed either, and none is outstanding. Both numbers being # zero is the point: anything at all that epubcheck calls an error is a # regression, and there is no list to add to. # # No warning is outstanding either, though warnings are not counted here. FATALS_ALLOWED=${SpineEpubFatalsAllowed:-0} ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-0} if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 echo " e.g. SpinePOD=../../markup/sisudoc-spine-samples/markup/pod-samples/pod" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/$SpinePOD" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else SAMPLES_DIR="$SAMPLES_RAW" fi SPINE_BIN="${1:-}" # find spine binary if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi if [ ! -d "$SAMPLES_DIR" ]; then echo "ERROR: samples not found: $SAMPLES_DIR" >&2 exit 2 fi if ! command -v epubcheck >/dev/null 2>&1; then echo "ERROR: epubcheck not on PATH." >&2 echo " e.g. nix shell \"nixpkgs#epubcheck\" -c $0 $SPINE_BIN" >&2 exit 2 fi echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" OUT_DIR="$(mktemp -d)" trap 'rm -rf "$OUT_DIR"' EXIT echo "Generating epubs ..." "$SPINE_BIN" --epub --output="$OUT_DIR" "$SAMPLES_DIR"/* 2>&1 | tail -1 FATALS=0 ERRORS=0 DOCS=0 for f in $(find "$OUT_DIR" -name '*.epub' | sort); do DOCS=$((DOCS + 1)) msg=$(epubcheck "$f" 2>&1 | grep -E '^Messages:' || true) ff=$(echo "$msg" | sed -n 's/.*: \([0-9]*\) fatal.*/\1/p') ee=$(echo "$msg" | sed -n 's/.*\/ \([0-9]*\) error.*/\1/p') ff=${ff:-0}; ee=${ee:-0} FATALS=$((FATALS + ff)) ERRORS=$((ERRORS + ee)) if [ "$ff" -gt 0 ]; then echo " FATAL in $(basename "$f"): $ff" epubcheck "$f" 2>&1 | grep -i 'FATAL' | head -3 | sed 's/^/ /' fi done echo "checked $DOCS epubs: $FATALS fatal (allowed $FATALS_ALLOWED), $ERRORS errors (allowed $ERRORS_ALLOWED)" if [ "$FATALS" -gt "$FATALS_ALLOWED" ]; then echo "FAIL: $FATALS fatal(s), more than the $FATALS_ALLOWED allowed;" echo " a fatal means a reader cannot parse that file at all" exit 1 fi if [ "$ERRORS" -gt "$ERRORS_ALLOWED" ]; then echo "FAIL: $ERRORS errors, more than the $ERRORS_ALLOWED allowed" echo " if this is a deliberate change, say why and adjust ERRORS_ALLOWED" exit 1 fi echo "PASS: $FATALS fatal and $ERRORS errors, both within what is recorded" exit 0 #+END_SRC * collection search db + cgi form ** test search db schema #+HEADER: :tangle ../test/test-search-db-schema.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-search-db-schema.sh # # Schema contract between spine's --sqlite-update output and the search form # in sisudoc-spine-search-cgi. # # Spine holds the only copy of the DDL; the search form holds no DDL at all, # only a small set of column references. Nothing connects the two, so a # column renamed in spine is found by whoever runs a search next. This is the # check that finds it earlier: build a search database, then read the column # references out of the search form's source and require every one of them to # exist in what was built. # # It also requires the three-way agreement on schema version: the version # spine declares, the version stamped into the database it built, and the # version the search form expects. # # Usage: # ./test/test-search-db-schema.sh [/path/to/spine] # # Environment: # SpinePOD pod directory to build the db from, relative to the # spine directory (default: data/pod, the bundled set) # SpineSearchCGIsrc path to the search cgi source file or its repository # (default: ../sisudoc-spine-search-cgi) # # sqlite3 is needed. If it is not on PATH the script re-runs itself inside a # nix shell that has it. # # Exit codes: # 0 spine's database satisfies every expectation the search form has # 1 a mismatch was found (printed) # 2 set-up problem (no binary, no cgi source, no sqlite3, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" if ! command -v sqlite3 > /dev/null 2>&1; then if command -v nix > /dev/null 2>&1; then echo "sqlite3 not on PATH, re-running inside nix shell \"nixpkgs#sqlite\"" exec nix shell "nixpkgs#sqlite" -c "$0" "$@" fi echo "ERROR: sqlite3 not on PATH and nix not available." >&2 exit 2 fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as argument." >&2 exit 2 fi fi # the search form's source, the other half of the contract CGI_SRC="${SpineSearchCGIsrc:-$SPINE_DIR/../sisudoc-spine-search-cgi}" if [ -d "$CGI_SRC" ]; then CGI_SRC="$CGI_SRC/src/sisudoc/spine_search.d" fi if [ ! -f "$CGI_SRC" ]; then echo "ERROR: search cgi source not found at $CGI_SRC" >&2 echo " set \$SpineSearchCGIsrc to the sisudoc-spine-search-cgi checkout" >&2 exit 2 fi SAMPLES_RAW="$SPINE_DIR/${SpinePOD:-data/pod}" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else echo "ERROR: sample documents not found at $SAMPLES_RAW" >&2 exit 2 fi OUT_DIR="$SCRIPT_DIR/current-search-db" DB_DIR="$OUT_DIR/sqlite-database" DB_NAME="spine.search.db" DB="$DB_DIR/$DB_NAME" echo "spine binary: $SPINE_BIN" echo "search cgi: $CGI_SRC" echo "samples: $SAMPLES_DIR" rm -rf "$OUT_DIR" mkdir -p "$DB_DIR" echo "Building search database ..." "$SPINE_BIN" --sqlite-db-recreate \ --sqlite-db-path="$DB_DIR" --sqlite-db-filename="$DB_NAME" > /dev/null # the update has to be able to fail the test: spine exits non-zero when any # sqlite statement failed, and that is exactly the case this test is here for if ! "$SPINE_BIN" --sqlite-update \ --sqlite-db-path="$DB_DIR" --sqlite-db-filename="$DB_NAME" \ --output="$OUT_DIR" "$SAMPLES_DIR"/* > "$OUT_DIR/build.log" 2>&1; then echo "FAIL: spine --sqlite-update reported failure" grep "ERROR SQLite\|run FAILED" "$OUT_DIR/build.log" | head -20 exit 1 fi FAILURES=0 fail() { echo " MISMATCH $*"; FAILURES=$((FAILURES + 1)); } # ── the database was built at all if [ ! -f "$DB" ]; then echo "FAIL: no database at $DB" exit 1 fi # ── the tables the search form joins echo "Checking tables ..." for t in metadata_and_text doc_objects topic_register; do found=$(sqlite3 "$DB" \ "select count(*) from sqlite_master where type='table' and name='$t';") [ "$found" = "1" ] || fail "table $t missing" done # ── the tables are populated, an empty db satisfies a column check trivially docs=$(sqlite3 "$DB" "select count(*) from metadata_and_text;" 2>/dev/null || echo 0) objs=$(sqlite3 "$DB" "select count(*) from doc_objects;" 2>/dev/null || echo 0) echo " documents: $docs, objects: $objs" [ "$docs" -gt 0 ] || fail "metadata_and_text is empty" [ "$objs" -gt 0 ] || fail "doc_objects is empty" # ── schema version: spine's declaration, the stamp in the db, the search # form's expectation; all three have to agree echo "Checking schema version ..." SPINE_SQL_SRC="$SPINE_DIR/src/sisudoc/outputs/io_out/sqlite_collection_db.d" declared=$(sed -n 's/^enum sqlite_db_schema_version *= *\([0-9][0-9]*\);.*/\1/p' \ "$SPINE_SQL_SRC" | head -1) stamped=$(sqlite3 "$DB" "pragma user_version;") expected=$(sed -n 's/^enum sqlite_db_schema_version_expected *= *\([0-9][0-9]*\);.*/\1/p' \ "$CGI_SRC" | head -1) echo " spine declares: ${declared:-unset}, db stamped: ${stamped:-unset}, cgi expects: ${expected:-unset}" if [ -z "$declared" ]; then fail "spine declares no sqlite_db_schema_version in $SPINE_SQL_SRC" elif [ "$stamped" != "$declared" ]; then fail "db stamped user_version $stamped, spine declares $declared" fi if [ -z "$expected" ]; then fail "search form declares no sqlite_db_schema_version_expected" elif [ -n "$declared" ] && [ "$expected" != "$declared" ]; then fail "search form expects schema version $expected, spine writes $declared" fi # ── every column the search form names has to exist # the references are qualified, table.column, which is what makes them # findable without parsing D echo "Checking columns named by the search form ..." COLS=$(grep -o \ 'metadata_and_text\.[a-z_]*\|doc_objects\.[a-z_]*\|topic_register\.[a-z_]*' \ "$CGI_SRC" | sort -u) if [ -z "$COLS" ]; then fail "no column references found in $CGI_SRC (has the source changed shape?)" fi count=0 for ref in $COLS; do table=${ref%%.*} column=${ref#*.} [ -n "$column" ] || continue count=$((count + 1)) found=$(sqlite3 "$DB" \ "select count(*) from pragma_table_info('$table') where name='$column';") [ "$found" = "1" ] || fail "$table.$column named by the search form, absent from the db" done echo " column references checked: $count" echo "" if [ "$FAILURES" -eq 0 ]; then echo "PASS: spine's search database satisfies the search form's expectations" exit 0 else echo "FAIL: $FAILURES mismatch(es)" exit 1 fi #+END_SRC ** test search cgi #+HEADER: :tangle ../test/test-search-cgi.sh #+HEADER: :tangle-mode (identity #o755) #+HEADER: :shebang "#!/usr/bin/env sh" #+BEGIN_SRC shell # test-search-cgi.sh # # End to end check that the search cgi works against a database spine has # just built. # # The schema check (test-search-db-schema.sh) compares names. This one runs # the real binary against the real database, which is the only thing that # tests the query spine's schema actually has to satisfy: the joins, the # ordering, the types, and the fields the form fills in from a request. # # No web server is involved. The cgi binary is invoked as CGI, with the # request in the environment, which is what a web server would do. # # Usage: # ./test/test-search-cgi.sh [/path/to/spine] [/path/to/spine_search] # # Environment: # SpinePOD pod directory to build the db from, relative to the # spine directory (default: data/pod, the bundled set) # SpineSearchCGIbin the built search cgi # (default: ../sisudoc-spine-search-cgi/result/cgi-bin/spine_search, # then .../cgi-bin/spine-search-ldc.cgi) # SpineSearchDB an existing search database to test against; when set, # nothing is built and this database is used as is # # The cgi is pointed at the database with SPINE_SEARCH_DB_PATH, so the binary # under test needs no rebuild and its compiled-in path is left alone. # # Exit codes: # 0 every request behaved # 1 a request did not (printed) # 2 set-up problem (no binary, no cgi, no samples) set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # sqlite3 is not required here: the parts that use it fall back to fixed probe # values and to skipping the schema version gate. Both are checks, though, and # a run that quietly leaves them out still says PASS, so where sqlite3 is # absent and nix is to hand, re-run inside a nix shell that has it. With # neither, the test runs as before and says what it skipped. if ! command -v sqlite3 > /dev/null 2>&1; then if command -v nix > /dev/null 2>&1; then echo "sqlite3 not on PATH, re-running inside nix shell \"nixpkgs#sqlite\"" exec nix shell "nixpkgs#sqlite" -c "$0" "$@" fi fi SPINE_BIN="${1:-}" if [ -z "$SPINE_BIN" ]; then if [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine" elif [ -x "$SPINE_DIR/bin/spine-ldc" ]; then SPINE_BIN="$SPINE_DIR/bin/spine-ldc" elif [ -x "$SPINE_DIR/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/bin/spine" else echo "ERROR: spine binary not found. Specify path as first argument." >&2 exit 2 fi fi CGI_BIN="${2:-${SpineSearchCGIbin:-}}" if [ -z "$CGI_BIN" ]; then CGI_REPO="$SPINE_DIR/../sisudoc-spine-search-cgi" for c in \ "$CGI_REPO/result/cgi-bin/spine_search" \ "$CGI_REPO/cgi-bin/spine-search-ldc.cgi" \ "$CGI_REPO/cgi-bin/spine_search" do [ -x "$c" ] && CGI_BIN="$c" && break done fi if [ -z "$CGI_BIN" ] || [ ! -x "$CGI_BIN" ]; then echo "ERROR: search cgi binary not found." >&2 echo " build it: cd ../sisudoc-spine-search-cgi && nix build \".#spine-search-overlay-ldc\"" >&2 echo " or set \$SpineSearchCGIbin / pass it as the second argument" >&2 exit 2 fi OUT_DIR="$SCRIPT_DIR/current-search-cgi" DB_NAME="spine.search.db" if [ -n "$SpineSearchDB" ]; then DB_DIR="$(cd "$(dirname "$SpineSearchDB")" && pwd)" DB_NAME="$(basename "$SpineSearchDB")" echo "spine binary: (not used, \$SpineSearchDB set)" else SAMPLES_RAW="$SPINE_DIR/${SpinePOD:-data/pod}" if [ -d "$SAMPLES_RAW" ]; then SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)" else echo "ERROR: sample documents not found at $SAMPLES_RAW" >&2 exit 2 fi DB_DIR="$OUT_DIR/sqlite-database" echo "spine binary: $SPINE_BIN" echo "samples: $SAMPLES_DIR" rm -rf "$OUT_DIR" mkdir -p "$DB_DIR" echo "Building search database ..." "$SPINE_BIN" --sqlite-db-recreate \ --sqlite-db-path="$DB_DIR" --sqlite-db-filename="$DB_NAME" > /dev/null if ! "$SPINE_BIN" --sqlite-update \ --sqlite-db-path="$DB_DIR" --sqlite-db-filename="$DB_NAME" \ --output="$OUT_DIR" "$SAMPLES_DIR"/* > "$OUT_DIR/build.log" 2>&1; then echo "FAIL: spine --sqlite-update reported failure" grep "ERROR SQLite\|run FAILED" "$OUT_DIR/build.log" | head -20 exit 1 fi fi echo "search cgi: $CGI_BIN" echo "database: $DB_DIR/$DB_NAME" mkdir -p "$OUT_DIR" RESP="$OUT_DIR/response.html" FAILURES=0 # one GET request, as a web server would present it request() { SPINE_SEARCH_DB_PATH="$DB_DIR" \ REQUEST_METHOD=GET \ SCRIPT_NAME=/spine_search \ SERVER_NAME=localhost \ SERVER_PORT=80 \ REQUEST_SCHEME=http \ GATEWAY_INTERFACE=CGI/1.1 \ SERVER_PROTOCOL=HTTP/1.1 \ QUERY_STRING="$1" \ "$CGI_BIN" > "$RESP" 2>"$OUT_DIR/response.err" || true } # a request that has to answer 200, name no sql error, and find documents expect_hits() { _query="$1" _what="$2" request "$_query" if ! head -1 "$RESP" | grep -q "200 OK"; then echo " FAIL $_what: no 200 status ($(head -1 "$RESP"))" FAILURES=$((FAILURES + 1)) return fi if grep -qi "SqliteException\|no such column\|no such table" "$RESP" "$OUT_DIR/response.err"; then echo " FAIL $_what: sql error" grep -i "SqliteException\|no such column\|no such table" "$RESP" "$OUT_DIR/response.err" | head -3 FAILURES=$((FAILURES + 1)) return fi _hits=$(grep -o 'class="publication"' "$RESP" | wc -l) _hits=$((_hits / 2)) if [ "$_hits" -lt 1 ]; then echo " FAIL $_what: no results" FAILURES=$((FAILURES + 1)) return fi echo " ok $_what ($_hits document(s))" } # a request that has to answer 200 and say the named thing expect_says() { _query="$1" _text="$2" _what="$3" request "$_query" if ! head -1 "$RESP" | grep -q "200 OK"; then echo " FAIL $_what: no 200 status ($(head -1 "$RESP"))" FAILURES=$((FAILURES + 1)) return fi if ! grep -q "$_text" "$RESP"; then echo " FAIL $_what: response does not say \"$_text\"" FAILURES=$((FAILURES + 1)) return fi echo " ok $_what" } # the queries below name a different column of the schema each, so between # them they exercise the whole of what the search form selects on. The values # searched for are read out of the database itself where sqlite3 is present, # so that the test says something whichever corpus it is pointed at; the # fallbacks are words the bundled data/pod documents contain. Every field is # matched with LIKE '%value%', so a single safe word out of a longer value # still matches the row it came from. PROBE_TEXT="document" PROBE_AUTHOR="Amissah" PROBE_TITLE="markup" PROBE_FN="sisu-spine-markup" PROBE_UID="markup" PROBE_LNG="en" if command -v sqlite3 > /dev/null 2>&1; then # first word of at least four letters or digits, nothing else: the values in # the database carry punctuation, markup and newlines, none of which belong # in a query string _word() { printf '%s' "$1" | tr -c 'A-Za-z0-9' ' ' \ | awk '{for (i = 1; i <= NF; i++) if (length($i) >= 4) { print $i; exit } }' } _ask() { sqlite3 "$DB_DIR/$DB_NAME" "$1" 2>/dev/null | head -1 || true; } _v=$(_word "$(_ask "select clean from doc_objects where length(clean) > 60 limit 1;")") [ -n "$_v" ] && PROBE_TEXT="$_v" _v=$(_word "$(_ask "select creator_author from metadata_and_text where creator_author != '' limit 1;")") [ -n "$_v" ] && PROBE_AUTHOR="$_v" _v=$(_word "$(_ask "select title from metadata_and_text where title != '' limit 1;")") [ -n "$_v" ] && PROBE_TITLE="$_v" _v=$(_word "$(_ask "select src_filename_base from metadata_and_text limit 1;")") [ -n "$_v" ] && PROBE_FN="$_v" _v=$(_word "$(_ask "select uid from metadata_and_text limit 1;")") [ -n "$_v" ] && PROBE_UID="$_v" _v=$(_ask "select language_document_char from metadata_and_text limit 1;") case "$_v" in [a-z][a-z]) PROBE_LNG="$_v" ;; esac fi echo "Requests (text \"$PROBE_TEXT\", author \"$PROBE_AUTHOR\", title \"$PROBE_TITLE\"):" expect_hits "sf=$PROBE_TEXT&rt=idx&sml=1000" "text, index results" expect_hits "sf=$PROBE_TEXT&rt=txt&sml=1000" "text, text results" expect_hits "sf=$PROBE_TEXT%20AND%20$PROBE_TEXT&rt=idx&sml=1000" "text, boolean AND" expect_hits "sf=$PROBE_TEXT%20OR%20zzzznotfound&rt=idx&sml=1000" "text, boolean OR" expect_hits "au=$PROBE_AUTHOR&sf=$PROBE_TEXT&rt=idx&sml=1000" "creator_author" expect_hits "ti=$PROBE_TITLE&sf=$PROBE_TEXT&rt=idx&sml=1000" "title" expect_hits "lng=$PROBE_LNG&sf=$PROBE_TEXT&rt=idx&sml=1000" "language_document_char" expect_hits "fn=$PROBE_FN&sf=$PROBE_TEXT&rt=idx&sml=1000" "src_filename_base" expect_hits "uid=$PROBE_UID&sf=$PROBE_TEXT&rt=idx&sml=1000" "uid" # the failure modes a database change would show up as, each of which used to # be a blank page, a 500, or a silently created empty file echo "Refusals:" expect_says 'sf=document&rt=idx&selected_db=no_such_file.db' \ "search database not found" "a database that is not there is named, not created" if [ -e "$DB_DIR/no_such_file.db" ]; then echo " FAIL a missing database was created by the request" FAILURES=$((FAILURES + 1)) fi expect_hits "sf=$PROBE_TEXT&rt=idx&sml=1000&selected_db=../../../../etc/passwd" \ "a path in selected_db falls back to the configured database" # the schema version gate, end to end: a database stamped with a version the # search form does not expect has to be named as such, not searched. Needs # sqlite3 to do the stamping, so it is skipped when sqlite3 is absent. if command -v sqlite3 > /dev/null 2>&1; then echo "Schema version gate:" cp "$DB_DIR/$DB_NAME" "$DB_DIR/wrong_version.db" sqlite3 "$DB_DIR/wrong_version.db" "pragma user_version = 99;" expect_says 'sf=document&rt=idx&selected_db=wrong_version.db' \ "schema version" "a database of another schema version is refused, with the version named" rm -f "$DB_DIR/wrong_version.db" else echo "Schema version gate: skipped, sqlite3 not on PATH" fi echo "" if [ "$FAILURES" -eq 0 ]; then echo "PASS: the search cgi works against this database" exit 0 else echo "FAIL: $FAILURES request(s) misbehaved" exit 1 fi #+END_SRC