1*0e16a31dSJim Jagielski# ODF validation harness 2*0e16a31dSJim Jagielski 3*0e16a31dSJim JagielskiThis harness has OpenOffice save a corpus of documents as ODF, checks every 4*0e16a31dSJim Jagielskisaved package against the OASIS ODF 1.2, ODF 1.3 and ODF 1.4 RELAX NG schemas with 5*0e16a31dSJim Jagielskijing, and reports the errors grouped by kind. Comparing those reports against 6*0e16a31dSJim Jagielskia baseline shows whether an export change fixed errors or introduced new ones. 7*0e16a31dSJim Jagielski 8*0e16a31dSJim JagielskiIt runs on macOS only (zsh scripts, `hdiutil`, `.app` layout). 9*0e16a31dSJim Jagielski 10*0e16a31dSJim Jagielski## Requirements 11*0e16a31dSJim Jagielski 12*0e16a31dSJim Jagielski- An AOO macOS build (`.dmg`). 13*0e16a31dSJim Jagielski- The Python that the office's pyuno links against. Check with 14*0e16a31dSJim Jagielski `otool -L <app>/Contents/program/libpyuno.dylib`, then set `ODFV_PYTHON`. 15*0e16a31dSJim Jagielski- `jing.jar` (set `ODFV_JING`) and `java`; `ODFV_ODFVALIDATOR` and `ODFV_JAVA` for `crosscheck.py`. 16*0e16a31dSJim Jagielski- `gtimeout` (GNU coreutils), used by `drive.sh`. 17*0e16a31dSJim Jagielski 18*0e16a31dSJim JagielskiDefaults for `ODFV_APP`, `ODFV_PYTHON`, `ODFV_JING` and `ODFV_SOLVER` are in 19*0e16a31dSJim Jagielski`env.sh`. 20*0e16a31dSJim Jagielski 21*0e16a31dSJim Jagielski## Setup 22*0e16a31dSJim Jagielski 23*0e16a31dSJim Jagielski ./fetch-schemas.sh # OASIS schemas, checksum-verified, into schema/ 24*0e16a31dSJim Jagielski ./setup-app.sh <path-to-AOO.dmg> # copies OpenOffice.app into app/ 25*0e16a31dSJim Jagielski ./fetch-odfvalidator.sh # optional: odfvalidator for crosscheck.py 26*0e16a31dSJim Jagielski python3 -I selftest.py # validator negative controls 27*0e16a31dSJim Jagielski 28*0e16a31dSJim JagielskiThe schemas are not committed because they carry OASIS copyright terms. 29*0e16a31dSJim Jagielski 30*0e16a31dSJim Jagielski## Running a corpus 31*0e16a31dSJim Jagielski 32*0e16a31dSJim Jagielski ./make-jobs.sh runs/base_strict/out > runs/base_strict/jobs.tsv 33*0e16a31dSJim Jagielski ./drive.sh strict 2013 runs/base_strict/jobs.tsv runs/base_strict/done.tsv 34*0e16a31dSJim Jagielski python3 -I validate.py runs/base_strict/out runs/base_strict/val.json 35*0e16a31dSJim Jagielski python3 -I report.py runs/base_strict/val.json baseline/strict.kinds.json 12 36*0e16a31dSJim Jagielski 37*0e16a31dSJim JagielskiThe steps: 38*0e16a31dSJim Jagielski- `make-jobs.sh` collects the test documents under `main/` (doc, docx, rtf, 39*0e16a31dSJim Jagielski xls, ppt, sx*, od*). 40*0e16a31dSJim Jagielski- `drive.sh <ext|strict> <port> <jobs> <done>` saves them in ODF 41*0e16a31dSJim Jagielski 1.2 Extended (`DefaultVersion=3`) or strict ODF 1.2 (`DefaultVersion=4`). 42*0e16a31dSJim Jagielski - It runs a headless office on the given port, using a profile under `runs/`. 43*0e16a31dSJim Jagielski - It resumes where it left off, kills a hung office and continues, and 44*0e16a31dSJim Jagielski stops at the first crash. 45*0e16a31dSJim Jagielski- `validate.py` checks content, styles, meta, settings and manifest streams 46*0e16a31dSJim Jagielski against 1.2 as written, against 1.3 (`13`) and 1.4 (`14`) with 47*0e16a31dSJim Jagielski `office:version` rewritten, and against 1.2 and 1.3 as extended documents 48*0e16a31dSJim Jagielski (`12x`, `13x`): `foreign.py` first removes 49*0e16a31dSJim Jagielski foreign elements and attributes as Part 1 §2.2.2 B.1 and §3.17 describe (the 50*0e16a31dSJim Jagielski manifest is left as written). Use `report.py <val.json> <kinds.json> 12x` to 51*0e16a31dSJim Jagielski measure what is wrong apart from foreign content. 52*0e16a31dSJim Jagielski Embedded objects whose `content.xml` is MathML are skipped (ODF 1.2 Part 1 53*0e16a31dSJim Jagielski §2.2.1 B.4). 54*0e16a31dSJim Jagielski- `report.py` groups the errors by normalized message. 55*0e16a31dSJim Jagielski- `pkgcheck.py <outdir> <pkg.json>` checks the ODF 1.2 Part 3 package rules 56*0e16a31dSJim Jagielski (§2.2.1, 3.2, 3.3, 3.4.1): zip methods, `mimetype` first / stored / no extra 57*0e16a31dSJim Jagielski field / equal to the `/` media type, one manifest entry per file, META-INF 58*0e16a31dSJim Jagielski contents, `manifest:size` and STORED on encrypted entries, and signature 59*0e16a31dSJim Jagielski files (well-formed, root, `OpenDocument-v1.2-os-dsig-schema.rng`). Findings 60*0e16a31dSJim Jagielski are `shall:` or `should:`; only `shall` findings are non-conformances. 61*0e16a31dSJim Jagielski- `doctypecheck.py <outdir> <dt.json>` checks Part 1 §2.2.1 A.2/E and 62*0e16a31dSJim Jagielski §2.2.3-2.2.10: the `mimetype` file (or `office:mimetype` of a flat file), the 63*0e16a31dSJim Jagielski root element and the `office:body` child must agree on one document type. 64*0e16a31dSJim Jagielski Embedded sub documents are compared with their manifest media type 65*0e16a31dSJim Jagielski (`should:` findings). Two deviations from the literal text of §2.2.5: it 66*0e16a31dSJim Jagielski spells the drawing values `application.vnd.oasis.opendocument.graphics` and 67*0e16a31dSJim Jagielski `...graphic-template`; the registered `...graphics` and `...graphics-template` 68*0e16a31dSJim Jagielski of Part 1 appendix C are accepted. `...text-web` and `...formula-template` 69*0e16a31dSJim Jagielski are registered but no §2.2.x allows them, so they are reported. 70*0e16a31dSJim Jagielski- `formulacheck.py <outdir> <formulas.json>` parses the `style:condition`, 71*0e16a31dSJim Jagielski `table:condition`, `table:expression`, `table:formula` and `text:formula` 72*0e16a31dSJim Jagielski values (Part 1 §2.2.1 D.3, §2.2.4 D/E) with the OpenFormula grammar (Part 2 73*0e16a31dSJim Jagielski chapter 5) and the condition syntaxes of Part 1 §19.468 and §19.596. 74*0e16a31dSJim Jagielski - Syntax only; no semantics, function arity or type checking. 75*0e16a31dSJim Jagielski - A prefix bound to another namespace is out of Part 2's reach: `should:` in 76*0e16a31dSJim Jagielski general, `shall:` for `table:formula` inside `office:spreadsheet` (§2.2.4 D). 77*0e16a31dSJim Jagielski - Non-ASCII name characters use Unicode categories instead of the XML 1.0 78*0e16a31dSJim Jagielski 4th edition tables; an IRI is checked by character class, not the full 79*0e16a31dSJim Jagielski RFC 3987 grammar; `openformula-functions.txt` holds the Part 2 function 80*0e16a31dSJim Jagielski names. 81*0e16a31dSJim Jagielski - The condition syntaxes follow the prose of Part 1, which has no grammar: 82*0e16a31dSJim Jagielski `,` between arguments, lowercase ` and `, `!=` as the relational operator. 83*0e16a31dSJim Jagielski - `formula-cases.tsv` is the table of good and bad values that `selftest.py` 84*0e16a31dSJim Jagielski runs. 85*0e16a31dSJim Jagielski- `crosscheck.py <outdir> <val.json> <cross.json>` runs the Apache ODF Toolkit 86*0e16a31dSJim Jagielski `odfvalidator` 1.1.8 (`fetch-odfvalidator.sh`; MSV instead of jing, the same 87*0e16a31dSJim Jagielski OASIS schemas) in conformance mode (`-c`) and extended mode (`-e`) and 88*0e16a31dSJim Jagielski compares each stream with the `12` and `12x` results of `validate.py`. 89*0e16a31dSJim Jagielski - Error counts differ between engines; it compares whether a stream has 90*0e16a31dSJim Jagielski any error and, in `-c` mode, where its first error is. 91*0e16a31dSJim Jagielski - It exits if odfvalidator stops before finishing every package. Its 92*0e16a31dSJim Jagielski failures are silent (an invalid schema prints "Validation aborted"; a 93*0e16a31dSJim Jagielski crash on one package aborts the run): crashes are skipped and listed. 94*0e16a31dSJim Jagielski - odfvalidator's own schema lookup fails on current JDKs, so the schemas are 95*0e16a31dSJim Jagielski passed explicitly; its jar has no MathML 2 schema, so a schema that accepts 96*0e16a31dSJim Jagielski anything stands in (as `validate.py` skips MathML). 97*0e16a31dSJim Jagielski - Known leniencies: it truncates a `draw:points` over 2048 characters and 98*0e16a31dSJim Jagielski adapts old OOo namespaces. It is stricter than Part 3 §3.2 in wanting 99*0e16a31dSJim Jagielski `META-INF/documentsignatures.xml` listed in the manifest. It checks ID 100*0e16a31dSJim Jagielski uniqueness, which `jing -i` does not. 101*0e16a31dSJim Jagielski- `fixtures/doctypes/` (`run.sh`, `make.py`, `derive-chart.py`), `fixtures/encrypted/`, `fixtures/signed/`, `fixtures/base/`: documents 102*0e16a31dSJim Jagielski of the kinds the corpus lacks (templates, master, Writer/Web, Impress as Draw, Math, chart, encrypted, signed, Base), saved 103*0e16a31dSJim Jagielski in strict and extended mode under `runs/task12/gen-*`. `decrypt.py <in-dir> <out-dir> <password>` writes plain copies of 104*0e16a31dSJim Jagielski encrypted packages for the schema checkers; `pkgcheck.py` must still run on the real ones. Signing needs a visible 105*0e16a31dSJim Jagielski (`-invisible`) office because `-headless` cancels the dialog: see the header of `fixtures/signed/make.py`. 106*0e16a31dSJim Jagielski- `fixtures/{writer,calc,draw,chart,styles-text,styles-gfx,leak}/make.py`, run with `fixtures/gen-run.sh <area> <strict|ext> <port> <abs-outdir>`: 107*0e16a31dSJim Jagielski documents built through UNO to exercise the elements, attributes and enumerated values that `cov_report.py` lists as gaps 108*0e16a31dSJim Jagielski (`leak` classifies the names AOO emits that the schema does not define). Use an absolute `<outdir>`. 109*0e16a31dSJim Jagielski- `cov_schema.py`, `cov_source.py`, `cov_corpus.py`, `cov_report.py`: schema-coverage report. The schema universe 110*0e16a31dSJim Jagielski (ODF 1.2 elements, element/attribute pairs, enumerated values), the names the export code in `main/` can emit (static 111*0e16a31dSJim Jagielski C++ scan; a lower bound, table-driven names are missed) and what a corpus exercises are joined by namespace URI and 112*0e16a31dSJim Jagielski local name into the gaps worth generating documents for. Outputs live under `runs/`. 113*0e16a31dSJim Jagielski- `idcheck.py <outdir> <ids.json>` parses every stream namespace-aware and 114*0e16a31dSJim Jagielski checks `xml:id` syntax and uniqueness per stream (ODF 1.2 Part 1 §2.2.1 115*0e16a31dSJim Jagielski D.1, D.2). `jing -i` does not check ID uniqueness. 116*0e16a31dSJim Jagielski 117*0e16a31dSJim Jagielski## The regression gate 118*0e16a31dSJim Jagielski 119*0e16a31dSJim Jagielski python3 -I gate.py <tag> --prev runs/gate-<earlier tag> # everything 120*0e16a31dSJim Jagielski python3 -I gate.py <tag> --build xmloff,sw --prev ... # rebuild first 121*0e16a31dSJim Jagielski python3 -I gate.py smoke --limit 8 --prev ... --stages corpus,validate,dump,diff 122*0e16a31dSJim Jagielski 123*0e16a31dSJim Jagielski`gate.py` runs the stages below in `runs/gate-<tag>/`, records `results.json`, 124*0e16a31dSJim Jagielskiskips stages already done (`--force` reruns) and exits 0 only if everything 125*0e16a31dSJim Jagielskithat ran passed. 126*0e16a31dSJim Jagielski 127*0e16a31dSJim Jagielski| stage | does | passes if | 128*0e16a31dSJim Jagielski|---|---|---| 129*0e16a31dSJim Jagielski| `build` | `build.pl` and `deliver.pl` for the `--build` modules (environment script: `ODFV_BUILD_ENV`) | exit codes 0 | 130*0e16a31dSJim Jagielski| `refresh` | copies `--libs` from the solver into `app/` (`refresh-app.sh`) | copies identical | 131*0e16a31dSJim Jagielski| `corpus` | saves the job list in `ext` and `strict` mode | no crash, every job accounted for, OK/FAIL counts equal `--prev` | 132*0e16a31dSJim Jagielski| `validate` | `validate.py`, `report.py` and the four checkers per mode | kinds not worse than `baseline/<mode>.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 | 133*0e16a31dSJim Jagielski| `dump` | reloads every saved package with `dump7.py` | no office failure; hangs and crashes are listed | 134*0e16a31dSJim Jagielski| `diff` | `diff7.py` on the dumps and `cmp7.py` on the packages against `--prev` | every difference is a document in `gate-volatile.tsv` | 135*0e16a31dSJim Jagielski| `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`) | 136*0e16a31dSJim Jagielski 137*0e16a31dSJim Jagielski- Inputs are frozen: without `--jobs` the list of `--prev` is reused (outputs 138*0e16a31dSJim Jagielski are rewritten), so the same documents are saved when files under `main/` 139*0e16a31dSJim Jagielski change. A later run's `--prev` may be any earlier gate directory or a 140*0e16a31dSJim Jagielski directory with the same layout (`<mode>/out`, `<mode>/dump`). 141*0e16a31dSJim Jagielski- The dump stage kills the office only when the dump makes no progress for 142*0e16a31dSJim Jagielski `--stall` seconds (default 300), then writes a `HANG` marker for that 143*0e16a31dSJim Jagielski document alone. A long batch that keeps progressing is never killed. 144*0e16a31dSJim Jagielski- `gate-volatile.tsv` names the documents whose dumps differ between runs 145*0e16a31dSJim Jagielski without an export change (volatile Calc formulas, redline timestamps, the 146*0e16a31dSJim Jagielski `0238` loader order, the intermittent Basic library loss of `0048`). A new 147*0e16a31dSJim Jagielski entry needs a reason. 148*0e16a31dSJim Jagielski- The first fixtures run has nothing to compare with: its mismatch pattern is 149*0e16a31dSJim Jagielski only recorded. By design, strict, ODF 1.0 and ODF 1.1 mode fail the 14 150*0e16a31dSJim Jagielski reload rows that need gradient or transparency fills. 151*0e16a31dSJim Jagielski 152*0e16a31dSJim Jagielski## Checking a change 153*0e16a31dSJim Jagielski 154*0e16a31dSJim Jagielski python3 -I compare.py baseline/strict.kinds.json runs/new/strict.kinds.json 155*0e16a31dSJim Jagielski 156*0e16a31dSJim Jagielski`compare.py` exits 1 if any kind is new or any kind got worse. 157*0e16a31dSJim Jagielski 158*0e16a31dSJim JagielskiTo test a rebuilt module without repackaging, copy its libraries from the 159*0e16a31dSJim Jagielskisolver into the app copy: 160*0e16a31dSJim Jagielski 161*0e16a31dSJim Jagielski ./refresh-app.sh libxo.dylib 162*0e16a31dSJim Jagielski 163*0e16a31dSJim Jagielski`roundtrip.py <port> <file> <checks.json>` loads a document and compares UNO 164*0e16a31dSJim Jagielskiexpressions with expected values; `--capture <out.json>` records them instead. 165*0e16a31dSJim Jagielski 166*0e16a31dSJim Jagielski## Formula evaluation baseline (calcval) 167*0e16a31dSJim Jagielski 168*0e16a31dSJim Jagielski ./start.sh calcval 2792 169*0e16a31dSJim Jagielski ./runpy.sh calcval.py 2792 calcval-cases.tsv runs/phase4/baseline.json 170*0e16a31dSJim Jagielski python3 -I calcval.py --compare runs/phase4/baseline.json runs/phase4/new.json 171*0e16a31dSJim Jagielski 172*0e16a31dSJim Jagielski`calcval.py` opens a new Calc document, writes the data area described in the 173*0e16a31dSJim Jagielskiheader of `calcval-cases.tsv`, and for each case enters the formula in a fresh 174*0e16a31dSJim Jagielskiscratch cell (`cell`, `cell@N`) or range (`array:CxR`), recalculates, records 175*0e16a31dSJim Jagielskithe result type, `getError()`, `getString()`, `getValue()` and the formula as 176*0e16a31dSJim Jagielskiread back, then clears the scratch area. `expected` is the value the ODF 1.3 177*0e16a31dSJim JagielskiPart 4 text requires (`-` records the current behaviour only); the status of 178*0e16a31dSJim Jagielskieach case is `match`, `mismatch`, `record` or `exception`. The run prints 179*0e16a31dSJim Jagielskievery mismatch, which are the current deviations from ODF 1.3. `--compare` 180*0e16a31dSJim Jagielskilists each case whose recorded result changed (exit 1 if any). RECEIVED and 181*0e16a31dSJim JagielskiBESSELK/BESSELY are Analysis add-ins and are entered by programmatic name. 182*0e16a31dSJim Jagielski 183*0e16a31dSJim Jagielski## Notes 184*0e16a31dSJim Jagielski 185*0e16a31dSJim Jagielski- AOO has no `-convert-to`; conversion goes over UNO (`convert.py`). 186*0e16a31dSJim Jagielski- jing must run with `-i`, otherwise it rejects the ODF schemas themselves. 187*0e16a31dSJim Jagielski- A jing fatal error can hide later errors. Such lines are reported as 188*0e16a31dSJim Jagielski `UNPARSED` kinds, and `compare.py` fails on any new `UNPARSED` kind. 189*0e16a31dSJim Jagielski- jing stops at a fatal error (a stream that is not well-formed); 190*0e16a31dSJim Jagielski `validate.py` records it and carries on with the remaining files. 191*0e16a31dSJim Jagielski- `setup-app.sh` renames an existing `app/OpenOffice.app` to 192*0e16a31dSJim Jagielski `OpenOffice.app.old.<timestamp>` instead of deleting it. 193