Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,6 @@ __pycache__/

# Environment configuration files
.env

# Docker test packages
dockertests/app-packages/
7 changes: 7 additions & 0 deletions dockertests/app-src/TimezoneCheck/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
74 changes: 74 additions & 0 deletions dockertests/app-src/TimezoneCheck/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.microsoft.azure.samples</groupId>
<artifactId>timezone-check</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>Azure Functions Java Timezone Check</name>
<description>Simple HTTP trigger that returns the current timezone</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<azure.functions.maven.plugin.version>1.39.0</azure.functions.maven.plugin.version>
<azure.functions.java.library.version>3.2.2</azure.functions.java.library.version>
<functionAppName>timezone-check</functionAppName>
</properties>

<dependencies>
<dependency>
<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-library</artifactId>
<version>${azure.functions.java.library.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>${azure.functions.maven.plugin.version}</version>
<configuration>
<appName>timezone-check</appName>
<resourceGroup>rg-functions-quickstart-java</resourceGroup>
<region>eastus</region>
<runtime>
<os>linux</os>
<javaVersion>17</javaVersion>
</runtime>
<appSettings>
<property>
<name>FUNCTIONS_EXTENSION_VERSION</name>
<value>~4</value>
</property>
</appSettings>
</configuration>
<executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
24 changes: 24 additions & 0 deletions dockertests/azure-functions-test-kit/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .azurite_container_controller import AzuriteContainerController
from .functions_container_controller import FunctionsContainerController
Original file line number Diff line number Diff line change
Expand Up @@ -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)
flush=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .consumption import LinuxConsumptionTestEnvironment
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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
Expand Down Expand Up @@ -111,22 +107,31 @@ 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."""
if self.use_azurite:
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."""
Expand All @@ -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}'...")
Expand Down Expand Up @@ -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}'...")
Expand All @@ -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:
Expand Down Expand Up @@ -213,15 +225,15 @@ 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
print(f"🔑 Generated container SAS token (expires in 7 days)")

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():
Expand Down Expand Up @@ -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.

Expand Down
Loading
Loading