diff options
| -rw-r--r-- | org/test_shell_script_ssp_document_abstraction.org | 202 | ||||
| -rw-r--r-- | org/tests_for_document_abstraction_shell_scripts.org | 438 | ||||
| -rwxr-xr-x | test/test-abstraction-db.sh | 228 |
3 files changed, 666 insertions, 202 deletions
diff --git a/org/test_shell_script_ssp_document_abstraction.org b/org/test_shell_script_ssp_document_abstraction.org deleted file mode 100644 index ccbe6dd..0000000 --- a/org/test_shell_script_ssp_document_abstraction.org +++ /dev/null @@ -1,202 +0,0 @@ --*- 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 - -* test shell script ssp document 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" -$SPINE_BIN --abstraction --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 diff --git a/org/tests_for_document_abstraction_shell_scripts.org b/org/tests_for_document_abstraction_shell_scripts.org new file mode 100644 index 0000000..df02509 --- /dev/null +++ b/org/tests_for_document_abstraction_shell_scripts.org @@ -0,0 +1,438 @@ +-*- 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 + +* test shell scripts document abstraction +** ssp document 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" +$SPINE_BIN --abstraction --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 + +** db document abstraction 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 <doc>.abstraction.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 .abstraction.db ..." +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" +$SPINE_BIN --show-abstraction --show-abstraction-db --skip-output \ + --output="$OUT_DIR" "$SAMPLES_DIR"/* 2>&1 | tail -1 + +FAILURES=0 +DOCS=0 + +# ssp property -> sqlite predicate counting the same thing +# "<property>|<predicate>" +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_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"; } + +for ssp in "$OUT_DIR"/*/abstraction/*.ssp; do + [ -f "$ssp" ] || continue + db="${ssp%.ssp}.abstraction.db" + base=$(basename "$ssp" .ssp) + if [ ! -f "$db" ]; then + echo "MISSING db for $base" + FAILURES=$((FAILURES + 1)) + continue + fi + DOCS=$((DOCS + 1)) + doc_fail=0 + + # object population + s_objs=$(grep -c '^\[' "$ssp" || true) + d_objs=$(sqlite3 "$db" "select count(*) from objects;" 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 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 $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;" 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 + want=$(sqlite3 "$db" "select count(distinct name) from object_images where missing = 0;" 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);" 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 ocn > 0 and section='body';" 2>/dev/null || echo "") + [ -z "$ocn" ] && continue + sqlite3 "$db" "select text from objects where 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 file says what it is + sv=$(sqlite3 "$db" "select value from metadata where 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 + fi + + FAILURES=$((FAILURES + doc_fail)) +done + +rm -rf "$OUT_DIR" + +if [ "$FAILURES" -eq 0 ]; then + echo "PASS: .ssp and .abstraction.db agree for all $DOCS documents" + exit 0 +else + echo "FAIL: $FAILURES document(s) disagree" + exit 1 +fi +#+END_SRC diff --git a/test/test-abstraction-db.sh b/test/test-abstraction-db.sh new file mode 100755 index 0000000..91748a3 --- /dev/null +++ b/test/test-abstraction-db.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env sh +# test-abstraction-db.sh +# +# Cross-check for spine's two serialisations of the document abstraction: +# the .ssp text file and the <doc>.abstraction.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 .abstraction.db ..." +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" +$SPINE_BIN --show-abstraction --show-abstraction-db --skip-output \ + --output="$OUT_DIR" "$SAMPLES_DIR"/* 2>&1 | tail -1 + +FAILURES=0 +DOCS=0 + +# ssp property -> sqlite predicate counting the same thing +# "<property>|<predicate>" +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_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"; } + +for ssp in "$OUT_DIR"/*/abstraction/*.ssp; do + [ -f "$ssp" ] || continue + db="${ssp%.ssp}.abstraction.db" + base=$(basename "$ssp" .ssp) + if [ ! -f "$db" ]; then + echo "MISSING db for $base" + FAILURES=$((FAILURES + 1)) + continue + fi + DOCS=$((DOCS + 1)) + doc_fail=0 + + # object population + s_objs=$(grep -c '^\[' "$ssp" || true) + d_objs=$(sqlite3 "$db" "select count(*) from objects;" 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 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 $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;" 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 + want=$(sqlite3 "$db" "select count(distinct name) from object_images where missing = 0;" 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);" 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 ocn > 0 and section='body';" 2>/dev/null || echo "") + [ -z "$ocn" ] && continue + sqlite3 "$db" "select text from objects where 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 file says what it is + sv=$(sqlite3 "$db" "select value from metadata where 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 + fi + + FAILURES=$((FAILURES + doc_fail)) +done + +rm -rf "$OUT_DIR" + +if [ "$FAILURES" -eq 0 ]; then + echo "PASS: .ssp and .abstraction.db agree for all $DOCS documents" + exit 0 +else + echo "FAIL: $FAILURES document(s) disagree" + exit 1 +fi |
