Skip to content

PoC commit for E2E tests #558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: dev
Choose a base branch
from
Draft
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
135 changes: 135 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.9

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -58,3 +59,137 @@ jobs:
- name: Run tests
run: |
python -m pytest

e2e-azurestorage-linux:
runs-on: ubuntu-latest
env:
E2E_TEST_DURABLE_BACKEND: 'AzureStorage'
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x

- name: Set up Node.js (needed for Azurite)
uses: actions/setup-node@v3
with:
node-version: '18.x' # Azurite requires at least Node 18

- name: Setup E2E tests
shell: pwsh
run: |
.\test\e2e\Tests\build-e2e-test.ps1

- name: Build
working-directory: test/e2e/Tests
run: dotnet build

- name: Run E2E tests
working-directory: test/e2e/Tests
run: dotnet test --filter AzureStorage!=Skip

e2e-azurestorage-windows:
runs-on: windows-latest
env:
E2E_TEST_DURABLE_BACKEND: 'AzureStorage'
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x

- name: Set up Node.js (needed for Azurite)
uses: actions/setup-node@v3
with:
node-version: '18.x' # Azurite requires at least Node 18

- name: Setup E2E tests
shell: pwsh
run: |
.\test\e2e\Tests\build-e2e-test.ps1

- name: Build
working-directory: test/e2e/Tests
run: dotnet build

- name: Run E2E tests
working-directory: test/e2e/Tests
run: dotnet test --filter AzureStorage!=Skip

e2e-mssql:
runs-on: ubuntu-latest
env:
E2E_TEST_DURABLE_BACKEND: "MSSQL"
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x

- name: Initialize Environment Variables
run: |
echo "MSSQL_SA_PASSWORD=TEST12_$(echo $RANDOM)!" >> $GITHUB_ENV

- name: Setup E2E tests
shell: pwsh
run: |
.\test\e2e\Tests\build-e2e-test.ps1 -StartMSSqlContainer

- name: Build
working-directory: test/e2e/Tests
run: dotnet build

- name: Run E2E tests
working-directory: test/e2e/Tests
run: dotnet test --filter MSSQL!=Skip

e2e-dts:
runs-on: ubuntu-latest
env:
E2E_TEST_DURABLE_BACKEND: "azureManaged"
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x

- name: Setup E2E tests
shell: pwsh
run: |
.\test\e2e\Tests\build-e2e-test.ps1 -StartDTSContainer

- name: Build
working-directory: test/e2e/Tests
run: dotnet build

- name: Run E2E tests
working-directory: test/e2e/Tests
run: dotnet test --logger "console;verbosity=detailed" --filter DTS!=Skip
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,6 @@ appsettings.*.json

# azurite emulator
__azurite_db_*.json

# E2E test folders
/test/e2e/tests/node_modules/*
2 changes: 1 addition & 1 deletion samples-v2/function_chaining/function_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def my_orchestrator(context: df.DurableOrchestrationContext):

@myApp.activity_trigger(input_name="city")
def say_hello(city: str) -> str:
return f"Hello {city}!"
return f"Hello {city}!"
103 changes: 103 additions & 0 deletions test/e2e/Apps/BasicPython/activity_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from datetime import datetime
import logging
import azure.functions as func
import azure.durable_functions as df

bp = df.Blueprint()

attempt_count = {}

class CustomException(Exception):
pass

@bp.route(route="RethrowActivityException_HttpStart")
@bp.durable_client_input(client_name="client")
async def rethrow_activity_exception_http(req: func.HttpRequest, client):
instance_id = await client.start_new('rethrow_activity_exception')

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.route(route="CatchActivityException_HttpStart")
@bp.durable_client_input(client_name="client")
async def catch_activity_exception_http(req: func.HttpRequest, client):
instance_id = await client.start_new('catch_activity_exception')

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.route(route="CatchActivityExceptionFailureDetails_HttpStart")
@bp.durable_client_input(client_name="client")
async def catch_activity_exception_fd_http(req: func.HttpRequest, client):
instance_id = await client.start_new('catch_activity_exception_failure_details')

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.route(route="RetryActivityException_HttpStart")
@bp.durable_client_input(client_name="client")
async def retry_activity_exception_http(req: func.HttpRequest, client):
instance_id = await client.start_new('retry_activity_function')

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.route(route="CustomRetryActivityException_HttpStart")
@bp.durable_client_input(client_name="client")
async def custom_retry_activity_exception_http(req: func.HttpRequest, client):
instance_id = await client.start_new('custom_retry_activity_function')

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.orchestration_trigger(context_name="context")
def rethrow_activity_exception(context: df.DurableOrchestrationContext):
yield context.call_activity('raise_exception', context.instance_id)

@bp.orchestration_trigger(context_name="context")
def catch_activity_exception(context: df.DurableOrchestrationContext):
try:
yield context.call_activity('raise_exception', context.instance_id)
except Exception as e:
logging.error(f"Caught exception: {e}")
return f"Caught exception: {e}"

@bp.orchestration_trigger(context_name="context")
def catch_activity_exception_failure_details(context: df.DurableOrchestrationContext):
try:
yield context.call_activity('raise_exception', context.instance_id)
except Exception as e:
logging.error(f"Caught exception: {e}")
return f"Caught exception: {e}"

@bp.orchestration_trigger(context_name="context")
def retry_activity_function(context: df.DurableOrchestrationContext):
yield context.call_activity_with_retry('raise_exception', retry_options=df.RetryOptions(
first_retry_interval_in_milliseconds=5000,
max_number_of_attempts=3
), input_=context.instance_id)
return "Success"

@bp.orchestration_trigger(context_name="context")
def custom_retry_activity_function(context: df.DurableOrchestrationContext):
yield context.call_activity_with_retry('raise_complex_exception', retry_options=df.RetryOptions(
first_retry_interval_in_milliseconds=5000,
max_number_of_attempts=3
), input_=context.instance_id)
return "Success"

@bp.activity_trigger(input_name="instance")
def raise_exception(instance: str) -> str:
global attempt_count
if instance not in attempt_count:
attempt_count[instance] = 1
raise CustomException(f"This activity failed")
return "This activity succeeded"

@bp.activity_trigger(input_name="instance2")
def raise_complex_exception(instance2: str) -> str:
global attempt_count
if instance2 not in attempt_count:
attempt_count[instance2] = 1
raise CustomException(f"This activity failed") from Exception("More information about the failure")
return "This activity succeeded"
31 changes: 31 additions & 0 deletions test/e2e/Apps/BasicPython/function_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import azure.functions as func
import logging

from hello_cities import bp
from activity_error_handling import bp as error_handling_bp

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function processed a request.')

name = req.params.get('name')
if not name:
try:
req_body = req.get_json()
except ValueError:
pass
else:
name = req_body.get('name')

if name:
return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
else:
return func.HttpResponse(
"This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
status_code=200
)

app.register_blueprint(bp)
app.register_blueprint(error_handling_bp)
41 changes: 41 additions & 0 deletions test/e2e/Apps/BasicPython/hello_cities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from datetime import datetime
import logging
import azure.functions as func
import azure.durable_functions as df

bp = df.Blueprint()

@bp.route(route="HelloCities_HttpStart")
@bp.durable_client_input(client_name="client")
async def http_start(req: func.HttpRequest, client):
instance_id = await client.start_new('hello_cities')

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.route(route="HelloCities_HttpStart_Scheduled")
@bp.durable_client_input(client_name="client")
async def http_start_scheduled(req: func.HttpRequest, client):
instance_id = await client.start_new('hello_cities', None, req.params.get('ScheduledStartTime'))

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@bp.orchestration_trigger(context_name="context")
def hello_cities(context: df.DurableOrchestrationContext):
scheduled_start_time = context.get_input() or context.current_utc_datetime
if isinstance(scheduled_start_time, str):
scheduled_start_time = datetime.fromisoformat(scheduled_start_time)

if scheduled_start_time > context.current_utc_datetime:
yield context.create_timer(scheduled_start_time)

result1 = yield context.call_activity('say_hello', "Tokyo")
result2 = yield context.call_activity('say_hello', "Seattle")
result3 = yield context.call_activity('say_hello', "London")
return [result1, result2, result3]

@bp.activity_trigger(input_name="city")
def say_hello(city: str) -> str:
logging.info(f"Saying hello to {city}.")
return f"Hello {city}!"
24 changes: 24 additions & 0 deletions test/e2e/Apps/BasicPython/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
},
"extensions": {
"durableTask": {
"tracing": {
"DistributedTracingEnabled": true,
"Version": "V2"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle.Preview",
"version": "[4.29.0, 5.0.0)"
}
}
8 changes: 8 additions & 0 deletions test/e2e/Apps/BasicPython/local.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python",
"APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=xxxx;IngestionEndpoint =https://xxxx.applicationinsights.azure.com/;LiveEndpoint=https://xxxx.livediagnostics.monitor.azure.com/"
}
}
6 changes: 6 additions & 0 deletions test/e2e/Apps/BasicPython/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
azure-functions-durable
Loading
Loading