xref: /trunk/test/odf-validate/crosscheck.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
1*0e16a31dSJim Jagielski#**************************************************************
2*0e16a31dSJim Jagielski#
3*0e16a31dSJim Jagielski#  Licensed to the Apache Software Foundation (ASF) under one
4*0e16a31dSJim Jagielski#  or more contributor license agreements.  See the NOTICE file
5*0e16a31dSJim Jagielski#  distributed with this work for additional information
6*0e16a31dSJim Jagielski#  regarding copyright ownership.  The ASF licenses this file
7*0e16a31dSJim Jagielski#  to you under the Apache License, Version 2.0 (the
8*0e16a31dSJim Jagielski#  "License"); you may not use this file except in compliance
9*0e16a31dSJim Jagielski#  with the License.  You may obtain a copy of the License at
10*0e16a31dSJim Jagielski#
11*0e16a31dSJim Jagielski#    http://www.apache.org/licenses/LICENSE-2.0
12*0e16a31dSJim Jagielski#
13*0e16a31dSJim Jagielski#  Unless required by applicable law or agreed to in writing,
14*0e16a31dSJim Jagielski#  software distributed under the License is distributed on an
15*0e16a31dSJim Jagielski#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16*0e16a31dSJim Jagielski#  KIND, either express or implied.  See the License for the
17*0e16a31dSJim Jagielski#  specific language governing permissions and limitations
18*0e16a31dSJim Jagielski#  under the License.
19*0e16a31dSJim Jagielski#
20*0e16a31dSJim Jagielski#**************************************************************
21*0e16a31dSJim Jagielski
22*0e16a31dSJim Jagielski# usage: crosscheck.py <outdir> <val.json> <cross.json>
23*0e16a31dSJim Jagielski# Validates every package in <outdir> with the Apache ODF Toolkit odfvalidator (fetch-odfvalidator.sh; MSV engine,
24*0e16a31dSJim Jagielski# the OASIS schemas given explicitly) in conformance mode (-c, compared with validate.py's "12") and extended
25*0e16a31dSJim Jagielski# conformance mode (-e, compared with "12x"), and compares per stream with the jing results in <val.json>.
26*0e16a31dSJim Jagielski# Error counts are not comparable between engines (different error recovery); the comparison is whether a stream
27*0e16a31dSJim Jagielski# has any error and where its first error is.
28*0e16a31dSJim Jagielskiimport sys, os, re, json, subprocess, collections
29*0e16a31dSJim Jagielskifrom xml.sax.saxutils import escape
30*0e16a31dSJim Jagielski
31*0e16a31dSJim JagielskiD = os.path.dirname(os.path.abspath(__file__))
32*0e16a31dSJim JagielskiJAR = os.environ.get("ODFV_ODFVALIDATOR", D + "/tools/odfvalidator-1.1.8-incubating-jar-with-dependencies.jar")
33*0e16a31dSJim JagielskiJAVA = os.environ.get("ODFV_JAVA", "java")
34*0e16a31dSJim JagielskiSCH = D + "/schema/"
35*0e16a31dSJim JagielskiSTREAMS = {"content.xml", "styles.xml", "meta.xml", "settings.xml", "manifest.xml"}
36*0e16a31dSJim JagielskiLINE = re.compile(r"^(?P<path>/.*?)(?:\[(?P<l>\d+),(?P<c>\d+)\])?:\s+(?P<lvl>Error|Fatal|Warning|Info):\s*(?P<msg>.*)$")
37*0e16a31dSJim Jagielski# odfvalidator needs a MathML 2 schema for embedded formula objects and its jar has none; this accepts anything,
38*0e16a31dSJim Jagielski# which is what validate.py does too (it skips MathML).
39*0e16a31dSJim JagielskiANY = ('<grammar xmlns="http://relaxng.org/ns/structure/1.0"><start><ref name="e"/></start>'
40*0e16a31dSJim Jagielski       '<define name="e"><element><anyName/><interleave><zeroOrMore><attribute><anyName/></attribute></zeroOrMore>'
41*0e16a31dSJim Jagielski       '<zeroOrMore><choice><ref name="e"/><text/></choice></zeroOrMore></interleave></element></define></grammar>')
42*0e16a31dSJim Jagielski
43*0e16a31dSJim Jagielski
44*0e16a31dSJim Jagielskidef run(flag, outdir, pkgs, work, tag):
45*0e16a31dSJim Jagielski    """Returns (events, crashed packages); events are (package, entry, line, col, level, message).
46*0e16a31dSJim Jagielski    Exits if odfvalidator stops without a completion line for every package: its own failures are silent otherwise."""
47*0e16a31dSJim Jagielski    open(work + "/any.rng", "w").write(ANY)
48*0e16a31dSJim Jagielski    events, crashed, todo, round_ = [], [], list(pkgs), 0
49*0e16a31dSJim Jagielski    while todo:
50*0e16a31dSJim Jagielski        round_ += 1
51*0e16a31dSJim Jagielski        cfg = work + "/%s.%d.xml" % (tag, round_)
52*0e16a31dSJim Jagielski        entries = {"schema": SCH + "OpenDocument-v1.2-os-schema.rng", "manifest-schema": SCH + "OpenDocument-v1.2-os-manifest-schema.rng",
53*0e16a31dSJim Jagielski                   "dsig-schema": SCH + "OpenDocument-v1.2-os-dsig-schema.rng", "mathml2-schema": work + "/any.rng"}
54*0e16a31dSJim Jagielski        for i, p in enumerate(todo):
55*0e16a31dSJim Jagielski            entries["path%06d" % i] = os.path.join(outdir, p)
56*0e16a31dSJim Jagielski        open(cfg, "w").write('<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">\n<properties>\n'
57*0e16a31dSJim Jagielski                             + "".join('<entry key="%s">%s</entry>\n' % (k, escape(v)) for k, v in entries.items()) + "</properties>\n")
58*0e16a31dSJim Jagielski        r = subprocess.run([JAVA, "-jar", JAR, "-v", flag, "-C", cfg], capture_output=True, text=True, timeout=3600)
59*0e16a31dSJim Jagielski        text = r.stdout + r.stderr
60*0e16a31dSJim Jagielski        done = set()
61*0e16a31dSJim Jagielski        prefix = os.path.join(outdir, "")
62*0e16a31dSJim Jagielski        for line in text.splitlines():
63*0e16a31dSJim Jagielski            m = LINE.match(line)
64*0e16a31dSJim Jagielski            if not m or not m.group("path").startswith(prefix):
65*0e16a31dSJim Jagielski                continue
66*0e16a31dSJim Jagielski            rest = m.group("path")[len(prefix):]
67*0e16a31dSJim Jagielski            pkg, _, entry = rest.partition("/")
68*0e16a31dSJim Jagielski            if m.group("lvl") == "Info" and entry == "" and re.match(r"no errors|\d+ errors", m.group("msg")):
69*0e16a31dSJim Jagielski                done.add(pkg)
70*0e16a31dSJim Jagielski            if m.group("lvl") != "Info":
71*0e16a31dSJim Jagielski                events.append((pkg, entry, int(m.group("l") or 0), int(m.group("c") or 0), m.group("lvl"), m.group("msg")))
72*0e16a31dSJim Jagielski        pending = [p for p in todo if p not in done]
73*0e16a31dSJim Jagielski        if "Exception in thread" in text and pending:
74*0e16a31dSJim Jagielski            crashed.append(pending[0])
75*0e16a31dSJim Jagielski            todo = todo[todo.index(pending[0]) + 1:]
76*0e16a31dSJim Jagielski        elif pending:
77*0e16a31dSJim Jagielski            sys.exit("crosscheck.py: odfvalidator %s did not finish %d of %d packages (first: %s); output tail:\n%s"
78*0e16a31dSJim Jagielski                     % (flag, len(pending), len(todo), pending[0], "\n".join(text.splitlines()[-4:])))
79*0e16a31dSJim Jagielski        else:
80*0e16a31dSJim Jagielski            todo = []
81*0e16a31dSJim Jagielski    return events, crashed
82*0e16a31dSJim Jagielski
83*0e16a31dSJim Jagielski
84*0e16a31dSJim Jagielskidef compare(events, jing_errors, pkgs):
85*0e16a31dSJim Jagielski    odf = collections.defaultdict(list)
86*0e16a31dSJim Jagielski    for pkg, entry, l, c, lvl, msg in events:
87*0e16a31dSJim Jagielski        if lvl in ("Error", "Fatal") and entry.rsplit("/", 1)[-1] in STREAMS:
88*0e16a31dSJim Jagielski            odf[pkg + "/" + entry].append((l, c, msg))
89*0e16a31dSJim Jagielski    jing = collections.defaultdict(list)
90*0e16a31dSJim Jagielski    for f, l, c, msg in jing_errors:
91*0e16a31dSJim Jagielski        if f != "?":
92*0e16a31dSJim Jagielski            jing[f].append((l, c, msg))
93*0e16a31dSJim Jagielski    both, same_pos, only_jing, only_odf = 0, 0, [], []
94*0e16a31dSJim Jagielski    diffpos = []
95*0e16a31dSJim Jagielski    for k in sorted(set(odf) | set(jing)):
96*0e16a31dSJim Jagielski        if k in odf and k in jing:
97*0e16a31dSJim Jagielski            both += 1
98*0e16a31dSJim Jagielski            if min(odf[k])[:2] == min(jing[k])[:2]:
99*0e16a31dSJim Jagielski                same_pos += 1
100*0e16a31dSJim Jagielski            else:
101*0e16a31dSJim Jagielski                diffpos.append([k, min(jing[k])[:2], min(odf[k])[:2], min(jing[k])[2][:100], min(odf[k])[2][:100]])
102*0e16a31dSJim Jagielski        elif k in jing:
103*0e16a31dSJim Jagielski            only_jing.append([k, min(jing[k])[:2], min(jing[k])[2][:140]])
104*0e16a31dSJim Jagielski        else:
105*0e16a31dSJim Jagielski            only_odf.append([k, min(odf[k])[:2], min(odf[k])[2][:140]])
106*0e16a31dSJim Jagielski    pkgmsgs = collections.Counter()
107*0e16a31dSJim Jagielski    for pkg, entry, l, c, lvl, msg in events:
108*0e16a31dSJim Jagielski        if entry.rsplit("/", 1)[-1] not in STREAMS and entry.rsplit("/", 1)[-1] != "":
109*0e16a31dSJim Jagielski            pkgmsgs["%s %s: %s" % (lvl, entry.rsplit("/", 1)[-1], re.sub(r"\d+", "N", msg)[:120])] += 1
110*0e16a31dSJim Jagielski        elif entry == "":
111*0e16a31dSJim Jagielski            pkgmsgs["%s (package): %s" % (lvl, re.sub(r"\d+", "N", msg)[:120])] += 1
112*0e16a31dSJim Jagielski    return {"odfvalidator_error_streams": sorted(odf), "streams_both_with_errors": both, "first_error_same_position": same_pos, "only_jing": only_jing, "only_odfvalidator": only_odf,
113*0e16a31dSJim Jagielski            "different_first_error": diffpos, "package_level": dict(pkgmsgs)}
114*0e16a31dSJim Jagielski
115*0e16a31dSJim Jagielski
116*0e16a31dSJim Jagielskidef main():
117*0e16a31dSJim Jagielski    outdir, valjson, crossjson = os.path.abspath(sys.argv[1]), sys.argv[2], sys.argv[3]
118*0e16a31dSJim Jagielski    if not os.path.abspath(crossjson).startswith(os.path.join(D, "runs") + os.sep):
119*0e16a31dSJim Jagielski        sys.exit("crosscheck.py: <cross.json> must live under " + os.path.join(D, "runs"))
120*0e16a31dSJim Jagielski    if not os.path.isfile(JAR):
121*0e16a31dSJim Jagielski        sys.exit("crosscheck.py: " + JAR + " not found; run fetch-odfvalidator.sh or set ODFV_ODFVALIDATOR")
122*0e16a31dSJim Jagielski    work = os.path.abspath(crossjson[:-5] + ".d")
123*0e16a31dSJim Jagielski    os.makedirs(work, exist_ok=True)
124*0e16a31dSJim Jagielski    val = json.load(open(valjson))
125*0e16a31dSJim Jagielski    pkgs = sorted(p for p in os.listdir(outdir) if not p.startswith(".") and p not in val.get("bad_zip", []))
126*0e16a31dSJim Jagielski    res = {}
127*0e16a31dSJim Jagielski    for flag, jv, name in (("-c", "12", "conformance"), ("-e", "12x", "extended")):
128*0e16a31dSJim Jagielski        events, crashed = run(flag, outdir, pkgs, work, name)
129*0e16a31dSJim Jagielski        res[name] = compare(events, val["errors"][jv], pkgs)
130*0e16a31dSJim Jagielski        res[name]["odfvalidator_crashed_on"] = crashed
131*0e16a31dSJim Jagielski        r = res[name]
132*0e16a31dSJim Jagielski        print(f"{name} (odfvalidator {flag} vs jing {jv}): both {r['streams_both_with_errors']} (same first position {r['first_error_same_position']}), "
133*0e16a31dSJim Jagielski              f"only jing {len(r['only_jing'])}, only odfvalidator {len(r['only_odfvalidator'])}, odfvalidator crashed on {len(crashed)} packages")
134*0e16a31dSJim Jagielski        for k, v in sorted(r["package_level"].items()):
135*0e16a31dSJim Jagielski            print(f"    {v:5d} {k}")
136*0e16a31dSJim Jagielski    json.dump(res, open(crossjson, "w"), indent=1)
137*0e16a31dSJim Jagielski
138*0e16a31dSJim Jagielski
139*0e16a31dSJim Jagielskimain()
140