From 48561b291a5bcb125f31feeb748b39a579c17c4f Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 26 Mar 2024 13:59:27 -0500 Subject: [PATCH 1/3] added meta, sdk, and utils tests --- .../functions/extension/base/__init__.py | 4 +- .../azure/functions/extension/base/meta.py | 5 - .../azure/functions/extension/base/sdkType.py | 1 + .../tests/test_code_quality.py | 35 ---- .../tests/test_meta.py | 175 +++++++++++++++++- .../tests/test_sdk_type.py | 22 +++ .../tests/test_utils.py | 129 +++++++++++++ 7 files changed, 325 insertions(+), 46 deletions(-) delete mode 100644 azure-functions-extension-base/tests/test_code_quality.py create mode 100644 azure-functions-extension-base/tests/test_sdk_type.py diff --git a/azure-functions-extension-base/azure/functions/extension/base/__init__.py b/azure-functions-extension-base/azure/functions/extension/base/__init__.py index 6e7a0b9..36700c3 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/__init__.py +++ b/azure-functions-extension-base/azure/functions/extension/base/__init__.py @@ -8,7 +8,6 @@ InConverter, OutConverter, get_binding_registry, - check_deferred_bindings_enabled ) from .sdkType import SdkType from .web import ( @@ -29,7 +28,6 @@ 'OutConverter', 'SdkType', 'get_binding_registry', - 'check_deferred_bindings_enabled', 'ModuleTrackerMeta', 'RequestTrackerMeta', 'ResponseTrackerMeta', @@ -37,4 +35,4 @@ 'ResponseLabels', 'WebServer', 'WebApp' -] \ No newline at end of file +] diff --git a/azure-functions-extension-base/azure/functions/extension/base/meta.py b/azure-functions-extension-base/azure/functions/extension/base/meta.py index 5c3d626..d984627 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/meta.py +++ b/azure-functions-extension-base/azure/functions/extension/base/meta.py @@ -182,8 +182,3 @@ def encode(cls, obj: Any, *, def get_binding_registry(): return _ConverterMeta - - -def check_deferred_bindings_enabled(cls, sdk_binding_registry: _ConverterMeta, pytype: type) -> bool: - return (sdk_binding_registry is not None - and _ConverterMeta.check_supported_type(pytype)) diff --git a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py index ce5964f..e01278f 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py +++ b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py @@ -3,6 +3,7 @@ from abc import abstractmethod + class SdkType: def __init__(self, *, data: dict = None): self._data = data or {} diff --git a/azure-functions-extension-base/tests/test_code_quality.py b/azure-functions-extension-base/tests/test_code_quality.py deleted file mode 100644 index 0e8ebfd..0000000 --- a/azure-functions-extension-base/tests/test_code_quality.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pathlib -import subprocess -import sys -import unittest - - -ROOT_PATH = pathlib.Path(__file__).parent.parent - - -class TestCodeQuality(unittest.TestCase): - - def test_flake8(self): - try: - import flake8 # NoQA - except ImportError: - raise unittest.SkipTest('flake8 module is missing') - - config_path = ROOT_PATH / '.flake8' - if not config_path.exists(): - raise unittest.SkipTest('could not locate the .flake8 file') - - try: - subprocess.run( - [sys.executable, '-m', 'flake8', '--config', str(config_path)], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=str(ROOT_PATH)) - except subprocess.CalledProcessError as ex: - output = ex.output.decode() - raise AssertionError( - f'flake8 validation failed:\n{output}') from None diff --git a/azure-functions-extension-base/tests/test_meta.py b/azure-functions-extension-base/tests/test_meta.py index d23f9b7..baa3559 100644 --- a/azure-functions-extension-base/tests/test_meta.py +++ b/azure-functions-extension-base/tests/test_meta.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from typing import Mapping, List import unittest +from unittest.mock import patch -from azure.functions.extension.base import meta +from azure.functions.extension.base import meta, sdkType class TestMeta(unittest.TestCase): @@ -34,11 +34,15 @@ def test_datum_single_level_python_value(self): self.assertEqual(datum.python_value, 43.2103) self.assertEqual(datum.python_type, float) + datum = meta.Datum(value="other", type="other") + self.assertEqual(datum.python_value, "other") + def test_datum_collections_python_value(self): class DatumCollectionString: def __init__(self, *args: List[str]): self.string = args - datum = meta.Datum(value=DatumCollectionString("string 1", "string 2"), + datum = meta.Datum(value=DatumCollectionString("string 1", + "string 2"), type="collection_string") self.assertListEqual(datum.python_value, ["string 1", "string 2"]) self.assertEqual(datum.python_type, list) @@ -51,6 +55,14 @@ def __init__(self, *args: List[bytes]): self.assertListEqual(datum.python_value, [b"bytes 1", b"bytes 2"]) self.assertEqual(datum.python_type, list) + class DatumCollectionDouble: + def __init__(self, *args: List[bytes]): + self.double = args + datum = meta.Datum(value=DatumCollectionDouble(43.2103, 45.601), + type="collection_double") + self.assertListEqual(datum.python_value, [43.2103, 45.601]) + self.assertEqual(datum.python_type, list) + class DatumCollectionSint64: def __init__(self, *args: List[int]): self.sint64 = args @@ -105,3 +117,160 @@ def test_datum_json_python_value(self): "name": "awesome", "value": "cool"}) self.assertEqual(datum.python_type, dict) + + def test_equals(self): + str_datum = meta.Datum(value="awesome string", type="string") + str_datum_copy = meta.Datum(value="awesome string", type="string") + str_datum_wrong_copy = meta.Datum(value="not awesome string", + type="string") + self.assertFalse(str_datum.__eq__(dict)) + self.assertTrue(str_datum.__eq__(str_datum_copy)) + self.assertFalse(str_datum.__eq__(str_datum_wrong_copy)) + + def test_hash(self): + str_datum = meta.Datum(value="awesome string", type="string") + datum_hash = str_datum.__hash__() + self.assertIsInstance(datum_hash, int) + + def test_registry(self): + registry = meta.get_binding_registry() + self.assertIsInstance(registry, type(meta._ConverterMeta)) + self.assertIsNone(registry.get('test')) + + class MockIndexedFunction: + _bindings = {} + _trigger = None + + self.assertEqual(registry.get_raw_bindings(MockIndexedFunction, []), + []) + + self.assertFalse(registry.check_supported_type(None)) + self.assertFalse(registry.check_supported_type("hello")) + self.assertTrue(registry.check_supported_type(sdkType.SdkType)) + + self.assertFalse(registry.has_trigger_support(MockIndexedFunction)) + + def test_decode_typed_data(self): + # Case 1: data is None + self.assertIsNone(meta._BaseConverter._decode_typed_data( + data=None, python_type=str)) + + # Case 2: data.type is model_binding_data + datum_mbd = meta.Datum(value='{}', type='model_binding_data') + self.assertEqual(meta._BaseConverter._decode_typed_data( + datum_mbd, python_type=str), '{}') + + # Case 3: data.type is None + datum_none = meta.Datum(value='{}', type=None) + self.assertIsNone(meta._BaseConverter._decode_typed_data( + datum_none, python_type=str)) + + # Case 4: data.type is unsupported + datum_unsupp = meta.Datum(value='{}', type=dict) + with self.assertRaises(ValueError): + meta._BaseConverter._decode_typed_data( + datum_unsupp, python_type=str) + + # Case 5: can't coerce + datum_coerce_fail = meta.Datum(value='{}', type='model_binding_data') + with self.assertRaises(ValueError): + meta._BaseConverter._decode_typed_data( + datum_coerce_fail, python_type=dict) + + # Case 6: attempt coerce & fail + datum_attempt_coerce = meta.Datum(value=1, type='model_binding_data') + with self.assertRaises(ValueError): + meta._BaseConverter._decode_typed_data( + datum_attempt_coerce, python_type=dict) + + # Case 7: attempt to coerce and pass + datum_coerce_pass = meta.Datum(value=1, type='model_binding_data') + self.assertEquals(meta._BaseConverter._decode_typed_data( + datum_coerce_pass, python_type=str), '1') + + def test_decode_trigger_metadata_field(self): + datum_mbd = meta.Datum(value='{}', type='model_binding_data') + mock_trigger_metadata = {'key': datum_mbd} + + self.assertIsNone(meta._BaseConverter._decode_trigger_metadata_field( + trigger_metadata=mock_trigger_metadata, + field="fakeKey", + python_type=str)) + + self.assertEqual( + meta._BaseConverter._decode_trigger_metadata_field( + trigger_metadata=mock_trigger_metadata, + field="key", + python_type=str), + '{}') + + @patch("azure.functions.extension.base.meta." + "InConverter.__abstractmethods__", set()) + def test_in_converter(self): + class MockInConverter(meta.InConverter, binding='test1'): + _sample = '' + + mock_converter = MockInConverter() + self.assertIsNone(mock_converter.check_input_type_annotation( + pytype=str)) + + with self.assertRaises(NotImplementedError): + mock_converter.decode(data=None, trigger_metadata={}) + + self.assertFalse(mock_converter.has_implicit_output()) + + @patch("azure.functions.extension.base.meta." + "OutConverter.__abstractmethods__", set()) + def test_out_converter(self): + class MockOutConverter(meta.OutConverter, binding='test2'): + _sample = '' + + mock_converter = MockOutConverter() + mock_converter.check_output_type_annotation(pytype=str) + + with self.assertRaises(NotImplementedError): + mock_converter.encode(obj=None, expected_type=None) + + def test_get_registry(self): + registry = meta.get_binding_registry() + self.assertEqual(registry, meta._ConverterMeta) + + @patch("azure.functions.extension.base.meta." + "OutConverter.__abstractmethods__", set()) + def test_converter_meta(self): + class BindingNoneConverter(meta.OutConverter, binding=None): + _sample = '' + + registry = meta.get_binding_registry() + self.assertEqual(len(registry._bindings), 0) + + class BindingBlobConverter(meta.OutConverter, binding='blob'): + _sample = '' + + registry = meta.get_binding_registry() + self.assertEqual(len(registry._bindings), 1) + self.assertIsNotNone(registry._bindings.get('blob')) + self.assertEqual(registry._bindings.get('blob'), BindingBlobConverter) + + with self.assertRaises(RuntimeError): + class BindingBlob2Converter(meta.OutConverter, binding='blob'): + _sample = '' + + registry = meta.get_binding_registry() + self.assertEqual(len(registry._bindings), 1) + self.assertIsNotNone(registry._bindings.get('blob')) + self.assertEqual(registry._bindings.get('blob'), BindingBlobConverter) + + class BindingServiceBusConverter(meta.OutConverter, + binding='serviceBus', + trigger='serviceBusTrigger'): + _sample = '' + + registry = meta.get_binding_registry() + self.assertEqual(len(registry._bindings), 3) + self.assertIsNotNone(registry._bindings.get('serviceBus')) + self.assertEqual(registry._bindings.get('serviceBus'), + BindingServiceBusConverter) + self.assertIsNotNone(registry._bindings.get('serviceBusTrigger')) + self.assertEqual(registry._bindings.get('serviceBusTrigger'), + BindingServiceBusConverter) diff --git a/azure-functions-extension-base/tests/test_sdk_type.py b/azure-functions-extension-base/tests/test_sdk_type.py new file mode 100644 index 0000000..21e25f8 --- /dev/null +++ b/azure-functions-extension-base/tests/test_sdk_type.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.extension.base import sdkType + + +class TestSdkType(unittest.TestCase): + + def test_init(self): + data_populated = sdkType.SdkType(data={'key': 'value'}) + self.assertEqual(data_populated._data, {'key': 'value'}) + + data_empty = sdkType.SdkType() + self.assertEqual(data_empty._data, {}) + + def test_get_sdk_type(self): + class MockSdkType(sdkType.SdkType): + _sample = '' + + mock_type = MockSdkType() + self.assertIsNone(mock_type.get_sdk_type()) diff --git a/azure-functions-extension-base/tests/test_utils.py b/azure-functions-extension-base/tests/test_utils.py index 657ffa4..48d8844 100644 --- a/azure-functions-extension-base/tests/test_utils.py +++ b/azure-functions-extension-base/tests/test_utils.py @@ -97,3 +97,132 @@ def test_is_word(self): test_str = "foo" self.assertTrue(utils.is_word(test_str)) + + def test_snake_case_to_camel_case_multi(self): + self.assertEqual(utils.to_camel_case("data_type"), "dataType") + + def test_snake_case_to_camel_case_trailing_underscore(self): + self.assertEqual(utils.to_camel_case("data_type_"), "dataType") + + def test_snake_case_to_camel_case_leading_underscore(self): + self.assertEqual(utils.to_camel_case("_dataType"), "Datatype") + + def test_snake_case_to_camel_case_single(self): + self.assertEqual(utils.to_camel_case("dataType"), "dataType") + + def test_snake_case_to_camel_case_empty_str(self): + with self.assertRaises(ValueError) as err: + utils.to_camel_case("") + self.assertEqual(err.exception.args[0], + 'Please ensure arg name is not ' + 'empty!') + + def test_snake_case_to_camel_case_none(self): + with self.assertRaises(ValueError) as err: + utils.to_camel_case(None) + self.assertEqual(err.exception.args[0], + 'Please ensure arg name None is not ' + 'empty!') + + def test_snake_case_to_camel_case_not_one_word_nor_snake_case(self): + with self.assertRaises(ValueError) as err: + utils.to_camel_case("data-type") + self.assertEqual(err.exception.args[0], + 'Please ensure data-type is a word or snake case ' + 'string with underscore as separator.') + + def test_is_snake_case_letters_only(self): + self.assertTrue(utils.is_snake_case("dataType_foo")) + + def test_is_snake_case_lowercase_with_digit(self): + self.assertTrue(utils.is_snake_case("data_type_233")) + + def test_is_snake_case_uppercase_with_digit(self): + self.assertTrue(utils.is_snake_case("Data_Type_233")) + + def test_is_snake_case_leading_digit(self): + self.assertFalse(utils.is_snake_case("233_Data_Type_233")) + + def test_is_snake_case_no_separator(self): + self.assertFalse(utils.is_snake_case("DataType233")) + + def test_is_snake_case_invalid_separator(self): + self.assertFalse(utils.is_snake_case("Data-Type-233")) + + def test_is_word_letters_only(self): + self.assertTrue(utils.is_word("dataType")) + + def test_is_word_letters_with_digits(self): + self.assertTrue(utils.is_word("dataType233")) + + def test_is_word_leading_digits(self): + self.assertFalse(utils.is_word("233dataType")) + + def test_is_word_invalid_symbol(self): + self.assertFalse(utils.is_word("233!dataType")) + + def test_clean_nones_none(self): + self.assertEqual(utils.BuildDictMeta.clean_nones(None), None) + + def test_clean_nones_nested(self): + self.assertEqual(utils.BuildDictMeta.clean_nones( + { + "hello": None, + "hello2": ["dummy1", None, "dummy2", ["dummy3", None], + {"hello3": None}], + "hello4": { + "dummy5": "pass1", + "dummy6": None + } + }), + { + "hello2": ["dummy1", "dummy2", ["dummy3"], {}], + "hello4": {"dummy5": "pass1"} + } # NoQA + ) + + def test_add_to_dict_no_args(self): + with self.assertRaises(ValueError) as err: + @utils.BuildDictMeta.add_to_dict + def dummy(): + pass + + dummy() + + self.assertEqual(err.exception.args[0], + 'dummy has no args. Please ensure func is an object ' + 'method.') + + def test_add_to_dict_valid(self): + class TestDict: + @utils.BuildDictMeta.add_to_dict + def __init__(self, arg1, arg2, **kwargs): + self.arg1 = arg1 + self.arg2 = arg2 + + test_obj = TestDict('val1', 'val2', dummy1="dummy1", dummy2="dummy2") + + self.assertCountEqual(getattr(test_obj, 'init_params'), + {'self', 'arg1', 'arg2', 'kwargs', 'dummy1', + 'dummy2'}) + self.assertEqual(getattr(test_obj, "arg1", None), "val1") + self.assertEqual(getattr(test_obj, "arg2", None), "val2") + self.assertEqual(getattr(test_obj, "dummy1", None), "dummy1") + self.assertEqual(getattr(test_obj, "dummy2", None), "dummy2") + + def test_build_dict_meta(self): + class TestBuildDict(metaclass=utils.BuildDictMeta): + def __init__(self, arg1, arg2): + pass + + def get_dict_repr(self): + return { + "hello": None, + "world": ["dummy", None] + } + + test_obj = TestBuildDict('val1', 'val2') + + self.assertCountEqual(getattr(test_obj, 'init_params'), + {'self', 'arg1', 'arg2'}) + self.assertEqual(test_obj.get_dict_repr(), {"world": ["dummy"]}) From 595798197e59459408d1c6be32a3e6f5db021106 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 26 Mar 2024 14:47:59 -0500 Subject: [PATCH 2/3] more tests --- .../tests/test_meta.py | 10 +++- .../tests/test_utils.py | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/azure-functions-extension-base/tests/test_meta.py b/azure-functions-extension-base/tests/test_meta.py index baa3559..db5155f 100644 --- a/azure-functions-extension-base/tests/test_meta.py +++ b/azure-functions-extension-base/tests/test_meta.py @@ -132,6 +132,14 @@ def test_hash(self): datum_hash = str_datum.__hash__() self.assertIsInstance(datum_hash, int) + def test_repr(self): + str_datum = meta.Datum(value="awesome", type="string") + self.assertEqual(str_datum.__repr__(), "") + + long_str_datum = meta.Datum(value="awesome string", type="string") + self.assertEqual(long_str_datum.__repr__(), + "") + def test_registry(self): registry = meta.get_binding_registry() self.assertIsInstance(registry, type(meta._ConverterMeta)) @@ -175,7 +183,7 @@ def test_decode_typed_data(self): datum_coerce_fail = meta.Datum(value='{}', type='model_binding_data') with self.assertRaises(ValueError): meta._BaseConverter._decode_typed_data( - datum_coerce_fail, python_type=dict) + datum_coerce_fail, python_type=(tuple, list, dict)) # Case 6: attempt coerce & fail datum_attempt_coerce = meta.Datum(value=1, type='model_binding_data') diff --git a/azure-functions-extension-base/tests/test_utils.py b/azure-functions-extension-base/tests/test_utils.py index 48d8844..1847802 100644 --- a/azure-functions-extension-base/tests/test_utils.py +++ b/azure-functions-extension-base/tests/test_utils.py @@ -19,6 +19,21 @@ def __init__(self, bindings: utils.Binding): self._bindings = bindings +class MockInitParams(utils.Binding): + def __init__(self, name, direction, data_type, + type, init_params): + self.type = 'blob' + self.name = name + self._direction = direction + self._data_type = data_type + self._dict = { + "direction": self._direction, + "dataType": self._data_type, + "type": self.type + } + self.init_params = init_params + + class TestUtils(unittest.TestCase): # Test Utils class def test_get_dict_repr_sdk(self): @@ -69,6 +84,42 @@ def test_get_dict_repr_non_sdk(self): '"properties": ' '{"SupportsDeferredBinding": false}}']) + def test_get_dict_repr_init_params(self): + # Create mock blob + meta._ConverterMeta._bindings = {"blob"} + + # Create test binding + mock_blob = MockInitParams(name="client", + direction=utils.BindingDirection.IN, + data_type=None, type='blob', + init_params=['test', 'type', 'direction']) + + # Create test input_types dict + mock_input_types = {"client": MockParamTypeInfo( + binding_name='blobTrigger', pytype=sdkType.SdkType)} + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr = utils.get_raw_bindings(mock_indexed_functions, + mock_input_types) + self.assertEqual(dict_repr, + ['{"direction": "IN", "dataType": null, ' + '"type": "blob", "test": null, "properties": ' + '{"SupportsDeferredBinding": true}}']) + + def test_binding_data_type(self): + mock_blob = utils.Binding(name="blob", + direction=utils.BindingDirection.IN, + data_type=None, type='blob') + self.assertIsNone(mock_blob.data_type) + + mock_data_type = utils.Binding(name="blob", + direction=utils.BindingDirection.IN, + data_type=utils.DataType.STRING, + type='blob') + self.assertEqual(mock_data_type.data_type, 1) + def test_to_camel_case(self): test_str = "" self.assertRaises(ValueError, From 766156bbce89e5323800523058dcc150cf29effc Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 26 Mar 2024 14:59:49 -0500 Subject: [PATCH 3/3] cov for utils --- .../tests/test_utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/azure-functions-extension-base/tests/test_utils.py b/azure-functions-extension-base/tests/test_utils.py index 1847802..659fbc7 100644 --- a/azure-functions-extension-base/tests/test_utils.py +++ b/azure-functions-extension-base/tests/test_utils.py @@ -261,6 +261,22 @@ def __init__(self, arg1, arg2, **kwargs): self.assertEqual(getattr(test_obj, "dummy1", None), "dummy1") self.assertEqual(getattr(test_obj, "dummy2", None), "dummy2") + def test_add_to_dict_duplicate(self): + class TestDict: + @utils.BuildDictMeta.add_to_dict + def __init__(self, arg1, arg2, **kwargs): + self.arg1 = arg1 + self.arg2 = arg2 + self.arg3 = arg1 + + test_obj = TestDict('val1', 'val2', arg3="dummy1") + + self.assertCountEqual(getattr(test_obj, 'init_params'), + {'self', 'arg1', 'arg2', 'kwargs', 'arg3'}) + self.assertEqual(getattr(test_obj, "arg1", None), "val1") + self.assertEqual(getattr(test_obj, "arg2", None), "val2") + self.assertEqual(getattr(test_obj, "arg3", None), "val1") + def test_build_dict_meta(self): class TestBuildDict(metaclass=utils.BuildDictMeta): def __init__(self, arg1, arg2):