Skip to content

Commit 2d71726

Browse files
authored
Adding E2E tests for EventGrid output binding (#724)
* Adding E2E tests for EventGrid output binding * Skipping the test due to 401 issues from the host.
1 parent ab828a2 commit 2d71726

File tree

16 files changed

+241
-48
lines changed

16 files changed

+241
-48
lines changed

.ci/linux_devops_e2e_tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ export AzureWebJobsStorage=$LINUXSTORAGECONNECTIONSTRING
55
export AzureWebJobsCosmosDBConnectionString=$LINUXCOSMOSDBCONNECTIONSTRING
66
export AzureWebJobsEventHubConnectionString=$LINUXEVENTHUBCONNECTIONSTRING
77
export AzureWebJobsServiceBusConnectionString=$LINUXSERVICEBUSCONNECTIONSTRING
8+
export AzureWebJobsEventGridTopicUri=$LINUXEVENTGRIDTOPICURI
9+
export AzureWebJobsEventGridConnectionKey=$LINUXEVENTGRIDTOPICCONNECTIONKEY
810

9-
pytest --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend
11+
pytest --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,6 @@ py3env/
119119
# PyCharm
120120
.idea/
121121
.idea_modules/
122+
123+
# Profiling info
124+
prof/

azure-pipelines.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,24 @@ jobs:
3131
linuxCosmosDB: $(LinuxCosmosDBConnectionString36)
3232
linuxEventHub: $(LinuxEventHubConnectionString36)
3333
linuxServiceBus: $(LinuxServiceBusConnectionString36)
34+
linuxEventGridTopicUri: $(LinuxEventGridTopicUriString36)
35+
linuxEventGridConnectionKey: $(LinuxEventGridConnectionKeyString36)
3436
Python37:
3537
pythonVersion: '3.7'
3638
linuxStorage: $(LinuxStorageConnectionString37)
3739
linuxCosmosDB: $(LinuxCosmosDBConnectionString37)
3840
linuxEventHub: $(LinuxEventHubConnectionString37)
3941
linuxServiceBus: $(LinuxServiceBusConnectionString37)
42+
linuxEventGridTopicUri: $(LinuxEventGridTopicUriString37)
43+
linuxEventGridConnectionKey: $(LinuxEventGridConnectionKeyString37)
4044
Python38:
4145
pythonVersion: '3.8'
4246
linuxStorage: $(LinuxStorageConnectionString38)
4347
linuxCosmosDB: $(LinuxCosmosDBConnectionString38)
4448
linuxEventHub: $(LinuxEventHubConnectionString38)
4549
linuxServiceBus: $(LinuxServiceBusConnectionString38)
50+
linuxEventGridTopicUri: $(LinuxEventGridTopicUriString38)
51+
linuxEventGridConnectionKey: $(LinuxEventGridConnectionKeyString38)
4652
steps:
4753
- task: UsePythonVersion@0
4854
inputs:
@@ -70,6 +76,8 @@ jobs:
7076
LINUXCOSMOSDBCONNECTIONSTRING: $(linuxCosmosDB)
7177
LINUXEVENTHUBCONNECTIONSTRING: $(linuxEventHub)
7278
LINUXSERVICEBUSCONNECTIONSTRING: $(linuxServiceBus)
79+
LINUXEVENTGRIDTOPICURI: $(linuxEventGridTopicUri)
80+
LINUXEVENTGRIDTOPICCONNECTIONKEY: $(linuxEventGridConnectionKey)
7381
displayName: 'E2E Tests'
7482
- task: PublishCodeCoverageResults@1
7583
inputs:

azure_functions_worker/testutils.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,14 @@
4444
E2E_TESTS_ROOT = TESTS_ROOT / E2E_TESTS_FOLDER
4545
UNIT_TESTS_FOLDER = pathlib.Path('unittests')
4646
UNIT_TESTS_ROOT = TESTS_ROOT / UNIT_TESTS_FOLDER
47-
DEFAULT_WEBHOST_DLL_PATH = PROJECT_ROOT / 'build' / 'webhost' / \
48-
'Microsoft.Azure.WebJobs.Script.WebHost.dll'
47+
WEBHOST_DLL = "Microsoft.Azure.WebJobs.Script.WebHost.dll"
48+
DEFAULT_WEBHOST_DLL_PATH = PROJECT_ROOT / 'build' / 'webhost' / WEBHOST_DLL
4949
EXTENSIONS_PATH = PROJECT_ROOT / 'build' / 'extensions' / 'bin'
5050
FUNCS_PATH = TESTS_ROOT / UNIT_TESTS_FOLDER / 'http_functions'
5151
WORKER_PATH = PROJECT_ROOT / 'python' / 'test'
5252
WORKER_CONFIG = PROJECT_ROOT / '.testconfig'
5353
ON_WINDOWS = platform.system() == 'Windows'
54+
LOCALHOST = "127.0.0.1"
5455

5556
HOST_JSON_TEMPLATE = """\
5657
{
@@ -317,7 +318,7 @@ def __init__(self, loop, scripts_dir):
317318
self._server = grpc.server(self._threadpool)
318319
self._servicer = _MockWebHostServicer(self)
319320
protos.add_FunctionRpcServicer_to_server(self._servicer, self._server)
320-
self._port = self._server.add_insecure_port('127.0.0.1:0')
321+
self._port = self._server.add_insecure_port(f'{LOCALHOST}:0')
321322

322323
self._worker_id = self.make_id()
323324
self._request_id = self.make_id()
@@ -459,7 +460,7 @@ async def __aenter__(self):
459460
await self._host.start()
460461

461462
self._worker = await dispatcher. \
462-
Dispatcher.connect('127.0.0.1', self._host._port,
463+
Dispatcher.connect(LOCALHOST, self._host._port,
463464
self._host.worker_id,
464465
self._host.request_id, connect_timeout=5.0)
465466

@@ -469,6 +470,7 @@ async def __aenter__(self):
469470
wait([self._host._connected_fut, self._worker_task],
470471
return_when=asyncio.FIRST_COMPLETED)
471472

473+
# noinspection PyBroadException
472474
try:
473475
if self._worker_task in done:
474476
self._worker_task.result()
@@ -502,7 +504,7 @@ async def __aexit__(self, *exc):
502504
def start_mockhost(*, script_root=FUNCS_PATH):
503505
tests_dir = TESTS_ROOT
504506
scripts_dir = tests_dir / script_root
505-
if not scripts_dir.exists() or not scripts_dir.is_dir():
507+
if not (scripts_dir.exists() and scripts_dir.is_dir()):
506508
raise RuntimeError(
507509
f'invalid script_root argument: '
508510
f'{scripts_dir} directory does not exist')
@@ -536,7 +538,7 @@ def close(self):
536538

537539
def _find_open_port():
538540
with socket.socket() as s:
539-
s.bind(('127.0.0.1', 0))
541+
s.bind((LOCALHOST, 0))
540542
s.listen(1)
541543
return s.getsockname()[1]
542544

@@ -572,10 +574,10 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None):
572574
if not dll:
573575
dll = DEFAULT_WEBHOST_DLL_PATH
574576

575-
secrets = SECRETS_TEMPLATE
576-
577577
os.makedirs(dll.parent / 'Secrets', exist_ok=True)
578578
with open(dll.parent / 'Secrets' / 'host.json', 'w') as f:
579+
secrets = SECRETS_TEMPLATE
580+
579581
f.write(secrets)
580582

581583
if dll and pathlib.Path(dll).exists():
@@ -605,11 +607,7 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None):
605607
]))
606608

607609
worker_path = os.environ.get('PYAZURE_WORKER_DIR')
608-
if not worker_path:
609-
worker_path = WORKER_PATH
610-
else:
611-
worker_path = pathlib.Path(worker_path)
612-
610+
worker_path = WORKER_PATH if not worker_path else pathlib.Path(worker_path)
613611
if not worker_path.exists():
614612
raise RuntimeError(f'Worker path {worker_path} does not exist')
615613

@@ -640,6 +638,15 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None):
640638
if servicebus:
641639
extra_env['AzureWebJobsServiceBusConnectionString'] = servicebus
642640

641+
eventgrid_topic_uri = testconfig['azure'].get('eventgrid_topic_uri')
642+
if eventgrid_topic_uri:
643+
extra_env['AzureWebJobsEventGridTopicUri'] = eventgrid_topic_uri
644+
645+
eventgrid_topic_key = testconfig['azure'].get('eventgrid_topic_key')
646+
if eventgrid_topic_key:
647+
extra_env['AzureWebJobsEventGridConnectionKey'] = \
648+
eventgrid_topic_key
649+
643650
if port is not None:
644651
extra_env['ASPNETCORE_URLS'] = f'http://*:{port}'
645652

@@ -655,11 +662,7 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None):
655662

656663

657664
def start_webhost(*, script_dir=None, stdout=None):
658-
if script_dir:
659-
script_root = TESTS_ROOT / script_dir
660-
else:
661-
script_root = FUNCS_PATH
662-
665+
script_root = TESTS_ROOT / script_dir if script_dir else FUNCS_PATH
663666
if stdout is None:
664667
if is_envvar_true(PYAZURE_WEBHOST_DEBUG):
665668
stdout = sys.stdout
@@ -670,8 +673,8 @@ def start_webhost(*, script_dir=None, stdout=None):
670673
proc = popen_webhost(stdout=stdout, stderr=subprocess.STDOUT,
671674
script_root=script_root, port=port)
672675

673-
addr = f'http://127.0.0.1:{port}'
674-
for n in range(10):
676+
addr = f'http://{LOCALHOST}:{port}'
677+
for _ in range(10):
675678
try:
676679
r = requests.get(f'{addr}/api/ping',
677680
params={'code': 'testFunctionKey'})
@@ -682,11 +685,11 @@ def start_webhost(*, script_dir=None, stdout=None):
682685
except requests.exceptions.ConnectionError:
683686
pass
684687

685-
time.sleep(0.5)
688+
time.sleep(1)
686689
else:
687690
proc.terminate()
688691
try:
689-
proc.wait(10)
692+
proc.wait(20)
690693
except subprocess.TimeoutExpired:
691694
proc.kill()
692695
raise RuntimeError('could not start the webworker')
@@ -697,7 +700,7 @@ def start_webhost(*, script_dir=None, stdout=None):
697700
def create_dummy_dispatcher():
698701
dummy_event_loop = asyncio.new_event_loop()
699702
disp = dispatcher.Dispatcher(
700-
dummy_event_loop, '127.0.0.1', 0,
703+
dummy_event_loop, LOCALHOST, 0,
701704
'test_worker_id', 'test_request_id',
702705
1.0, 1000)
703706
dummy_event_loop.close()

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,8 @@ def _install_webhost(self):
195195
print('Downloading Azure Functions Web Host...')
196196
urllib.request.urlretrieve(self.webhost_url, zipf.name)
197197
except Exception as e:
198-
print(
199-
f"could not download Azure Functions Web Host binaries "
200-
f"from {self.webhost_url}: {e!r}", file=sys.stderr)
198+
print(f"could not download Azure Functions Web Host binaries "
199+
f"from {self.webhost_url}: {e!r}", file=sys.stderr)
201200
sys.exit(1)
202201

203202
if not self.webhost_dir.exists():
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from datetime import datetime
5+
6+
import azure.functions as func
7+
8+
9+
def main(req: func.HttpRequest,
10+
outputEvent: func.Out[func.EventGridOutputEvent]) -> func.HttpResponse:
11+
test_uuid = req.params.get('test_uuid')
12+
data_to_event_grid = func.EventGridOutputEvent(id="test-id",
13+
data={
14+
"test_uuid": test_uuid
15+
},
16+
subject="test-subject",
17+
event_type="test-event-1",
18+
event_time=datetime.utcnow(),
19+
data_version="1.0")
20+
21+
outputEvent.set(data_to_event_grid)
22+
r_value = "Sent event with subject: {}, id: {}, data: {}, event_type: {} " \
23+
"to EventGrid!".format(data_to_event_grid.subject,
24+
data_to_event_grid.id,
25+
data_to_event_grid.get_json(),
26+
data_to_event_grid.event_type)
27+
return func.HttpResponse(r_value)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"scriptFile": "__init__.py",
3+
4+
"bindings": [
5+
{
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"name": "req"
9+
},
10+
{
11+
"type": "eventGrid",
12+
"name": "outputEvent",
13+
"topicEndpointUri": "AzureWebJobsEventGridTopicUri",
14+
"topicKeySetting": "AzureWebJobsEventGridConnectionKey",
15+
"direction": "out"
16+
},
17+
{
18+
"type": "http",
19+
"direction": "out",
20+
"name": "$return"
21+
}
22+
]
23+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import azure.functions as func
5+
6+
7+
def main(msg: func.QueueMessage) -> bytes:
8+
return msg.get_body()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"scriptFile": "__init__.py",
3+
"bindings": [
4+
{
5+
"name": "msg",
6+
"type": "queueTrigger",
7+
"direction": "in",
8+
"queueName": "test-event-grid-storage-queue",
9+
"connection": "AzureWebJobsStorage"
10+
},
11+
{
12+
"type": "blob",
13+
"direction": "out",
14+
"name": "$return",
15+
"connection": "AzureWebJobsStorage",
16+
"path": "python-worker-tests/test-eventgrid-output-binding.txt"
17+
}
18+
]
19+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import azure.functions as func
5+
6+
7+
def main(req: func.HttpRequest, file: func.InputStream) -> str:
8+
return file.read().decode('utf-8')

0 commit comments

Comments
 (0)