Bug 2272972

Summary: cloud-init fails to build with pytest 8: AssertionError: Unexpectedly used subp.subp
Product: [Fedora] Fedora Reporter: Tomáš Hrnčiar <thrnciar>
Component: cloud-initAssignee: Major Hayden 🤠 <mhayden>
Status: CLOSED NOTABUG QA Contact: Fedora Extras Quality Assurance <extras-qa>
Severity: unspecified Docs Contact:
Priority: unspecified    
Version: rawhideCC: dustymabe, eterrell, gholms, lars, mhayden, mhroncok, python-packagers-sig, rominf, thrnciar
Target Milestone: ---   
Target Release: ---   
Hardware: Unspecified   
OS: Unspecified   
Whiteboard:
Fixed In Version: Doc Type: If docs needed, set a value
Doc Text:
Story Points: ---
Clone Of: Environment:
Last Closed: 2024-04-19 08:30:37 UTC Type: Bug
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:
Embargoed:
Bug Depends On:    
Bug Blocks: 2256331    

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/