xref: /trunk/test/aoo-test/tests/harness/test_office.py (revision 2cb6d6f119bbe2f78742f1c5c8538cb0d4e72ede)
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"""office.py: profile, command line, process handling, and the session that restarts after failures."""
23import os
24import sys
25import time
26import xml.etree.ElementTree as ET
27
28import pytest
29
30from aootest import client, office
31from aootest.client import WorkerCrash, WorkerHang
32from aootest.install import find_layout
33
34pytestmark = pytest.mark.selftest
35POSIX = os.name == "posix"
36NS = {"oor": "http://openoffice.org/2001/registry"}
37
38
39@pytest.mark.parametrize("version", [0, 3, 4, 5, 6])
40def test_registry_xcu_sets_the_odf_version(version):
41    root = ET.fromstring(office.registry_xcu(version))
42    item = root.find("item")
43    assert item.get("{http://openoffice.org/2001/registry}path") == "/org.openoffice.Office.Common/Save/ODF"
44    prop = item.find("prop")
45    assert prop.get("{http://openoffice.org/2001/registry}name") == "DefaultVersion" and prop.find("value").text == str(version)
46
47
48def test_registry_xcu_without_a_version_changes_nothing():
49    assert ET.fromstring(office.registry_xcu()).findall("item") == []
50
51
52def test_make_profile(tmp_path):
53    url = office.make_profile(tmp_path / "p", 4)
54    assert url.startswith("file:///") and url.endswith("/p")
55    assert "<value>4</value>" in (tmp_path / "p" / "user" / "registrymodifications.xcu").read_text()
56    with pytest.raises(ValueError, match="not empty"):
57        office.make_profile(tmp_path / "p", 4)
58    (tmp_path / "present").mkdir()
59    assert office.make_profile(tmp_path / "present").startswith("file:///")           # an empty directory is fine
60
61
62def test_accept_strings_are_unique_pipes_or_sockets():
63    a, b = office.accept_string(), office.accept_string()
64    assert a != b and a.startswith("pipe,name=aootest_") and a.endswith(";urp;")
65    assert office.accept_string("socket", port=2002) == "socket,host=localhost,port=2002;urp;"
66    assert 1024 < office.free_port() < 65536
67    with pytest.raises(ValueError):
68        office.accept_string("fax")
69
70
71def test_command_line(fake):
72    lay = find_layout(fake("macos"))
73    cmd = office.command(lay, "file:///tmp/prof", "pipe,name=x;urp;")
74    assert cmd[0] == str(lay.soffice)
75    assert "-headless" in cmd and "-invisible" in cmd and "-env:UserInstallation=file:///tmp/prof" in cmd and cmd[-1] == "-accept=pipe,name=x;urp;"
76    assert not [a for a in cmd[1:] if a.startswith("--")]                  # AOO ignores double-dash options
77    assert "-headless" not in office.command(lay, "file:///tmp/prof", "pipe,name=x;urp;", headless=False)
78    assert "-invisible" in office.command(lay, "file:///tmp/prof", "pipe,name=x;urp;", headless=False)
79
80
81FAKE_OFFICE = "import subprocess, sys, time\nsubprocess.Popen([sys.executable, '-c', 'import time; time.sleep(120)'])\nprint('started', flush=True)\ntime.sleep(120)\n"
82
83
84@pytest.mark.skipif(not POSIX, reason="process ids are only checked on POSIX")
85def test_office_kill_takes_the_whole_tree(tmp_path):
86    o = office.Office([sys.executable, "-c", FAKE_OFFICE], log=tmp_path / "office.log").start()
87    deadline = time.time() + 5
88    while time.time() < deadline and not (tmp_path / "office.log").read_text():
89        time.sleep(0.05)
90    assert o.alive() and "started" in (tmp_path / "office.log").read_text()
91    o.kill()
92    assert not o.alive()
93
94
95class FakeOffice:
96    def __init__(self):
97        self.killed = False
98
99    def kill(self):
100        self.killed = True
101
102
103def make_session(start_worker, modes, max_restarts=5, hello_timeout=5):
104    """A session whose successive workers behave as listed in `modes`."""
105    offices, workers, it = [], [], iter(modes)
106
107    def start_office():
108        offices.append(FakeOffice())
109        return offices[-1]
110
111    def start_w(off):
112        w = start_worker(next(it), hello_timeout=hello_timeout)
113        workers.append(w)
114        return w
115    return office.OfficeSession(start_office, start_w, max_restarts), offices, workers
116
117
118def test_session_starts_lazily_and_reuses_the_worker(start_worker):
119    s, offices, workers = make_session(start_worker, ["good"])
120    assert offices == []
121    assert s.call("add", {"a": 1, "b": 2}) == {"sum": 3} and s.call("add", {"a": 2, "b": 2}) == {"sum": 4}
122    assert len(offices) == len(workers) == 1 and s.failures == 0
123    s.close()
124    assert offices[0].killed and workers[0]._proc.poll() is not None
125
126
127def test_session_restarts_after_a_hang_and_the_next_test_passes(start_worker):
128    s, offices, workers = make_session(start_worker, ["hang", "good"])
129    with pytest.raises(WorkerHang):
130        s.call("echo", timeout=1)
131    assert offices[0].killed and s.failures == 1 and not s.aborted
132    assert s.call("add", {"a": 1, "b": 1}) == {"sum": 2}               # a fresh office and worker
133    assert len(offices) == 2 and not offices[1].killed
134    s.close()
135
136
137def test_session_restarts_after_a_crash(start_worker):
138    s, offices, workers = make_session(start_worker, ["crash139", "good"])
139    with pytest.raises(WorkerCrash):
140        s.call("echo")
141    assert s.call("echo", {"k": 1}) == {"k": 1} and s.failures == 1
142    s.close()
143
144
145def test_session_restarts_after_the_office_reports_itself_dead(start_worker):
146    s, offices, workers = make_session(start_worker, ["good", "good"])
147    with pytest.raises(client.OfficeDead):
148        s.call("office-gone")
149    assert offices[0].killed and s.failures == 1
150    assert s.call("add", {"a": 1, "b": 2}) == {"sum": 3} and len(offices) == 2
151    s.close()
152
153
154def test_session_aborts_after_too_many_failures(start_worker):
155    s, offices, workers = make_session(start_worker, ["crash139"] * 5, max_restarts=2)
156    for _ in range(3):
157        with pytest.raises(WorkerCrash):
158            s.call("echo")
159    assert s.aborted and s.failures == 3
160    with pytest.raises(office.SessionAborted, match="limit 2"):
161        s.call("echo")
162    assert len(offices) == 3 and all(o.killed for o in offices) and all(w._proc.poll() is not None for w in workers)
163
164
165def test_a_worker_that_cannot_start_counts_as_a_failure(start_worker):
166    s, offices, workers = make_session(start_worker, ["exit-before-hello", "good"], hello_timeout=5)
167    with pytest.raises(WorkerCrash):
168        s.call("echo")
169    assert s.failures == 1 and offices[0].killed
170    assert s.call("add", {"a": 5, "b": 5}) == {"sum": 10}
171    s.close()
172
173
174def test_an_error_reply_is_not_a_failure(start_worker):
175    s, offices, workers = make_session(start_worker, ["good"])
176    with pytest.raises(client.WorkerError):
177        s.call("boom")
178    assert s.failures == 0 and not offices[0].killed
179    s.close()
180