This is an automated email from the ASF dual-hosted git repository.

jimjag pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/openoffice.git

commit 9fb741fc0a32772b6a4c20c7b885bb4cc21d52d0
Author: Jim Jagielski <[email protected]>
AuthorDate: Tue Nov 3 06:14:09 2026 -0500

    test/odf-validate: ODF 1.4 fixtures for handles, charts, forms and consumer 
packages; compare the 1.4 modes against the 1.4 schema
---
 test/odf-validate/.gitignore                    |   1 +
 test/odf-validate/calcval-cases14.tsv           |   5 +-
 test/odf-validate/consumer_smoke.py             |  82 +++++++++
 test/odf-validate/fixtures/t14/chart.py         |  74 ++++++++
 test/odf-validate/fixtures/t14/forms.py         |  73 ++++++++
 test/odf-validate/fixtures/t14/handles.py       |  74 ++++++++
 test/odf-validate/fixtures/t14/make-consumer.py | 215 ++++++++++++++++++++++++
 test/odf-validate/fixtures/t14/resave.py        |  44 +++++
 test/odf-validate/fixtures/t14/run-in-mode.sh   |  41 +++++
 test/odf-validate/gate.py                       |  14 +-
 10 files changed, 616 insertions(+), 7 deletions(-)

diff --git a/test/odf-validate/.gitignore b/test/odf-validate/.gitignore
index 56e213084b..b03b2f819a 100644
--- a/test/odf-validate/.gitignore
+++ b/test/odf-validate/.gitignore
@@ -7,3 +7,4 @@ fixtures/control/
 fixtures/control-pkg/
 fixtures/control-doctype/
 tools/
+fixtures/t14/consumer/
diff --git a/test/odf-validate/calcval-cases14.tsv 
b/test/odf-validate/calcval-cases14.tsv
index 7d9e1c2622..ddee2473a2 100644
--- a/test/odf-validate/calcval-cases14.tsv
+++ b/test/odf-validate/calcval-cases14.tsv
@@ -31,7 +31,7 @@ CONVERT.atm   =CONVERT(1;"atm";"Pa")  cell    101325.1        
6.16.18 Table 24: atm is the St
 CONVERT.SI_at  =CONVERT(1;"SI_at";"Pa")        cell    98066.5 6.16.18 Table 
24: SI_at is the Technical atmosphere, 9.8066510E+4 Pa
 CONVERT.at     =CONVERT(1;"at";"Pa")   cell    -       6.16.18 Table 24: at is 
deprecated, recorded only
 NPER.omit      =NPER(0.01;-100;1000)-NPER(0.01;-100;1000;0;0)  cell    0       
6.12.29 Fv and PayType default to 0
-NPER.empty     =NPER(0.01;-100;1000;;)-NPER(0.01;-100;1000;0;0)        cell    
0       6.12.29 empty Fv and PayType parameters take the defaults
+NPER.empty     =NPER(0.01;-100;1000;;)-NPER(0.01;-100;1000;0;0)        cell    
0       6.12.29 empty Fv and PayType take the defaults; known deviation: the 
formula compiler rejects an empty last parameter before ')' for every function 
except IF (Err:511)
 NPER.paytype1  =NPER(0.01;-100;1000;0;1)-NPER(0.01;-100;1000;0;0)      cell    
-       6.12.29 PayType 1 differs; recorded only
 INDEX.col.row  =INDEX(D1:D5;2) cell    20      6.14.6 column vector, Row only: 
the entire row of the area
 INDEX.col.rowcol       =INDEX(D1:D5;2;1)       cell    20      6.14.6 column 
vector, Row and Column
@@ -65,3 +65,6 @@ Y1500.leap    =DATE(1500;3;1)-DATE(1500;2;28) cell    -       
7.4 before 1583 the system is
 Y1582.switch   =DATE(1582;10;15)-DATE(1582;10;4)       cell    -       7.4 
proleptic Gregorian gives 11, a Julian-to-Gregorian switch gives 1
 CONVERT.g.kg   =CONVERT(1;"g";"kg")    cell    0.001   control: the add-in 
rewrite reaches a known unit pair
 CONVERT.bogus  =CONVERT(1;"nosuchunit";"Pa")   cell    #ERR    control: an 
unknown unit is an error
+EASTER.mid     =EASTERSUNDAY(1000)     cell    #ERR    6.10.8 constraint 1583 
<= Year (100 to 1582)
+EASTER.neg     =EASTERSUNDAY(-1)       cell    #ERR    6.10.8 constraint 1583 
<= Year (negative years used to wrap)
+EASTER.wrap    =EASTERSUNDAY(67536)    cell    #ERR    6.10.8 constraint Year 
<= 9956 (67536 used to wrap to 2000)
diff --git a/test/odf-validate/consumer_smoke.py 
b/test/odf-validate/consumer_smoke.py
new file mode 100644
index 0000000000..da50cf9c3c
--- /dev/null
+++ b/test/odf-validate/consumer_smoke.py
@@ -0,0 +1,82 @@
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+# usage: consumer_smoke.py <tag> [--port N] [--stall SECONDS] [--dir DIR]
+# Import smoke test for the ODF 1.4 additions: reloads every package of 
fixtures/t14/consumer (made by make-consumer.py)
+# with the office of the app copy, dumps a fixed property set of each 
(dump7.py with the gate's stall watchdog) and compares
+# every package with its '-base' twin (the same document without the 
construct, diff7.py). Reports per package: loaded or
+# not (an error marker in the dump), and the number of differing dump keys 
against the twin. A package without a twin
+# is only checked for loading. Results in runs/consumer-<tag>/ (summary in 
summary.json).
+import argparse, importlib.util, json, os, shutil, subprocess, sys
+
+D = os.path.dirname(os.path.abspath(__file__))
+spec = importlib.util.spec_from_file_location("gate", D + "/gate.py")
+gate = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(gate)
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("tag")
+    ap.add_argument("--port", type=int, default=2950)
+    ap.add_argument("--stall", type=int, default=120)
+    ap.add_argument("--dir", default=os.path.join(D, "fixtures", "t14", 
"consumer"))
+    a = ap.parse_args()
+    root = os.path.join(D, "runs", "consumer-" + a.tag)
+    shutil.rmtree(root, ignore_errors=True)
+    os.makedirs(root)
+    dest = os.path.join(root, "dump")
+    log = os.path.join(root, "dump.log")
+    r = gate.dump_dir(a.dir, dest, "consumer_" + a.tag, a.port, a.stall, log)
+    gate.kill_office("consumer_" + a.tag)
+    names = sorted(f[:-5] for f in os.listdir(dest) if f.endswith(".json"))
+    summary = {}
+    for n in names:
+        d = json.load(open(os.path.join(dest, n + ".json")))
+        summary[n] = {"loaded": "error" not in d or d.get("error") is None, 
"error": d.get("error"), "body_error": d.get("body_error")}
+    base = {n[:-5]: n for n in names if 
os.path.splitext(n)[0].endswith("-base")}
+    for n in names:
+        stem, ext = os.path.splitext(n)
+        twin = stem + "-base" + ext
+        if twin not in names:
+            continue
+        pa, pb = os.path.join(root, "pairA"), os.path.join(root, "pairB")
+        for p in (pa, pb):
+            os.makedirs(p, exist_ok=True)
+        shutil.copy(os.path.join(dest, n + ".json"), os.path.join(pa, n + 
".json"))
+        shutil.copy(os.path.join(dest, twin + ".json"), os.path.join(pb, n + 
".json"))
+    if os.path.isdir(os.path.join(root, "pairA")):
+        out = os.path.join(root, "pairs.json")
+        subprocess.run([sys.executable, "-I", os.path.join(D, "diff7.py"), 
os.path.join(root, "pairA"), os.path.join(root, "pairB"), out],
+                       capture_output=True, check=True)
+        for n, v in json.load(open(out)).items():
+            summary[n]["ndiff_vs_base"] = v["ndiff"]
+            summary[n]["diff_keys"] = [x[0] for x in v["diffs"][:8]]
+    json.dump(summary, open(os.path.join(root, "summary.json"), "w"), indent=1)
+    print("dump:", r)
+    for n in sorted(summary):
+        s = summary[n]
+        print("%-36s %-6s %s" % (n, "loaded" if s["loaded"] else "FAILED", 
("diff vs base: %d %s" % (s["ndiff_vs_base"], s["diff_keys"][:3])) if 
"ndiff_vs_base" in s else ""))
+    bad = [n for n, s in summary.items() if not s["loaded"]]
+    print("not loaded:", bad)
+    sys.exit(1 if bad else 0)
+
+
+main()
diff --git a/test/odf-validate/fixtures/t14/chart.py 
b/test/odf-validate/fixtures/t14/chart.py
new file mode 100644
index 0000000000..56dc2cc974
--- /dev/null
+++ b/test/odf-validate/fixtures/t14/chart.py
@@ -0,0 +1,74 @@
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+# usage: runpy.sh fixtures/t14/chart.py <port> <outdir> <tag>
+# A Calc document with a chart over A1:C4 (data in the sheet, so the chart 
uses the sheet's data provider), saved with the
+# office's configured ODF version and reloaded. Writes <outdir>/<tag>.json: 
whether the reloaded chart has an internal data
+# provider, the source range of every data sequence, and whether the chart 
still follows a changed cell.
+import json, os, sys
+import uno
+from unohelp import connect, desktop, pv
+
+port, out, tag = int(sys.argv[1]), os.path.abspath(sys.argv[2]), sys.argv[3]
+os.makedirs(out, exist_ok=True)
+ctx = connect(port)
+desk = desktop(ctx)
+
+
+def info(doc):
+    sh = doc.Sheets.getByIndex(0)
+    cd = sh.Charts.getByName("chart1").EmbeddedObject
+    seqs = []
+    cs = cd.FirstDiagram.CoordinateSystems[0]
+    for ct in cs.ChartTypes:
+        for ser in ct.DataSeries:
+            for seq in ser.DataSequences:
+                seqs.append((seq.Role if hasattr(seq, "Role") else 
seq.Values.Role, seq.Values.SourceRangeRepresentation))
+    first = cd.DataProvider if hasattr(cd, "DataProvider") else 
cd.getDataProvider()
+    before = [seq.Values.getNumericalData() for ct in cs.ChartTypes for ser in 
ct.DataSeries for seq in ser.DataSequences]
+    sh.getCellByPosition(1, 1).Value = 12345
+    after = [seq.Values.getNumericalData() for ct in cs.ChartTypes for ser in 
ct.DataSeries for seq in ser.DataSequences]
+    return {"internal_data_provider": bool(cd.hasInternalDataProvider()), 
"ranges": seqs,
+            "follows_cell": before != after, "label_sequences": 
[s.Label.SourceRangeRepresentation for ct in cs.ChartTypes for ser in 
ct.DataSeries for s in [ser.DataSequences[0]]] if False else None}
+
+
+doc = desk.loadComponentFromURL("private:factory/scalc", "_blank", 0, 
(pv("Hidden", True),))
+sh = doc.Sheets.getByIndex(0)
+for r, row in enumerate((("", "a", "b"), ("x", 1, 2), ("y", 3, 4), ("z", 5, 
6))):
+    for c, v in enumerate(row):
+        cell = sh.getCellByPosition(c, r)
+        if isinstance(v, str):
+            cell.String = v
+        else:
+            cell.Value = v
+addr = uno.createUnoStruct("com.sun.star.table.CellRangeAddress")
+addr.Sheet, addr.StartColumn, addr.StartRow, addr.EndColumn, addr.EndRow = 0, 
0, 0, 2, 3
+rect = uno.createUnoStruct("com.sun.star.awt.Rectangle", 6000, 500, 8000, 6000)
+sh.Charts.addNewByName("chart1", rect, (addr,), True, True)
+path = os.path.join(out, tag + ".ods")
+if os.path.exists(path):
+    os.remove(path)
+doc.storeToURL(uno.systemPathToFileUrl(path), (pv("FilterName", "calc8"), 
pv("Overwrite", True)))
+doc.close(True)
+d2 = desk.loadComponentFromURL(uno.systemPathToFileUrl(path), "_blank", 0, 
(pv("Hidden", True),))
+res = info(d2)
+d2.close(True)
+json.dump(res, open(os.path.join(out, "chart-" + tag + ".json"), "w"), 
indent=1, sort_keys=True)
+print(tag, res)
diff --git a/test/odf-validate/fixtures/t14/forms.py 
b/test/odf-validate/fixtures/t14/forms.py
new file mode 100644
index 0000000000..e49c57d1bd
--- /dev/null
+++ b/test/odf-validate/fixtures/t14/forms.py
@@ -0,0 +1,73 @@
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+# usage: runpy.sh fixtures/t14/forms.py <port> <outdir> <tag> [file.odt]
+# With a file nothing is created: the file is loaded and its InputRequired 
values are written.
+# A Writer document with one form holding two controls of each type that can 
carry form:input-required: one with InputRequired
+# set to false and one left at its default (true in AOO). Saved with the 
office's configured ODF version and reloaded.
+# Writes <outdir>/forms-<tag>.json with the InputRequired value of every 
control after the reload (a control written with
+# the wrong value, or without the attribute where the reader's default 
differs, changes value).
+import json, os, sys
+import uno
+from unohelp import connect, desktop, pv
+
+port, out, tag = int(sys.argv[1]), os.path.abspath(sys.argv[2]), sys.argv[3]
+os.makedirs(out, exist_ok=True)
+ctx = connect(port)
+desk = desktop(ctx)
+KINDS = ["TextField", "CheckBox", "ComboBox", "ListBox", "RadioButton", 
"DateField", "TimeField", "FormattedField", "NumericField",
+         "CurrencyField", "PatternField", "DatabaseImageControl", 
"FileControl"]
+if len(sys.argv) > 4:
+    path = os.path.abspath(sys.argv[4])
+else:
+    doc = desk.loadComponentFromURL("private:factory/swriter", "_blank", 0, 
(pv("Hidden", True),))
+    forms = doc.DrawPage.Forms
+    form = 
ctx.ServiceManager.createInstanceWithContext("com.sun.star.form.component.Form",
 ctx)
+    forms.insertByName("f", form)
+    for i, k in enumerate(KINDS):
+        for suffix in ("", "_default"):
+            m = 
ctx.ServiceManager.createInstanceWithContext("com.sun.star.form.component." + 
k, ctx)
+            if m is None:
+                print("service not available:", k)
+                break
+            m.Name = k + suffix
+            if not suffix:
+                try:
+                    m.InputRequired = False
+                except Exception as e:
+                    print("no InputRequired on", k, e)
+            form.insertByIndex(form.Count, m)
+    path = os.path.join(out, tag + ".odt")
+    if os.path.exists(path):
+        os.remove(path)
+    doc.storeToURL(uno.systemPathToFileUrl(path), (pv("FilterName", 
"writer8"), pv("Overwrite", True)))
+    doc.close(True)
+d2 = desk.loadComponentFromURL(uno.systemPathToFileUrl(path), "_blank", 0, 
(pv("Hidden", True),))
+f2 = d2.DrawPage.Forms.getByName("f")
+res = {}
+for i in range(f2.Count):
+    c = f2.getByIndex(i)
+    try:
+        res[c.Name] = c.InputRequired
+    except Exception as e:
+        res[c.Name] = "n/a"
+d2.close(True)
+json.dump(res, open(os.path.join(out, "forms-" + tag + ".json"), "w"), 
indent=1, sort_keys=True)
+print(tag, res)
diff --git a/test/odf-validate/fixtures/t14/handles.py 
b/test/odf-validate/fixtures/t14/handles.py
new file mode 100644
index 0000000000..01252ae796
--- /dev/null
+++ b/test/odf-validate/fixtures/t14/handles.py
@@ -0,0 +1,74 @@
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+# usage: runpy.sh fixtures/t14/handles.py <port> <outdir> <tag> [file.odg]
+# With a file, nothing is drawn: the file is loaded and the handles of its 
shapes (in the order of the types below) are written.
+# Draws custom shapes with polar handles (circular-arrow, block-arc, ring), 
with XY handles (sun, round-rectangle) and a ribbon
+# in a Draw document, saves it with the office's configured ODF version, 
reloads the file and writes <outdir>/<tag>.json with the
+# handles of every shape after the reload; the test compares the lists between 
the modes (v13 is the control).
+import json, os, sys
+import uno
+from unohelp import connect, desktop, pv
+
+port, out, tag = int(sys.argv[1]), os.path.abspath(sys.argv[2]), sys.argv[3]
+os.makedirs(out, exist_ok=True)
+ctx = connect(port)
+desk = desktop(ctx)
+TYPES = ["circular-arrow", "block-arc", "ring", "sun", "round-rectangle", 
"ribbon", "mso-spt163", "smiley", "heart"]
+
+
+def handles(shape):
+    geo = shape.CustomShapeGeometry
+    res = []
+    for prop in geo:
+        if prop.Name == "Handles":
+            for h in prop.Value:
+                d = {}
+                for p in h:
+                    v = p.Value
+                    d[p.Name] = [(x.Value, x.Type) for x in (v.First, 
v.Second)] if hasattr(v, "First") else ((v.Value, v.Type) if hasattr(v, "Type") 
and hasattr(v, "Value") else v)
+                res.append(d)
+    return res
+
+
+doc = None if len(sys.argv) > 4 else 
desk.loadComponentFromURL("private:factory/sdraw", "_blank", 0, (pv("Hidden", 
True),))
+page = doc.DrawPages.getByIndex(0) if doc else None
+for i, t in enumerate(TYPES if doc else []):
+    sh = doc.createInstance("com.sun.star.drawing.CustomShape")
+    page.add(sh)
+    sh.Size = uno.createUnoStruct("com.sun.star.awt.Size", 4000, 3000)
+    sh.Position = uno.createUnoStruct("com.sun.star.awt.Point", 500 + (i % 3) 
* 5000, 500 + (i // 3) * 4000)
+    sh.CustomShapeGeometry = (pv("Type", t),)
+if doc:
+    path = os.path.join(out, tag + ".odg")
+    if os.path.exists(path):
+        os.remove(path)
+    doc.storeToURL(uno.systemPathToFileUrl(path), (pv("FilterName", "draw8"), 
pv("Overwrite", True)))
+    doc.close(True)
+else:
+    path = os.path.abspath(sys.argv[4])
+d2 = desk.loadComponentFromURL(uno.systemPathToFileUrl(path), "_blank", 0, 
(pv("Hidden", True),))
+page2 = d2.DrawPages.getByIndex(0)
+res = {}
+for i, t in enumerate(TYPES):
+    res[t] = handles(page2.getByIndex(i))
+d2.close(True)
+json.dump(res, open(os.path.join(out, "handles-" + tag + ".json"), "w"), 
indent=1, sort_keys=True, default=str)
+print(tag, {t: len(v) for t, v in res.items()})
diff --git a/test/odf-validate/fixtures/t14/make-consumer.py 
b/test/odf-validate/fixtures/t14/make-consumer.py
new file mode 100644
index 0000000000..0894cf5c91
--- /dev/null
+++ b/test/odf-validate/fixtures/t14/make-consumer.py
@@ -0,0 +1,215 @@
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+#**************************************************************
+
+# usage: make-consumer.py                 -- (re)generate the ODF 1.4 consumer 
test packages in fixtures/t14/consumer/
+#        make-consumer.py --report <val.json>  -- print the table of 
validate.py results for them
+# Each CASES entry is one construct from the ODF 1.4 additions inside an 
otherwise plain document, written with
+# office:version and manifest:version 1.4, plus (unless base is None) a 
"<name>-base" control without the construct.
+# "fields" fills the ${...} placeholders of the case; "base" overrides them 
for the control (all others become empty).
+import json, os, struct, sys, zipfile, zlib
+from string import Template
+
+OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "consumer")
+NS = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "style": 
"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+      "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0", "table": 
"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
+      "draw": "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", "fo": 
"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+      "svg": "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0", 
"xlink": "http://www.w3.org/1999/xlink";,
+      "chart": "urn:oasis:names:tc:opendocument:xmlns:chart:1.0", "number": 
"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
+      "dc": "http://purl.org/dc/elements/1.1/";, "meta": 
"urn:oasis:names:tc:opendocument:xmlns:meta:1.0"}
+NSDECL = " ".join('xmlns:%s="%s"' % kv for kv in NS.items())
+MT = "application/vnd.oasis.opendocument."
+# ext: (media type, body element, wrapper around the case's "content", has 
page layout and master page)
+KINDS = {"odt": (MT + "text", "office:text", "${content}", True),
+         "ods": (MT + "spreadsheet", "office:spreadsheet", "${content}", True),
+         "odg": (MT + "graphics", "office:drawing", '<draw:page 
draw:name="page1" draw:master-page-name="Default">${content}</draw:page>', 
True),
+         "odc": (MT + "chart", "office:chart", "${content}", False)}
+TXT = "<text:p>Plain text.</text:p>"
+CELLS = '<table:table 
table:name="Sheet1"><table:table-column/><table:table-row>%s</table:table-row></table:table>'
+RECT = 'svg:x="1cm" svg:y="1cm" svg:width="6cm" svg:height="4cm"'
+GEOM = '<draw:custom-shape %s><draw:enhanced-geometry svg:viewBox="0 0 21600 
21600" draw:type="rectangle" 
${eg}>${handles}</draw:enhanced-geometry></draw:custom-shape>' % RECT
+FRAME = '<draw:frame draw:name="F1" text:anchor-type="paragraph" 
svg:width="4cm" 
svg:height="2cm"%s><draw:text-box%s><text:p>Box</text:p></draw:text-box></draw:frame>'
+PSTYLE = '<style:style style:name="%s" 
style:family="paragraph">%s</style:style>'
+GSTYLE = '<style:style style:name="%s" style:family="graphic">%s</style:style>'
+
+
+def png():
+    """A 1x1 RGB PNG."""
+    def chunk(t, d):
+        c = t + d
+        return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c) 
& 0xffffffff)
+    return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 
1, 8, 2, 0, 0, 0))
+            + chunk(b"IDAT", zlib.compress(b"\x00\xff\x00\x00")) + 
chunk(b"IEND", b""))
+
+
+def pstyle(n, tag):
+    """Paragraph style n with the given properties element."""
+    return PSTYLE % (n, tag)
+
+
+# name, ext, construct, content, auto (content.xml automatic styles), styles 
(styles.xml office:styles), fields, base
+CASES = [
+    dict(name="value-type-error", ext="ods", 
construct='office:value-type="error" with office:string-value, text:p child',
+         content=CELLS % '<table:table-cell 
${c}><text:p>#DIV/0!</text:p></table:table-cell>',
+         fields={"c": 'office:value-type="error" 
office:string-value="#DIV/0!"'}, base={"c": 'office:value-type="string"'}),
+    dict(name="value-type-error-nostring", ext="ods", 
construct='office:value-type="error" without office:string-value, text:p child',
+         content=CELLS % '<table:table-cell 
${c}><text:p>#N/A</text:p></table:table-cell>',
+         fields={"c": 'office:value-type="error"'}, base=None),
+    dict(name="extrusion-metal-type", ext="odg", 
construct='draw:extrusion-metal-type="draw:default" on draw:enhanced-geometry 
(value arbitrary: the RNG type is namespacedToken with a declared prefix)',
+         content=GEOM, fields={"eg": 'draw:extrusion="true" ${m}', "m": 
'draw:extrusion-metal="true" draw:extrusion-metal-type="draw:default"', 
"handles": ""},
+         base={"m": ""}),
+    dict(name="extrusion-specularity", ext="odg", 
construct='draw:extrusion-specularity="150%" on draw:enhanced-geometry',
+         content=GEOM, fields={"eg": 'draw:extrusion="true" 
draw:extrusion-specularity="${v}"', "v": "150%", "handles": ""}, base={"v": 
"80%"}),
+    dict(name="writing-mode-sideways", ext="odt", 
construct='style:writing-mode sideways-rl / sideways-lr on paragraph styles, 
sideways-rl on graphic-properties',
+         auto=pstyle("P1", '<style:paragraph-properties 
style:writing-mode="${w1}"/>') + pstyle("P2", '<style:paragraph-properties 
style:writing-mode="${w2}"/>')
+              + GSTYLE % ("G1", '<style:graphic-properties 
style:writing-mode="${w3}"/>'),
+         content='<text:p text:style-name="P1">One</text:p><text:p 
text:style-name="P2">Two</text:p><text:p>' + FRAME % (' draw:style-name="G1"', 
"") + '</text:p>',
+         fields={"w1": "sideways-rl", "w2": "sideways-lr", "w3": 
"sideways-rl"}, base={"w1": "lr-tb", "w2": "rl-tb", "w3": "lr-tb"}),
+    dict(name="break-even-odd-page", ext="odt", 
construct='fo:break-before="even-page" / "odd-page" on paragraph styles',
+         auto=pstyle("P1", '<style:paragraph-properties 
fo:break-before="${b1}"/>') + pstyle("P2", '<style:paragraph-properties 
fo:break-before="${b2}"/>'),
+         content='<text:p text:style-name="P1">Even</text:p><text:p 
text:style-name="P2">Odd</text:p>',
+         fields={"b1": "even-page", "b2": "odd-page"}, base={"b1": "page", 
"b2": "auto"}),
+    dict(name="vertical-rel-page-content", ext="odt", 
construct='style:vertical-rel="page-content-bottom" / "page-content-top" on 
graphic-properties of anchored text-box frames',
+         auto=GSTYLE % ("G1", '<style:graphic-properties 
style:vertical-pos="bottom" style:vertical-rel="${r1}"/>')
+              + GSTYLE % ("G2", '<style:graphic-properties 
style:vertical-pos="top" style:vertical-rel="${r2}"/>'),
+         content='<text:p>' + FRAME % (' draw:style-name="G1"', "") + 
FRAME.replace("F1", "F2") % (' draw:style-name="G2"', "") + '</text:p>',
+         fields={"r1": "page-content-bottom", "r2": "page-content-top"}, 
base={"r1": "page-content", "r2": "page-content"}),
+    dict(name="repeat-scale", ext="odg", construct='style:repeat="scale" on a 
graphic style with a bitmap fill',
+         auto=GSTYLE % ("gr1", '<style:graphic-properties draw:fill="bitmap" 
draw:fill-image-name="Img" style:repeat="${r}"/>'),
+         styles='<draw:fill-image draw:name="Img" xlink:href="Pictures/p.png" 
xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>',
+         content='<draw:rect draw:style-name="gr1" %s/>' % RECT, fields={"r": 
"scale"}, base={"r": "stretch"}, extra={"Pictures/p.png": png()}),
+    dict(name="svg-rx-ry", ext="odt", construct='svg:rx / svg:ry on a 
draw:text-box in a draw:frame and on an office:annotation',
+         content='<text:p>' + FRAME % ("", ' ${r}') + 'x<office:annotation 
${r}><dc:creator>A</dc:creator><text:p>Note</text:p></office:annotation></text:p>',
+         fields={"r": 'svg:rx="0.3cm" svg:ry="0.2cm"'}, base={}),
+    dict(name="table-in-shape", ext="odg", construct='table:table as a child 
of draw:rect',
+         content='<draw:rect %s>${t}</draw:rect>' % RECT,
+         fields={"t": '<table:table 
table:name="T1"><table:table-column/><table:table-row><table:table-cell 
office:value-type="string"><text:p>cell</text:p></table:table-cell></table:table-row></table:table>'},
+         base={"t": "<text:p>text</text:p>"}),
+    dict(name="annotation-in-page", ext="odg", construct='office:annotation as 
a child of draw:page',
+         content='<draw:rect %s><text:p>shape</text:p></draw:rect>${a}' % RECT,
+         fields={"a": '<office:annotation svg:x="8cm" svg:y="1cm" 
svg:width="4cm" 
svg:height="2cm"><dc:creator>A</dc:creator><dc:date>2026-11-03T10:00:00</dc:date><text:p>Note</text:p></office:annotation>'},
 base={}),
+    dict(name="background-fill-paragraph", ext="odt", 
construct='style:background-fill (solid, #ff0000) in 
style:paragraph-properties',
+         auto=pstyle("P1", 
'<style:paragraph-properties>${bf}</style:paragraph-properties>'),
+         content='<text:p text:style-name="P1">Red</text:p>', fields={"bf": 
'<style:background-fill draw:fill="solid" draw:fill-color="#ff0000"/>'}, 
base={}),
+    dict(name="background-fill-cell", ext="ods", 
construct='style:background-fill (solid, #ff0000) in 
style:table-cell-properties',
+         auto='<style:style style:name="ce1" 
style:family="table-cell"><style:table-cell-properties>${bf}</style:table-cell-properties></style:style>',
+         content=CELLS % '<table:table-cell table:style-name="ce1" 
office:value-type="string"><text:p>Red</text:p></table:table-cell>',
+         fields={"bf": '<style:background-fill draw:fill="solid" 
draw:fill-color="#ff0000"/>'}, base={}),
+    dict(name="num-list-format", ext="odt", construct='number:num-list-format 
/ number:num-list-label in office:styles, style:num-list-format-name on 
text:list-level-style-number',
+         styles='${nlf}<text:list-style 
style:name="L1"><text:list-level-style-number text:level="1" 
style:num-format="1" ${nm}/></text:list-style>',
+         content='<text:list 
text:style-name="L1"><text:list-item><text:p>One</text:p></text:list-item></text:list>',
+         fields={"nlf": '<number:num-list-format 
style:name="NLF1"><number:text>(</number:text><number:num-list-label 
text:level="1"/><number:text>)</number:text></number:num-list-format>',
+                 "nm": 'style:num-list-format-name="NLF1"'}, base={}),
+    dict(name="filter-color-condition", ext="ods", 
construct='table:filter-condition with table:data-type="background-color" and 
"text-color", table:value a colour',
+         content=CELLS % '<table:table-cell 
office:value-type="string"><text:p>a</text:p></table:table-cell>'
+                 + '<table:database-ranges><table:database-range 
table:name="DB1" table:target-range-address="Sheet1.A1:Sheet1.A3">'
+                   
'<table:filter><table:filter-and>${f}</table:filter-and></table:filter></table:database-range></table:database-ranges>',
+         fields={"f": '<table:filter-condition table:field-number="0" 
table:operator="=" table:data-type="background-color" table:value="#ff0000"/>'
+                      '<table:filter-condition table:field-number="0" 
table:operator="=" table:data-type="text-color" table:value="#0000ff"/>'},
+         base={"f": '<table:filter-condition table:field-number="0" 
table:operator="=" table:value="a"/>'
+                    '<table:filter-condition table:field-number="0" 
table:operator="!=" table:value="b"/>'}),
+    dict(name="chart-major-origin", ext="odc", construct='chart:major-origin 
and chart:minor-logarithmic on style:chart-properties',
+         auto='<style:style style:name="ch1" 
style:family="chart"><style:chart-properties ${a}/></style:style>',
+         content='<chart:chart 
chart:class="chart:bar"><chart:plot-area><chart:axis chart:dimension="y" 
chart:name="primary-y" 
chart:style-name="ch1"/></chart:plot-area></chart:chart>',
+         fields={"a": 'chart:major-origin="0" 
chart:minor-logarithmic="true"'}, base={}),
+    dict(name="allow-overlap-decorative", ext="odt", 
construct='draw:allow-overlap and draw:decorative on style:graphic-properties',
+         auto=GSTYLE % ("G1", '<style:graphic-properties ${a}/>'), 
content='<text:p>' + FRAME % (' draw:style-name="G1"', "") + '</text:p>',
+         fields={"a": 'draw:allow-overlap="true" draw:decorative="true"'}, 
base={}),
+    dict(name="margin-gutter", ext="odt", construct='style:margin-gutter on 
style:page-layout-properties',
+         content=TXT, fields={"pl": 'fo:margin-left="2cm" 
style:margin-gutter="1cm"'}, base={"pl": 'fo:margin-left="2cm"'}),
+    dict(name="relative-tab-stop-position", ext="odt", 
construct='text:relative-tab-stop-position on style:paragraph-properties',
+         auto=pstyle("P1", '<style:paragraph-properties 
${a}><style:tab-stops><style:tab-stop 
style:position="5cm"/></style:tab-stops></style:paragraph-properties>'),
+         content='<text:p text:style-name="P1">A<text:tab/>B</text:p>', 
fields={"a": 'text:relative-tab-stop-position="false"'}, base={}),
+    dict(name="start-value-zero", ext="odt", construct='text:start-value="0" 
on text:list-level-style-number',
+         auto='<text:list-style style:name="L1"><text:list-level-style-number 
text:level="1" style:num-format="1" 
text:start-value="${v}"/></text:list-style>',
+         content='<text:list 
text:style-name="L1"><text:list-item><text:p>Zero</text:p></text:list-item><text:list-item><text:p>One</text:p></text:list-item></text:list>',
+         fields={"v": "0"}, base={"v": "1"}),
+    dict(name="handle-new-attributes", ext="odg", construct='draw:handle with 
only draw:handle-position-x/-y, and a polar one with -polar-pole-x/-y, 
-polar-radius/-angle',
+         content=GEOM, fields={"eg": "", "handles": '<draw:handle 
draw:handle-position-x="10800" draw:handle-position-y="0"/>'
+                               '<draw:handle draw:handle-polar-pole-x="10800" 
draw:handle-polar-pole-y="10800" draw:handle-polar-radius="5000" 
draw:handle-polar-angle="0"/>'},
+         base={"handles": ""}),
+]
+
+
+def build(case, base):
+    mime, body, wrap, paged = KINDS[case["ext"]]
+    f = {k: "" for k in case["fields"]}
+    f.update(case["base"] if base else case["fields"])
+    f = {k: Template(v).substitute(f) for k, v in f.items()}  # fields may 
refer to other fields, one level
+    sub = lambda s: Template(s).substitute(f)
+    content = sub(Template(wrap).substitute(content=case["content"]))
+    head = '<?xml version="1.0" encoding="UTF-8"?>\n<%s ' + NSDECL + ' 
office:version="1.4">%s</%s>\n'
+    cbody = 
'<office:automatic-styles>%s</office:automatic-styles><office:body><%s>%s</%s></office:body>'
 % (sub(case.get("auto", "")), body, content, body)
+    sbody = '<office:styles>%s</office:styles>' % sub(case.get("styles", ""))
+    if paged:
+        sbody += ('<office:automatic-styles><style:page-layout 
style:name="PM1"><style:page-layout-properties 
%s/></style:page-layout></office:automatic-styles>'
+                  '<office:master-styles><style:master-page 
style:name="Default" style:page-layout-name="PM1"/></office:master-styles>') % 
f.get("pl", "")
+    streams = {"content.xml": head % ("office:document-content", cbody, 
"office:document-content"),
+               "styles.xml": head % ("office:document-styles", sbody, 
"office:document-styles")}
+    entries = "".join('<manifest:file-entry manifest:full-path="%s" 
manifest:media-type="%s"/>' % (n, "image/png" if n.endswith(".png") else 
"text/xml")
+                      for n in list(streams) + list(case.get("extra", {})))
+    mani = ('<?xml version="1.0" encoding="UTF-8"?>\n<manifest:manifest 
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" 
manifest:version="1.4">'
+            '<manifest:file-entry manifest:full-path="/" 
manifest:version="1.4" manifest:media-type="%s"/>%s</manifest:manifest>\n') % 
(mime, entries)
+    path = os.path.join(OUT, case["name"] + ("-base" if base else "") + "." + 
case["ext"])
+    with zipfile.ZipFile(path, "w") as z:
+        def put(n, data, comp):
+            z.writestr(zipfile.ZipInfo(n, (2026, 11, 3, 0, 0, 0)), data, 
compress_type=comp)
+        put("mimetype", mime, zipfile.ZIP_STORED)
+        for n, t in streams.items():
+            put(n, t, zipfile.ZIP_DEFLATED)
+        for n, d in case.get("extra", {}).items():
+            put(n, d, zipfile.ZIP_STORED)
+        put("META-INF/manifest.xml", mani, zipfile.ZIP_DEFLATED)
+    return path
+
+
+def report(valjson):
+    val = json.load(open(valjson))
+    count = {v: {} for v in ("13", "14")}
+    for v in count:
+        for e in val["errors"][v]:
+            # validate.py only rewrites a 1.2/1.3 version to the target, so 
every 1.4 file draws its three version errors under 1.3
+            if v == "13" and e[3].startswith(('value of attribute 
"office:version"', 'value of attribute "manifest:version"')):
+                continue
+            count[v][e[0].split("/")[0]] = count[v].get(e[0].split("/")[0], 0) 
+ 1
+    rows = []
+    for c in CASES:
+        for base in (False, True):
+            if base and c["base"] is None:
+                continue
+            n = c["name"] + ("-base" if base else "") + "." + c["ext"]
+            if n in val["packages"]:
+                rows.append((n, "(control without the construct)" if base else 
c["construct"], count["14"].get(n, 0), count["13"].get(n, 0)))
+            else:
+                rows.append((n, "NOT VALIDATED", -1, -1))
+    w = max(len(r[0]) for r in rows)
+    print("%-*s  %4s  %4s  construct  (1.3 count leaves out the 
office:version/manifest:version errors)" % (w, "package", "1.4", "1.3"))
+    for n, k, e14, e13 in rows:
+        print("%-*s  %4d  %4d  %s" % (w, n, e14, e13, k))
+
+
+if len(sys.argv) == 3 and sys.argv[1] == "--report":
+    report(sys.argv[2])
+else:
+    os.makedirs(OUT, exist_ok=True)
+    for c in CASES:
+        print(build(c, False))
+        if c["base"] is not None:
+            print(build(c, True))
diff --git a/test/odf-validate/fixtures/t14/resave.py 
b/test/odf-validate/fixtures/t14/resave.py
new file mode 100644
index 0000000000..03fe4214f7
--- /dev/null
+++ b/test/odf-validate/fixtures/t14/resave.py
@@ -0,0 +1,44 @@
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+# usage: runpy.sh fixtures/t14/resave.py <port> <outdir> <tag> <input>...
+# Loads each input hidden and saves it again (writer8, calc8, impress8, draw8 
by extension) as <outdir>/<tag>-<name>, with whatever
+# ODF version the running office is configured for. Used with an older office 
(ODFV_APP=/Applications/OpenOffice5.app) to see
+# what it keeps of a file written in a newer ODF version.
+import os, sys
+import uno
+from unohelp import FILTERS, connect, desktop, pv
+
+port, out, tag = int(sys.argv[1]), os.path.abspath(sys.argv[2]), sys.argv[3]
+os.makedirs(out, exist_ok=True)
+ctx = connect(port)
+desk = desktop(ctx)
+for src in sys.argv[4:]:
+    src = os.path.abspath(src)
+    doc = desk.loadComponentFromURL(uno.systemPathToFileUrl(src), "_blank", 0, 
(pv("Hidden", True), pv("ReadOnly", True)))
+    if doc is None:
+        print("could not load", src)
+        continue
+    dst = os.path.join(out, tag + "-" + os.path.basename(src))
+    if os.path.exists(dst):
+        os.remove(dst)
+    doc.storeToURL(uno.systemPathToFileUrl(dst), (pv("FilterName", 
FILTERS[os.path.splitext(src)[1][1:]]), pv("Overwrite", True)))
+    doc.close(True)
+    print("resaved", os.path.basename(src))
diff --git a/test/odf-validate/fixtures/t14/run-in-mode.sh 
b/test/odf-validate/fixtures/t14/run-in-mode.sh
new file mode 100755
index 0000000000..744674760f
--- /dev/null
+++ b/test/odf-validate/fixtures/t14/run-in-mode.sh
@@ -0,0 +1,41 @@
+#!/bin/zsh
+#**************************************************************
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+#**************************************************************
+# usage: run-in-mode.sh <strict|ext|v13|x13|v14|x14> <port> <outdir> 
<script.py> [args...]
+# Starts an office whose profile (runs/profile_t14_<mode>) sets 
Save/ODF/DefaultVersion for the mode, runs the pyuno script in it
+# and stops the office. Use a port that no gate is using.
+source ${0:A:h}/../../env.sh
+mode=$1; port=$2; out=$3; script=$4; shift 4
+case $mode in ext) v=3;; strict) v=4;; v13) v=5;; x13) v=6;; v14) v=7;; x14) 
v=8;; *) echo "run-in-mode.sh: unknown mode $mode" >&2; exit 2;; esac
+prof=t14_$mode
+lsof -nP -iTCP:$port -sTCP:LISTEN >/dev/null 2>&1 && { echo "run-in-mode.sh: 
port $port is in use" >&2; exit 2; }
+mkdir -p $D/runs/profile_$prof/user
+cat > $D/runs/profile_$prof/user/registrymodifications.xcu <<XCU
+<?xml version="1.0" encoding="UTF-8"?>
+<oor:items xmlns:oor="http://openoffice.org/2001/registry"; 
xmlns:xs="http://www.w3.org/2001/XMLSchema"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+<item oor:path="/org.openoffice.Office.Common/Save/ODF"><prop 
oor:name="DefaultVersion" oor:op="fuse"><value>$v</value></prop></item>
+</oor:items>
+XCU
+$D/start.sh $prof $port || exit 2
+(cd $D && gtimeout 180 $D/runpy.sh $script $port $out $mode "$@")
+rc=$?
+pkill -f "profile_$prof"; sleep 2
+exit $rc
diff --git a/test/odf-validate/gate.py b/test/odf-validate/gate.py
index d2f1c96011..00d90e1299 100644
--- a/test/odf-validate/gate.py
+++ b/test/odf-validate/gate.py
@@ -267,7 +267,7 @@ class Gate:
             bad.append("shall_formulas %d > %d in --prev" % 
(r["shall_formulas"], pf))
         return bad
 
-    def validate_dir(self, md, out, kinds_base, label):
+    def validate_dir(self, md, out, kinds_base, label, 
kinds_file="kinds.json"):
         """validate.py + report.py + the checkers on out/; returns a result 
dict."""
         r = {}
         log = os.path.join(md, "validate.log")
@@ -286,21 +286,23 @@ class Gate:
             r["shall_" + name] = int(m.group(1)) if m and o.returncode == 0 
else None
         base = kinds_base
         if base and os.path.exists(base):
-            r["vs_baseline"] = run(PY + [os.path.join(D, "compare.py"), base, 
os.path.join(md, "kinds.json")], os.path.join(md, "compare_base.log"))
+            r["vs_baseline"] = run(PY + [os.path.join(D, "compare.py"), base, 
os.path.join(md, kinds_file)], os.path.join(md, "compare_base.log"))
         return r
 
     def validate(self):
         res = {}
         for mode in self.modes:
             md = os.path.join(self.run, mode)
-            r = self.validate_dir(md, os.path.join(md, "out"), os.path.join(D, 
"baseline", mode + ".kinds.json"), mode)
-            pk = self.prev_path(mode + "/kinds.json")
+            # the ODF 1.4 modes are measured against the 1.4 schema, the 
others against 1.2 as written
+            kf = {"v14": "kinds14.json", "x14": "kinds14x.json"}.get(mode, 
"kinds.json")
+            r = self.validate_dir(md, os.path.join(md, "out"), os.path.join(D, 
"baseline", mode + ".kinds.json"), mode, kf)
+            pk = self.prev_path(mode + "/" + kf)
             r["problems"] = self.shall_problems(r, 
self.prev_validate("validate", mode))
             if not self.a.limit and not pk and "vs_baseline" not in r:
                 r["problems"].append("no baseline/%s.kinds.json and no --prev 
kinds to compare with" % mode)
             if pk and not self.a.limit:
-                r["vs_prev"] = run(PY + [os.path.join(D, "compare.py"), pk, 
os.path.join(md, "kinds.json")], os.path.join(md, "compare_prev.log"))
-                r["vs_prev_reverse"] = run(PY + [os.path.join(D, 
"compare.py"), os.path.join(md, "kinds.json"), pk], os.path.join(md, 
"compare_prev_rev.log"))
+                r["vs_prev"] = run(PY + [os.path.join(D, "compare.py"), pk, 
os.path.join(md, kf)], os.path.join(md, "compare_prev.log"))
+                r["vs_prev_reverse"] = run(PY + [os.path.join(D, 
"compare.py"), os.path.join(md, kf), pk], os.path.join(md, 
"compare_prev_rev.log"))
             res[mode] = r
         res["ok"] = all(r.get("vs_baseline", 0) == 0 and r.get("vs_prev", 0) 
== 0 and r.get("vs_prev_reverse", 0) == 0 and not r["problems"] for r in 
res.values())
         return res

Reply via email to