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"""client.py against the fake worker: normal replies, error replies, and every way a worker can go wrong.""" 23import os 24import time 25 26import pytest 27 28from aootest import client 29from aootest.client import ProtocolError, WorkerCrash, WorkerError, WorkerHang 30 31pytestmark = pytest.mark.selftest 32POSIX = os.name == "posix" 33 34 35def gone(pid, wait=5.0): 36 end = time.time() + wait 37 while time.time() < end: 38 try: 39 os.kill(pid, 0) 40 except ProcessLookupError: 41 return True 42 time.sleep(0.05) 43 return False 44 45 46def test_round_trip(start_worker): 47 w = start_worker() 48 assert w.hello["protocol"] == 1 and w.hello["pid"] == w.pid 49 assert w.call("echo", {"x": 1, "y": [1, 2]}) == {"x": 1, "y": [1, 2]} 50 assert w.call("add", {"a": 2, "b": 3}) == {"sum": 5} 51 assert w.call("ping") == {"pong": True} 52 53 54def test_printing_in_the_worker_does_not_corrupt_the_protocol(start_worker): 55 w = start_worker() 56 assert w.call("chatter") == {"printed": True} 57 assert w.call("echo", {"after": True}) == {"after": True} 58 59 60def test_error_replies_keep_the_worker_usable(start_worker): 61 w = start_worker() 62 with pytest.raises(WorkerError) as e: 63 w.call("fail", {"kind": "uno", "message": "the document would not load"}) 64 assert (e.value.kind, e.value.message) == ("uno", "the document would not load") 65 with pytest.raises(WorkerError) as e: 66 w.call("boom") 67 assert e.value.kind == "python" and e.value.exc == "ZeroDivisionError" and "ZeroDivisionError" in e.value.traceback 68 with pytest.raises(WorkerError) as e: 69 w.call("no-such-op") 70 assert e.value.kind == "bad_args" 71 with pytest.raises(WorkerError) as e: 72 w.call("add", {"a": 1}) 73 assert e.value.kind == "bad_args" and "missing" in e.value.message 74 assert w.call("add", {"a": 1, "b": 1}) == {"sum": 2} 75 76 77def test_close_ends_the_worker(start_worker): 78 w = start_worker() 79 p = w._proc 80 w.close() 81 assert p.poll() is not None 82 83 84# ---- HANG ---------------------------------------------------------------------------------------------------------- 85 86def test_hang_is_reported_and_the_worker_killed(start_worker): 87 w = start_worker("hang") 88 t0 = time.time() 89 with pytest.raises(WorkerHang, match="no answer within 1s"): 90 w.call("echo", timeout=1) 91 assert time.time() - t0 < 6 92 assert w._proc.poll() is not None 93 with pytest.raises(WorkerHang): # a finished client stays finished 94 w.call("echo") 95 96 97@pytest.mark.skipif(not POSIX, reason="child process ids are only checked on POSIX") 98def test_hang_kills_the_workers_children_too(start_worker): 99 w = start_worker() 100 child = w.call("spawn_child")["pid"] 101 os.kill(child, 0) 102 with pytest.raises(WorkerHang): 103 w.call("sleep", {"seconds": 60}, timeout=1) 104 assert gone(child) 105 106 107def test_worker_that_never_says_hello(start_worker): 108 with pytest.raises(WorkerHang, match="hello"): 109 start_worker("silent", hello_timeout=1) 110 111 112# ---- CRASH --------------------------------------------------------------------------------------------------------- 113 114def test_exit_139(start_worker): 115 w = start_worker("crash139") 116 with pytest.raises(WorkerCrash) as e: 117 w.call("echo") 118 assert e.value.returncode == 139 and "128 + SIGSEGV" in str(e.value) 119 120 121@pytest.mark.skipif(not POSIX, reason="signals") 122def test_killed_by_a_signal(start_worker): 123 w = start_worker("sigkill") 124 with pytest.raises(WorkerCrash) as e: 125 w.call("echo") 126 assert e.value.returncode == -9 and "killed by SIGKILL" in str(e.value) 127 128 129def test_stderr_tail_is_kept(start_worker): 130 w = start_worker("stderr-then-crash") 131 with pytest.raises(WorkerCrash) as e: 132 w.call("echo") 133 assert e.value.returncode == 7 and "the office bridge fell over" in str(e.value) 134 135 136def test_office_dead_reply_is_a_crash_and_the_worker_is_finished(start_worker): 137 w = start_worker() 138 with pytest.raises(client.OfficeDead, match="ServiceManager") as e: 139 w.call("office-gone") 140 assert isinstance(e.value, WorkerCrash) 141 with pytest.raises(client.OfficeDead): 142 w.call("echo") 143 assert w._proc.poll() is not None 144 145 146def test_death_mid_call_in_the_good_worker(start_worker): 147 w = start_worker() 148 with pytest.raises(WorkerCrash) as e: 149 w.call("die139") 150 assert e.value.returncode == 139 151 with pytest.raises(WorkerCrash): 152 w.call("echo") 153 154 155def test_exit_before_hello(start_worker): 156 with pytest.raises(WorkerCrash) as e: 157 start_worker("exit-before-hello") 158 assert e.value.returncode == 3 159 160 161# ---- garbage ------------------------------------------------------------------------------------------------------- 162 163@pytest.mark.parametrize("mode", ["garbage-reply", "not-an-object", "wrong-id", "binary-reply"]) 164def test_bad_replies_are_protocol_errors_and_the_worker_is_killed(start_worker, mode): 165 w = start_worker(mode) 166 with pytest.raises(ProtocolError): 167 w.call("echo", timeout=5) 168 assert w._proc.poll() is not None 169 170 171@pytest.mark.parametrize("mode", ["garbage-hello", "wrong-protocol"]) 172def test_bad_hello(start_worker, mode): 173 with pytest.raises(ProtocolError): 174 start_worker(mode, hello_timeout=5) 175 176 177@pytest.mark.parametrize("rc, text", [(0, "exit 0"), (1, "exit 1"), (7, "exit 7"), (139, "exit 139, 128 + SIGSEGV"), (-9, "killed by SIGKILL"), 178 (-11, "killed by SIGSEGV"), (None, "no exit status")]) 179def test_describe_exit(rc, text): 180 assert client.describe_exit(rc) == text 181