Bug 2272972 - cloud-init fails to build with pytest 8: AssertionError: Unexpectedly used subp.subp
Summary: cloud-init fails to build with pytest 8: AssertionError: Unexpectedly used su...
Keywords:
Status: CLOSED NOTABUG
Alias: None
Product: Fedora
Classification: Fedora
Component: cloud-init
Version: rawhide
Hardware: Unspecified
OS: Unspecified
unspecified
unspecified
Target Milestone: ---
Assignee: Major Hayden 🤠
QA Contact: Fedora Extras Quality Assurance
URL:
Whiteboard:
Depends On:
Blocks: 2256331
TreeView+ depends on / blocked
 
Reported: 2024-04-03 13:58 UTC by Tomáš Hrnčiar
Modified: 2024-04-19 08:30 UTC (History)
9 users (show)

Fixed In Version:
Doc Type: If docs needed, set a value
Doc Text:
Clone Of:
Environment:
Last Closed: 2024-04-19 08:30:37 UTC
Type: Bug
Embargoed:


Attachments (Terms of Use)

Description Tomáš Hrnčiar 2024-04-03 13:58:17 UTC
cloud-init fails to build with pytest 8.

=================================== FAILURES ===================================
_______ TestCLI.test_status_wrapper_init_local_writes_fresh_status_info ________

self = <tests.unittests.test_cli.TestCLI object at 0x7f42d248cbf0>
m_json = <MagicMock name='write_json' id='139923590366032'>
tmpdir = local('/tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0')

    @mock.patch("cloudinit.cmd.main.atomic_helper.write_json")
    def test_status_wrapper_init_local_writes_fresh_status_info(
        self,
        m_json,
        tmpdir,
    ):
        """When running in init-local mode, status_wrapper writes status.json.
    
        Old status and results artifacts are also removed.
        """
        data_d = tmpdir.join("data")
        link_d = tmpdir.join("link")
        # Write old artifacts which will be removed or updated.
        for _dir in data_d, link_d:
            test_helpers.populate_dir(
                str(_dir), {"status.json": "old", "result.json": "old"}
            )
    
        FakeArgs = namedtuple("FakeArgs", ["action", "local", "mode"])
    
        def myaction(name, args):
            # Return an error to watch status capture them
            return "SomeDatasource", ["an error"]
    
        myargs = FakeArgs(("ignored_name", myaction), True, "bogusmode")
>       cli.status_wrapper("init", myargs, data_d, link_d)

tests/unittests/test_cli.py:90: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'init'
args = FakeArgs(action=('ignored_name', <function TestCLI.test_status_wrapper_init_local_writes_fresh_status_info.<locals>.myaction at 0x7f427fbb8b80>), local=True, mode='bogusmode')
data_d = local('/tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0/data')
link_d = local('/tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0/link')

    def status_wrapper(name, args, data_d=None, link_d=None):
        if data_d is None:
            paths = read_cfg_paths()
            data_d = paths.get_cpath("data")
        if link_d is None:
            link_d = os.path.normpath("/run/cloud-init")
    
        status_path = os.path.join(data_d, "status.json")
        status_link = os.path.join(link_d, "status.json")
        result_path = os.path.join(data_d, "result.json")
        result_link = os.path.join(link_d, "result.json")
        root_logger = logging.getLogger()
    
        util.ensure_dirs(
            (
                data_d,
                link_d,
            )
        )
    
        (_name, functor) = args.action
    
        if name == "init":
            if args.local:
                mode = "init-local"
            else:
                mode = "init"
        elif name == "modules":
            mode = "modules-%s" % args.mode
        else:
            raise ValueError("unknown name: %s" % name)
    
        modes = (
            "init",
            "init-local",
            "modules-config",
            "modules-final",
        )
        if mode not in modes:
            raise ValueError(
                "Invalid cloud init mode specified '{0}'".format(mode)
            )
    
        status = None
        if mode == "init-local":
            for f in (status_link, result_link, status_path, result_path):
                util.del_file(f)
        else:
            try:
                status = json.loads(util.load_file(status_path))
            except Exception:
                pass
    
        nullstatus = {
            "errors": [],
            "start": None,
            "finished": None,
        }
    
        if status is None:
            status = {"v1": {}}
            status["v1"]["datasource"] = None
    
        for m in modes:
            if m not in status["v1"]:
                status["v1"][m] = nullstatus.copy()
    
        v1 = status["v1"]
        v1["stage"] = mode
        v1[mode]["start"] = time.time()
>       v1[mode]["recoverable_errors"] = next(
            filter(lambda h: isinstance(h, LogExporter), root_logger.handlers)
        ).export_logs()
E       StopIteration

cloudinit/cmd/main.py:770: StopIteration

The above exception was the direct cause of the following exception:

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x7f42824298a0>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: Optional[
            Union[Type[BaseException], Tuple[Type[BaseException], ...]]
        ] = None,
    ) -> "CallInfo[TResult]":
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: Optional[TResult] = func()

/usr/lib/python3.12/site-packages/_pytest/runner.py:340: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib/python3.12/site-packages/_pytest/runner.py:240: in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
/usr/lib/python3.12/site-packages/pluggy/_hooks.py:501: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/usr/lib/python3.12/site-packages/pluggy/_manager.py:119: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/lib/python3.12/site-packages/_pytest/threadexception.py:87: in pytest_runtest_call
    yield from thread_exception_runtest_hook()
/usr/lib/python3.12/site-packages/_pytest/threadexception.py:63: in thread_exception_runtest_hook
    yield
/usr/lib/python3.12/site-packages/_pytest/unraisableexception.py:90: in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
/usr/lib/python3.12/site-packages/_pytest/unraisableexception.py:65: in unraisable_exception_runtest_hook
    yield
/usr/lib/python3.12/site-packages/_pytest/logging.py:849: in pytest_runtest_call
    yield from self._runtest_for(item, "call")
/usr/lib/python3.12/site-packages/_pytest/logging.py:832: in _runtest_for
    yield
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=5 _state='suspended' tmpfile=<_io....xtIOWrapper name='/dev/null' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>
item = <Function test_status_wrapper_init_local_writes_fresh_status_info>

    @hookimpl(wrapper=True)
    def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
        with self.item_capture("call", item):
>           return (yield)
E           RuntimeError: generator raised StopIteration

/usr/lib/python3.12/site-packages/_pytest/capture.py:883: RuntimeError
------------------------------ Captured log call -------------------------------
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0/link/status.json
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0/link/result.json
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0/data/status.json
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local0/data/result.json
____________ TestCLI.test_status_wrapper_init_local_honor_cloud_dir ____________

self = <tests.unittests.test_cli.TestCLI object at 0x7f42d248c320>
m_json = <MagicMock name='write_json' id='139923589432432'>
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f427fe64440>
tmpdir = local('/tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1')

    @mock.patch("cloudinit.cmd.main.atomic_helper.write_json")
    def test_status_wrapper_init_local_honor_cloud_dir(
        self, m_json, mocker, tmpdir
    ):
        """When running in init-local mode, status_wrapper honors cloud_dir."""
        cloud_dir = tmpdir.join("cloud")
        paths = helpers.Paths({"cloud_dir": str(cloud_dir)})
        mocker.patch(M_PATH + "read_cfg_paths", return_value=paths)
        data_d = cloud_dir.join("data")
        link_d = tmpdir.join("link")
    
        FakeArgs = namedtuple("FakeArgs", ["action", "local", "mode"])
    
        def myaction(name, args):
            # Return an error to watch status capture them
            return "SomeDatasource", ["an_error"]
    
        myargs = FakeArgs(("ignored_name", myaction), True, "bogusmode")
>       cli.status_wrapper("init", myargs, link_d=link_d)  # No explicit data_d

tests/unittests/test_cli.py:128: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'init'
args = FakeArgs(action=('ignored_name', <function TestCLI.test_status_wrapper_init_local_honor_cloud_dir.<locals>.myaction at 0x7f4282724220>), local=True, mode='bogusmode')
data_d = '/tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1/cloud/data'
link_d = local('/tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1/link')

    def status_wrapper(name, args, data_d=None, link_d=None):
        if data_d is None:
            paths = read_cfg_paths()
            data_d = paths.get_cpath("data")
        if link_d is None:
            link_d = os.path.normpath("/run/cloud-init")
    
        status_path = os.path.join(data_d, "status.json")
        status_link = os.path.join(link_d, "status.json")
        result_path = os.path.join(data_d, "result.json")
        result_link = os.path.join(link_d, "result.json")
        root_logger = logging.getLogger()
    
        util.ensure_dirs(
            (
                data_d,
                link_d,
            )
        )
    
        (_name, functor) = args.action
    
        if name == "init":
            if args.local:
                mode = "init-local"
            else:
                mode = "init"
        elif name == "modules":
            mode = "modules-%s" % args.mode
        else:
            raise ValueError("unknown name: %s" % name)
    
        modes = (
            "init",
            "init-local",
            "modules-config",
            "modules-final",
        )
        if mode not in modes:
            raise ValueError(
                "Invalid cloud init mode specified '{0}'".format(mode)
            )
    
        status = None
        if mode == "init-local":
            for f in (status_link, result_link, status_path, result_path):
                util.del_file(f)
        else:
            try:
                status = json.loads(util.load_file(status_path))
            except Exception:
                pass
    
        nullstatus = {
            "errors": [],
            "start": None,
            "finished": None,
        }
    
        if status is None:
            status = {"v1": {}}
            status["v1"]["datasource"] = None
    
        for m in modes:
            if m not in status["v1"]:
                status["v1"][m] = nullstatus.copy()
    
        v1 = status["v1"]
        v1["stage"] = mode
        v1[mode]["start"] = time.time()
>       v1[mode]["recoverable_errors"] = next(
            filter(lambda h: isinstance(h, LogExporter), root_logger.handlers)
        ).export_logs()
E       StopIteration

cloudinit/cmd/main.py:770: StopIteration

The above exception was the direct cause of the following exception:

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x7f427fbb9120>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: Optional[
            Union[Type[BaseException], Tuple[Type[BaseException], ...]]
        ] = None,
    ) -> "CallInfo[TResult]":
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: Optional[TResult] = func()

/usr/lib/python3.12/site-packages/_pytest/runner.py:340: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib/python3.12/site-packages/_pytest/runner.py:240: in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
/usr/lib/python3.12/site-packages/pluggy/_hooks.py:501: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/usr/lib/python3.12/site-packages/pluggy/_manager.py:119: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/lib/python3.12/site-packages/_pytest/threadexception.py:87: in pytest_runtest_call
    yield from thread_exception_runtest_hook()
/usr/lib/python3.12/site-packages/_pytest/threadexception.py:63: in thread_exception_runtest_hook
    yield
/usr/lib/python3.12/site-packages/_pytest/unraisableexception.py:90: in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
/usr/lib/python3.12/site-packages/_pytest/unraisableexception.py:65: in unraisable_exception_runtest_hook
    yield
/usr/lib/python3.12/site-packages/_pytest/logging.py:849: in pytest_runtest_call
    yield from self._runtest_for(item, "call")
/usr/lib/python3.12/site-packages/_pytest/logging.py:832: in _runtest_for
    yield
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=5 _state='suspended' tmpfile=<_io....xtIOWrapper name='/dev/null' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>
item = <Function test_status_wrapper_init_local_honor_cloud_dir>

    @hookimpl(wrapper=True)
    def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
        with self.item_capture("call", item):
>           return (yield)
E           RuntimeError: generator raised StopIteration

/usr/lib/python3.12/site-packages/_pytest/capture.py:883: RuntimeError
------------------------------ Captured log call -------------------------------
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1/link/status.json
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1/link/result.json
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1/cloud/data/status.json
2024-04-02 16:25:11 DEBUG     cloudinit.util:util.py:2052 Attempting to remove /tmp/pytest-of-mockbuild/pytest-0/test_status_wrapper_init_local1/cloud/data/result.json
_______________ TestInit.test_apply_network_on_same_instance_id ________________

self = <tests.unittests.test_stages.TestInit object at 0x7f42d260e480>
m_ubuntu = <MagicMock name='Distro' id='139923557571504'>
caplog = <_pytest.logging.LogCaptureFixture object at 0x7f427df1ffb0>

    @mock.patch("cloudinit.distros.ubuntu.Distro")
    def test_apply_network_on_same_instance_id(self, m_ubuntu, caplog):
        """Only call distro.networking.apply_network_config_names on same
        instance id."""
        self.init.is_new_instance = self._real_is_new_instance
        old_instance_id = os.path.join(
            self.init.paths.get_cpath("data"), "instance-id"
        )
        write_file(old_instance_id, TEST_INSTANCE_ID)
        net_cfg = {
            "version": 1,
            "config": [
                {
                    "subnets": [{"type": "dhcp"}],
                    "type": "physical",
                    "name": "eth9",
                    "mac_address": "42:42:42:42:42:42",
                }
            ],
        }
    
        def fake_network_config():
            return net_cfg, NetworkConfigSource.FALLBACK
    
        self.init._find_networking_config = fake_network_config
    
>       self.init.apply_network_config(True)

tests/unittests/test_stages.py:469: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cloudinit/stages.py:1007: in apply_network_config
    and not should_run_on_boot_event()
cloudinit/stages.py:1001: in should_run_on_boot_event
    and event_enabled_and_metadata_updated(EventType.BOOT)
cloudinit/stages.py:996: in event_enabled_and_metadata_updated
    ) and self.datasource.update_metadata_if_supported([event_type])
cloudinit/sources/__init__.py:903: in update_metadata_if_supported
    result = self.get_data()
cloudinit/sources/__init__.py:438: in get_data
    return_value = self._check_and_get_data()
cloudinit/sources/__init__.py:363: in _check_and_get_data
    if self.override_ds_detect():
cloudinit/sources/__init__.py:347: in override_ds_detect
    if self.dsname.lower() == parse_cmdline().lower():
cloudinit/sources/__init__.py:1187: in parse_cmdline
    return parse_cmdline_or_dmi(util.get_cmdline())
cloudinit/util.py:1622: in get_cmdline
    return _get_cmdline()
cloudinit/util.py:1601: in _get_cmdline
    if is_container():
cloudinit/util.py:2386: in is_container
    if helper():
cloudinit/util.py:2357: in _is_container_systemd
    return _cmd_exits_zero(["systemd-detect-virt", "--quiet", "--container"])
cloudinit/util.py:2350: in _cmd_exits_zero
    subp.subp(cmd)
<string>:3: in subp
    ???
/usr/lib64/python3.12/unittest/mock.py:1134: in __call__
    return self._mock_call(*args, **kwargs)
/usr/lib64/python3.12/unittest/mock.py:1138: in _mock_call
    return self._execute_mock_call(*args, **kwargs)
/usr/lib64/python3.12/unittest/mock.py:1199: in _execute_mock_call
    result = effect(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = ['systemd-detect-virt', '--quiet', '--container'], other_args = ()
kwargs = {}

    def side_effect(args, *other_args, **kwargs):
>       raise AssertionError("Unexpectedly used subp.subp")
E       AssertionError: Unexpectedly used subp.subp

conftest.py:145: AssertionError

https://docs.pytest.org/en/stable/changelog.html

For the build logs, see:
https://copr-be.cloud.fedoraproject.org/results/thrnciar/pytest/fedora-rawhide-x86_64/07247517-cloud-init/

For all our attempts to build cloud-init with pytest 8, see:
https://copr.fedorainfracloud.org/coprs/thrnciar/pytest/package/cloud-init/

Let us know here if you have any questions.

Pytest 8 is planned to be included in Fedora 41. And this bugzilla is a
heads up before we merge new pytest into rawhide. For more info see a Fedora Change
proposal https://fedoraproject.org/wiki/Changes/Pytest_8

We'd appreciate help from the people who know this package best,
but if you don't want to work on this now, let us know so we can try to work around it on our side.

Comment 1 Tomáš Hrnčiar 2024-04-19 08:30:37 UTC
I see there is a successfull build with pytest 8 in copr. I am closing this as NOTABUG, sorry for the noise.

https://copr.fedorainfracloud.org/coprs/thrnciar/pytest/build/7321288/


Note You need to log in before you can comment on or make changes to this bug.