From 6b989e0cdcee58c840fef18ef99eb09e64dfb63e Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 24 Apr 2026 14:25:36 -0500 Subject: [PATCH 01/15] initial commit for supporting runtime base --- workers/proxy_worker/dispatcher.py | 113 ++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 20 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 0276a044..2272e824 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -14,6 +14,7 @@ from typing import Any, Optional import grpc +import importlib from proxy_worker import protos from proxy_worker.logging import ( @@ -417,28 +418,100 @@ def stop(self) -> None: @staticmethod def reload_library_worker(directory: str): + """ + Load the appropriate runtime using the base package pattern. + + This uses the runtime base package to automatically discover which + runtime is loaded. Runtimes auto-register via metaclass when imported. + """ global _library_worker, _library_worker_has_cv - v2_scriptfile = os.path.join(directory, get_script_file_name()) - if os.path.exists(v2_scriptfile): - try: - import azure_functions_runtime # NoQA - _library_worker = azure_functions_runtime - _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') - logger.debug("azure_functions_runtime import succeeded: %s", - _library_worker.__file__) - except ImportError: - logger.debug("azure_functions_runtime library not found: : %s", - traceback.format_exc()) - else: - try: - import azure_functions_runtime_v1 # NoQA - _library_worker = azure_functions_runtime_v1 + + try: + # Import runtime base package + from importlib.metadata import entry_points + import azurefunctions.extensions.base as runtime_base + + # Discover all installed runtime packages via entry points + available_runtimes = entry_points(group='azurefunctions.runtimes') + + for ep in available_runtimes: + try: + # Load the entry point (triggers import and metaclass registration) + ep.load() + logger.debug(f"Loaded runtime entry point: {ep.name}") + except Exception as e: + logger.debug(f"Could not load runtime {ep.name}: {e}") + continue + + # Check if a runtime was registered + if runtime_base.RuntimeFeatureChecker.runtime_loaded(): + # Get the registered runtime module (e.g., "azure_functions_fastapi.runtime") + runtime_module_name = runtime_base.RuntimeTrackerMeta.get_module() + runtime_name = runtime_base.RuntimeTrackerMeta.get_runtime_name() + + logger.info("Runtime registered: %s (module: %s)", + runtime_name, runtime_module_name) + + # Extract the package name (e.g., "azure_functions_fastapi" from "azure_functions_fastapi.runtime") + # The package is everything before ".runtime" + if '.runtime' in runtime_module_name: + package_name = runtime_module_name.rsplit('.runtime', 1)[0] + else: + # Fallback: use the first part of the module name + package_name = runtime_module_name.split('.')[0] + + logger.debug("Importing runtime package: %s", package_name) + + # Import the top-level runtime package (which exports the public API) + runtime_module = importlib.import_module(package_name) + _library_worker = runtime_module _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') - logger.debug("azure_functions_runtime_v1 import succeeded: %s", - _library_worker.__file__) # type: ignore[union-attr] - except ImportError: - logger.debug("azure_functions_runtime_v1 library not found: %s", - traceback.format_exc()) + + logger.info("Using runtime: %s, version: %s", + runtime_name, + getattr(_library_worker, 'VERSION', 'unknown')) + else: + # Fallback: No runtime registered via base package + # Use traditional detection (backward compatibility) + logger.debug("No runtime registered via base package, using fallback") + v2_scriptfile = os.path.join(directory, get_script_file_name()) + if os.path.exists(v2_scriptfile): + try: + import azure_functions_runtime # NoQA + _library_worker = azure_functions_runtime + _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + logger.debug("azure_functions_runtime import succeeded: %s", + _library_worker.__file__) + except ImportError: + logger.debug("azure_functions_runtime library not found") + else: + try: + import azure_functions_runtime_v1 # NoQA + _library_worker = azure_functions_runtime_v1 + _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + logger.debug("azure_functions_runtime_v1 import succeeded: %s", + _library_worker.__file__) # type: ignore[union-attr] + except ImportError: + logger.debug("azure_functions_runtime_v1 library not found") + + except ImportError as e: + logger.error("Failed to import runtime base package: %s", e) + # Fallback to traditional method + v2_scriptfile = os.path.join(directory, get_script_file_name()) + if os.path.exists(v2_scriptfile): + try: + import azure_functions_runtime # NoQA + _library_worker = azure_functions_runtime + _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + except ImportError: + pass + else: + try: + import azure_functions_runtime_v1 # NoQA + _library_worker = azure_functions_runtime_v1 + _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + except ImportError: + pass async def _handle__worker_init_request(self, request): logger.info('Received WorkerInitRequest, ' From fb6b6e8d8fcd2c7e15ed7617b6bcb15c12003c8c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 24 Apr 2026 14:45:02 -0500 Subject: [PATCH 02/15] lint + add tests --- workers/proxy_worker/dispatcher.py | 82 ++-- .../tests/unittest_proxy/test_dispatcher.py | 382 ++++++++++++++++++ 2 files changed, 431 insertions(+), 33 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 2272e824..5d4dc638 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -420,80 +420,94 @@ def stop(self) -> None: def reload_library_worker(directory: str): """ Load the appropriate runtime using the base package pattern. - + This uses the runtime base package to automatically discover which runtime is loaded. Runtimes auto-register via metaclass when imported. """ global _library_worker, _library_worker_has_cv - + try: # Import runtime base package from importlib.metadata import entry_points import azurefunctions.extensions.base as runtime_base - + # Discover all installed runtime packages via entry points available_runtimes = entry_points(group='azurefunctions.runtimes') - + for ep in available_runtimes: try: - # Load the entry point (triggers import and metaclass registration) + # Load the entry point (triggers import and + # metaclass registration) ep.load() logger.debug(f"Loaded runtime entry point: {ep.name}") except Exception as e: logger.debug(f"Could not load runtime {ep.name}: {e}") continue - + # Check if a runtime was registered if runtime_base.RuntimeFeatureChecker.runtime_loaded(): - # Get the registered runtime module (e.g., "azure_functions_fastapi.runtime") - runtime_module_name = runtime_base.RuntimeTrackerMeta.get_module() - runtime_name = runtime_base.RuntimeTrackerMeta.get_runtime_name() - - logger.info("Runtime registered: %s (module: %s)", - runtime_name, runtime_module_name) - - # Extract the package name (e.g., "azure_functions_fastapi" from "azure_functions_fastapi.runtime") + # Get the registered runtime module + # (e.g., "azure_functions_fastapi.runtime") + runtime_module_name = ( + runtime_base.RuntimeTrackerMeta.get_module()) + runtime_name = ( + runtime_base.RuntimeTrackerMeta.get_runtime_name()) + + logger.info("Runtime registered: %s (module: %s)", + runtime_name, runtime_module_name) + + # Extract the package name (e.g., "azure_functions_fastapi" + # from "azure_functions_fastapi.runtime") # The package is everything before ".runtime" if '.runtime' in runtime_module_name: package_name = runtime_module_name.rsplit('.runtime', 1)[0] else: # Fallback: use the first part of the module name package_name = runtime_module_name.split('.')[0] - + logger.debug("Importing runtime package: %s", package_name) - - # Import the top-level runtime package (which exports the public API) + + # Import the top-level runtime package (which exports + # the public API) runtime_module = importlib.import_module(package_name) _library_worker = runtime_module - _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') - + _library_worker_has_cv = hasattr(_library_worker, + 'invocation_id_cv') + logger.info("Using runtime: %s, version: %s", - runtime_name, - getattr(_library_worker, 'VERSION', 'unknown')) + runtime_name, + getattr(_library_worker, 'VERSION', 'unknown')) else: # Fallback: No runtime registered via base package # Use traditional detection (backward compatibility) - logger.debug("No runtime registered via base package, using fallback") + logger.debug( + "No runtime registered via base package, using fallback") v2_scriptfile = os.path.join(directory, get_script_file_name()) if os.path.exists(v2_scriptfile): try: import azure_functions_runtime # NoQA _library_worker = azure_functions_runtime - _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') - logger.debug("azure_functions_runtime import succeeded: %s", - _library_worker.__file__) + _library_worker_has_cv = hasattr( + _library_worker, 'invocation_id_cv') + logger.debug( + "azure_functions_runtime import succeeded: %s", + _library_worker.__file__) except ImportError: - logger.debug("azure_functions_runtime library not found") + logger.debug( + "azure_functions_runtime library not found") else: try: import azure_functions_runtime_v1 # NoQA _library_worker = azure_functions_runtime_v1 - _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') - logger.debug("azure_functions_runtime_v1 import succeeded: %s", - _library_worker.__file__) # type: ignore[union-attr] + _library_worker_has_cv = hasattr( + _library_worker, 'invocation_id_cv') + logger.debug( + "azure_functions_runtime_v1 import succeeded: %s", + _library_worker.__file__) # type: ignore except ImportError: - logger.debug("azure_functions_runtime_v1 library not found") - + logger.debug( + "azure_functions_runtime_v1 library not found") + except ImportError as e: logger.error("Failed to import runtime base package: %s", e) # Fallback to traditional method @@ -502,14 +516,16 @@ def reload_library_worker(directory: str): try: import azure_functions_runtime # NoQA _library_worker = azure_functions_runtime - _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + _library_worker_has_cv = hasattr( + _library_worker, 'invocation_id_cv') except ImportError: pass else: try: import azure_functions_runtime_v1 # NoQA _library_worker = azure_functions_runtime_v1 - _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + _library_worker_has_cv = hasattr( + _library_worker, 'invocation_id_cv') except ImportError: pass diff --git a/workers/tests/unittest_proxy/test_dispatcher.py b/workers/tests/unittest_proxy/test_dispatcher.py index eff94de0..d0751f43 100644 --- a/workers/tests/unittest_proxy/test_dispatcher.py +++ b/workers/tests/unittest_proxy/test_dispatcher.py @@ -774,3 +774,385 @@ async def test_invocation_request_handles_no_current_task( # Verify response self.assertEqual(result, "mocked_stream_response") + + +class TestReloadLibraryWorkerWithRuntimeBase(unittest.TestCase): + """Test suite for reload_library_worker with runtime base package pattern""" + + def setUp(self): + """Clear library worker state before each test""" + import proxy_worker.dispatcher as dispatcher_module + dispatcher_module._library_worker = None + dispatcher_module._library_worker_has_cv = False + + def tearDown(self): + """Clean up after each test""" + import proxy_worker.dispatcher as dispatcher_module + dispatcher_module._library_worker = None + dispatcher_module._library_worker_has_cv = False + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_runtime_base_success_with_runtime_suffix( + self, mock_entry_points, mock_import_module, mock_logger): + """Test successful runtime loading via base package with .runtime suffix""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup mock entry point + mock_ep = Mock() + mock_ep.name = "fastapi" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( + "azure_functions_fastapi.runtime") + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "fastapi" + + # Setup mock runtime module + mock_runtime_module = Mock() + mock_runtime_module.VERSION = "2.0.0" + mock_runtime_module.invocation_id_cv = Mock() + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify entry points were queried + mock_entry_points.assert_called_once_with(group='azurefunctions.runtimes') + + # Verify entry point was loaded + mock_ep.load.assert_called_once() + + # Verify runtime module was imported (package name extracted correctly) + mock_import_module.assert_called_once_with("azure_functions_fastapi") + + # Verify library worker was set + self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) + self.assertTrue(dispatcher_module._library_worker_has_cv) + + # Verify logging + mock_logger.info.assert_any_call( + "Runtime registered: %s (module: %s)", + "fastapi", "azure_functions_fastapi.runtime" + ) + mock_logger.info.assert_any_call( + "Using runtime: %s, version: %s", + "fastapi", "2.0.0" + ) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_runtime_base_success_without_runtime_suffix( + self, mock_entry_points, mock_import_module, mock_logger): + """Test successful runtime loading when module name has no .runtime suffix""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup mock entry point + mock_ep = Mock() + mock_ep.name = "custom_runtime" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = "custom_package" + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "custom" + + # Setup mock runtime module + mock_runtime_module = Mock() + mock_runtime_module.VERSION = "1.5.0" + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify package name extraction (fallback to first part) + mock_import_module.assert_called_once_with("custom_package") + + # Verify library worker was set + self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_runtime_base_entry_point_load_exception( + self, mock_entry_points, mock_logger): + """Test handling of exceptions when loading entry points""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup mock entry points - first fails, second succeeds + mock_ep1 = Mock() + mock_ep1.name = "broken_runtime" + mock_ep1.load.side_effect = Exception("Load failed") + + mock_ep2 = Mock() + mock_ep2.name = "good_runtime" + mock_ep2.load = Mock() + + mock_entry_points.return_value = [mock_ep1, mock_ep2] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = False + + # Patch the runtime base import and traditional fallback + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + with patch("proxy_worker.dispatcher.os.path.exists", + return_value=False): + with patch("builtins.__import__", + side_effect=ImportError("No fallback runtime")): + dispatcher_module.Dispatcher.reload_library_worker( + "/home/site/wwwroot") + + # Verify both entry points were attempted + mock_ep1.load.assert_called_once() + mock_ep2.load.assert_called_once() + + # Verify exception was logged + mock_logger.debug.assert_any_call( + "Could not load runtime %s: %s", + "broken_runtime", + ANY + ) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_runtime_base_no_runtime_registered_fallback_to_v2( + self, mock_import, mock_exists, mock_entry_points, mock_logger): + """Test fallback to traditional v2 when no runtime registered""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup mock entry points (none succeed in registration) + mock_ep = Mock() + mock_ep.name = "test_runtime" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module - no runtime registered + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = False + + # Mock traditional fallback + mock_exists.return_value = True # v2 script exists + + mock_runtime_v2 = types.SimpleNamespace( + __file__="azure_functions_runtime.py", + invocation_id_cv=Mock() + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime": + return mock_runtime_v2 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify fallback logging + mock_logger.debug.assert_any_call( + "No runtime registered via base package, using fallback" + ) + mock_logger.debug.assert_any_call( + "azure_functions_runtime import succeeded: %s", + "azure_functions_runtime.py" + ) + + # Verify library worker was set to v2 + self.assertEqual(dispatcher_module._library_worker, mock_runtime_v2) + self.assertTrue(dispatcher_module._library_worker_has_cv) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_runtime_base_no_runtime_registered_fallback_to_v1( + self, mock_import, mock_exists, mock_entry_points, mock_logger): + """Test fallback to traditional v1 when no runtime registered + and v2 script absent""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup mock entry points + mock_entry_points.return_value = [] + + # Setup mock runtime base module - no runtime registered + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = False + + # Mock traditional fallback - v2 script doesn't exist + mock_exists.return_value = False + + mock_runtime_v1 = types.SimpleNamespace( + __file__="azure_functions_runtime_v1.py" + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime_v1": + return mock_runtime_v1 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify fallback logging + mock_logger.debug.assert_any_call( + "azure_functions_runtime_v1 import succeeded: %s", + "azure_functions_runtime_v1.py" + ) + + # Verify library worker was set to v1 + self.assertEqual(dispatcher_module._library_worker, mock_runtime_v1) + self.assertFalse(dispatcher_module._library_worker_has_cv) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_runtime_base_import_error_fallback_to_traditional( + self, mock_import, mock_exists, mock_logger): + """Test fallback when runtime base package import fails""" + import proxy_worker.dispatcher as dispatcher_module + + # Mock runtime base import failure + def custom_import(name, *args, **kwargs): + if name == "importlib.metadata" or "azurefunctions.extensions.base" in name: + raise ImportError("Runtime base not installed") + if name == "azure_functions_runtime": + mock_runtime = types.SimpleNamespace( + __file__="azure_functions_runtime.py", + invocation_id_cv=Mock() + ) + return mock_runtime + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + mock_exists.return_value = True + + # This will trigger the import in reload_library_worker + # The ImportError should be caught and fallback should occur + with patch("proxy_worker.dispatcher.importlib") as mock_importlib_module: + # Make the entry_points import raise ImportError + mock_importlib_module.metadata.entry_points.side_effect = ( + ImportError("Runtime base not installed")) + + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify error was logged + mock_logger.error.assert_called_once() + self.assertIn("Failed to import runtime base package", + str(mock_logger.error.call_args)) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_runtime_base_multiple_entry_points( + self, mock_entry_points, mock_import_module, mock_logger): + """Test handling of multiple entry points (only first + registered runtime used)""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup multiple mock entry points + mock_ep1 = Mock() + mock_ep1.name = "runtime1" + mock_ep1.load = Mock() + + mock_ep2 = Mock() + mock_ep2.name = "runtime2" + mock_ep2.load = Mock() + + mock_entry_points.return_value = [mock_ep1, mock_ep2] + + # Setup mock runtime base module - runtime registered after first load + call_count = [0] + + def runtime_loaded_side_effect(): + call_count[0] += 1 + return call_count[0] == 1 # True after first load + + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.side_effect = ( + runtime_loaded_side_effect) + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( + "runtime1_package.runtime") + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "runtime1" + + # Setup mock runtime module + mock_runtime_module = Mock() + mock_runtime_module.VERSION = "1.0.0" + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify first entry point was loaded + mock_ep1.load.assert_called_once() + + # Note: In the actual implementation, all entry points are loaded + # but only the first registered runtime is used + # This test documents that behavior + self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_runtime_base_version_unknown( + self, mock_entry_points, mock_import_module, mock_logger): + """Test handling when runtime module has no VERSION attribute""" + import proxy_worker.dispatcher as dispatcher_module + + # Setup mock entry point + mock_ep = Mock() + mock_ep.name = "no_version_runtime" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( + "no_version_package.runtime") + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = ( + "no_version") + + # Setup mock runtime module without VERSION + mock_runtime_module = Mock(spec=[]) # No attributes + del mock_runtime_module.VERSION # Ensure VERSION doesn't exist + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify "unknown" is used when VERSION is missing + mock_logger.info.assert_any_call( + "Using runtime: %s, version: %s", + "no_version", "unknown" + ) From d0bccced656bddcbb66f3ffb085777e659eb9ecc Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 24 Apr 2026 15:46:17 -0500 Subject: [PATCH 03/15] lint --- workers/proxy_worker/dispatcher.py | 153 +++++++++++++---------------- 1 file changed, 67 insertions(+), 86 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 5d4dc638..c6ba9adb 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import asyncio +import importlib import logging import os import queue @@ -11,10 +12,10 @@ import typing from asyncio import AbstractEventLoop from dataclasses import dataclass +from importlib.metadata import entry_points from typing import Any, Optional import grpc -import importlib from proxy_worker import protos from proxy_worker.logging import ( @@ -422,95 +423,64 @@ def reload_library_worker(directory: str): Load the appropriate runtime using the base package pattern. This uses the runtime base package to automatically discover which - runtime is loaded. Runtimes auto-register via metaclass when imported. + runtime is loaded. + + If no runtime is registered via the base package, it falls back to + the traditional detection method. """ global _library_worker, _library_worker_has_cv - try: - # Import runtime base package - from importlib.metadata import entry_points - import azurefunctions.extensions.base as runtime_base + # Import base package + import azurefunctions.extensions.base as runtime_base - # Discover all installed runtime packages via entry points - available_runtimes = entry_points(group='azurefunctions.runtimes') + # Discover all installed runtime packages via entry points + available_runtimes = entry_points(group='azurefunctions.runtimes') - for ep in available_runtimes: - try: - # Load the entry point (triggers import and - # metaclass registration) - ep.load() - logger.debug(f"Loaded runtime entry point: {ep.name}") - except Exception as e: - logger.debug(f"Could not load runtime {ep.name}: {e}") - continue - - # Check if a runtime was registered - if runtime_base.RuntimeFeatureChecker.runtime_loaded(): - # Get the registered runtime module - # (e.g., "azure_functions_fastapi.runtime") - runtime_module_name = ( - runtime_base.RuntimeTrackerMeta.get_module()) - runtime_name = ( - runtime_base.RuntimeTrackerMeta.get_runtime_name()) - - logger.info("Runtime registered: %s (module: %s)", - runtime_name, runtime_module_name) - - # Extract the package name (e.g., "azure_functions_fastapi" - # from "azure_functions_fastapi.runtime") - # The package is everything before ".runtime" - if '.runtime' in runtime_module_name: - package_name = runtime_module_name.rsplit('.runtime', 1)[0] - else: - # Fallback: use the first part of the module name - package_name = runtime_module_name.split('.')[0] - - logger.debug("Importing runtime package: %s", package_name) - - # Import the top-level runtime package (which exports - # the public API) - runtime_module = importlib.import_module(package_name) - _library_worker = runtime_module - _library_worker_has_cv = hasattr(_library_worker, - 'invocation_id_cv') - - logger.info("Using runtime: %s, version: %s", - runtime_name, - getattr(_library_worker, 'VERSION', 'unknown')) + for ep in available_runtimes: + try: + # Load the entry point (triggers import and + # metaclass registration) + ep.load() + logger.debug(f"Loaded runtime entry point: {ep.name}") + except Exception as e: + logger.debug(f"Could not load runtime {ep.name}: {e}") + continue + + # Check if a runtime was registered + if runtime_base.RuntimeFeatureChecker.runtime_loaded(): + # Get the registered runtime module + # (e.g., "azure_functions_fastapi.runtime") + runtime_module_name = ( + runtime_base.RuntimeTrackerMeta.get_module()) + runtime_name = ( + runtime_base.RuntimeTrackerMeta.get_runtime_name()) + + logger.debug("Runtime registered: %s (module: %s)", + runtime_name, runtime_module_name) + + # Extract the package name (e.g., "azure_functions_fastapi" + # from "azure_functions_fastapi.runtime") + # The package is everything before ".runtime" + if '.runtime' in runtime_module_name: + package_name = runtime_module_name.rsplit('.runtime', 1)[0] else: - # Fallback: No runtime registered via base package - # Use traditional detection (backward compatibility) - logger.debug( - "No runtime registered via base package, using fallback") - v2_scriptfile = os.path.join(directory, get_script_file_name()) - if os.path.exists(v2_scriptfile): - try: - import azure_functions_runtime # NoQA - _library_worker = azure_functions_runtime - _library_worker_has_cv = hasattr( - _library_worker, 'invocation_id_cv') - logger.debug( - "azure_functions_runtime import succeeded: %s", - _library_worker.__file__) - except ImportError: - logger.debug( - "azure_functions_runtime library not found") - else: - try: - import azure_functions_runtime_v1 # NoQA - _library_worker = azure_functions_runtime_v1 - _library_worker_has_cv = hasattr( - _library_worker, 'invocation_id_cv') - logger.debug( - "azure_functions_runtime_v1 import succeeded: %s", - _library_worker.__file__) # type: ignore - except ImportError: - logger.debug( - "azure_functions_runtime_v1 library not found") - - except ImportError as e: - logger.error("Failed to import runtime base package: %s", e) - # Fallback to traditional method + # Fallback: use the first part of the module name + package_name = runtime_module_name.split('.')[0] + + logger.debug("Importing runtime package: %s", package_name) + + # Import the top-level runtime package (which exports + # the public API) + runtime_module = importlib.import_module(package_name) + _library_worker = runtime_module + _library_worker_has_cv = hasattr(_library_worker, + 'invocation_id_cv') + + else: + # Fallback: No runtime registered via base package + # Use traditional detection + logger.debug( + "No runtime registered via base package, using fallback") v2_scriptfile = os.path.join(directory, get_script_file_name()) if os.path.exists(v2_scriptfile): try: @@ -518,16 +488,27 @@ def reload_library_worker(directory: str): _library_worker = azure_functions_runtime _library_worker_has_cv = hasattr( _library_worker, 'invocation_id_cv') + logger.debug( + "azure_functions_runtime import succeeded: %s", + _library_worker.__file__) except ImportError: - pass + logger.debug( + "azure_functions_runtime library not found") else: try: import azure_functions_runtime_v1 # NoQA _library_worker = azure_functions_runtime_v1 _library_worker_has_cv = hasattr( _library_worker, 'invocation_id_cv') + logger.debug( + "azure_functions_runtime_v1 import succeeded: %s", + _library_worker.__file__) # type: ignore except ImportError: - pass + logger.debug( + "azure_functions_runtime_v1 library not found") + logger.info("Using runtime: %s, version: %s", + _library_worker, + getattr(_library_worker, 'VERSION', 'unknown')) async def _handle__worker_init_request(self, request): logger.info('Received WorkerInitRequest, ' From f2c0c7dd9c8993f9ee8adae2fe128bd05f026798 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 27 Apr 2026 14:35:22 -0500 Subject: [PATCH 04/15] wrap around app setting --- workers/proxy_worker/dispatcher.py | 89 ++-- workers/proxy_worker/utils/constants.py | 2 + .../test_dispatcher_agent_runtime.py | 390 ++++++++++++++++++ 3 files changed, 437 insertions(+), 44 deletions(-) create mode 100644 workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index c6ba9adb..8884052b 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -32,6 +32,7 @@ check_python_eol ) from proxy_worker.utils.constants import ( + PYTHON_ENABLE_AGENT_RUNTIME, PYTHON_ENABLE_DEBUG_LOGGING, ) from proxy_worker.version import VERSION @@ -430,52 +431,52 @@ def reload_library_worker(directory: str): """ global _library_worker, _library_worker_has_cv - # Import base package - import azurefunctions.extensions.base as runtime_base + if is_envvar_true(PYTHON_ENABLE_AGENT_RUNTIME): + # Import base package + import azurefunctions.extensions.base as runtime_base - # Discover all installed runtime packages via entry points - available_runtimes = entry_points(group='azurefunctions.runtimes') - - for ep in available_runtimes: - try: - # Load the entry point (triggers import and - # metaclass registration) - ep.load() - logger.debug(f"Loaded runtime entry point: {ep.name}") - except Exception as e: - logger.debug(f"Could not load runtime {ep.name}: {e}") - continue - - # Check if a runtime was registered - if runtime_base.RuntimeFeatureChecker.runtime_loaded(): - # Get the registered runtime module - # (e.g., "azure_functions_fastapi.runtime") - runtime_module_name = ( - runtime_base.RuntimeTrackerMeta.get_module()) - runtime_name = ( - runtime_base.RuntimeTrackerMeta.get_runtime_name()) - - logger.debug("Runtime registered: %s (module: %s)", - runtime_name, runtime_module_name) - - # Extract the package name (e.g., "azure_functions_fastapi" - # from "azure_functions_fastapi.runtime") - # The package is everything before ".runtime" - if '.runtime' in runtime_module_name: - package_name = runtime_module_name.rsplit('.runtime', 1)[0] - else: - # Fallback: use the first part of the module name - package_name = runtime_module_name.split('.')[0] - - logger.debug("Importing runtime package: %s", package_name) - - # Import the top-level runtime package (which exports - # the public API) - runtime_module = importlib.import_module(package_name) - _library_worker = runtime_module - _library_worker_has_cv = hasattr(_library_worker, - 'invocation_id_cv') + # Discover all installed runtime packages via entry points + available_runtimes = entry_points(group='azurefunctions.runtimes') + for ep in available_runtimes: + try: + # Load the entry point (triggers import and + # metaclass registration) + ep.load() + logger.debug(f"Loaded runtime entry point: {ep.name}") + except Exception as e: + logger.debug(f"Could not load runtime {ep.name}: {e}") + continue + + # Check if a runtime was registered + if runtime_base.RuntimeFeatureChecker.runtime_loaded(): + # Get the registered runtime module + # (e.g., "azure_functions_fastapi.runtime") + runtime_module_name = ( + runtime_base.RuntimeTrackerMeta.get_module()) + runtime_name = ( + runtime_base.RuntimeTrackerMeta.get_runtime_name()) + + logger.debug("Runtime registered: %s (module: %s)", + runtime_name, runtime_module_name) + + # Extract the package name (e.g., "azure_functions_fastapi" + # from "azure_functions_fastapi.runtime") + # The package is everything before ".runtime" + if '.runtime' in runtime_module_name: + package_name = runtime_module_name.rsplit('.runtime', 1)[0] + else: + # Fallback: use the first part of the module name + package_name = runtime_module_name.split('.')[0] + + logger.debug("Importing runtime package: %s", package_name) + + # Import the top-level runtime package (which exports + # the public API) + runtime_module = importlib.import_module(package_name) + _library_worker = runtime_module + _library_worker_has_cv = hasattr(_library_worker, + 'invocation_id_cv') else: # Fallback: No runtime registered via base package # Use traditional detection diff --git a/workers/proxy_worker/utils/constants.py b/workers/proxy_worker/utils/constants.py index 3d6d0c64..409bad11 100644 --- a/workers/proxy_worker/utils/constants.py +++ b/workers/proxy_worker/utils/constants.py @@ -15,6 +15,8 @@ PYTHON_SCRIPT_FILE_NAME = "PYTHON_SCRIPT_FILE_NAME" PYTHON_SCRIPT_FILE_NAME_DEFAULT = "function_app.py" +PYTHON_ENABLE_AGENT_RUNTIME = "PYTHON_ENABLE_AGENT_RUNTIME" + # EOL Dates PYTHON_EOL_DATES = { '3.13': '2029-10', diff --git a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py new file mode 100644 index 00000000..6acf1c5d --- /dev/null +++ b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py @@ -0,0 +1,390 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +Unit tests for PYTHON_ENABLE_AGENT_RUNTIME environment variable logic +in dispatcher.reload_library_worker +""" +import builtins +import os +import types +import unittest +from unittest.mock import Mock, patch + +from proxy_worker.utils.constants import PYTHON_ENABLE_AGENT_RUNTIME + + +_real_import = builtins.__import__ + + +class TestReloadLibraryWorkerAgentRuntime(unittest.TestCase): + """Test suite for reload_library_worker with PYTHON_ENABLE_AGENT_RUNTIME env var""" + + def setUp(self): + """Clear library worker state and environment before each test""" + import proxy_worker.dispatcher as dispatcher_module + dispatcher_module._library_worker = None + dispatcher_module._library_worker_has_cv = False + + # Clear environment variable + if PYTHON_ENABLE_AGENT_RUNTIME in os.environ: + del os.environ[PYTHON_ENABLE_AGENT_RUNTIME] + + def tearDown(self): + """Clean up after each test""" + import proxy_worker.dispatcher as dispatcher_module + dispatcher_module._library_worker = None + dispatcher_module._library_worker_has_cv = False + + # Clear environment variable + if PYTHON_ENABLE_AGENT_RUNTIME in os.environ: + del os.environ[PYTHON_ENABLE_AGENT_RUNTIME] + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_agent_runtime_enabled_with_true_string( + self, mock_entry_points, mock_import_module, mock_logger): + """Test that entry points are used when PYTHON_ENABLE_AGENT_RUNTIME='true'""" + import proxy_worker.dispatcher as dispatcher_module + + # Set environment variable to enable agent runtime + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "true" + + # Setup mock entry point + mock_ep = Mock() + mock_ep.name = "fastapi" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( + "azure_functions_fastapi.runtime") + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "fastapi" + + # Setup mock runtime module + mock_runtime_module = Mock() + mock_runtime_module.VERSION = "2.0.0" + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify entry points were queried + mock_entry_points.assert_called_once_with(group='azurefunctions.runtimes') + + # Verify entry point was loaded + mock_ep.load.assert_called_once() + + # Verify runtime module was imported + mock_import_module.assert_called_once_with("azure_functions_fastapi") + + # Verify library worker was set + self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_agent_runtime_enabled_with_1_string( + self, mock_entry_points, mock_import_module, mock_logger): + """Test that entry points are used when PYTHON_ENABLE_AGENT_RUNTIME='1'""" + import proxy_worker.dispatcher as dispatcher_module + + # Set environment variable to enable agent runtime + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "1" + + # Setup mock entry point + mock_ep = Mock() + mock_ep.name = "test_runtime" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( + "test_package.runtime") + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = ( + "test") + + # Setup mock runtime module + mock_runtime_module = Mock() + mock_runtime_module.VERSION = "1.0.0" + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify entry points were queried (agent runtime path was used) + mock_entry_points.assert_called_once_with( + group='azurefunctions.runtimes') + + # Verify library worker was set + self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_agent_runtime_disabled_uses_traditional_detection( + self, mock_import, mock_exists, mock_entry_points, mock_logger): + """Test that traditional detection is used when + PYTHON_ENABLE_AGENT_RUNTIME is not set""" + import proxy_worker.dispatcher as dispatcher_module + + # Do NOT set PYTHON_ENABLE_AGENT_RUNTIME - should use traditional detection + + # Mock traditional fallback + mock_exists.return_value = True # v2 script exists + + mock_runtime_v2 = types.SimpleNamespace( + __file__="azure_functions_runtime.py", + invocation_id_cv=Mock(), + VERSION="1.10.0" + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime": + return mock_runtime_v2 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify entry points were NOT queried + mock_entry_points.assert_not_called() + + # Verify library worker was set to v2 via traditional detection + self.assertEqual(dispatcher_module._library_worker, mock_runtime_v2) + self.assertTrue(dispatcher_module._library_worker_has_cv) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_agent_runtime_disabled_with_false_string( + self, mock_import, mock_exists, mock_entry_points, mock_logger): + """Test that traditional detection is used when + PYTHON_ENABLE_AGENT_RUNTIME='false'""" + import proxy_worker.dispatcher as dispatcher_module + + # Set environment variable to disable agent runtime + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "false" + + # Mock traditional fallback + mock_exists.return_value = False # v2 script doesn't exist + + mock_runtime_v1 = types.SimpleNamespace( + __file__="azure_functions_runtime_v1.py", + VERSION="1.0.0" + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime_v1": + return mock_runtime_v1 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify entry points were NOT queried + mock_entry_points.assert_not_called() + + # Verify library worker was set to v1 via traditional detection + self.assertEqual(dispatcher_module._library_worker, mock_runtime_v1) + self.assertFalse(dispatcher_module._library_worker_has_cv) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_agent_runtime_disabled_with_0_string( + self, mock_import, mock_exists, mock_entry_points, mock_logger): + """Test that traditional detection is used when + PYTHON_ENABLE_AGENT_RUNTIME='0'""" + import proxy_worker.dispatcher as dispatcher_module + + # Set environment variable to disable agent runtime + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "0" + + # Mock traditional fallback + mock_exists.return_value = True + + mock_runtime_v2 = types.SimpleNamespace( + __file__="azure_functions_runtime.py", + invocation_id_cv=Mock(), + VERSION="1.11.0" + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime": + return mock_runtime_v2 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify entry points were NOT queried + mock_entry_points.assert_not_called() + + # Verify library worker was set via traditional detection + self.assertEqual(dispatcher_module._library_worker, mock_runtime_v2) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_agent_runtime_enabled_no_runtime_registered_fallback_still_used( + self, mock_entry_points, mock_logger): + """Test that when agent runtime is enabled but no runtime registers, + we still use the traditional fallback within the agent runtime path""" + import proxy_worker.dispatcher as dispatcher_module + + # Set environment variable to enable agent runtime + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "true" + + # Setup mock entry points + mock_entry_points.return_value = [] + + # Setup mock runtime base module - no runtime registered + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = False + + # Mock traditional fallback + with patch("proxy_worker.dispatcher.os.path.exists", return_value=True): + with patch("builtins.__import__") as mock_import: + mock_runtime_v2 = types.SimpleNamespace( + __file__="azure_functions_runtime.py", + invocation_id_cv=Mock(), + VERSION="1.12.0" + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime": + return mock_runtime_v2 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker( + "/home/site/wwwroot") + + # When agent runtime is enabled, entry points are queried even if + # no runtime registers + mock_entry_points.assert_called_once_with(group='azurefunctions.runtimes') + + # But we should NOT fall back - agent runtime path doesn't have fallback + # Note: Based on current implementation, the fallback logic only runs + # when PYTHON_ENABLE_AGENT_RUNTIME is False/not set + # When True, if no runtime is loaded, _library_worker should remain None + # This documents the current behavior + self.assertIsNone(dispatcher_module._library_worker) + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.importlib.import_module") + @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + def test_agent_runtime_enabled_logs_debug_messages( + self, mock_entry_points, mock_import_module, mock_logger): + """Test that appropriate debug messages are logged when + agent runtime is enabled""" + import proxy_worker.dispatcher as dispatcher_module + + # Set environment variable to enable agent runtime + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "1" + + # Setup mock entry point + mock_ep = Mock() + mock_ep.name = "fastapi" + mock_ep.load = Mock() + mock_entry_points.return_value = [mock_ep] + + # Setup mock runtime base module + mock_runtime_base = Mock() + mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True + mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( + "azure_functions_fastapi.runtime") + mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "fastapi" + + # Setup mock runtime module + mock_runtime_module = Mock() + mock_runtime_module.VERSION = "2.5.0" + mock_import_module.return_value = mock_runtime_module + + # Patch the runtime base import + with patch.dict('sys.modules', { + 'azurefunctions.extensions.base': mock_runtime_base + }): + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify debug logging for loaded entry point + mock_logger.debug.assert_any_call( + f"Loaded runtime entry point: {mock_ep.name}") + + # Verify debug logging for runtime registration + mock_logger.debug.assert_any_call( + "Runtime registered: %s (module: %s)", + "fastapi", + "azure_functions_fastapi.runtime" + ) + + # Verify debug logging for package import + mock_logger.debug.assert_any_call( + "Importing runtime package: %s", + "azure_functions_fastapi" + ) + + # Verify info logging + mock_logger.info.assert_called() + + @patch("proxy_worker.dispatcher.logger") + @patch("proxy_worker.dispatcher.os.path.exists") + @patch("builtins.__import__") + def test_agent_runtime_disabled_logs_debug_fallback_message( + self, mock_import, mock_exists, mock_logger): + """Test that fallback message is logged when agent runtime is disabled""" + import proxy_worker.dispatcher as dispatcher_module + + # Ensure agent runtime is disabled (not set) + if PYTHON_ENABLE_AGENT_RUNTIME in os.environ: + del os.environ[PYTHON_ENABLE_AGENT_RUNTIME] + + # Mock traditional fallback + mock_exists.return_value = True + + mock_runtime_v2 = types.SimpleNamespace( + __file__="azure_functions_runtime.py", + invocation_id_cv=Mock(), + VERSION="1.13.0" + ) + + def custom_import(name, *args, **kwargs): + if name == "azure_functions_runtime": + return mock_runtime_v2 + return _real_import(name, *args, **kwargs) + + mock_import.side_effect = custom_import + + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + + # Verify fallback debug message is logged + mock_logger.debug.assert_any_call( + "No runtime registered via base package, using fallback" + ) + + # Verify traditional runtime import logged + mock_logger.debug.assert_any_call( + "azure_functions_runtime import succeeded: %s", + "azure_functions_runtime.py" + ) From 41b782d5bcb94a849222aebdbf1927c764d8699d Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 4 May 2026 10:10:30 -0500 Subject: [PATCH 05/15] add hasattr check to not break old base ext versions --- workers/proxy_worker/dispatcher.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 8884052b..44fda86c 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -449,7 +449,10 @@ def reload_library_worker(directory: str): continue # Check if a runtime was registered - if runtime_base.RuntimeFeatureChecker.runtime_loaded(): + # Only check if an available runtime was found + # Check if the runtime base package has the RuntimeFeatureChecker + if ep and hasattr(runtime_base, 'RuntimeFeatureChecker') \ + and runtime_base.RuntimeFeatureChecker.runtime_loaded(): # Get the registered runtime module # (e.g., "azure_functions_fastapi.runtime") runtime_module_name = ( From 3c03c90d1fcc564f41cdd8084a33b83b24448e39 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 4 May 2026 11:02:11 -0500 Subject: [PATCH 06/15] enable feature for running tests --- eng/templates/official/jobs/ci-docker-consumption-tests.yml | 1 + eng/templates/official/jobs/ci-docker-dedicated-tests.yml | 1 + eng/templates/official/jobs/ci-e2e-tests.yml | 1 + eng/templates/official/jobs/ci-lc-tests.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/eng/templates/official/jobs/ci-docker-consumption-tests.yml b/eng/templates/official/jobs/ci-docker-consumption-tests.yml index d8ee29cb..aec10597 100644 --- a/eng/templates/official/jobs/ci-docker-consumption-tests.yml +++ b/eng/templates/official/jobs/ci-docker-consumption-tests.yml @@ -61,5 +61,6 @@ jobs: AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) + PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} displayName: "Running $(PYTHON_VERSION) Docker Consumption tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml index 711c21d0..e57d780f 100644 --- a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml +++ b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml @@ -61,5 +61,6 @@ jobs: AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) + PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} displayName: "Running $(PYTHON_VERSION) Docker Dedicated tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml index 71e96e2e..6dda587c 100644 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-e2e-tests.yml @@ -157,6 +157,7 @@ jobs: AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) skipTest: $(skipTest) PYAZURE_WEBHOST_DEBUG: true + PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. displayName: "Running $(PYTHON_VERSION) Python E2E Tests" workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} diff --git a/eng/templates/official/jobs/ci-lc-tests.yml b/eng/templates/official/jobs/ci-lc-tests.yml index e8df65d2..8e32fdd4 100644 --- a/eng/templates/official/jobs/ci-lc-tests.yml +++ b/eng/templates/official/jobs/ci-lc-tests.yml @@ -130,6 +130,7 @@ jobs: AzureWebJobsStorage: $(AZURE_STORAGE_CONNECTION_STRING) _DUMMY_CONT_KEY: $(_DUMMY_CONT_KEY) CONTAINER_SAS_TOKEN: $(CONTAINER_SAS_TOKEN) + PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. displayName: "Running $(PYTHON_VERSION) Linux Consumption tests" workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) From 225d20e714b9e4763ba0a9f3febba217a15434fd Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 5 May 2026 11:06:55 -0500 Subject: [PATCH 07/15] fix if condition --- workers/proxy_worker/dispatcher.py | 65 +++++++++++++++--------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 44fda86c..0c55d56b 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -451,7 +451,7 @@ def reload_library_worker(directory: str): # Check if a runtime was registered # Only check if an available runtime was found # Check if the runtime base package has the RuntimeFeatureChecker - if ep and hasattr(runtime_base, 'RuntimeFeatureChecker') \ + if available_runtimes and hasattr(runtime_base, 'RuntimeFeatureChecker') \ and runtime_base.RuntimeFeatureChecker.runtime_loaded(): # Get the registered runtime module # (e.g., "azure_functions_fastapi.runtime") @@ -480,39 +480,38 @@ def reload_library_worker(directory: str): _library_worker = runtime_module _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + # Module has been imported, end check + return + + # No runtime registered via base package + # Use traditional detection + logger.debug( + "No runtime registered via base package, using fallback") + v2_scriptfile = os.path.join(directory, get_script_file_name()) + if os.path.exists(v2_scriptfile): + try: + import azure_functions_runtime # NoQA + _library_worker = azure_functions_runtime + _library_worker_has_cv = hasattr( + _library_worker, 'invocation_id_cv') + logger.debug( + "azure_functions_runtime import succeeded: %s", + _library_worker.__file__) + except ImportError: + logger.debug( + "azure_functions_runtime library not found") else: - # Fallback: No runtime registered via base package - # Use traditional detection - logger.debug( - "No runtime registered via base package, using fallback") - v2_scriptfile = os.path.join(directory, get_script_file_name()) - if os.path.exists(v2_scriptfile): - try: - import azure_functions_runtime # NoQA - _library_worker = azure_functions_runtime - _library_worker_has_cv = hasattr( - _library_worker, 'invocation_id_cv') - logger.debug( - "azure_functions_runtime import succeeded: %s", - _library_worker.__file__) - except ImportError: - logger.debug( - "azure_functions_runtime library not found") - else: - try: - import azure_functions_runtime_v1 # NoQA - _library_worker = azure_functions_runtime_v1 - _library_worker_has_cv = hasattr( - _library_worker, 'invocation_id_cv') - logger.debug( - "azure_functions_runtime_v1 import succeeded: %s", - _library_worker.__file__) # type: ignore - except ImportError: - logger.debug( - "azure_functions_runtime_v1 library not found") - logger.info("Using runtime: %s, version: %s", - _library_worker, - getattr(_library_worker, 'VERSION', 'unknown')) + try: + import azure_functions_runtime_v1 # NoQA + _library_worker = azure_functions_runtime_v1 + _library_worker_has_cv = hasattr( + _library_worker, 'invocation_id_cv') + logger.debug( + "azure_functions_runtime_v1 import succeeded: %s", + _library_worker.__file__) # type: ignore + except ImportError: + logger.debug( + "azure_functions_runtime_v1 library not found") async def _handle__worker_init_request(self, request): logger.info('Received WorkerInitRequest, ' From a594c877891d78b1a47be6563ae04f61b70dc2de Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 5 May 2026 11:27:17 -0500 Subject: [PATCH 08/15] feedback part 1 --- workers/proxy_worker/dispatcher.py | 110 +++++++++++++++-------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 0c55d56b..ec48ca36 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -436,9 +436,19 @@ def reload_library_worker(directory: str): import azurefunctions.extensions.base as runtime_base # Discover all installed runtime packages via entry points - available_runtimes = entry_points(group='azurefunctions.runtimes') - - for ep in available_runtimes: + available_runtimes = list(entry_points(group='azurefunctions.runtimes')) + + # Only one runtime should be defined + if len(available_runtimes) > 1: + runtime_names = [ep.name for ep in available_runtimes] + raise RuntimeError( + f"Multiple runtimes detected: {runtime_names}. " + f"Only one runtime should be defined." + ) + + # Load the single runtime entry point if available + if available_runtimes: + ep = available_runtimes[0] try: # Load the entry point (triggers import and # metaclass registration) @@ -446,42 +456,40 @@ def reload_library_worker(directory: str): logger.debug(f"Loaded runtime entry point: {ep.name}") except Exception as e: logger.debug(f"Could not load runtime {ep.name}: {e}") - continue - - # Check if a runtime was registered - # Only check if an available runtime was found - # Check if the runtime base package has the RuntimeFeatureChecker - if available_runtimes and hasattr(runtime_base, 'RuntimeFeatureChecker') \ - and runtime_base.RuntimeFeatureChecker.runtime_loaded(): - # Get the registered runtime module - # (e.g., "azure_functions_fastapi.runtime") - runtime_module_name = ( - runtime_base.RuntimeTrackerMeta.get_module()) - runtime_name = ( - runtime_base.RuntimeTrackerMeta.get_runtime_name()) - - logger.debug("Runtime registered: %s (module: %s)", - runtime_name, runtime_module_name) - - # Extract the package name (e.g., "azure_functions_fastapi" - # from "azure_functions_fastapi.runtime") - # The package is everything before ".runtime" - if '.runtime' in runtime_module_name: - package_name = runtime_module_name.rsplit('.runtime', 1)[0] - else: - # Fallback: use the first part of the module name - package_name = runtime_module_name.split('.')[0] - - logger.debug("Importing runtime package: %s", package_name) - - # Import the top-level runtime package (which exports - # the public API) - runtime_module = importlib.import_module(package_name) - _library_worker = runtime_module - _library_worker_has_cv = hasattr(_library_worker, - 'invocation_id_cv') - # Module has been imported, end check - return + + # Check if a runtime was registered + # Check if the runtime base package has the RuntimeFeatureChecker + # Check if the runtime is loaded + if hasattr(runtime_base, 'RuntimeFeatureChecker') \ + and runtime_base.RuntimeFeatureChecker.runtime_loaded(): + # Get the registered runtime module + # (e.g., "azure_functions_fastapi.runtime") + runtime_module_name = ( + runtime_base.RuntimeTrackerMeta.get_module()) + runtime_name = ( + runtime_base.RuntimeTrackerMeta.get_runtime_name()) + + logger.debug("Runtime registered: %s (module: %s)", + runtime_name, runtime_module_name) + + # Extract the package name (e.g., "azure_functions_fastapi" + # from "azure_functions_fastapi.runtime") + # The package is everything before ".runtime" + if '.runtime' in runtime_module_name: + package_name = runtime_module_name.rsplit('.runtime', 1)[0] + else: + # Fallback: use the first part of the module name + package_name = runtime_module_name.split('.')[0] + + logger.debug("Importing runtime package: %s", package_name) + + # Import the top-level runtime package (which exports + # the public API) + runtime_module = importlib.import_module(package_name) + _library_worker = runtime_module + _library_worker_has_cv = _library_worker.invocation_id_cv + # Module has been imported, end check + return # No runtime registered via base package # Use traditional detection @@ -492,26 +500,22 @@ def reload_library_worker(directory: str): try: import azure_functions_runtime # NoQA _library_worker = azure_functions_runtime - _library_worker_has_cv = hasattr( - _library_worker, 'invocation_id_cv') - logger.debug( - "azure_functions_runtime import succeeded: %s", - _library_worker.__file__) + _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + logger.debug("azure_functions_runtime import succeeded: %s", + _library_worker.__file__) except ImportError: - logger.debug( - "azure_functions_runtime library not found") + logger.debug("azure_functions_runtime library not found: : %s", + traceback.format_exc()) else: try: import azure_functions_runtime_v1 # NoQA _library_worker = azure_functions_runtime_v1 - _library_worker_has_cv = hasattr( - _library_worker, 'invocation_id_cv') - logger.debug( - "azure_functions_runtime_v1 import succeeded: %s", - _library_worker.__file__) # type: ignore + _library_worker_has_cv = hasattr(_library_worker, 'invocation_id_cv') + logger.debug("azure_functions_runtime_v1 import succeeded: %s", + _library_worker.__file__) # type: ignore[union-attr] except ImportError: - logger.debug( - "azure_functions_runtime_v1 library not found") + logger.debug("azure_functions_runtime_v1 library not found: %s", + traceback.format_exc()) async def _handle__worker_init_request(self, request): logger.info('Received WorkerInitRequest, ' From cbaa2d7a3a861da060c431d5a3e2a8240e942e2a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 5 May 2026 12:05:48 -0500 Subject: [PATCH 09/15] feedback part 2 --- workers/proxy_worker/dispatcher.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index ec48ca36..3d442ebe 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -468,20 +468,10 @@ def reload_library_worker(directory: str): runtime_base.RuntimeTrackerMeta.get_module()) runtime_name = ( runtime_base.RuntimeTrackerMeta.get_runtime_name()) + package_name = runtime_base.RuntimeTrackerMeta.get_package_name() - logger.debug("Runtime registered: %s (module: %s)", - runtime_name, runtime_module_name) - - # Extract the package name (e.g., "azure_functions_fastapi" - # from "azure_functions_fastapi.runtime") - # The package is everything before ".runtime" - if '.runtime' in runtime_module_name: - package_name = runtime_module_name.rsplit('.runtime', 1)[0] - else: - # Fallback: use the first part of the module name - package_name = runtime_module_name.split('.')[0] - - logger.debug("Importing runtime package: %s", package_name) + logger.debug("Runtime registered: %s (module: %s). Importing runtime package: %s", + runtime_name, runtime_module_name, package_name) # Import the top-level runtime package (which exports # the public API) From 3faaf4865f3c12e6b717c9279a25c4c263f93983 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 5 May 2026 13:13:24 -0500 Subject: [PATCH 10/15] lint --- workers/proxy_worker/dispatcher.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 3d442ebe..3a3e93df 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -470,8 +470,9 @@ def reload_library_worker(directory: str): runtime_base.RuntimeTrackerMeta.get_runtime_name()) package_name = runtime_base.RuntimeTrackerMeta.get_package_name() - logger.debug("Runtime registered: %s (module: %s). Importing runtime package: %s", - runtime_name, runtime_module_name, package_name) + logger.debug("Runtime registered: %s (module: %s). " + "Importing runtime package: %s", + runtime_name, runtime_module_name, package_name) # Import the top-level runtime package (which exports # the public API) From 546e668badfe6b2a8acfdf88f1a162265abef15b Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 5 May 2026 15:24:13 -0500 Subject: [PATCH 11/15] run tests with runtime feature disable --- eng/templates/official/jobs/ci-docker-consumption-tests.yml | 1 - eng/templates/official/jobs/ci-docker-dedicated-tests.yml | 1 - eng/templates/official/jobs/ci-e2e-tests.yml | 1 - eng/templates/official/jobs/ci-lc-tests.yml | 1 - 4 files changed, 4 deletions(-) diff --git a/eng/templates/official/jobs/ci-docker-consumption-tests.yml b/eng/templates/official/jobs/ci-docker-consumption-tests.yml index aec10597..d8ee29cb 100644 --- a/eng/templates/official/jobs/ci-docker-consumption-tests.yml +++ b/eng/templates/official/jobs/ci-docker-consumption-tests.yml @@ -61,6 +61,5 @@ jobs: AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) - PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} displayName: "Running $(PYTHON_VERSION) Docker Consumption tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml index e57d780f..711c21d0 100644 --- a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml +++ b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml @@ -61,6 +61,5 @@ jobs: AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) - PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} displayName: "Running $(PYTHON_VERSION) Docker Dedicated tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml index 6dda587c..71e96e2e 100644 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-e2e-tests.yml @@ -157,7 +157,6 @@ jobs: AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) skipTest: $(skipTest) PYAZURE_WEBHOST_DEBUG: true - PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. displayName: "Running $(PYTHON_VERSION) Python E2E Tests" workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} diff --git a/eng/templates/official/jobs/ci-lc-tests.yml b/eng/templates/official/jobs/ci-lc-tests.yml index 8e32fdd4..e8df65d2 100644 --- a/eng/templates/official/jobs/ci-lc-tests.yml +++ b/eng/templates/official/jobs/ci-lc-tests.yml @@ -130,7 +130,6 @@ jobs: AzureWebJobsStorage: $(AZURE_STORAGE_CONNECTION_STRING) _DUMMY_CONT_KEY: $(_DUMMY_CONT_KEY) CONTAINER_SAS_TOKEN: $(CONTAINER_SAS_TOKEN) - PYTHON_ENABLE_AGENT_RUNTIME: true # Run tests with agent runtime enabled to validate related code paths and ensure compatibility. This will not enable the feature in prod, it's only for testing purposes. displayName: "Running $(PYTHON_VERSION) Linux Consumption tests" workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) From 8e495cb8f28d35ecef5ffeb716a4003a31d88fbe Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 6 May 2026 11:26:58 -0500 Subject: [PATCH 12/15] fix tests --- workers/proxy_worker/dispatcher.py | 102 +++++++++--------- .../tests/unittest_proxy/test_dispatcher.py | 32 ++++-- .../test_dispatcher_agent_runtime.py | 42 ++++---- 3 files changed, 100 insertions(+), 76 deletions(-) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 3a3e93df..24c714a1 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -432,55 +432,59 @@ def reload_library_worker(directory: str): global _library_worker, _library_worker_has_cv if is_envvar_true(PYTHON_ENABLE_AGENT_RUNTIME): - # Import base package - import azurefunctions.extensions.base as runtime_base - - # Discover all installed runtime packages via entry points - available_runtimes = list(entry_points(group='azurefunctions.runtimes')) - - # Only one runtime should be defined - if len(available_runtimes) > 1: - runtime_names = [ep.name for ep in available_runtimes] - raise RuntimeError( - f"Multiple runtimes detected: {runtime_names}. " - f"Only one runtime should be defined." - ) - - # Load the single runtime entry point if available - if available_runtimes: - ep = available_runtimes[0] - try: - # Load the entry point (triggers import and - # metaclass registration) - ep.load() - logger.debug(f"Loaded runtime entry point: {ep.name}") - except Exception as e: - logger.debug(f"Could not load runtime {ep.name}: {e}") - - # Check if a runtime was registered - # Check if the runtime base package has the RuntimeFeatureChecker - # Check if the runtime is loaded - if hasattr(runtime_base, 'RuntimeFeatureChecker') \ - and runtime_base.RuntimeFeatureChecker.runtime_loaded(): - # Get the registered runtime module - # (e.g., "azure_functions_fastapi.runtime") - runtime_module_name = ( - runtime_base.RuntimeTrackerMeta.get_module()) - runtime_name = ( - runtime_base.RuntimeTrackerMeta.get_runtime_name()) - package_name = runtime_base.RuntimeTrackerMeta.get_package_name() - - logger.debug("Runtime registered: %s (module: %s). " - "Importing runtime package: %s", - runtime_name, runtime_module_name, package_name) - - # Import the top-level runtime package (which exports - # the public API) - runtime_module = importlib.import_module(package_name) - _library_worker = runtime_module - _library_worker_has_cv = _library_worker.invocation_id_cv - # Module has been imported, end check - return + try: + # Import base package + import azurefunctions.extensions.base as runtime_base + # Discover all installed runtime packages via entry points + available_runtimes = list(entry_points(group='azurefunctions.runtimes')) + + # Only one runtime should be defined + if len(available_runtimes) > 1: + runtime_names = [ep.name for ep in available_runtimes] + raise RuntimeError( + f"Multiple runtimes detected: {runtime_names}. " + f"Only one runtime should be defined." + ) + + # Load the single runtime entry point if available + if available_runtimes: + ep = available_runtimes[0] + try: + # Load the entry point (triggers import and + # metaclass registration) + ep.load() + logger.debug(f"Loaded runtime entry point: {ep.name}") + except Exception as e: + logger.debug(f"Could not load runtime {ep.name}: {e}") + + # Check if a runtime was registered + # Check if the runtime base package has the RuntimeFeatureChecker + # Check if the runtime is loaded + if hasattr(runtime_base, 'RuntimeFeatureChecker') \ + and runtime_base.RuntimeFeatureChecker.runtime_loaded(): + # Get the registered runtime module + # (e.g., "azure_functions_fastapi.runtime") + runtime_module_name = ( + runtime_base.RuntimeTrackerMeta.get_module()) + runtime_name = ( + runtime_base.RuntimeTrackerMeta.get_runtime_name()) + package_name = ( + runtime_base.RuntimeTrackerMeta.get_package_name()) + + logger.debug("Runtime registered: %s (module: %s). " + "Importing runtime package: %s", + runtime_name, runtime_module_name, package_name) + + # Import the top-level runtime package (which exports + # the public API) + runtime_module = importlib.import_module(package_name) + _library_worker = runtime_module + _library_worker_has_cv = _library_worker.invocation_id_cv + # Module has been imported, end check + return + except ImportError: + logger.debug("ImportError when importing base extension: %s", + traceback.format_exc()) # No runtime registered via base package # Use traditional detection diff --git a/workers/tests/unittest_proxy/test_dispatcher.py b/workers/tests/unittest_proxy/test_dispatcher.py index d0751f43..a89e8ad0 100644 --- a/workers/tests/unittest_proxy/test_dispatcher.py +++ b/workers/tests/unittest_proxy/test_dispatcher.py @@ -1,6 +1,8 @@ import asyncio import builtins import logging +import os +import sys import threading import types import unittest @@ -18,6 +20,7 @@ get_thread_invocation_id, clear_thread_invocation_id, ) +from proxy_worker.utils.constants import PYTHON_ENABLE_AGENT_RUNTIME _real_import = builtins.__import__ @@ -784,12 +787,17 @@ def setUp(self): import proxy_worker.dispatcher as dispatcher_module dispatcher_module._library_worker = None dispatcher_module._library_worker_has_cv = False + # Enable agent runtime for these tests + os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "true" def tearDown(self): """Clean up after each test""" import proxy_worker.dispatcher as dispatcher_module dispatcher_module._library_worker = None dispatcher_module._library_worker_has_cv = False + # Clean up environment variable + if PYTHON_ENABLE_AGENT_RUNTIME in os.environ: + del os.environ[PYTHON_ENABLE_AGENT_RUNTIME] @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") @@ -811,6 +819,8 @@ def test_runtime_base_success_with_runtime_suffix( mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( "azure_functions_fastapi.runtime") mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "fastapi" + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "azure_functions_fastapi") # Setup mock runtime module mock_runtime_module = Mock() @@ -819,7 +829,7 @@ def test_runtime_base_success_with_runtime_suffix( mock_import_module.return_value = mock_runtime_module # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -866,6 +876,8 @@ def test_runtime_base_success_without_runtime_suffix( mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = True mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = "custom_package" mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "custom" + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "custom_package") # Setup mock runtime module mock_runtime_module = Mock() @@ -873,7 +885,7 @@ def test_runtime_base_success_without_runtime_suffix( mock_import_module.return_value = mock_runtime_module # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -907,7 +919,7 @@ def test_runtime_base_entry_point_load_exception( mock_runtime_base.RuntimeFeatureChecker.runtime_loaded.return_value = False # Patch the runtime base import and traditional fallback - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): with patch("proxy_worker.dispatcher.os.path.exists", @@ -963,7 +975,7 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -1013,7 +1025,7 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -1098,6 +1110,8 @@ def runtime_loaded_side_effect(): mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( "runtime1_package.runtime") mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "runtime1" + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "runtime1_package") # Setup mock runtime module mock_runtime_module = Mock() @@ -1105,7 +1119,7 @@ def runtime_loaded_side_effect(): mock_import_module.return_value = mock_runtime_module # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -1139,14 +1153,16 @@ def test_runtime_base_version_unknown( "no_version_package.runtime") mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = ( "no_version") + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "no_version_package") # Setup mock runtime module without VERSION mock_runtime_module = Mock(spec=[]) # No attributes del mock_runtime_module.VERSION # Ensure VERSION doesn't exist mock_import_module.return_value = mock_runtime_module - # Patch the runtime base import - with patch.dict('sys.modules', { + # Patch the runtime base import - use sys directly instead of string + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") diff --git a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py index 6acf1c5d..cad8caf3 100644 --- a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py +++ b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py @@ -6,6 +6,7 @@ """ import builtins import os +import sys import types import unittest from unittest.mock import Mock, patch @@ -62,6 +63,8 @@ def test_agent_runtime_enabled_with_true_string( mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( "azure_functions_fastapi.runtime") mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "fastapi" + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "azure_functions_fastapi") # Setup mock runtime module mock_runtime_module = Mock() @@ -69,7 +72,7 @@ def test_agent_runtime_enabled_with_true_string( mock_import_module.return_value = mock_runtime_module # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -110,6 +113,8 @@ def test_agent_runtime_enabled_with_1_string( "test_package.runtime") mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = ( "test") + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "test_package") # Setup mock runtime module mock_runtime_module = Mock() @@ -117,7 +122,7 @@ def test_agent_runtime_enabled_with_1_string( mock_import_module.return_value = mock_runtime_module # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -275,7 +280,7 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker( @@ -285,12 +290,11 @@ def custom_import(name, *args, **kwargs): # no runtime registers mock_entry_points.assert_called_once_with(group='azurefunctions.runtimes') - # But we should NOT fall back - agent runtime path doesn't have fallback - # Note: Based on current implementation, the fallback logic only runs - # when PYTHON_ENABLE_AGENT_RUNTIME is False/not set - # When True, if no runtime is loaded, _library_worker should remain None - # This documents the current behavior - self.assertIsNone(dispatcher_module._library_worker) + # Based on the updated implementation, when no runtime registers via + # entry points, the dispatcher falls back to traditional detection + # So we should have the v2 runtime loaded + self.assertEqual(dispatcher_module._library_worker, mock_runtime_v2) + self.assertTrue(dispatcher_module._library_worker_has_cv) @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") @@ -316,6 +320,8 @@ def test_agent_runtime_enabled_logs_debug_messages( mock_runtime_base.RuntimeTrackerMeta.get_module.return_value = ( "azure_functions_fastapi.runtime") mock_runtime_base.RuntimeTrackerMeta.get_runtime_name.return_value = "fastapi" + mock_runtime_base.RuntimeTrackerMeta.get_package_name.return_value = ( + "azure_functions_fastapi") # Setup mock runtime module mock_runtime_module = Mock() @@ -323,7 +329,7 @@ def test_agent_runtime_enabled_logs_debug_messages( mock_import_module.return_value = mock_runtime_module # Patch the runtime base import - with patch.dict('sys.modules', { + with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base }): dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") @@ -332,22 +338,20 @@ def test_agent_runtime_enabled_logs_debug_messages( mock_logger.debug.assert_any_call( f"Loaded runtime entry point: {mock_ep.name}") - # Verify debug logging for runtime registration - mock_logger.debug.assert_any_call( + # Verify info logging for runtime registration (changed from debug to info) + mock_logger.info.assert_any_call( "Runtime registered: %s (module: %s)", "fastapi", "azure_functions_fastapi.runtime" ) - # Verify debug logging for package import - mock_logger.debug.assert_any_call( - "Importing runtime package: %s", - "azure_functions_fastapi" + # Verify info logging for runtime version + mock_logger.info.assert_any_call( + "Using runtime: %s, version: %s", + "fastapi", + "2.5.0" ) - # Verify info logging - mock_logger.info.assert_called() - @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.os.path.exists") @patch("builtins.__import__") From 7933430301f7cc892bfdf6e9f1572991c3141d1a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 6 May 2026 11:58:19 -0500 Subject: [PATCH 13/15] fix tests --- .../tests/unittest_proxy/test_dispatcher.py | 37 +++++++++++-------- .../test_dispatcher_agent_runtime.py | 30 +++++++++++---- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/workers/tests/unittest_proxy/test_dispatcher.py b/workers/tests/unittest_proxy/test_dispatcher.py index a89e8ad0..a812acc5 100644 --- a/workers/tests/unittest_proxy/test_dispatcher.py +++ b/workers/tests/unittest_proxy/test_dispatcher.py @@ -801,7 +801,7 @@ def tearDown(self): @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_runtime_base_success_with_runtime_suffix( self, mock_entry_points, mock_import_module, mock_logger): """Test successful runtime loading via base package with .runtime suffix""" @@ -859,7 +859,7 @@ def test_runtime_base_success_with_runtime_suffix( @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_runtime_base_success_without_runtime_suffix( self, mock_entry_points, mock_import_module, mock_logger): """Test successful runtime loading when module name has no .runtime suffix""" @@ -897,7 +897,7 @@ def test_runtime_base_success_without_runtime_suffix( self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_runtime_base_entry_point_load_exception( self, mock_entry_points, mock_logger): """Test handling of exceptions when loading entry points""" @@ -941,7 +941,7 @@ def test_runtime_base_entry_point_load_exception( ) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") @patch("proxy_worker.dispatcher.os.path.exists") @patch("builtins.__import__") def test_runtime_base_no_runtime_registered_fallback_to_v2( @@ -974,6 +974,10 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import + # Clear sys.modules to force re-import + if 'azure_functions_runtime' in sys.modules: + del sys.modules['azure_functions_runtime'] + # Patch the runtime base import with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base @@ -994,7 +998,7 @@ def custom_import(name, *args, **kwargs): self.assertTrue(dispatcher_module._library_worker_has_cv) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") @patch("proxy_worker.dispatcher.os.path.exists") @patch("builtins.__import__") def test_runtime_base_no_runtime_registered_fallback_to_v1( @@ -1024,6 +1028,10 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import + # Clear sys.modules to force re-import + if 'azure_functions_runtime_v1' in sys.modules: + del sys.modules['azure_functions_runtime_v1'] + # Patch the runtime base import with patch.dict(sys.modules, { 'azurefunctions.extensions.base': mock_runtime_base @@ -1048,9 +1056,9 @@ def test_runtime_base_import_error_fallback_to_traditional( """Test fallback when runtime base package import fails""" import proxy_worker.dispatcher as dispatcher_module - # Mock runtime base import failure + # Mock runtime base import failure - raise error when importing base package def custom_import(name, *args, **kwargs): - if name == "importlib.metadata" or "azurefunctions.extensions.base" in name: + if "azurefunctions.extensions.base" in name: raise ImportError("Runtime base not installed") if name == "azure_functions_runtime": mock_runtime = types.SimpleNamespace( @@ -1063,14 +1071,11 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import mock_exists.return_value = True - # This will trigger the import in reload_library_worker - # The ImportError should be caught and fallback should occur - with patch("proxy_worker.dispatcher.importlib") as mock_importlib_module: - # Make the entry_points import raise ImportError - mock_importlib_module.metadata.entry_points.side_effect = ( - ImportError("Runtime base not installed")) + # Clear sys.modules to force re-import + if 'azure_functions_runtime' in sys.modules: + del sys.modules['azure_functions_runtime'] - dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") # Verify error was logged mock_logger.error.assert_called_once() @@ -1079,7 +1084,7 @@ def custom_import(name, *args, **kwargs): @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_runtime_base_multiple_entry_points( self, mock_entry_points, mock_import_module, mock_logger): """Test handling of multiple entry points (only first @@ -1134,7 +1139,7 @@ def runtime_loaded_side_effect(): @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_runtime_base_version_unknown( self, mock_entry_points, mock_import_module, mock_logger): """Test handling when runtime module has no VERSION attribute""" diff --git a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py index cad8caf3..b37ff66e 100644 --- a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py +++ b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py @@ -42,7 +42,7 @@ def tearDown(self): @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_agent_runtime_enabled_with_true_string( self, mock_entry_points, mock_import_module, mock_logger): """Test that entry points are used when PYTHON_ENABLE_AGENT_RUNTIME='true'""" @@ -91,7 +91,7 @@ def test_agent_runtime_enabled_with_true_string( @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_agent_runtime_enabled_with_1_string( self, mock_entry_points, mock_import_module, mock_logger): """Test that entry points are used when PYTHON_ENABLE_AGENT_RUNTIME='1'""" @@ -135,7 +135,7 @@ def test_agent_runtime_enabled_with_1_string( self.assertEqual(dispatcher_module._library_worker, mock_runtime_module) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") @patch("proxy_worker.dispatcher.os.path.exists") @patch("builtins.__import__") def test_agent_runtime_disabled_uses_traditional_detection( @@ -162,6 +162,10 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import + # Clear sys.modules to force re-import + if 'azure_functions_runtime' in sys.modules: + del sys.modules['azure_functions_runtime'] + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") # Verify entry points were NOT queried @@ -172,7 +176,7 @@ def custom_import(name, *args, **kwargs): self.assertTrue(dispatcher_module._library_worker_has_cv) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") @patch("proxy_worker.dispatcher.os.path.exists") @patch("builtins.__import__") def test_agent_runtime_disabled_with_false_string( @@ -199,6 +203,10 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import + # Clear sys.modules to force re-import + if 'azure_functions_runtime_v1' in sys.modules: + del sys.modules['azure_functions_runtime_v1'] + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") # Verify entry points were NOT queried @@ -209,7 +217,7 @@ def custom_import(name, *args, **kwargs): self.assertFalse(dispatcher_module._library_worker_has_cv) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") @patch("proxy_worker.dispatcher.os.path.exists") @patch("builtins.__import__") def test_agent_runtime_disabled_with_0_string( @@ -237,6 +245,10 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import + # Clear sys.modules to force re-import + if 'azure_functions_runtime' in sys.modules: + del sys.modules['azure_functions_runtime'] + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") # Verify entry points were NOT queried @@ -246,7 +258,7 @@ def custom_import(name, *args, **kwargs): self.assertEqual(dispatcher_module._library_worker, mock_runtime_v2) @patch("proxy_worker.dispatcher.logger") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_agent_runtime_enabled_no_runtime_registered_fallback_still_used( self, mock_entry_points, mock_logger): """Test that when agent runtime is enabled but no runtime registers, @@ -298,7 +310,7 @@ def custom_import(name, *args, **kwargs): @patch("proxy_worker.dispatcher.logger") @patch("proxy_worker.dispatcher.importlib.import_module") - @patch("proxy_worker.dispatcher.importlib.metadata.entry_points") + @patch("proxy_worker.dispatcher.entry_points") def test_agent_runtime_enabled_logs_debug_messages( self, mock_entry_points, mock_import_module, mock_logger): """Test that appropriate debug messages are logged when @@ -380,6 +392,10 @@ def custom_import(name, *args, **kwargs): mock_import.side_effect = custom_import + # Clear sys.modules to force re-import + if 'azure_functions_runtime' in sys.modules: + del sys.modules['azure_functions_runtime'] + dispatcher_module.Dispatcher.reload_library_worker("/home/site/wwwroot") # Verify fallback debug message is logged From 60568ef39f070ccd726eac812a19a0e4835fbaa9 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 6 May 2026 13:09:08 -0500 Subject: [PATCH 14/15] fix tests --- .../tests/unittest_proxy/test_dispatcher.py | 19 +------------------ .../test_dispatcher_agent_runtime.py | 19 +------------------ 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/workers/tests/unittest_proxy/test_dispatcher.py b/workers/tests/unittest_proxy/test_dispatcher.py index a812acc5..ea5f5fae 100644 --- a/workers/tests/unittest_proxy/test_dispatcher.py +++ b/workers/tests/unittest_proxy/test_dispatcher.py @@ -21,6 +21,7 @@ clear_thread_invocation_id, ) from proxy_worker.utils.constants import PYTHON_ENABLE_AGENT_RUNTIME +import proxy_worker.dispatcher as dispatcher_module _real_import = builtins.__import__ @@ -784,7 +785,6 @@ class TestReloadLibraryWorkerWithRuntimeBase(unittest.TestCase): def setUp(self): """Clear library worker state before each test""" - import proxy_worker.dispatcher as dispatcher_module dispatcher_module._library_worker = None dispatcher_module._library_worker_has_cv = False # Enable agent runtime for these tests @@ -792,7 +792,6 @@ def setUp(self): def tearDown(self): """Clean up after each test""" - import proxy_worker.dispatcher as dispatcher_module dispatcher_module._library_worker = None dispatcher_module._library_worker_has_cv = False # Clean up environment variable @@ -805,8 +804,6 @@ def tearDown(self): def test_runtime_base_success_with_runtime_suffix( self, mock_entry_points, mock_import_module, mock_logger): """Test successful runtime loading via base package with .runtime suffix""" - import proxy_worker.dispatcher as dispatcher_module - # Setup mock entry point mock_ep = Mock() mock_ep.name = "fastapi" @@ -863,8 +860,6 @@ def test_runtime_base_success_with_runtime_suffix( def test_runtime_base_success_without_runtime_suffix( self, mock_entry_points, mock_import_module, mock_logger): """Test successful runtime loading when module name has no .runtime suffix""" - import proxy_worker.dispatcher as dispatcher_module - # Setup mock entry point mock_ep = Mock() mock_ep.name = "custom_runtime" @@ -901,8 +896,6 @@ def test_runtime_base_success_without_runtime_suffix( def test_runtime_base_entry_point_load_exception( self, mock_entry_points, mock_logger): """Test handling of exceptions when loading entry points""" - import proxy_worker.dispatcher as dispatcher_module - # Setup mock entry points - first fails, second succeeds mock_ep1 = Mock() mock_ep1.name = "broken_runtime" @@ -947,8 +940,6 @@ def test_runtime_base_entry_point_load_exception( def test_runtime_base_no_runtime_registered_fallback_to_v2( self, mock_import, mock_exists, mock_entry_points, mock_logger): """Test fallback to traditional v2 when no runtime registered""" - import proxy_worker.dispatcher as dispatcher_module - # Setup mock entry points (none succeed in registration) mock_ep = Mock() mock_ep.name = "test_runtime" @@ -1005,8 +996,6 @@ def test_runtime_base_no_runtime_registered_fallback_to_v1( self, mock_import, mock_exists, mock_entry_points, mock_logger): """Test fallback to traditional v1 when no runtime registered and v2 script absent""" - import proxy_worker.dispatcher as dispatcher_module - # Setup mock entry points mock_entry_points.return_value = [] @@ -1054,8 +1043,6 @@ def custom_import(name, *args, **kwargs): def test_runtime_base_import_error_fallback_to_traditional( self, mock_import, mock_exists, mock_logger): """Test fallback when runtime base package import fails""" - import proxy_worker.dispatcher as dispatcher_module - # Mock runtime base import failure - raise error when importing base package def custom_import(name, *args, **kwargs): if "azurefunctions.extensions.base" in name: @@ -1089,8 +1076,6 @@ def test_runtime_base_multiple_entry_points( self, mock_entry_points, mock_import_module, mock_logger): """Test handling of multiple entry points (only first registered runtime used)""" - import proxy_worker.dispatcher as dispatcher_module - # Setup multiple mock entry points mock_ep1 = Mock() mock_ep1.name = "runtime1" @@ -1143,8 +1128,6 @@ def runtime_loaded_side_effect(): def test_runtime_base_version_unknown( self, mock_entry_points, mock_import_module, mock_logger): """Test handling when runtime module has no VERSION attribute""" - import proxy_worker.dispatcher as dispatcher_module - # Setup mock entry point mock_ep = Mock() mock_ep.name = "no_version_runtime" diff --git a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py index b37ff66e..dbbd546a 100644 --- a/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py +++ b/workers/tests/unittest_proxy/test_dispatcher_agent_runtime.py @@ -12,6 +12,7 @@ from unittest.mock import Mock, patch from proxy_worker.utils.constants import PYTHON_ENABLE_AGENT_RUNTIME +import proxy_worker.dispatcher as dispatcher_module _real_import = builtins.__import__ @@ -22,7 +23,6 @@ class TestReloadLibraryWorkerAgentRuntime(unittest.TestCase): def setUp(self): """Clear library worker state and environment before each test""" - import proxy_worker.dispatcher as dispatcher_module dispatcher_module._library_worker = None dispatcher_module._library_worker_has_cv = False @@ -32,7 +32,6 @@ def setUp(self): def tearDown(self): """Clean up after each test""" - import proxy_worker.dispatcher as dispatcher_module dispatcher_module._library_worker = None dispatcher_module._library_worker_has_cv = False @@ -46,8 +45,6 @@ def tearDown(self): def test_agent_runtime_enabled_with_true_string( self, mock_entry_points, mock_import_module, mock_logger): """Test that entry points are used when PYTHON_ENABLE_AGENT_RUNTIME='true'""" - import proxy_worker.dispatcher as dispatcher_module - # Set environment variable to enable agent runtime os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "true" @@ -95,8 +92,6 @@ def test_agent_runtime_enabled_with_true_string( def test_agent_runtime_enabled_with_1_string( self, mock_entry_points, mock_import_module, mock_logger): """Test that entry points are used when PYTHON_ENABLE_AGENT_RUNTIME='1'""" - import proxy_worker.dispatcher as dispatcher_module - # Set environment variable to enable agent runtime os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "1" @@ -142,8 +137,6 @@ def test_agent_runtime_disabled_uses_traditional_detection( self, mock_import, mock_exists, mock_entry_points, mock_logger): """Test that traditional detection is used when PYTHON_ENABLE_AGENT_RUNTIME is not set""" - import proxy_worker.dispatcher as dispatcher_module - # Do NOT set PYTHON_ENABLE_AGENT_RUNTIME - should use traditional detection # Mock traditional fallback @@ -183,8 +176,6 @@ def test_agent_runtime_disabled_with_false_string( self, mock_import, mock_exists, mock_entry_points, mock_logger): """Test that traditional detection is used when PYTHON_ENABLE_AGENT_RUNTIME='false'""" - import proxy_worker.dispatcher as dispatcher_module - # Set environment variable to disable agent runtime os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "false" @@ -224,8 +215,6 @@ def test_agent_runtime_disabled_with_0_string( self, mock_import, mock_exists, mock_entry_points, mock_logger): """Test that traditional detection is used when PYTHON_ENABLE_AGENT_RUNTIME='0'""" - import proxy_worker.dispatcher as dispatcher_module - # Set environment variable to disable agent runtime os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "0" @@ -263,8 +252,6 @@ def test_agent_runtime_enabled_no_runtime_registered_fallback_still_used( self, mock_entry_points, mock_logger): """Test that when agent runtime is enabled but no runtime registers, we still use the traditional fallback within the agent runtime path""" - import proxy_worker.dispatcher as dispatcher_module - # Set environment variable to enable agent runtime os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "true" @@ -315,8 +302,6 @@ def test_agent_runtime_enabled_logs_debug_messages( self, mock_entry_points, mock_import_module, mock_logger): """Test that appropriate debug messages are logged when agent runtime is enabled""" - import proxy_worker.dispatcher as dispatcher_module - # Set environment variable to enable agent runtime os.environ[PYTHON_ENABLE_AGENT_RUNTIME] = "1" @@ -370,8 +355,6 @@ def test_agent_runtime_enabled_logs_debug_messages( def test_agent_runtime_disabled_logs_debug_fallback_message( self, mock_import, mock_exists, mock_logger): """Test that fallback message is logged when agent runtime is disabled""" - import proxy_worker.dispatcher as dispatcher_module - # Ensure agent runtime is disabled (not set) if PYTHON_ENABLE_AGENT_RUNTIME in os.environ: del os.environ[PYTHON_ENABLE_AGENT_RUNTIME] From b79418f660ce6996fa1a0577c17cea0709b07c78 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 6 May 2026 13:32:14 -0500 Subject: [PATCH 15/15] fix tests --- workers/tests/unittest_proxy/test_dispatcher.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/workers/tests/unittest_proxy/test_dispatcher.py b/workers/tests/unittest_proxy/test_dispatcher.py index ea5f5fae..a0ec1b54 100644 --- a/workers/tests/unittest_proxy/test_dispatcher.py +++ b/workers/tests/unittest_proxy/test_dispatcher.py @@ -65,8 +65,6 @@ def test_dispatcher_initialization(self, mock_thread, mock_queue): def test_on_logging_levels_and_categories(self, mock_is_system, mock_rpc_log, mock_streaming_message): # Import module to access cached constants - import proxy_worker.dispatcher as dispatcher_module - loop = Mock() dispatcher = Dispatcher(loop, "localhost", 5000, "worker", "req", 5.0) @@ -419,9 +417,6 @@ class TestInvocationTracking(unittest.TestCase): def setUp(self): """Clear any existing invocation state before each test""" - # Import the module-level variables properly - import proxy_worker.dispatcher as dispatcher_module - # Clear thread registry with dispatcher_module._registry_lock: dispatcher_module._thread_invocation_registry.clear() @@ -435,9 +430,6 @@ def setUp(self): def tearDown(self): """Clean up after each test""" - # Import the module-level variables properly - import proxy_worker.dispatcher as dispatcher_module - # Clear thread registry with dispatcher_module._registry_lock: dispatcher_module._thread_invocation_registry.clear() @@ -629,9 +621,6 @@ class TestDispatcherInvocationHandling(unittest.TestCase): def setUp(self): """Clear any existing invocation state before each test""" - # Import the module-level variables properly - import proxy_worker.dispatcher as dispatcher_module - # Clear thread registry with dispatcher_module._registry_lock: dispatcher_module._thread_invocation_registry.clear() @@ -642,9 +631,6 @@ def setUp(self): def tearDown(self): """Clean up after each test""" - # Import the module-level variables properly - import proxy_worker.dispatcher as dispatcher_module - # Clear thread registry with dispatcher_module._registry_lock: dispatcher_module._thread_invocation_registry.clear()