diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 56525b442e..ad9258c20f 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -170,7 +170,6 @@ jobs: environment: integration timeout-minutes: 60 env: - UV_PYTHON: "3.10" OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index 6d169948db..1ad170a6b0 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -285,7 +285,6 @@ jobs: runs-on: ubuntu-latest environment: integration env: - UV_PYTHON: "3.10" OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} diff --git a/python/packages/azurefunctions/tests/integration_tests/conftest.py b/python/packages/azurefunctions/tests/integration_tests/conftest.py index 3f6060d93d..907ad039ca 100644 --- a/python/packages/azurefunctions/tests/integration_tests/conftest.py +++ b/python/packages/azurefunctions/tests/integration_tests/conftest.py @@ -361,6 +361,14 @@ def _start_function_app(sample_path: Path, port: int) -> subprocess.Popen[Any]: # use the task hub name to separate orchestration state. env["TASKHUB_NAME"] = f"test{uuid.uuid4().hex[:8]}" + # The Azure Functions Python worker's dependency isolation mechanism crashes + # on Python 3.13 with a SIGSEGV in the protobuf C extension (google._upb). + # Disabling isolation lets the worker load dependencies from the app's own + # environment, which avoids the crash. + # See: https://github.com/Azure/azure-functions-python-worker/issues/1797 + if sys.version_info >= (3, 13): + env.setdefault("PYTHON_ISOLATE_WORKER_DEPENDENCIES", "0") + # On Windows, use CREATE_NEW_PROCESS_GROUP to allow proper termination # shell=True only on Windows to handle PATH resolution if sys.platform == "win32": @@ -371,8 +379,15 @@ def _start_function_app(sample_path: Path, port: int) -> subprocess.Popen[Any]: shell=True, env=env, ) - # On Unix, don't use shell=True to avoid shell wrapper issues - return subprocess.Popen(["func", "start", "--port", str(port)], cwd=str(sample_path), env=env) + # On Unix, use start_new_session=True to isolate the process group from the + # pytest-xdist worker. Without this, signals (e.g. from test-timeout) can + # propagate to the func host and vice-versa, potentially killing the worker. + return subprocess.Popen( + ["func", "start", "--port", str(port)], + cwd=str(sample_path), + env=env, + start_new_session=True, + ) def _wait_for_function_app_ready(func_process: subprocess.Popen[Any], port: int, max_wait: int = 60) -> None: @@ -529,18 +544,33 @@ class TestSample01SingleAgent: _load_and_validate_env() max_attempts = 3 + # The overall budget MUST be shorter than the pytest-timeout value + # (--timeout=120 by default) so that the fixture finishes cleanly instead + # of being killed by os._exit() which crashes the xdist worker. + overall_budget = 100 # seconds – leaves headroom below the 120 s test timeout last_error: Exception | None = None func_process: subprocess.Popen[Any] | None = None base_url = "" port = 0 + overall_start = time.monotonic() + attempts_made = 0 for _ in range(max_attempts): + remaining = overall_budget - (time.monotonic() - overall_start) + if remaining < 10: + # Not enough time for another attempt; bail out. + break + + attempts_made += 1 port = _find_available_port() base_url = _build_base_url(port) func_process = _start_function_app(sample_path, port) try: - _wait_for_function_app_ready(func_process, port) + # Cap each attempt's wait to the remaining budget minus a small + # buffer for cleanup. + per_attempt_wait = min(60, int(remaining) - 5) + _wait_for_function_app_ready(func_process, port, max_wait=max(per_attempt_wait, 10)) last_error = None break except FunctionAppStartupError as exc: @@ -549,7 +579,8 @@ class TestSample01SingleAgent: func_process = None if func_process is None: - error_message = f"Function app failed to start after {max_attempts} attempt(s)." + elapsed = int(time.monotonic() - overall_start) + error_message = f"Function app failed to start after {attempts_made} attempt(s) ({elapsed}s elapsed)." if last_error is not None: error_message += f" Last error: {last_error}" pytest.fail(error_message)