xref: /trunk/test/aoo-test/src/aootest/twins.py (revision 2cb6d6f119bbe2f78742f1c5c8538cb0d4e72ede)
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