Skip to content

tests: add @payload_type decorator for DNF VM provisioning#1310

Draft
KKoukiou wants to merge 1 commit into
rhinstaller:mainfrom
KKoukiou:dnf-provision
Draft

tests: add @payload_type decorator for DNF VM provisioning#1310
KKoukiou wants to merge 1 commit into
rhinstaller:mainfrom
KKoukiou:dnf-provision

Conversation

@KKoukiou

Copy link
Copy Markdown
Contributor

That replaces the payload_type in the provision attribute. The custom provision is incompatible with non-destructive tests.

This fixes the Payload Basic test being skipped with: "Cannot provision multiple machines if a specific machine address is specified"

https://cockpit-ci-logs.s3.us-east-1.amazonaws.com/pull-1151-de40b6ef-20260520-073235-fedora-rawhide-boot-dnf/log.html

That replaces the payload_type in the provision attribute. The custom
provision is incompatible with non-destructive tests.

This fixes the Payload Basic test being skipped with:
"Cannot provision multiple machines if a specific machine address is
specified"

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In setUp, the payload_type decorator will overwrite any existing payload_type in self.provision for all machines; if mixed payloads or preconfigured payloads are ever needed, consider only setting it when not already present or scoping it to a specific machine.
  • The payload_type decorator currently lowercases and blindly assigns the mode; adding simple validation (e.g., restricting to known payload types like liveimg/dnf) would help catch typos or unsupported values early.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `setUp`, the `payload_type` decorator will overwrite any existing `payload_type` in `self.provision` for all machines; if mixed payloads or preconfigured payloads are ever needed, consider only setting it when not already present or scoping it to a specific machine.
- The `payload_type` decorator currently lowercases and blindly assigns the mode; adding simple validation (e.g., restricting to known payload types like `liveimg`/`dnf`) would help catch typos or unsupported values early.

## Individual Comments

### Comment 1
<location path="test/anacondalib.py" line_range="70-75" />
<code_context>

     def setUp(self):
         method = getattr(self, self._testMethodName)
+        payload_type = getattr(method, "payload_type", None)
+        if payload_type and not self.is_nondestructive():
+            provision = dict(self.provision or {"machine1": {}})
+            self.provision = {
+                key: {**dict(opts), "payload_type": payload_type}
+                for key, opts in provision.items()
+            }
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests that exercise the new `payload_type` setup behavior (destructive vs. non-destructive and default provision).

The new `setUp` logic is central to payload behavior but isn't directly tested. Please add tests that:

- For a destructive test decorated with `@payload_type("dnf")`, assert that each machine in `self.provision` includes `{"payload_type": "dnf"}` while existing fields (e.g. `memory`, `image`) remain unchanged.
- For a non-destructive test (`is_nondestructive()` returns `True`), assert that `@payload_type(...)` does not modify `self.provision`.
- When `self.provision` starts as `None`, assert that the implicit `{"machine1": {}}` default is used and receives the expected `payload_type`.

A focused test (or helper) around `AnacondaTestCase.setUp` would help prevent regressions in decorator-driven provisioning.

Suggested implementation:

```python
    def setUp(self):
        method = getattr(self, self._testMethodName)
        payload_type = getattr(method, "payload_type", None)
        if payload_type and not self.is_nondestructive():
            provision = dict(self.provision or {"machine1": {}})
            self.provision = {
                key: {**dict(opts), "payload_type": payload_type}
                for key, opts in provision.items()
            }


class _PayloadTypeTestCase(AnacondaTestCase):
    """Helper TestCase to exercise payload_type-driven provisioning."""

    def is_nondestructive(self):
        # Default to destructive; individual tests can override via subclassing
        return False


class PayloadTypeSetupTests(unittest.TestCase):
    def test_destructive_test_applies_payload_type_to_all_machines(self):
        class DestructiveCase(_PayloadTypeTestCase):
            @payload_type("dnf")
            def test_method(self):
                pass

        case = DestructiveCase("test_method")
        case.provision = {
            "machine1": {"memory": "2G", "image": "base.img"},
            "machine2": {"memory": "4G", "image": "advanced.img"},
        }

        case.setUp()

        self.assertEqual(
            {
                "machine1": {
                    "memory": "2G",
                    "image": "base.img",
                    "payload_type": "dnf",
                },
                "machine2": {
                    "memory": "4G",
                    "image": "advanced.img",
                    "payload_type": "dnf",
                },
            },
            case.provision,
        )

    def test_nondestructive_test_does_not_modify_provision(self):
        class NonDestructiveCase(_PayloadTypeTestCase):
            def is_nondestructive(self):
                return True

            @payload_type("dnf")
            def test_method(self):
                pass

        original_provision = {
            "machine1": {"memory": "2G"},
            "machine2": {"memory": "4G"},
        }

        case = NonDestructiveCase("test_method")
        case.provision = copy.deepcopy(original_provision)

        case.setUp()

        self.assertEqual(original_provision, case.provision)

    def test_default_provision_used_when_none(self):
        class DefaultProvisionCase(_PayloadTypeTestCase):
            @payload_type("dnf")
            def test_method(self):
                pass

        case = DefaultProvisionCase("test_method")
        case.provision = None

        case.setUp()

        self.assertEqual(
            {"machine1": {"payload_type": "dnf"}},
            case.provision,
        )

```

1. Ensure the following are imported at the top of `test/anacondalib.py` (or adjust to match existing import style/module layout):
   - `import unittest` (likely already present)
   - `import copy`
   - `from anacondalib import AnacondaTestCase, payload_type` (or the appropriate module path where `AnacondaTestCase` and `payload_type` are defined).
2. If `AnacondaTestCase` already has its own tests or base helpers in this file, you may want to place `_PayloadTypeTestCase` and `PayloadTypeSetupTests` near them for coherence.
3. There appear to be two `setUp` definitions in the snippet you shared. The older, no-op `setUp` (`def setUp(self): method = getattr(self, self._testMethodName)`) should likely be removed to avoid confusion; keep only the payload-aware implementation.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread test/anacondalib.py
Comment on lines +70 to +75
payload_type = getattr(method, "payload_type", None)
if payload_type and not self.is_nondestructive():
provision = dict(self.provision or {"machine1": {}})
self.provision = {
key: {**dict(opts), "payload_type": payload_type}
for key, opts in provision.items()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests that exercise the new payload_type setup behavior (destructive vs. non-destructive and default provision).

The new setUp logic is central to payload behavior but isn't directly tested. Please add tests that:

  • For a destructive test decorated with @payload_type("dnf"), assert that each machine in self.provision includes {"payload_type": "dnf"} while existing fields (e.g. memory, image) remain unchanged.
  • For a non-destructive test (is_nondestructive() returns True), assert that @payload_type(...) does not modify self.provision.
  • When self.provision starts as None, assert that the implicit {"machine1": {}} default is used and receives the expected payload_type.

A focused test (or helper) around AnacondaTestCase.setUp would help prevent regressions in decorator-driven provisioning.

Suggested implementation:

    def setUp(self):
        method = getattr(self, self._testMethodName)
        payload_type = getattr(method, "payload_type", None)
        if payload_type and not self.is_nondestructive():
            provision = dict(self.provision or {"machine1": {}})
            self.provision = {
                key: {**dict(opts), "payload_type": payload_type}
                for key, opts in provision.items()
            }


class _PayloadTypeTestCase(AnacondaTestCase):
    """Helper TestCase to exercise payload_type-driven provisioning."""

    def is_nondestructive(self):
        # Default to destructive; individual tests can override via subclassing
        return False


class PayloadTypeSetupTests(unittest.TestCase):
    def test_destructive_test_applies_payload_type_to_all_machines(self):
        class DestructiveCase(_PayloadTypeTestCase):
            @payload_type("dnf")
            def test_method(self):
                pass

        case = DestructiveCase("test_method")
        case.provision = {
            "machine1": {"memory": "2G", "image": "base.img"},
            "machine2": {"memory": "4G", "image": "advanced.img"},
        }

        case.setUp()

        self.assertEqual(
            {
                "machine1": {
                    "memory": "2G",
                    "image": "base.img",
                    "payload_type": "dnf",
                },
                "machine2": {
                    "memory": "4G",
                    "image": "advanced.img",
                    "payload_type": "dnf",
                },
            },
            case.provision,
        )

    def test_nondestructive_test_does_not_modify_provision(self):
        class NonDestructiveCase(_PayloadTypeTestCase):
            def is_nondestructive(self):
                return True

            @payload_type("dnf")
            def test_method(self):
                pass

        original_provision = {
            "machine1": {"memory": "2G"},
            "machine2": {"memory": "4G"},
        }

        case = NonDestructiveCase("test_method")
        case.provision = copy.deepcopy(original_provision)

        case.setUp()

        self.assertEqual(original_provision, case.provision)

    def test_default_provision_used_when_none(self):
        class DefaultProvisionCase(_PayloadTypeTestCase):
            @payload_type("dnf")
            def test_method(self):
                pass

        case = DefaultProvisionCase("test_method")
        case.provision = None

        case.setUp()

        self.assertEqual(
            {"machine1": {"payload_type": "dnf"}},
            case.provision,
        )
  1. Ensure the following are imported at the top of test/anacondalib.py (or adjust to match existing import style/module layout):
    • import unittest (likely already present)
    • import copy
    • from anacondalib import AnacondaTestCase, payload_type (or the appropriate module path where AnacondaTestCase and payload_type are defined).
  2. If AnacondaTestCase already has its own tests or base helpers in this file, you may want to place _PayloadTypeTestCase and PayloadTypeSetupTests near them for coherence.
  3. There appear to be two setUp definitions in the snippet you shared. The older, no-op setUp (def setUp(self): method = getattr(self, self._testMethodName)) should likely be removed to avoid confusion; keep only the payload-aware implementation.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a @payload_type decorator to allow specifying the installer VM payload type at the test method level, updating anacondalib.py and migrating existing tests to this new pattern. Feedback highlights a missing propagation of the payload_type to the machine instance for non-destructive tests, which may cause issues with certain helpers. Additionally, a simplification was suggested for the dictionary comprehension in the setUp method to remove redundant dict() calls.

Comment thread test/anacondalib.py
def setUp(self):
method = getattr(self, self._testMethodName)
payload_type = getattr(method, "payload_type", None)
if payload_type and not self.is_nondestructive():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation correctly avoids modifying self.provision for non-destructive tests to prevent infrastructure errors. However, it fails to propagate the payload_type to the machine instance in those cases. Helpers like Installer and resetPayloadDNF rely on self.machine.payload_type being set. For non-destructive tests, you should ensure self.machine.payload_type = payload_type is called after super().setUp() (around line 101), as self.machine is initialized there.

Comment thread test/anacondalib.py
Comment on lines +72 to +76
provision = dict(self.provision or {"machine1": {}})
self.provision = {
key: {**dict(opts), "payload_type": payload_type}
for key, opts in provision.items()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dict() calls are redundant here. Dictionary unpacking {**opts} already creates a new dictionary, and provision is only used for iteration, so a shallow copy of self.provision is not needed before the comprehension.

Suggested change
provision = dict(self.provision or {"machine1": {}})
self.provision = {
key: {**dict(opts), "payload_type": payload_type}
for key, opts in provision.items()
}
provision = self.provision or {"machine1": {}}
self.provision = {
key: {**opts, "payload_type": payload_type}
for key, opts in provision.items()
}

@KKoukiou KKoukiou marked this pull request as draft May 20, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant