xref: /trunk/test/aoo-test/conftest.py (revision 2c760c133df3253aa7075d23c106a30339fca6f2)
1################################################################
2#
3#  Licensed to the Apache Software Foundation (ASF) under one
4#  or more contributor license agreements.  See the NOTICE file
5#  distributed with this work for additional information
6#  regarding copyright ownership.  The ASF licenses this file
7#  to you under the Apache License, Version 2.0 (the
8#  "License"); you may not use this file except in compliance
9#  with the License.  You may obtain a copy of the License at
10#
11#    http://www.apache.org/licenses/LICENSE-2.0
12#
13#  Unless required by applicable law or agreed to in writing,
14#  software distributed under the License is distributed on an
15#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16#  KIND, either express or implied.  See the License for the
17#  specific language governing permissions and limitations
18#  under the License.
19#
20################################################################
21
22"""Shared pytest configuration for the AOO test suite."""
23
24import os
25
26import pytest
27
28from aootest import documents, expect, install, pipeline, session, twins
29from aootest.odfversion import SAVE_MODES, odf_versions
30
31
32def pytest_addoption(parser):
33    g = parser.getgroup("aoo", "AOO test suite")
34    g.addoption("--office", default=None, help="an AOO 5+ install: .app, install root, program dir or soffice (default: $AOO_OFFICE)")
35    g.addoption("--uno-python", default=None, help="Python that can import uno (default: $AOO_UNO_PYTHON, else probed)")
36    g.addoption("--connect", choices=("socket", "pipe"), default="socket", help="how the worker reaches the office")
37    g.addoption("--start-timeout", type=float, default=240.0, help="seconds an office may take to start and accept a connection (default %(default)s)")
38    g.addoption("--max-restarts", type=int, default=5, help="office restarts allowed per mode before the session aborts")
39    g.addoption("--require-office", action="store_true", help="fail instead of skipping when no usable office is found")
40    g.addoption("--documents", default=None, help="a directory of ODF documents to run through every save mode, in addition to the ones the office ships")
41    g.addoption("--shipped-limit", type=int, default=documents.DEFAULT_LIMIT, help="how many of the templates and documents shipped with the office to use (default %(default)s)")
42    g.addoption("--twin-corpus", default=None, help="a directory with v12, v13, v12ext and v13ext subdirectories holding the same documents saved as ODF 1.2 and 1.3 by another producer (default: $AOO_TWIN_CORPUS)")
43    g.addoption("--strict-expect", action="store_true", help="an item that a data/expect row expects to fail must fail (XPASS becomes a failure)")
44    g.addoption("--write-kinds", action="store_true", help="write data/baseline/ from this run instead of comparing with it (tests/odf/test_kinds.py)")
45    g.addoption("--require-odf13", action="store_true", help="fail instead of skipping the ODF 1.3 save modes when the office cannot write 1.3")
46    g.addoption("--require-odf14", action="store_true", help="fail instead of skipping the ODF 1.4 save modes when the office cannot write 1.4")
47
48
49def _unavailable(config, message):
50    if config.getoption("--require-office"):
51        pytest.fail(message, pytrace=False)
52    pytest.skip(message)
53
54
55@pytest.fixture(scope="session")
56def aoo_layout(pytestconfig):
57    """The install named by --office or AOO_OFFICE. There is no search of default places: an office is only started on request."""
58    location = office_location(pytestconfig)
59    if not location:
60        _unavailable(pytestconfig, "set AOO_OFFICE or --office to an installed AOO 5 to run office tests")
61    try:
62        return install.discover(location)
63    except install.LayoutError as e:
64        _unavailable(pytestconfig, "no usable office at %s: %s" % (location, e))
65
66
67@pytest.fixture(scope="session")
68def aoo_python(pytestconfig, aoo_layout, tmp_path_factory):
69    try:
70        return install.find_uno_python(aoo_layout, explicit=pytestconfig.getoption("--uno-python"),
71                                       cache_file=tmp_path_factory.getbasetemp() / "uno-python.json")
72    except install.NoUnoPython as e:
73        _unavailable(pytestconfig, "no UNO Python for this install: %s; tried %s" % (e, e.tried))
74
75
76class Offices:
77    """One lazily started office and worker per ODF mode (None = product default, or a Save/ODF/DefaultVersion number)."""
78
79    def __init__(self, layout, python, workdir, connect, max_restarts, start_timeout=240.0):
80        self.layout, self.python, self.workdir, self.connect, self.max_restarts = layout, python, workdir, connect, max_restarts
81        self.start_timeout = start_timeout
82        self.sessions = {}
83
84    def get(self, mode=None, extra=None, tag=None):
85        """The session for `mode`; `extra` (other Save/ODF settings) needs a `tag` that names it."""
86        key = (mode, tag)
87        if key not in self.sessions:
88            self.sessions[key] = session.ModeSession(self.layout, self.python, mode, self.workdir, self.connect, self.max_restarts,
89                                                     connect_timeout=self.start_timeout, extra=extra, tag=tag)
90        return self.sessions[key]
91
92    def close(self):
93        for s in self.sessions.values():
94            s.close()
95        left = [(s.name, o.pid) for s in self.sessions.values() for o in s.started if o.alive()]
96        assert not left, "offices still running after the session: %s" % left
97
98
99@pytest.fixture(scope="session")
100def aoo_offices(pytestconfig, aoo_layout, aoo_python, tmp_path_factory):
101    offices = Offices(aoo_layout, aoo_python, tmp_path_factory.mktemp("aoo"), pytestconfig.getoption("--connect"),
102                      pytestconfig.getoption("--max-restarts"), pytestconfig.getoption("--start-timeout"))
103    yield offices
104    offices.close()
105
106
107def office_location(config):
108    return config.getoption("--office") or os.environ.get("AOO_OFFICE")
109
110
111@pytest.fixture(scope="session")
112def odf_runs(pytestconfig, aoo_offices, tmp_path_factory):
113    """{mode name: ModeRun}, created on first use: `odf_runs("strict")`."""
114    work = tmp_path_factory.mktemp("odf")
115    runs = {}
116    sources = documents.from_config(pytestconfig)
117    # the sources are made in ODF 1.2 extended mode, which keeps every feature the reload checks look for
118    prepared = documents.Prepared(aoo_offices.get(SAVE_MODES["ext"]), work / "src",
119                                  lambda builder: aoo_offices.get(SAVE_MODES["ext"], tag="build-" + builder))
120
121    def get(name):
122        if name not in runs:
123            runs[name] = pipeline.ModeRun(name, aoo_offices.get(SAVE_MODES[name]), work, sources, prepared)
124        return runs[name]
125    return get
126
127
128@pytest.fixture(scope="session")
129def twin_run(pytestconfig, aoo_offices, tmp_path_factory):
130    """The TwinRun over the corpus named by --twin-corpus or AOO_TWIN_CORPUS; skips when there is none."""
131    root = pytestconfig.getoption("--twin-corpus") or os.environ.get("AOO_TWIN_CORPUS")
132    if not root:
133        pytest.skip("set AOO_TWIN_CORPUS or --twin-corpus to a corpus of ODF 1.2 and 1.3 twins to run the import twin tests")
134    problem = twins.corpus_problem(root)
135    if problem:
136        pytest.fail("twin corpus: " + problem, pytrace=False)
137    return twins.TwinRun(aoo_offices.get(None), root, tmp_path_factory.mktemp("twins"))
138
139
140_EXPECT_ROWS = []
141
142
143def pytest_collection_modifyitems(config, items):
144    rows = _EXPECT_ROWS[:] or expect.load()
145    _EXPECT_ROWS[:] = rows
146    strict = config.getoption("--strict-expect")
147    for item in items:
148        row = expect.match(rows, item.name)
149        if row is None:
150            continue
151        row.used += 1
152        if row.kind in ("xfail", "flaky"):
153            item.add_marker(pytest.mark.xfail(reason=row.reason, strict=strict and row.kind == "xfail"))
154        else:
155            item.add_marker(pytest.mark.skip(reason=row.reason))
156
157
158@pytest.fixture(scope="session")
159def odf13(aoo_offices, tmp_path_factory):
160    """Whether this build writes ODF 1.3 in save mode 5: builds from before the ODF 1.3 work ignore the setting."""
161    dst = tmp_path_factory.mktemp("odf13") / "probe.odt"
162    aoo_offices.get(5).call("new_and_store", {"app": "writer", "dst": str(dst), "filter": "writer8"}, timeout=120)
163    return odf_versions(dst) == ("1.3", "1.3")
164
165
166@pytest.fixture(scope="session")
167def odf14(aoo_offices, tmp_path_factory):
168    """Whether this build writes ODF 1.4 in save mode 7: builds from before the ODF 1.4 work ignore the setting."""
169    dst = tmp_path_factory.mktemp("odf14") / "probe.odt"
170    aoo_offices.get(7).call("new_and_store", {"app": "writer", "dst": str(dst), "filter": "writer8"}, timeout=120)
171    return odf_versions(dst) == ("1.4", "1.4")
172
173
174@pytest.fixture
175def need_mode(request, pytestconfig):
176    """need_mode(mode): skip (or fail with --require-odf13 or --require-odf14) when the build cannot write that save mode (names or numbers)."""
177    def check(mode):
178        number = SAVE_MODES.get(mode, mode)
179        for numbers, probe, option, label in (((5, 6), "odf13", "--require-odf13", "1.3"), ((7, 8), "odf14", "--require-odf14", "1.4")):
180            if number in numbers and not request.getfixturevalue(probe):
181                message = "this build does not write ODF %s (save mode %s)" % (label, mode)
182                if pytestconfig.getoption(option):
183                    pytest.fail(message, pytrace=False)
184                pytest.skip(message)
185    return check
186
187
188def pytest_terminal_summary(terminalreporter, config):
189    unused = [r for r in _EXPECT_ROWS if not r.used]
190    if unused and not config.option.keyword and not config.args[1:]:
191        terminalreporter.write_line("expectation rows that matched no collected item: %d (first: %s at %s)" % (len(unused), unused[0].pattern, unused[0].origin))
192