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