Skip to content

Commit 393b706

Browse files
committed
PoC commit for E2E tests
- Includes only a subset of tests implemented in python - Has a lot of duplicated code from the C# tests
1 parent 6c7bc55 commit 393b706

33 files changed

+2545
-1
lines changed

.github/workflows/validate.yml

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
uses: actions/setup-python@v2
5050
with:
5151
python-version: 3.9
52+
5253
- name: Install dependencies
5354
run: |
5455
python -m pip install --upgrade pip
@@ -58,3 +59,137 @@ jobs:
5859
- name: Run tests
5960
run: |
6061
python -m pytest
62+
63+
e2e-azurestorage-linux:
64+
runs-on: ubuntu-latest
65+
env:
66+
E2E_TEST_DURABLE_BACKEND: 'AzureStorage'
67+
steps:
68+
- uses: actions/checkout@v4
69+
70+
- name: Set up Python
71+
uses: actions/setup-python@v2
72+
with:
73+
python-version: 3.11
74+
75+
- name: Setup .NET Core
76+
uses: actions/setup-dotnet@v3
77+
with:
78+
dotnet-version: 8.0.x
79+
80+
- name: Set up Node.js (needed for Azurite)
81+
uses: actions/setup-node@v3
82+
with:
83+
node-version: '18.x' # Azurite requires at least Node 18
84+
85+
- name: Setup E2E tests
86+
shell: pwsh
87+
run: |
88+
.\test\e2e\Tests\build-e2e-test.ps1
89+
90+
- name: Build
91+
working-directory: test/e2e/Tests
92+
run: dotnet build
93+
94+
- name: Run E2E tests
95+
working-directory: test/e2e/Tests
96+
run: dotnet test --filter AzureStorage!=Skip
97+
98+
e2e-azurestorage-windows:
99+
runs-on: windows-latest
100+
env:
101+
E2E_TEST_DURABLE_BACKEND: 'AzureStorage'
102+
steps:
103+
- uses: actions/checkout@v4
104+
105+
- name: Set up Python
106+
uses: actions/setup-python@v2
107+
with:
108+
python-version: 3.11
109+
110+
- name: Setup .NET Core
111+
uses: actions/setup-dotnet@v3
112+
with:
113+
dotnet-version: 8.0.x
114+
115+
- name: Set up Node.js (needed for Azurite)
116+
uses: actions/setup-node@v3
117+
with:
118+
node-version: '18.x' # Azurite requires at least Node 18
119+
120+
- name: Setup E2E tests
121+
shell: pwsh
122+
run: |
123+
.\test\e2e\Tests\build-e2e-test.ps1
124+
125+
- name: Build
126+
working-directory: test/e2e/Tests
127+
run: dotnet build
128+
129+
- name: Run E2E tests
130+
working-directory: test/e2e/Tests
131+
run: dotnet test --filter AzureStorage!=Skip
132+
133+
e2e-mssql:
134+
runs-on: ubuntu-latest
135+
env:
136+
E2E_TEST_DURABLE_BACKEND: "MSSQL"
137+
steps:
138+
- uses: actions/checkout@v4
139+
140+
- name: Set up Python
141+
uses: actions/setup-python@v2
142+
with:
143+
python-version: 3.11
144+
145+
- name: Setup .NET Core
146+
uses: actions/setup-dotnet@v3
147+
with:
148+
dotnet-version: 8.0.x
149+
150+
- name: Initialize Environment Variables
151+
run: |
152+
echo "MSSQL_SA_PASSWORD=TEST12_$(echo $RANDOM)!" >> $GITHUB_ENV
153+
154+
- name: Setup E2E tests
155+
shell: pwsh
156+
run: |
157+
.\test\e2e\Tests\build-e2e-test.ps1 -StartMSSqlContainer
158+
159+
- name: Build
160+
working-directory: test/e2e/Tests
161+
run: dotnet build
162+
163+
- name: Run E2E tests
164+
working-directory: test/e2e/Tests
165+
run: dotnet test --filter MSSQL!=Skip
166+
167+
e2e-dts:
168+
runs-on: ubuntu-latest
169+
env:
170+
E2E_TEST_DURABLE_BACKEND: "azureManaged"
171+
steps:
172+
- uses: actions/checkout@v4
173+
174+
- name: Set up Python
175+
uses: actions/setup-python@v2
176+
with:
177+
python-version: 3.11
178+
179+
- name: Setup .NET Core
180+
uses: actions/setup-dotnet@v3
181+
with:
182+
dotnet-version: 8.0.x
183+
184+
- name: Setup E2E tests
185+
shell: pwsh
186+
run: |
187+
.\test\e2e\Tests\build-e2e-test.ps1 -StartDTSContainer
188+
189+
- name: Build
190+
working-directory: test/e2e/Tests
191+
run: dotnet build
192+
193+
- name: Run E2E tests
194+
working-directory: test/e2e/Tests
195+
run: dotnet test --logger "console;verbosity=detailed" --filter DTS!=Skip

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,6 @@ appsettings.*.json
135135

136136
# azurite emulator
137137
__azurite_db_*.json
138+
139+
# E2E test folders
140+
/test/e2e/tests/node_modules/*

samples-v2/function_chaining/function_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ def my_orchestrator(context: df.DurableOrchestrationContext):
2222

2323
@myApp.activity_trigger(input_name="city")
2424
def say_hello(city: str) -> str:
25-
return f"Hello {city}!"
25+
return f"Hello {city}!"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from datetime import datetime
2+
import logging
3+
import azure.functions as func
4+
import azure.durable_functions as df
5+
6+
bp = df.Blueprint()
7+
8+
attempt_count = {}
9+
10+
class CustomException(Exception):
11+
pass
12+
13+
@bp.route(route="RethrowActivityException_HttpStart")
14+
@bp.durable_client_input(client_name="client")
15+
async def rethrow_activity_exception_http(req: func.HttpRequest, client):
16+
instance_id = await client.start_new('rethrow_activity_exception')
17+
18+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
19+
return client.create_check_status_response(req, instance_id)
20+
21+
@bp.route(route="CatchActivityException_HttpStart")
22+
@bp.durable_client_input(client_name="client")
23+
async def catch_activity_exception_http(req: func.HttpRequest, client):
24+
instance_id = await client.start_new('catch_activity_exception')
25+
26+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
27+
return client.create_check_status_response(req, instance_id)
28+
29+
@bp.route(route="CatchActivityExceptionFailureDetails_HttpStart")
30+
@bp.durable_client_input(client_name="client")
31+
async def catch_activity_exception_fd_http(req: func.HttpRequest, client):
32+
instance_id = await client.start_new('catch_activity_exception_failure_details')
33+
34+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
35+
return client.create_check_status_response(req, instance_id)
36+
37+
@bp.route(route="RetryActivityException_HttpStart")
38+
@bp.durable_client_input(client_name="client")
39+
async def retry_activity_exception_http(req: func.HttpRequest, client):
40+
instance_id = await client.start_new('retry_activity_function')
41+
42+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
43+
return client.create_check_status_response(req, instance_id)
44+
45+
@bp.route(route="CustomRetryActivityException_HttpStart")
46+
@bp.durable_client_input(client_name="client")
47+
async def custom_retry_activity_exception_http(req: func.HttpRequest, client):
48+
instance_id = await client.start_new('custom_retry_activity_function')
49+
50+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
51+
return client.create_check_status_response(req, instance_id)
52+
53+
@bp.orchestration_trigger(context_name="context")
54+
def rethrow_activity_exception(context: df.DurableOrchestrationContext):
55+
yield context.call_activity('raise_exception', context.instance_id)
56+
57+
@bp.orchestration_trigger(context_name="context")
58+
def catch_activity_exception(context: df.DurableOrchestrationContext):
59+
try:
60+
yield context.call_activity('raise_exception', context.instance_id)
61+
except Exception as e:
62+
logging.error(f"Caught exception: {e}")
63+
return f"Caught exception: {e}"
64+
65+
@bp.orchestration_trigger(context_name="context")
66+
def catch_activity_exception_failure_details(context: df.DurableOrchestrationContext):
67+
try:
68+
yield context.call_activity('raise_exception', context.instance_id)
69+
except Exception as e:
70+
logging.error(f"Caught exception: {e}")
71+
return f"Caught exception: {e}"
72+
73+
@bp.orchestration_trigger(context_name="context")
74+
def retry_activity_function(context: df.DurableOrchestrationContext):
75+
yield context.call_activity_with_retry('raise_exception', retry_options=df.RetryOptions(
76+
first_retry_interval_in_milliseconds=5000,
77+
max_number_of_attempts=3
78+
), input_=context.instance_id)
79+
return "Success"
80+
81+
@bp.orchestration_trigger(context_name="context")
82+
def custom_retry_activity_function(context: df.DurableOrchestrationContext):
83+
yield context.call_activity_with_retry('raise_complex_exception', retry_options=df.RetryOptions(
84+
first_retry_interval_in_milliseconds=5000,
85+
max_number_of_attempts=3
86+
), input_=context.instance_id)
87+
return "Success"
88+
89+
@bp.activity_trigger(input_name="instance")
90+
def raise_exception(instance: str) -> str:
91+
global attempt_count
92+
if instance not in attempt_count:
93+
attempt_count[instance] = 1
94+
raise CustomException(f"This activity failed")
95+
return "This activity succeeded"
96+
97+
@bp.activity_trigger(input_name="instance2")
98+
def raise_complex_exception(instance2: str) -> str:
99+
global attempt_count
100+
if instance2 not in attempt_count:
101+
attempt_count[instance2] = 1
102+
raise CustomException(f"This activity failed") from Exception("More information about the failure")
103+
return "This activity succeeded"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import azure.functions as func
2+
import logging
3+
4+
from hello_cities import bp
5+
from activity_error_handling import bp as error_handling_bp
6+
7+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
8+
9+
@app.route(route="http_trigger")
10+
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
11+
logging.info('Python HTTP trigger function processed a request.')
12+
13+
name = req.params.get('name')
14+
if not name:
15+
try:
16+
req_body = req.get_json()
17+
except ValueError:
18+
pass
19+
else:
20+
name = req_body.get('name')
21+
22+
if name:
23+
return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
24+
else:
25+
return func.HttpResponse(
26+
"This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
27+
status_code=200
28+
)
29+
30+
app.register_blueprint(bp)
31+
app.register_blueprint(error_handling_bp)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from datetime import datetime
2+
import logging
3+
import azure.functions as func
4+
import azure.durable_functions as df
5+
6+
bp = df.Blueprint()
7+
8+
@bp.route(route="HelloCities_HttpStart")
9+
@bp.durable_client_input(client_name="client")
10+
async def http_start(req: func.HttpRequest, client):
11+
instance_id = await client.start_new('hello_cities')
12+
13+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
14+
return client.create_check_status_response(req, instance_id)
15+
16+
@bp.route(route="HelloCities_HttpStart_Scheduled")
17+
@bp.durable_client_input(client_name="client")
18+
async def http_start_scheduled(req: func.HttpRequest, client):
19+
instance_id = await client.start_new('hello_cities', None, req.params.get('ScheduledStartTime'))
20+
21+
logging.info(f"Started orchestration with ID = '{instance_id}'.")
22+
return client.create_check_status_response(req, instance_id)
23+
24+
@bp.orchestration_trigger(context_name="context")
25+
def hello_cities(context: df.DurableOrchestrationContext):
26+
scheduled_start_time = context.get_input() or context.current_utc_datetime
27+
if isinstance(scheduled_start_time, str):
28+
scheduled_start_time = datetime.fromisoformat(scheduled_start_time)
29+
30+
if scheduled_start_time > context.current_utc_datetime:
31+
yield context.create_timer(scheduled_start_time)
32+
33+
result1 = yield context.call_activity('say_hello', "Tokyo")
34+
result2 = yield context.call_activity('say_hello', "Seattle")
35+
result3 = yield context.call_activity('say_hello', "London")
36+
return [result1, result2, result3]
37+
38+
@bp.activity_trigger(input_name="city")
39+
def say_hello(city: str) -> str:
40+
logging.info(f"Saying hello to {city}.")
41+
return f"Hello {city}!"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"version": "2.0",
3+
"logging": {
4+
"applicationInsights": {
5+
"samplingSettings": {
6+
"isEnabled": true,
7+
"excludedTypes": "Request"
8+
},
9+
"enableLiveMetricsFilters": true
10+
}
11+
},
12+
"extensions": {
13+
"durableTask": {
14+
"tracing": {
15+
"DistributedTracingEnabled": true,
16+
"Version": "V2"
17+
}
18+
}
19+
},
20+
"extensionBundle": {
21+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
22+
"version": "[4.*, 5.0.0)"
23+
}
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"IsEncrypted": false,
3+
"Values": {
4+
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
5+
"FUNCTIONS_WORKER_RUNTIME": "python",
6+
"APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=xxxx;IngestionEndpoint =https://xxxx.applicationinsights.azure.com/;LiveEndpoint=https://xxxx.livediagnostics.monitor.azure.com/"
7+
}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# DO NOT include azure-functions-worker in this file
2+
# The Python Worker is managed by Azure Functions platform
3+
# Manually managing azure-functions-worker may cause unexpected issues
4+
5+
azure-functions
6+
azure-functions-durable

0 commit comments

Comments
 (0)