Source code for tests.test_help

# Copyright Kevin Deldycke <[email protected]> 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.

"""Keeps the CLI test plan's per-command `--help` roster in sync.

The `--help` invocations themselves live as `[[cases]]` entries in
`tests/cli-test-plan.toml`, executed by `click-extra test-suite` in CI and
against the compiled binaries during releases. A static roster drifts as
commands are added or removed, so this conformance test compares it against
the live command tree and fails naming the missing or orphaned entries.
"""

from __future__ import annotations

from pathlib import Path

import click

from meta_package_manager.cli import mpm

from .conftest import tomllib

PLAN_PATH = Path(__file__).parent / "cli-test-plan.toml"


def _collect_commands(
    group: click.Group | click.Command,
    prefix: tuple[str, ...] = (),
) -> list[tuple[str, ...]]:
    """Recursively collect all command paths from a Click group."""
    paths: list[tuple[str, ...]] = [prefix] if prefix else [()]
    if isinstance(group, click.Group):
        for name in sorted(group.list_commands(click.Context(group))):
            cmd = group.get_command(click.Context(group), name)
            if cmd is None:
                continue
            child = (*prefix, name)
            paths.extend(_collect_commands(cmd, child))
    return paths


def _plan_help_paths() -> set[tuple[str, ...]]:
    """Extract the command path of each `--help` case in the test plan.

    A `--help` case is one whose `cli_parameters` end with `--help`; the
    words before it are the command path (empty for the root CLI).
    """
    plan = tomllib.loads(PLAN_PATH.read_text(encoding="UTF-8"))
    paths: set[tuple[str, ...]] = set()
    for case in plan["cases"]:
        params = case["cli_parameters"]
        if isinstance(params, str):
            params = params.split()
        if params and params[-1] == "--help":
            paths.add(tuple(params[:-1]))
    return paths


[docs] def test_plan_covers_all_commands(): """Every command and subcommand must have a `--help` case in the plan.""" tree = set(_collect_commands(mpm)) plan_paths = _plan_help_paths() missing = sorted(tree - plan_paths) orphans = sorted(plan_paths - tree) assert not missing, ( "Commands without a --help case in tests/cli-test-plan.toml: " + ", ".join(" ".join(path) or "(root)" for path in missing) ) assert not orphans, ( "--help cases in tests/cli-test-plan.toml without a live command: " + ", ".join(" ".join(path) for path in orphans) )