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 ""
