mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-02 22:16:58 +02:00
The Filesystem module stored the paths of an iOS dump with the separator of the system checking it, so on Windows the process and file path indicators, which split paths on "/", never matched. It now stores them as POSIX paths. The rest are test fixes: - the completion install tests also redirect USERPROFILE, which Path.home() reads on Windows; they wrote to the real home folder; - the plugin table helpers accept the light header Rich draws on consoles that cannot show the heavy one; - the completion test quotes the command path, whose backslashes were dropped when COMP_WORDS was split; - tests that need symbolic links or the sqlite3 binary are skipped when those are not available; - two assertions no longer depend on the path separator or on the line ending text mode writes.
567 lines
17 KiB
Python
567 lines
17 KiB
Python
import shlex
|
|
from types import SimpleNamespace
|
|
|
|
import click
|
|
from click.testing import CliRunner
|
|
|
|
from mvt.cli import cli as mvt_cli
|
|
from mvt.common.cli_plugins import (
|
|
ANDROID_CLI_PLUGIN_GROUP,
|
|
IOS_CLI_PLUGIN_GROUP,
|
|
MVT_CUSTOM_COMMANDS_ENV,
|
|
NEUTRAL_CLI_PLUGIN_GROUP,
|
|
BrokenPluginCommand,
|
|
load_cli_commands_option,
|
|
register_cli_commands_from_path,
|
|
register_cli_plugins,
|
|
register_installed_cli_commands,
|
|
)
|
|
|
|
|
|
# Keep the banner of the mvt group callback from checking for updates online.
|
|
OFFLINE = ["--disable-update-check", "--disable-indicator-update-check"]
|
|
|
|
COMMAND_TEMPLATE = """
|
|
import click
|
|
|
|
|
|
@click.command({name!r})
|
|
@click.pass_context
|
|
def cli(ctx):
|
|
click.echo({message!r})
|
|
if ctx.obj:
|
|
click.echo(ctx.obj.get("marker", ""))
|
|
"""
|
|
|
|
|
|
def _write_command(path, name, message="command ran"):
|
|
path.write_text(
|
|
COMMAND_TEMPLATE.format(name=name, message=message),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def _make_group():
|
|
@click.group()
|
|
@load_cli_commands_option
|
|
@click.pass_context
|
|
def group(ctx):
|
|
ctx.ensure_object(dict)
|
|
ctx.obj["marker"] = "parent context"
|
|
|
|
return group
|
|
|
|
|
|
def _entry_point(name, value, command=None, exception=None, distribution="plugin"):
|
|
def load():
|
|
if exception is not None:
|
|
raise exception
|
|
return command
|
|
|
|
dist = SimpleNamespace(metadata={"Name": distribution}, version="1.0")
|
|
return SimpleNamespace(name=name, value=value, load=load, dist=dist)
|
|
|
|
|
|
def test_load_command_option_registers_command_before_resolution(tmp_path):
|
|
command_path = _write_command(tmp_path / "hello.py", "hello")
|
|
group = _make_group()
|
|
|
|
result = CliRunner().invoke(
|
|
group,
|
|
["--load-command", str(command_path), "hello"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "command ran" in result.output
|
|
assert "parent context" in result.output
|
|
|
|
|
|
def test_load_command_option_supports_folders_and_repeated_paths(tmp_path):
|
|
folder = tmp_path / "commands"
|
|
folder.mkdir()
|
|
_write_command(folder / "b.py", "second")
|
|
_write_command(folder / "a.py", "first")
|
|
_write_command(folder / ".hidden.py", "hidden")
|
|
_write_command(folder / "__init__.py", "init")
|
|
other = _write_command(tmp_path / "third.py", "third")
|
|
group = _make_group()
|
|
|
|
result = CliRunner().invoke(
|
|
group,
|
|
[
|
|
"--load-command",
|
|
str(folder),
|
|
"--load-command",
|
|
str(other),
|
|
"--load-command",
|
|
str(other),
|
|
"--help",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "first" in result.output
|
|
assert "second" in result.output
|
|
assert "third" in result.output
|
|
assert "hidden" not in result.output
|
|
assert "init" not in result.output
|
|
|
|
|
|
def test_loaded_command_participates_in_shell_completion(tmp_path):
|
|
command_path = _write_command(tmp_path / "hello.py", "hello")
|
|
group = _make_group()
|
|
words = f"group --load-command {shlex.quote(str(command_path))} he"
|
|
|
|
result = CliRunner().invoke(
|
|
group,
|
|
[],
|
|
env={
|
|
"_GROUP_COMPLETE": "bash_complete",
|
|
"COMP_WORDS": words,
|
|
"COMP_CWORD": "3",
|
|
},
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "plain,hello" in result.output
|
|
|
|
|
|
def test_explicit_command_import_and_contract_failures_are_usage_errors(tmp_path):
|
|
broken_path = tmp_path / "broken.py"
|
|
broken_path.write_text("raise RuntimeError('broken import')", encoding="utf-8")
|
|
missing_cli_path = tmp_path / "missing_cli.py"
|
|
missing_cli_path.write_text("value = 1", encoding="utf-8")
|
|
|
|
broken_result = CliRunner().invoke(
|
|
_make_group(),
|
|
["--load-command", str(broken_path), "broken"],
|
|
)
|
|
missing_cli_result = CliRunner().invoke(
|
|
_make_group(),
|
|
["--load-command", str(missing_cli_path), "missing-cli"],
|
|
)
|
|
|
|
assert broken_result.exit_code == 2
|
|
assert "broken import" in broken_result.output
|
|
assert missing_cli_result.exit_code == 2
|
|
assert "must export a Click command or group named 'cli'" in (
|
|
missing_cli_result.output
|
|
)
|
|
|
|
|
|
def test_environment_command_failure_gets_broken_placeholder(tmp_path):
|
|
command_path = tmp_path / "broken_command.py"
|
|
command_path.write_text("raise RuntimeError('broken import')", encoding="utf-8")
|
|
group = click.Group()
|
|
|
|
registered = register_cli_commands_from_path(group, command_path)
|
|
|
|
assert registered == ["broken-command"]
|
|
assert isinstance(group.commands["broken-command"], BrokenPluginCommand)
|
|
result = CliRunner().invoke(group, ["broken-command"])
|
|
assert result.exit_code == 1
|
|
assert "broken import" in result.output
|
|
assert str(command_path) in result.output
|
|
|
|
|
|
def test_environment_command_system_exit_gets_broken_placeholder(tmp_path):
|
|
command_path = tmp_path / "exiting_command.py"
|
|
command_path.write_text("raise SystemExit(7)", encoding="utf-8")
|
|
group = click.Group()
|
|
|
|
registered = register_cli_commands_from_path(group, command_path)
|
|
|
|
assert registered == ["exiting-command"]
|
|
assert isinstance(group.commands["exiting-command"], BrokenPluginCommand)
|
|
result = CliRunner().invoke(group, ["exiting-command"])
|
|
assert result.exit_code == 1
|
|
assert "Unable to import custom command" in result.output
|
|
assert result.output.rstrip().endswith(": 7")
|
|
|
|
|
|
def test_installed_entry_point_name_is_the_command_name(monkeypatch):
|
|
@click.command("internal-name")
|
|
def command():
|
|
click.echo("installed command ran")
|
|
|
|
entry_point = _entry_point(
|
|
"external-name",
|
|
"example_plugin:cli",
|
|
command=command,
|
|
)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
lambda **kwargs: [entry_point],
|
|
)
|
|
group = click.Group()
|
|
|
|
registered = register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)
|
|
|
|
assert registered == ["external-name"]
|
|
assert "internal-name" not in group.commands
|
|
result = CliRunner().invoke(group, ["external-name"])
|
|
assert result.exit_code == 0
|
|
assert result.output == "installed command ran\n"
|
|
|
|
|
|
def test_broken_installed_plugin_does_not_break_cli(monkeypatch):
|
|
broken = _entry_point(
|
|
"broken",
|
|
"broken_plugin:cli",
|
|
exception=RuntimeError("missing dependency"),
|
|
distribution="broken-plugin",
|
|
)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
lambda **kwargs: [broken],
|
|
)
|
|
group = click.Group()
|
|
|
|
register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)
|
|
|
|
help_result = CliRunner().invoke(group, ["--help"])
|
|
assert help_result.exit_code == 0
|
|
assert "Warning: external command could not be loaded." in help_result.output
|
|
|
|
result = CliRunner().invoke(group, ["broken"])
|
|
assert result.exit_code == 1
|
|
assert "broken-plugin 1.0 (broken_plugin:cli)" in result.output
|
|
assert "RuntimeError: missing dependency" in result.output
|
|
|
|
|
|
def test_installed_plugin_system_exit_does_not_break_cli(monkeypatch):
|
|
exiting = _entry_point(
|
|
"exiting",
|
|
"exiting_plugin:cli",
|
|
exception=SystemExit(7),
|
|
distribution="exiting-plugin",
|
|
)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
lambda **kwargs: [exiting],
|
|
)
|
|
group = click.Group()
|
|
|
|
register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)
|
|
|
|
help_result = CliRunner().invoke(group, ["--help"])
|
|
assert help_result.exit_code == 0
|
|
result = CliRunner().invoke(group, ["exiting"])
|
|
assert result.exit_code == 1
|
|
assert "SystemExit: 7" in result.output
|
|
|
|
|
|
def test_non_click_entry_point_gets_broken_placeholder(monkeypatch):
|
|
invalid = _entry_point("invalid", "plugin:value", command=object())
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
lambda **kwargs: [invalid],
|
|
)
|
|
group = click.Group()
|
|
|
|
register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)
|
|
|
|
assert isinstance(group.commands["invalid"], BrokenPluginCommand)
|
|
result = CliRunner().invoke(group, ["invalid"])
|
|
assert result.exit_code == 1
|
|
assert "must resolve to a Click command or group" in result.output
|
|
|
|
|
|
def test_entry_point_discovery_failure_does_not_break_group(monkeypatch, caplog):
|
|
def fail_discovery(**kwargs):
|
|
raise RuntimeError("invalid package metadata")
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
fail_discovery,
|
|
)
|
|
group = click.Group()
|
|
|
|
registered = register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)
|
|
|
|
assert registered == []
|
|
assert not group.commands
|
|
assert "Unable to discover external commands" in caplog.text
|
|
assert "invalid package metadata" in caplog.text
|
|
|
|
|
|
def test_builtin_and_first_external_command_win_collisions(monkeypatch, caplog):
|
|
@click.command("version")
|
|
def core_version():
|
|
pass
|
|
|
|
@click.command()
|
|
def first():
|
|
pass
|
|
|
|
@click.command()
|
|
def second():
|
|
pass
|
|
|
|
entry_points = [
|
|
_entry_point("duplicate", "z_plugin:cli", command=second, distribution="z"),
|
|
_entry_point("version", "plugin:version", command=first),
|
|
_entry_point("duplicate", "a_plugin:cli", command=first, distribution="a"),
|
|
]
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
lambda **kwargs: entry_points,
|
|
)
|
|
group = click.Group(commands={"version": core_version})
|
|
|
|
registered = register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)
|
|
|
|
assert registered == ["duplicate"]
|
|
assert group.commands["version"] is core_version
|
|
assert group.commands["duplicate"] is first
|
|
assert "the command name is already registered" in caplog.text
|
|
|
|
|
|
def test_explicit_command_cannot_replace_existing_command(tmp_path):
|
|
command_path = _write_command(tmp_path / "version.py", "version")
|
|
group = _make_group()
|
|
|
|
@group.command("version")
|
|
def core_version():
|
|
pass
|
|
|
|
result = CliRunner().invoke(
|
|
group,
|
|
["--load-command", str(command_path), "version"],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "the command name is already registered" in result.output
|
|
|
|
|
|
def test_platform_entry_point_groups_and_environment_paths_are_separate(
|
|
tmp_path, monkeypatch
|
|
):
|
|
ios_path = _write_command(tmp_path / "ios.py", "ios-file")
|
|
android_path = _write_command(tmp_path / "android.py", "android-file")
|
|
|
|
@click.command()
|
|
def ios_package():
|
|
pass
|
|
|
|
@click.command()
|
|
def android_package():
|
|
pass
|
|
|
|
def entry_points(*, group):
|
|
if group == IOS_CLI_PLUGIN_GROUP:
|
|
return [_entry_point("ios-package", "ios_plugin:cli", ios_package)]
|
|
if group == ANDROID_CLI_PLUGIN_GROUP:
|
|
return [
|
|
_entry_point("android-package", "android_plugin:cli", android_package)
|
|
]
|
|
return []
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
entry_points,
|
|
)
|
|
monkeypatch.setenv("TEST_IOS_COMMANDS", str(ios_path))
|
|
monkeypatch.setenv("TEST_ANDROID_COMMANDS", str(android_path))
|
|
ios_group = click.Group()
|
|
android_group = click.Group()
|
|
|
|
register_cli_plugins(
|
|
ios_group,
|
|
entry_point_group=IOS_CLI_PLUGIN_GROUP,
|
|
environment_variable="TEST_IOS_COMMANDS",
|
|
)
|
|
register_cli_plugins(
|
|
android_group,
|
|
entry_point_group=ANDROID_CLI_PLUGIN_GROUP,
|
|
environment_variable="TEST_ANDROID_COMMANDS",
|
|
)
|
|
|
|
assert set(ios_group.commands) == {"ios-file", "ios-package"}
|
|
assert set(android_group.commands) == {"android-file", "android-package"}
|
|
|
|
|
|
def test_neutral_entry_point_group_is_not_registered_on_the_platform_clis(
|
|
monkeypatch,
|
|
):
|
|
@click.command()
|
|
def neutral_package():
|
|
pass
|
|
|
|
def entry_points(*, group):
|
|
if group == NEUTRAL_CLI_PLUGIN_GROUP:
|
|
return [
|
|
_entry_point("neutral-package", "neutral_plugin:cli", neutral_package)
|
|
]
|
|
return []
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
entry_points,
|
|
)
|
|
ios_group = click.Group()
|
|
android_group = click.Group()
|
|
|
|
register_cli_plugins(
|
|
ios_group,
|
|
entry_point_group=IOS_CLI_PLUGIN_GROUP,
|
|
environment_variable="TEST_IOS_COMMANDS",
|
|
)
|
|
register_cli_plugins(
|
|
android_group,
|
|
entry_point_group=ANDROID_CLI_PLUGIN_GROUP,
|
|
environment_variable="TEST_ANDROID_COMMANDS",
|
|
)
|
|
|
|
assert not ios_group.commands
|
|
assert not android_group.commands
|
|
|
|
|
|
def test_environment_command_wins_collision_with_installed_command(
|
|
tmp_path, monkeypatch, caplog
|
|
):
|
|
command_path = _write_command(
|
|
tmp_path / "duplicate.py",
|
|
"duplicate",
|
|
message="environment command ran",
|
|
)
|
|
|
|
@click.command()
|
|
def installed_command():
|
|
pass
|
|
|
|
def entry_points(*, group):
|
|
if group == IOS_CLI_PLUGIN_GROUP:
|
|
return [
|
|
_entry_point(
|
|
"duplicate",
|
|
"ios_plugin:cli",
|
|
installed_command,
|
|
distribution="ios-plugin",
|
|
)
|
|
]
|
|
return []
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
entry_points,
|
|
)
|
|
monkeypatch.setenv("TEST_IOS_COMMANDS", str(command_path))
|
|
group = click.Group()
|
|
|
|
register_cli_plugins(
|
|
group,
|
|
entry_point_group=IOS_CLI_PLUGIN_GROUP,
|
|
environment_variable="TEST_IOS_COMMANDS",
|
|
)
|
|
|
|
assert group.commands["duplicate"] is not installed_command
|
|
result = CliRunner().invoke(group, ["duplicate"])
|
|
assert result.exit_code == 0
|
|
assert "environment command ran" in result.output
|
|
assert "the command name is already registered" in caplog.text
|
|
assert "ios-plugin 1.0 (ios_plugin:cli)" in caplog.text
|
|
|
|
|
|
def test_the_mvt_cli_gets_the_neutral_commands_and_no_platform_command(
|
|
monkeypatch, restore_cli_commands
|
|
):
|
|
@click.command()
|
|
def shared_package():
|
|
click.echo("shared command ran")
|
|
|
|
@click.command()
|
|
def ios_package():
|
|
pass
|
|
|
|
def entry_points(*, group):
|
|
if group == NEUTRAL_CLI_PLUGIN_GROUP:
|
|
return [_entry_point("shared-package", "shared_plugin:cli", shared_package)]
|
|
if group == IOS_CLI_PLUGIN_GROUP:
|
|
return [_entry_point("ios-package", "ios_plugin:cli", ios_package)]
|
|
return []
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
entry_points,
|
|
)
|
|
|
|
register_cli_plugins(
|
|
mvt_cli,
|
|
entry_point_group=NEUTRAL_CLI_PLUGIN_GROUP,
|
|
environment_variable=MVT_CUSTOM_COMMANDS_ENV,
|
|
)
|
|
|
|
assert "ios-package" not in mvt_cli.commands
|
|
result = CliRunner().invoke(mvt_cli, [*OFFLINE, "shared-package"])
|
|
assert result.exit_code == 0
|
|
assert "shared command ran" in result.output
|
|
|
|
|
|
def test_builtin_mvt_command_wins_collision_with_neutral_command(
|
|
monkeypatch, caplog, restore_cli_commands
|
|
):
|
|
@click.command()
|
|
def neutral_version():
|
|
pass
|
|
|
|
def entry_points(*, group):
|
|
if group == NEUTRAL_CLI_PLUGIN_GROUP:
|
|
return [
|
|
_entry_point(
|
|
"version",
|
|
"neutral_plugin:cli",
|
|
neutral_version,
|
|
distribution="neutral-plugin",
|
|
)
|
|
]
|
|
return []
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
entry_points,
|
|
)
|
|
builtin_version = mvt_cli.commands["version"]
|
|
|
|
register_cli_plugins(
|
|
mvt_cli,
|
|
entry_point_group=NEUTRAL_CLI_PLUGIN_GROUP,
|
|
environment_variable=MVT_CUSTOM_COMMANDS_ENV,
|
|
)
|
|
|
|
assert mvt_cli.commands["version"] is builtin_version
|
|
assert "the command name is already registered" in caplog.text
|
|
assert "neutral-plugin 1.0 (neutral_plugin:cli)" in caplog.text
|
|
|
|
|
|
def test_broken_neutral_plugin_does_not_break_the_mvt_cli(
|
|
monkeypatch, restore_cli_commands
|
|
):
|
|
def entry_points(*, group):
|
|
if group == NEUTRAL_CLI_PLUGIN_GROUP:
|
|
return [
|
|
_entry_point(
|
|
"broken",
|
|
"broken_plugin:cli",
|
|
exception=RuntimeError("missing dependency"),
|
|
distribution="broken-plugin",
|
|
)
|
|
]
|
|
return []
|
|
|
|
monkeypatch.setattr(
|
|
"mvt.common.cli_plugins.importlib.metadata.entry_points",
|
|
entry_points,
|
|
)
|
|
|
|
register_cli_plugins(
|
|
mvt_cli,
|
|
entry_point_group=NEUTRAL_CLI_PLUGIN_GROUP,
|
|
environment_variable=MVT_CUSTOM_COMMANDS_ENV,
|
|
)
|
|
|
|
assert isinstance(mvt_cli.commands["broken"], BrokenPluginCommand)
|
|
result = CliRunner().invoke(mvt_cli, [*OFFLINE, "version"])
|
|
assert result.exit_code == 0
|