diff --git a/.gitignore b/.gitignore
index b0e770af..68ad7d72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -288,3 +288,6 @@ __pycache__/
# Environment configuration files
.env
+
+# Docker test packages
+dockertests/app-packages/
diff --git a/dockertests/app-src/TimezoneCheck/host.json b/dockertests/app-src/TimezoneCheck/host.json
new file mode 100644
index 00000000..b7e5ad1c
--- /dev/null
+++ b/dockertests/app-src/TimezoneCheck/host.json
@@ -0,0 +1,7 @@
+{
+ "version": "2.0",
+ "extensionBundle": {
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
+ "version": "[4.*, 5.0.0)"
+ }
+}
diff --git a/dockertests/app-src/TimezoneCheck/pom.xml b/dockertests/app-src/TimezoneCheck/pom.xml
new file mode 100644
index 00000000..9abd2db0
--- /dev/null
+++ b/dockertests/app-src/TimezoneCheck/pom.xml
@@ -0,0 +1,74 @@
+
+
+ 4.0.0
+
+ com.microsoft.azure.samples
+ timezone-check
+ 1.0.0
+ jar
+
+ Azure Functions Java Timezone Check
+ Simple HTTP trigger that returns the current timezone
+
+
+ UTF-8
+ 8
+ 8
+ 8
+ 1.39.0
+ 3.2.2
+ timezone-check
+
+
+
+
+ com.microsoft.azure.functions
+ azure-functions-java-library
+ ${azure.functions.java.library.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+ com.microsoft.azure
+ azure-functions-maven-plugin
+ ${azure.functions.maven.plugin.version}
+
+ timezone-check
+ rg-functions-quickstart-java
+ eastus
+
+ linux
+ 17
+
+
+
+ FUNCTIONS_EXTENSION_VERSION
+ ~4
+
+
+
+
+
+ package-functions
+
+ package
+
+
+
+
+
+
+
diff --git a/dockertests/app-src/TimezoneCheck/src/main/java/com/microsoft/azure/samples/TimezoneFunction.java b/dockertests/app-src/TimezoneCheck/src/main/java/com/microsoft/azure/samples/TimezoneFunction.java
new file mode 100644
index 00000000..6d14940b
--- /dev/null
+++ b/dockertests/app-src/TimezoneCheck/src/main/java/com/microsoft/azure/samples/TimezoneFunction.java
@@ -0,0 +1,40 @@
+package com.microsoft.azure.samples;
+
+import com.microsoft.azure.functions.annotation.*;
+import com.microsoft.azure.functions.*;
+
+import java.util.TimeZone;
+
+/**
+ * Azure Functions HTTP Trigger that returns the current timezone information.
+ * This function is used to verify that the TZ environment variable is correctly
+ * applied to the Java runtime.
+ */
+public class TimezoneFunction {
+
+ @FunctionName("GetTimezone")
+ public HttpResponseMessage run(
+ @HttpTrigger(
+ name = "req",
+ methods = {HttpMethod.GET},
+ authLevel = AuthorizationLevel.ANONYMOUS
+ ) HttpRequestMessage request,
+ final ExecutionContext context) {
+
+ context.getLogger().info("Processing timezone request.");
+
+ // Get the default timezone
+ TimeZone defaultTimezone = TimeZone.getDefault();
+ String timezoneId = defaultTimezone.getID();
+ String tzEnvVar = System.getenv("TZ");
+
+ context.getLogger().info(String.format("Default timezone ID: %s, TZ env var: %s",
+ timezoneId, tzEnvVar != null ? tzEnvVar : "not set"));
+
+ // Return the timezone ID
+ return request.createResponseBuilder(HttpStatus.OK)
+ .header("Content-Type", "text/plain")
+ .body(timezoneId)
+ .build();
+ }
+}
diff --git a/dockertests/azure-functions-test-kit/pyproject.toml b/dockertests/azure-functions-test-kit/pyproject.toml
new file mode 100644
index 00000000..953a5088
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/pyproject.toml
@@ -0,0 +1,24 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "azure-functions-test-kit"
+version = "0.1.0"
+description = "Reusable test kit for Azure Functions workers"
+requires-python = ">=3.8"
+dependencies = [
+ "pytest>=7.0",
+ "pytest-xdist>=3.0",
+ "azure-storage-blob>=12.19.0",
+ "requests>=2.31.0",
+ "cryptography>=41.0.0",
+ "python-dotenv>=1.0.0",
+ "pyjwt>=2.8.0"
+]
+
+[project.entry-points.pytest11]
+azure_functions_test_kit = "azure_functions_test_kit.plugin"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/azure_functions_test_kit"]
diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/__init__.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/__init__.py
new file mode 100644
index 00000000..e74d36a5
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/__init__.py
@@ -0,0 +1,26 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""
+Azure Functions Test Kit - Testing utilities for Azure Functions.
+
+Usage:
+ from azure_functions_test_kit import LinuxConsumptionTestEnvironment
+
+ @pytest.fixture
+ def test_env():
+ with LinuxConsumptionTestEnvironment(apps_to_upload=['MyApp']) as env:
+ yield env
+"""
+
+__version__ = "0.1.0"
+
+# Export main classes for easy import
+from .controllers.azurite_container_controller import AzuriteContainerController
+from .controllers.functions_container_controller import FunctionsContainerController
+from .environments.consumption import LinuxConsumptionTestEnvironment
+
+__all__ = [
+ "AzuriteContainerController",
+ "FunctionsContainerController",
+ "LinuxConsumptionTestEnvironment"
+]
diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/__init__.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/__init__.py
new file mode 100644
index 00000000..09786be5
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/__init__.py
@@ -0,0 +1,2 @@
+from .azurite_container_controller import AzuriteContainerController
+from .functions_container_controller import FunctionsContainerController
diff --git a/dockertests/utils/azurite_container_controller.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py
similarity index 100%
rename from dockertests/utils/azurite_container_controller.py
rename to dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/azurite_container_controller.py
diff --git a/dockertests/utils/functions_container_controller.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/functions_container_controller.py
similarity index 99%
rename from dockertests/utils/functions_container_controller.py
rename to dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/functions_container_controller.py
index 6955aa26..b723374c 100644
--- a/dockertests/utils/functions_container_controller.py
+++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/controllers/functions_container_controller.py
@@ -613,4 +613,4 @@ def __exit__(self, exc_type, exc_value, traceback):
if traceback:
print(f'â Test failed with container logs:\n{logs}',
file=sys.stderr,
- flush=True)
\ No newline at end of file
+ flush=True)
diff --git a/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/__init__.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/__init__.py
new file mode 100644
index 00000000..409ee712
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/__init__.py
@@ -0,0 +1 @@
+from .consumption import LinuxConsumptionTestEnvironment
diff --git a/dockertests/utils/linux_consumption_test_environment.py b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py
similarity index 89%
rename from dockertests/utils/linux_consumption_test_environment.py
rename to dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py
index f39562f8..0359c355 100644
--- a/dockertests/utils/linux_consumption_test_environment.py
+++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/environments/consumption.py
@@ -1,66 +1,61 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""
-Test environment manager for Azure Functions container testing.
-Manages both Functions containers and storage (Azurite or real Azure Storage).
+Linux Consumption test environment.
"""
import os
import uuid
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from pathlib import Path
-from typing import Optional, Dict, List
+from typing import Dict, Optional, List
from azure.storage.blob import BlobServiceClient, generate_container_sas, ContainerSasPermissions
-from .azurite_container_controller import AzuriteContainerController
-from .functions_container_controller import FunctionsContainerController
+from ..controllers.azurite_container_controller import AzuriteContainerController
+from ..controllers.functions_container_controller import FunctionsContainerController
class LinuxConsumptionTestEnvironment:
- """Manages the complete test environment including storage and Functions containers."""
+ """Test environment for Linux Consumption plan."""
# Supported app package extensions
SUPPORTED_EXTENSIONS = ['.zip', '.squashfs']
- def __init__(
- self,
- use_azurite: Optional[bool] = None,
- storage_connection_string: Optional[str] = None,
- apps_directory: Optional[str] = None,
- apps_to_upload: Optional[List[str]] = None,
- apps_container_name: str = "app",
- runtime: Optional[str] = None,
- runtime_version: Optional[str] = None,
- host_version: Optional[str] = None,
- worker_directory: Optional[str] = None,
- docker_flags: Optional[List[str]] = None,
- environment_id: Optional[str] = None
- ):
- """Initialize the test environment.
+ def __init__(self,
+ use_azurite: Optional[bool] = None,
+ storage_connection_string: Optional[str] = None,
+ apps_directory: Optional[str] = None,
+ apps_to_upload: Optional[list] = None,
+ apps_container_name: str = "app",
+ runtime: Optional[str] = None,
+ runtime_version: Optional[str] = None,
+ host_version: Optional[str] = None,
+ worker_directory: Optional[str] = None,
+ docker_flags: Optional[list] = None,
+ environment_id: Optional[str] = None):
+ """Initialize the environment.
Args:
use_azurite: If True, use Azurite emulator. If False, use real Azure Storage.
If None, reads from FUNCTIONS_TEST_USE_AZURITE env var (default: True)
- storage_connection_string: Connection string for real Azure Storage (required if use_azurite=False).
+ storage_connection_string: Connection string for real Azure Storage.
If None, reads from FUNCTIONS_TEST_STORAGE_CONNECTION_STRING env var
apps_directory: Directory containing app packages to upload.
- If None, reads from FUNCTIONS_TEST_APPS_DIR env var (default: "./apps")
+ If None, reads from FUNCTIONS_TEST_APPS_DIR env var (default: "./app-packages")
apps_to_upload: Optional list of app names to upload (with or without extensions).
Examples: ['app1', 'app2.squashfs']
- If specified without extension, will match any supported extension (.zip, .squashfs)
If None, all apps in apps_directory will be uploaded
apps_container_name: Blob container name for storing app packages
- runtime: Functions runtime (java, python, dotnet, node, etc.).
+ runtime: Functions runtime (java, python, etc.).
If None, reads from FUNCTIONS_TEST_RUNTIME env var (default: "java")
runtime_version: Runtime version.
If None, reads from FUNCTIONS_TEST_RUNTIME_VERSION env var (default: "21")
host_version: Azure Functions host version.
If None, reads from FUNCTIONS_TEST_HOST_VERSION env var (default: "4")
worker_directory: Path to custom worker directory to mount.
- If None, reads from FUNCTIONS_TEST_WORKER_DIR env var.
- If still None, uses built-in worker from image.
+ If None, reads from FUNCTIONS_TEST_WORKER_DIR env var
docker_flags: Additional Docker flags for the Functions container
- environment_id: Optional unique ID for this environment (auto-generated if not provided)
+ environment_id: Unique identifier for this environment (auto-generated if None)
"""
# Read from environment variables with defaults
self.use_azurite = use_azurite if use_azurite is not None else \
@@ -80,6 +75,7 @@ def __init__(
# Generate unique environment ID for this test environment
# Use full UUID with hyphens (36 characters)
+ import uuid
self.environment_id = environment_id or str(uuid.uuid4())
# Storage configuration
@@ -111,14 +107,23 @@ def __init__(
self._blob_service_client: Optional[BlobServiceClient] = None
self._container_sas_token: Optional[str] = None
self._uploaded_apps: Dict[str, str] = {} # {blob_name_with_ext: blob_url}
-
+
+ def __enter__(self):
+ """Start the environment."""
+ self.start()
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Stop the environment."""
+ self.stop()
+
@property
def storage_connection_string(self) -> str:
"""Get the storage connection string."""
if self.use_azurite:
return self.azurite.connection_string
return self._storage_connection_string
-
+
@property
def docker_storage_connection_string(self) -> str:
"""Get the storage connection string for use from Docker containers."""
@@ -126,7 +131,7 @@ def docker_storage_connection_string(self) -> str:
return self.azurite.docker_connection_string
# For real Azure Storage, the connection string is the same
return self._storage_connection_string
-
+
@property
def blob_service_client(self) -> BlobServiceClient:
"""Get or create the BlobServiceClient."""
@@ -135,7 +140,7 @@ def blob_service_client(self) -> BlobServiceClient:
self.storage_connection_string
)
return self._blob_service_client
-
+
def start(self) -> 'LinuxConsumptionTestEnvironment':
"""Start the test environment (storage and Functions container)."""
print(f"đ Starting test environment '{self.environment_id}'...")
@@ -171,7 +176,7 @@ def start(self) -> 'LinuxConsumptionTestEnvironment':
print(f"â
Test environment '{self.environment_id}' ready")
return self
-
+
def stop(self) -> None:
"""Stop the test environment and clean up resources."""
print(f"đ Stopping test environment '{self.environment_id}'...")
@@ -183,7 +188,14 @@ def stop(self) -> None:
self.azurite.safe_kill_container()
print(f"â
Test environment '{self.environment_id}' stopped")
-
+
+ @property
+ def url(self) -> str:
+ """Get the function app URL."""
+ if not self.functions_controller:
+ raise RuntimeError("Environment not started")
+ return self.functions_controller.url
+
def _ensure_blob_container(self) -> None:
"""Ensure the blob container exists."""
try:
@@ -213,7 +225,7 @@ def _generate_container_sas(self) -> None:
container_name=self.apps_container_name,
account_key=account_key,
permission=ContainerSasPermissions(read=True, list=True),
- expiry=datetime.utcnow() + timedelta(days=7)
+ expiry=datetime.now(datetime.UTC if hasattr(datetime, 'UTC') else timezone.utc) + timedelta(days=7)
)
self._container_sas_token = sas_token
@@ -221,7 +233,7 @@ def _generate_container_sas(self) -> None:
except Exception as e:
raise RuntimeError(f"Failed to generate SAS token: {e}")
-
+
def _upload_app_packages(self) -> None:
"""Upload app packages from the apps directory to blob storage."""
if not self.apps_directory.exists():
@@ -289,7 +301,7 @@ def _upload_app_packages(self) -> None:
print(f" â
Uploaded: {blob_url}")
print(f"â
Uploaded {len(app_files)} app package(s)")
-
+
def get_blob_sas_url(self, blob_name: str, container_name: Optional[str] = None) -> str:
"""Get a SAS URL for a specific blob.
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
new file mode 100644
index 00000000..32610f26
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/src/azure_functions_test_kit/plugin.py
@@ -0,0 +1,32 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""
+Pytest plugin for Azure Functions Test Kit.
+
+Provides automatic .env file loading and optional helper fixtures.
+Tests are free to create their own fixtures using the exported classes.
+"""
+import os
+from pathlib import Path
+from dotenv import load_dotenv
+
+
+def pytest_configure(config):
+ """Load .env file if it exists in the test directory."""
+ # Try to find .env file in the test directory or parent directories
+ test_dir = Path.cwd()
+ env_file = test_dir / '.env'
+
+ if env_file.exists():
+ load_dotenv(env_file)
+ print(f"â
[azure-functions-test-kit] Loaded configuration from {env_file}")
+ else:
+ # Check parent directories (up to 3 levels)
+ for parent in test_dir.parents[:3]:
+ env_file = parent / '.env'
+ if env_file.exists():
+ load_dotenv(env_file)
+ print(f"â
[azure-functions-test-kit] Loaded configuration from {env_file}")
+ break
+ else:
+ print("âšī¸ [azure-functions-test-kit] No .env file found, using environment variables")
diff --git a/dockertests/azure-functions-test-kit/tests/test_import.py b/dockertests/azure-functions-test-kit/tests/test_import.py
new file mode 100644
index 00000000..a0808e2e
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/tests/test_import.py
@@ -0,0 +1,14 @@
+"""Test that the package imports work correctly."""
+from azure_functions_test_kit import (
+ AzuriteContainerController,
+ FunctionsContainerController,
+ LinuxConsumptionTestEnvironment
+)
+
+
+def test_imports():
+ """Verify all main classes can be imported."""
+ assert AzuriteContainerController is not None
+ assert FunctionsContainerController is not None
+ assert LinuxConsumptionTestEnvironment is not None
+
diff --git a/dockertests/azure-functions-test-kit/tests/test_plugin.py b/dockertests/azure-functions-test-kit/tests/test_plugin.py
new file mode 100644
index 00000000..529c4e46
--- /dev/null
+++ b/dockertests/azure-functions-test-kit/tests/test_plugin.py
@@ -0,0 +1,25 @@
+"""Test that the pytest plugin loads .env files correctly."""
+import os
+from pathlib import Path
+from unittest.mock import patch, MagicMock
+
+
+def test_plugin_loads_dotenv():
+ """Test that the plugin's pytest_configure function loads .env files."""
+ from azure_functions_test_kit.plugin import pytest_configure
+
+ # Create a mock config object (pytest doesn't require anything from it in our plugin)
+ mock_config = MagicMock()
+
+ with patch('azure_functions_test_kit.plugin.load_dotenv') as mock_load_dotenv:
+ with patch('azure_functions_test_kit.plugin.Path.cwd') as mock_cwd:
+ # Mock that .env file exists in current directory
+ mock_path = MagicMock()
+ mock_path.exists.return_value = True
+ mock_cwd.return_value = MagicMock(__truediv__=lambda self, x: mock_path)
+
+ pytest_configure(mock_config)
+
+ # Verify load_dotenv was called
+ mock_load_dotenv.assert_called_once()
+
diff --git a/dockertests/conftest.py b/dockertests/conftest.py
deleted file mode 100644
index 153cf2a4..00000000
--- a/dockertests/conftest.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-Shared pytest fixtures for Azure Functions Java worker tests.
-"""
-
-import pytest
-import sys
-from pathlib import Path
-from dotenv import load_dotenv
-
-
-@pytest.fixture(scope="session", autouse=True)
-def load_env():
- """Load .env file before running tests"""
- env_file = Path(__file__).parent / '.env'
- if env_file.exists():
- load_dotenv(env_file)
- print(f"â
Loaded configuration from {env_file}")
- else:
- print(f"â ī¸ No .env file found at {env_file}, using defaults")
diff --git a/dockertests/linux-consumption-tests/test_sdk_types.py b/dockertests/linux-consumption-tests/test_sdk_types.py
index e0c2145b..2bf89663 100644
--- a/dockertests/linux-consumption-tests/test_sdk_types.py
+++ b/dockertests/linux-consumption-tests/test_sdk_types.py
@@ -7,7 +7,7 @@
import pytest
import time
-from utils import LinuxConsumptionTestEnvironment
+from azure_functions_test_kit import LinuxConsumptionTestEnvironment
# Configuration
diff --git a/dockertests/linux-consumption-tests/test_telemetry_capabilities.py b/dockertests/linux-consumption-tests/test_telemetry_capabilities.py
index 85ec7174..a44f86d9 100644
--- a/dockertests/linux-consumption-tests/test_telemetry_capabilities.py
+++ b/dockertests/linux-consumption-tests/test_telemetry_capabilities.py
@@ -6,7 +6,7 @@
"""
import pytest
-from utils import LinuxConsumptionTestEnvironment
+from azure_functions_test_kit import LinuxConsumptionTestEnvironment
@pytest.fixture
diff --git a/dockertests/linux-consumption-tests/test_timezone.py b/dockertests/linux-consumption-tests/test_timezone.py
new file mode 100644
index 00000000..35de69d2
--- /dev/null
+++ b/dockertests/linux-consumption-tests/test_timezone.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+"""
+Pytest tests for timezone handling in Azure Functions Java worker.
+Tests verify that the WEBSITE_TIME_ZONE and TZ environment variables are correctly applied
+and the Java runtime returns the expected timezone.
+WEBSITE_TIME_ZONE takes precedence over TZ.
+"""
+
+import pytest
+import requests
+from azure_functions_test_kit import LinuxConsumptionTestEnvironment
+
+
+# Configuration
+APP_NAME = "TimezoneCheck"
+DEFAULT_TIMEZONE = "Etc/UTC"
+
+
+@pytest.fixture
+def test_env():
+ """Create a fresh test environment for each test"""
+ with LinuxConsumptionTestEnvironment(apps_to_upload=[APP_NAME]) as env:
+ yield env
+
+
+def verify_timezone(test_env, website_tz=None, tz=None, expected_timezone=DEFAULT_TIMEZONE):
+ """
+ Helper function to verify timezone is correctly set based on env variables.
+
+ Args:
+ test_env: TestEnvironment fixture
+ website_tz: Value for WEBSITE_TIME_ZONE environment variable (None to omit)
+ tz: Value for TZ environment variable (None to omit)
+ expected_timezone: Expected timezone ID returned by the function
+ """
+ # Build environment variables
+ app_url = test_env.get_blob_sas_url(APP_NAME)
+ env_vars = {
+ 'SCM_RUN_FROM_PACKAGE': app_url,
+ 'AzureWebJobsStorage': test_env.docker_storage_connection_string
+ }
+
+ # Add timezone variables if specified
+ if website_tz is not None:
+ env_vars['WEBSITE_TIME_ZONE'] = website_tz
+ if tz is not None:
+ env_vars['TZ'] = tz
+
+ # Assign container with environment variables
+ test_env.functions_controller.assign_container(env=env_vars)
+
+ # Wait for functions to be loaded
+ assert test_env.functions_controller.wait_for_host_running(timeout=360), \
+ "Functions host did not reach running state within timeout"
+
+ assert test_env.functions_controller.wait_for_functions_loaded(timeout=360), \
+ "Functions did not load within timeout"
+
+ # Invoke the function and get the timezone
+ req = requests.Request('GET', f'{test_env.functions_controller.url}/api/GetTimezone')
+ response = test_env.functions_controller.send_request(req, post_assignment=True)
+
+ # Verify response is successful
+ assert response.status_code == 200, \
+ f"Function returned status {response.status_code}. Response: {response.text}"
+
+ actual_timezone = response.text.strip()
+
+ # Verify the timezone matches expected
+ assert actual_timezone == expected_timezone, \
+ f"Timezone mismatch. Expected: {expected_timezone}, Actual: {actual_timezone}"
+
+ # Build display message
+ tz_sources = []
+ if website_tz is not None:
+ tz_sources.append(f"WEBSITE_TIME_ZONE='{website_tz}'")
+ if tz is not None:
+ tz_sources.append(f"TZ='{tz}'")
+ if not tz_sources:
+ tz_sources.append("not set (default)")
+
+ print(f"â
Test passed: {', '.join(tz_sources)} returned timezone '{actual_timezone}'")
+
+
+def test_timezone_default_when_not_set(test_env):
+ """
+ Test that timezone defaults to Etc/UTC when neither WEBSITE_TIME_ZONE nor TZ is set.
+ """
+ verify_timezone(
+ test_env=test_env,
+ website_tz=None,
+ tz=None,
+ expected_timezone=DEFAULT_TIMEZONE
+ )
+
+
+def test_timezone_website_time_zone_takes_precedence(test_env):
+ """
+ Test that WEBSITE_TIME_ZONE takes precedence over TZ when both are set.
+ """
+ verify_timezone(
+ test_env=test_env,
+ website_tz='America/New_York',
+ tz='Europe/London', # This should be ignored
+ expected_timezone='America/New_York'
+ )
+
+
+def test_timezone_website_time_zone_america_new_york(test_env):
+ """
+ Test that WEBSITE_TIME_ZONE=America/New_York returns the correct timezone.
+ """
+ verify_timezone(
+ test_env=test_env,
+ website_tz='America/New_York',
+ expected_timezone='America/New_York'
+ )
+
+
+def test_timezone_tz_fallback_when_website_time_zone_not_set(test_env):
+ """
+ Test that TZ is used as fallback when WEBSITE_TIME_ZONE is not set.
+ """
+ verify_timezone(
+ test_env=test_env,
+ tz='Europe/London',
+ expected_timezone='Europe/London'
+ )
+
+if __name__ == "__main__":
+ """Allow running tests directly with python"""
+ pytest.main([__file__, "-v", "-s"])
diff --git a/dockertests/utils/__init__.py b/dockertests/utils/__init__.py
deleted file mode 100644
index 257b9834..00000000
--- a/dockertests/utils/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""
-Utilities for Azure Functions container testing.
-"""
-
-from .functions_container_controller import FunctionsContainerController
-from .azurite_container_controller import AzuriteContainerController
-from .linux_consumption_test_environment import LinuxConsumptionTestEnvironment
-
-__all__ = [
- 'FunctionsContainerController',
- 'AzuriteContainerController',
- 'LinuxConsumptionTestEnvironment',
-]
diff --git a/eng/ci/templates/jobs/run-docker-tests-linux.yml b/eng/ci/templates/jobs/run-docker-tests-linux.yml
index 4457d518..c8e32a87 100644
--- a/eng/ci/templates/jobs/run-docker-tests-linux.yml
+++ b/eng/ci/templates/jobs/run-docker-tests-linux.yml
@@ -13,6 +13,7 @@ jobs:
os: linux
strategy:
+ maxParallel: 4
matrix:
Java-8:
javaVersion: '8'
@@ -47,7 +48,7 @@ jobs:
- bash: |
python -m pip install --upgrade pip
- pip install -e dockertests/
+ pip install -e dockertests/azure-functions-test-kit
displayName: 'Install Python dependencies'
- pwsh: |
diff --git a/eng/ci/templates/jobs/run-emulated-tests-linux.yml b/eng/ci/templates/jobs/run-emulated-tests-linux.yml
index 61e77781..96b5ed25 100644
--- a/eng/ci/templates/jobs/run-emulated-tests-linux.yml
+++ b/eng/ci/templates/jobs/run-emulated-tests-linux.yml
@@ -21,7 +21,7 @@ jobs:
value: $[variables.isTagTemp]
strategy:
- maxParallel: 4
+ maxParallel: 5
matrix:
open-jdk-8-linux:
JDK_DOWNLOAD_LINK: 'https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u$(JDK8_LINUX_VERSION)-b$(JDK8_LINUX_BUILD)/OpenJDK8U-jdk_x64_linux_hotspot_8u$(JDK8_LINUX_VERSION)b$(JDK8_LINUX_BUILD).tar.gz'
diff --git a/eng/ci/templates/jobs/run-emulated-tests-windows.yml b/eng/ci/templates/jobs/run-emulated-tests-windows.yml
index 48caeb96..f2a87f0d 100644
--- a/eng/ci/templates/jobs/run-emulated-tests-windows.yml
+++ b/eng/ci/templates/jobs/run-emulated-tests-windows.yml
@@ -21,7 +21,7 @@ jobs:
value: $[variables.isTagTemp]
strategy:
- maxParallel: 4
+ maxParallel: 5
matrix:
open-jdk-8-windows:
JDK_DOWNLOAD_LINK: 'https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u$(JDK8_WINDOWS_VERSION)-b$(JDK8_WINDOWS_BUILD)/OpenJDK8U-jdk_x64_windows_hotspot_8u$(JDK8_WINDOWS_VERSION)b$(JDK8_WINDOWS_BUILD).zip'
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 d3b0c048..5fbd1f67 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
@@ -4,6 +4,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.TimeZone;
import java.util.logging.Level;
import com.microsoft.azure.functions.rpc.messages.*;
@@ -35,6 +36,7 @@ String execute(FunctionEnvironmentReloadRequest request, Builder response) throw
return "Ignoring FunctionEnvironmentReloadRequest as newSettings map is empty.";
}
setEnv(environmentVariables);
+ setTimeZone(environmentVariables);
setCapabilities(response, environmentVariables);
return "FunctionEnvironmentReloadRequest completed";
@@ -53,6 +55,35 @@ private void setCapabilities(FunctionEnvironmentReloadResponse.Builder response,
}
}
+ /*
+ * Sets the default timezone based on the TZ environment variable
+ */
+ 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";
+
+ if (tzValue == null || tzValue.isEmpty()) {
+ tzValue = environmentVariables.get("TZ");
+ tzSource = "TZ";
+ }
+
+ 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()));
+ }
+ }
+ }
+
/*
* This is a helper utility specifically to reload environment variables if java
* language worker is started in standby mode by the functions runtime and