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