diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index 523f36364..d3fc5570b 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -41,11 +41,13 @@ PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39 = True PYTHON_EXTENSIONS_RELOAD_FUNCTIONS = "PYTHON_EXTENSIONS_RELOAD_FUNCTIONS" +# new programming model default script file name +PYTHON_SCRIPT_FILE_NAME = "PYTHON_SCRIPT_FILE_NAME" +PYTHON_SCRIPT_FILE_NAME_DEFAULT = "function_app.py" + # External Site URLs MODULE_NOT_FOUND_TS_URL = "https://aka.ms/functions-modulenotfound" -# new programming model script file name -SCRIPT_FILE_NAME = "function_app.py" PYTHON_LANGUAGE_RUNTIME = "python" # Settings for V2 programming model diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 1dcc9f658..d94c96457 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -28,14 +28,16 @@ PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, PYTHON_THREADPOOL_THREAD_COUNT_MIN, PYTHON_ENABLE_DEBUG_LOGGING, - SCRIPT_FILE_NAME, + PYTHON_SCRIPT_FILE_NAME, + PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, CUSTOMER_PACKAGES_PATH) from .extension import ExtensionManager from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) from .utils.app_setting_manager import get_python_appsetting_state -from .utils.common import get_app_setting, is_envvar_true +from .utils.common import (get_app_setting, is_envvar_true, + validate_script_file_name) from .utils.dependency import DependencyManager from .utils.tracing import marshall_exception_trace from .utils.wrappers import disable_feature_by @@ -327,24 +329,29 @@ async def _handle__worker_status_request(self, request): async def _handle__functions_metadata_request(self, request): metadata_request = request.functions_metadata_request directory = metadata_request.function_app_directory - function_path = os.path.join(directory, SCRIPT_FILE_NAME) + script_file_name = get_app_setting( + setting=PYTHON_SCRIPT_FILE_NAME, + default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') + function_path = os.path.join(directory, script_file_name) logger.info( - 'Received WorkerMetadataRequest, request ID %s, directory: %s', - self.request_id, directory) - - if not os.path.exists(function_path): - # Fallback to legacy model - logger.info("%s does not exist. " - "Switching to host indexing.", SCRIPT_FILE_NAME) - return protos.StreamingMessage( - request_id=request.request_id, - function_metadata_response=protos.FunctionMetadataResponse( - use_default_metadata_indexing=True, - result=protos.StatusResult( - status=protos.StatusResult.Success))) + 'Received WorkerMetadataRequest, request ID %s, function_path: %s', + self.request_id, function_path) try: + validate_script_file_name(script_file_name) + + if not os.path.exists(function_path): + # Fallback to legacy model + logger.info("%s does not exist. " + "Switching to host indexing.", script_file_name) + return protos.StreamingMessage( + request_id=request.request_id, + function_metadata_response=protos.FunctionMetadataResponse( + use_default_metadata_indexing=True, + result=protos.StatusResult( + status=protos.StatusResult.Success))) + fx_metadata_results = self.index_functions(function_path) return protos.StreamingMessage( @@ -367,8 +374,6 @@ async def _handle__function_load_request(self, request): function_id = func_request.function_id function_metadata = func_request.metadata function_name = function_metadata.name - function_path = os.path.join(function_metadata.directory, - SCRIPT_FILE_NAME) logger.info( 'Received WorkerLoadRequest, request ID %s, function_id: %s,' @@ -377,6 +382,14 @@ async def _handle__function_load_request(self, request): programming_model = "V1" try: if not self._functions.get_function(function_id): + script_file_name = get_app_setting( + setting=PYTHON_SCRIPT_FILE_NAME, + default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') + validate_script_file_name(script_file_name) + function_path = os.path.join( + function_metadata.directory, + script_file_name) + if function_metadata.properties.get("worker_indexed", False) \ or os.path.exists(function_path): # This is for the second worker and above where the worker diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 7277eb3eb..bd64cdfca 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -17,8 +17,10 @@ from . import protos, functions from .bindings.retrycontext import RetryPolicy -from .constants import MODULE_NOT_FOUND_TS_URL, SCRIPT_FILE_NAME, \ - PYTHON_LANGUAGE_RUNTIME, RETRY_POLICY, CUSTOMER_PACKAGES_PATH +from .utils.common import get_app_setting +from .constants import MODULE_NOT_FOUND_TS_URL, PYTHON_SCRIPT_FILE_NAME, \ + PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, \ + CUSTOMER_PACKAGES_PATH, RETRY_POLICY from .logging import logger from .utils.wrappers import attach_message_to_exception @@ -225,7 +227,10 @@ def index_function_app(function_path: str): f"level function app instances are defined.") if not app: + script_file_name = get_app_setting( + setting=PYTHON_SCRIPT_FILE_NAME, + default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') raise ValueError("Could not find top level function app instances in " - f"{SCRIPT_FILE_NAME}.") + f"{script_file_name}.") return app.get_functions() diff --git a/azure_functions_worker/utils/app_setting_manager.py b/azure_functions_worker/utils/app_setting_manager.py index b0a680364..a1e367329 100644 --- a/azure_functions_worker/utils/app_setting_manager.py +++ b/azure_functions_worker/utils/app_setting_manager.py @@ -10,7 +10,8 @@ PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT, PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39, PYTHON_ENABLE_DEBUG_LOGGING, - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED) + FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, + PYTHON_SCRIPT_FILE_NAME) def get_python_appsetting_state(): @@ -21,7 +22,8 @@ def get_python_appsetting_state(): PYTHON_ISOLATE_WORKER_DEPENDENCIES, PYTHON_ENABLE_DEBUG_LOGGING, PYTHON_ENABLE_WORKER_EXTENSIONS, - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED] + FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, + PYTHON_SCRIPT_FILE_NAME] app_setting_states = "".join( f"{app_setting}: {current_vars[app_setting]} | " diff --git a/azure_functions_worker/utils/common.py b/azure_functions_worker/utils/common.py index 0f12a5f5c..f7365476c 100644 --- a/azure_functions_worker/utils/common.py +++ b/azure_functions_worker/utils/common.py @@ -3,6 +3,7 @@ import importlib import os import sys +import re from types import ModuleType from typing import Optional, Callable @@ -136,3 +137,20 @@ def get_sdk_from_sys_path() -> ModuleType: sys.path.insert(0, CUSTOMER_PACKAGES_PATH) return importlib.import_module('azure.functions') + + +class InvalidFileNameError(Exception): + + def __init__(self, file_name: str) -> None: + super().__init__( + f'Invalid file name: {file_name}') + + +def validate_script_file_name(file_name: str): + # First character can be a letter, number, or underscore + # Following characters can be a letter, number, underscore, hyphen, or dash + # Ending must be .py + pattern = re.compile(r'^[a-zA-Z0-9_][a-zA-Z0-9_\-]*\.py$') + if not pattern.match(file_name): + raise InvalidFileNameError(file_name) + return True diff --git a/tests/endtoend/http_functions/http_functions_stein/file_name/main.py b/tests/endtoend/http_functions/http_functions_stein/file_name/main.py new file mode 100644 index 000000000..f5d4e1171 --- /dev/null +++ b/tests/endtoend/http_functions/http_functions_stein/file_name/main.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +import logging +import time + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="default_template") +def default_template(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return func.HttpResponse( + "This HTTP triggered function executed successfully. " + "Pass a name in the query string or in the request body for a" + " personalized response.", + status_code=200 + ) + + +@app.route(route="http_func") +def http_func(req: func.HttpRequest) -> func.HttpResponse: + time.sleep(1) + + current_time = datetime.now().strftime("%H:%M:%S") + return func.HttpResponse(f"{current_time}") diff --git a/tests/endtoend/test_file_name_functions.py b/tests/endtoend/test_file_name_functions.py new file mode 100644 index 000000000..3185de63f --- /dev/null +++ b/tests/endtoend/test_file_name_functions.py @@ -0,0 +1,109 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import requests + +from azure_functions_worker.constants import PYTHON_SCRIPT_FILE_NAME +from tests.utils import testutils + +REQUEST_TIMEOUT_SEC = 10 + + +class TestHttpFunctionsFileName(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. + """ + @classmethod + def get_script_name(cls): + return "main.py" + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_stein' / \ + 'file_name' + + @testutils.retryable_test(3, 5) + def test_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) + + @testutils.retryable_test(3, 5) + 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) + + @testutils.retryable_test(3, 5) + 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.' + ) + + @testutils.retryable_test(3, 5) + 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.' + ) + + @testutils.retryable_test(3, 5) + def test_worker_status_endpoint_should_return_ok(self): + """Test if the worker status endpoint will trigger + _handle__worker_status_request and sends a worker status response back + to host + """ + root_url = self.webhost._addr + health_check_url = f'{root_url}/admin/host/ping' + r = requests.post(health_check_url, + params={'checkHealth': '1'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + + @testutils.retryable_test(3, 5) + def test_worker_status_endpoint_should_return_ok_when_disabled(self): + """Test if the worker status endpoint will trigger + _handle__worker_status_request and sends a worker status response back + to host + """ + os.environ['WEBSITE_PING_METRICS_SCALE_ENABLED'] = '0' + root_url = self.webhost._addr + health_check_url = f'{root_url}/admin/host/ping' + r = requests.post(health_check_url, + params={'checkHealth': '1'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + + def test_correct_file_name(self): + self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) + self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), + 'main.py') diff --git a/tests/unittests/broken_functions/invalid_app_stein/function_app.py b/tests/unittests/broken_functions/invalid_app_stein/function_app.py new file mode 100644 index 000000000..3454a59ed --- /dev/null +++ b/tests/unittests/broken_functions/invalid_app_stein/function_app.py @@ -0,0 +1,5 @@ +import azure.functions as func + + +def main(req: func.HttpRequest): + pass diff --git a/tests/unittests/file_name_functions/default_file_name/function_app.py b/tests/unittests/file_name_functions/default_file_name/function_app.py new file mode 100644 index 000000000..7eeb55331 --- /dev/null +++ b/tests/unittests/file_name_functions/default_file_name/function_app.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import azure.functions as func + +app = func.FunctionApp() + + +@app.route(route="return_str") +def return_str(req: func.HttpRequest) -> str: + return 'Hello World!' diff --git a/tests/unittests/file_name_functions/invalid_file_name/main b/tests/unittests/file_name_functions/invalid_file_name/main new file mode 100644 index 000000000..7eeb55331 --- /dev/null +++ b/tests/unittests/file_name_functions/invalid_file_name/main @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import azure.functions as func + +app = func.FunctionApp() + + +@app.route(route="return_str") +def return_str(req: func.HttpRequest) -> str: + return 'Hello World!' diff --git a/tests/unittests/file_name_functions/new_file_name/test.py b/tests/unittests/file_name_functions/new_file_name/test.py new file mode 100644 index 000000000..7eeb55331 --- /dev/null +++ b/tests/unittests/file_name_functions/new_file_name/test.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import azure.functions as func + +app = func.FunctionApp() + + +@app.route(route="return_str") +def return_str(req: func.HttpRequest) -> str: + return 'Hello World!' diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 0605a7ddd..d607216ed 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -20,9 +20,6 @@ DISPATCHER_STEIN_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ 'dispatcher_functions' / \ 'dispatcher_functions_stein' -DISPATCHER_STEIN_INVALID_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'broken_functions' / \ - 'invalid_stein' class TestThreadPoolSettingsPython37(testutils.AsyncTestCase): diff --git a/tests/unittests/test_invalid_stein.py b/tests/unittests/test_invalid_stein.py new file mode 100644 index 000000000..5c8e04c68 --- /dev/null +++ b/tests/unittests/test_invalid_stein.py @@ -0,0 +1,69 @@ +# 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 + +SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro", + "releaselevel", "serial"]) +STEIN_INVALID_APP_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ + 'broken_functions' / \ + 'invalid_app_stein' +STEIN_INVALID_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ + 'broken_functions' / \ + 'invalid_stein' + + +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: + 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: + 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_loader.py b/tests/unittests/test_loader.py index 85ff14727..d68ebe13b 100644 --- a/tests/unittests/test_loader.py +++ b/tests/unittests/test_loader.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import asyncio +import os import pathlib import subprocess import sys @@ -12,6 +13,8 @@ from azure.functions.decorators.timer import TimerTrigger from azure_functions_worker import functions +from azure_functions_worker.constants import PYTHON_SCRIPT_FILE_NAME, \ + PYTHON_SCRIPT_FILE_NAME_DEFAULT from azure_functions_worker.loader import build_retry_protos from tests.utils import testutils @@ -241,3 +244,26 @@ async def _runner(): '--disable-pip-version-check', 'uninstall', '-y', '--quiet', 'foo-binding' ], check=True) + + +class TestConfigurableFileName(testutils.WebHostTestCase): + + def setUp(self) -> None: + def test_function(): + return "Test" + + self.file_name = PYTHON_SCRIPT_FILE_NAME_DEFAULT + self.test_function = test_function + self.func = Function(self.test_function, script_file="function_app.py") + self.function_registry = functions.Registry() + + @classmethod + def get_script_dir(cls): + return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_stein' + + def test_correct_file_name(self): + os.environ.update({PYTHON_SCRIPT_FILE_NAME: self.file_name}) + self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) + self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), + 'function_app.py') diff --git a/tests/unittests/test_script_file_name.py b/tests/unittests/test_script_file_name.py new file mode 100644 index 000000000..15b89a3da --- /dev/null +++ b/tests/unittests/test_script_file_name.py @@ -0,0 +1,85 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from tests.utils import testutils +from azure_functions_worker.constants import \ + PYTHON_SCRIPT_FILE_NAME, PYTHON_SCRIPT_FILE_NAME_DEFAULT + +DEFAULT_SCRIPT_FILE_NAME_DIR = testutils.UNIT_TESTS_FOLDER / \ + 'file_name_functions' / \ + 'default_file_name' + +NEW_SCRIPT_FILE_NAME_DIR = testutils.UNIT_TESTS_FOLDER / \ + 'file_name_functions' / \ + 'new_file_name' + +INVALID_SCRIPT_FILE_NAME_DIR = testutils.UNIT_TESTS_FOLDER / \ + 'file_name_functions' / \ + 'invalid_file_name' + + +class TestDefaultScriptFileName(testutils.WebHostTestCase): + """ + Tests for default file name + """ + + @classmethod + def get_script_name(cls): + return "function_app.py" + + @classmethod + def get_script_dir(cls): + return DEFAULT_SCRIPT_FILE_NAME_DIR + + def test_default_file_name(self): + """ + Test the default file name + """ + self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) + self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), + PYTHON_SCRIPT_FILE_NAME_DEFAULT) + + +class TestNewScriptFileName(testutils.WebHostTestCase): + """ + Tests for changed file name + """ + + @classmethod + def get_script_dir(cls): + return NEW_SCRIPT_FILE_NAME_DIR + + @classmethod + def get_script_name(cls): + return "test.py" + + def test_new_file_name(self): + """ + Test the new file name + """ + self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) + self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), + 'test.py') + + +class TestInvalidScriptFileName(testutils.WebHostTestCase): + """ + Tests for invalid file name + """ + + @classmethod + def get_script_dir(cls): + return INVALID_SCRIPT_FILE_NAME_DIR + + @classmethod + def get_script_name(cls): + return "main" + + def test_invalid_file_name(self): + """ + Test the invalid file name + """ + self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) + self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), + 'main') diff --git a/tests/unittests/test_utilities.py b/tests/unittests/test_utilities.py index c71bcce37..3ea0900ba 100644 --- a/tests/unittests/test_utilities.py +++ b/tests/unittests/test_utilities.py @@ -374,6 +374,16 @@ def test_get_sdk_dummy_version_with_flag_enabled(self): sdk_version = common.get_sdk_version(module) self.assertEqual(sdk_version, 'dummy') + def test_valid_script_file_name(self): + file_name = 'test.py' + valid_name = common.validate_script_file_name(file_name) + self.assertTrue(valid_name) + + def test_invalid_script_file_name(self): + file_name = 'test' + with self.assertRaises(common.InvalidFileNameError): + common.validate_script_file_name(file_name) + def _unset_feature_flag(self): try: os.environ.pop(TEST_FEATURE_FLAG) diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index 57fd0a4eb..c77414da6 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -226,6 +226,10 @@ def get_libraries_to_install(cls): def get_environment_variables(cls): pass + @classmethod + def get_script_name(cls): + pass + @classmethod def setUpClass(cls): script_dir = pathlib.Path(cls.get_script_dir()) @@ -234,6 +238,8 @@ def setUpClass(cls): 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") if is_envvar_true(PYAZURE_WEBHOST_DEBUG): cls.host_stdout = None