|
2 | 2 | # License: BSD-3-Clause |
3 | 3 | # Copyright the MNE-Python contributors. |
4 | 4 |
|
| 5 | +import json |
5 | 6 | import os |
6 | 7 | import platform |
| 8 | +import random |
7 | 9 | import re |
| 10 | +import time |
8 | 11 | from functools import partial |
9 | 12 | from pathlib import Path |
10 | 13 | from urllib.error import URLError |
@@ -232,3 +235,103 @@ def bad_open(url, timeout, msg): |
232 | 235 | out = out.getvalue() |
233 | 236 | assert "devel, " in out |
234 | 237 | assert "updating.html" not in out |
| 238 | + |
| 239 | + |
| 240 | +def _worker_update_config_loop(home_dir, worker_id, iterations=10): |
| 241 | + """Util function to update config in parallel. |
| 242 | +
|
| 243 | + Worker function that repeatedly reads the config (via get_config) |
| 244 | + and then updates it (via set_config) with a unique key/value pair. |
| 245 | + A short random sleep is added to encourage interleaving. |
| 246 | +
|
| 247 | + Dummy function to simulate a worker that reads and updates the config. |
| 248 | +
|
| 249 | + Parameters |
| 250 | + ---------- |
| 251 | + home_dir : str |
| 252 | + The home directory where the config file is located. |
| 253 | + worker_id : int |
| 254 | + The ID of the worker (for creating unique keys). |
| 255 | + iterations : int |
| 256 | + The number of iterations to run the loop. |
| 257 | +
|
| 258 | + """ |
| 259 | + for i in range(iterations): |
| 260 | + # Read current configuration (to simulate a read-modify cycle) |
| 261 | + _ = get_config(home_dir=home_dir) |
| 262 | + # Create a unique key/value pair. |
| 263 | + new_key = f"worker_{worker_id}_{i}" |
| 264 | + new_value = f"value_{worker_id}_{i}" |
| 265 | + # Update the configuration (our set_config holds the lock over the full cycle) |
| 266 | + set_config(new_key, new_value, home_dir=home_dir) |
| 267 | + time.sleep(random.uniform(0, 0.05)) |
| 268 | + return worker_id |
| 269 | + |
| 270 | + |
| 271 | +def test_parallel_get_set_config(tmp_path: Path): |
| 272 | + """Test that uses parallel workers to get and set config. |
| 273 | +
|
| 274 | + All the workers update the same configuration file concurrently. In a |
| 275 | + correct implementation with proper path file locking, the final |
| 276 | + config file remains valid JSON and includes all expected updates. |
| 277 | +
|
| 278 | + """ |
| 279 | + pytest.importorskip("joblib") |
| 280 | + pytest.importorskip("filelock") |
| 281 | + from joblib import Parallel, delayed |
| 282 | + |
| 283 | + # Use the temporary directory as our home directory. |
| 284 | + home_dir = str(tmp_path) |
| 285 | + # get_config_path will return home_dir/.mne/mne-python.json |
| 286 | + config_file = get_config_path(home_dir=home_dir) |
| 287 | + |
| 288 | + # if the config file already exists, remove it |
| 289 | + if os.path.exists(config_file): |
| 290 | + os.remove(config_file) |
| 291 | + |
| 292 | + # Ensure that the .mne directory exists. |
| 293 | + config_dir = tmp_path / ".mne" |
| 294 | + config_dir.mkdir(exist_ok=True) |
| 295 | + |
| 296 | + # Write an initial (valid) config file. |
| 297 | + initial_config = {"initial": "True"} |
| 298 | + with open(config_file, "w") as f: |
| 299 | + json.dump(initial_config, f) |
| 300 | + |
| 301 | + n_workers = 50 |
| 302 | + iterations = 10 |
| 303 | + |
| 304 | + # Launch multiple workers concurrently using joblib. |
| 305 | + Parallel(n_jobs=10)( |
| 306 | + delayed(_worker_update_config_loop)(home_dir, worker_id, iterations) |
| 307 | + for worker_id in range(n_workers) |
| 308 | + ) |
| 309 | + |
| 310 | + # Now, read back the config file. |
| 311 | + final_config = get_config(home_dir=home_dir) |
| 312 | + expected_keys = set() |
| 313 | + expected_values = set() |
| 314 | + # For each worker and iteration, check that the expected key/value pair is present. |
| 315 | + for worker_id in range(n_workers): |
| 316 | + for i in range(iterations): |
| 317 | + expected_key = f"worker_{worker_id}_{i}" |
| 318 | + expected_value = f"value_{worker_id}_{i}" |
| 319 | + |
| 320 | + assert final_config.get(expected_key) == expected_value, ( |
| 321 | + f"Missing or incorrect value for key {expected_key}" |
| 322 | + ) |
| 323 | + expected_keys.add(expected_key) |
| 324 | + expected_values.add(expected_value) |
| 325 | + |
| 326 | + # include the initial key/value pair |
| 327 | + # that was written before the workers started |
| 328 | + |
| 329 | + assert len(expected_keys - set(final_config.keys())) == 0 |
| 330 | + assert len(expected_values - set(final_config.values())) == 0 |
| 331 | + |
| 332 | + # Check that the final config is valid JSON. |
| 333 | + with open(config_file) as f: |
| 334 | + try: |
| 335 | + json.load(f) |
| 336 | + except json.JSONDecodeError as e: |
| 337 | + pytest.fail(f"Config file is not valid JSON: {e}") |
0 commit comments