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 fbc76c7d28640d3e663ac0dbfe0daecb23c9f5a0
Author: Jim Jagielski <[email protected]>
AuthorDate: Tue Nov 3 07:21:40 2026 -0500

    test/odf-validate: dump every property of the objects the ODF 1.4 consumer 
packages touch
---
 test/odf-validate/consumer_smoke.py             |   5 +-
 test/odf-validate/dump14.py                     | 210 ++++++++++++++++++++++++
 test/odf-validate/fixtures/t14/make-consumer.py |   7 +-
 test/odf-validate/gate.py                       |   4 +-
 4 files changed, 219 insertions(+), 7 deletions(-)

diff --git a/test/odf-validate/consumer_smoke.py 
b/test/odf-validate/consumer_smoke.py
index da50cf9c3c..1f8b998caa 100644
--- a/test/odf-validate/consumer_smoke.py
+++ b/test/odf-validate/consumer_smoke.py
@@ -20,7 +20,7 @@
 
 # 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
+# with the office of the app copy, dumps every property of the objects it 
touches (dump14.py, or dump7.py with --dumper; 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).
@@ -38,13 +38,14 @@ def main():
     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"))
+    ap.add_argument("--dumper", default="dump14.py", help="dump14.py (every 
property of the touched objects) or dump7.py (the gate's fixed set)")
     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)
+    r = gate.dump_dir(a.dir, dest, "consumer_" + a.tag, a.port, a.stall, log, 
dumper=a.dumper)
     gate.kill_office("consumer_" + a.tag)
     names = sorted(f[:-5] for f in os.listdir(dest) if f.endswith(".json"))
     summary = {}
diff --git a/test/odf-validate/dump14.py b/test/odf-validate/dump14.py
new file mode 100644
index 0000000000..f680165b92
--- /dev/null
+++ b/test/odf-validate/dump14.py
@@ -0,0 +1,210 @@
+#**************************************************************
+#
+#  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 dump14.py <port> <outdir> <file>...
+# Dump for consumer_smoke.py: loads each ODF package read-only and writes 
EVERY property value (not a fixed list, unlike dump7.py)
+# of the objects the ODF 1.4 additions touch: paragraphs, user-defined styles, 
text frames, numbering rules, page styles, custom
+# shape geometry, database range filters, chart axes. A construct the office 
reads then shows up as a difference against the twin
+# without anyone having to guess its property name. Same output layout and 
.current marker as dump7.py (diff7.py compares it).
+import sys, os, json, hashlib, uno
+from unohelp import connect, desktop, load
+
+port, outdir, files = int(sys.argv[1]), sys.argv[2], sys.argv[3:]
+CAP = 100
+
+
+def conv(x, depth=0):
+    if depth > 8:
+        return "<deep>"
+    if isinstance(x, uno.Enum):
+        return x.value
+    if x is None or isinstance(x, (bool, int, float)):
+        return x
+    if isinstance(x, str):
+        return x[:300]
+    if isinstance(x, bytes):
+        return "bytes:" + hashlib.sha256(x).hexdigest()[:12]
+    if isinstance(x, (tuple, list)):
+        return [conv(i, depth + 1) for i in x][:CAP]
+    if hasattr(x, "typeName"):
+        if x.typeName.endswith(".PropertyValue"):
+            return [x.Name, conv(x.Value, depth + 1)]
+        try:
+            return {k: conv(getattr(x, k), depth + 1) for k in dir(x) if not 
k.startswith("_") and k not in ("typeName", "value")}
+        except Exception:
+            return "<struct>"
+    if hasattr(x, "getCount") and hasattr(x, "getByIndex"):
+        try:
+            return [conv(x.getByIndex(i), depth + 1) for i in 
range(min(x.getCount(), 20))]
+        except Exception:
+            return "<index>"
+    return "<obj>"
+
+
+def tryf(f, d="n/a"):
+    try:
+        return f()
+    except Exception:
+        return d
+
+
+NOISE = ("RuntimeUID", "ListId", "NumberingStyleName")   # differ between any 
two documents
+
+
+def allprops(o):
+    """Every readable property of o; values that are interfaces (conv gives 
'<obj>') are left out."""
+    out = {}
+    try:
+        names = sorted(p.Name for p in o.getPropertySetInfo().getProperties())
+    except Exception:
+        return out
+    for n in names:
+        if n in NOISE:
+            continue
+        try:
+            val = conv(o.getPropertyValue(n))
+        except Exception:
+            val = "n/a"
+        if val != "<obj>":
+            out[n] = val
+    return out
+
+
+def styles(doc, family):
+    """User-defined styles of a family, plus the default page/paragraph 
style."""
+    out = {}
+    try:
+        fam = doc.StyleFamilies.getByName(family)
+    except Exception:
+        return out
+    for n in fam.ElementNames:
+        s = fam.getByName(n)
+        if tryf(lambda: s.isUserDefined(), False) or n in ("Standard", 
"Default", "Default Style"):
+            out[n] = allprops(s)
+    return out
+
+
+def shapes(page, limit=100):
+    out = []
+    for k in range(min(page.Count, limit)):
+        s = page.getByIndex(k)
+        d = {"type": tryf(lambda: s.ShapeType), "text": tryf(lambda: 
s.String[:80]), "props": allprops(s)}
+        if tryf(lambda: s.supportsService("com.sun.star.drawing.GroupShape"), 
False):
+            d["kids"] = shapes(s, 30)
+        out.append(d)
+    return out
+
+
+def writer(doc):
+    o = {"paras": [], "frames": {}}
+    e = doc.Text.createEnumeration()
+    while e.hasMoreElements() and len(o["paras"]) < 50:
+        p = e.nextElement()
+        if p.supportsService("com.sun.star.text.Paragraph"):
+            o["paras"].append({"text": p.String[:80], "props": allprops(p)})
+    for n in doc.TextFrames.ElementNames[:30]:
+        o["frames"][n] = allprops(doc.TextFrames.getByName(n))
+    o["shapes"] = shapes(doc.DrawPage)
+    o["parastyles"] = styles(doc, "ParagraphStyles")
+    o["framestyles"] = styles(doc, "FrameStyles")
+    o["pagestyles"] = styles(doc, "PageStyles")
+    o["liststyles"] = styles(doc, "NumberingStyles")
+    o["settings"] = 
allprops(doc.createInstance("com.sun.star.document.Settings"))
+    o["docprops"] = allprops(doc)
+    return o
+
+
+def calc(doc):
+    o = {"sheets": []}
+    for i in range(doc.Sheets.Count):
+        sh = doc.Sheets.getByIndex(i)
+        so = {"name": sh.Name, "props": allprops(sh), "cells": {}}
+        for r in range(0, 5):
+            for c in range(0, 3):
+                cell = sh.getCellByPosition(c, r)
+                if cell.Type.value != "EMPTY":
+                    so["cells"]["%d,%d" % (c, r)] = {"type": cell.Type.value, 
"error": cell.getError(), "string": cell.String[:80], "props": allprops(cell)}
+        dbs = {}
+        for n in tryf(lambda: doc.DatabaseRanges.ElementNames, []):
+            dr = doc.DatabaseRanges.getByName(n)
+            fd = tryf(lambda: dr.getFilterDescriptor(), None)
+            dbs[n] = {"props": allprops(dr), "filter": allprops(fd) if fd else 
None,
+                      "fields": conv(tryf(lambda: fd.getFilterFields(), []))}
+        so["dbranges"] = dbs
+        so["shapes"] = shapes(sh.DrawPage)
+        o["sheets"].append(so)
+    o["cellstyles"] = styles(doc, "CellStyles")
+    o["pagestyles"] = styles(doc, "PageStyles")
+    o["docprops"] = allprops(doc)
+    return o
+
+
+def draw(doc):
+    o = {"pages": []}
+    for i in range(doc.DrawPages.Count):
+        pg = doc.DrawPages.getByIndex(i)
+        o["pages"].append({"props": allprops(pg), "shapes": shapes(pg), 
"annotations": tryf(lambda: conv(pg.Annotations), "n/a") if hasattr(pg, 
"Annotations") else "n/a"})
+    o["graphicstyles"] = styles(doc, "graphics")
+    o["pagestyles"] = styles(doc, "PageStyles")
+    return o
+
+
+def chart(doc):
+    o = {}
+    try:
+        cs = doc.getFirstDiagram().getCoordinateSystems()[0]
+        o["coordsys"] = allprops(cs)
+        for dim in range(cs.getDimension()):
+            for idx in range(cs.getMaximumAxisIndexByDimension(dim) + 1):
+                ax = cs.getAxisByDimension(dim, idx)
+                o["axis%d_%d" % (dim, idx)] = {"props": allprops(ax), "scale": 
conv(ax.getScaleData())}
+    except Exception as e:
+        o["error"] = repr(e)[:100]
+    o["docprops"] = allprops(doc)
+    return o
+
+
+def has(doc, s):
+    return tryf(lambda: doc.supportsService(s), False)
+
+
+DESK = desktop(connect(port))
+os.makedirs(outdir, exist_ok=True)
+for f in files:
+    dest = os.path.join(outdir, os.path.basename(f) + ".json")
+    if os.path.exists(dest):
+        continue
+    open(os.path.join(outdir, ".current"), "w").write(f)
+    out = {}
+    try:
+        doc = load(DESK, f)
+        out["type"] = ("writer" if has(doc, "com.sun.star.text.TextDocument") 
else "calc" if has(doc, "com.sun.star.sheet.SpreadsheetDocument")
+                       else "impress" if has(doc, 
"com.sun.star.presentation.PresentationDocument") else "draw" if has(doc, 
"com.sun.star.drawing.DrawingDocument")
+                       else "chart" if has(doc, 
"com.sun.star.chart2.ChartDocument") else "other")
+        try:
+            out["body"] = {"writer": writer, "calc": calc, "impress": draw, 
"draw": draw, "chart": chart}.get(out["type"], lambda d: {})(doc)
+        except Exception as e:
+            out["body_error"] = repr(e)[:200]
+        doc.close(True)
+    except Exception as e:
+        out["error"] = repr(e)[:200]
+    json.dump(out, open(dest, "w"), sort_keys=True, indent=0, default=str)
+print("done", len(files))
diff --git a/test/odf-validate/fixtures/t14/make-consumer.py 
b/test/odf-validate/fixtures/t14/make-consumer.py
index 0894cf5c91..f33a539bae 100644
--- a/test/odf-validate/fixtures/t14/make-consumer.py
+++ b/test/odf-validate/fixtures/t14/make-consumer.py
@@ -45,7 +45,7 @@ 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>'
+FRAME = '<draw:frame draw:name="F1" text:anchor-type="paragraph" 
svg:width="4cm"%s><draw:text-box 
fo:min-height="2cm"%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>'
 
@@ -74,9 +74,10 @@ CASES = [
          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": ""}),
+         base={"eg": 'draw:extrusion="true" draw:extrusion-metal="true"', 
"handles": ""}),
     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%"}),
+         content=GEOM, fields={"eg": 'draw:extrusion="true" 
draw:extrusion-specularity="${v}"', "v": "150%", "handles": ""},
+         base={"eg": 'draw:extrusion="true" draw:extrusion-specularity="80%"', 
"handles": ""}),
     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}"/>'),
diff --git a/test/odf-validate/gate.py b/test/odf-validate/gate.py
index 00d90e1299..9d2dfd9fa1 100644
--- a/test/odf-validate/gate.py
+++ b/test/odf-validate/gate.py
@@ -114,7 +114,7 @@ def bridge_dead(path):
     return isinstance(d, dict) and (str(d.get("error", "")).startswith("Binary 
URP bridge") or str(d.get("body_error", "")).startswith("Binary URP bridge"))
 
 
-def dump_dir(src, dest, profile, port, stall, log, tool=None, poll=2.0):
+def dump_dir(src, dest, profile, port, stall, log, tool=None, poll=2.0, 
dumper="dump7.py"):
     """Reloads every package in src into dest/*.json. A document that stalls 
the office or kills it gets an error
     marker only after `stall` seconds without progress, never because the 
whole batch took long.
     tool(port, dest, files) -> argv replaces dump7.py under the office (for 
tests); then no office is started."""
@@ -130,7 +130,7 @@ def dump_dir(src, dest, profile, port, stall, log, 
tool=None, poll=2.0):
                     return {"error": "office did not start", "stalls": stalls}
             before = count()
             marker = os.path.join(dest, ".current")
-            argv = tool(port, dest, files) if tool else [os.path.join(D, 
"runpy.sh"), os.path.join(D, "dump7.py"), str(port), dest] + files
+            argv = tool(port, dest, files) if tool else [os.path.join(D, 
"runpy.sh"), os.path.join(D, dumper), str(port), dest] + files
             how, rc = run_with_stall(argv, lambda: (count(), 
os.path.getmtime(marker) if os.path.exists(marker) else 0), stall, poll, 
log=log)
             if how == "stalled" or rc != 0:
                 cur = open(marker).read().strip() if os.path.exists(marker) 
else ""

Reply via email to