test_sandcastle_wait.py (6805B)
1 # This file is in the public domain. 2 3 import os 4 import signal 5 import subprocess 6 import tempfile 7 import time 8 import unittest 9 from pathlib import Path 10 11 REPOSITORY = Path(__file__).resolve().parents[1] 12 WAIT_SCRIPT = REPOSITORY / "sandcastle-wait" 13 14 FAKE_PODMAN = r"""#!/usr/bin/env python3 15 import os 16 import signal 17 import sys 18 import time 19 from pathlib import Path 20 21 22 arguments = sys.argv[1:] 23 with Path(os.environ["FAKE_PODMAN_LOG"]).open("a", encoding="utf-8") as log: 24 log.write(" ".join(arguments) + "\n") 25 26 if arguments[0] == "inspect": 27 running = os.environ.get("FAKE_CONTAINER_RUNNING", "true") 28 if running == "missing": 29 raise SystemExit(125) 30 print(running) 31 raise SystemExit(0) 32 33 if arguments[:2] != ["exec", "taler-sandcastle"]: 34 raise SystemExit(125) 35 36 command = arguments[2:] 37 if command[:2] == ["systemctl", "show"]: 38 states = os.environ["FAKE_UNIT_STATES"].split(",") 39 state_index = Path(os.environ["FAKE_STATE_INDEX"]) 40 try: 41 index = int(state_index.read_text(encoding="utf-8")) 42 except FileNotFoundError: 43 index = 0 44 state_index.write_text(str(index + 1), encoding="utf-8") 45 state = states[min(index, len(states) - 1)] 46 if state == "ERROR": 47 raise SystemExit(125) 48 print(state) 49 raise SystemExit(0) 50 51 if command[:2] == ["systemctl", "status"]: 52 print("diagnostic unit status") 53 raise SystemExit(3) 54 55 if command[0] == "journalctl": 56 stopped = Path(os.environ["FAKE_JOURNAL_STOPPED"]) 57 58 def terminate(_signum, _frame): 59 stopped.write_text("stopped\n", encoding="utf-8") 60 raise SystemExit(0) 61 62 signal.signal(signal.SIGTERM, terminate) 63 print("provisioning journal output", flush=True) 64 while True: 65 time.sleep(0.1) 66 67 raise SystemExit(125) 68 """ 69 70 71 class SandcastleWaitTests(unittest.TestCase): 72 def setUp(self): 73 self.temporary = tempfile.TemporaryDirectory() 74 self.root = Path(self.temporary.name) 75 self.bin_dir = self.root / "bin" 76 self.bin_dir.mkdir() 77 podman = self.bin_dir / "podman" 78 podman.write_text(FAKE_PODMAN, encoding="utf-8") 79 podman.chmod(0o755) 80 81 def tearDown(self): 82 self.temporary.cleanup() 83 84 def environment(self, states="active", running="true"): 85 environment = os.environ.copy() 86 environment.update( 87 { 88 "PATH": f"{self.bin_dir}:{environment['PATH']}", 89 "FAKE_CONTAINER_RUNNING": running, 90 "FAKE_UNIT_STATES": states, 91 "FAKE_STATE_INDEX": str(self.root / "state-index"), 92 "FAKE_PODMAN_LOG": str(self.root / "podman-log"), 93 "FAKE_JOURNAL_STOPPED": str(self.root / "journal-stopped"), 94 } 95 ) 96 return environment 97 98 def run_wait(self, states="active", running="true"): 99 return subprocess.run( 100 [str(WAIT_SCRIPT)], 101 env=self.environment(states, running), 102 stdout=subprocess.PIPE, 103 stderr=subprocess.PIPE, 104 text=True, 105 timeout=5, 106 ) 107 108 def podman_log(self): 109 try: 110 return (self.root / "podman-log").read_text(encoding="utf-8") 111 except FileNotFoundError: 112 return "" 113 114 def test_missing_or_stopped_container_fails_immediately(self): 115 for running in ("missing", "false"): 116 with self.subTest(running=running): 117 result = self.run_wait(running=running) 118 self.assertNotEqual(0, result.returncode) 119 self.assertIn("does not exist or is not running", result.stderr) 120 121 def test_already_successful_unit_exits_without_following_logs(self): 122 result = self.run_wait() 123 124 self.assertEqual(0, result.returncode) 125 self.assertIn("provisioning succeeded", result.stdout) 126 self.assertNotIn("journalctl", self.podman_log()) 127 128 def test_transient_exec_race_is_retried(self): 129 result = self.run_wait(states="ERROR,active") 130 131 self.assertEqual(0, result.returncode) 132 self.assertEqual(2, self.podman_log().count("systemctl show")) 133 134 def test_already_failed_unit_prints_diagnostics(self): 135 result = self.run_wait(states="failed") 136 137 self.assertNotEqual(0, result.returncode) 138 self.assertIn("provisioning failed", result.stderr) 139 self.assertIn("diagnostic unit status", result.stderr) 140 self.assertIn("systemctl status", self.podman_log()) 141 142 def test_unexpected_unit_state_fails(self): 143 result = self.run_wait(states="inactive") 144 145 self.assertNotEqual(0, result.returncode) 146 self.assertIn("unexpected state 'inactive'", result.stderr) 147 self.assertIn("diagnostic unit status", result.stderr) 148 149 def test_running_unit_follows_logs_and_succeeds(self): 150 result = self.run_wait(states="activating,active") 151 152 self.assertEqual(0, result.returncode) 153 self.assertIn("provisioning journal output", result.stdout) 154 self.assertIn("provisioning succeeded", result.stdout) 155 self.assertTrue((self.root / "journal-stopped").is_file()) 156 157 def test_running_unit_follows_logs_and_reports_failure(self): 158 result = self.run_wait(states="activating,failed") 159 160 self.assertNotEqual(0, result.returncode) 161 self.assertIn("provisioning journal output", result.stdout) 162 self.assertIn("provisioning failed", result.stderr) 163 self.assertIn("diagnostic unit status", result.stderr) 164 self.assertTrue((self.root / "journal-stopped").is_file()) 165 166 def test_losing_contact_while_waiting_fails_and_stops_journal(self): 167 result = self.run_wait(states="activating,ERROR") 168 169 self.assertNotEqual(0, result.returncode) 170 self.assertIn("Lost contact", result.stderr) 171 self.assertTrue((self.root / "journal-stopped").is_file()) 172 173 def test_termination_signal_stops_journal(self): 174 process = subprocess.Popen( 175 [str(WAIT_SCRIPT)], 176 env=self.environment(states="activating"), 177 stdout=subprocess.PIPE, 178 stderr=subprocess.PIPE, 179 text=True, 180 ) 181 try: 182 deadline = time.monotonic() + 5 183 while "journalctl" not in self.podman_log(): 184 self.assertIsNone(process.poll()) 185 if time.monotonic() >= deadline: 186 self.fail("journal follower did not start") 187 time.sleep(0.01) 188 189 process.send_signal(signal.SIGTERM) 190 process.communicate(timeout=5) 191 finally: 192 if process.poll() is None: 193 process.terminate() 194 process.communicate(timeout=5) 195 196 self.assertEqual(128 + signal.SIGTERM, process.returncode) 197 self.assertTrue((self.root / "journal-stopped").is_file()) 198 199 200 if __name__ == "__main__": 201 unittest.main()