|
| 1 | +# -------------------------------------------------------------------------------------------- |
| 2 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | +# Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | +# -------------------------------------------------------------------------------------------- |
| 5 | + |
| 6 | +import fcntl |
| 7 | +import importlib |
| 8 | +import os |
| 9 | +import subprocess |
| 10 | +import tempfile |
| 11 | +import psutil |
| 12 | +import pytest |
| 13 | +import sys |
| 14 | +import shutil |
| 15 | + |
| 16 | +from pathlib import Path |
| 17 | + |
| 18 | + |
| 19 | +# This fixture ensures tests are run against final built wheels of the extension |
| 20 | +# instead of the unbuilt local code, which may have breaking differences with |
| 21 | +# the thing we actually ship to users. All but the test modules themselves are |
| 22 | +# replaced with the wheel in case the tests themselves rely on unshipped code. |
| 23 | +@pytest.fixture(autouse=True, scope="session") |
| 24 | +def run_on_wheel(request): |
| 25 | + |
| 26 | + modules_to_test = {i.module for i in request.session.items} |
| 27 | + extensions_to_build = {module.__name__.split(".")[0] for module in modules_to_test} |
| 28 | + extension_dirs = {Path(a.split("/azext_")[0]) for a in request.config.args} |
| 29 | + |
| 30 | + # Azdev doesn't respect the session scope of the fixture, therefore we need |
| 31 | + # to implement equivalent behaviour by getting a unique ID for the current |
| 32 | + # run and using that to determine if wheels have already been built. Search |
| 33 | + # process parentage until we find the first shell process and use it's |
| 34 | + # child's PID as the run ID. |
| 35 | + parent = psutil.Process(os.getpid()) |
| 36 | + while not any(parent.cmdline()[0].endswith(i) for i in ["bash", "sh"]): |
| 37 | + parent = parent.parent() |
| 38 | + RUN_ID = parent.children()[0].pid |
| 39 | + |
| 40 | + build_dir = Path(tempfile.gettempdir()) / f"wheels_{RUN_ID}" |
| 41 | + build_dir.mkdir(exist_ok=True) |
| 42 | + |
| 43 | + # Build all extensions being tested into wheels |
| 44 | + for extension in extensions_to_build: |
| 45 | + |
| 46 | + extension_name = extension.replace("azext_", "") |
| 47 | + |
| 48 | + # Ensure we acquire a lock while operating on the build dir to avoid races |
| 49 | + lock_file = build_dir / ".dir.lock" |
| 50 | + with lock_file.open("w") as f: |
| 51 | + fcntl.flock(f, fcntl.LOCK_EX) |
| 52 | + try: |
| 53 | + |
| 54 | + # Delete the extensions build dir, as azdev extension build doesn't |
| 55 | + # reliably handle changes |
| 56 | + for extension_dir in extension_dirs: |
| 57 | + if (extension_dir / "build").exists(): |
| 58 | + shutil.rmtree((extension_dir / "build").as_posix(), ignore_errors=True) |
| 59 | + |
| 60 | + if not any(build_dir.glob(f"{extension_name}*.whl")): |
| 61 | + subprocess.run( |
| 62 | + ["azdev", "extension", "build", extension.replace("azext_", ""), "--dist-dir", build_dir.as_posix()], |
| 63 | + check=True, |
| 64 | + ) |
| 65 | + |
| 66 | + finally: |
| 67 | + fcntl.flock(f, fcntl.LOCK_UN) |
| 68 | + |
| 69 | + # Add the wheel to the path and reload extension modules so the |
| 70 | + # tests pick up the wheel code over the unbuilt code |
| 71 | + sys.path.insert(0, build_dir.glob("*.whl").__next__().as_posix()) |
| 72 | + for module in list(sys.modules.values()): |
| 73 | + if extension in module.__name__ and module not in modules_to_test: |
| 74 | + del sys.modules[module.__name__] |
| 75 | + importlib.import_module(module.__name__) |
| 76 | + |
| 77 | + yield |
0 commit comments