Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/confcom/azext_confcom/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import fcntl
import importlib
import os
import subprocess
import tempfile
import psutil
import pytest
import sys
import shutil

from pathlib import Path


# This fixture ensures tests are run against final built wheels of the extension
# instead of the unbuilt local code, which may have breaking differences with
# the thing we actually ship to users. All but the test modules themselves are
# replaced with the wheel in case the tests themselves rely on unshipped code.
@pytest.fixture(autouse=True, scope="session")
def run_on_wheel(request):

modules_to_test = {i.module for i in request.session.items}
extensions_to_build = {module.__name__.split(".")[0] for module in modules_to_test}
extension_dirs = {Path(a.split("/azext_")[0]) for a in request.config.args}

# Azdev doesn't respect the session scope of the fixture, therefore we need
# to implement equivalent behaviour by getting a unique ID for the current
# run and using that to determine if wheels have already been built. Search
# process parentage until we find the first shell process and use it's
# child's PID as the run ID.
parent = psutil.Process(os.getpid())
while not any(parent.cmdline()[0].endswith(i) for i in ["bash", "sh"]):
parent = parent.parent()
RUN_ID = parent.children()[0].pid
Comment on lines +36 to +38
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 36 assumes the parent process command line will end with "bash" or "sh", which may not be true in all environments (e.g., zsh, fish, Windows, or when run from other tools). This could cause an infinite loop or crash if no such parent is found. Consider adding a maximum depth check or handling other shell types.

Suggested change
while not any(parent.cmdline()[0].endswith(i) for i in ["bash", "sh"]):
parent = parent.parent()
RUN_ID = parent.children()[0].pid
# Recognize common shell process names
shell_names = ["bash", "sh", "zsh", "fish", "cmd.exe", "powershell.exe"]
max_depth = 10
depth = 0
while True:
try:
cmdline = parent.cmdline()
if cmdline and any(cmdline[0].endswith(shell) for shell in shell_names):
break
parent = parent.parent()
depth += 1
if parent is None or depth >= max_depth:
# Could not find a shell parent, fallback to current process
parent = psutil.Process(os.getpid())
break
except (psutil.Error, IndexError):
# Fallback if process info is unavailable
parent = psutil.Process(os.getpid())
break
# Use the first child PID if available, else fallback to current PID
children = parent.children()
RUN_ID = children[0].pid if children else parent.pid

Copilot uses AI. Check for mistakes.

build_dir = Path(tempfile.gettempdir()) / f"wheels_{RUN_ID}"
build_dir.mkdir(exist_ok=True)

# Build all extensions being tested into wheels
for extension in extensions_to_build:

extension_name = extension.replace("azext_", "")

# Ensure we acquire a lock while operating on the build dir to avoid races
lock_file = build_dir / ".dir.lock"
with lock_file.open("w") as f:
fcntl.flock(f, fcntl.LOCK_EX)
try:

# Delete the extensions build dir, as azdev extension build doesn't
# reliably handle changes
for extension_dir in extension_dirs:
if (extension_dir / "build").exists():
shutil.rmtree((extension_dir / "build").as_posix(), ignore_errors=True)

if not any(build_dir.glob(f"{extension_name}*.whl")):
subprocess.run(
["azdev", "extension", "build", extension.replace("azext_", ""), "--dist-dir", build_dir.as_posix()],
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 62 duplicates the string replacement extension.replace("azext_", "") which was already computed on line 46 as extension_name. Use the extension_name variable instead to avoid duplication.

Suggested change
["azdev", "extension", "build", extension.replace("azext_", ""), "--dist-dir", build_dir.as_posix()],
["azdev", "extension", "build", extension_name, "--dist-dir", build_dir.as_posix()],

Copilot uses AI. Check for mistakes.
check=True,
)

finally:
fcntl.flock(f, fcntl.LOCK_UN)

# Add the wheel to the path and reload extension modules so the
# tests pick up the wheel code over the unbuilt code
sys.path.insert(0, build_dir.glob("*.whl").__next__().as_posix())
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic on line 71 uses __next__() which will raise StopIteration if no wheel files are found in build_dir. This could happen if the build process fails silently or if the wheel files have unexpected names. Consider adding error handling or validation to ensure at least one wheel was built successfully.

Suggested change
sys.path.insert(0, build_dir.glob("*.whl").__next__().as_posix())
wheel_files = list(build_dir.glob("*.whl"))
if not wheel_files:
raise RuntimeError(f"No wheel files found in {build_dir}. The build may have failed or produced unexpected file names.")
sys.path.insert(0, wheel_files[0].as_posix())

Copilot uses AI. Check for mistakes.
for module in list(sys.modules.values()):
if extension in module.__name__ and module not in modules_to_test:
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable extension is referenced on line 73 outside the loop where it's defined (lines 44-67). This will only work correctly if there's exactly one extension in extensions_to_build. If there are multiple extensions, only modules from the last extension will be reloaded. Consider moving the module reloading logic inside the extension loop or storing all extension names to iterate over later.

Suggested change
if extension in module.__name__ and module not in modules_to_test:
if any(ext in module.__name__ for ext in extensions_to_build) and module not in modules_to_test:

Copilot uses AI. Check for mistakes.
del sys.modules[module.__name__]
importlib.import_module(module.__name__)

yield
Loading