Skip to content

Configurable file name #1323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 55 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8f80e5d
configurable file name
hallvictoria Oct 10, 2023
7b36194
lint & error fix
hallvictoria Oct 11, 2023
be976ae
test fix
hallvictoria Oct 11, 2023
a2fc96b
Merge branch de into hallvictoria/configurable_file_name
hallvictoria Oct 11, 2023
9b3ce3a
fix merge lint errors
hallvictoria Oct 11, 2023
d0351ac
e2e and loader test
hallvictoria Oct 12, 2023
cc0f762
test linting errors
hallvictoria Oct 12, 2023
665b2ea
test linting errors
hallvictoria Oct 12, 2023
389e7d6
e2e file name test
hallvictoria Oct 13, 2023
b7bb44b
test lint checks
hallvictoria Oct 13, 2023
9150156
testing fixes
hallvictoria Oct 30, 2023
d187db7
moved tests to new file
hallvictoria Oct 30, 2023
11abe8b
fixed last test
hallvictoria Oct 30, 2023
f11a1bf
merge with dev
hallvictoria Nov 7, 2023
1c15b57
Merge branch 'dev' of https://github.com/Azure/azure-functions-python…
hallvictoria Nov 9, 2023
9452139
file name, conciseness
hallvictoria Nov 9, 2023
88c695e
Merge branch 'dev' of https://github.com/Azure/azure-functions-python…
hallvictoria Nov 13, 2023
f0a01b8
Merge branch 'dev' into hallvictoria/configurable_file_name
vrdmr Nov 14, 2023
a40c5ff
additional tests
hallvictoria Nov 20, 2023
479d3a3
Merge branch 'hallvictoria/configurable_file_name' of https://github.…
hallvictoria Nov 20, 2023
af861b8
merge with dev
hallvictoria Nov 21, 2023
5ce888a
slight test changes
hallvictoria Nov 22, 2023
0175c33
Merge branch 'dev' of https://github.com/Azure/azure-functions-python…
hallvictoria Nov 22, 2023
9cc2985
Merge branch 'dev' of https://github.com/Azure/azure-functions-python…
hallvictoria Nov 22, 2023
1bea4ae
updated tests
hallvictoria Nov 22, 2023
008ad85
fixing tests
hallvictoria Nov 22, 2023
43b21fb
refactored tests
hallvictoria Nov 27, 2023
24119e5
lint
hallvictoria Nov 27, 2023
ed2555c
moved invalid stein tests to broken tests file
hallvictoria Nov 28, 2023
acefbdd
fixed test check
hallvictoria Nov 28, 2023
11a74cb
sep broken stein tests
hallvictoria Nov 28, 2023
e3465a9
renamed, removed prints
hallvictoria Dec 1, 2023
23e1de5
Merge branch 'dev' of https://github.com/Azure/azure-functions-python…
hallvictoria Dec 4, 2023
e0fe45c
edited logs to now also show script file name
hallvictoria Dec 4, 2023
52b3784
formatting
hallvictoria Dec 5, 2023
7b927a2
Merge branch 'dev' into hallvictoria/configurable_file_name
hallvictoria Dec 11, 2023
4657b15
increase timeout
hallvictoria Dec 20, 2023
f36e463
Merge branch 'hallvictoria/configurable_file_name' of https://github.…
hallvictoria Dec 20, 2023
935c4d7
env vars method
hallvictoria Dec 20, 2023
c9c2035
printing env variables for failing tests
hallvictoria Dec 21, 2023
a1a7a78
cls stop env
hallvictoria Dec 21, 2023
6fee33f
class methods
hallvictoria Dec 21, 2023
97d1045
setting file name explicitly
hallvictoria Dec 21, 2023
0f66e14
reset script file name after test ends
hallvictoria Dec 21, 2023
0be62f8
lint + misc fixes
hallvictoria Dec 21, 2023
791ddea
refactor tests
hallvictoria Dec 22, 2023
447a699
added extra tests
hallvictoria Dec 22, 2023
cec0b35
debugging logs
hallvictoria Dec 22, 2023
adf54a9
global var
hallvictoria Dec 22, 2023
8fcd03d
added method to testutils
hallvictoria Dec 22, 2023
c13c997
validate file name + tests
hallvictoria Jan 11, 2024
d05f4ad
fixed tests
hallvictoria Jan 11, 2024
98023be
copyright
hallvictoria Jan 11, 2024
b29ac0d
Merge branch 'dev' into hallvictoria/configurable_file_name
hallvictoria Jan 11, 2024
9347e42
Merge branch 'dev' into hallvictoria/configurable_file_name
gavin-aguiar Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions azure_functions_worker/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39 = True
PYTHON_EXTENSIONS_RELOAD_FUNCTIONS = "PYTHON_EXTENSIONS_RELOAD_FUNCTIONS"

# new programming model default script file name
SCRIPT_FILE_NAME = "SCRIPT_FILE_NAME"
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
Expand Down
15 changes: 12 additions & 3 deletions azure_functions_worker/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
PYTHON_THREADPOOL_THREAD_COUNT_MIN,
PYTHON_ENABLE_DEBUG_LOGGING, SCRIPT_FILE_NAME,
SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, CUSTOMER_PACKAGES_PATH)
from .extension import ExtensionManager
from .logging import disable_console_logging, enable_console_logging
Expand Down Expand Up @@ -321,7 +322,10 @@ 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=SCRIPT_FILE_NAME,
default_value=SCRIPT_FILE_NAME_DEFAULT)
function_path = os.path.join(directory, script_file_name)

logger.info(
'Received WorkerMetadataRequest, request ID %s, directory: %s',
Expand All @@ -330,7 +334,7 @@ async def _handle__functions_metadata_request(self, request):
if not os.path.exists(function_path):
# Fallback to legacy model
logger.info("%s does not exist. "
"Switching to host indexing.", SCRIPT_FILE_NAME)
"Switching to host indexing.", script_file_name)
return protos.StreamingMessage(
request_id=request.request_id,
function_metadata_response=protos.FunctionMetadataResponse(
Expand Down Expand Up @@ -361,8 +365,13 @@ async def _handle__function_load_request(self, request):
function_id = func_request.function_id
function_metadata = func_request.metadata
function_name = function_metadata.name

script_file_name = get_app_setting(
setting=SCRIPT_FILE_NAME,
default_value=SCRIPT_FILE_NAME_DEFAULT)

function_path = os.path.join(function_metadata.directory,
SCRIPT_FILE_NAME)
script_file_name)

logger.info(
'Received WorkerLoadRequest, request ID %s, function_id: %s,'
Expand Down
8 changes: 6 additions & 2 deletions azure_functions_worker/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

from . import protos, functions
from .bindings.retrycontext import RetryPolicy
from .utils.common import get_app_setting
from .constants import MODULE_NOT_FOUND_TS_URL, SCRIPT_FILE_NAME, \
PYTHON_LANGUAGE_RUNTIME, RETRY_POLICY, CUSTOMER_PACKAGES_PATH
SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, \
CUSTOMER_PACKAGES_PATH, RETRY_POLICY
from .logging import logger
from .utils.wrappers import attach_message_to_exception

Expand Down Expand Up @@ -225,7 +227,9 @@ def index_function_app(function_path: str):
f"level function app instances are defined.")

if not app:
script_file_name = get_app_setting(setting=SCRIPT_FILE_NAME,
default=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()
Original file line number Diff line number Diff line change
@@ -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}")
124 changes: 124 additions & 0 deletions tests/endtoend/test_file_name_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import os
from unittest.mock import patch

import requests

from azure_functions_worker.constants import SCRIPT_FILE_NAME
from tests.utils import testutils

REQUEST_TIMEOUT_SEC = 5


class TestHttpFunctionsFileName(testutils.WebHostTestCase):
"""Test the native Http Trigger in the local webhost.

This test class will spawn a webhost from your <project_root>/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 setUpClass(cls):
cls.env_variables['SCRIPT_FILE_NAME'] = 'main.py'

os_environ = os.environ.copy()
os_environ.update(cls.env_variables)

cls._patch_environ = patch.dict('os.environ', os_environ)
cls._patch_environ.start()
super().setUpClass()

def tearDown(self):
super().tearDown()
self._patch_environ.stop()

@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_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)

@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):
os.environ.update({SCRIPT_FILE_NAME: "main.py"})
self.assertIsNotNone(os.environ.get(SCRIPT_FILE_NAME))
self.assertEqual(os.environ.get(SCRIPT_FILE_NAME),
'main.py')
42 changes: 41 additions & 1 deletion tests/unittests/test_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
from tests.utils import testutils
from azure_functions_worker.constants import PYTHON_THREADPOOL_THREAD_COUNT, \
PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, \
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, PYTHON_THREADPOOL_THREAD_COUNT_MIN
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, \
PYTHON_THREADPOOL_THREAD_COUNT_MIN, \
SCRIPT_FILE_NAME, SCRIPT_FILE_NAME_DEFAULT

SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro",
"releaselevel", "serial"])
Expand Down Expand Up @@ -667,3 +669,41 @@ async def test_dispatcher_load_modules_con_app_placeholder_disabled(self):
"worker_dependencies_path: , customer_dependencies_path: , "
"working_directory: , Linux Consumption: True,"
" Placeholder: False", logs)


class TestConfigurableFileName(testutils.AsyncTestCase):

def setUp(self):
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_FUNCTIONS_DIR)
self._default_file_name: Optional[str] = SCRIPT_FILE_NAME_DEFAULT
self._new_file_name: str = 'test.py'
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_dispatcher_default_file_name(self):
"""
Test the default file name
"""
os.environ.update({SCRIPT_FILE_NAME: self._default_file_name})
self.assertIsNotNone(os.environ.get(SCRIPT_FILE_NAME))
self.assertEqual(os.environ.get(SCRIPT_FILE_NAME),
self._default_file_name)

# test changing value
async def test_dispatcher_new_file_name(self):
"""
Test the updated file name
"""
os.environ.update({SCRIPT_FILE_NAME: self._new_file_name})
self.assertIsNotNone(os.environ.get(SCRIPT_FILE_NAME))
self.assertEqual(os.environ.get(SCRIPT_FILE_NAME),
self._new_file_name)
26 changes: 26 additions & 0 deletions tests/unittests/test_loader.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,6 +13,8 @@
from azure.functions.decorators.timer import TimerTrigger

from azure_functions_worker import functions
from azure_functions_worker.constants import SCRIPT_FILE_NAME, \
SCRIPT_FILE_NAME_DEFAULT
from azure_functions_worker.loader import build_retry_protos
from tests.utils import testutils

Expand Down Expand Up @@ -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 = 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({SCRIPT_FILE_NAME: self.file_name})
self.assertIsNotNone(os.environ.get(SCRIPT_FILE_NAME))
self.assertEqual(os.environ.get(SCRIPT_FILE_NAME),
'function_app.py')