sandcastle-ng

Scripts for the deployment of Sandcastle (GNU Taler)
Log | Files | Refs | README

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()