# ODF validation harness This harness has OpenOffice save a corpus of documents as ODF, checks every saved package against the OASIS ODF 1.2, ODF 1.3 and ODF 1.4 RELAX NG schemas with jing, and reports the errors grouped by kind. Comparing those reports against a baseline shows whether an export change fixed errors or introduced new ones. It runs on macOS only (zsh scripts, `hdiutil`, `.app` layout). ## Requirements - An AOO macOS build (`.dmg`). - The Python that the office's pyuno links against. Check with `otool -L /Contents/program/libpyuno.dylib`, then set `ODFV_PYTHON`. - `jing.jar` (set `ODFV_JING`) and `java`; `ODFV_ODFVALIDATOR` and `ODFV_JAVA` for `crosscheck.py`. - `gtimeout` (GNU coreutils), used by `drive.sh`. Defaults for `ODFV_APP`, `ODFV_PYTHON`, `ODFV_JING` and `ODFV_SOLVER` are in `env.sh`. ## Setup ./fetch-schemas.sh # OASIS schemas, checksum-verified, into schema/ ./setup-app.sh # copies OpenOffice.app into app/ ./fetch-odfvalidator.sh # optional: odfvalidator for crosscheck.py python3 -I selftest.py # validator negative controls The schemas are not committed because they carry OASIS copyright terms. ## Running a corpus ./make-jobs.sh runs/base_strict/out > runs/base_strict/jobs.tsv ./drive.sh strict 2013 runs/base_strict/jobs.tsv runs/base_strict/done.tsv python3 -I validate.py runs/base_strict/out runs/base_strict/val.json python3 -I report.py runs/base_strict/val.json baseline/strict.kinds.json 12 The steps: - `make-jobs.sh` collects the test documents under `main/` (doc, docx, rtf, xls, ppt, sx*, od*). - `drive.sh ` saves them in ODF 1.2 Extended (`DefaultVersion=3`) or strict ODF 1.2 (`DefaultVersion=4`). - It runs a headless office on the given port, using a profile under `runs/`. - It resumes where it left off, kills a hung office and continues, and stops at the first crash. - `validate.py` checks content, styles, meta, settings and manifest streams against 1.2 as written, against 1.3 (`13`) and 1.4 (`14`) with `office:version` rewritten, and against 1.2 and 1.3 as extended documents (`12x`, `13x`): `foreign.py` first removes foreign elements and attributes as Part 1 §2.2.2 B.1 and §3.17 describe (the manifest is left as written). Use `report.py 12x` to measure what is wrong apart from foreign content. Embedded objects whose `content.xml` is MathML are skipped (ODF 1.2 Part 1 §2.2.1 B.4). - `report.py` groups the errors by normalized message. - `pkgcheck.py ` checks the ODF 1.2 Part 3 package rules (§2.2.1, 3.2, 3.3, 3.4.1): zip methods, `mimetype` first / stored / no extra field / equal to the `/` media type, one manifest entry per file, META-INF contents, `manifest:size` and STORED on encrypted entries, and signature files (well-formed, root, `OpenDocument-v1.2-os-dsig-schema.rng`). Findings are `shall:` or `should:`; only `shall` findings are non-conformances. - `doctypecheck.py ` checks Part 1 §2.2.1 A.2/E and §2.2.3-2.2.10: the `mimetype` file (or `office:mimetype` of a flat file), the root element and the `office:body` child must agree on one document type. Embedded sub documents are compared with their manifest media type (`should:` findings). Two deviations from the literal text of §2.2.5: it spells the drawing values `application.vnd.oasis.opendocument.graphics` and `...graphic-template`; the registered `...graphics` and `...graphics-template` of Part 1 appendix C are accepted. `...text-web` and `...formula-template` are registered but no §2.2.x allows them, so they are reported. - `formulacheck.py ` parses the `style:condition`, `table:condition`, `table:expression`, `table:formula` and `text:formula` values (Part 1 §2.2.1 D.3, §2.2.4 D/E) with the OpenFormula grammar (Part 2 chapter 5) and the condition syntaxes of Part 1 §19.468 and §19.596. - Syntax only; no semantics, function arity or type checking. - A prefix bound to another namespace is out of Part 2's reach: `should:` in general, `shall:` for `table:formula` inside `office:spreadsheet` (§2.2.4 D). - Non-ASCII name characters use Unicode categories instead of the XML 1.0 4th edition tables; an IRI is checked by character class, not the full RFC 3987 grammar; `openformula-functions.txt` holds the Part 2 function names. - The condition syntaxes follow the prose of Part 1, which has no grammar: `,` between arguments, lowercase ` and `, `!=` as the relational operator. - `formula-cases.tsv` is the table of good and bad values that `selftest.py` runs. - `crosscheck.py ` runs the Apache ODF Toolkit `odfvalidator` 1.1.8 (`fetch-odfvalidator.sh`; MSV instead of jing, the same OASIS schemas) in conformance mode (`-c`) and extended mode (`-e`) and compares each stream with the `12` and `12x` results of `validate.py`. - Error counts differ between engines; it compares whether a stream has any error and, in `-c` mode, where its first error is. - It exits if odfvalidator stops before finishing every package. Its failures are silent (an invalid schema prints "Validation aborted"; a crash on one package aborts the run): crashes are skipped and listed. - odfvalidator's own schema lookup fails on current JDKs, so the schemas are passed explicitly; its jar has no MathML 2 schema, so a schema that accepts anything stands in (as `validate.py` skips MathML). - Known leniencies: it truncates a `draw:points` over 2048 characters and adapts old OOo namespaces. It is stricter than Part 3 §3.2 in wanting `META-INF/documentsignatures.xml` listed in the manifest. It checks ID uniqueness, which `jing -i` does not. - `fixtures/doctypes/` (`run.sh`, `make.py`, `derive-chart.py`), `fixtures/encrypted/`, `fixtures/signed/`, `fixtures/base/`: documents of the kinds the corpus lacks (templates, master, Writer/Web, Impress as Draw, Math, chart, encrypted, signed, Base), saved in strict and extended mode under `runs/task12/gen-*`. `decrypt.py ` writes plain copies of encrypted packages for the schema checkers; `pkgcheck.py` must still run on the real ones. Signing needs a visible (`-invisible`) office because `-headless` cancels the dialog: see the header of `fixtures/signed/make.py`. - `fixtures/{writer,calc,draw,chart,styles-text,styles-gfx,leak}/make.py`, run with `fixtures/gen-run.sh `: documents built through UNO to exercise the elements, attributes and enumerated values that `cov_report.py` lists as gaps (`leak` classifies the names AOO emits that the schema does not define). Use an absolute ``. - `cov_schema.py`, `cov_source.py`, `cov_corpus.py`, `cov_report.py`: schema-coverage report. The schema universe (ODF 1.2 elements, element/attribute pairs, enumerated values), the names the export code in `main/` can emit (static C++ scan; a lower bound, table-driven names are missed) and what a corpus exercises are joined by namespace URI and local name into the gaps worth generating documents for. Outputs live under `runs/`. - `idcheck.py ` parses every stream namespace-aware and checks `xml:id` syntax and uniqueness per stream (ODF 1.2 Part 1 §2.2.1 D.1, D.2). `jing -i` does not check ID uniqueness. ## The regression gate python3 -I gate.py --prev runs/gate- # everything python3 -I gate.py --build xmloff,sw --prev ... # rebuild first python3 -I gate.py smoke --limit 8 --prev ... --stages corpus,validate,dump,diff `gate.py` runs the stages below in `runs/gate-/`, records `results.json`, skips stages already done (`--force` reruns) and exits 0 only if everything that ran passed. | stage | does | passes if | |---|---|---| | `build` | `build.pl` and `deliver.pl` for the `--build` modules (environment script: `ODFV_BUILD_ENV`) | exit codes 0 | | `refresh` | copies `--libs` from the solver into `app/` (`refresh-app.sh`) | copies identical | | `corpus` | saves the job list in `ext` and `strict` mode | no crash, every job accounted for, OK/FAIL counts equal `--prev` | | `validate` | `validate.py`, `report.py` and the four checkers per mode | kinds not worse than `baseline/.kinds.json` or than `--prev` (checked both ways); all four checkers ran; no `shall` finding from the id, package and document-type checkers; formula findings not above `--prev`; a baseline or `--prev` exists | | `dump` | reloads every saved package with `dump7.py` | no office failure; hangs and crashes are listed | | `diff` | `diff7.py` on the dumps and `cmp7.py` on the packages against `--prev` | every difference is a document in `gate-volatile.tsv` | | `fixtures` | `gate-fixtures.tsv`: saves in `v00 v10 v11 ext strict`, runs the reload checks, validates | saved, checked, and unchanged against `--prev`; the same checker rules as `validate` (formula findings are compared per mode with `--prev`) | - Inputs are frozen: without `--jobs` the list of `--prev` is reused (outputs are rewritten), so the same documents are saved when files under `main/` change. A later run's `--prev` may be any earlier gate directory or a directory with the same layout (`/out`, `/dump`). - The dump stage kills the office only when the dump makes no progress for `--stall` seconds (default 300), then writes a `HANG` marker for that document alone. A long batch that keeps progressing is never killed. - `gate-volatile.tsv` names the documents whose dumps differ between runs without an export change (volatile Calc formulas, redline timestamps, the `0238` loader order, the intermittent Basic library loss of `0048`). A new entry needs a reason. - The first fixtures run has nothing to compare with: its mismatch pattern is only recorded. By design, strict, ODF 1.0 and ODF 1.1 mode fail the 14 reload rows that need gradient or transparency fills. ## Checking a change python3 -I compare.py baseline/strict.kinds.json runs/new/strict.kinds.json `compare.py` exits 1 if any kind is new or any kind got worse. To test a rebuilt module without repackaging, copy its libraries from the solver into the app copy: ./refresh-app.sh libxo.dylib `roundtrip.py ` loads a document and compares UNO expressions with expected values; `--capture ` records them instead. ## Formula evaluation baseline (calcval) ./start.sh calcval 2792 ./runpy.sh calcval.py 2792 calcval-cases.tsv runs/phase4/baseline.json python3 -I calcval.py --compare runs/phase4/baseline.json runs/phase4/new.json `calcval.py` opens a new Calc document, writes the data area described in the header of `calcval-cases.tsv`, and for each case enters the formula in a fresh scratch cell (`cell`, `cell@N`) or range (`array:CxR`), recalculates, records the result type, `getError()`, `getString()`, `getValue()` and the formula as read back, then clears the scratch area. `expected` is the value the ODF 1.3 Part 4 text requires (`-` records the current behaviour only); the status of each case is `match`, `mismatch`, `record` or `exception`. The run prints every mismatch, which are the current deviations from ODF 1.3. `--compare` lists each case whose recorded result changed (exit 1 if any). RECEIVED and BESSELK/BESSELY are Analysis add-ins and are entered by programmatic name. ## Notes - AOO has no `-convert-to`; conversion goes over UNO (`convert.py`). - jing must run with `-i`, otherwise it rejects the ODF schemas themselves. - A jing fatal error can hide later errors. Such lines are reported as `UNPARSED` kinds, and `compare.py` fails on any new `UNPARSED` kind. - jing stops at a fatal error (a stream that is not well-formed); `validate.py` records it and carries on with the remaining files. - `setup-app.sh` renames an existing `app/OpenOffice.app` to `OpenOffice.app.old.` instead of deleting it.