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