xref: /trunk/test/odf-validate/README.md (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
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