1*2cb6d6f1SJim Jagielski################################################################ 2*2cb6d6f1SJim Jagielski# 3*2cb6d6f1SJim Jagielski# Licensed to the Apache Software Foundation (ASF) under one 4*2cb6d6f1SJim Jagielski# or more contributor license agreements. See the NOTICE file 5*2cb6d6f1SJim Jagielski# distributed with this work for additional information 6*2cb6d6f1SJim Jagielski# regarding copyright ownership. The ASF licenses this file 7*2cb6d6f1SJim Jagielski# to you under the Apache License, Version 2.0 (the 8*2cb6d6f1SJim Jagielski# "License"); you may not use this file except in compliance 9*2cb6d6f1SJim Jagielski# with the License. You may obtain a copy of the License at 10*2cb6d6f1SJim Jagielski# 11*2cb6d6f1SJim Jagielski# http://www.apache.org/licenses/LICENSE-2.0 12*2cb6d6f1SJim Jagielski# 13*2cb6d6f1SJim Jagielski# Unless required by applicable law or agreed to in writing, 14*2cb6d6f1SJim Jagielski# software distributed under the License is distributed on an 15*2cb6d6f1SJim Jagielski# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16*2cb6d6f1SJim Jagielski# KIND, either express or implied. See the License for the 17*2cb6d6f1SJim Jagielski# specific language governing permissions and limitations 18*2cb6d6f1SJim Jagielski# under the License. 19*2cb6d6f1SJim Jagielski 20*2cb6d6f1SJim Jagielski"""Import twins: the same source saved as ODF 1.2 and as ODF 1.3 by another producer (plain and extended) must import alike. 21*2cb6d6f1SJim JagielskiThe corpus is a directory with v12, v13, v12ext and v13ext subdirectories holding the same file names. Each document is 22*2cb6d6f1SJim Jagielskiloaded once, dumped (aootest_worker.ops.dump) and the dumps of every pair are compared.""" 23*2cb6d6f1SJim Jagielskiimport json 24*2cb6d6f1SJim Jagielskiimport re 25*2cb6d6f1SJim Jagielskiimport zipfile 26*2cb6d6f1SJim Jagielskifrom pathlib import Path 27*2cb6d6f1SJim Jagielski 28*2cb6d6f1SJim Jagielskifrom aootest.checkers import diff7 29*2cb6d6f1SJim Jagielskifrom aootest.client import WorkerError, WorkerFailure, WorkerHang 30*2cb6d6f1SJim Jagielski 31*2cb6d6f1SJim JagielskiPAIRS = (("v12", "v13"), ("v12ext", "v13ext")) 32*2cb6d6f1SJim JagielskiMODES = sorted({m for p in PAIRS for m in p}) 33*2cb6d6f1SJim JagielskiSUFFIXES = (".odt", ".ods", ".odp", ".odg") 34*2cb6d6f1SJim Jagielski 35*2cb6d6f1SJim Jagielski 36*2cb6d6f1SJim Jagielskidef corpus_problem(root): 37*2cb6d6f1SJim Jagielski """Why `root` is not a usable twin corpus, or None.""" 38*2cb6d6f1SJim Jagielski for m in MODES: 39*2cb6d6f1SJim Jagielski if not (Path(root) / m).is_dir(): 40*2cb6d6f1SJim Jagielski return "no %s directory in %s" % (m, root) 41*2cb6d6f1SJim Jagielski names = [set(d.name for d in (Path(root) / m).iterdir() if d.suffix in SUFFIXES) for m in MODES] 42*2cb6d6f1SJim Jagielski if not names[0]: 43*2cb6d6f1SJim Jagielski return "no documents in %s" % (Path(root) / MODES[0]) 44*2cb6d6f1SJim Jagielski if any(n != names[0] for n in names): 45*2cb6d6f1SJim Jagielski return "the four directories do not hold the same file names" 46*2cb6d6f1SJim Jagielski return None 47*2cb6d6f1SJim Jagielski 48*2cb6d6f1SJim Jagielski 49*2cb6d6f1SJim Jagielskidef document_names(root, mode="v12"): 50*2cb6d6f1SJim Jagielski return sorted(p.name for p in (Path(root) / mode).iterdir() if p.suffix in SUFFIXES) 51*2cb6d6f1SJim Jagielski 52*2cb6d6f1SJim Jagielski 53*2cb6d6f1SJim Jagielskidef mutate(src, dst): 54*2cb6d6f1SJim Jagielski """Copy of an ODF text package with one word appended to the first paragraph that has text; False if there is none.""" 55*2cb6d6f1SJim Jagielski done = False 56*2cb6d6f1SJim Jagielski with zipfile.ZipFile(src) as zi, zipfile.ZipFile(dst, "w") as zo: 57*2cb6d6f1SJim Jagielski for it in zi.infolist(): 58*2cb6d6f1SJim Jagielski data = zi.read(it.filename) 59*2cb6d6f1SJim Jagielski if it.filename == "content.xml" and not done: 60*2cb6d6f1SJim Jagielski text = data.decode("utf-8") 61*2cb6d6f1SJim Jagielski text2, n = re.subn(r">([A-Za-z][^<>]{2,})</text:p>", r">\1 mutated</text:p>", text, count=1) 62*2cb6d6f1SJim Jagielski if n: 63*2cb6d6f1SJim Jagielski data, done = text2.encode("utf-8"), True 64*2cb6d6f1SJim Jagielski zo.writestr(it, data, compress_type=it.compress_type) 65*2cb6d6f1SJim Jagielski return done 66*2cb6d6f1SJim Jagielski 67*2cb6d6f1SJim Jagielski 68*2cb6d6f1SJim Jagielskidef dump_one(session, path, timeout=300): 69*2cb6d6f1SJim Jagielski """The dump of one document, or {"error": text} when it did not load, hung or killed the office.""" 70*2cb6d6f1SJim Jagielski try: 71*2cb6d6f1SJim Jagielski return session.call("dump_document", {"path": str(path)}, timeout=timeout) 72*2cb6d6f1SJim Jagielski except WorkerError as e: 73*2cb6d6f1SJim Jagielski return {"error": "%s: %s" % (e.kind, e.message)} 74*2cb6d6f1SJim Jagielski except WorkerHang as e: 75*2cb6d6f1SJim Jagielski return {"error": "HANG: %s" % e} 76*2cb6d6f1SJim Jagielski except WorkerFailure as e: 77*2cb6d6f1SJim Jagielski return {"error": "CRASH: %s" % e} 78*2cb6d6f1SJim Jagielski 79*2cb6d6f1SJim Jagielski 80*2cb6d6f1SJim Jagielskiclass TwinRun: 81*2cb6d6f1SJim Jagielski def __init__(self, session, root, workdir): 82*2cb6d6f1SJim Jagielski self.session, self.root, self.dir = session, Path(root), Path(workdir) 83*2cb6d6f1SJim Jagielski self._dumps, self._diffs, self._controls = {}, {}, None 84*2cb6d6f1SJim Jagielski 85*2cb6d6f1SJim Jagielski def dumps(self, mode, source_dir=None, tag=None): 86*2cb6d6f1SJim Jagielski """Directory of <document>.json dumps for the documents of `mode` (or of `source_dir`), written on first use.""" 87*2cb6d6f1SJim Jagielski tag = tag or mode 88*2cb6d6f1SJim Jagielski if tag not in self._dumps: 89*2cb6d6f1SJim Jagielski out = self.dir / "dump" / tag 90*2cb6d6f1SJim Jagielski out.mkdir(parents=True, exist_ok=True) 91*2cb6d6f1SJim Jagielski src = Path(source_dir) if source_dir else self.root / mode 92*2cb6d6f1SJim Jagielski for p in sorted(src.iterdir()): 93*2cb6d6f1SJim Jagielski if p.suffix in SUFFIXES: 94*2cb6d6f1SJim Jagielski (out / (p.name + ".json")).write_text(json.dumps(dump_one(self.session, p), sort_keys=True, indent=0, default=str)) 95*2cb6d6f1SJim Jagielski self._dumps[tag] = out 96*2cb6d6f1SJim Jagielski return self._dumps[tag] 97*2cb6d6f1SJim Jagielski 98*2cb6d6f1SJim Jagielski def load(self, mode, name): 99*2cb6d6f1SJim Jagielski return json.loads((self.dumps(mode) / (name + ".json")).read_text()) 100*2cb6d6f1SJim Jagielski 101*2cb6d6f1SJim Jagielski def diff(self, pair): 102*2cb6d6f1SJim Jagielski """{document: {"ndiff", "diffs", ...}} for the dumps of a pair of modes.""" 103*2cb6d6f1SJim Jagielski if pair not in self._diffs: 104*2cb6d6f1SJim Jagielski self._diffs[pair] = diff7.diff_dirs(str(self.dumps(pair[0])), str(self.dumps(pair[1]))) 105*2cb6d6f1SJim Jagielski return self._diffs[pair] 106*2cb6d6f1SJim Jagielski 107*2cb6d6f1SJim Jagielski def controls(self): 108*2cb6d6f1SJim Jagielski """Compares an unchanged copy and a copy with one changed word of a text document: ({"same", "mutated"} ndiff, base name), 109*2cb6d6f1SJim Jagielski or (None, reason). The comparison can only be trusted if the first is 0 and the second is not.""" 110*2cb6d6f1SJim Jagielski if self._controls is None: 111*2cb6d6f1SJim Jagielski base = None 112*2cb6d6f1SJim Jagielski for name in document_names(self.root): 113*2cb6d6f1SJim Jagielski d = self.load("v12", name) 114*2cb6d6f1SJim Jagielski if name.endswith(".odt") and d.get("type") == "writer" and (d.get("body") or {}).get("chars", 0) > 20: 115*2cb6d6f1SJim Jagielski base = self.root / "v12" / name 116*2cb6d6f1SJim Jagielski break 117*2cb6d6f1SJim Jagielski if base is None: 118*2cb6d6f1SJim Jagielski self._controls = (None, "no text document with characters in the corpus") 119*2cb6d6f1SJim Jagielski else: 120*2cb6d6f1SJim Jagielski old, new = self.dir / "ctl" / "old", self.dir / "ctl" / "new" 121*2cb6d6f1SJim Jagielski old.mkdir(parents=True), new.mkdir(parents=True) 122*2cb6d6f1SJim Jagielski for d in (old, new): 123*2cb6d6f1SJim Jagielski (d / "ctl-same.odt").write_bytes(base.read_bytes()) 124*2cb6d6f1SJim Jagielski (old / "ctl-mut.odt").write_bytes(base.read_bytes()) 125*2cb6d6f1SJim Jagielski if not mutate(base, new / "ctl-mut.odt"): 126*2cb6d6f1SJim Jagielski self._controls = (None, "could not change a word in %s" % base.name) 127*2cb6d6f1SJim Jagielski else: 128*2cb6d6f1SJim Jagielski res = diff7.diff_dirs(str(self.dumps(None, old, "ctl-old")), str(self.dumps(None, new, "ctl-new"))) 129*2cb6d6f1SJim Jagielski self._controls = ({"same": res["ctl-same.odt"]["ndiff"], "mutated": res["ctl-mut.odt"]["ndiff"]}, base.name) 130*2cb6d6f1SJim Jagielski return self._controls 131