diff --git a/.github/workflows/dev_workflow_func_app.yaml b/.github/workflows/dev_workflow_func_app.yaml new file mode 100644 index 0000000..b1beb19 --- /dev/null +++ b/.github/workflows/dev_workflow_func_app.yaml @@ -0,0 +1,105 @@ +--- +# Dev Workflow – build image and deploy to Azure Function App (Development environment). +# +# Required secrets (Settings → Environments → Development → Environment secrets): +# - REGISTRY_DOMAIN – Azure Container Registry login server (e.g. myregistry.azurecr.io) +# - REGISTRY_USERNAME – ACR username +# - REGISTRY_PASSWORD – ACR password +# - REGISTRY_REPO – Repository name in ACR for this app +# - TDEI_CORE_AZURE_CREDS – Azure service principal JSON (for az login) +# +# Required variables (Settings → Environments → Development → Environment variables): +# - FUNCTION_APP_NAME – Azure Function App name to deploy to +# - RESOURCE_GROUP – Azure resource group containing the Function App +# +# Optional variables (defaults used if not set): +# - RESTART_APP – Set to 'true' or 'false'; default 'true' +# - APP_SETTINGS_JSON – JSON object of extra app settings to apply; default '{}' +# +######### Dev Workflow ######## +on: + pull_request: + branches: [dev] + types: + - closed + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + Build: + environment: Development + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - uses: actions/checkout@v2 + - uses: azure/docker-login@v1 + with: + login-server: ${{ secrets.REGISTRY_DOMAIN }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Publish image to Azure Registry + run: | + docker build -t ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.sha }} -t ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.ref_name == 'main' && 'prod' || github.ref_name }}${{ github.ref_name != 'main' && '-latest' || 'latest' }} . + docker push ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }} --all-tags + deploy: + environment: Development + runs-on: ubuntu-latest + needs: [Build] + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2.0.0 + with: + creds: ${{ secrets.TDEI_CORE_AZURE_CREDS }} + + - name: Resolve deploy config from environment + id: deploy_config + run: | + echo "function_app_name=${{ vars.FUNCTION_APP_NAME }}" >> "$GITHUB_OUTPUT" + echo "resource_group=${{ vars.RESOURCE_GROUP }}" >> "$GITHUB_OUTPUT" + echo "aci_image=${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "restart_app=${{ vars.RESTART_APP || 'true' }}" >> "$GITHUB_OUTPUT" + + - name: Log target environment + shell: bash + run: | + echo "Deploying to:" + echo " Function App: ${{ steps.deploy_config.outputs.function_app_name }}" + echo " Resource Group: ${{ steps.deploy_config.outputs.resource_group }}" + echo " ACI_IMAGE: ${{ steps.deploy_config.outputs.aci_image }}" + + - name: Update app settings (ACI_IMAGE + extras) + shell: bash + run: | + python - <<'PY' + import json + import os + app_settings = os.environ.get("APP_SETTINGS_JSON", "{}") + data = json.loads(app_settings) if app_settings else {} + data["ACI_IMAGE"] = os.environ["ACI_IMAGE"] + with open("/tmp/appsettings.txt", "w", encoding="utf-8") as handle: + for key, value in data.items(): + handle.write(f"{key}={value}\n") + PY + echo "Updating only provided settings (no clearing of others)." + az functionapp config appsettings set \ + --name "${{ steps.deploy_config.outputs.function_app_name }}" \ + --resource-group "${{ steps.deploy_config.outputs.resource_group }}" \ + --settings $(cat /tmp/appsettings.txt | tr '\n' ' ') + env: + ACI_IMAGE: ${{ steps.deploy_config.outputs.aci_image }} + APP_SETTINGS_JSON: ${{ vars.APP_SETTINGS_JSON || '{}' }} + + - name: Restart function app + if: ${{ steps.deploy_config.outputs.restart_app == 'true' }} + shell: bash + run: | + az functionapp restart \ + --name "${{ steps.deploy_config.outputs.function_app_name }}" \ + --resource-group "${{ steps.deploy_config.outputs.resource_group }}" \ No newline at end of file diff --git a/.github/workflows/prod_workflow_func_app.yaml b/.github/workflows/prod_workflow_func_app.yaml new file mode 100644 index 0000000..923c11f --- /dev/null +++ b/.github/workflows/prod_workflow_func_app.yaml @@ -0,0 +1,105 @@ +--- +# Prod Workflow – build image and deploy to Azure Function App (Production environment). +# +# Required secrets (Settings → Environments → Production → Environment secrets): +# - REGISTRY_DOMAIN – Azure Container Registry login server (e.g. myregistry.azurecr.io) +# - REGISTRY_USERNAME – ACR username +# - REGISTRY_PASSWORD – ACR password +# - REGISTRY_REPO – Repository name in ACR for this app +# - TDEI_CORE_AZURE_CREDS – Azure service principal JSON (for az login) +# +# Required variables (Settings → Environments → Production → Environment variables): +# - FUNCTION_APP_NAME – Azure Function App name to deploy to +# - RESOURCE_GROUP – Azure resource group containing the Function App +# +# Optional variables (defaults used if not set): +# - RESTART_APP – Set to 'true' or 'false'; default 'true' +# - APP_SETTINGS_JSON – JSON object of extra app settings to apply; default '{}' +# +######### Prod Workflow ######## +on: + pull_request: + branches: [main] + types: + - closed + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + Build: + environment: Production + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - uses: actions/checkout@v2 + - uses: azure/docker-login@v1 + with: + login-server: ${{ secrets.REGISTRY_DOMAIN }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Publish image to Azure Registry + run: | + docker build -t ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.sha }} -t ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.ref_name == 'main' && 'prod' || github.ref_name }}${{ github.ref_name != 'main' && '-latest' || 'latest' }} . + docker push ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }} --all-tags + deploy: + environment: Production + runs-on: ubuntu-latest + needs: [Build] + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2.0.0 + with: + creds: ${{ secrets.TDEI_CORE_AZURE_CREDS }} + + - name: Resolve deploy config from environment + id: deploy_config + run: | + echo "function_app_name=${{ vars.FUNCTION_APP_NAME }}" >> "$GITHUB_OUTPUT" + echo "resource_group=${{ vars.RESOURCE_GROUP }}" >> "$GITHUB_OUTPUT" + echo "aci_image=${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "restart_app=${{ vars.RESTART_APP || 'true' }}" >> "$GITHUB_OUTPUT" + + - name: Log target environment + shell: bash + run: | + echo "Deploying to:" + echo " Function App: ${{ steps.deploy_config.outputs.function_app_name }}" + echo " Resource Group: ${{ steps.deploy_config.outputs.resource_group }}" + echo " ACI_IMAGE: ${{ steps.deploy_config.outputs.aci_image }}" + + - name: Update app settings (ACI_IMAGE + extras) + shell: bash + run: | + python - <<'PY' + import json + import os + app_settings = os.environ.get("APP_SETTINGS_JSON", "{}") + data = json.loads(app_settings) if app_settings else {} + data["ACI_IMAGE"] = os.environ["ACI_IMAGE"] + with open("/tmp/appsettings.txt", "w", encoding="utf-8") as handle: + for key, value in data.items(): + handle.write(f"{key}={value}\n") + PY + echo "Updating only provided settings (no clearing of others)." + az functionapp config appsettings set \ + --name "${{ steps.deploy_config.outputs.function_app_name }}" \ + --resource-group "${{ steps.deploy_config.outputs.resource_group }}" \ + --settings $(cat /tmp/appsettings.txt | tr '\n' ' ') + env: + ACI_IMAGE: ${{ steps.deploy_config.outputs.aci_image }} + APP_SETTINGS_JSON: ${{ vars.APP_SETTINGS_JSON || '{}' }} + + - name: Restart function app + if: ${{ steps.deploy_config.outputs.restart_app == 'true' }} + shell: bash + run: | + az functionapp restart \ + --name "${{ steps.deploy_config.outputs.function_app_name }}" \ + --resource-group "${{ steps.deploy_config.outputs.resource_group }}" \ No newline at end of file diff --git a/.github/workflows/stage_workflow_func_app.yaml b/.github/workflows/stage_workflow_func_app.yaml new file mode 100644 index 0000000..07b0e76 --- /dev/null +++ b/.github/workflows/stage_workflow_func_app.yaml @@ -0,0 +1,105 @@ +--- +# Stage Workflow – build image and deploy to Azure Function App (Stage environment). +# +# Required secrets (Settings → Environments → Stage → Environment secrets): +# - REGISTRY_DOMAIN – Azure Container Registry login server (e.g. myregistry.azurecr.io) +# - REGISTRY_USERNAME – ACR username +# - REGISTRY_PASSWORD – ACR password +# - REGISTRY_REPO – Repository name in ACR for this app +# - TDEI_CORE_AZURE_CREDS – Azure service principal JSON (for az login) +# +# Required variables (Settings → Environments → Stage → Environment variables): +# - FUNCTION_APP_NAME – Azure Function App name to deploy to +# - RESOURCE_GROUP – Azure resource group containing the Function App +# +# Optional variables (defaults used if not set): +# - RESTART_APP – Set to 'true' or 'false'; default 'true' +# - APP_SETTINGS_JSON – JSON object of extra app settings to apply; default '{}' +# +######### Stage Workflow ######## +on: + pull_request: + branches: [stage] + types: + - closed + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + Build: + environment: Stage + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - uses: actions/checkout@v2 + - uses: azure/docker-login@v1 + with: + login-server: ${{ secrets.REGISTRY_DOMAIN }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Publish image to Azure Registry + run: | + docker build -t ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.sha }} -t ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.ref_name == 'main' && 'prod' || github.ref_name }}${{ github.ref_name != 'main' && '-latest' || 'latest' }} . + docker push ${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }} --all-tags + deploy: + environment: Stage + runs-on: ubuntu-latest + needs: [Build] + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2.0.0 + with: + creds: ${{ secrets.TDEI_CORE_AZURE_CREDS }} + + - name: Resolve deploy config from environment + id: deploy_config + run: | + echo "function_app_name=${{ vars.FUNCTION_APP_NAME }}" >> "$GITHUB_OUTPUT" + echo "resource_group=${{ vars.RESOURCE_GROUP }}" >> "$GITHUB_OUTPUT" + echo "aci_image=${{ secrets.REGISTRY_DOMAIN }}/${{ secrets.REGISTRY_REPO }}:${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "restart_app=${{ vars.RESTART_APP || 'true' }}" >> "$GITHUB_OUTPUT" + + - name: Log target environment + shell: bash + run: | + echo "Deploying to:" + echo " Function App: ${{ steps.deploy_config.outputs.function_app_name }}" + echo " Resource Group: ${{ steps.deploy_config.outputs.resource_group }}" + echo " ACI_IMAGE: ${{ steps.deploy_config.outputs.aci_image }}" + + - name: Update app settings (ACI_IMAGE + extras) + shell: bash + run: | + python - <<'PY' + import json + import os + app_settings = os.environ.get("APP_SETTINGS_JSON", "{}") + data = json.loads(app_settings) if app_settings else {} + data["ACI_IMAGE"] = os.environ["ACI_IMAGE"] + with open("/tmp/appsettings.txt", "w", encoding="utf-8") as handle: + for key, value in data.items(): + handle.write(f"{key}={value}\n") + PY + echo "Updating only provided settings (no clearing of others)." + az functionapp config appsettings set \ + --name "${{ steps.deploy_config.outputs.function_app_name }}" \ + --resource-group "${{ steps.deploy_config.outputs.resource_group }}" \ + --settings $(cat /tmp/appsettings.txt | tr '\n' ' ') + env: + ACI_IMAGE: ${{ steps.deploy_config.outputs.aci_image }} + APP_SETTINGS_JSON: ${{ vars.APP_SETTINGS_JSON || '{}' }} + + - name: Restart function app + if: ${{ steps.deploy_config.outputs.restart_app == 'true' }} + shell: bash + run: | + az functionapp restart \ + --name "${{ steps.deploy_config.outputs.function_app_name }}" \ + --resource-group "${{ steps.deploy_config.outputs.resource_group }}" \ No newline at end of file diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml index 90ce6b6..67486f1 100644 --- a/.github/workflows/unit_tests.yaml +++ b/.github/workflows/unit_tests.yaml @@ -42,28 +42,35 @@ jobs: else branch_name=$GITHUB_REF_NAME fi - if [[ $branch_name == "main" ]]; then - echo "output_folder=prod" >> $GITHUB_ENV - elif [[ $branch_name == "stage" ]]; then - echo "output_folder=stage" >> $GITHUB_ENV - elif [[ $branch_name == "dev" ]]; then - echo "output_folder=dev" >> $GITHUB_ENV - else - echo "Unknown branch: $branch_name" - exit 1 - fi + + case "$branch_name" in + main) echo "output_folder=prod" >> "$GITHUB_ENV" ;; + stage) echo "output_folder=stage" >> "$GITHUB_ENV" ;; + dev) echo "output_folder=dev" >> "$GITHUB_ENV" ;; + *) echo "Unknown branch: $branch_name"; exit 1 ;; + esac - name: Run tests with coverage run: | - timestamp=$(date '+%Y-%m-%d_%H-%M-%S') + set -o pipefail + timestamp="$(date '+%Y-%m-%d_%H-%M-%S')" mkdir -p test_results log_file="test_results/${timestamp}_report.log" - echo -e "\nTest Cases Report Report\n" >> $log_file - # Run the tests and append output to the log file - python -m coverage run --source=src -m unittest discover -s tests/unit_tests >> $log_file 2>&1 - echo -e "\nCoverage Report\n" >> $log_file - coverage report >> $log_file - coverage xml + + { + echo + echo "Test Cases Report" + echo + } | tee -a "$log_file" + + # Run unittest in verbose mode; mirror output to console and file + python -m coverage run --source=src -m unittest discover -s tests/unit_tests -v 2>&1 | tee -a "$log_file" + test_status=${PIPESTATUS[0]} + + echo -e "\nCoverage Report\n" | tee -a "$log_file" + coverage report 2>&1 | tee -a "$log_file" + + exit $test_status - name: Check coverage run: | diff --git a/.gitignore b/.gitignore index 2191abc..6f19d42 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,5 @@ dmypy.json reports/ test_report.html integration_test_report.html -test_results \ No newline at end of file +test_results +.idea \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4f0b448..a44ce6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ fastapi==0.88.0 pydantic==1.10.4 -python-ms-core==0.0.23 +python-ms-core==0.0.25 uvicorn==0.20.0 html_testRunner==1.2.1 -osm-osw-reformatter==0.3.1 +osm-osw-reformatter==0.3.2 numpy==1.26.4 \ No newline at end of file diff --git a/src/config.py b/src/config.py index 88a4a57..389db87 100644 --- a/src/config.py +++ b/src/config.py @@ -17,7 +17,13 @@ class EventBusSettings: class Settings(BaseSettings): app_name: str = 'python-osw-formatter' event_bus = EventBusSettings() - max_concurrent_messages: int = os.environ.get('MAX_CONCURRENT_MESSAGES', 2) + max_concurrent_messages: int = os.environ.get('MAX_CONCURRENT_MESSAGES', 1) + # Single-run worker should consume one message and then shut down. + max_receivable_messages: int = os.environ.get('MAX_RECEIVABLE_MESSAGES', 1) + # Wait for queue message completion/abandon settlement before terminating process. + message_settle_wait_seconds: float = os.environ.get('MESSAGE_SETTLE_WAIT_SECONDS', 10.0) + # Delay gives queue client time to settle/complete the in-flight message before exit. + shutdown_delay_seconds: float = os.environ.get('SHUTDOWN_DELAY_SECONDS', 2.0) def get_root_directory(self) -> str: return os.path.dirname(os.path.abspath(__file__)) diff --git a/src/service/osw_formatter_service.py b/src/service/osw_formatter_service.py index 2e398cc..564b3cc 100644 --- a/src/service/osw_formatter_service.py +++ b/src/service/osw_formatter_service.py @@ -1,6 +1,7 @@ import gc import os import time +import signal import logging import traceback import urllib.parse @@ -36,6 +37,7 @@ def __init__(self): self.logger = self.core.get_logger() self.storage_client = self.core.get_storage_client() self.container_name = self._settings.event_bus.container_name + self._shutdown_triggered = threading.Event() self.listening_thread = threading.Thread(target=self.start_listening) self.listening_thread.start() self.download_dir = self._settings.get_download_directory() @@ -56,7 +58,7 @@ def process(message: QueueMessage) -> None: messageId=message.messageId, data=queue_message['data'] ) - logger.info(f'Received on demand request: {ondemand_request.data.jobId}') + logger.info(f'Received on demand request: {ondemand_request.data.jobId}, Core: {Core.__version__}') self.process_on_demand_format(request=ondemand_request) except Exception as e: logger.error(f"Error occurred while processing on demand message, {e}") @@ -83,8 +85,11 @@ def process(message: QueueMessage) -> None: upload_message=message) self.listening_topic.subscribe( - subscription=self.subscription_name, callback=process + subscription=self.subscription_name, callback=process, + max_receivable_messages=self._settings.max_receivable_messages ) + logger.info('Listener finished processing available messages; stopping server/container.') + self._stop_server_and_container(delay_seconds=self._settings.shutdown_delay_seconds) def format(self, received_message: OSWValidationMessage): tdei_record_id: str = "" @@ -300,3 +305,27 @@ def _prepare_upload_file(self, formatter: OSWFormat, generated_files): def stop_listening(self): self.listening_thread.join(timeout=0) return + + def _stop_server_and_container(self, delay_seconds: float = 0.0): + """ + Attempt to gracefully stop the current process (stopping FastAPI/uvicorn and the Docker container). + """ + logger.info('Gracefully stopping FastAPI/uvicorn and Docker container') + if self._shutdown_triggered.is_set(): + logger.info('Server stop already in progress; skipping duplicate trigger.') + return + self._shutdown_triggered.set() + logger.info('Server stop triggered; scheduling shutdown.') + def _terminate(): + if delay_seconds: + time.sleep(delay_seconds) + try: + logger.info('Sending SIGTERM to stop server/container.') + os.kill(os.getpid(), signal.SIGTERM) + except Exception as err: + logger.warning(f'Error occurred while sending SIGTERM: {err}') + finally: + logger.info('Forcing process exit to stop server/container.') + os._exit(0) + + threading.Thread(target=_terminate, daemon=True).start() \ No newline at end of file diff --git a/tests/unit_tests/service/test_osw_formatter_service.py b/tests/unit_tests/service/test_osw_formatter_service.py index 844b4e3..163750f 100644 --- a/tests/unit_tests/service/test_osw_formatter_service.py +++ b/tests/unit_tests/service/test_osw_formatter_service.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Optional from dataclasses import dataclass, asdict -from unittest.mock import Mock, MagicMock, patch, call +from unittest.mock import ANY, Mock, MagicMock, patch, call from src.service.osw_formatter_service import OSWFomatterService from src.osw_format import OSWFormat from src.models.queue_message_content import ValidationResult @@ -50,6 +50,9 @@ def setUp(self): self.formatter.core.return_value = MagicMock() self.formatter.core.get_topic = MagicMock() self.formatter.core.get_topic.return_value = MagicMock() + self.formatter._settings = MagicMock() + self.formatter._settings.max_receivable_messages = 1 + self.formatter._settings.shutdown_delay_seconds = 2.0 self.formatter.download_dir = DOWNLOAD_PATH @patch.object(OSWFomatterService, 'start_listening') @@ -60,6 +63,19 @@ def test_start_listening(self, mock_start_listening): # Assert mock_start_listening.assert_called_once() + @patch.object(OSWFomatterService, '_stop_server_and_container') + def test_start_listening_stops_container_after_subscribe_returns(self, mock_stop_server_and_container): + self.formatter.start_listening() + + self.formatter.listening_topic.subscribe.assert_called_once_with( + subscription=self.formatter.subscription_name, + callback=ANY, + max_receivable_messages=self.formatter._settings.max_receivable_messages, + ) + mock_stop_server_and_container.assert_called_once_with( + delay_seconds=self.formatter._settings.shutdown_delay_seconds + ) + @patch('src.service.osw_formatter_service.OSWFormat') @patch.object(OSWFormat, 'download_single_file') @patch.object(OSWFomatterService, 'send_status') diff --git a/tests/unit_tests/service/test_service.py b/tests/unit_tests/service/test_service.py index cc8d016..addc118 100644 --- a/tests/unit_tests/service/test_service.py +++ b/tests/unit_tests/service/test_service.py @@ -8,21 +8,36 @@ class TestOSWFormatterService(unittest.TestCase): - @patch('src.service.osw_formatter_service.Settings') - @patch('src.service.osw_formatter_service.Core') - def setUp(self, mock_core, mock_settings): - # Mock Settings - mock_settings.return_value.event_bus.validation_subscription = 'test_subscription' - mock_settings.return_value.event_bus.validation_topic = 'test_request_topic' - mock_settings.return_value.event_bus.formatter_topic = 'test_response_topic' - mock_settings.return_value.max_concurrent_messages = 10 - mock_settings.return_value.get_download_directory.return_value = '/tmp' - mock_settings.return_value.event_bus.container_name = 'test_container' - - # Mock Core + def setUp(self): + self.settings_patcher = patch.object(OSWFomatterService, '_settings') + mock_settings = self.settings_patcher.start() + self.addCleanup(self.settings_patcher.stop) + mock_settings.event_bus = MagicMock() + mock_settings.event_bus.validation_subscription = 'test_subscription' + mock_settings.event_bus.validation_topic = 'test_request_topic' + mock_settings.event_bus.formatter_topic = 'test_response_topic' + mock_settings.event_bus.container_name = 'test_container' + mock_settings.max_concurrent_messages = 10 + mock_settings.max_receivable_messages = 1 + mock_settings.shutdown_delay_seconds = 0 + mock_settings.get_download_directory.return_value = '/tmp' + + self.core_patcher = patch('src.service.osw_formatter_service.Core') + mock_core = self.core_patcher.start() + self.addCleanup(self.core_patcher.stop) + mock_core.__version__ = '0.test' mock_core.return_value.get_topic.return_value = MagicMock() mock_core.return_value.get_storage_client.return_value = MagicMock() + self.thread_patcher = patch('src.service.osw_formatter_service.threading.Thread') + mock_thread = self.thread_patcher.start() + self.addCleanup(self.thread_patcher.stop) + mock_thread.return_value = MagicMock() + + self.stop_server_patcher = patch.object(OSWFomatterService, '_stop_server_and_container') + self.stop_server_patcher.start() + self.addCleanup(self.stop_server_patcher.stop) + # Initialize InclinationService with mocked dependencies self.service = OSWFomatterService() self.service.storage_client = MagicMock() @@ -77,6 +92,7 @@ def test_on_demand_request_success(self, mock_queue_message, mock_logger, mock_r self.service.process_on_demand_format = MagicMock() # Act + self.service.start_listening() callback = self.service.listening_topic.subscribe.call_args[1]['callback'] callback(mock_message) @@ -86,7 +102,8 @@ def test_on_demand_request_success(self, mock_queue_message, mock_logger, mock_r messageId='1234', data={'jobId': '5678'} ) - mock_logger.info.assert_called_with('Received on demand request: 5678') + logged_message = mock_logger.info.call_args[0][0] + self.assertIn('Received on demand request: 5678', logged_message) self.service.process_on_demand_format.assert_called_once_with(request=mock_request.return_value) @@ -105,6 +122,7 @@ def test_on_demand_request_exception(self, mock_logger, mock_response, mock_requ self.service.send_on_demand_response = MagicMock() # Act + self.service.start_listening() callback = self.service.listening_topic.subscribe.call_args[1]['callback'] callback(mock_message) diff --git a/tests/unit_tests/test_osw_format.py b/tests/unit_tests/test_osw_format.py index ce9daf6..3eced93 100644 --- a/tests/unit_tests/test_osw_format.py +++ b/tests/unit_tests/test_osw_format.py @@ -208,6 +208,8 @@ def test_download_single_file_file_not_found(self): class TesOSWFormatCleanUp(unittest.TestCase): + def setUp(self): + os.makedirs(DOWNLOAD_FILE_PATH, exist_ok=True) def test_clean_up_file_exists(self): filename = 'file1.txt'