|
11 | 11 |
|
12 | 12 | from __future__ import annotations |
13 | 13 |
|
| 14 | +import os |
| 15 | +import sys |
| 16 | +import threading |
| 17 | +import types |
14 | 18 | import unittest |
15 | 19 | from unittest import mock |
16 | 20 |
|
| 21 | +from monai.apps.nnunet import nnunetv2_runner |
17 | 22 | from monai.apps.nnunet.nnunetv2_runner import nnUNetV2Runner |
| 23 | +from monai.bundle import ConfigParser |
18 | 24 |
|
19 | 25 |
|
20 | 26 | def _make_runner(export_validation_probabilities=False): |
@@ -74,5 +80,136 @@ def test_validate_emits_bare_val_flag(self): |
74 | 80 | self.assertNotIn("True", cmd) |
75 | 81 |
|
76 | 82 |
|
| 83 | +class TestTrainParallelCommand(unittest.TestCase): |
| 84 | + def test_train_parallel_uses_argv_list_without_shell(self): |
| 85 | + runner = _make_runner() |
| 86 | + runner.dataset_name = "Dataset001_Test" |
| 87 | + runner.nnunet_results = "/tmp/nnunet_results" |
| 88 | + |
| 89 | + all_cmds = [ |
| 90 | + { |
| 91 | + 0: [ |
| 92 | + (["python", "-m", "train", "--fold", "0"], {"CUDA_VISIBLE_DEVICES": "0"}), |
| 93 | + (["python", "-m", "train", "--fold", "1"], {"CUDA_VISIBLE_DEVICES": "0"}), |
| 94 | + ], |
| 95 | + 1: [(["python", "-m", "train", "--fold", "2"], {"CUDA_VISIBLE_DEVICES": "1"})], |
| 96 | + } |
| 97 | + ] |
| 98 | + |
| 99 | + with mock.patch.object(runner, "train_parallel_cmd", return_value=all_cmds): |
| 100 | + with mock.patch("monai.apps.nnunet.nnunetv2_runner.subprocess.Popen") as popen: |
| 101 | + popen.return_value.wait.return_value = None |
| 102 | + runner.train_parallel() |
| 103 | + |
| 104 | + self.assertEqual(popen.call_count, 3) |
| 105 | + for call in popen.call_args_list: |
| 106 | + self.assertIsInstance(call.args[0], list) |
| 107 | + self.assertFalse(call.kwargs["shell"]) |
| 108 | + |
| 109 | + def test_commands_run_sequentially_per_device(self): |
| 110 | + runner = _make_runner() |
| 111 | + runner.dataset_name = "Dataset001_Test" |
| 112 | + runner.nnunet_results = "/tmp/nnunet_results" |
| 113 | + |
| 114 | + all_cmds = [ |
| 115 | + {0: [(["python", "-m", "train", "--fold", "0"], {}), (["python", "-m", "train", "--fold", "1"], {})]} |
| 116 | + ] |
| 117 | + |
| 118 | + events = [] |
| 119 | + lock = threading.Lock() |
| 120 | + |
| 121 | + class _FakeProcess: |
| 122 | + def __init__(self, cmd): |
| 123 | + self.cmd = cmd |
| 124 | + |
| 125 | + def wait(self): |
| 126 | + with lock: |
| 127 | + events.append(("wait", self.cmd)) |
| 128 | + return 0 |
| 129 | + |
| 130 | + def _fake_popen(cmd, *args, **kwargs): |
| 131 | + with lock: |
| 132 | + events.append(("popen", cmd)) |
| 133 | + return _FakeProcess(cmd) |
| 134 | + |
| 135 | + with mock.patch.object(runner, "train_parallel_cmd", return_value=all_cmds): |
| 136 | + with mock.patch("monai.apps.nnunet.nnunetv2_runner.subprocess.Popen", side_effect=_fake_popen): |
| 137 | + runner.train_parallel() |
| 138 | + |
| 139 | + self.assertEqual( |
| 140 | + events, |
| 141 | + [ |
| 142 | + ("popen", ["python", "-m", "train", "--fold", "0"]), |
| 143 | + ("wait", ["python", "-m", "train", "--fold", "0"]), |
| 144 | + ("popen", ["python", "-m", "train", "--fold", "1"]), |
| 145 | + ("wait", ["python", "-m", "train", "--fold", "1"]), |
| 146 | + ], |
| 147 | + ) |
| 148 | + |
| 149 | + |
| 150 | +class TestPredictEnsemblePostprocessingWarnings(unittest.TestCase): |
| 151 | + def test_postprocessing_pickle_warns_on_untrusted_file(self): |
| 152 | + runner = _make_runner() |
| 153 | + runner.dataset_name = "Dataset001_Test" |
| 154 | + runner.nnunet_raw = "/tmp/nnunet_raw" |
| 155 | + runner.nnunet_results = "/tmp/nnunet_results" |
| 156 | + runner.best_configuration = { |
| 157 | + "best_model_or_ensemble": { |
| 158 | + "selected_model_or_models": [{"configuration": "3d_fullres"}], |
| 159 | + "postprocessing_file": "/tmp/attacker_controlled_postprocessing.pkl", |
| 160 | + "some_plans_file": "/tmp/plans.json", |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + ensemble_mod = types.ModuleType("nnunetv2.ensembling.ensemble") |
| 165 | + ensemble_mod.ensemble_folders = mock.MagicMock() |
| 166 | + pp_mod = types.ModuleType("nnunetv2.postprocessing.remove_connected_components") |
| 167 | + pp_mod.apply_postprocessing_to_folder = mock.MagicMock() |
| 168 | + fp_mod = types.ModuleType("nnunetv2.utilities.file_path_utilities") |
| 169 | + fp_mod.get_output_folder = mock.MagicMock(return_value="/tmp/model_folder") |
| 170 | + |
| 171 | + fake_modules = { |
| 172 | + "nnunetv2.ensembling.ensemble": ensemble_mod, |
| 173 | + "nnunetv2.postprocessing.remove_connected_components": pp_mod, |
| 174 | + "nnunetv2.utilities.file_path_utilities": fp_mod, |
| 175 | + } |
| 176 | + |
| 177 | + events = [] |
| 178 | + |
| 179 | + def _load_pickle(path): |
| 180 | + """Record a ``load_pickle`` call and return an empty postprocessing pipeline. |
| 181 | +
|
| 182 | + Args: |
| 183 | + path: path to the pickle file (unused). |
| 184 | +
|
| 185 | + Returns: |
| 186 | + A tuple of ``(postprocessing_fns, postprocessing_kwargs)``. |
| 187 | + """ |
| 188 | + events.append("load_pickle") |
| 189 | + return [], {} |
| 190 | + |
| 191 | + def _warn(*args, **kwargs): |
| 192 | + """Record a ``warnings.warn`` call. |
| 193 | +
|
| 194 | + Args: |
| 195 | + *args: positional arguments passed to ``warnings.warn``. |
| 196 | + **kwargs: keyword arguments passed to ``warnings.warn``. |
| 197 | + """ |
| 198 | + events.append("warn") |
| 199 | + |
| 200 | + load_pickle = mock.MagicMock(side_effect=_load_pickle) |
| 201 | + with mock.patch.dict(sys.modules, fake_modules): |
| 202 | + with mock.patch.object(ConfigParser, "load_config_file", return_value=runner.best_configuration): |
| 203 | + with mock.patch.object(nnunetv2_runner, "join", os.path.join): |
| 204 | + with mock.patch.object(nnunetv2_runner, "load_pickle", load_pickle): |
| 205 | + with mock.patch.object(nnunetv2_runner.warnings, "warn", side_effect=_warn): |
| 206 | + runner.predict_ensemble_postprocessing( |
| 207 | + run_predict=False, run_ensemble=False, run_postprocessing=True |
| 208 | + ) |
| 209 | + |
| 210 | + load_pickle.assert_called_once_with("/tmp/attacker_controlled_postprocessing.pkl") |
| 211 | + self.assertEqual(events, ["warn", "load_pickle"]) |
| 212 | + |
| 213 | + |
77 | 214 | if __name__ == "__main__": |
78 | 215 | unittest.main() |
0 commit comments