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.
241 lines
7.2 KiB
Python
241 lines
7.2 KiB
Python
import json
|
|
import re
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from mvt.android.cli import cli as android_cli
|
|
from mvt.cli import cli as mvt_cli
|
|
from mvt.common.cli_plugins import (
|
|
ANDROID_CLI_PLUGIN_GROUP,
|
|
IOS_CLI_PLUGIN_GROUP,
|
|
NEUTRAL_CLI_PLUGIN_GROUP,
|
|
)
|
|
from mvt.common.cmd_plugins import plugins
|
|
from mvt.common.module import MVTModule
|
|
from mvt.common.module_loader import MODULES_ENTRY_POINT_GROUP
|
|
from mvt.common.updates import PluginUpdates
|
|
from mvt.ios.cli import cli as ios_cli
|
|
|
|
|
|
class ExampleModule(MVTModule):
|
|
pass
|
|
|
|
|
|
class AnotherModule(MVTModule):
|
|
pass
|
|
|
|
|
|
class FakeDistribution:
|
|
def __init__(self, name, version="1.0.0", direct_url=None):
|
|
self.name = name
|
|
self.version = version
|
|
self.direct_url = direct_url
|
|
|
|
def read_text(self, file_name):
|
|
if file_name == "direct_url.json" and self.direct_url is not None:
|
|
return json.dumps(self.direct_url)
|
|
return None
|
|
|
|
|
|
def _entry_point(name, distribution, modules=None, exception=None):
|
|
def load():
|
|
if exception is not None:
|
|
raise exception
|
|
return modules
|
|
|
|
return SimpleNamespace(
|
|
name=name, value="example_plugin:modules", dist=distribution, load=load
|
|
)
|
|
|
|
|
|
def _run(command, arguments):
|
|
# Keep rich from wrapping the table while its content is being asserted.
|
|
return CliRunner().invoke(command, arguments, env={"COLUMNS": "200"})
|
|
|
|
|
|
def _table_lines(output):
|
|
"""Return the cells of each line of the table, header first."""
|
|
return [
|
|
[cell.strip() for cell in re.split("[│┃]", line.strip().strip("│┃"))]
|
|
for line in output.splitlines()
|
|
if "│" in line or "┃" in line
|
|
]
|
|
|
|
|
|
def _table_rows(output):
|
|
"""Return the content of the table rows, without the header and the box."""
|
|
return _table_lines(output)[1:]
|
|
|
|
|
|
def _table_header(output):
|
|
lines = _table_lines(output)
|
|
return lines[0] if lines else []
|
|
|
|
|
|
def _install(monkeypatch, distributions, entry_points):
|
|
monkeypatch.setattr(
|
|
"mvt.common.cmd_plugins.installed_plugin_distributions",
|
|
lambda: distributions,
|
|
)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cmd_plugins.importlib.metadata.entry_points",
|
|
lambda *, group: entry_points.get(group, []),
|
|
)
|
|
|
|
|
|
def test_plugins_is_a_builtin_command_of_the_mvt_cli_only():
|
|
assert mvt_cli.commands["plugins"] is plugins
|
|
assert "plugins" not in ios_cli.commands
|
|
assert "plugins" not in android_cli.commands
|
|
|
|
|
|
def test_list_shows_what_every_plugin_contributes(monkeypatch):
|
|
index_plugin = FakeDistribution("example-plugin", version="1.2.0")
|
|
repository_plugin = FakeDistribution(
|
|
"repository-plugin",
|
|
version="0.1.0",
|
|
direct_url={
|
|
"url": "https://example.org/plugin.git",
|
|
"vcs_info": {"vcs": "git", "commit_id": "b" * 40},
|
|
},
|
|
)
|
|
local_plugin = FakeDistribution(
|
|
"local-plugin",
|
|
direct_url={"url": "file:///plugins", "dir_info": {"editable": True}},
|
|
)
|
|
_install(
|
|
monkeypatch,
|
|
[index_plugin, local_plugin, repository_plugin],
|
|
{
|
|
MODULES_ENTRY_POINT_GROUP: [
|
|
_entry_point(
|
|
"example", index_plugin, modules=[ExampleModule, AnotherModule]
|
|
),
|
|
_entry_point("local", local_plugin, modules=lambda: [ExampleModule]),
|
|
],
|
|
IOS_CLI_PLUGIN_GROUP: [_entry_point("summarize", repository_plugin)],
|
|
ANDROID_CLI_PLUGIN_GROUP: [_entry_point("triage", local_plugin)],
|
|
NEUTRAL_CLI_PLUGIN_GROUP: [_entry_point("report", repository_plugin)],
|
|
},
|
|
)
|
|
|
|
result = _run(plugins, ["list"])
|
|
|
|
assert result.exit_code == 0
|
|
# Plugins are listed by name, with the modules and the commands each of
|
|
# them contributes.
|
|
assert _table_header(result.output) == [
|
|
"Name",
|
|
"Version",
|
|
"Origin",
|
|
"Modules",
|
|
"Commands",
|
|
]
|
|
assert _table_rows(result.output) == [
|
|
["example-plugin", "1.2.0", "pypi", "2", "-"],
|
|
["local-plugin", "1.0.0", "local", "1", "triage"],
|
|
["repository-plugin", "0.1.0", "git+bbbbbbbb", "0", "report, summarize"],
|
|
]
|
|
|
|
|
|
def test_list_reports_a_broken_module_entry_point(monkeypatch):
|
|
plugin = FakeDistribution("broken-plugin")
|
|
_install(
|
|
monkeypatch,
|
|
[plugin],
|
|
{
|
|
MODULES_ENTRY_POINT_GROUP: [
|
|
_entry_point(
|
|
"broken", plugin, exception=ImportError("missing dependency")
|
|
)
|
|
]
|
|
},
|
|
)
|
|
|
|
result = _run(plugins, ["list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert _table_rows(result.output) == [
|
|
["broken-plugin", "1.0.0", "pypi", "error", "-"]
|
|
]
|
|
|
|
|
|
def test_list_without_plugins(monkeypatch):
|
|
_install(monkeypatch, [], {})
|
|
|
|
result = _run(plugins, ["list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output == "No MVT plugins are installed.\n"
|
|
|
|
|
|
def test_check_updates_prints_the_findings_and_ignores_the_throttle(monkeypatch):
|
|
findings = [
|
|
{
|
|
"name": "example-plugin",
|
|
"installed": "1.0.0",
|
|
"latest": "1.2.0",
|
|
"origin": "pypi",
|
|
"upgrade_command": "pip install -U example-plugin",
|
|
}
|
|
]
|
|
_install(monkeypatch, [FakeDistribution("example-plugin")], {})
|
|
monkeypatch.setattr(PluginUpdates, "check", lambda self: findings)
|
|
monkeypatch.setattr(
|
|
PluginUpdates,
|
|
"should_check",
|
|
lambda self: pytest.fail("an explicit check must not be throttled"),
|
|
)
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Plugin updates available:" in result.output
|
|
assert "example-plugin 1.0.0 → 1.2.0" in result.output
|
|
assert "Upgrade with: pip install -U example-plugin" in result.output
|
|
assert "MVT does not install plugin updates." in result.output
|
|
|
|
|
|
def test_check_updates_without_available_updates(monkeypatch):
|
|
_install(monkeypatch, [FakeDistribution("example-plugin")], {})
|
|
monkeypatch.setattr(PluginUpdates, "check", lambda self: [])
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "All plugins are up to date." in result.output
|
|
|
|
|
|
def test_check_updates_without_plugins(monkeypatch):
|
|
_install(monkeypatch, [], {})
|
|
monkeypatch.setattr(
|
|
PluginUpdates,
|
|
"check",
|
|
lambda self: pytest.fail("nothing must be checked without plugins"),
|
|
)
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No MVT plugins are installed." in result.output
|
|
|
|
|
|
def test_check_updates_without_network_access(monkeypatch):
|
|
monkeypatch.setattr("mvt.common.cmd_plugins.settings.NETWORK_ACCESS_ALLOWED", False)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cmd_plugins.installed_plugin_distributions",
|
|
lambda: pytest.fail("plugins must not be listed without network access"),
|
|
)
|
|
monkeypatch.setattr(
|
|
PluginUpdates,
|
|
"check",
|
|
lambda self: pytest.fail("nothing must be checked without network access"),
|
|
)
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Network access is disabled" in result.output
|