From 49a3af5abbe978bebefad2a7f5c5615267eacd4e Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 14:13:04 -0600 Subject: [PATCH 01/10] add retries --- .../environments/consumption.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py index 0359c355..f21c3612 100644 --- a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py +++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py @@ -198,15 +198,26 @@ def url(self) -> str: def _ensure_blob_container(self) -> None: """Ensure the blob container exists.""" - try: - container_client = self.blob_service_client.get_container_client(self.apps_container_name) - if not container_client.exists(): - print(f"šŸ“¦ Creating blob container '{self.apps_container_name}'...") - container_client.create_container() - else: - print(f"šŸ“¦ Using existing blob container '{self.apps_container_name}'") - except Exception as e: - raise RuntimeError(f"Failed to ensure blob container: {e}") + import time + max_retries = 5 + retry_delay = 2 + + for attempt in range(max_retries): + try: + container_client = self.blob_service_client.get_container_client(self.apps_container_name) + if not container_client.exists(): + print(f"šŸ“¦ Creating blob container '{self.apps_container_name}'...") + container_client.create_container() + else: + print(f"šŸ“¦ Using existing blob container '{self.apps_container_name}'") + return # Success + except Exception as e: + if attempt < max_retries - 1: + print(f"āš ļø Failed to ensure blob container (attempt {attempt + 1}/{max_retries}): {e}") + print(f"ā³ Retrying in {retry_delay}s...") + time.sleep(retry_delay) + else: + raise RuntimeError(f"Failed to ensure blob container after {max_retries} attempts: {e}") def _generate_container_sas(self) -> None: """Generate a SAS token for the container.""" From 2b4d1bbb2b677b10d6c17f296b75ca28327fdaed Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 14:51:27 -0600 Subject: [PATCH 02/10] use windows --- eng/ci/templates/jobs/run-docker-tests-linux.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/eng/ci/templates/jobs/run-docker-tests-linux.yml b/eng/ci/templates/jobs/run-docker-tests-linux.yml index c8e32a87..696b2ab7 100644 --- a/eng/ci/templates/jobs/run-docker-tests-linux.yml +++ b/eng/ci/templates/jobs/run-docker-tests-linux.yml @@ -5,12 +5,12 @@ parameters: jobs: - job: "TestDocker" - displayName: 'Docker Integration Tests - Linux' + displayName: 'Docker Integration Tests - Windows' pool: name: ${{ parameters.poolName }} - image: 1es-ubuntu-22.04 - os: linux + image: 1es-windows-2022 + os: windows strategy: maxParallel: 4 @@ -65,6 +65,8 @@ jobs: pytest . -v -n auto --tb=short displayName: 'Run Docker integration tests' env: + PYTHONUTF8: '1' + PYTHONIOENCODING: 'utf-8' FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)/java-worker' FUNCTIONS_TEST_USE_AZURITE: 'true' FUNCTIONS_TEST_RUNTIME: 'java' From 4cbd00ecf8fc0fbecefb03706198f034f6640d34 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 15:02:02 -0600 Subject: [PATCH 03/10] run sequentially --- eng/ci/templates/jobs/run-docker-tests-linux.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/eng/ci/templates/jobs/run-docker-tests-linux.yml b/eng/ci/templates/jobs/run-docker-tests-linux.yml index 696b2ab7..36410d04 100644 --- a/eng/ci/templates/jobs/run-docker-tests-linux.yml +++ b/eng/ci/templates/jobs/run-docker-tests-linux.yml @@ -5,12 +5,12 @@ parameters: jobs: - job: "TestDocker" - displayName: 'Docker Integration Tests - Windows' + displayName: 'Docker Integration Tests - Linux' pool: name: ${{ parameters.poolName }} - image: 1es-windows-2022 - os: windows + image: 1es-ubuntu-22.04 + os: linux strategy: maxParallel: 4 @@ -62,11 +62,9 @@ jobs: - bash: | cd dockertests - pytest . -v -n auto --tb=short + pytest . -v --tb=short displayName: 'Run Docker integration tests' env: - PYTHONUTF8: '1' - PYTHONIOENCODING: 'utf-8' FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)/java-worker' FUNCTIONS_TEST_USE_AZURITE: 'true' FUNCTIONS_TEST_RUNTIME: 'java' From badce06feb5634e6859785b61b60e6cffa1895d1 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 15:44:09 -0600 Subject: [PATCH 04/10] use windows --- .../templates/jobs/run-docker-tests-linux.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/eng/ci/templates/jobs/run-docker-tests-linux.yml b/eng/ci/templates/jobs/run-docker-tests-linux.yml index 36410d04..2debd4cc 100644 --- a/eng/ci/templates/jobs/run-docker-tests-linux.yml +++ b/eng/ci/templates/jobs/run-docker-tests-linux.yml @@ -5,12 +5,12 @@ parameters: jobs: - job: "TestDocker" - displayName: 'Docker Integration Tests - Linux' + displayName: 'Docker Integration Tests - Windows' pool: name: ${{ parameters.poolName }} - image: 1es-ubuntu-22.04 - os: linux + image: 1es-windows-2022 + os: windows strategy: maxParallel: 4 @@ -56,18 +56,19 @@ jobs: displayName: 'Package Java worker' - pwsh: | - cd dockertests - ./build-apps.ps1 + .\dockertests\build-apps.ps1 displayName: 'Build and package test apps' - - bash: | + - pwsh: | cd dockertests - pytest . -v --tb=short + python -m pytest . -v --tb=short displayName: 'Run Docker integration tests' env: - FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)/java-worker' + PYTHONUTF8: '1' + PYTHONIOENCODING: 'utf-8' + FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)\java-worker' FUNCTIONS_TEST_USE_AZURITE: 'true' FUNCTIONS_TEST_RUNTIME: 'java' FUNCTIONS_TEST_RUNTIME_VERSION: '$(javaVersion)' FUNCTIONS_TEST_HOST_VERSION: '4' - FUNCTIONS_TEST_APPS_DIR: '$(Build.SourcesDirectory)/dockertests/app-packages' + FUNCTIONS_TEST_APPS_DIR: '$(Build.SourcesDirectory)\dockertests\app-packages' From 7060cc60beddbeb4ea3f5a5da704774d2509dd64 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 16:15:07 -0600 Subject: [PATCH 05/10] output complete exception --- .../templates/jobs/run-docker-tests-linux.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/eng/ci/templates/jobs/run-docker-tests-linux.yml b/eng/ci/templates/jobs/run-docker-tests-linux.yml index 2debd4cc..aebe62e0 100644 --- a/eng/ci/templates/jobs/run-docker-tests-linux.yml +++ b/eng/ci/templates/jobs/run-docker-tests-linux.yml @@ -5,12 +5,12 @@ parameters: jobs: - job: "TestDocker" - displayName: 'Docker Integration Tests - Windows' + displayName: 'Docker Integration Tests - Linux' pool: name: ${{ parameters.poolName }} - image: 1es-windows-2022 - os: windows + image: 1es-ubuntu-22.04 + os: linux strategy: maxParallel: 4 @@ -56,19 +56,18 @@ jobs: displayName: 'Package Java worker' - pwsh: | - .\dockertests\build-apps.ps1 + cd dockertests + ./build-apps.ps1 displayName: 'Build and package test apps' - - pwsh: | + - bash: | cd dockertests - python -m pytest . -v --tb=short + pytest . -v --tb=long displayName: 'Run Docker integration tests' env: - PYTHONUTF8: '1' - PYTHONIOENCODING: 'utf-8' - FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)\java-worker' + FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)/java-worker' FUNCTIONS_TEST_USE_AZURITE: 'true' FUNCTIONS_TEST_RUNTIME: 'java' FUNCTIONS_TEST_RUNTIME_VERSION: '$(javaVersion)' FUNCTIONS_TEST_HOST_VERSION: '4' - FUNCTIONS_TEST_APPS_DIR: '$(Build.SourcesDirectory)\dockertests\app-packages' + FUNCTIONS_TEST_APPS_DIR: '$(Build.SourcesDirectory)/dockertests/app-packages' From 1a14834a1265b077006b1fe8a228cbe1f315a77e Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 16:32:52 -0600 Subject: [PATCH 06/10] skip api version check --- .../controllers/azurite_container_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py index d3920963..4e434b9c 100644 --- a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py +++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py @@ -119,7 +119,9 @@ def spawn_container(self) -> None: "-p", queue_port_arg, "-p", table_port_arg, "-e", f"AZURITE_ACCOUNTS={self._account_name}:{self._account_key}", - "mcr.microsoft.com/azure-storage/azurite" + "mcr.microsoft.com/azure-storage/azurite", + "azurite", + "--skipApiVersionCheck" ] print(f"šŸ”‘ Using account: {self._account_name}") From ae39bc631dbc0f7d064104d63d813cfccb427224 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Mon, 12 Jan 2026 16:56:54 -0600 Subject: [PATCH 07/10] pin azure-storage-blob --- dockertests/azure-functions-test-kit/pyproject.toml | 2 +- eng/ci/templates/jobs/run-docker-tests-linux.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dockertests/azure-functions-test-kit/pyproject.toml b/dockertests/azure-functions-test-kit/pyproject.toml index 953a5088..c8bebcdd 100644 --- a/dockertests/azure-functions-test-kit/pyproject.toml +++ b/dockertests/azure-functions-test-kit/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.8" dependencies = [ "pytest>=7.0", "pytest-xdist>=3.0", - "azure-storage-blob>=12.19.0", + "azure-storage-blob==12.27.1", "requests>=2.31.0", "cryptography>=41.0.0", "python-dotenv>=1.0.0", diff --git a/eng/ci/templates/jobs/run-docker-tests-linux.yml b/eng/ci/templates/jobs/run-docker-tests-linux.yml index aebe62e0..70c4564c 100644 --- a/eng/ci/templates/jobs/run-docker-tests-linux.yml +++ b/eng/ci/templates/jobs/run-docker-tests-linux.yml @@ -62,7 +62,7 @@ jobs: - bash: | cd dockertests - pytest . -v --tb=long + pytest . -v -n auto --tb=long displayName: 'Run Docker integration tests' env: FUNCTIONS_TEST_WORKER_DIR: '$(Build.SourcesDirectory)/java-worker' From 95e73c3a79ad436768e36a04e8d8f7206059bd49 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 13 Jan 2026 09:55:58 -0600 Subject: [PATCH 08/10] revert other changes back --- .../azurite_container_controller.py | 4 +-- .../environments/consumption.py | 29 ++++++------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py index 4e434b9c..d3920963 100644 --- a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py +++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py @@ -119,9 +119,7 @@ def spawn_container(self) -> None: "-p", queue_port_arg, "-p", table_port_arg, "-e", f"AZURITE_ACCOUNTS={self._account_name}:{self._account_key}", - "mcr.microsoft.com/azure-storage/azurite", - "azurite", - "--skipApiVersionCheck" + "mcr.microsoft.com/azure-storage/azurite" ] print(f"šŸ”‘ Using account: {self._account_name}") diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py index f21c3612..0359c355 100644 --- a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py +++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py @@ -198,26 +198,15 @@ def url(self) -> str: def _ensure_blob_container(self) -> None: """Ensure the blob container exists.""" - import time - max_retries = 5 - retry_delay = 2 - - for attempt in range(max_retries): - try: - container_client = self.blob_service_client.get_container_client(self.apps_container_name) - if not container_client.exists(): - print(f"šŸ“¦ Creating blob container '{self.apps_container_name}'...") - container_client.create_container() - else: - print(f"šŸ“¦ Using existing blob container '{self.apps_container_name}'") - return # Success - except Exception as e: - if attempt < max_retries - 1: - print(f"āš ļø Failed to ensure blob container (attempt {attempt + 1}/{max_retries}): {e}") - print(f"ā³ Retrying in {retry_delay}s...") - time.sleep(retry_delay) - else: - raise RuntimeError(f"Failed to ensure blob container after {max_retries} attempts: {e}") + try: + container_client = self.blob_service_client.get_container_client(self.apps_container_name) + if not container_client.exists(): + print(f"šŸ“¦ Creating blob container '{self.apps_container_name}'...") + container_client.create_container() + else: + print(f"šŸ“¦ Using existing blob container '{self.apps_container_name}'") + except Exception as e: + raise RuntimeError(f"Failed to ensure blob container: {e}") def _generate_container_sas(self) -> None: """Generate a SAS token for the container.""" From ae3dfdd7482a12c160bc07f4e4d6e066fa787d2d Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 13 Jan 2026 10:26:22 -0600 Subject: [PATCH 09/10] set timezone before setting env variables --- ...nctionEnvironmentReloadRequestHandler.java | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/microsoft/azure/functions/worker/handler/FunctionEnvironmentReloadRequestHandler.java b/src/main/java/com/microsoft/azure/functions/worker/handler/FunctionEnvironmentReloadRequestHandler.java index 5fbd1f67..141e09b1 100644 --- a/src/main/java/com/microsoft/azure/functions/worker/handler/FunctionEnvironmentReloadRequestHandler.java +++ b/src/main/java/com/microsoft/azure/functions/worker/handler/FunctionEnvironmentReloadRequestHandler.java @@ -35,8 +35,10 @@ String execute(FunctionEnvironmentReloadRequest request, Builder response) throw if (environmentVariables.isEmpty()) { return "Ignoring FunctionEnvironmentReloadRequest as newSettings map is empty."; } - setEnv(environmentVariables); - setTimeZone(environmentVariables); + + // Set timezone and get modified environment variables with TZ adjusted if needed + Map modifiedEnvVars = setTimeZone(environmentVariables); + setEnv(modifiedEnvVars); setCapabilities(response, environmentVariables); return "FunctionEnvironmentReloadRequest completed"; @@ -56,32 +58,38 @@ private void setCapabilities(FunctionEnvironmentReloadResponse.Builder response, } /* - * Sets the default timezone based on the TZ environment variable + * Sets the default timezone based on the TZ environment variable. + * Returns a modified map where both WEBSITE_TIME_ZONE and TZ are synchronized. */ - private void setTimeZone(Map environmentVariables) { - // Check WEBSITE_TIME_ZONE first, fall back to TZ if not set - String tzValue = environmentVariables.get("WEBSITE_TIME_ZONE"); - String tzSource = "WEBSITE_TIME_ZONE"; + private Map setTimeZone(Map environmentVariables) { + String websiteTimeZone = environmentVariables.get("WEBSITE_TIME_ZONE"); + String tz = environmentVariables.get("TZ"); + + // Determine which timezone to use (WEBSITE_TIME_ZONE takes precedence) + String tzValue = (websiteTimeZone != null && !websiteTimeZone.isEmpty()) ? websiteTimeZone : tz; if (tzValue == null || tzValue.isEmpty()) { - tzValue = environmentVariables.get("TZ"); - tzSource = "TZ"; + return environmentVariables; } - if (tzValue != null && !tzValue.isEmpty()) { - try { - TimeZone timeZone = TimeZone.getTimeZone(tzValue); - TimeZone.setDefault(timeZone); - System.setProperty("user.timezone", timeZone.getID()); - WorkerLogManager.getSystemLogger().log(Level.INFO, - String.format("Set default timezone to: %s (from %s environment variable: %s)", - timeZone.getID(), tzSource, tzValue)); - } catch (Exception e) { - WorkerLogManager.getSystemLogger().log(Level.WARNING, - String.format("Failed to set timezone from %s environment variable '%s': %s", - tzSource, tzValue, e.getMessage())); - } + // Set the JVM timezone + try { + TimeZone timeZone = TimeZone.getTimeZone(tzValue); + TimeZone.setDefault(timeZone); + System.setProperty("user.timezone", timeZone.getID()); + WorkerLogManager.getSystemLogger().log(Level.INFO, + String.format("Set default timezone to: %s", timeZone.getID())); + } catch (Exception e) { + WorkerLogManager.getSystemLogger().log(Level.WARNING, + String.format("Failed to set timezone '%s': %s", tzValue, e.getMessage())); } + + // Synchronize both environment variables to the same value to prevent race conditions + Map modifiedVars = new HashMap<>(environmentVariables); + modifiedVars.put("WEBSITE_TIME_ZONE", tzValue); + modifiedVars.put("TZ", tzValue); + + return modifiedVars; } /* From 3b51bd911c54a24f929247d3162b8d8c046f3ddb Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 13 Jan 2026 16:17:51 -0600 Subject: [PATCH 10/10] output logs for failing tests --- .../src/azure_functions_test_kit/plugin.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/plugin.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/plugin.py index 32610f26..fe51b77c 100644 --- a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/plugin.py +++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/plugin.py @@ -9,6 +9,7 @@ import os from pathlib import Path from dotenv import load_dotenv +import pytest def pytest_configure(config): @@ -30,3 +31,42 @@ def pytest_configure(config): break else: print("ā„¹ļø [azure-functions-test-kit] No .env file found, using environment variables") + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Hook to capture test failures and display container logs.""" + # Execute all other hooks to obtain the report object + outcome = yield + rep = outcome.get_result() + + # Only process test failures in the call phase (actual test execution) + if rep.when == "call" and rep.failed: + # Try to get the test_env fixture from the test + test_env = None + if hasattr(item, 'funcargs') and 'test_env' in item.funcargs: + test_env = item.funcargs['test_env'] + + if test_env and hasattr(test_env, 'functions_controller'): + functions_controller = test_env.functions_controller + if functions_controller: + try: + logs = functions_controller.get_container_logs() + + # Add container logs to the test report + print("\n" + "="*80) + print("🐳 CONTAINER LOGS FOR FAILED TEST") + print("="*80) + print(logs) + print("="*80 + "\n") + + # Also append to the test's longrepr for visibility in reports + if hasattr(rep, 'longrepr'): + log_section = f"\n\n{'='*80}\n🐳 CONTAINER LOGS\n{'='*80}\n{logs}\n{'='*80}\n" + if rep.longrepr: + rep.longrepr = str(rep.longrepr) + log_section + else: + rep.longrepr = log_section + + except Exception as e: + print(f"\nāš ļø Failed to retrieve container logs: {e}\n")