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):