Source code for meta_package_manager.tests.test_bar_plugin

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

from __future__ import annotations

import re
import subprocess
from collections import Counter
from itertools import product

import pytest
from boltons.iterutils import flatten
from click_extra.testing import args_cleanup, env_copy
from click_extra.tests.conftest import unless_macos

from meta_package_manager import bar_plugin
from meta_package_manager.version import parse_version


def _invocation_matrix(*iterables):
    """Pre-compute a matrix of all possible options for invocation."""
    for args in product(*iterables):
        yield args_cleanup(args)


def _shell_invocation_matrix():
    """Pre-compute a matrix of all possible options used for shell invocation.

    See the list of shell supported by SwiftBar at:
    https://github.com/swiftbar/SwiftBar/commit/366695d594884fe141bc1752ab0f25d2c43334fa

    Returns
    -------
    .. code-block:: python
        (
            ("bash", "-c"),
            ("bash", "--login", "-c"),
            ("/bin/bash", "-c"),
            ("/bin/bash", "--login", "-c"),
            ("zsh", "-c"),
            ("zsh", "--login", "-c"),
            ("/bin/zsh", "-c"),
            ("/bin/zsh", "--login", "-c"),
            ("/usr/bin/env", "bash", "-c"),
            ("/usr/bin/env", "bash", "--login", "-c"),
            ("/usr/bin/env", "/bin/bash", "-c"),
            ("/usr/bin/env", "/bin/bash", "--login", "-c"),
            ("/usr/bin/env", "zsh", "-c"),
            ("/usr/bin/env", "zsh", "--login", "-c"),
            ("/usr/bin/env", "/bin/zsh", "-c"),
            ("/usr/bin/env", "/bin/zsh", "--login", "-c"),
            None,
        )
    """
    return list(
        _invocation_matrix(
            # Env prefixes.
            (None, "/usr/bin/env"),
            # Naked and full binary paths.
            flatten((bin_id, f"/bin/{bin_id}") for bin_id in ("bash", "zsh")),
            # Options.
            (
                "-c",
                # XXX Login shell defaults to Python 2.7 on GitHub macOS runners and is
                # picked up by surprise for bar plugin tests:
                # Traceback (most recent call last):
                #   File "/Library/Frameworks/.../2.7/lib/python2.7/runpy.py",
                #   line 163, in _run_module_as_main
                #     mod_name, _Error)
                #   File "/Library/Frameworks/.../2.7/lib/python2.7/runpy.py",
                #   line 111, in _get_module_details
                #     __import__(mod_name)  # Do not catch exceptions initializing package
                #   File "meta_package_manager/__init__.py", line 33, in <module>
                #     from click_extra.logging import logger
                # ImportError: No module named click_extra.logging
                # ("--login", "-c"),
            ),
        )
    ) + [None]


def _python_invocation_matrix():
    """Pre-compute a matrix of all possible options used for python invocation.

    Returns
    -------
    .. code-block:: python
        (
            ("python",),
            ("python3",),
            ("/usr/bin/env", "python"),
            ("/usr/bin/env", "python3"),
        )
    """
    return _invocation_matrix(
        # Env prefixes.
        (None, "/usr/bin/env"),
        # Binary paths
        ("python", "python3"),
    )


shell_args = pytest.mark.parametrize(
    "shell_args",
    tuple(
        pytest.param(p, id=" ".join(args_cleanup(p)))
        for p in _shell_invocation_matrix()
    ),
)


shell_python_args = pytest.mark.parametrize(
    "shell_args,python_args",
    (
        pytest.param(s_args, p_args, id=" ".join(args_cleanup(s_args, p_args)))
        for s_args, p_args in product(
            _shell_invocation_matrix(), _python_invocation_matrix()
        )
    ),
)


def _subcmd_args(invoke_args: tuple[str, ...] | None, *subcmd_args: str):
    """Cleanup args and eventually concatenate all ``subcmd_args`` items to a space
    separated string if ``invoke_args`` is defined and its last argument is equal to
    ``-c``."""
    raw_args: list[str] = []
    if invoke_args:
        raw_args.extend(invoke_args)
        if invoke_args[-1] == "-c":
            subcmd_args = (" ".join(subcmd_args),)
    raw_args.extend(subcmd_args)
    return args_cleanup(raw_args)


[docs]@unless_macos class TestBarPlugin: common_checklist = [ # Menubar line. Required. (r"(πŸŽβ†‘\d+|πŸ“¦βœ“)( ⚠️\d+)? \| dropdown=false$", True), # Submenus and sections marker. Required. (r"-{3,5}$", True), # Upgrade all line. Required. ( r"(--)?πŸ†™ Upgrade all \S+ packages? \| shell=\S+( param\d+=\S+)+ " r"refresh=true terminal=(false|true alternate=true)$", True, ), # Error line. Optional. ( r"(--)?.+ \| font=Menlo size=12 color=red trim=false " r"ansi=false emojize=false( symbolize=false)?$", False, ), ]
[docs] def plugin_output_checks(self, checklist, extra_env=None): """Run the plugin script and check its output against the checklist.""" process = subprocess.run( bar_plugin.__file__, capture_output=True, shell=True, encoding="utf-8", env=env_copy(extra_env), ) assert not process.stderr assert process.returncode == 0 checks = checklist + self.common_checklist match_counter = Counter() for line in process.stdout.splitlines(): # The line is expected to match at least one regex. matches = False for index, (regex, _) in enumerate(checks): if re.match(regex, line): matches = True match_counter[index] += 1 break if not matches: print(process.stdout) msg = f"plugin output line {line!r} did not match any regex." raise Exception(msg) # Check all required regex did match at least once. for index, (regex, required) in enumerate(checks): if required and not match_counter[index]: msg = f"{regex!r} regex did not match any plugin output line." raise Exception(msg)
[docs] @pytest.mark.xdist_group(name="avoid_concurrent_plugin_runs") @pytest.mark.parametrize("submenu_layout", (True, False, None)) @pytest.mark.parametrize("table_rendering", (True, False, None)) def test_rendering(self, submenu_layout, table_rendering): extra_checks = [] if table_rendering is False: extra_checks.extend( ( # Package manager section header. (r"(⚠️ )?\d+ outdated .+ packages?", True), # Package upgrade line. ( r"(--)?[\S ]+ \S+ β†’ \S+ \| shell=\S+( param\d+=\S+)+ " r"refresh=true terminal=(false|true alternate=true)$", True, ), ), ) # Default case is VAR_TABLE_RENDERING=true. else: extra_checks.extend( ( # Package manager section header. (r"(⚠️ )?\S+ - \d+ packages?\s+\| font=Menlo size=12", True), # Package upgrade line. ( r"(--)?[\S ]+\s+\S+ β†’ \S+\s+\| shell=\S+( param\d+=\S+)+ " r"font=Menlo size=12 refresh=true " r"terminal=(false|true alternate=true)?$", True, ), ), ) extra_env = {} if submenu_layout is not None: extra_env["VAR_SUBMENU_LAYOUT"] = str(submenu_layout) if table_rendering is not None: extra_env["VAR_TABLE_RENDERING"] = str(table_rendering) self.plugin_output_checks(extra_checks, extra_env=extra_env)
[docs] @pytest.mark.xdist_group(name="avoid_concurrent_plugin_runs") @shell_args def test_plugin_shell_invocation(self, shell_args): """Test execution of plugin on different shells. Do not execute the complete search for outdated packages, just stop at searching for the mpm executable and extract its version. """ process = subprocess.run( _subcmd_args(shell_args, bar_plugin.__file__, "--search-mpm"), capture_output=True, encoding="utf-8", ) assert not process.stderr assert process.returncode == 0 assert process.stdout for line in process.stdout.splitlines(): assert re.match( r"^.+ \| runnable: \S+ \| up to date: \S+" r" \| version: .+ \| error: .*$", line, )
[docs] @shell_python_args def test_python_shell_invocation(self, shell_args, python_args): """Test any Python shell invocation is properly configured and all are compatible with plugin requirements.""" process = subprocess.run( _subcmd_args(shell_args, *python_args, "--version"), capture_output=True, encoding="utf-8", ) assert not process.stderr assert process.stdout assert process.returncode == 0 # We need to parse the version to account for alpha release, # like Python `3.12.0a4`. assert parse_version(process.stdout.split()[1]) >= parse_version( ".".join(str(i) for i in bar_plugin.PYTHON_MIN_VERSION), )