From 3710fbdf1a73fff4d0eb6859044d62ca18155cfd Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 23 Apr 2024 14:50:43 -0500 Subject: [PATCH 1/4] tests, support for dict & httpresp --- azure_functions_worker/bindings/datumdef.py | 14 ++++++ azure_functions_worker/bindings/generic.py | 9 +++- .../generic_functions_stein/function_app.py | 43 +++++++++++++++++++ .../return_bytes/function.json | 21 +++++++++ .../generic_functions/return_bytes/main.py | 11 +++++ .../return_dict/function.json | 21 +++++++++ .../generic_functions/return_dict/main.py | 11 +++++ .../return_http/function.json | 21 +++++++++ .../generic_functions/return_http/main.py | 11 +++++ .../return_string/function.json | 21 +++++++++ .../generic_functions/return_string/main.py | 11 +++++ tests/endtoend/test_generic_functions.py | 23 +++++++--- .../foobar_as_none/function.json | 11 +++++ .../generic_functions/foobar_as_none/main.py | 6 +++ .../unittests/test_mock_generic_functions.py | 24 +++++++++++ 15 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 tests/endtoend/generic_functions/return_bytes/function.json create mode 100644 tests/endtoend/generic_functions/return_bytes/main.py create mode 100644 tests/endtoend/generic_functions/return_dict/function.json create mode 100644 tests/endtoend/generic_functions/return_dict/main.py create mode 100644 tests/endtoend/generic_functions/return_http/function.json create mode 100644 tests/endtoend/generic_functions/return_http/main.py create mode 100644 tests/endtoend/generic_functions/return_string/function.json create mode 100644 tests/endtoend/generic_functions/return_string/main.py create mode 100644 tests/unittests/generic_functions/foobar_as_none/function.json create mode 100644 tests/unittests/generic_functions/foobar_as_none/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 2df29da47..d55e3c380 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -199,6 +199,20 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: enable_content_negotiation=False, body=datum_as_proto(datum.value['body']), )) + elif datum.type == 'dict': + # TypedData doesn't support dict, so we return it as json + return protos.TypedData(json=json.dumps(datum.value)) + elif datum.type == 'http_response': + return protos.TypedData(http=protos.RpcHttp( + status_code=str(datum.value.status_code), + headers={ + k: v.value + for k, v in datum.value.headers.items() + }, + cookies=parse_to_rpc_http_cookie_list(None), + enable_content_negotiation=False, + body=datum_as_proto(Datum(type="string", value=datum.value.get_body())), + )) elif datum.type is None: return None else: diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index 28db5fe78..29d0b8f15 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -30,8 +30,15 @@ def encode(cls, obj: Any, *, return datumdef.Datum(type='bytes', value=bytes(obj)) elif obj is None: return datumdef.Datum(type=None, value=obj) + elif isinstance(obj, dict): + return datumdef.Datum(type='dict', value=obj) else: - raise NotImplementedError + # This isn't a common case so we do it last + from azure.functions import HttpResponse + if isinstance(obj, HttpResponse): + return datumdef.Datum(type='http_response', value=obj) + else: + raise NotImplementedError @classmethod def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any: diff --git a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py index 2ac571849..a153517c3 100644 --- a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py +++ b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import azure.functions as func + import logging app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) @@ -43,3 +44,45 @@ def return_not_processed_last(req: func.HttpRequest, testEntities): table_name="EventHubBatchTest") def mytimer(mytimer: func.TimerRequest, testEntity) -> None: logging.info("This timer trigger function executed successfully") + + +@app.function_name(name="return_string") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_string(mytimer: func.TimerRequest, testEntity): + logging.info("Return string") + return "hi!" + + +@app.function_name(name="return_bytes") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_bytes(mytimer: func.TimerRequest, testEntity): + logging.info("Return bytes") + return "test-dată" + + +@app.function_name(name="return_dict") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_dict(mytimer: func.TimerRequest, testEntity): + logging.info("Return dict") + return {"hello": "world"} diff --git a/tests/endtoend/generic_functions/return_bytes/function.json b/tests/endtoend/generic_functions/return_bytes/function.json new file mode 100644 index 000000000..4dc852e37 --- /dev/null +++ b/tests/endtoend/generic_functions/return_bytes/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/tests/endtoend/generic_functions/return_bytes/main.py b/tests/endtoend/generic_functions/return_bytes/main.py new file mode 100644 index 000000000..c02b678c0 --- /dev/null +++ b/tests/endtoend/generic_functions/return_bytes/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return bytes") + return "test-dată" diff --git a/tests/endtoend/generic_functions/return_dict/function.json b/tests/endtoend/generic_functions/return_dict/function.json new file mode 100644 index 000000000..4dc852e37 --- /dev/null +++ b/tests/endtoend/generic_functions/return_dict/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/tests/endtoend/generic_functions/return_dict/main.py b/tests/endtoend/generic_functions/return_dict/main.py new file mode 100644 index 000000000..27f343fcb --- /dev/null +++ b/tests/endtoend/generic_functions/return_dict/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return dict") + return {"hello": "world"} diff --git a/tests/endtoend/generic_functions/return_http/function.json b/tests/endtoend/generic_functions/return_http/function.json new file mode 100644 index 000000000..4dc852e37 --- /dev/null +++ b/tests/endtoend/generic_functions/return_http/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/tests/endtoend/generic_functions/return_http/main.py b/tests/endtoend/generic_functions/return_http/main.py new file mode 100644 index 000000000..13a355934 --- /dev/null +++ b/tests/endtoend/generic_functions/return_http/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return HttpResponse") + return func.HttpResponse("Hello world") diff --git a/tests/endtoend/generic_functions/return_string/function.json b/tests/endtoend/generic_functions/return_string/function.json new file mode 100644 index 000000000..4dc852e37 --- /dev/null +++ b/tests/endtoend/generic_functions/return_string/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/tests/endtoend/generic_functions/return_string/main.py b/tests/endtoend/generic_functions/return_string/main.py new file mode 100644 index 000000000..02f7aa432 --- /dev/null +++ b/tests/endtoend/generic_functions/return_string/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return string") + return "hi!" diff --git a/tests/endtoend/test_generic_functions.py b/tests/endtoend/test_generic_functions.py index a03571499..0e3522c18 100644 --- a/tests/endtoend/test_generic_functions.py +++ b/tests/endtoend/test_generic_functions.py @@ -44,16 +44,29 @@ def test_return_not_processed_last(self): r = self.webhost.request('GET', 'return_not_processed_last') self.assertEqual(r.status_code, 200) - def test_return_none(self): - time.sleep(1) + def test_return_types(self): + time.sleep(3) # Checking webhost status. r = self.webhost.request('GET', '', no_prefix=True, timeout=5) + time.sleep(3) self.assertTrue(r.ok) - def check_log_timer(self, host_out: typing.List[str]): - self.assertEqual(host_out.count("This timer trigger function executed " - "successfully"), 1) + def check_log_return_types(self, host_out: typing.List[str]): + # Checks that functions executed correctly + self.assertIn("This timer trigger function executed " + "successfully", host_out) + self.assertIn("Return string", host_out) + self.assertIn("Return bytes", host_out) + self.assertIn("Return dict", host_out) + + # Checks for failed executions + errors_found = False + for log in host_out: + if "Exception" in log: + errors_found = True + break + self.assertFalse(errors_found) @skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) diff --git a/tests/unittests/generic_functions/foobar_as_none/function.json b/tests/unittests/generic_functions/foobar_as_none/function.json new file mode 100644 index 000000000..7a458eb7f --- /dev/null +++ b/tests/unittests/generic_functions/foobar_as_none/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "direction": "out", + "name": "$return", + "type": "foobar", + "dataType": "binary" + } + ] +} diff --git a/tests/unittests/generic_functions/foobar_as_none/main.py b/tests/unittests/generic_functions/foobar_as_none/main.py new file mode 100644 index 000000000..b7acadcdd --- /dev/null +++ b/tests/unittests/generic_functions/foobar_as_none/main.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def main(): + return "hello" diff --git a/tests/unittests/test_mock_generic_functions.py b/tests/unittests/test_mock_generic_functions.py index 695d53110..1d95586e2 100644 --- a/tests/unittests/test_mock_generic_functions.py +++ b/tests/unittests/test_mock_generic_functions.py @@ -173,6 +173,9 @@ async def test_mock_generic_should_support_without_datatype(self): # implicitly self.assertEqual(r.response.result.status, protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(bytes=b'\x00\x01')) async def test_mock_generic_implicit_output_exemption(self): async with testutils.start_mockhost( @@ -223,3 +226,24 @@ async def test_mock_generic_as_nil_data(self): self.assertEqual( r.response.return_value, protos.TypedData()) + + async def test_mock_generic_as_none(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_as_none') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_as_none', [ + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(string="hello")) From 42abf895f64b7a4e606fb593ea047887fe7708ea Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 23 Apr 2024 16:41:26 -0500 Subject: [PATCH 2/4] support for returning list --- azure_functions_worker/bindings/datumdef.py | 10 ++++++--- azure_functions_worker/bindings/generic.py | 2 ++ .../generic_functions_stein/function_app.py | 14 +++++++++++++ .../return_list/function.json | 21 +++++++++++++++++++ .../generic_functions/return_list/main.py | 11 ++++++++++ tests/endtoend/test_generic_functions.py | 1 + 6 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/endtoend/generic_functions/return_list/function.json create mode 100644 tests/endtoend/generic_functions/return_list/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index d55e3c380..485b3aef4 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -199,9 +199,14 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: enable_content_negotiation=False, body=datum_as_proto(datum.value['body']), )) + elif datum.type is None: + return None elif datum.type == 'dict': # TypedData doesn't support dict, so we return it as json return protos.TypedData(json=json.dumps(datum.value)) + elif datum.type == 'list': + # TypedData doesn't support list, so we return it as json + return protos.TypedData(json=json.dumps(datum.value)) elif datum.type == 'http_response': return protos.TypedData(http=protos.RpcHttp( status_code=str(datum.value.status_code), @@ -211,10 +216,9 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: }, cookies=parse_to_rpc_http_cookie_list(None), enable_content_negotiation=False, - body=datum_as_proto(Datum(type="string", value=datum.value.get_body())), + body=datum_as_proto(Datum(type="string", + value=datum.value.get_body())), )) - elif datum.type is None: - return None else: raise NotImplementedError( 'unexpected Datum type: {!r}'.format(datum.type) diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index 29d0b8f15..1177ef8f0 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -32,6 +32,8 @@ def encode(cls, obj: Any, *, return datumdef.Datum(type=None, value=obj) elif isinstance(obj, dict): return datumdef.Datum(type='dict', value=obj) + elif isinstance(obj, list): + return datumdef.Datum(type='list', value=obj) else: # This isn't a common case so we do it last from azure.functions import HttpResponse diff --git a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py index a153517c3..067844499 100644 --- a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py +++ b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py @@ -86,3 +86,17 @@ def return_bytes(mytimer: func.TimerRequest, testEntity): def return_dict(mytimer: func.TimerRequest, testEntity): logging.info("Return dict") return {"hello": "world"} + + +@app.function_name(name="return_list") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_list(mytimer: func.TimerRequest, testEntity): + logging.info("Return list") + return [1, 2, 3] diff --git a/tests/endtoend/generic_functions/return_list/function.json b/tests/endtoend/generic_functions/return_list/function.json new file mode 100644 index 000000000..4dc852e37 --- /dev/null +++ b/tests/endtoend/generic_functions/return_list/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/tests/endtoend/generic_functions/return_list/main.py b/tests/endtoend/generic_functions/return_list/main.py new file mode 100644 index 000000000..feccec7e2 --- /dev/null +++ b/tests/endtoend/generic_functions/return_list/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return list") + return [1, 2, 3] diff --git a/tests/endtoend/test_generic_functions.py b/tests/endtoend/test_generic_functions.py index 0e3522c18..12e00ab36 100644 --- a/tests/endtoend/test_generic_functions.py +++ b/tests/endtoend/test_generic_functions.py @@ -59,6 +59,7 @@ def check_log_return_types(self, host_out: typing.List[str]): self.assertIn("Return string", host_out) self.assertIn("Return bytes", host_out) self.assertIn("Return dict", host_out) + self.assertIn("Return list", host_out) # Checks for failed executions errors_found = False From 15a101d26269e4d49c456fe9ba1b2570bb7a748c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 24 Apr 2024 13:30:14 -0500 Subject: [PATCH 3/4] added support for int, double --- azure_functions_worker/bindings/datumdef.py | 4 ++++ azure_functions_worker/bindings/generic.py | 4 ++++ .../generic_functions_stein/function_app.py | 14 +++++++++++++ .../return_double/function.json | 21 +++++++++++++++++++ .../generic_functions/return_double/main.py | 11 ++++++++++ .../return_int/function.json | 21 +++++++++++++++++++ .../generic_functions/return_int/main.py | 11 ++++++++++ tests/endtoend/test_generic_functions.py | 8 ++++--- 8 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/endtoend/generic_functions/return_double/function.json create mode 100644 tests/endtoend/generic_functions/return_double/main.py create mode 100644 tests/endtoend/generic_functions/return_int/function.json create mode 100644 tests/endtoend/generic_functions/return_int/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 485b3aef4..0a3eb47ec 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -207,6 +207,10 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: elif datum.type == 'list': # TypedData doesn't support list, so we return it as json return protos.TypedData(json=json.dumps(datum.value)) + elif datum.type == 'int': + return protos.TypedData(int=datum.value) + elif datum.type == 'double': + return protos.TypedData(double=datum.value) elif datum.type == 'http_response': return protos.TypedData(http=protos.RpcHttp( status_code=str(datum.value.status_code), diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index 1177ef8f0..79a8964ef 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -34,6 +34,10 @@ def encode(cls, obj: Any, *, return datumdef.Datum(type='dict', value=obj) elif isinstance(obj, list): return datumdef.Datum(type='list', value=obj) + elif isinstance(obj, int): + return datumdef.Datum(type='int', value=obj) + elif isinstance(obj, float): + return datumdef.Datum(type='double', value=obj) else: # This isn't a common case so we do it last from azure.functions import HttpResponse diff --git a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py index 067844499..53d5d91de 100644 --- a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py +++ b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py @@ -100,3 +100,17 @@ def return_dict(mytimer: func.TimerRequest, testEntity): def return_list(mytimer: func.TimerRequest, testEntity): logging.info("Return list") return [1, 2, 3] + + +@app.function_name(name="return_int") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_int(mytimer: func.TimerRequest, testEntity): + logging.info("Return int") + return 12 diff --git a/tests/endtoend/generic_functions/return_double/function.json b/tests/endtoend/generic_functions/return_double/function.json new file mode 100644 index 000000000..4dc852e37 --- /dev/null +++ b/tests/endtoend/generic_functions/return_double/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/tests/endtoend/generic_functions/return_double/main.py b/tests/endtoend/generic_functions/return_double/main.py new file mode 100644 index 000000000..6bfc4b9d7 --- /dev/null +++ b/tests/endtoend/generic_functions/return_double/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return double") + return 12.34 diff --git a/tests/endtoend/generic_functions/return_int/function.json b/tests/endtoend/generic_functions/return_int/function.json new file mode 100644 index 000000000..54c81f8f3 --- /dev/null +++ b/tests/endtoend/generic_functions/return_int/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] +} diff --git a/tests/endtoend/generic_functions/return_int/main.py b/tests/endtoend/generic_functions/return_int/main.py new file mode 100644 index 000000000..3a5e7175d --- /dev/null +++ b/tests/endtoend/generic_functions/return_int/main.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity): + logging.info("Return int") + return 12 diff --git a/tests/endtoend/test_generic_functions.py b/tests/endtoend/test_generic_functions.py index 12e00ab36..5bcf7ccfa 100644 --- a/tests/endtoend/test_generic_functions.py +++ b/tests/endtoend/test_generic_functions.py @@ -45,11 +45,11 @@ def test_return_not_processed_last(self): self.assertEqual(r.status_code, 200) def test_return_types(self): - time.sleep(3) + # Checking that the function app is okay + time.sleep(10) # Checking webhost status. r = self.webhost.request('GET', '', no_prefix=True, timeout=5) - time.sleep(3) self.assertTrue(r.ok) def check_log_return_types(self, host_out: typing.List[str]): @@ -60,8 +60,10 @@ def check_log_return_types(self, host_out: typing.List[str]): self.assertIn("Return bytes", host_out) self.assertIn("Return dict", host_out) self.assertIn("Return list", host_out) + self.assertIn("Return int", host_out) + self.assertIn("Return double", host_out) - # Checks for failed executions + # Checks for failed executions (TypeErrors, etc.) errors_found = False for log in host_out: if "Exception" in log: From 8359a44c58fd5c55b310b5a1f581275832d55dbc Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Thu, 25 Apr 2024 11:01:38 -0500 Subject: [PATCH 4/4] bool support, unit tests --- azure_functions_worker/bindings/datumdef.py | 15 +- azure_functions_worker/bindings/generic.py | 9 +- .../generic_functions_stein/function_app.py | 28 ++++ .../function.json | 0 .../{return_http => return_bool}/main.py | 4 +- tests/endtoend/test_generic_functions.py | 1 + .../foobar_return_bool/function.json | 11 ++ .../foobar_return_bool/main.py | 6 + .../foobar_return_dict/function.json | 11 ++ .../foobar_return_dict/main.py | 6 + .../foobar_return_double/function.json | 11 ++ .../foobar_return_double/main.py | 6 + .../foobar_return_int/function.json | 11 ++ .../foobar_return_int/main.py | 6 + .../foobar_return_list/function.json | 11 ++ .../foobar_return_list/main.py | 6 + .../unittests/test_mock_generic_functions.py | 140 ++++++++++++++++++ 17 files changed, 262 insertions(+), 20 deletions(-) rename tests/endtoend/generic_functions/{return_http => return_bool}/function.json (100%) rename tests/endtoend/generic_functions/{return_http => return_bool}/main.py (69%) create mode 100644 tests/unittests/generic_functions/foobar_return_bool/function.json create mode 100644 tests/unittests/generic_functions/foobar_return_bool/main.py create mode 100644 tests/unittests/generic_functions/foobar_return_dict/function.json create mode 100644 tests/unittests/generic_functions/foobar_return_dict/main.py create mode 100644 tests/unittests/generic_functions/foobar_return_double/function.json create mode 100644 tests/unittests/generic_functions/foobar_return_double/main.py create mode 100644 tests/unittests/generic_functions/foobar_return_int/function.json create mode 100644 tests/unittests/generic_functions/foobar_return_int/main.py create mode 100644 tests/unittests/generic_functions/foobar_return_list/function.json create mode 100644 tests/unittests/generic_functions/foobar_return_list/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 0a3eb47ec..48136e8bd 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -211,18 +211,9 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: return protos.TypedData(int=datum.value) elif datum.type == 'double': return protos.TypedData(double=datum.value) - elif datum.type == 'http_response': - return protos.TypedData(http=protos.RpcHttp( - status_code=str(datum.value.status_code), - headers={ - k: v.value - for k, v in datum.value.headers.items() - }, - cookies=parse_to_rpc_http_cookie_list(None), - enable_content_negotiation=False, - body=datum_as_proto(Datum(type="string", - value=datum.value.get_body())), - )) + elif datum.type == 'bool': + # TypedData doesn't support bool, so we return it as an int + return protos.TypedData(int=int(datum.value)) else: raise NotImplementedError( 'unexpected Datum type: {!r}'.format(datum.type) diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index 79a8964ef..901b15e78 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -38,13 +38,10 @@ def encode(cls, obj: Any, *, return datumdef.Datum(type='int', value=obj) elif isinstance(obj, float): return datumdef.Datum(type='double', value=obj) + elif isinstance(obj, bool): + return datumdef.Datum(type='bool', value=obj) else: - # This isn't a common case so we do it last - from azure.functions import HttpResponse - if isinstance(obj, HttpResponse): - return datumdef.Datum(type='http_response', value=obj) - else: - raise NotImplementedError + raise NotImplementedError @classmethod def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any: diff --git a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py index 53d5d91de..c77aaaf03 100644 --- a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py +++ b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py @@ -114,3 +114,31 @@ def return_list(mytimer: func.TimerRequest, testEntity): def return_int(mytimer: func.TimerRequest, testEntity): logging.info("Return int") return 12 + + +@app.function_name(name="return_double") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_double(mytimer: func.TimerRequest, testEntity): + logging.info("Return double") + return 12.34 + + +@app.function_name(name="return_bool") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def return_bool(mytimer: func.TimerRequest, testEntity): + logging.info("Return bool") + return True diff --git a/tests/endtoend/generic_functions/return_http/function.json b/tests/endtoend/generic_functions/return_bool/function.json similarity index 100% rename from tests/endtoend/generic_functions/return_http/function.json rename to tests/endtoend/generic_functions/return_bool/function.json diff --git a/tests/endtoend/generic_functions/return_http/main.py b/tests/endtoend/generic_functions/return_bool/main.py similarity index 69% rename from tests/endtoend/generic_functions/return_http/main.py rename to tests/endtoend/generic_functions/return_bool/main.py index 13a355934..08d693dff 100644 --- a/tests/endtoend/generic_functions/return_http/main.py +++ b/tests/endtoend/generic_functions/return_bool/main.py @@ -7,5 +7,5 @@ def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return HttpResponse") - return func.HttpResponse("Hello world") + logging.info("Return bool") + return True diff --git a/tests/endtoend/test_generic_functions.py b/tests/endtoend/test_generic_functions.py index 5bcf7ccfa..fa983a060 100644 --- a/tests/endtoend/test_generic_functions.py +++ b/tests/endtoend/test_generic_functions.py @@ -62,6 +62,7 @@ def check_log_return_types(self, host_out: typing.List[str]): self.assertIn("Return list", host_out) self.assertIn("Return int", host_out) self.assertIn("Return double", host_out) + self.assertIn("Return bool", host_out) # Checks for failed executions (TypeErrors, etc.) errors_found = False diff --git a/tests/unittests/generic_functions/foobar_return_bool/function.json b/tests/unittests/generic_functions/foobar_return_bool/function.json new file mode 100644 index 000000000..6f8a83ec0 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_bool/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "foobar", + "name": "input", + "direction": "in", + "dataType": "string" + } + ] +} diff --git a/tests/unittests/generic_functions/foobar_return_bool/main.py b/tests/unittests/generic_functions/foobar_return_bool/main.py new file mode 100644 index 000000000..4fadd2bff --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_bool/main.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def main(input): + return True diff --git a/tests/unittests/generic_functions/foobar_return_dict/function.json b/tests/unittests/generic_functions/foobar_return_dict/function.json new file mode 100644 index 000000000..6f8a83ec0 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_dict/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "foobar", + "name": "input", + "direction": "in", + "dataType": "string" + } + ] +} diff --git a/tests/unittests/generic_functions/foobar_return_dict/main.py b/tests/unittests/generic_functions/foobar_return_dict/main.py new file mode 100644 index 000000000..c8aef81a3 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_dict/main.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def main(input): + return {"hello": "world"} diff --git a/tests/unittests/generic_functions/foobar_return_double/function.json b/tests/unittests/generic_functions/foobar_return_double/function.json new file mode 100644 index 000000000..6f8a83ec0 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_double/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "foobar", + "name": "input", + "direction": "in", + "dataType": "string" + } + ] +} diff --git a/tests/unittests/generic_functions/foobar_return_double/main.py b/tests/unittests/generic_functions/foobar_return_double/main.py new file mode 100644 index 000000000..42aac3fc0 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_double/main.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def main(input): + return 12.34 diff --git a/tests/unittests/generic_functions/foobar_return_int/function.json b/tests/unittests/generic_functions/foobar_return_int/function.json new file mode 100644 index 000000000..6f8a83ec0 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_int/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "foobar", + "name": "input", + "direction": "in", + "dataType": "string" + } + ] +} diff --git a/tests/unittests/generic_functions/foobar_return_int/main.py b/tests/unittests/generic_functions/foobar_return_int/main.py new file mode 100644 index 000000000..8beb85606 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_int/main.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def main(input): + return 12 diff --git a/tests/unittests/generic_functions/foobar_return_list/function.json b/tests/unittests/generic_functions/foobar_return_list/function.json new file mode 100644 index 000000000..6f8a83ec0 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_list/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "foobar", + "name": "input", + "direction": "in", + "dataType": "string" + } + ] +} diff --git a/tests/unittests/generic_functions/foobar_return_list/main.py b/tests/unittests/generic_functions/foobar_return_list/main.py new file mode 100644 index 000000000..1d1a4a5ea --- /dev/null +++ b/tests/unittests/generic_functions/foobar_return_list/main.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def main(input): + return [1, 2, 3] diff --git a/tests/unittests/test_mock_generic_functions.py b/tests/unittests/test_mock_generic_functions.py index 1d95586e2..159ec681e 100644 --- a/tests/unittests/test_mock_generic_functions.py +++ b/tests/unittests/test_mock_generic_functions.py @@ -247,3 +247,143 @@ async def test_mock_generic_as_none(self): self.assertEqual( r.response.return_value, protos.TypedData(string="hello")) + + async def test_mock_generic_return_dict(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_return_dict') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_return_dict', [ + protos.ParameterBinding( + name='input', + data=protos.TypedData( + string='test' + ) + ) + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(json="{\"hello\": \"world\"}") + ) + + async def test_mock_generic_return_list(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_return_list') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_return_list', [ + protos.ParameterBinding( + name='input', + data=protos.TypedData( + string='test' + ) + ) + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(json="[1, 2, 3]") + ) + + async def test_mock_generic_return_int(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_return_int') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_return_int', [ + protos.ParameterBinding( + name='input', + data=protos.TypedData( + string='test' + ) + ) + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(int=12) + ) + + async def test_mock_generic_return_double(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_return_double') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_return_double', [ + protos.ParameterBinding( + name='input', + data=protos.TypedData( + string='test' + ) + ) + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(double=12.34) + ) + + async def test_mock_generic_return_bool(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_return_bool') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_return_bool', [ + protos.ParameterBinding( + name='input', + data=protos.TypedData( + string='test' + ) + ) + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData(int=1) + )