diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 69ae8576..17d8000c 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -60,6 +60,12 @@ extends: PROJECT_DIRECTORY: 'workers' # Skip the build stage for SDK and Extensions release branches. This stage will fail because pyproject.toml contains the updated (and unreleased) library version condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + - stage: CheckPythonWorkerDependencies + dependsOn: BuildPythonWorker + jobs: + - template: /eng/templates/jobs/ci-dependency-check.yml@self + parameters: + PoolName: 1es-pool-azfunc-public - stage: RunWorkerUnitTests dependsOn: BuildPythonWorker jobs: diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml new file mode 100644 index 00000000..13dfc89e --- /dev/null +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -0,0 +1,82 @@ +parameters: + PROJECT_DIRECTORY: 'workers' + +jobs: + - job: "TestPython" + displayName: "Run Dependency Checks" + + pool: + name: ${{ parameters.PoolName }} + image: 1es-ubuntu-22.04 + os: linux + + strategy: + matrix: + Python39: + PYTHON_VERSION: '3.9' + Python310: + PYTHON_VERSION: '3.10' + Python311: + PYTHON_VERSION: '3.11' + Python312: + PYTHON_VERSION: '3.12' + Python313: + PYTHON_VERSION: '3.13' + Python314: + PYTHON_VERSION: '3.14' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(PYTHON_VERSION) + - bash: | + cd workers + pip install --no-deps . + + PY_VER="$(PYTHON_VERSION)" + echo "Python version: $PY_VER" + + # Extract minor version + PY_MINOR="${PY_VER#*.}" + + if [ "$PY_MINOR" -ge 13 ]; then + python -c "import pkgutil, sys; + import proxy_worker; + print('OK: imports resolved')" + else + python -c "import pkgutil, sys; + import azure_functions_worker; + print('OK: imports resolved')" + fi + displayName: 'Python Worker: check for missing dependencies' + - bash: | + cd runtimes/v1 + pip install --no-deps . + + PY_VER="$(PYTHON_VERSION)" + echo "Python version: $PY_VER" + + # Extract minor version + PY_MINOR="${PY_VER#*.}" + + if [ "$PY_MINOR" -ge 13 ]; then + python -c "import pkgutil, sys; + import azure_functions_runtime_v1; + print('OK: imports resolved')" + fi + displayName: 'Python Library V1: check for missing dependencies' + - bash: | + cd runtimes/v2 + pip install --no-deps . + + PY_VER="$(PYTHON_VERSION)" + echo "Python version: $PY_VER" + + # Extract minor version + PY_MINOR="${PY_VER#*.}" + + if [ "$PY_MINOR" -ge 13 ]; then + python -c "import pkgutil, sys; + import azure_functions_runtime; + print('OK: imports resolved')" + fi + displayName: 'Python Library V2: check for missing dependencies' diff --git a/eng/templates/official/jobs/publish-release.yml b/eng/templates/official/jobs/publish-release.yml index 54d87313..2cba6990 100644 --- a/eng/templates/official/jobs/publish-release.yml +++ b/eng/templates/official/jobs/publish-release.yml @@ -123,7 +123,7 @@ jobs: # Modify release_notes.md Write-Host "Adding a new entry in release_notes.md" - Add-Content -Path release_notes.md -Value "`n- Update Python Worker Version to [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion)" + Add-Content -Path release_notes.md -Value "- Update Python Worker Version to [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion)" # Commit Python Version diff --git a/workers/tests/endtoend/test_basic_http_functions.py b/workers/tests/endtoend/test_basic_http_functions.py new file mode 100644 index 00000000..f4b04ff1 --- /dev/null +++ b/workers/tests/endtoend/test_basic_http_functions.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from tests.utils import testutils + +REQUEST_TIMEOUT_SEC = 5 + + +class TestHttpFunctions(testutils.WebHostTestCase): + """Test the native Http Trigger in the local webhost. + + This test class will spawn a webhost from your /build/webhost + folder and replace the built-in Python with azure_functions_worker from + your code base. Since the Http Trigger is a native suport from host, we + don't need to setup any external resources. + + Compared to the unittests/test_http_functions.py, this file is more focus + on testing the E2E flow scenarios. + """ + + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' + + def test_function_index_page_should_return_ok(self): + """The index page of Azure Functions should return OK in any + circumstances + """ + r = self.webhost.request('GET', '', no_prefix=True, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + + def test_default_http_template_should_return_ok(self): + """Test if the default template of Http trigger in Python Function app + will return OK + """ + r = self.webhost.request('GET', 'default_template', + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + + def test_default_http_template_should_accept_query_param(self): + """Test if the azure.functions SDK is able to deserialize query + parameter from the default template + """ + r = self.webhost.request('GET', 'default_template', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query. This HTTP triggered function executed successfully.' + ) + + def test_default_http_template_should_accept_body(self): + """Test if the azure.functions SDK is able to deserialize http body + and pass it to default template + """ + r = self.webhost.request('POST', 'default_template', + data='{ "name": "body" }'.encode('utf-8'), + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, body. This HTTP triggered function executed successfully.' + ) + + +class TestHttpFunctionsStein(TestHttpFunctions): + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_stein' + + def test_return_custom_class(self): + """Test if returning a custom class returns OK + """ + r = self.webhost.request('GET', 'custom_response', + timeout=REQUEST_TIMEOUT_SEC) + self.assertEqual( + r.content, + b'{"status": "healthy"}' + ) + self.assertTrue(r.ok) + + def test_return_custom_class_with_query_param(self): + """Test if query is accepted + """ + r = self.webhost.request('GET', 'custom_response', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'{"name": "query"}' + ) + + +class TestHttpFunctionsSteinGeneric(TestHttpFunctionsStein): + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_stein' / \ + 'generic'