|
1 | 1 | import datetime |
2 | 2 | import json |
| 3 | +import os |
3 | 4 | import pathlib |
| 5 | +import signal |
| 6 | +import threading |
| 7 | +import time |
| 8 | + |
4 | 9 | import pytz |
5 | 10 | from io import BufferedReader |
6 | 11 | import yaml |
|
11 | 16 |
|
12 | 17 | import docker.models.images |
13 | 18 | import schema_salad.exceptions |
| 19 | +from docker import DockerClient |
| 20 | +from docker.models.containers import Container |
14 | 21 |
|
15 | 22 | import xcengine.core |
16 | 23 | import xcengine.parameters |
@@ -90,6 +97,65 @@ def test_runner_init_with_image(): |
90 | 97 | ) |
91 | 98 | assert runner.image == image |
92 | 99 |
|
| 100 | +@pytest.mark.parametrize("keep", [False, True]) |
| 101 | +def test_runner_run_keep(keep: bool): |
| 102 | + runner = xcengine.core.ContainerRunner( |
| 103 | + image := Mock(docker.models.images.Image), |
| 104 | + None, |
| 105 | + client := Mock(DockerClient) |
| 106 | + ) |
| 107 | + image.tags = [] |
| 108 | + client.containers.run.return_value = (container := MagicMock(Container)) |
| 109 | + container.status = "exited" |
| 110 | + runner.run(False, 8080, False, keep) |
| 111 | + if keep: |
| 112 | + container.remove.assert_not_called() |
| 113 | + else: |
| 114 | + container.remove.assert_called_once_with(force=True) |
| 115 | + |
| 116 | + |
| 117 | +def test_runner_sigint(): |
| 118 | + runner = xcengine.core.ContainerRunner( |
| 119 | + image := Mock(docker.models.images.Image), |
| 120 | + None, |
| 121 | + client := Mock(DockerClient) |
| 122 | + ) |
| 123 | + image.tags = [] |
| 124 | + client.containers.run.return_value = (container := Mock(Container)) |
| 125 | + container.status = "running" |
| 126 | + def container_stop(): |
| 127 | + container.status = "stopped" |
| 128 | + container.stop = container_stop |
| 129 | + pid = os.getpid() |
| 130 | + |
| 131 | + old_alarm_handler = signal.getsignal(signal.SIGALRM) |
| 132 | + class AlarmException(Exception): |
| 133 | + pass |
| 134 | + def alarm_handler(signum, frame): |
| 135 | + raise AlarmException() |
| 136 | + signal.signal(signal.SIGALRM, alarm_handler) |
| 137 | + |
| 138 | + def interrupt_process(): |
| 139 | + time.sleep(1) # allow one second for runner to start |
| 140 | + os.kill(pid, signal.SIGINT) |
| 141 | + thread = threading.Thread(target=interrupt_process, daemon=True) |
| 142 | + thread.start() |
| 143 | + |
| 144 | + signal.alarm(5) |
| 145 | + try: |
| 146 | + # Should trap imminent SIGINT from interrupt_process and exit quickly |
| 147 | + runner.run(False, 8080, False, False) |
| 148 | + except AlarmException: |
| 149 | + # time-out, exception raised by alarm_handler |
| 150 | + # We need a time-out so that the test fails rather than hanging. |
| 151 | + assert False, "Container did not stop on SIGINT" |
| 152 | + finally: |
| 153 | + # Reset the alarm handler and cancel the alarm to avoid affecting |
| 154 | + # subsequent tests. |
| 155 | + signal.signal(signal.SIGALRM, old_alarm_handler) |
| 156 | + signal.alarm(0) |
| 157 | + assert container.status == "stopped" |
| 158 | + |
93 | 159 |
|
94 | 160 | @patch("xcengine.core.subprocess.run") |
95 | 161 | def test_pip(mock_run): |
|
0 commit comments