From 9f915481b773d5951444ba34f153e8e0a0e7a078 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 7 May 2025 12:11:17 -0500 Subject: [PATCH 01/20] Add servicebus sdk worker tests --- pyproject.toml | 3 +- .../servicebus_sdk_functions/function_app.py | 47 +++++++++++++++++++ .../test_servicebus_sdk_functions.py | 41 ++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/emulator_tests/servicebus_sdk_functions/function_app.py create mode 100644 tests/emulator_tests/test_servicebus_sdk_functions.py diff --git a/pyproject.toml b/pyproject.toml index 7f9e4b0e7..183f7de14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,8 @@ test-http-v2 = [ ] test-deferred-bindings = [ "azurefunctions-extensions-bindings-blob==1.0.0b3", - "azurefunctions-extensions-bindings-eventhub==1.0.0b1" + "azurefunctions-extensions-bindings-eventhub==1.0.0b1", + "azurefunctions-extensions-bindings-servicebus==1.0.0b1" ] [build-system] diff --git a/tests/emulator_tests/servicebus_sdk_functions/function_app.py b/tests/emulator_tests/servicebus_sdk_functions/function_app.py new file mode 100644 index 000000000..524dcaea9 --- /dev/null +++ b/tests/emulator_tests/servicebus_sdk_functions/function_app.py @@ -0,0 +1,47 @@ +import json + +import azure.functions as func +import azurefunctions.extensions.bindings.servicebus as sb + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="put_message") +@app.service_bus_queue_output( + arg_name="msg", + connection="AzureWebJobsServiceBusConnectionString", + queue_name="testqueue") +def put_message(req: func.HttpRequest, msg: func.Out[str]): + msg.set(req.get_body().decode('utf-8')) + return 'OK' + + +@app.route(route="get_servicebus_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-servicebus-sdk-triggered.txt", + connection="AzureWebJobsStorage") +def get_servicebus_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse( + file.read().decode('utf-8'), mimetype='application/json') + + +@app.service_bus_queue_trigger( + arg_name="msg", + connection="AzureWebJobsServiceBusConnectionString", + queue_name="testqueue") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-servicebus-sdk-triggered.txt", + connection="AzureWebJobsStorage") +def servicebus_trigger(msg: sb.ServiceBusReceivedMessage) -> str: + result = json.dumps({ + 'message': msg, + 'body': msg.body, + 'enqueued_time_utc': msg.enqueued_time_utc, + 'lock_token': msg.lock_token, + 'locked_until': msg.locked_until, + 'message_id': msg.message_id, + 'sequence_number': msg.sequence_number + }) + + return result diff --git a/tests/emulator_tests/test_servicebus_sdk_functions.py b/tests/emulator_tests/test_servicebus_sdk_functions.py new file mode 100644 index 000000000..b3461832c --- /dev/null +++ b/tests/emulator_tests/test_servicebus_sdk_functions.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import time + +from tests.utils import testutils + + +class TestServiceBusFunctions(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_sdk_functions' + + @testutils.retryable_test(3, 5) + def test_servicebus_basic(self): + data = str(round(time.time())) + r = self.webhost.request('POST', 'put_message', + data=data) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'OK') + + max_retries = 10 + + for try_no in range(max_retries): + # wait for trigger to process the queue item + time.sleep(1) + + try: + r = self.webhost.request('GET', 'get_servicebus_triggered') + self.assertEqual(r.status_code, 200) + msg = r.json() + self.assertEqual(msg['body'], data) + for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', + 'locked_until', 'message_id', 'sequence_number'}: + self.assertIn(attr, msg) + except (AssertionError, json.JSONDecodeError): + if try_no == max_retries - 1: + raise + else: + break From 43e4b4fd81d08c478634a621f7deb4a23a755527 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 7 May 2025 13:12:31 -0500 Subject: [PATCH 02/20] Add as part of current sb tests --- .../test_servicebus_functions.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index 2e6bd7310..82ec5ef3f 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -63,3 +63,38 @@ class TestServiceBusFunctionsSteinGeneric(TestServiceBusFunctions): def get_script_dir(cls): return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ 'servicebus_functions_stein' / 'generic' + + +class TestServiceBusSDKFunctions(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_sdk_functions' + + @testutils.retryable_test(3, 5) + def test_servicebus_basic(self): + data = str(round(time.time())) + r = self.webhost.request('POST', 'put_message', + data=data) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'OK') + + max_retries = 10 + + for try_no in range(max_retries): + # wait for trigger to process the queue item + time.sleep(1) + + try: + r = self.webhost.request('GET', 'get_servicebus_triggered') + self.assertEqual(r.status_code, 200) + msg = r.json() + self.assertEqual(msg['body'], data) + for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', + 'locked_until', 'message_id', 'sequence_number'}: + self.assertIn(attr, msg) + except (AssertionError, json.JSONDecodeError): + if try_no == max_retries - 1: + raise + else: + break From b43cd480f7b0042454d2f96c59279b47bfdd5dde Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 7 May 2025 13:36:24 -0500 Subject: [PATCH 03/20] remove old file --- .../test_servicebus_sdk_functions.py | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 tests/emulator_tests/test_servicebus_sdk_functions.py diff --git a/tests/emulator_tests/test_servicebus_sdk_functions.py b/tests/emulator_tests/test_servicebus_sdk_functions.py deleted file mode 100644 index b3461832c..000000000 --- a/tests/emulator_tests/test_servicebus_sdk_functions.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import time - -from tests.utils import testutils - - -class TestServiceBusFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_sdk_functions' - - @testutils.retryable_test(3, 5) - def test_servicebus_basic(self): - data = str(round(time.time())) - r = self.webhost.request('POST', 'put_message', - data=data) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - max_retries = 10 - - for try_no in range(max_retries): - # wait for trigger to process the queue item - time.sleep(1) - - try: - r = self.webhost.request('GET', 'get_servicebus_triggered') - self.assertEqual(r.status_code, 200) - msg = r.json() - self.assertEqual(msg['body'], data) - for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', - 'locked_until', 'message_id', 'sequence_number'}: - self.assertIn(attr, msg) - except (AssertionError, json.JSONDecodeError): - if try_no == max_retries - 1: - raise - else: - break From b1d88eee5387b491267dc11fb711137dfe0fbe96 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 7 May 2025 14:51:38 -0500 Subject: [PATCH 04/20] renaming --- eng/templates/jobs/ci-emulator-tests.yml | 2 ++ pyproject.toml | 3 +- .../servicebus_sdk_functions/function_app.py | 28 +++++++++++-------- .../test_servicebus_functions.py | 8 +++--- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/eng/templates/jobs/ci-emulator-tests.yml b/eng/templates/jobs/ci-emulator-tests.yml index 968585017..c36394e2c 100644 --- a/eng/templates/jobs/ci-emulator-tests.yml +++ b/eng/templates/jobs/ci-emulator-tests.yml @@ -84,6 +84,7 @@ jobs: env: AzureWebJobsStorage: "UseDevelopmentStorage=true" AzureWebJobsEventHubConnectionString: "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" + AZURE_STORAGE_CONNECTION_STRING: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" displayName: "Running $(PYTHON_VERSION) Python Linux Emulator Tests" - bash: | # Stop and remove EventHub Emulator container to free up the port @@ -99,4 +100,5 @@ jobs: env: AzureWebJobsStorage: "UseDevelopmentStorage=true" AzureWebJobsServiceBusConnectionString: "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" + AzureWebJobsServiceBusSDKConnectionString: "Endpoint=sb://127.0.0.1;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" displayName: "Running $(PYTHON_VERSION) Python ServiceBus Linux Emulator Tests" diff --git a/pyproject.toml b/pyproject.toml index 183f7de14..4e58f1e59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,8 @@ dev = [ "numpy", "pre-commit", "invoke", - "cryptography" + "cryptography", + "jsonpickle" ] test-http-v2 = [ "azurefunctions-extensions-http-fastapi==1.0.0b2", diff --git a/tests/emulator_tests/servicebus_sdk_functions/function_app.py b/tests/emulator_tests/servicebus_sdk_functions/function_app.py index 524dcaea9..e67d8ab57 100644 --- a/tests/emulator_tests/servicebus_sdk_functions/function_app.py +++ b/tests/emulator_tests/servicebus_sdk_functions/function_app.py @@ -1,4 +1,5 @@ import json +import jsonpickle import azure.functions as func import azurefunctions.extensions.bindings.servicebus as sb @@ -6,21 +7,21 @@ app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) -@app.route(route="put_message") +@app.route(route="put_message_sdk") @app.service_bus_queue_output( arg_name="msg", - connection="AzureWebJobsServiceBusConnectionString", + connection="AzureWebJobsServiceBusSDKConnectionString", queue_name="testqueue") -def put_message(req: func.HttpRequest, msg: func.Out[str]): +def put_message_sdk(req: func.HttpRequest, msg: func.Out[str]): msg.set(req.get_body().decode('utf-8')) return 'OK' -@app.route(route="get_servicebus_triggered") +@app.route(route="get_servicebus_triggered_sdk") @app.blob_input(arg_name="file", path="python-worker-tests/test-servicebus-sdk-triggered.txt", connection="AzureWebJobsStorage") -def get_servicebus_triggered(req: func.HttpRequest, +def get_servicebus_triggered_sdk(req: func.HttpRequest, file: func.InputStream) -> str: return func.HttpResponse( file.read().decode('utf-8'), mimetype='application/json') @@ -28,18 +29,21 @@ def get_servicebus_triggered(req: func.HttpRequest, @app.service_bus_queue_trigger( arg_name="msg", - connection="AzureWebJobsServiceBusConnectionString", + connection="AzureWebJobsServiceBusSDKConnectionString", queue_name="testqueue") @app.blob_output(arg_name="$return", path="python-worker-tests/test-servicebus-sdk-triggered.txt", connection="AzureWebJobsStorage") -def servicebus_trigger(msg: sb.ServiceBusReceivedMessage) -> str: +def servicebus_trigger_sdk(msg: sb.ServiceBusReceivedMessage) -> str: + msg_json = jsonpickle.encode(msg) + body_json = jsonpickle.encode(msg.body) + enqueued_time_json = jsonpickle.encode(msg.enqueued_time_utc) + lock_token_json = jsonpickle.encode(msg.lock_token) result = json.dumps({ - 'message': msg, - 'body': msg.body, - 'enqueued_time_utc': msg.enqueued_time_utc, - 'lock_token': msg.lock_token, - 'locked_until': msg.locked_until, + 'message': msg_json, + 'body': body_json, + 'enqueued_time_utc': enqueued_time_json, + 'lock_token': lock_token_json, 'message_id': msg.message_id, 'sequence_number': msg.sequence_number }) diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index 82ec5ef3f..af9f90d36 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -72,9 +72,9 @@ def get_script_dir(cls): return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_sdk_functions' @testutils.retryable_test(3, 5) - def test_servicebus_basic(self): + def test_servicebus_basic_sdk(self): data = str(round(time.time())) - r = self.webhost.request('POST', 'put_message', + r = self.webhost.request('POST', 'put_message_sdk', data=data) self.assertEqual(r.status_code, 200) self.assertEqual(r.text, 'OK') @@ -86,12 +86,12 @@ def test_servicebus_basic(self): time.sleep(1) try: - r = self.webhost.request('GET', 'get_servicebus_triggered') + r = self.webhost.request('GET', 'get_servicebus_triggered_sdk') self.assertEqual(r.status_code, 200) msg = r.json() self.assertEqual(msg['body'], data) for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', - 'locked_until', 'message_id', 'sequence_number'}: + 'message_id', 'sequence_number'}: self.assertIn(attr, msg) except (AssertionError, json.JSONDecodeError): if try_no == max_retries - 1: From f92b5a268067318c948a9299a52aa062495aa233 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 7 May 2025 15:23:37 -0500 Subject: [PATCH 05/20] fixes + refactor blob sdk to emulator --- .../blob_functions_sdk}/function_app.py | 58 +++++++++---------- .../test_deferred_bindings_blob_functions.py | 4 +- .../test_servicebus_functions.py | 5 +- .../http_functions_v2/fastapi/function_app.py | 0 .../test_http_v2.py | 4 +- .../function_app.py | 0 .../deferred_bindings_enabled/function_app.py | 0 .../function_app.py | 0 .../test_deferred_bindings.py | 6 +- 9 files changed, 39 insertions(+), 38 deletions(-) rename tests/{extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions => emulator_tests/blob_functions/blob_functions_sdk}/function_app.py (85%) rename tests/{extension_tests/deferred_bindings_tests => emulator_tests}/test_deferred_bindings_blob_functions.py (98%) rename tests/{extension_tests/http_v2_tests => endtoend}/http_functions_v2/fastapi/function_app.py (100%) rename tests/{extension_tests/http_v2_tests => endtoend}/test_http_v2.py (98%) rename tests/{extension_tests/deferred_bindings_tests => unittests}/deferred_bindings_functions/deferred_bindings_disabled/function_app.py (100%) rename tests/{extension_tests/deferred_bindings_tests => unittests}/deferred_bindings_functions/deferred_bindings_enabled/function_app.py (100%) rename tests/{extension_tests/deferred_bindings_tests => unittests}/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py (100%) rename tests/{extension_tests/deferred_bindings_tests => unittests}/test_deferred_bindings.py (97%) diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py b/tests/emulator_tests/blob_functions/blob_functions_sdk/function_app.py similarity index 85% rename from tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py rename to tests/emulator_tests/blob_functions/blob_functions_sdk/function_app.py index 075d8a78a..f92d180d3 100644 --- a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py +++ b/tests/emulator_tests/blob_functions/blob_functions_sdk/function_app.py @@ -11,7 +11,7 @@ @app.function_name(name="put_bc_trigger") @app.blob_output(arg_name="file", path="python-worker-tests/test-blobclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_bc_trigger") def put_bc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -21,10 +21,10 @@ def put_bc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="bc_blob_trigger") @app.blob_trigger(arg_name="client", path="python-worker-tests/test-blobclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_output(arg_name="$return", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def bc_blob_trigger(client: blob.BlobClient) -> str: blob_properties = client.get_blob_properties() file = client.download_blob(encoding='utf-8').readall() @@ -38,7 +38,7 @@ def bc_blob_trigger(client: blob.BlobClient) -> str: @app.function_name(name="get_bc_blob_triggered") @app.blob_input(arg_name="client", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="get_bc_blob_triggered") def get_bc_blob_triggered(req: func.HttpRequest, client: blob.BlobClient) -> str: @@ -48,7 +48,7 @@ def get_bc_blob_triggered(req: func.HttpRequest, @app.function_name(name="put_cc_trigger") @app.blob_output(arg_name="file", path="python-worker-tests/test-containerclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_cc_trigger") def put_cc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -58,10 +58,10 @@ def put_cc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="cc_blob_trigger") @app.blob_trigger(arg_name="client", path="python-worker-tests/test-containerclient-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_output(arg_name="$return", path="python-worker-tests/test-containerclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def cc_blob_trigger(client: blob.ContainerClient) -> str: container_properties = client.get_container_properties() file = client.download_blob("test-containerclient-trigger.txt", @@ -75,7 +75,7 @@ def cc_blob_trigger(client: blob.ContainerClient) -> str: @app.function_name(name="get_cc_blob_triggered") @app.blob_input(arg_name="client", path="python-worker-tests/test-containerclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="get_cc_blob_triggered") def get_cc_blob_triggered(req: func.HttpRequest, client: blob.ContainerClient) -> str: @@ -86,7 +86,7 @@ def get_cc_blob_triggered(req: func.HttpRequest, @app.function_name(name="put_ssd_trigger") @app.blob_output(arg_name="file", path="python-worker-tests/test-ssd-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_ssd_trigger") def put_ssd_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -96,10 +96,10 @@ def put_ssd_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="ssd_blob_trigger") @app.blob_trigger(arg_name="stream", path="python-worker-tests/test-ssd-trigger.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_output(arg_name="$return", path="python-worker-tests/test-ssd-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def ssd_blob_trigger(stream: blob.StorageStreamDownloader) -> str: # testing chunking file = "" @@ -113,7 +113,7 @@ def ssd_blob_trigger(stream: blob.StorageStreamDownloader) -> str: @app.function_name(name="get_ssd_blob_triggered") @app.blob_input(arg_name="stream", path="python-worker-tests/test-ssd-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="get_ssd_blob_triggered") def get_ssd_blob_triggered(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: @@ -124,7 +124,7 @@ def get_ssd_blob_triggered(req: func.HttpRequest, @app.route(route="get_bc_bytes") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_bc_bytes(req: func.HttpRequest, client: blob.BlobClient) -> str: return client.download_blob(encoding='utf-8').readall() @@ -133,7 +133,7 @@ def get_bc_bytes(req: func.HttpRequest, client: blob.BlobClient) -> str: @app.route(route="get_cc_bytes") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_cc_bytes(req: func.HttpRequest, client: blob.ContainerClient) -> str: return client.download_blob("test-blob-extension-bytes.txt", @@ -144,7 +144,7 @@ def get_cc_bytes(req: func.HttpRequest, @app.route(route="get_ssd_bytes") @app.blob_input(arg_name="stream", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_ssd_bytes(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: return stream.readall().decode('utf-8') @@ -154,7 +154,7 @@ def get_ssd_bytes(req: func.HttpRequest, @app.route(route="get_bc_str") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_bc_str(req: func.HttpRequest, client: blob.BlobClient) -> str: return client.download_blob(encoding='utf-8').readall() @@ -163,7 +163,7 @@ def get_bc_str(req: func.HttpRequest, client: blob.BlobClient) -> str: @app.route(route="get_cc_str") @app.blob_input(arg_name="client", path="python-worker-tests", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_cc_str(req: func.HttpRequest, client: blob.ContainerClient) -> str: return client.download_blob("test-blob-extension-str.txt", encoding='utf-8').readall() @@ -173,7 +173,7 @@ def get_cc_str(req: func.HttpRequest, client: blob.ContainerClient) -> str: @app.route(route="get_ssd_str") @app.blob_input(arg_name="stream", path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def get_ssd_str(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: return stream.readall().decode('utf-8') @@ -183,11 +183,11 @@ def get_ssd_str(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_input(arg_name="blob", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def bc_and_inputstream_input(req: func.HttpRequest, client: blob.BlobClient, blob: func.InputStream) -> str: output_msg = "" @@ -202,11 +202,11 @@ def bc_and_inputstream_input(req: func.HttpRequest, client: blob.BlobClient, @app.blob_input(arg_name="blob", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_input(arg_name="client", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def inputstream_and_bc_input(req: func.HttpRequest, blob: func.InputStream, client: blob.BlobClient) -> str: output_msg = "" @@ -221,7 +221,7 @@ def inputstream_and_bc_input(req: func.HttpRequest, blob: func.InputStream, @app.blob_input(arg_name="file", path="python-worker-tests/test-blob-extension-str.txt", data_type="STRING", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") def type_undefined(req: func.HttpRequest, file) -> str: assert not isinstance(file, blob.BlobClient) assert not isinstance(file, blob.ContainerClient) @@ -232,7 +232,7 @@ def type_undefined(req: func.HttpRequest, file) -> str: @app.function_name(name="put_blob_str") @app.blob_output(arg_name="file", path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_blob_str") def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: file.set(req.get_body()) @@ -242,7 +242,7 @@ def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: @app.function_name(name="put_blob_bytes") @app.blob_output(arg_name="file", path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="put_blob_bytes") def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str: file.set(req.get_body()) @@ -252,7 +252,7 @@ def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str: @app.function_name(name="blob_cache") @app.blob_input(arg_name="cachedClient", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="blob_cache") def blob_cache(req: func.HttpRequest, cachedClient: blob.BlobClient) -> str: @@ -262,7 +262,7 @@ def blob_cache(req: func.HttpRequest, @app.function_name(name="blob_cache2") @app.blob_input(arg_name="cachedClient", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="blob_cache2") def blob_cache2(req: func.HttpRequest, cachedClient: blob.BlobClient) -> func.HttpResponse: @@ -272,10 +272,10 @@ def blob_cache2(req: func.HttpRequest, @app.function_name(name="blob_cache3") @app.blob_input(arg_name="cachedClient", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.blob_input(arg_name="cachedClient2", path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") + connection="AZURE_STORAGE_CONNECTION_STRING") @app.route(route="blob_cache3") def blob_cache3(req: func.HttpRequest, cachedClient: blob.BlobClient, diff --git a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py b/tests/emulator_tests/test_deferred_bindings_blob_functions.py similarity index 98% rename from tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py rename to tests/emulator_tests/test_deferred_bindings_blob_functions.py index ed441a077..befa1229a 100644 --- a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py +++ b/tests/emulator_tests/test_deferred_bindings_blob_functions.py @@ -13,8 +13,8 @@ class TestDeferredBindingsBlobFunctions(testutils.WebHostTestCase): @classmethod def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'deferred_bindings_tests' / \ - 'deferred_bindings_blob_functions' + return testutils.E2E_TESTS_FOLDER / 'blob_functions' / \ + 'blob_functions_sdk' @classmethod def get_libraries_to_install(cls): diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index af9f90d36..ae7996c8d 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -1,7 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import json +import sys import time +import unittest from tests.utils import testutils @@ -65,6 +67,8 @@ def get_script_dir(cls): 'servicebus_functions_stein' / 'generic' +@unittest.skipIf(sys.version_info.minor <= 8, "The servicebus extension" + "is only supported for 3.9+.") class TestServiceBusSDKFunctions(testutils.WebHostTestCase): @classmethod @@ -89,7 +93,6 @@ def test_servicebus_basic_sdk(self): r = self.webhost.request('GET', 'get_servicebus_triggered_sdk') self.assertEqual(r.status_code, 200) msg = r.json() - self.assertEqual(msg['body'], data) for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', 'message_id', 'sequence_number'}: self.assertIn(attr, msg) diff --git a/tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions_v2/fastapi/function_app.py similarity index 100% rename from tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py rename to tests/endtoend/http_functions_v2/fastapi/function_app.py diff --git a/tests/extension_tests/http_v2_tests/test_http_v2.py b/tests/endtoend/test_http_v2.py similarity index 98% rename from tests/extension_tests/http_v2_tests/test_http_v2.py rename to tests/endtoend/test_http_v2.py index 514633743..29a44f335 100644 --- a/tests/extension_tests/http_v2_tests/test_http_v2.py +++ b/tests/endtoend/test_http_v2.py @@ -44,9 +44,7 @@ def get_environment_variables(cls): @classmethod def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'http_v2_tests' / \ - 'http_functions_v2' / \ - 'fastapi' + return testutils.E2E_TESTS_FOLDER / 'http_functions_v2' / 'fastapi' @classmethod def get_libraries_to_install(cls): diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py b/tests/unittests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py similarity index 100% rename from tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py rename to tests/unittests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py b/tests/unittests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py similarity index 100% rename from tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py rename to tests/unittests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py b/tests/unittests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py similarity index 100% rename from tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py rename to tests/unittests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py diff --git a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py b/tests/unittests/test_deferred_bindings.py similarity index 97% rename from tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py rename to tests/unittests/test_deferred_bindings.py index b8ced2834..37ec08088 100644 --- a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py +++ b/tests/unittests/test_deferred_bindings.py @@ -18,15 +18,15 @@ ContainerClient, StorageStreamDownloader) -DEFERRED_BINDINGS_ENABLED_DIR = testutils.EXTENSION_TESTS_FOLDER / \ +DEFERRED_BINDINGS_ENABLED_DIR = testutils.UNIT_TESTS_FOLDER / \ 'deferred_bindings_tests' / \ 'deferred_bindings_functions' / \ 'deferred_bindings_enabled' -DEFERRED_BINDINGS_DISABLED_DIR = testutils.EXTENSION_TESTS_FOLDER / \ +DEFERRED_BINDINGS_DISABLED_DIR = testutils.UNIT_TESTS_FOLDER / \ 'deferred_bindings_tests' / \ 'deferred_bindings_functions' / \ 'deferred_bindings_disabled' -DEFERRED_BINDINGS_ENABLED_DUAL_DIR = testutils.EXTENSION_TESTS_FOLDER / \ +DEFERRED_BINDINGS_ENABLED_DUAL_DIR = testutils.UNIT_TESTS_FOLDER / \ 'deferred_bindings_tests' / \ 'deferred_bindings_functions' / \ 'deferred_bindings_enabled_dual' From 3078d4d2f9d1f24d88eb70802e6ae14cec816599 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 8 May 2025 09:53:47 -0500 Subject: [PATCH 06/20] directory fix --- eng/templates/official/jobs/ci-e2e-tests.yml | 2 +- tests/emulator_tests/test_deferred_bindings_blob_functions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml index 1a45e06ce..2dcbabd4d 100644 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-e2e-tests.yml @@ -150,7 +150,7 @@ jobs: displayName: 'Display skipTest variable' condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - bash: | - python -m pytest -q -n auto --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend tests/extension_tests/deferred_bindings_tests tests/extension_tests/http_v2_tests + python -m pytest -q -n auto --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend env: AzureWebJobsStorage: $(STORAGE_CONNECTION) AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) diff --git a/tests/emulator_tests/test_deferred_bindings_blob_functions.py b/tests/emulator_tests/test_deferred_bindings_blob_functions.py index befa1229a..e3211afa8 100644 --- a/tests/emulator_tests/test_deferred_bindings_blob_functions.py +++ b/tests/emulator_tests/test_deferred_bindings_blob_functions.py @@ -13,7 +13,7 @@ class TestDeferredBindingsBlobFunctions(testutils.WebHostTestCase): @classmethod def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blob_functions' / \ + return testutils.EMULATOR_TESTS_FOLDER / 'blob_functions' / \ 'blob_functions_sdk' @classmethod From a6b1d0d6d139feb3ccea5275f76eb1ee5286587d Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 8 May 2025 10:41:24 -0500 Subject: [PATCH 07/20] directory fix --- eng/templates/jobs/ci-unit-tests.yml | 2 ++ tests/unittests/test_deferred_bindings.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index 11acf05cd..3d10952fd 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -53,6 +53,8 @@ jobs: displayName: "Running $(PYTHON_VERSION) Unit Tests" # Skip running tests for SDK and Extensions release branches. Public pipeline doesn't have permissions to download artifact. condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + # A connection string is used for the deferred bindings tests. Here, we reference the emulator value env: PYTHON_VERSION: $(PYTHON_VERSION) + AzureWebJobsStorage: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" \ No newline at end of file diff --git a/tests/unittests/test_deferred_bindings.py b/tests/unittests/test_deferred_bindings.py index 37ec08088..2e15f72f1 100644 --- a/tests/unittests/test_deferred_bindings.py +++ b/tests/unittests/test_deferred_bindings.py @@ -19,15 +19,12 @@ StorageStreamDownloader) DEFERRED_BINDINGS_ENABLED_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ 'deferred_bindings_functions' / \ 'deferred_bindings_enabled' DEFERRED_BINDINGS_DISABLED_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ 'deferred_bindings_functions' / \ 'deferred_bindings_disabled' DEFERRED_BINDINGS_ENABLED_DUAL_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ 'deferred_bindings_functions' / \ 'deferred_bindings_enabled_dual' From fad1242953e9d20b218049fc319868a99b4835c0 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 12 May 2025 11:38:34 -0500 Subject: [PATCH 08/20] rename sb directory --- .../servicebus_functions_sdk}/function_app.py | 2 +- tests/emulator_tests/test_deferred_bindings_blob_functions.py | 2 +- tests/emulator_tests/test_servicebus_functions.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) rename tests/emulator_tests/{servicebus_sdk_functions => servicebus_functions/servicebus_functions_sdk}/function_app.py (96%) diff --git a/tests/emulator_tests/servicebus_sdk_functions/function_app.py b/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py similarity index 96% rename from tests/emulator_tests/servicebus_sdk_functions/function_app.py rename to tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py index e67d8ab57..aec143953 100644 --- a/tests/emulator_tests/servicebus_sdk_functions/function_app.py +++ b/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py @@ -22,7 +22,7 @@ def put_message_sdk(req: func.HttpRequest, msg: func.Out[str]): path="python-worker-tests/test-servicebus-sdk-triggered.txt", connection="AzureWebJobsStorage") def get_servicebus_triggered_sdk(req: func.HttpRequest, - file: func.InputStream) -> str: + file: func.InputStream) -> str: return func.HttpResponse( file.read().decode('utf-8'), mimetype='application/json') diff --git a/tests/emulator_tests/test_deferred_bindings_blob_functions.py b/tests/emulator_tests/test_deferred_bindings_blob_functions.py index a69dc5cfc..e497d13b1 100644 --- a/tests/emulator_tests/test_deferred_bindings_blob_functions.py +++ b/tests/emulator_tests/test_deferred_bindings_blob_functions.py @@ -207,7 +207,7 @@ def test_caching(self): self.assertNotEqual(r.text, r2.text) @unittest.skipIf(sys.version_info.minor >= 13, "For python 3.13+," - "the cache is maintained in the ext and TBD.") + "the cache is maintained in the ext and TBD.") def test_caching_same_resource(self): ''' The cache returns the same type based on param name. diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index ae7996c8d..cbd2962b3 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -73,7 +73,8 @@ class TestServiceBusSDKFunctions(testutils.WebHostTestCase): @classmethod def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_sdk_functions' + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ + 'servicebus_functions_sdk' @testutils.retryable_test(3, 5) def test_servicebus_basic_sdk(self): From d30a219925faeb5a5e5310e2230f5295a4ebd1f7 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 13 May 2025 10:09:16 -0500 Subject: [PATCH 09/20] move emulator connection strings to variable groups --- eng/templates/jobs/ci-emulator-tests.yml | 12 ++++++------ eng/templates/utils/variables.yml | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/eng/templates/jobs/ci-emulator-tests.yml b/eng/templates/jobs/ci-emulator-tests.yml index c36394e2c..85843f51b 100644 --- a/eng/templates/jobs/ci-emulator-tests.yml +++ b/eng/templates/jobs/ci-emulator-tests.yml @@ -82,9 +82,9 @@ jobs: - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 --ignore=tests/emulator_tests/test_servicebus_functions.py tests/emulator_tests env: - AzureWebJobsStorage: "UseDevelopmentStorage=true" - AzureWebJobsEventHubConnectionString: "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" - AZURE_STORAGE_CONNECTION_STRING: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" + AzureWebJobsStorage: $(AzureWebJobsStorage) + AzureWebJobsEventHubConnectionString: $(AzureWebJobsEventHubConnectionString) + AZURE_STORAGE_CONNECTION_STRING: $(AZURE_STORAGE_CONNECTION_STRING) displayName: "Running $(PYTHON_VERSION) Python Linux Emulator Tests" - bash: | # Stop and remove EventHub Emulator container to free up the port @@ -98,7 +98,7 @@ jobs: - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 tests/emulator_tests/test_servicebus_functions.py env: - AzureWebJobsStorage: "UseDevelopmentStorage=true" - AzureWebJobsServiceBusConnectionString: "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" - AzureWebJobsServiceBusSDKConnectionString: "Endpoint=sb://127.0.0.1;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" + AzureWebJobsStorage: $(AzureWebJobsStorage) + AzureWebJobsServiceBusConnectionString: $(AzureWebJobsServiceBusConnectionString) + AzureWebJobsServiceBusSDKConnectionString: $(AzureWebJobsServiceBusSDKConnectionString) displayName: "Running $(PYTHON_VERSION) Python ServiceBus Linux Emulator Tests" diff --git a/eng/templates/utils/variables.yml b/eng/templates/utils/variables.yml index 6361d2d19..c061c431e 100644 --- a/eng/templates/utils/variables.yml +++ b/eng/templates/utils/variables.yml @@ -1,4 +1,5 @@ variables: + - group: python-emulator-resources - name: isSdkRelease value: $[startsWith(variables['Build.SourceBranch'], 'refs/heads/sdk/')] - name: isExtensionsRelease From a3cf630b75e98abec2e308bab18085a5751bf385 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 13 May 2025 10:10:31 -0500 Subject: [PATCH 10/20] same for unit tests --- eng/templates/jobs/ci-unit-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index 3d10952fd..b64cd84fb 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -53,8 +53,7 @@ jobs: displayName: "Running $(PYTHON_VERSION) Unit Tests" # Skip running tests for SDK and Extensions release branches. Public pipeline doesn't have permissions to download artifact. condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - # A connection string is used for the deferred bindings tests. Here, we reference the emulator value env: PYTHON_VERSION: $(PYTHON_VERSION) - AzureWebJobsStorage: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" + AzureWebJobsStorage: $(AZURE_STORAGE_CONNECTION_STRING) \ No newline at end of file From 4ade5eb5993437db3dce5e1bbcfc21406bf18f03 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 13 May 2025 10:58:03 -0500 Subject: [PATCH 11/20] add sb topic tests --- eng/templates/jobs/ci-unit-tests.yml | 2 +- .../servicebus_functions_sdk/function_app.py | 43 ++++++++++++ .../function_app.py | 67 +++++++++++++++++++ .../test_servicebus_functions.py | 63 +++++++++++++++++ .../utils/servicebus/config.json | 44 ++++++++++++ tests/unittests/test_deferred_bindings.py | 2 +- 6 files changed, 219 insertions(+), 2 deletions(-) diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index b64cd84fb..6f1c3598c 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -55,5 +55,5 @@ jobs: condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) env: PYTHON_VERSION: $(PYTHON_VERSION) - AzureWebJobsStorage: $(AZURE_STORAGE_CONNECTION_STRING) + AZURE_STORAGE_CONNECTION_STRING: $(AZURE_STORAGE_CONNECTION_STRING) \ No newline at end of file diff --git a/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py b/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py index aec143953..bf1b5bb0d 100644 --- a/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py +++ b/tests/emulator_tests/servicebus_functions/servicebus_functions_sdk/function_app.py @@ -49,3 +49,46 @@ def servicebus_trigger_sdk(msg: sb.ServiceBusReceivedMessage) -> str: }) return result + + +@app.route(route="put_message_sdk_topic") +@app.service_bus_topic_output(arg_name="msg", + connection="AzureWebJobsServiceBusSDKConnectionString", + topic_name="testtopic") +def put_message_sdk_topic(req: func.HttpRequest, msg: func.Out[str]): + msg.set(req.get_body().decode('utf-8')) + return 'OK' + + +@app.route(route="get_servicebus_triggered_sdk_topic") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-servicebus-sdk-triggered-topic.txt", + connection="AzureWebJobsStorage") +def get_servicebus_triggered_sdk_topic(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse( + file.read().decode('utf-8'), mimetype='application/json') + + +@app.service_bus_topic_trigger(arg_name="msg", + topic_name="testtopic", + connection="AzureWebJobsServiceBusSDKConnectionString", + subscription_name="testsub") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-servicebus-sdk-triggered-topic.txt", + connection="AzureWebJobsStorage") +def servicebus_trigger_sdk_topic(msg: sb.ServiceBusReceivedMessage) -> str: + msg_json = jsonpickle.encode(msg) + body_json = jsonpickle.encode(msg.body) + enqueued_time_json = jsonpickle.encode(msg.enqueued_time_utc) + lock_token_json = jsonpickle.encode(msg.lock_token) + result = json.dumps({ + 'message': msg_json, + 'body': body_json, + 'enqueued_time_utc': enqueued_time_json, + 'lock_token': lock_token_json, + 'message_id': msg.message_id, + 'sequence_number': msg.sequence_number + }) + + return result \ No newline at end of file diff --git a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py b/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py index 9e9d12246..351d050e1 100644 --- a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py +++ b/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py @@ -71,3 +71,70 @@ def servicebus_trigger(msg: func.ServiceBusMessage) -> str: }) return result + + +@app.route(route="put_message_topic") +@app.service_bus_topic_output(arg_name="msg", + connection="AzureWebJobsServiceBusConnectionString", + topic_name="testtopic") +def put_message_topic(req: func.HttpRequest, msg: func.Out[str]): + msg.set(req.get_body().decode('utf-8')) + return 'OK' + + +@app.route(route="get_servicebus_triggered_topic") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-servicebus-triggered-topic.txt", + connection="AzureWebJobsStorage") +def get_servicebus_triggered_topic(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse( + file.read().decode('utf-8'), mimetype='application/json') + + +@app.service_bus_topic_trigger(arg_name="msg", + topic_name="testtopic", + connection="AzureWebJobsServiceBusConnectionString", + subscription_name="testsub") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-servicebus-triggered-topic.txt", + connection="AzureWebJobsStorage") +def servicebus_trigger_topic(msg: func.ServiceBusMessage) -> str: + result = json.dumps({ + 'message_id': msg.message_id, + 'body': msg.get_body().decode('utf-8'), + 'content_type': msg.content_type, + 'delivery_count': msg.delivery_count, + 'expiration_time': (msg.expiration_time.isoformat() if + msg.expiration_time else None), + 'label': msg.label, + 'partition_key': msg.partition_key, + 'reply_to': msg.reply_to, + 'reply_to_session_id': msg.reply_to_session_id, + 'scheduled_enqueue_time': (msg.scheduled_enqueue_time.isoformat() if + msg.scheduled_enqueue_time else None), + 'session_id': msg.session_id, + 'time_to_live': msg.time_to_live, + 'to': msg.to, + 'user_properties': msg.user_properties, + + 'application_properties': msg.application_properties, + 'correlation_id': msg.correlation_id, + 'dead_letter_error_description': msg.dead_letter_error_description, + 'dead_letter_reason': msg.dead_letter_reason, + 'dead_letter_source': msg.dead_letter_source, + 'enqueued_sequence_number': msg.enqueued_sequence_number, + 'enqueued_time_utc': (msg.enqueued_time_utc.isoformat() if + msg.enqueued_time_utc else None), + 'expires_at_utc': (msg.expires_at_utc.isoformat() if + msg.expires_at_utc else None), + 'locked_until': (msg.locked_until.isoformat() if + msg.locked_until else None), + 'lock_token': msg.lock_token, + 'sequence_number': msg.sequence_number, + 'state': msg.state, + 'subject': msg.subject, + 'transaction_partition_key': msg.transaction_partition_key + }) + + return result diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index cbd2962b3..3b1e183fc 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -49,6 +49,42 @@ def test_servicebus_basic(self): raise else: break + + @testutils.retryable_test(3, 5) + def test_servicebus_basic_topic(self): + data = str(round(time.time())) + r = self.webhost.request('POST', 'put_message_topic', + data=data) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'OK') + + max_retries = 10 + + for try_no in range(max_retries): + # wait for trigger to process the queue item + time.sleep(1) + + try: + r = self.webhost.request('GET', 'get_servicebus_triggered_topic') + self.assertEqual(r.status_code, 200) + msg = r.json() + self.assertEqual(msg['body'], data) + for attr in {'message_id', 'body', 'content_type', 'delivery_count', + 'expiration_time', 'label', 'partition_key', 'reply_to', + 'reply_to_session_id', 'scheduled_enqueue_time', + 'session_id', 'time_to_live', 'to', 'user_properties', + 'application_properties', 'correlation_id', + 'dead_letter_error_description', 'dead_letter_reason', + 'dead_letter_source', 'enqueued_sequence_number', + 'enqueued_time_utc', 'expires_at_utc', 'locked_until', + 'lock_token', 'sequence_number', 'state', 'subject', + 'transaction_partition_key'}: + self.assertIn(attr, msg) + except (AssertionError, json.JSONDecodeError): + if try_no == max_retries - 1: + raise + else: + break class TestServiceBusFunctionsStein(TestServiceBusFunctions): @@ -102,3 +138,30 @@ def test_servicebus_basic_sdk(self): raise else: break + + @testutils.retryable_test(3, 5) + def test_servicebus_basic_sdk_topic(self): + data = str(round(time.time())) + r = self.webhost.request('POST', 'put_message_sdk_topic', + data=data) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'OK') + + max_retries = 10 + + for try_no in range(max_retries): + # wait for trigger to process the queue item + time.sleep(1) + + try: + r = self.webhost.request('GET', 'get_servicebus_triggered_sdk_topic') + self.assertEqual(r.status_code, 200) + msg = r.json() + for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', + 'message_id', 'sequence_number'}: + self.assertIn(attr, msg) + except (AssertionError, json.JSONDecodeError): + if try_no == max_retries - 1: + raise + else: + break diff --git a/tests/emulator_tests/utils/servicebus/config.json b/tests/emulator_tests/utils/servicebus/config.json index 20cf83447..797491452 100644 --- a/tests/emulator_tests/utils/servicebus/config.json +++ b/tests/emulator_tests/utils/servicebus/config.json @@ -18,7 +18,51 @@ "RequiresSession": false } } + ], + "Topics": [ + { + "Name": "testtopic", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "testsub", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "app-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "ContentType": "application/text", + "CorrelationId": "id1", + "Label": "subject1", + "MessageId": "msgid1", + "ReplyTo": "someQueue", + "ReplyToSessionId": "sessionId", + "SessionId": "session1", + "To": "xyz" + }}} + ] + } + ] + } ] + + + + } ], "Logging": { diff --git a/tests/unittests/test_deferred_bindings.py b/tests/unittests/test_deferred_bindings.py index 2e15f72f1..a5d830f85 100644 --- a/tests/unittests/test_deferred_bindings.py +++ b/tests/unittests/test_deferred_bindings.py @@ -155,7 +155,7 @@ def test_mbd_deferred_bindings_enabled_decode(self): sample_mbd = MockMBD(version="1.0", source="AzureStorageBlobs", content_type="application/json", - content="{\"Connection\":\"AzureWebJobsStorage\"," + content="{\"Connection\":\"AZURE_STORAGE_CONNECTION_STRING\"," "\"ContainerName\":" "\"python-worker-tests\"," "\"BlobName\":" From c9a239ab08b12ecffb246e274826556afe831b1e Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 28 May 2025 13:23:44 -0500 Subject: [PATCH 12/20] Revert me later --- python/test/worker.config.json | 2 +- .../function_app.py | 10 +- .../test_servicebus_functions.py | 124 +++++++++++----- .../utils/servicebus/config.json | 132 +++++++++++++----- .../utils/servicebus/docker-compose.yml | 4 +- tests/utils/testutils.py | 6 +- 6 files changed, 196 insertions(+), 82 deletions(-) diff --git a/python/test/worker.config.json b/python/test/worker.config.json index f778e45f3..f6f3e1d70 100644 --- a/python/test/worker.config.json +++ b/python/test/worker.config.json @@ -2,7 +2,7 @@ "description":{ "language":"python", "extensions":[".py"], - "defaultExecutablePath":"python", + "defaultExecutablePath":"C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\.venv311\\Scripts\\python.exe", "defaultWorkerPath":"worker.py", "workerIndexing": "true", "arguments": ["-X no_debug_ranges"] diff --git a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py b/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py index 351d050e1..e7929d7b6 100644 --- a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py +++ b/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py @@ -9,7 +9,7 @@ @app.service_bus_queue_output( arg_name="msg", connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue") + queue_name="queue.1") def put_message(req: func.HttpRequest, msg: func.Out[str]): msg.set(req.get_body().decode('utf-8')) return 'OK' @@ -28,7 +28,7 @@ def get_servicebus_triggered(req: func.HttpRequest, @app.service_bus_queue_trigger( arg_name="msg", connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue") + queue_name="queue.1") @app.blob_output(arg_name="$return", path="python-worker-tests/test-servicebus-triggered.txt", connection="AzureWebJobsStorage") @@ -76,7 +76,7 @@ def servicebus_trigger(msg: func.ServiceBusMessage) -> str: @app.route(route="put_message_topic") @app.service_bus_topic_output(arg_name="msg", connection="AzureWebJobsServiceBusConnectionString", - topic_name="testtopic") + topic_name="topic.1") def put_message_topic(req: func.HttpRequest, msg: func.Out[str]): msg.set(req.get_body().decode('utf-8')) return 'OK' @@ -93,9 +93,9 @@ def get_servicebus_triggered_topic(req: func.HttpRequest, @app.service_bus_topic_trigger(arg_name="msg", - topic_name="testtopic", + topic_name="topic.1", connection="AzureWebJobsServiceBusConnectionString", - subscription_name="testsub") + subscription_name="subscription.1") @app.blob_output(arg_name="$return", path="python-worker-tests/test-servicebus-triggered-topic.txt", connection="AzureWebJobsStorage") diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index 3b1e183fc..07323e540 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -6,6 +6,7 @@ import unittest from tests.utils import testutils +from azure.servicebus import ServiceBusClient, ServiceBusMessage class TestServiceBusFunctions(testutils.WebHostTestCase): @@ -49,8 +50,50 @@ def test_servicebus_basic(self): raise else: break + + +class TestServiceBusFunctionsStein(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ + 'servicebus_functions_stein' - @testutils.retryable_test(3, 5) + # def test_servicebus_basic(self): + # data = str(round(time.time())) + # r = self.webhost.request('POST', 'put_message', + # data=data) + # self.assertEqual(r.status_code, 200) + # self.assertEqual(r.text, 'OK') + + # max_retries = 10 + + # for try_no in range(max_retries): + # # wait for trigger to process the queue item + # time.sleep(1) + + # try: + # r = self.webhost.request('GET', 'get_servicebus_triggered') + # self.assertEqual(r.status_code, 200) + # msg = r.json() + # self.assertEqual(msg['body'], data) + # for attr in {'message_id', 'body', 'content_type', 'delivery_count', + # 'expiration_time', 'label', 'partition_key', 'reply_to', + # 'reply_to_session_id', 'scheduled_enqueue_time', + # 'session_id', 'time_to_live', 'to', 'user_properties', + # 'application_properties', 'correlation_id', + # 'dead_letter_error_description', 'dead_letter_reason', + # 'dead_letter_source', 'enqueued_sequence_number', + # 'enqueued_time_utc', 'expires_at_utc', 'locked_until', + # 'lock_token', 'sequence_number', 'state', 'subject', + # 'transaction_partition_key'}: + # self.assertIn(attr, msg) + # except (AssertionError, json.JSONDecodeError): + # if try_no == max_retries - 1: + # raise + # else: + # break + def test_servicebus_basic_topic(self): data = str(round(time.time())) r = self.webhost.request('POST', 'put_message_topic', @@ -58,41 +101,50 @@ def test_servicebus_basic_topic(self): self.assertEqual(r.status_code, 200) self.assertEqual(r.text, 'OK') - max_retries = 10 - - for try_no in range(max_retries): - # wait for trigger to process the queue item - time.sleep(1) - - try: - r = self.webhost.request('GET', 'get_servicebus_triggered_topic') - self.assertEqual(r.status_code, 200) - msg = r.json() - self.assertEqual(msg['body'], data) - for attr in {'message_id', 'body', 'content_type', 'delivery_count', - 'expiration_time', 'label', 'partition_key', 'reply_to', - 'reply_to_session_id', 'scheduled_enqueue_time', - 'session_id', 'time_to_live', 'to', 'user_properties', - 'application_properties', 'correlation_id', - 'dead_letter_error_description', 'dead_letter_reason', - 'dead_letter_source', 'enqueued_sequence_number', - 'enqueued_time_utc', 'expires_at_utc', 'locked_until', - 'lock_token', 'sequence_number', 'state', 'subject', - 'transaction_partition_key'}: - self.assertIn(attr, msg) - except (AssertionError, json.JSONDecodeError): - if try_no == max_retries - 1: - raise - else: - break - - -class TestServiceBusFunctionsStein(TestServiceBusFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ - 'servicebus_functions_stein' + CONNECTION_STR = "Endpoint=sb://127.0.0.1;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" + servicebus_client = ServiceBusClient.from_connection_string(conn_str=CONNECTION_STR, logging_enable=True) + with servicebus_client: + sender = servicebus_client.get_topic_sender(topic_name="topic.1") + with sender: + message = ServiceBusMessage("Single Message") + sender.send_messages(message) + with servicebus_client: + receiver = servicebus_client.get_subscription_receiver(topic_name="topic.1", + subscription_name="subscription.1") + with receiver: + received_msgs = receiver.receive_messages(max_message_count=10, max_wait_time=5) + for msg in received_msgs: + print(str(msg)) + receiver.complete_message(msg) + self.assertEqual(msg.body, data) + + # max_retries = 10 + # + # for try_no in range(max_retries): + # # wait for trigger to process the queue item + # time.sleep(1) + # + # try: + # r = self.webhost.request('GET', 'get_servicebus_triggered_topic') + # self.assertEqual(r.status_code, 200) + # msg = r.json() + # self.assertEqual(msg['body'], data) + # for attr in {'message_id', 'body', 'content_type', 'delivery_count', + # 'expiration_time', 'label', 'partition_key', 'reply_to', + # 'reply_to_session_id', 'scheduled_enqueue_time', + # 'session_id', 'time_to_live', 'to', 'user_properties', + # 'application_properties', 'correlation_id', + # 'dead_letter_error_description', 'dead_letter_reason', + # 'dead_letter_source', 'enqueued_sequence_number', + # 'enqueued_time_utc', 'expires_at_utc', 'locked_until', + # 'lock_token', 'sequence_number', 'state', 'subject', + # 'transaction_partition_key'}: + # self.assertIn(attr, msg) + # except (AssertionError, json.JSONDecodeError): + # if try_no == max_retries - 1: + # raise + # else: + # break class TestServiceBusFunctionsSteinGeneric(TestServiceBusFunctions): diff --git a/tests/emulator_tests/utils/servicebus/config.json b/tests/emulator_tests/utils/servicebus/config.json index 797491452..c01cc5caa 100644 --- a/tests/emulator_tests/utils/servicebus/config.json +++ b/tests/emulator_tests/utils/servicebus/config.json @@ -1,27 +1,28 @@ { - "UserConfig": { - "Namespaces": [ - { - "Name": "sbemulatorns", - "Queues": [ - { - "Name": "testqueue", - "Properties": { - "DeadLetteringOnMessageExpiration": false, - "DefaultMessageTimeToLive": "PT1H", - "DuplicateDetectionHistoryTimeWindow": "PT20S", - "ForwardDeadLetteredMessagesTo": "", - "ForwardTo": "", - "LockDuration": "PT1M", - "MaxDeliveryCount": 10, - "RequiresDuplicateDetection": false, - "RequiresSession": false - } - } - ], - "Topics": [ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ { - "Name": "testtopic", + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + + "Topics": [ + { + "Name": "topic.1", "Properties": { "DefaultMessageTimeToLive": "PT1H", "DuplicateDetectionHistoryTimeWindow": "PT20S", @@ -29,7 +30,7 @@ }, "Subscriptions": [ { - "Name": "testsub", + "Name": "subscription.1", "Properties": { "DeadLetteringOnMessageExpiration": false, "DefaultMessageTimeToLive": "PT1H", @@ -53,20 +54,81 @@ "ReplyToSessionId": "sessionId", "SessionId": "session1", "To": "xyz" - }}} + } + } + } + ] + }, + { + "Name": "subscription.2", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "user-prop-filter-1", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Properties": { + "prop1": "value1" + } + } + } + } + ] + }, + { + "Name": "subscription.3", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + } + }, + { + "Name": "subscription.4", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "RequiresSession": false + }, + "Rules": [ + { + "Name": "sql-filter-1", + "Properties": { + "FilterType": "Sql", + "SqlFilter": { + "SqlExpression": "sys.MessageId = '123456' AND userProp1 = 'value1'" + }, + "Action" : { + "SqlExpression": "SET sys.To = 'Entity'" + } + } + } ] } ] } - ] - - - - - } - ], - "Logging": { - "Type": "File" - } + ] + } + ], + "Logging": { + "Type": "File" + } } -} \ No newline at end of file + } \ No newline at end of file diff --git a/tests/emulator_tests/utils/servicebus/docker-compose.yml b/tests/emulator_tests/utils/servicebus/docker-compose.yml index c1781a858..9c6d92216 100644 --- a/tests/emulator_tests/utils/servicebus/docker-compose.yml +++ b/tests/emulator_tests/utils/servicebus/docker-compose.yml @@ -10,7 +10,7 @@ services: - "5672:5672" environment: SQL_SERVER: sqledge - MSSQL_SA_PASSWORD: ${AzureWebJobsSQLPassword} + MSSQL_SA_PASSWORD: Password123! ACCEPT_EULA: Y depends_on: - sqledge @@ -27,7 +27,7 @@ services: - "sqledge" environment: ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: ${AzureWebJobsSQLPassword} + MSSQL_SA_PASSWORD: Password123! # Service for the Azurite Storage Emulator azurite: container_name: "azurite-sb" diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index f90bd3258..663742383 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -159,7 +159,7 @@ def wrapper(self, *args, __meth__=test_case, __check_log__=check_log_case, **kwargs): if (__check_log__ is not None and callable(__check_log__) - and not is_envvar_true(PYAZURE_WEBHOST_DEBUG)): + and not True): # Check logging output for unit test scenarios result = self._run_test(__meth__, *args, **kwargs) @@ -233,7 +233,7 @@ def setUpClass(cls): docker_tests_enabled, sku = cls.docker_tests_enabled() - cls.host_stdout = None if is_envvar_true(PYAZURE_WEBHOST_DEBUG) \ + cls.host_stdout = None if True \ else tempfile.NamedTemporaryFile('w+t') try: @@ -971,7 +971,7 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None): def start_webhost(*, script_dir=None, stdout=None): script_root = TESTS_ROOT / script_dir if script_dir else FUNCS_PATH if stdout is None: - if is_envvar_true(PYAZURE_WEBHOST_DEBUG): + if True: stdout = sys.stdout else: stdout = subprocess.DEVNULL From f3033441055b954878a6a9765684eeee724751e0 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 6 Jun 2025 16:40:12 -0500 Subject: [PATCH 13/20] Add snake case tests + remove topic test + single sb batch test --- .../function_app.py | 2 +- .../function_app.py | 43 +++++ .../test_servicebus_batch_functions.py | 113 ++++++++++++ .../test_servicebus_functions.py | 27 --- .../utils/servicebus/config.json | 14 ++ .../classic_snake_case/__init__.py | 12 ++ .../classic_snake_case/function.json | 20 +++ .../double_underscore/__init__.py | 12 ++ .../double_underscore/function.json | 20 +++ .../double_underscore_prefix/__init__.py | 12 ++ .../double_underscore_prefix/function.json | 20 +++ .../double_underscore_suffix/__init__.py | 12 ++ .../double_underscore_suffix/function.json | 20 +++ .../just_double_underscore/__init__.py | 12 ++ .../just_double_underscore/function.json | 20 +++ .../python_main_keyword/__init__.py | 12 ++ .../python_main_keyword/function.json | 20 +++ .../snake_case_functions/sandwich/__init__.py | 12 ++ .../sandwich/function.json | 20 +++ .../single_underscore/__init__.py | 12 ++ .../single_underscore/function.json | 20 +++ .../ultimate_combo/__init__.py | 12 ++ .../ultimate_combo/function.json | 20 +++ .../ultimate_combo2/__init__.py | 12 ++ .../ultimate_combo2/function.json | 20 +++ .../underscore_prefix/__init__.py | 12 ++ .../underscore_prefix/function.json | 20 +++ .../underscore_prefix_snake/__init__.py | 12 ++ .../underscore_prefix_snake/function.json | 20 +++ .../underscore_suffix/__init__.py | 12 ++ .../underscore_suffix/function.json | 20 +++ .../underscore_suffix_snake/__init__.py | 12 ++ .../underscore_suffix_snake/function.json | 20 +++ .../valid_stein/function_app.py | 94 ++++++++++ tests/endtoend/test_snake_case_functions.py | 166 ++++++++++++++++++ 35 files changed, 879 insertions(+), 28 deletions(-) create mode 100644 tests/emulator_tests/servicebus_batch_functions/function_app.py create mode 100644 tests/emulator_tests/test_servicebus_batch_functions.py create mode 100644 tests/endtoend/snake_case_functions/classic_snake_case/__init__.py create mode 100644 tests/endtoend/snake_case_functions/classic_snake_case/function.json create mode 100644 tests/endtoend/snake_case_functions/double_underscore/__init__.py create mode 100644 tests/endtoend/snake_case_functions/double_underscore/function.json create mode 100644 tests/endtoend/snake_case_functions/double_underscore_prefix/__init__.py create mode 100644 tests/endtoend/snake_case_functions/double_underscore_prefix/function.json create mode 100644 tests/endtoend/snake_case_functions/double_underscore_suffix/__init__.py create mode 100644 tests/endtoend/snake_case_functions/double_underscore_suffix/function.json create mode 100644 tests/endtoend/snake_case_functions/just_double_underscore/__init__.py create mode 100644 tests/endtoend/snake_case_functions/just_double_underscore/function.json create mode 100644 tests/endtoend/snake_case_functions/python_main_keyword/__init__.py create mode 100644 tests/endtoend/snake_case_functions/python_main_keyword/function.json create mode 100644 tests/endtoend/snake_case_functions/sandwich/__init__.py create mode 100644 tests/endtoend/snake_case_functions/sandwich/function.json create mode 100644 tests/endtoend/snake_case_functions/single_underscore/__init__.py create mode 100644 tests/endtoend/snake_case_functions/single_underscore/function.json create mode 100644 tests/endtoend/snake_case_functions/ultimate_combo/__init__.py create mode 100644 tests/endtoend/snake_case_functions/ultimate_combo/function.json create mode 100644 tests/endtoend/snake_case_functions/ultimate_combo2/__init__.py create mode 100644 tests/endtoend/snake_case_functions/ultimate_combo2/function.json create mode 100644 tests/endtoend/snake_case_functions/underscore_prefix/__init__.py create mode 100644 tests/endtoend/snake_case_functions/underscore_prefix/function.json create mode 100644 tests/endtoend/snake_case_functions/underscore_prefix_snake/__init__.py create mode 100644 tests/endtoend/snake_case_functions/underscore_prefix_snake/function.json create mode 100644 tests/endtoend/snake_case_functions/underscore_suffix/__init__.py create mode 100644 tests/endtoend/snake_case_functions/underscore_suffix/function.json create mode 100644 tests/endtoend/snake_case_functions/underscore_suffix_snake/__init__.py create mode 100644 tests/endtoend/snake_case_functions/underscore_suffix_snake/function.json create mode 100644 tests/endtoend/snake_case_functions/valid_stein/function_app.py create mode 100644 tests/endtoend/test_snake_case_functions.py diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py index 0e4569132..0fb09e55f 100644 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py +++ b/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py @@ -40,7 +40,7 @@ def eventhub_multiple(events) -> str: connection="AzureWebJobsEventHubConnectionString", event_hub_name="python-worker-ci-eventhub-batch") @app.route(route="eventhub_output_batch", binding_arg_name="out") -def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str: +def c(req: func.HttpRequest, out: func.Out[str]) -> str: events = req.get_body().decode('utf-8') return events diff --git a/tests/emulator_tests/servicebus_batch_functions/function_app.py b/tests/emulator_tests/servicebus_batch_functions/function_app.py new file mode 100644 index 000000000..2869a3761 --- /dev/null +++ b/tests/emulator_tests/servicebus_batch_functions/function_app.py @@ -0,0 +1,43 @@ +import json + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="servicebus_output_batch") +@app.service_bus_queue_output( + arg_name="msg", + connection="AzureWebJobsServiceBusConnectionString", + queue_name="testqueue-batch") +def servicebus_output_batch(req: func.HttpRequest, msg: func.Out[str]): + msg.set(req.get_body().decode('utf-8')) + return 'OK' + + +@app.route(route="get_servicebus_batch_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-servicebus-batch-triggered.txt", + connection="AzureWebJobsStorage") +def get_servicebus_batch_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse( + file.read().decode('utf-8'), mimetype='application/json') + + +@app.service_bus_queue_trigger( + arg_name="events", + connection="AzureWebJobsServiceBusConnectionString", + queue_name="testqueue-batch", + cardinality="many") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-servicebus-batch-triggered.txt", + connection="AzureWebJobsStorage") +def servicebus_multiple(events) -> str: + table_entries = [] + for event in events: + json_entry = event.get_body().decode('utf-8') + table_entry = json.loads(json_entry) + table_entries.append(table_entry) + table_json = json.dumps(table_entries) + return table_json diff --git a/tests/emulator_tests/test_servicebus_batch_functions.py b/tests/emulator_tests/test_servicebus_batch_functions.py new file mode 100644 index 000000000..1620e85ee --- /dev/null +++ b/tests/emulator_tests/test_servicebus_batch_functions.py @@ -0,0 +1,113 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import sys +import time +from datetime import datetime +from unittest.case import skipIf + +from dateutil import parser +from tests.utils import testutils + +class TestServiceBusBatchFunctionsStein(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_batch_functions' / \ + 'servicebus_batch_functions_stein' + + @testutils.retryable_test(3, 5) + def test_servicebus_multiple(self): + NUM_EVENTS = 3 + all_row_keys_seen = dict([(i, True) for i in range(NUM_EVENTS)]) + partition_key = str(round(time.time())) + + docs = [] + for i in range(NUM_EVENTS): + doc = {'PartitionKey': partition_key, 'RowKey': i} + docs.append(doc) + + r = self.webhost.request('POST', 'servicebus_output_batch', + data=json.dumps(docs)) + self.assertEqual(r.status_code, 200) + + row_keys = [i for i in range(NUM_EVENTS)] + seen = [False] * NUM_EVENTS + row_keys_seen = dict(zip(row_keys, seen)) + + # Allow trigger to fire. + time.sleep(5) + + r = self.webhost.request( + 'GET', + 'get_servicebus_batch_triggered') + self.assertEqual(r.status_code, 200) + entries = r.json() + for entry in entries: + self.assertEqual(entry['PartitionKey'], partition_key) + row_key = entry['RowKey'] + row_keys_seen[row_key] = True + + self.assertDictEqual(all_row_keys_seen, row_keys_seen) + + # @testutils.retryable_test(3, 5) + # def test_servicebus_multiple_with_metadata(self): + # # Generate a unique event body for EventHub event + # # Record the start_time and end_time for checking event enqueue time + # start_time = datetime.utcnow() + # count = 10 + # random_number = str(round(time.time()) % 1000) + # req_body = { + # 'body': random_number + # } + + # # Invoke metadata_output HttpTrigger to generate an EventHub event + # # from azure-eventhub SDK + # r = self.webhost.request('POST', + # f'metadata_output_batch?count={count}', + # data=json.dumps(req_body)) + # self.assertEqual(r.status_code, 200) + # self.assertIn('OK', r.text) + # end_time = datetime.utcnow() + + # # Once the event get generated, allow function host to pool from + # # EventHub and wait for metadata_multiple to execute, + # # converting the event metadata into a blob. + # time.sleep(5) + + # # Call get_metadata_batch_triggered to retrieve event metadata + # r = self.webhost.request('GET', 'get_metadata_batch_triggered') + # self.assertEqual(r.status_code, 200) + + # # Check metadata and events length, events should be batched processed + # events = r.json() + # self.assertIsInstance(events, list) + # self.assertGreater(len(events), 1) + + # # EventhubEvent property check + # for event_index in range(len(events)): + # event = events[event_index] + + # # Check if the event is enqueued between start_time and end_time + # enqueued_time = parser.isoparse(event['enqueued_time']) + # self.assertTrue(start_time < enqueued_time < end_time) + + # # Check if event properties are properly set + # self.assertIsNone(event['partition_key']) # only 1 partition + # self.assertGreaterEqual(event['sequence_number'], 0) + # self.assertIsNotNone(event['offset']) + + # # Check if event.metadata field is properly set + # self.assertIsNotNone(event['metadata']) + # metadata = event['metadata'] + # sys_props_array = metadata['SystemPropertiesArray'] + # sys_props = sys_props_array[event_index] + # enqueued_time = parser.isoparse(sys_props['EnqueuedTimeUtc']) + + # # Check event trigger time and other system properties + # self.assertTrue( + # start_time.timestamp() < enqueued_time.timestamp() + # < end_time.timestamp()) # NoQA + # self.assertIsNone(sys_props['PartitionKey']) + # self.assertGreaterEqual(sys_props['SequenceNumber'], 0) + # self.assertIsNotNone(sys_props['Offset']) diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py index f3240bc79..cbd2962b3 100644 --- a/tests/emulator_tests/test_servicebus_functions.py +++ b/tests/emulator_tests/test_servicebus_functions.py @@ -102,30 +102,3 @@ def test_servicebus_basic_sdk(self): raise else: break - - @testutils.retryable_test(3, 5) - def test_servicebus_basic_sdk_topic(self): - data = str(round(time.time())) - r = self.webhost.request('POST', 'put_message_sdk_topic', - data=data) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - max_retries = 10 - - for try_no in range(max_retries): - # wait for trigger to process the queue item - time.sleep(1) - - try: - r = self.webhost.request('GET', 'get_servicebus_triggered_sdk_topic') - self.assertEqual(r.status_code, 200) - msg = r.json() - for attr in {'message', 'body', 'enqueued_time_utc', 'lock_token', - 'message_id', 'sequence_number'}: - self.assertIn(attr, msg) - except (AssertionError, json.JSONDecodeError): - if try_no == max_retries - 1: - raise - else: - break diff --git a/tests/emulator_tests/utils/servicebus/config.json b/tests/emulator_tests/utils/servicebus/config.json index 20cf83447..f5af57c5a 100644 --- a/tests/emulator_tests/utils/servicebus/config.json +++ b/tests/emulator_tests/utils/servicebus/config.json @@ -17,6 +17,20 @@ "RequiresDuplicateDetection": false, "RequiresSession": false } + }, + { + "Name": "testqueue-batch", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 10, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } } ] } diff --git a/tests/endtoend/snake_case_functions/classic_snake_case/__init__.py b/tests/endtoend/snake_case_functions/classic_snake_case/__init__.py new file mode 100644 index 000000000..6a9938230 --- /dev/null +++ b/tests/endtoend/snake_case_functions/classic_snake_case/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(req_snake: func.HttpRequest) -> func.HttpResponse: + name = req_snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/classic_snake_case/function.json b/tests/endtoend/snake_case_functions/classic_snake_case/function.json new file mode 100644 index 000000000..58ad53314 --- /dev/null +++ b/tests/endtoend/snake_case_functions/classic_snake_case/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req_snake", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/double_underscore/__init__.py b/tests/endtoend/snake_case_functions/double_underscore/__init__.py new file mode 100644 index 000000000..d5104161a --- /dev/null +++ b/tests/endtoend/snake_case_functions/double_underscore/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(req__snake: func.HttpRequest) -> func.HttpResponse: + name = req__snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/double_underscore/function.json b/tests/endtoend/snake_case_functions/double_underscore/function.json new file mode 100644 index 000000000..d895aefbc --- /dev/null +++ b/tests/endtoend/snake_case_functions/double_underscore/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req__snake", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/double_underscore_prefix/__init__.py b/tests/endtoend/snake_case_functions/double_underscore_prefix/__init__.py new file mode 100644 index 000000000..0452c9a72 --- /dev/null +++ b/tests/endtoend/snake_case_functions/double_underscore_prefix/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(__req: func.HttpRequest) -> func.HttpResponse: + name = __req.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/double_underscore_prefix/function.json b/tests/endtoend/snake_case_functions/double_underscore_prefix/function.json new file mode 100644 index 000000000..9c4ea3f8c --- /dev/null +++ b/tests/endtoend/snake_case_functions/double_underscore_prefix/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "__req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/double_underscore_suffix/__init__.py b/tests/endtoend/snake_case_functions/double_underscore_suffix/__init__.py new file mode 100644 index 000000000..9c529729b --- /dev/null +++ b/tests/endtoend/snake_case_functions/double_underscore_suffix/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(req__: func.HttpRequest) -> func.HttpResponse: + name = req__.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/double_underscore_suffix/function.json b/tests/endtoend/snake_case_functions/double_underscore_suffix/function.json new file mode 100644 index 000000000..dbbeaab54 --- /dev/null +++ b/tests/endtoend/snake_case_functions/double_underscore_suffix/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req__", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/just_double_underscore/__init__.py b/tests/endtoend/snake_case_functions/just_double_underscore/__init__.py new file mode 100644 index 000000000..d2a6b3e30 --- /dev/null +++ b/tests/endtoend/snake_case_functions/just_double_underscore/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(__: func.HttpRequest) -> func.HttpResponse: + name = __.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/just_double_underscore/function.json b/tests/endtoend/snake_case_functions/just_double_underscore/function.json new file mode 100644 index 000000000..54b67af8b --- /dev/null +++ b/tests/endtoend/snake_case_functions/just_double_underscore/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "__", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/python_main_keyword/__init__.py b/tests/endtoend/snake_case_functions/python_main_keyword/__init__.py new file mode 100644 index 000000000..c72788afd --- /dev/null +++ b/tests/endtoend/snake_case_functions/python_main_keyword/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(__main__: func.HttpRequest) -> func.HttpResponse: + name = __main__.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/python_main_keyword/function.json b/tests/endtoend/snake_case_functions/python_main_keyword/function.json new file mode 100644 index 000000000..dc66ab087 --- /dev/null +++ b/tests/endtoend/snake_case_functions/python_main_keyword/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "__main__", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/sandwich/__init__.py b/tests/endtoend/snake_case_functions/sandwich/__init__.py new file mode 100644 index 000000000..5f7792463 --- /dev/null +++ b/tests/endtoend/snake_case_functions/sandwich/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(_req_: func.HttpRequest) -> func.HttpResponse: + name = _req_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/sandwich/function.json b/tests/endtoend/snake_case_functions/sandwich/function.json new file mode 100644 index 000000000..7f51aceb9 --- /dev/null +++ b/tests/endtoend/snake_case_functions/sandwich/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "_req_", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/single_underscore/__init__.py b/tests/endtoend/snake_case_functions/single_underscore/__init__.py new file mode 100644 index 000000000..804a9f42c --- /dev/null +++ b/tests/endtoend/snake_case_functions/single_underscore/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(_: func.HttpRequest) -> func.HttpResponse: + name = _.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/single_underscore/function.json b/tests/endtoend/snake_case_functions/single_underscore/function.json new file mode 100644 index 000000000..94f26ae3a --- /dev/null +++ b/tests/endtoend/snake_case_functions/single_underscore/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "_", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/ultimate_combo/__init__.py b/tests/endtoend/snake_case_functions/ultimate_combo/__init__.py new file mode 100644 index 000000000..c87691ad2 --- /dev/null +++ b/tests/endtoend/snake_case_functions/ultimate_combo/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(_req_snake_snake_snake_snake_: func.HttpRequest) -> func.HttpResponse: + name = _req_snake_snake_snake_snake_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/ultimate_combo/function.json b/tests/endtoend/snake_case_functions/ultimate_combo/function.json new file mode 100644 index 000000000..51cb69358 --- /dev/null +++ b/tests/endtoend/snake_case_functions/ultimate_combo/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "_req_snake_snake_snake_snake_", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/ultimate_combo2/__init__.py b/tests/endtoend/snake_case_functions/ultimate_combo2/__init__.py new file mode 100644 index 000000000..7e454e5c0 --- /dev/null +++ b/tests/endtoend/snake_case_functions/ultimate_combo2/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(__9req__snake__sna_ke________snake__sn0ke_: func.HttpRequest) -> func.HttpResponse: + name = __9req__snake__sna_ke________snake__sn0ke_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/ultimate_combo2/function.json b/tests/endtoend/snake_case_functions/ultimate_combo2/function.json new file mode 100644 index 000000000..3b3506716 --- /dev/null +++ b/tests/endtoend/snake_case_functions/ultimate_combo2/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "__9req__snake__sna_ke________snake__sn0ke_", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/underscore_prefix/__init__.py b/tests/endtoend/snake_case_functions/underscore_prefix/__init__.py new file mode 100644 index 000000000..adfec92d4 --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_prefix/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(_req: func.HttpRequest) -> func.HttpResponse: + name = _req.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/underscore_prefix/function.json b/tests/endtoend/snake_case_functions/underscore_prefix/function.json new file mode 100644 index 000000000..f2bb4fd06 --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_prefix/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "_req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/underscore_prefix_snake/__init__.py b/tests/endtoend/snake_case_functions/underscore_prefix_snake/__init__.py new file mode 100644 index 000000000..c949433ec --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_prefix_snake/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(_req_snake: func.HttpRequest) -> func.HttpResponse: + name = _req_snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/underscore_prefix_snake/function.json b/tests/endtoend/snake_case_functions/underscore_prefix_snake/function.json new file mode 100644 index 000000000..7aa61592f --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_prefix_snake/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "_req_snake", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/underscore_suffix/__init__.py b/tests/endtoend/snake_case_functions/underscore_suffix/__init__.py new file mode 100644 index 000000000..c59b69a4b --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_suffix/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(req_: func.HttpRequest) -> func.HttpResponse: + name = req_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/underscore_suffix/function.json b/tests/endtoend/snake_case_functions/underscore_suffix/function.json new file mode 100644 index 000000000..75301ae9a --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_suffix/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req_", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/underscore_suffix_snake/__init__.py b/tests/endtoend/snake_case_functions/underscore_suffix_snake/__init__.py new file mode 100644 index 000000000..c1600f747 --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_suffix_snake/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# flake8: noqa +import logging + +import azure.functions as func + + +def main(req_snake_: func.HttpRequest) -> func.HttpResponse: + name = req_snake_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/snake_case_functions/underscore_suffix_snake/function.json b/tests/endtoend/snake_case_functions/underscore_suffix_snake/function.json new file mode 100644 index 000000000..f7a5dc106 --- /dev/null +++ b/tests/endtoend/snake_case_functions/underscore_suffix_snake/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req_snake_", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/endtoend/snake_case_functions/valid_stein/function_app.py b/tests/endtoend/snake_case_functions/valid_stein/function_app.py new file mode 100644 index 000000000..3b6f68f30 --- /dev/null +++ b/tests/endtoend/snake_case_functions/valid_stein/function_app.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="classic_snake_case", trigger_arg_name="req_snake_snake_snake_snake") +def classic_snake_case(req_snake_snake_snake_snake: func.HttpRequest)\ + -> func.HttpResponse: + name = req_snake_snake_snake_snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="single_underscore", trigger_arg_name="_") +def single_underscore(_: func.HttpRequest) -> func.HttpResponse: + name = _.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_prefix", trigger_arg_name="_req") +def underscore_prefix(_req: func.HttpRequest) -> func.HttpResponse: + name = _req.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_prefix_snake", trigger_arg_name="_req_snake") +def underscore_prefix_snake(_req_snake: func.HttpRequest) -> func.HttpResponse: + name = _req_snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_suffix", trigger_arg_name="req_") +def underscore_suffix(req_: func.HttpRequest) -> func.HttpResponse: + name = req_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="underscore_suffix_snake", trigger_arg_name="req_snake_") +def underscore_suffix_snake(req_snake_: func.HttpRequest) -> func.HttpResponse: + name = req_snake_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="ultimate_combo", trigger_arg_name="_req_snake_snake_snake_snake_") +def ultimate_combo(_req_snake_snake_snake_snake_: func.HttpRequest)\ + -> func.HttpResponse: + name = _req_snake_snake_snake_snake_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="sandwich", trigger_arg_name="_req_") +def sandwich(_req_: func.HttpRequest)\ + -> func.HttpResponse: + name = _req_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="double_underscore", trigger_arg_name="req__snake") +def double_underscore(req__snake: func.HttpRequest) -> func.HttpResponse: + name = req__snake.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="double_underscore_prefix", trigger_arg_name="__req") +def classic_double_underscore(__req: func.HttpRequest) -> func.HttpResponse: + name = __req.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +def double_underscore_suffix(req__: func.HttpRequest) -> func.HttpResponse: + name = req__.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="jsut_double_underscore", trigger_arg_name="__") +def jsut_double_underscore(__: func.HttpRequest) -> func.HttpResponse: + name = __.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="python_main_keyword", trigger_arg_name="__main__") +def python_main_keyword(__main__: func.HttpRequest) -> func.HttpResponse: + name = __main__.params.get('name') + return func.HttpResponse(f"Hello, {name}.") + + +@app.route(route="ultimate_combo2", + trigger_arg_name="__9req__snake__sna_ke________snake__sn0ke_") +def ultimate_combo2( + __9req__snake__sna_ke________snake__sn0ke_: func.HttpRequest)\ + -> func.HttpResponse: + name = __9req__snake__sna_ke________snake__sn0ke_.params.get('name') + return func.HttpResponse(f"Hello, {name}.") diff --git a/tests/endtoend/test_snake_case_functions.py b/tests/endtoend/test_snake_case_functions.py new file mode 100644 index 000000000..92aaa916f --- /dev/null +++ b/tests/endtoend/test_snake_case_functions.py @@ -0,0 +1,166 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +from unittest.mock import patch + +from tests.utils import testutils + +REQUEST_TIMEOUT_SEC = 5 + + +class TestValidSnakeCaseFunctions(testutils.WebHostTestCase): + def setUp(self): + self._patch_environ = patch.dict('os.environ', os.environ.copy()) + self._patch_environ.start() + super().setUp() + + def tearDown(self): + super().tearDown() + self._patch_environ.stop() + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'snake_case_functions' + + @testutils.retryable_test(3, 5) + def test_classic_snake_case(self): + r = self.webhost.request('GET', 'classic_snake_case', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_single_underscore(self): + r = self.webhost.request('GET', 'single_underscore', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_prefix(self): + r = self.webhost.request('GET', 'underscore_prefix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_suffix(self): + r = self.webhost.request('GET', 'underscore_suffix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_ultimate_combo(self): + r = self.webhost.request('GET', 'ultimate_combo', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_prefix_snake(self): + r = self.webhost.request('GET', 'underscore_prefix_snake', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_underscore_suffix_snake(self): + r = self.webhost.request('GET', 'underscore_suffix_snake', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_double_underscore(self): + r = self.webhost.request('GET', 'double_underscore', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_double_underscore_prefix(self): + r = self.webhost.request('GET', 'double_underscore_prefix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_double_underscore_suffix(self): + r = self.webhost.request('GET', 'double_underscore_suffix', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_just_double_underscore(self): + r = self.webhost.request('GET', 'just_double_underscore', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_python_main_keyword(self): + r = self.webhost.request('GET', 'python_main_keyword', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) + + @testutils.retryable_test(3, 5) + def test_ultimate_combo2(self): + r = self.webhost.request('GET', 'ultimate_combo2', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query.' + ) From b48471879d1996a979a02447f19f90ed17f037c3 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 9 Jun 2025 15:19:04 -0500 Subject: [PATCH 14/20] dir fix --- .../{ => servicebus_batch_functions_stein}/function_app.py | 0 tests/emulator_tests/test_servicebus_batch_functions.py | 4 ---- 2 files changed, 4 deletions(-) rename tests/emulator_tests/servicebus_batch_functions/{ => servicebus_batch_functions_stein}/function_app.py (100%) diff --git a/tests/emulator_tests/servicebus_batch_functions/function_app.py b/tests/emulator_tests/servicebus_batch_functions/servicebus_batch_functions_stein/function_app.py similarity index 100% rename from tests/emulator_tests/servicebus_batch_functions/function_app.py rename to tests/emulator_tests/servicebus_batch_functions/servicebus_batch_functions_stein/function_app.py diff --git a/tests/emulator_tests/test_servicebus_batch_functions.py b/tests/emulator_tests/test_servicebus_batch_functions.py index 1620e85ee..dc486643e 100644 --- a/tests/emulator_tests/test_servicebus_batch_functions.py +++ b/tests/emulator_tests/test_servicebus_batch_functions.py @@ -1,12 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import json -import sys import time -from datetime import datetime -from unittest.case import skipIf -from dateutil import parser from tests.utils import testutils class TestServiceBusBatchFunctionsStein(testutils.WebHostTestCase): From d9cf25054b0369e1210498f214a692c314f0609a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 27 Jun 2025 14:53:16 -0500 Subject: [PATCH 15/20] fix tests --- .../deferred_bindings_bug_report.yml | 71 ------------------- .../deferred_bindings_feature_request.yml | 36 ---------- .../function_app.py | 2 +- .../test_servicebus_batch_functions.py | 62 ---------------- 4 files changed, 1 insertion(+), 170 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml b/.github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml deleted file mode 100644 index fecce8046..000000000 --- a/.github/ISSUE_TEMPLATE/deferred_bindings_bug_report.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Python Worker Deferred Bindings Bug Report -description: File a Deferred Bindings bug report -title: "[Bug] Bug Title Here" -labels: ["python", "bug", "deferred-bindings"] -body: - - type: markdown - attributes: - value: | - This form will help you to fill in a bug report for the Azure Functions Python Worker Deferred Bindings feature. - - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: A clear and concise description of what you expected to happen. - placeholder: What should have occurred? - - - type: textarea - id: actual-behavior - attributes: - label: Actual Behavior - description: A clear and concise description of what actually happened. - placeholder: What went wrong? - - - type: textarea - id: reproduction-steps - attributes: - label: Steps to Reproduce - description: Please provide detailed step-by-step instructions on how to reproduce the bug. - placeholder: | - 1. Go to the [specific page or section] in the application. - 2. Click on [specific button or link]. - 3. Scroll down to [specific location]. - 4. Observe [describe what you see, e.g., an error message or unexpected behavior]. - 5. Include any additional steps or details that may be relevant. - - - type: textarea - id: code-snippet - attributes: - label: Relevant code being tried - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - - - type: textarea - id: requirements - attributes: - label: requirements.txt file - description: Please copy and paste your requirements.txt file. This will be automatically formatted into code, so no need for backticks. - render: shell - - - type: dropdown - id: environment - attributes: - label: Where are you facing this problem? - default: 0 - options: - - Local - Core Tools - - Production Environment (explain below) - - - type: textarea - id: additional-info - attributes: - label: Additional Information - description: Add any other information about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml b/.github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml deleted file mode 100644 index 809771438..000000000 --- a/.github/ISSUE_TEMPLATE/deferred_bindings_feature_request.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Python Worker Deferred Bindings Feature Request -description: File a Deferred Bindings Feature report -title: "Request a feature" -labels: ["python", "feature", "deferred-bindings"] -body: - - type: markdown - attributes: - value: | - This form will help you to fill in a feature request for the Azure Functions Python Worker Deferred Bindings feature. - - - type: textarea - id: binding-type - attributes: - label: Binding Type - description: Add information about the binding type. - placeholder: Is this on an existing binding or new binding? - - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: A clear and concise description of what you expected to happen. - placeholder: What should have occurred? - - - type: textarea - id: code-snippet - attributes: - label: Relevant sample code snipped - description: Please copy and paste any relevant code snippet of how you want the feature to be used. (This will be automatically formatted into code, so no need for backticks) - render: shell - - - type: textarea - id: additional-info - attributes: - label: Additional Information - description: Add any other information about the problem here. \ No newline at end of file diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py index 0fb09e55f..0e4569132 100644 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py +++ b/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py @@ -40,7 +40,7 @@ def eventhub_multiple(events) -> str: connection="AzureWebJobsEventHubConnectionString", event_hub_name="python-worker-ci-eventhub-batch") @app.route(route="eventhub_output_batch", binding_arg_name="out") -def c(req: func.HttpRequest, out: func.Out[str]) -> str: +def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str: events = req.get_body().decode('utf-8') return events diff --git a/tests/emulator_tests/test_servicebus_batch_functions.py b/tests/emulator_tests/test_servicebus_batch_functions.py index dc486643e..87397145e 100644 --- a/tests/emulator_tests/test_servicebus_batch_functions.py +++ b/tests/emulator_tests/test_servicebus_batch_functions.py @@ -45,65 +45,3 @@ def test_servicebus_multiple(self): row_keys_seen[row_key] = True self.assertDictEqual(all_row_keys_seen, row_keys_seen) - - # @testutils.retryable_test(3, 5) - # def test_servicebus_multiple_with_metadata(self): - # # Generate a unique event body for EventHub event - # # Record the start_time and end_time for checking event enqueue time - # start_time = datetime.utcnow() - # count = 10 - # random_number = str(round(time.time()) % 1000) - # req_body = { - # 'body': random_number - # } - - # # Invoke metadata_output HttpTrigger to generate an EventHub event - # # from azure-eventhub SDK - # r = self.webhost.request('POST', - # f'metadata_output_batch?count={count}', - # data=json.dumps(req_body)) - # self.assertEqual(r.status_code, 200) - # self.assertIn('OK', r.text) - # end_time = datetime.utcnow() - - # # Once the event get generated, allow function host to pool from - # # EventHub and wait for metadata_multiple to execute, - # # converting the event metadata into a blob. - # time.sleep(5) - - # # Call get_metadata_batch_triggered to retrieve event metadata - # r = self.webhost.request('GET', 'get_metadata_batch_triggered') - # self.assertEqual(r.status_code, 200) - - # # Check metadata and events length, events should be batched processed - # events = r.json() - # self.assertIsInstance(events, list) - # self.assertGreater(len(events), 1) - - # # EventhubEvent property check - # for event_index in range(len(events)): - # event = events[event_index] - - # # Check if the event is enqueued between start_time and end_time - # enqueued_time = parser.isoparse(event['enqueued_time']) - # self.assertTrue(start_time < enqueued_time < end_time) - - # # Check if event properties are properly set - # self.assertIsNone(event['partition_key']) # only 1 partition - # self.assertGreaterEqual(event['sequence_number'], 0) - # self.assertIsNotNone(event['offset']) - - # # Check if event.metadata field is properly set - # self.assertIsNotNone(event['metadata']) - # metadata = event['metadata'] - # sys_props_array = metadata['SystemPropertiesArray'] - # sys_props = sys_props_array[event_index] - # enqueued_time = parser.isoparse(sys_props['EnqueuedTimeUtc']) - - # # Check event trigger time and other system properties - # self.assertTrue( - # start_time.timestamp() < enqueued_time.timestamp() - # < end_time.timestamp()) # NoQA - # self.assertIsNone(sys_props['PartitionKey']) - # self.assertGreaterEqual(sys_props['SequenceNumber'], 0) - # self.assertIsNotNone(sys_props['Offset']) From b0133f71cb3a7e23781fd4d6530bbb1bdb7aeac3 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 16 Jul 2025 14:15:39 -0500 Subject: [PATCH 16/20] remove testqueue-batch + debugging e2e --- .../function_app.py | 4 ++-- .../emulator_tests/utils/servicebus/config.json | 14 -------------- workers/tests/utils/testutils.py | 6 +++--- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/workers/tests/emulator_tests/servicebus_batch_functions/servicebus_batch_functions_stein/function_app.py b/workers/tests/emulator_tests/servicebus_batch_functions/servicebus_batch_functions_stein/function_app.py index 2869a3761..1ba0c8c47 100644 --- a/workers/tests/emulator_tests/servicebus_batch_functions/servicebus_batch_functions_stein/function_app.py +++ b/workers/tests/emulator_tests/servicebus_batch_functions/servicebus_batch_functions_stein/function_app.py @@ -9,7 +9,7 @@ @app.service_bus_queue_output( arg_name="msg", connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue-batch") + queue_name="testqueue") def servicebus_output_batch(req: func.HttpRequest, msg: func.Out[str]): msg.set(req.get_body().decode('utf-8')) return 'OK' @@ -28,7 +28,7 @@ def get_servicebus_batch_triggered(req: func.HttpRequest, @app.service_bus_queue_trigger( arg_name="events", connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue-batch", + queue_name="testqueue", cardinality="many") @app.blob_output(arg_name="$return", path="python-worker-tests/test-servicebus-batch-triggered.txt", diff --git a/workers/tests/emulator_tests/utils/servicebus/config.json b/workers/tests/emulator_tests/utils/servicebus/config.json index f5af57c5a..20cf83447 100644 --- a/workers/tests/emulator_tests/utils/servicebus/config.json +++ b/workers/tests/emulator_tests/utils/servicebus/config.json @@ -17,20 +17,6 @@ "RequiresDuplicateDetection": false, "RequiresSession": false } - }, - { - "Name": "testqueue-batch", - "Properties": { - "DeadLetteringOnMessageExpiration": false, - "DefaultMessageTimeToLive": "PT1H", - "DuplicateDetectionHistoryTimeWindow": "PT20S", - "ForwardDeadLetteredMessagesTo": "", - "ForwardTo": "", - "LockDuration": "PT1M", - "MaxDeliveryCount": 10, - "RequiresDuplicateDetection": false, - "RequiresSession": false - } } ] } diff --git a/workers/tests/utils/testutils.py b/workers/tests/utils/testutils.py index f90bd3258..663742383 100644 --- a/workers/tests/utils/testutils.py +++ b/workers/tests/utils/testutils.py @@ -159,7 +159,7 @@ def wrapper(self, *args, __meth__=test_case, __check_log__=check_log_case, **kwargs): if (__check_log__ is not None and callable(__check_log__) - and not is_envvar_true(PYAZURE_WEBHOST_DEBUG)): + and not True): # Check logging output for unit test scenarios result = self._run_test(__meth__, *args, **kwargs) @@ -233,7 +233,7 @@ def setUpClass(cls): docker_tests_enabled, sku = cls.docker_tests_enabled() - cls.host_stdout = None if is_envvar_true(PYAZURE_WEBHOST_DEBUG) \ + cls.host_stdout = None if True \ else tempfile.NamedTemporaryFile('w+t') try: @@ -971,7 +971,7 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None): def start_webhost(*, script_dir=None, stdout=None): script_root = TESTS_ROOT / script_dir if script_dir else FUNCS_PATH if stdout is None: - if is_envvar_true(PYAZURE_WEBHOST_DEBUG): + if True: stdout = sys.stdout else: stdout = subprocess.DEVNULL From 172bf2b86a5068f6176669f6b75d285949ea7132 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 16 Jul 2025 14:31:58 -0500 Subject: [PATCH 17/20] remove debug log --- workers/tests/utils/testutils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workers/tests/utils/testutils.py b/workers/tests/utils/testutils.py index 663742383..f90bd3258 100644 --- a/workers/tests/utils/testutils.py +++ b/workers/tests/utils/testutils.py @@ -159,7 +159,7 @@ def wrapper(self, *args, __meth__=test_case, __check_log__=check_log_case, **kwargs): if (__check_log__ is not None and callable(__check_log__) - and not True): + and not is_envvar_true(PYAZURE_WEBHOST_DEBUG)): # Check logging output for unit test scenarios result = self._run_test(__meth__, *args, **kwargs) @@ -233,7 +233,7 @@ def setUpClass(cls): docker_tests_enabled, sku = cls.docker_tests_enabled() - cls.host_stdout = None if True \ + cls.host_stdout = None if is_envvar_true(PYAZURE_WEBHOST_DEBUG) \ else tempfile.NamedTemporaryFile('w+t') try: @@ -971,7 +971,7 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None): def start_webhost(*, script_dir=None, stdout=None): script_root = TESTS_ROOT / script_dir if script_dir else FUNCS_PATH if stdout is None: - if True: + if is_envvar_true(PYAZURE_WEBHOST_DEBUG): stdout = sys.stdout else: stdout = subprocess.DEVNULL From 42e537e08d8fbb0a5b7bc1701fbf5fe7d4a0dda4 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 16 Jul 2025 15:03:18 -0500 Subject: [PATCH 18/20] fix eg connection reference --- eng/templates/official/jobs/ci-e2e-tests.yml | 2 +- .../eventgrid_functions_stein/function_app.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml index 949540ee1..c108b8584 100644 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-e2e-tests.yml @@ -152,7 +152,7 @@ jobs: - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend env: - AzureWebJobsStorage: $(STORAGE_CONNECTION) + AzureWebJobsStorageE2E: $(STORAGE_CONNECTION) AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) diff --git a/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py b/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py index 94f05bf22..ea40e11e3 100644 --- a/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py +++ b/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py @@ -12,7 +12,7 @@ @app.event_grid_trigger(arg_name="event") @app.blob_output(arg_name="$return", path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorage") + connection="AzureWebJobsStorageE2E") def event_grid_trigger(event: func.EventGridEvent) -> str: logging.info("Event grid function is triggered!") return json.dumps({ @@ -54,10 +54,10 @@ def eventgrid_output_binding( @app.function_name(name="eventgrid_output_binding_message_to_blobstore") @app.queue_trigger(arg_name="msg", queue_name="test-event-grid-storage-queue", - connection="AzureWebJobsStorage") + connection="AzureWebJobsStorageE2E") @app.blob_output(arg_name="$return", path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorage") + connection="AzureWebJobsStorageE2E") def eventgrid_output_binding_message_to_blobstore( msg: func.QueueMessage) -> bytes: return msg.get_body() @@ -67,7 +67,7 @@ def eventgrid_output_binding_message_to_blobstore( @app.route(route="eventgrid_output_binding_success") @app.blob_input(arg_name="file", path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorage") + connection="AzureWebJobsStorageE2E") def eventgrid_output_binding_success( req: func.HttpRequest, file: func.InputStream) -> str: return file.read().decode('utf-8') @@ -77,7 +77,7 @@ def eventgrid_output_binding_success( @app.route(route="get_eventgrid_triggered") @app.blob_input(arg_name="file", path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorage") + connection="AzureWebJobsStorageE2E") def get_eventgrid_triggered( req: func.HttpRequest, file: func.InputStream) -> str: return file.read().decode('utf-8') From 549c5d710d985ccdef4798be029bc8af0eb442c9 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 17 Jul 2025 10:17:59 -0500 Subject: [PATCH 19/20] move emulator var group to separate file --- eng/ci/emulator-tests.yml | 1 + eng/ci/public-build.yml | 1 + eng/templates/official/jobs/ci-e2e-tests.yml | 2 +- eng/templates/utils/emulator-variables.yml | 2 ++ eng/templates/utils/variables.yml | 1 - .../eventgrid_functions_stein/function_app.py | 10 +++++----- 6 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 eng/templates/utils/emulator-variables.yml diff --git a/eng/ci/emulator-tests.yml b/eng/ci/emulator-tests.yml index b2e789c16..7b83b3e04 100644 --- a/eng/ci/emulator-tests.yml +++ b/eng/ci/emulator-tests.yml @@ -34,6 +34,7 @@ variables: - template: /ci/variables/build.yml@eng - template: /ci/variables/cfs.yml@eng - template: /eng/templates/utils/variables.yml@self + - template: /eng/templates/utils/emulator-variables.yml@self extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 470a94f9c..31dbf4fb0 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -28,6 +28,7 @@ resources: variables: - template: /eng/templates/utils/variables.yml@self + - template: /eng/templates/utils/emulator-variables.yml@self extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml index c108b8584..949540ee1 100644 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-e2e-tests.yml @@ -152,7 +152,7 @@ jobs: - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend env: - AzureWebJobsStorageE2E: $(STORAGE_CONNECTION) + AzureWebJobsStorage: $(STORAGE_CONNECTION) AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) diff --git a/eng/templates/utils/emulator-variables.yml b/eng/templates/utils/emulator-variables.yml new file mode 100644 index 000000000..0f4ce54fb --- /dev/null +++ b/eng/templates/utils/emulator-variables.yml @@ -0,0 +1,2 @@ +variables: + - group: python-emulator-resources \ No newline at end of file diff --git a/eng/templates/utils/variables.yml b/eng/templates/utils/variables.yml index c061c431e..6361d2d19 100644 --- a/eng/templates/utils/variables.yml +++ b/eng/templates/utils/variables.yml @@ -1,5 +1,4 @@ variables: - - group: python-emulator-resources - name: isSdkRelease value: $[startsWith(variables['Build.SourceBranch'], 'refs/heads/sdk/')] - name: isExtensionsRelease diff --git a/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py b/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py index ea40e11e3..94f05bf22 100644 --- a/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py +++ b/workers/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py @@ -12,7 +12,7 @@ @app.event_grid_trigger(arg_name="event") @app.blob_output(arg_name="$return", path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorageE2E") + connection="AzureWebJobsStorage") def event_grid_trigger(event: func.EventGridEvent) -> str: logging.info("Event grid function is triggered!") return json.dumps({ @@ -54,10 +54,10 @@ def eventgrid_output_binding( @app.function_name(name="eventgrid_output_binding_message_to_blobstore") @app.queue_trigger(arg_name="msg", queue_name="test-event-grid-storage-queue", - connection="AzureWebJobsStorageE2E") + connection="AzureWebJobsStorage") @app.blob_output(arg_name="$return", path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorageE2E") + connection="AzureWebJobsStorage") def eventgrid_output_binding_message_to_blobstore( msg: func.QueueMessage) -> bytes: return msg.get_body() @@ -67,7 +67,7 @@ def eventgrid_output_binding_message_to_blobstore( @app.route(route="eventgrid_output_binding_success") @app.blob_input(arg_name="file", path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorageE2E") + connection="AzureWebJobsStorage") def eventgrid_output_binding_success( req: func.HttpRequest, file: func.InputStream) -> str: return file.read().decode('utf-8') @@ -77,7 +77,7 @@ def eventgrid_output_binding_success( @app.route(route="get_eventgrid_triggered") @app.blob_input(arg_name="file", path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorageE2E") + connection="AzureWebJobsStorage") def get_eventgrid_triggered( req: func.HttpRequest, file: func.InputStream) -> str: return file.read().decode('utf-8') From 551568a2b9917c24b5a96ab12ed43ae180cc0733 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 17 Jul 2025 10:48:15 -0500 Subject: [PATCH 20/20] off --- .../test_servicebus_batch_functions.py | 47 ------------------- .../test_servicebus_functions.py | 42 +++++++++++++++++ 2 files changed, 42 insertions(+), 47 deletions(-) delete mode 100644 workers/tests/emulator_tests/test_servicebus_batch_functions.py diff --git a/workers/tests/emulator_tests/test_servicebus_batch_functions.py b/workers/tests/emulator_tests/test_servicebus_batch_functions.py deleted file mode 100644 index 87397145e..000000000 --- a/workers/tests/emulator_tests/test_servicebus_batch_functions.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import time - -from tests.utils import testutils - -class TestServiceBusBatchFunctionsStein(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_batch_functions' / \ - 'servicebus_batch_functions_stein' - - @testutils.retryable_test(3, 5) - def test_servicebus_multiple(self): - NUM_EVENTS = 3 - all_row_keys_seen = dict([(i, True) for i in range(NUM_EVENTS)]) - partition_key = str(round(time.time())) - - docs = [] - for i in range(NUM_EVENTS): - doc = {'PartitionKey': partition_key, 'RowKey': i} - docs.append(doc) - - r = self.webhost.request('POST', 'servicebus_output_batch', - data=json.dumps(docs)) - self.assertEqual(r.status_code, 200) - - row_keys = [i for i in range(NUM_EVENTS)] - seen = [False] * NUM_EVENTS - row_keys_seen = dict(zip(row_keys, seen)) - - # Allow trigger to fire. - time.sleep(5) - - r = self.webhost.request( - 'GET', - 'get_servicebus_batch_triggered') - self.assertEqual(r.status_code, 200) - entries = r.json() - for entry in entries: - self.assertEqual(entry['PartitionKey'], partition_key) - row_key = entry['RowKey'] - row_keys_seen[row_key] = True - - self.assertDictEqual(all_row_keys_seen, row_keys_seen) diff --git a/workers/tests/emulator_tests/test_servicebus_functions.py b/workers/tests/emulator_tests/test_servicebus_functions.py index cbd2962b3..2f34dde4f 100644 --- a/workers/tests/emulator_tests/test_servicebus_functions.py +++ b/workers/tests/emulator_tests/test_servicebus_functions.py @@ -102,3 +102,45 @@ def test_servicebus_basic_sdk(self): raise else: break + + +class TestServiceBusBatchFunctionsStein(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_batch_functions' / \ + 'servicebus_batch_functions_stein' + + @testutils.retryable_test(3, 5) + def test_servicebus_multiple(self): + NUM_EVENTS = 3 + all_row_keys_seen = dict([(i, True) for i in range(NUM_EVENTS)]) + partition_key = str(round(time.time())) + + docs = [] + for i in range(NUM_EVENTS): + doc = {'PartitionKey': partition_key, 'RowKey': i} + docs.append(doc) + + r = self.webhost.request('POST', 'servicebus_output_batch', + data=json.dumps(docs)) + self.assertEqual(r.status_code, 200) + + row_keys = [i for i in range(NUM_EVENTS)] + seen = [False] * NUM_EVENTS + row_keys_seen = dict(zip(row_keys, seen)) + + # Allow trigger to fire. + time.sleep(5) + + r = self.webhost.request( + 'GET', + 'get_servicebus_batch_triggered') + self.assertEqual(r.status_code, 200) + entries = r.json() + for entry in entries: + self.assertEqual(entry['PartitionKey'], partition_key) + row_key = entry['RowKey'] + row_keys_seen[row_key] = True + + self.assertDictEqual(all_row_keys_seen, row_keys_seen) \ No newline at end of file