Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Documentation on creating custom TestInfra Module #660

Open
filbotblue opened this issue Aug 22, 2022 · 4 comments
Open

Documentation on creating custom TestInfra Module #660

filbotblue opened this issue Aug 22, 2022 · 4 comments

Comments

@filbotblue
Copy link

filbotblue commented Aug 22, 2022

Details

I am wanting to write a custom TestInfra module for something that isn't provided by TestInfra by default. So, I wanted to test this out by doing something trivial. So, I looked at the testinfra/modules/podman.py file and thought I had identified how this should work, but I am not doing it correctly. Here is what my module is doing.

"""TestInfra Plugin Module
"""

from testinfra.modules.base import Module


class Myname(Module):
    """Myname Class. It inherits the TestInfra Module class.
    """

    def __init__(self, name):
        """Class Constructor
        """
        self.name = name
        super().__init__()

    @property
    def is_dudley(self):
        """Verify that the string is Dudley
        """
        string: str = "Dudley"

        return string

I then used Poetry to install the module. Then I use the following test file:

"""Test the TestInfra Module
"""


def test_is_dudley(host):
    """Test if is_dudley returns Dudley.
    """
    dudley = host.myname("phillip")
    assert dudley.is_dudley == "Dudley"

Then I ran the following command to attempt to run the module.

poetry run py.test

Then I got the following error.

➜  pytest-testinfra-dudley
> poetry run py.test
================ test session starts ================
platform linux -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: /var/home/filbot/bluekc/development/pytest-testinfra-dudley
plugins: testinfra-6.8.0
collected 1 item

tests/test_pytest_testinfra_dudley.py F                                                                                                                                                                       [100%]

=============== FAILURES ========================
_______________ test_is_dudley[local] ____________________

host = <testinfra.host.Host local>

    def test_is_dudley(host):
        """Test if is_dudley returns Dudley.
        """
>       dudley = host.myname("phillip")

tests/test_pytest_testinfra_dudley.py:8:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <testinfra.host.Host local>, name = 'myname'

    def __getattr__(self, name):
        if name in testinfra.modules.modules:
            module_class = testinfra.modules.get_module_class(name)
            obj = module_class.get_module(self)
            setattr(self, name, obj)
            return obj
>       raise AttributeError(
            "'{}' object has no attribute '{}'".format(self.__class__.__name__, name)
        )
E       AttributeError: 'Host' object has no attribute 'myname'

../../../.cache/pypoetry/virtualenvs/pytest-testinfra-dudley-djwyNVfS-py3.10/lib/python3.10/site-packages/testinfra/host.py:120: AttributeError
=============== short test summary info ==============
FAILED tests/test_pytest_testinfra_dudley.py::test_is_dudley[local] - AttributeError: 'Host' object has no attribute 'myname'
=============== 1 failed in 0.03s =================

Question

Is there any documentation on how to extend TestInfra with a custom module plugin? I'm not doing something correctly.

@FilBot3
Copy link

FilBot3 commented Oct 24, 2022

I also created a Reddit post asking for any pointers as well.

@Tetha
Copy link

Tetha commented Nov 20, 2023

Moin,

I've been looking at this because I needed something similar. The main blocker for getting modules from other python modules loaded is in the testinfra.modules.get_modules method:

def get_module_class(name: str) -> type["testinfra.modules.base.Module"]:
modname, classname = modules[name].split(":")
modname = ".".join([__name__, modname])
module = importlib.import_module(modname)
return getattr(module, classname) # type: ignore[no-any-return]

This pretty much hardcodes all modules to live in python modules testinfra.modules.foo.

Changing this wouldn't be such a massive change. I've done so in a branch on a fork. I mostly split the builtin modules into a separate object and added a decorator to register modules, so external code only needs to care about the decorator:

https://github.com/Serviceware/pytest-testinfra/blob/ecd50844024f00a4fcf83b2a360fbaf220c91a4a/testinfra/modules/__init__.py#L47-L57

https://github.com/Serviceware/pytest-testinfra/blob/ecd50844024f00a4fcf83b2a360fbaf220c91a4a/testinfra/modules/base.py#L17-L39

With this, I can setup a new module like this (just using firewalld as an example, as that's currently on my mind, and I haven't really implemented any proper parsing yet):

from testinfra.modules.base import Module, register_module


@register_module("firewalld_zone")
class FirewalldZone(Module):
    def __init__(self, zone):
        self.zone = zone
        super().__init__()

    def info(self):
        return self.check_output("firewall-cmd --info-zone {}".format(self.zone))
def test_foo(host):
    zone = host.firewalld_zone("public")
    ...

This should be fine from a security perspective, because I have to import the module anyhow to execute the module registration.

I'm mostly concerned about introducing public APIs, but if the maintainers are fine with this, I could finish this up with some documentation and open a PR. Tests are running cleanly on the branch for me.

@snikiten-harmonicinc
Copy link

I was looking for this feature.
It would be nice to be able to extend the testinfra with custom modules without modifying the package.

@pecigonzalo
Copy link

@philpep sorry for the ping, but can I call your attention to this issue? I think we even have a way to address it thanks to @Tetha and since this was opened back in 2022, it would be awesome to find a solution.

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

No branches or pull requests

5 participants