diff --git a/.github/workflows/ci_docker_con_workflow.yml b/.github/workflows/ci_docker_con_workflow.yml index d5a32a59a..bd8b649b4 100644 --- a/.github/workflows/ci_docker_con_workflow.yml +++ b/.github/workflows/ci_docker_con_workflow.yml @@ -80,3 +80,15 @@ jobs: AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString310 }} run: | python -m pytest --reruns 4 -vv --instafail tests/endtoend + - name: Running 3.11 Tests + if: matrix.python-version == 3.11 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString311 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString311 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString311 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString311 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString311 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString311 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString311 }} + run: | + python -m pytest --reruns 4 -vv --instafail tests/endtoend diff --git a/.github/workflows/ci_docker_ded_workflow.yml b/.github/workflows/ci_docker_ded_workflow.yml index 28db20b9b..4f43b7420 100644 --- a/.github/workflows/ci_docker_ded_workflow.yml +++ b/.github/workflows/ci_docker_ded_workflow.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10" ] + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] permissions: read-all env: DEDICATED_DOCKER_TEST: "true" @@ -80,3 +80,16 @@ jobs: AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString310 }} run: | python -m pytest --reruns 4 -vv --instafail tests/endtoend + - name: Running 3.11 Tests + if: matrix.python-version == 3.11 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString311 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString311 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString311 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString311 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString311 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString311 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString311 }} + run: | + python -m pytest --reruns 4 -vv --instafail tests/endtoend + diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 2d23d2c2c..557f6a0bb 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -63,7 +63,6 @@ jobs: # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev - retry 5 python setup.py extension mkdir logs - name: Test with pytest env: diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 6b9964f31..ffdc92069 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -3,7 +3,6 @@ """Python functions loader.""" import importlib import importlib.machinery -import importlib.util import os import os.path import pathlib diff --git a/azure_functions_worker/utils/dependency.py b/azure_functions_worker/utils/dependency.py index 2c92d8171..f977b56c9 100644 --- a/azure_functions_worker/utils/dependency.py +++ b/azure_functions_worker/utils/dependency.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import importlib +import importlib.util import inspect import os import re diff --git a/setup.py b/setup.py index a696cdf1d..403cd7d20 100644 --- a/setup.py +++ b/setup.py @@ -21,46 +21,12 @@ from setuptools.command import develop from azure_functions_worker.version import VERSION +from tests.utils.constants import EXTENSIONS_CSPROJ_TEMPLATE # The GitHub repository of the Azure Functions Host WEBHOST_GITHUB_API = "https://api.github.com/repos/Azure/azure-functions-host" WEBHOST_TAG_PREFIX = "v4." WEBHOST_GIT_REPO = "https://github.com/Azure/azure-functions-host/archive" - -# Extensions necessary for non-core bindings. -AZURE_EXTENSIONS = """\ - - - - netcoreapp3.1 - v4 - - ** - - - - - - - - - - - - - -""" - NUGET_CONFIG = """\ @@ -277,7 +243,7 @@ def _install_extensions(self): if not (self.extensions_dir / "extensions.csproj").exists(): with open(self.extensions_dir / "extensions.csproj", "w") as f: - print(AZURE_EXTENSIONS, file=f) + print(EXTENSIONS_CSPROJ_TEMPLATE, file=f) with open(self.extensions_dir / "NuGet.config", "w") as f: print(NUGET_CONFIG, file=f) diff --git a/tests/endtoend/test_file_name_functions.py b/tests/endtoend/test_file_name_functions.py index 3185de63f..27ee9058c 100644 --- a/tests/endtoend/test_file_name_functions.py +++ b/tests/endtoend/test_file_name_functions.py @@ -20,9 +20,17 @@ class TestHttpFunctionsFileName(testutils.WebHostTestCase): Compared to the unittests/test_http_functions.py, this file is more focus on testing the E2E flow scenarios. """ + + @classmethod + def setUpClass(cls): + os.environ["PYTHON_SCRIPT_FILE_NAME"] = "main.py" + super().setUpClass() + @classmethod - def get_script_name(cls): - return "main.py" + def tearDownClass(cls): + # Remove the WEBSITE_HOSTNAME environment variable + os.environ.pop('PYTHON_SCRIPT_FILE_NAME') + super().tearDownClass() @classmethod def get_script_dir(cls): diff --git a/tests/unittests/test_invalid_stein.py b/tests/unittests/test_invalid_stein.py index 5c8e04c68..7cbb2d018 100644 --- a/tests/unittests/test_invalid_stein.py +++ b/tests/unittests/test_invalid_stein.py @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import collections as col -import os -from unittest.mock import patch from azure_functions_worker import protos from tests.utils import testutils @@ -19,51 +17,24 @@ class TestInvalidAppStein(testutils.AsyncTestCase): - def setUp(self): - self._ctrl = testutils.start_mockhost( - script_root=STEIN_INVALID_APP_FUNCTIONS_DIR) - self._pre_env = dict(os.environ) - self.mock_version_info = patch( - 'azure_functions_worker.dispatcher.sys.version_info', - SysVersionInfo(3, 9, 0, 'final', 0)) - self.mock_version_info.start() - - def tearDown(self): - os.environ.clear() - os.environ.update(self._pre_env) - self.mock_version_info.stop() - async def test_indexing_not_app(self): """Test if the functions metadata response will be 0 when an invalid app is provided """ - async with self._ctrl as host: + mock_host = testutils.start_mockhost( + script_root=STEIN_INVALID_APP_FUNCTIONS_DIR) + async with mock_host as host: r = await host.get_functions_metadata() self.assertIsInstance(r.response, protos.FunctionMetadataResponse) self.assertIn("Error", r.response.result.exception.message) - -class TestInvalidStein(testutils.AsyncTestCase): - - def setUp(self): - self._ctrl = testutils.start_mockhost( - script_root=STEIN_INVALID_FUNCTIONS_DIR) - self._pre_env = dict(os.environ) - self.mock_version_info = patch( - 'azure_functions_worker.dispatcher.sys.version_info', - SysVersionInfo(3, 9, 0, 'final', 0)) - self.mock_version_info.start() - - def tearDown(self): - os.environ.clear() - os.environ.update(self._pre_env) - self.mock_version_info.stop() - async def test_indexing_invalid_app(self): """Test if the functions metadata response will be 0 when an invalid app is provided """ - async with self._ctrl as host: + mock_host = testutils.start_mockhost( + script_root=STEIN_INVALID_APP_FUNCTIONS_DIR) + async with mock_host as host: r = await host.get_functions_metadata() self.assertIsInstance(r.response, protos.FunctionMetadataResponse) self.assertIn("Error", r.response.result.exception.message) diff --git a/tests/unittests/test_script_file_name.py b/tests/unittests/test_script_file_name.py index 15b89a3da..43da69f87 100644 --- a/tests/unittests/test_script_file_name.py +++ b/tests/unittests/test_script_file_name.py @@ -25,8 +25,15 @@ class TestDefaultScriptFileName(testutils.WebHostTestCase): """ @classmethod - def get_script_name(cls): - return "function_app.py" + def setUpClass(cls): + os.environ["PYTHON_SCRIPT_FILE_NAME"] = "function_app.py" + super().setUpClass() + + @classmethod + def tearDownClass(cls): + # Remove the PYTHON_SCRIPT_FILE_NAME environment variable + os.environ.pop('PYTHON_SCRIPT_FILE_NAME') + super().tearDownClass() @classmethod def get_script_dir(cls): @@ -47,12 +54,19 @@ class TestNewScriptFileName(testutils.WebHostTestCase): """ @classmethod - def get_script_dir(cls): - return NEW_SCRIPT_FILE_NAME_DIR + def setUpClass(cls): + os.environ["PYTHON_SCRIPT_FILE_NAME"] = "test.py" + super().setUpClass() @classmethod - def get_script_name(cls): - return "test.py" + def tearDownClass(cls): + # Remove the PYTHON_SCRIPT_FILE_NAME environment variable + os.environ.pop('PYTHON_SCRIPT_FILE_NAME') + super().tearDownClass() + + @classmethod + def get_script_dir(cls): + return NEW_SCRIPT_FILE_NAME_DIR def test_new_file_name(self): """ @@ -69,12 +83,19 @@ class TestInvalidScriptFileName(testutils.WebHostTestCase): """ @classmethod - def get_script_dir(cls): - return INVALID_SCRIPT_FILE_NAME_DIR + def setUpClass(cls): + os.environ["PYTHON_SCRIPT_FILE_NAME"] = "main" + super().setUpClass() @classmethod - def get_script_name(cls): - return "main" + def tearDownClass(cls): + # Remove the PYTHON_SCRIPT_FILE_NAME environment variable + os.environ.pop('PYTHON_SCRIPT_FILE_NAME') + super().tearDownClass() + + @classmethod + def get_script_dir(cls): + return INVALID_SCRIPT_FILE_NAME_DIR def test_invalid_file_name(self): """ diff --git a/tests/utils/constants.py b/tests/utils/constants.py index c099eb895..b5df573b3 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -2,6 +2,44 @@ # Licensed under the MIT License. import pathlib +# Extensions necessary for non-core bindings. +EXTENSIONS_CSPROJ_TEMPLATE = """\ + + + + netcoreapp3.1 + + ** + + + + + + + + + + + + + + + +""" + + # PROJECT_ROOT refers to the path to azure-functions-python-worker # TODO: Find root folder without .parent PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index b0326cfb2..57946f1eb 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -48,7 +48,7 @@ from tests.utils.constants import PYAZURE_WORKER_DIR, \ PYAZURE_INTEGRATION_TEST, PROJECT_ROOT, WORKER_CONFIG, \ CONSUMPTION_DOCKER_TEST, DEDICATED_DOCKER_TEST, PYAZURE_WEBHOST_DEBUG, \ - ARCHIVE_WEBHOST_LOGS + ARCHIVE_WEBHOST_LOGS, EXTENSIONS_CSPROJ_TEMPLATE from tests.utils.testutils_docker import WebHostConsumption, WebHostDedicated, \ DockerConfigs @@ -75,35 +75,6 @@ } """ -EXTENSION_CSPROJ_TEMPLATE = """\ - - - netcoreapp3.1 - - ** - - - - - - - - - - - -""" - SECRETS_TEMPLATE = """\ { "masterKey": { @@ -219,7 +190,7 @@ def get_script_dir(cls): raise NotImplementedError @classmethod - def get_libraries_to_install(cls): + def get_libraries_to_install(cls) -> typing.List: pass @classmethod @@ -227,46 +198,59 @@ def get_environment_variables(cls): pass @classmethod - def get_script_name(cls): - pass + def docker_tests_enabled(self) -> (bool, str): + """ + Returns True if the environment variables + CONSUMPTION_DOCKER_TEST or DEDICATED_DOCKER_TEST + is enabled else returns False + """ + if is_envvar_true(CONSUMPTION_DOCKER_TEST): + return True, CONSUMPTION_DOCKER_TEST + elif is_envvar_true(DEDICATED_DOCKER_TEST): + return True, DEDICATED_DOCKER_TEST + else: + return False, None @classmethod def setUpClass(cls): script_dir = pathlib.Path(cls.get_script_dir()) + is_unit_test = True if 'unittests' in script_dir.parts else False - docker_configs = DockerConfigs - docker_configs.script_path = script_dir - docker_configs.libraries = cls.get_libraries_to_install() - docker_configs.env = cls.get_environment_variables() or {} - os.environ["PYTHON_SCRIPT_FILE_NAME"] = (cls.get_script_name() - or "function_app.py") + docker_tests_enabled, sku = cls.docker_tests_enabled() - if is_envvar_true(PYAZURE_WEBHOST_DEBUG): - cls.host_stdout = None - else: - cls.host_stdout = tempfile.NamedTemporaryFile('w+t') + cls.host_stdout = None if is_envvar_true(PYAZURE_WEBHOST_DEBUG) \ + else tempfile.NamedTemporaryFile('w+t') try: - if is_envvar_true(CONSUMPTION_DOCKER_TEST): - cls.webhost = \ - WebHostConsumption(docker_configs).spawn_container() - elif is_envvar_true(DEDICATED_DOCKER_TEST): - cls.webhost = \ - WebHostDedicated(docker_configs).spawn_container() + if docker_tests_enabled: + docker_configs = DockerConfigs( + script_path=script_dir, + libraries=cls.get_libraries_to_install(), + env=cls.get_environment_variables() or {}) + if sku == CONSUMPTION_DOCKER_TEST: + cls.webhost = \ + WebHostConsumption(docker_configs).spawn_container() + elif sku == DEDICATED_DOCKER_TEST: + cls.webhost = \ + WebHostDedicated(docker_configs).spawn_container() else: - _setup_func_app(TESTS_ROOT / script_dir) - cls.webhost = start_webhost(script_dir=script_dir, - stdout=cls.host_stdout) + _setup_func_app(TESTS_ROOT / script_dir, is_unit_test) + try: + cls.webhost = start_webhost(script_dir=script_dir, + stdout=cls.host_stdout) + except Exception: + raise + if not cls.webhost.is_healthy(): cls.host_out = cls.host_stdout.read() if cls.host_out is not None and len(cls.host_out) > 0: - error_message = 'WebHost is not started correctly. ' + error_message = 'WebHost is not started correctly.' f'{cls.host_stdout.name}: {cls.host_out}' cls.host_stdout_logger.error(error_message) raise RuntimeError(error_message) - - except Exception: - _teardown_func_app(TESTS_ROOT / script_dir) + except Exception as ex: + cls.host_stdout_logger.error(f"WebHost is not started correctly. {ex}") + cls.tearDownClass() raise @classmethod @@ -843,7 +827,6 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None): testconfig.read(WORKER_CONFIG) hostexe_args = [] - os.environ['AzureWebJobsFeatureFlags'] = 'EnableWorkerIndexing' # If we want to use core-tools coretools_exe = os.environ.get('CORE_TOOLS_EXE_PATH') @@ -1037,7 +1020,7 @@ def _symlink_dir(src, dst): dst.symlink_to(src, target_is_directory=True) -def _setup_func_app(app_root): +def _setup_func_app(app_root, is_unit_test=False): extensions = app_root / 'bin' host_json = app_root / 'host.json' extensions_csproj_file = app_root / 'extensions.csproj' @@ -1046,11 +1029,11 @@ def _setup_func_app(app_root): with open(host_json, 'w') as f: f.write(HOST_JSON_TEMPLATE) - if not os.path.isfile(extensions_csproj_file): + if not os.path.isfile(extensions_csproj_file) and not is_unit_test: with open(extensions_csproj_file, 'w') as f: - f.write(EXTENSION_CSPROJ_TEMPLATE) + f.write(EXTENSIONS_CSPROJ_TEMPLATE) - _symlink_dir(EXTENSIONS_PATH, extensions) + _symlink_dir(EXTENSIONS_PATH, extensions) def _teardown_func_app(app_root): diff --git a/tests/utils/testutils_docker.py b/tests/utils/testutils_docker.py index 830a9b1cb..2fe69074c 100644 --- a/tests/utils/testutils_docker.py +++ b/tests/utils/testutils_docker.py @@ -6,6 +6,7 @@ import unittest import uuid from dataclasses import dataclass +from pathlib import Path from time import sleep import requests @@ -29,7 +30,7 @@ @dataclass class DockerConfigs: - script_path: str + script_path: Path libraries: typing.List = None env: typing.Dict = None @@ -58,6 +59,9 @@ def close(self) -> bool: return exit_code == 0 + def is_healthy(self) -> bool: + pass + class WebHostDockerContainerBase(unittest.TestCase):