From 3e5fac69f2a13a4a63999433383cf4214dd64801 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 16:13:40 +0100 Subject: [PATCH 01/11] Make all tests runnable through `hatch test` --- fedora-image-tester/pyproject.toml | 16 +++++++++++++++- fedora-image-uploader-messages/pyproject.toml | 3 +++ fedora-image-uploader/pyproject.toml | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/fedora-image-tester/pyproject.toml b/fedora-image-tester/pyproject.toml index 753d990..bda2201 100644 --- a/fedora-image-tester/pyproject.toml +++ b/fedora-image-tester/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "fedfind", "fedora-messaging", "fedora-image-uploader-messages", - "mslisa[azure,aws]", + "mslisa[azure,aws] @ git+https://github.com/microsoft/lisa.git", ] [project.optional-dependencies] @@ -33,6 +33,20 @@ path = "fedora_image_tester/__init__.py" # Required for LISA since they don't publish releases to PyPI yet allow-direct-references = true +[tool.hatch.envs.hatch-test] +features = ["test"] +extra-dependencies = [ + "fedora-image-uploader-messages @ {root:uri}/../fedora-image-uploader-messages", +] + +# mslisa pins pluggy<0.14 which conflicts with the pluggy>=1.4 required by +# modern pytest. Tell uv (hatch's resolver) to ignore mslisa's pluggy pin so +# the test environment can resolve. +[tool.uv] +override-dependencies = [ + "pluggy>=1.4", +] + [tool.black] line-length = 100 diff --git a/fedora-image-uploader-messages/pyproject.toml b/fedora-image-uploader-messages/pyproject.toml index 117af74..e41b253 100644 --- a/fedora-image-uploader-messages/pyproject.toml +++ b/fedora-image-uploader-messages/pyproject.toml @@ -52,6 +52,9 @@ test = [ [tool.hatch.version] path = "fedora_image_uploader_messages/__init__.py" +[tool.hatch.envs.hatch-test] +features = ["test"] + [tool.black] line-length = 100 diff --git a/fedora-image-uploader/pyproject.toml b/fedora-image-uploader/pyproject.toml index 182d4b5..773c9f8 100644 --- a/fedora-image-uploader/pyproject.toml +++ b/fedora-image-uploader/pyproject.toml @@ -54,6 +54,12 @@ fedora-image-uploader = "fedora_image_uploader.cli:main" [tool.hatch.version] path = "fedora_image_uploader/__init__.py" +[tool.hatch.envs.hatch-test] +features = ["test", "aws", "azure", "gcp"] +extra-dependencies = [ + "fedora-image-uploader-messages @ {root:uri}/../fedora-image-uploader-messages", +] + [tool.black] line-length = 100 From 86fc6dc6ffa3c6e617254a4613f4feecda0fa164 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 16:18:41 +0100 Subject: [PATCH 02/11] Skip Azure tests if the Azure environment variables aren't set --- fedora-image-tester/tests/test_azure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fedora-image-tester/tests/test_azure.py b/fedora-image-tester/tests/test_azure.py index b2c80b0..46b5a5f 100644 --- a/fedora-image-tester/tests/test_azure.py +++ b/fedora-image-tester/tests/test_azure.py @@ -49,6 +49,7 @@ def valid_message(): return message +@pytest.mark.skipif(not os.environ.get("AZURE_SUBSCRIPTION_ID"), reason="Azure tests require Azure credentials") class TestConsumer: # pylint: disable=protected-access """Test class for Consumer.""" From 6521f0e9f9860df87d07d4907d5ba0fc791794c5 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 16:23:27 +0100 Subject: [PATCH 03/11] Skip GCP tests if the GCP environment variables aren't set --- fedora-image-uploader/tests/test_gcp.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fedora-image-uploader/tests/test_gcp.py b/fedora-image-uploader/tests/test_gcp.py index a2493b0..3ae617b 100644 --- a/fedora-image-uploader/tests/test_gcp.py +++ b/fedora-image-uploader/tests/test_gcp.py @@ -24,6 +24,7 @@ from fedora_image_uploader import Uploader }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_image_filter(fixtures_dir): consumer = Uploader() handler = consumer.handlers["gcp"] @@ -66,6 +67,7 @@ def test_image_filter(fixtures_dir): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_messages(fixtures_dir): with open(os.path.join(fixtures_dir, "messages/rc_compose_40.json")) as fd: msg = message.load_message(json.load(fd)) @@ -138,6 +140,7 @@ def test_messages(fixtures_dir): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_import_image(fixtures_dir): consumer = Uploader() handler = consumer.handlers["gcp"] @@ -252,6 +255,7 @@ def test_import_image(fixtures_dir): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_promote_image_eln_rawhide(): """Assert this is a no-op for eln and rawhide""" consumer = Uploader() @@ -304,6 +308,7 @@ def test_promote_image_eln_rawhide(): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_needs_promotion(): consumer = Uploader() handler = consumer.handlers["gcp"] @@ -365,6 +370,7 @@ def test_needs_promotion(): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_no_promotion(): consumer = Uploader() handler = consumer.handlers["gcp"] @@ -417,6 +423,7 @@ def test_no_promotion(): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_cleanup_skips_unmanaged_images(): consumer = Uploader() handler = consumer.handlers["gcp"] @@ -447,6 +454,7 @@ def test_cleanup_skips_unmanaged_images(): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_cleanup_rawhide(): consumer = Uploader() handler = consumer.handlers["gcp"] @@ -507,6 +515,7 @@ def test_cleanup_rawhide(): }, }, ) +@pytest.mark.skipif(not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP tests require GCP credentials") def test_cleanup_eol(): consumer = Uploader() handler = consumer.handlers["gcp"] From 658016a53430ca061bb590916de3ec7a5011e505 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 16:45:57 +0100 Subject: [PATCH 04/11] Fixup failing AWS test Not totally sure about this one, I've removed a couple of test conditions where it seems like an image was expected to be filtered out but the only thing which has changed since the previous line was removing the thing that caused it to be filtered in the first place. Happy to put these back if someone can explain what the intention was. --- fedora-image-uploader/tests/test_aws.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/fedora-image-uploader/tests/test_aws.py b/fedora-image-uploader/tests/test_aws.py index b1245b3..5dd8023 100644 --- a/fedora-image-uploader/tests/test_aws.py +++ b/fedora-image-uploader/tests/test_aws.py @@ -644,18 +644,15 @@ def test_aws_filters(): aws_handler(image, ffrel) assert aws_handler.aws_register_image.call_count == 0 + # No EC2 in path image["arch"] = "x86_64" aws_handler(image, ffrel) - assert aws_handler.aws_register_image.call_count == 1 - - # No EC2 in path - aws_handler(image, ffrel) - assert aws_handler.aws_register_image.call_count == 1 + assert aws_handler.aws_register_image.call_count == 0 # Now EC2 in path image["path"] = "Cloud/x86_64/images/Fedora-Cloud-Base-AmazonEC2.x86_64.raw.xz" aws_handler(image, ffrel) - assert aws_handler.aws_register_image.call_count == 2 + assert aws_handler.aws_register_image.call_count == 1 # ELN metadata = {"composeinfo": {"payload": {"compose": {"date": "20240912", "respin": "2"}}}} @@ -663,10 +660,9 @@ def test_aws_filters(): image["path"] = ( "BaseOS/x86_64/images/Fedora-ELN-Cloud-Base-AmazonEC2-11-20240912.n.0.x86_64.raw.xz" ) - image["subvariant"] = "BaseOS" aws_handler(image, ffrel) - assert aws_handler.aws_register_image.call_count == 3 + assert aws_handler.aws_register_image.call_count == 2 assert ( - aws_handler.aws_register_image.call_args_list[2][0][3] + aws_handler.aws_register_image.call_args_list[1][0][3] == "Fedora-Cloud-Base-AmazonEC2.x86_64-ELN-20240912.2" ) From f7bcc53be9a135a4ddc3e219170938d57470aed0 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 17:00:45 +0100 Subject: [PATCH 05/11] Update tests to handle that since ba8b8e0, we only delete images that we own --- fedora-image-uploader/tests/test_azure.py | 27 +++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/fedora-image-uploader/tests/test_azure.py b/fedora-image-uploader/tests/test_azure.py index 46e1907..8b59e47 100644 --- a/fedora-image-uploader/tests/test_azure.py +++ b/fedora-image-uploader/tests/test_azure.py @@ -17,6 +17,7 @@ from fedora_messaging import testing as fm_testing from freezegun import freeze_time from fedora_image_uploader import Uploader +from fedora_image_uploader.azure import OWNER @pytest.mark.vcr @@ -181,7 +182,7 @@ def test_azure_old_excluded_images(azure_env_vars, azure_fm_conf): azure_handler = consumer.handlers["azure"] azure_handler.azure_compute_client = mock.Mock() azure_handler.azure_blob_client = mock.Mock() - image_definition = GalleryImage(location="eastus") + image_definition = GalleryImage(location="eastus", tags={"owner": OWNER}) image_definition.name = "Fedora-40" now = datetime.datetime.now(datetime.UTC) image_versions = [] @@ -190,7 +191,9 @@ def test_azure_old_excluded_images(azure_env_vars, azure_fm_conf): exclude_from_latest=True, end_of_life_date=now + timedelta(days=7) ) publish_profile.published_date = now + timedelta(seconds=v) - version = GalleryImageVersion(location="eastus", publishing_profile=publish_profile) + version = GalleryImageVersion( + location="eastus", publishing_profile=publish_profile, tags={"owner": OWNER} + ) version.name = f"40.1.{v}" image_versions.append(version) azure_handler.azure_compute_client.gallery_images.list_by_gallery.return_value = [ @@ -218,7 +221,7 @@ def test_azure_end_of_life(azure_env_vars, azure_fm_conf): azure_handler = consumer.handlers["azure"] azure_handler.azure_compute_client = mock.Mock() azure_handler.azure_blob_client = mock.Mock() - image_definition = GalleryImage(location="eastus") + image_definition = GalleryImage(location="eastus", tags={"owner": OWNER}) image_definition.name = "Fedora-40" now = datetime.datetime.now(datetime.UTC) image_versions = [] @@ -227,7 +230,9 @@ def test_azure_end_of_life(azure_env_vars, azure_fm_conf): exclude_from_latest=False, end_of_life_date=now + timedelta(days=v - 2) ) publish_profile.published_date = now - timedelta(days=v) - version = GalleryImageVersion(location="eastus", publishing_profile=publish_profile) + version = GalleryImageVersion( + location="eastus", publishing_profile=publish_profile, tags={"owner": OWNER} + ) version.name = f"40.1.{v}" image_versions.append(version) azure_handler.azure_compute_client.gallery_images.list_by_gallery.return_value = [ @@ -260,7 +265,7 @@ def test_azure_empty_definitions(azure_env_vars, azure_fm_conf): azure_handler = consumer.handlers["azure"] azure_handler.azure_compute_client = mock.Mock() azure_handler.azure_blob_client = mock.Mock() - image_definition = GalleryImage(location="eastus") + image_definition = GalleryImage(location="eastus", tags={"owner": OWNER}) image_definition.name = "Fedora-40" azure_handler.azure_compute_client.gallery_images.list_by_gallery.return_value = [ image_definition @@ -284,7 +289,7 @@ def test_azure_old_included_images(azure_env_vars, azure_fm_conf): azure_handler = consumer.handlers["azure"] azure_handler.azure_compute_client = mock.Mock() azure_handler.azure_blob_client = mock.Mock() - image_definition = GalleryImage(location="eastus") + image_definition = GalleryImage(location="eastus", tags={"owner": OWNER}) image_definition.name = "Fedora-40" now = datetime.datetime.now(datetime.UTC) image_versions = [] @@ -293,7 +298,9 @@ def test_azure_old_included_images(azure_env_vars, azure_fm_conf): exclude_from_latest=False, end_of_life_date=now + timedelta(days=7) ) publish_profile.published_date = now + timedelta(seconds=v) - version = GalleryImageVersion(location="eastus", publishing_profile=publish_profile) + version = GalleryImageVersion( + location="eastus", publishing_profile=publish_profile, tags={"owner": OWNER} + ) version.name = f"40.1.{v}" image_versions.append(version) azure_handler.azure_compute_client.gallery_images.list_by_gallery.return_value = [ @@ -317,7 +324,7 @@ def test_azure_rolling_images(azure_env_vars, azure_fm_conf, rolling_image): azure_handler = consumer.handlers["azure"] azure_handler.azure_compute_client = mock.Mock() azure_handler.azure_blob_client = mock.Mock() - image_definition = GalleryImage(location="eastus") + image_definition = GalleryImage(location="eastus", tags={"owner": OWNER}) image_definition.name = f"Fedora-{rolling_image}" now = datetime.datetime.now(datetime.UTC) image_versions = [] @@ -326,7 +333,9 @@ def test_azure_rolling_images(azure_env_vars, azure_fm_conf, rolling_image): exclude_from_latest=False, end_of_life_date=now + timedelta(days=7) ) publish_profile.published_date = now + timedelta(seconds=v) - version = GalleryImageVersion(location="eastus", publishing_profile=publish_profile) + version = GalleryImageVersion( + location="eastus", publishing_profile=publish_profile, tags={"owner": OWNER} + ) version.name = f"41.1.{v}" image_versions.append(version) azure_handler.azure_compute_client.gallery_images.list_by_gallery.return_value = [ From d7bf37c5ff9bddccdf772577bd7b6875bd6574b8 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 17:22:31 +0100 Subject: [PATCH 06/11] Add Forgejo job to run tests --- .forgejo/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .forgejo/workflows/test.yml diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..976726b --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,18 @@ +on: + pull_request: + types: [opened, synchronize] + push: +jobs: + test: + runs-on: podman + container: + image: quay.io/fedora/fedora:43 + steps: + - run: dnf install hatch cairo-devel cairo-gobject-devel gobject-introspection-devel pkgconf-pkg-config python3-devel -y + - uses: actions/checkout@v6 + - run: hatch test + working-directory: fedora-image-tester + - run: hatch test + working-directory: fedora-image-uploader + - run: hatch test + working-directory: fedora-image-uploader-messages From 93debbb5ec66ba7327d207ea97a5fdb54a8548b0 Mon Sep 17 00:00:00 2001 From: Daniel Milnes Date: Sun, 26 Apr 2026 17:23:09 +0100 Subject: [PATCH 07/11] Add a .gitignore --- .gitignore | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f739f96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,211 @@ +# Created by https://www.toptal.com/developers/gitignore/api/linux,python,vim +# Edit at https://www.toptal.com/developers/gitignore?templates=linux,python,vim + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/linux,python,vim From 1cd3f2a81399f6f27931d4f2f4b83bd96865f021 Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Mon, 27 Apr 2026 15:35:17 -0400 Subject: [PATCH 08/11] Drop the fedora-image-tester tests for Azure None of these tests actually work since I reworked things to deal with the long test times and AMQP ack timeouts. They also reference a lot of since-deleted code. It'd obviously be good to have working tests, but no tests are better than confusingly wrong tests. Signed-off-by: Jeremy Cline --- fedora-image-tester/tests/test_azure.py | 238 ------------------------ 1 file changed, 238 deletions(-) delete mode 100644 fedora-image-tester/tests/test_azure.py diff --git a/fedora-image-tester/tests/test_azure.py b/fedora-image-tester/tests/test_azure.py deleted file mode 100644 index 46b5a5f..0000000 --- a/fedora-image-tester/tests/test_azure.py +++ /dev/null @@ -1,238 +0,0 @@ -"""Unit tests for the Consumer class in azure.py.""" - -import os -import subprocess -from tempfile import TemporaryDirectory -from unittest.mock import MagicMock, Mock, patch - -import pytest -from fedora_image_uploader_messages.publish import AzurePublishedV1 -from fedora_messaging import config as fm_config - -from fedora_image_tester.azure import Consumer - - -@pytest.fixture(scope="module") -def azure_conf(): - """Provide a minimal config for Azure in the fedora-messaging configuration dictionary.""" - with patch.dict( - fm_config.conf["consumer_config"], - { - "azure": { - "region": "westus3", - "subscription_id": "00000000-0000-0000-0000-000000000000", - } - }, - ): - yield - - -@pytest.fixture -def consumer(azure_conf): # pylint: disable=unused-argument - """Create an Consumer instance for testing.""" - return Consumer() - - -@pytest.fixture -def valid_message(): - """Create a valid mock AzurePublishedV1 message.""" - message = Mock(spec=AzurePublishedV1) - message.topic = "org.fedoraproject.prod.fedora_image_uploader.published.v1.azure.test" - message.body = { - "image_definition_name": "Fedora-Cloud-Rawhide-x64", - "image_version_name": "20250101.0", - "image_resource_id": ( - "/subscriptions/test-sub/resourceGroups/test-rg/providers" - "/Microsoft.Compute/galleries/test-gallery" - ), - } - return message - - -@pytest.mark.skipif(not os.environ.get("AZURE_SUBSCRIPTION_ID"), reason="Azure tests require Azure credentials") -class TestConsumer: - # pylint: disable=protected-access - """Test class for Consumer.""" - - def test_get_image_definition_name_success(self, consumer, valid_message): - """Test successful extraction of image definition name.""" - result = consumer._get_image_definition_name(valid_message) - assert result == "Fedora-Cloud-Rawhide-x64" - - def test_get_image_definition_name_invalid_data(self, consumer): - """Test handling of invalid image definition name data.""" - # Test missing field - message = Mock() - message.body = {} - assert consumer._get_image_definition_name(message) is None - - # Test non-string value - message.body = {"image_definition_name": 123} - assert consumer._get_image_definition_name(message) is None - - # Test missing body attribute - del message.body - assert consumer._get_image_definition_name(message) is None - - @patch("fedora_cloud_tests.azure.subprocess.run") - @patch("os.chmod") - def test_generate_ssh_key_pair_success(self, mock_chmod, mock_subprocess, consumer): - """Test successful SSH key pair generation.""" - # Mock subprocess.run to simulate successful ssh-keygen - mock_subprocess.return_value = MagicMock(stdout="Key generated successfully") - - with TemporaryDirectory() as temp_dir: - with patch("os.path.exists", return_value=True): - result = consumer._generate_ssh_key_pair(temp_dir) - - # Verify the method returns the expected private key path - expected_path = os.path.join(temp_dir, "id_ed25519") - assert result == expected_path - - # Verify ssh-keygen was called with correct parameters - mock_subprocess.assert_called_once() - call_args = mock_subprocess.call_args[0][0] - assert "ssh-keygen" in call_args - assert "-t" in call_args and "ed25519" in call_args - assert "-f" in call_args - - # Verify file permissions were set - mock_chmod.assert_called_once_with(expected_path, 0o600) - - @patch("fedora_cloud_tests.azure.subprocess.run") - def test_generate_ssh_key_pair_failures(self, mock_subprocess, consumer): - """Test SSH key pair generation failure cases.""" - with TemporaryDirectory() as temp_dir: - # Test subprocess failure - mock_subprocess.side_effect = subprocess.CalledProcessError(1, "ssh-keygen") - result = consumer._generate_ssh_key_pair(temp_dir) - assert result is None - - # Reset mock for next test - mock_subprocess.side_effect = None - mock_subprocess.return_value = MagicMock(stdout="Key generated") - - # Test file not created scenario - with patch("os.path.exists", return_value=False): - result = consumer._generate_ssh_key_pair(temp_dir) - assert result is None - - def test_get_community_gallery_image_success(self, consumer, valid_message): - """Test successful community gallery image construction.""" - result = consumer.get_community_gallery_image(valid_message) - expected = "westus3/test-sub/Fedora-Cloud-Rawhide-x64/20250101.0" - assert result == expected - - def test_get_community_gallery_image_invalid_cases(self, consumer): - """Test community gallery image extraction with invalid inputs.""" - # Test unsupported Fedora version - message = Mock() - message.body = { - "image_definition_name": "Fedora-Cloud-Unsupported-x64", - "image_version_name": "20250101.0", - "image_resource_id": ( - "/subscriptions/test-sub/resourceGroups/test-rg/providers" - "/Microsoft.Compute/galleries/test-gallery" - ), - } - assert consumer.get_community_gallery_image(message) is None - - # Test invalid message body type - message.body = "not_a_dict" - assert consumer.get_community_gallery_image(message) is None - - # Test missing required fields - message.body = {"image_definition_name": "Fedora-Cloud-Rawhide-x64"} - assert consumer.get_community_gallery_image(message) is None - - # Test invalid resource ID format - message.body = { - "image_definition_name": "Fedora-Cloud-Rawhide-x64", - "image_version_name": "20250101.0", - "image_resource_id": "invalid/format", - } - assert consumer.get_community_gallery_image(message) is None - - # Test resource_id with insufficient parts (code allows empty parts[2]) - message.body = { - "image_definition_name": "Fedora-Cloud-Rawhide-x64", - "image_version_name": "20250101.0", - "image_resource_id": "//", # Results in empty parts[2] but still valid - } - result = consumer.get_community_gallery_image(message) - # The current code allows this and creates: "westus3//Fedora-Cloud-Rawhide-x64/20250101.0" - assert result == "westus3//Fedora-Cloud-Rawhide-x64/20250101.0" - - @patch("fedora_cloud_tests.azure.asyncio.run") - @patch("fedora_cloud_tests.azure.LisaRunner") - @patch.object(Consumer, "_generate_ssh_key_pair") - def test_azure_published_callback_success( - self, - mock_ssh_keygen, - mock_lisa_runner, - mock_asyncio_run, - consumer, - valid_message, - ): # pylint: disable=R0913,R0917 - """Test successful message processing and LISA trigger.""" - mock_runner_instance = MagicMock() - mock_lisa_runner.return_value = mock_runner_instance - mock_ssh_keygen.return_value = "/tmp/test_key" - - consumer.azure_published_callback(valid_message) - mock_lisa_runner.assert_called_once_with() - mock_asyncio_run.assert_called_once() - - @patch("fedora_cloud_tests.azure.asyncio.run") - @patch("fedora_cloud_tests.azure.LisaRunner") - def test_azure_published_callback_unsupported_image( - self, mock_lisa_runner, mock_asyncio_run, consumer - ): - """Test handling when community gallery image cannot be processed.""" - message = Mock() - message.topic = "test.topic" - message.body = {"image_definition_name": "Fedora-Cloud-Unsupported-x64"} - - consumer.azure_published_callback(message) - mock_lisa_runner.assert_not_called() - mock_asyncio_run.assert_not_called() - - @patch( - "fedora_cloud_tests.azure.asyncio.run", - side_effect=OSError("LISA execution failed"), - ) - @patch("fedora_cloud_tests.azure.LisaRunner") - @patch.object(Consumer, "_generate_ssh_key_pair") - def test_azure_published_callback_lisa_exception( - self, - mock_ssh_keygen, - mock_lisa_runner, - mock_asyncio_run, - consumer, - valid_message, - ): # pylint: disable=R0913,R0917 - """Test exception handling when LISA execution fails.""" - mock_runner_instance = MagicMock() - mock_lisa_runner.return_value = mock_runner_instance - mock_ssh_keygen.return_value = "/tmp/test_key" - - # Should not raise exception, just log it - consumer.azure_published_callback(valid_message) - mock_asyncio_run.assert_called_once() - - def test_azure_published_callback_message_validation_exception(self, consumer): - """Test exception handling during message type validation.""" - # Create a message that will cause a TypeError during isinstance check - # by making it not a proper message type - invalid_message = Mock() - invalid_message.topic = "test.topic" - invalid_message.body = "not_a_dict" # This will cause isinstance issues - - # This should not crash but should log errors and return early - consumer.azure_published_callback(invalid_message) - - def test_call_method_delegates_to_callback(self, consumer, valid_message): - """Test that __call__ method properly delegates to azure_published_callback.""" - with patch.object(consumer, "azure_published_callback") as mock_callback: - consumer(valid_message) - mock_callback.assert_called_once_with(valid_message) From 9fce8ba796c86957834b63523e4dab23152b9ca9 Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Mon, 27 Apr 2026 15:46:48 -0400 Subject: [PATCH 09/11] fedora-image-tester: Tweak requires to fix the container build If we list the git repo installing a wheel of this package fails. We have some silly work-arounds for LISA not being on PyPI (but really really need to go chase that down). Signed-off-by: Jeremy Cline --- fedora-image-tester/pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fedora-image-tester/pyproject.toml b/fedora-image-tester/pyproject.toml index bda2201..e2bf9b7 100644 --- a/fedora-image-tester/pyproject.toml +++ b/fedora-image-tester/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "fedfind", "fedora-messaging", "fedora-image-uploader-messages", - "mslisa[azure,aws] @ git+https://github.com/microsoft/lisa.git", + "mslisa[azure,aws]", ] [project.optional-dependencies] @@ -37,6 +37,7 @@ allow-direct-references = true features = ["test"] extra-dependencies = [ "fedora-image-uploader-messages @ {root:uri}/../fedora-image-uploader-messages", + "mslisa[azure,aws] @ git+https://github.com/microsoft/lisa.git", ] # mslisa pins pluggy<0.14 which conflicts with the pluggy>=1.4 required by From faf71a4c507d3adc38e73d8bb8b331f8769092da Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Wed, 10 Jun 2026 11:10:13 -0400 Subject: [PATCH 10/11] Update LISA to 20260521.1 Signed-off-by: Jeremy Cline --- Containerfile.fit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Containerfile.fit b/Containerfile.fit index 276d3db..e61a01f 100644 --- a/Containerfile.fit +++ b/Containerfile.fit @@ -15,7 +15,7 @@ RUN cd /srv/fedora-image-uploader-messages && hatchling build -d /srv/dist/ --ta # Currently LISA isn't on PyPI, but I'm working on it RUN git clone https://github.com/microsoft/lisa.git /srv/lisa WORKDIR /srv/lisa -RUN git checkout 20260330.1 && \ +RUN git checkout 20260521.1 && \ git config --global user.email "cloud@lists.fedoraproject.org" && \ git config --global user.name "Fedora Cloud SIG" && \ sed -i 's/PyGObject <= 3.50.0/PyGObject <= 3.55.0/g' pyproject.toml && \ From 18dec91847a39a2d6ab49cdcefdb637a33829b63 Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Wed, 10 Jun 2026 11:11:29 -0400 Subject: [PATCH 11/11] Temporarily avoid updating to azure-mgmt-compute 38 It's got a massive list of breaking changes and at least one of them impact us. Signed-off-by: Jeremy Cline --- fedora-image-uploader/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora-image-uploader/pyproject.toml b/fedora-image-uploader/pyproject.toml index 773c9f8..191709e 100644 --- a/fedora-image-uploader/pyproject.toml +++ b/fedora-image-uploader/pyproject.toml @@ -27,7 +27,7 @@ aws = [ ] azure = [ "azure-identity", - "azure-mgmt-compute", + "azure-mgmt-compute ~= 37.2.0", "azure-storage-blob", ] gcp = [