mirror of
https://github.com/borgbackup/borg.git
synced 2026-03-26 20:34:45 -04:00
tests: CLI UX regression coverage and test-ux.sh
Add cli_guidance and argparsing hint tests; adjust repository remote tests; manual UX smoke script.
This commit is contained in:
parent
ed85f0004f
commit
dddbaf02fc
5 changed files with 269 additions and 2 deletions
37
scripts/test-ux.sh
Normal file
37
scripts/test-ux.sh
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
set -x
|
||||
#errors that should be have helpful help
|
||||
|
||||
borg --repo /tmp/demo-repo init -e repokey-aes-ocb
|
||||
borg --repo /tmp/demo-repo rcreate -e repokey-aes-ocb
|
||||
|
||||
#Typo suggestions (Did you mean ...?)
|
||||
|
||||
borg repo-creat
|
||||
borg repoo-list
|
||||
Borg1 -> Borg2 option hints
|
||||
|
||||
borg --repo /tmp/demo-repo list --glob-archives 'my*'
|
||||
borg --repo /tmp/demo-repo create --numeric-owner test ~/data
|
||||
borg --repo /tmp/demo-repo create --nobsdflags test ~/data
|
||||
borg --repo /tmp/demo-repo create --remote-ratelimit 1000 test ~/data
|
||||
|
||||
#Missing encryption guidance for repo-create
|
||||
|
||||
borg --repo /tmp/demo-repo repo-create
|
||||
|
||||
#repo::archive migration help (BORG_REPO / --repo guidance)
|
||||
|
||||
borg --repo /tmp/demo-repo::test1 repo-info
|
||||
borg --repo /tmp/demo-repo::test1 list
|
||||
|
||||
#Missing repo recovery hint (includes repo-create example + -e modes)
|
||||
|
||||
borg --repo /tmp/does-not-exist repo-info
|
||||
borg --repo /tmp/does-not-exist list
|
||||
|
||||
#Common fixes block (missing repo / unknown command)
|
||||
|
||||
borg list
|
||||
borg frobnicate
|
||||
|
||||
#Options are preserved by command-line correction.
|
||||
217
src/borg/testsuite/archiver/cli_guidance_test.py
Normal file
217
src/borg/testsuite/archiver/cli_guidance_test.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ...helpers.argparsing import ArgumentParser
|
||||
from ...repository import Repository
|
||||
from . import exec_cmd
|
||||
|
||||
|
||||
def test_unknown_command_typo_suggests_fuzzy_match(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("repo-creat")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `repo-creat`:" in output
|
||||
assert "\tborg repo-create" in output
|
||||
|
||||
|
||||
def test_unknown_command_typo_list(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("lst")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `list` not `lst`:" in output
|
||||
assert "\tborg list" in output
|
||||
|
||||
|
||||
def test_fuzzy_typo_preserves_following_args(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("creat", "foo", "--stats")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `create` not `creat`:" in output
|
||||
assert "\tborg create foo --stats" in output
|
||||
|
||||
|
||||
def test_legacy_rm_synonym(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("rm")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `delete` not `rm`:" in output
|
||||
assert "\tborg delete" in output
|
||||
|
||||
|
||||
def test_legacy_rm_synonym_preserves_trailing_tokens_in_delete_example(cmd_fixture, tmp_path):
|
||||
"""Tokens after 'rm' must appear in the suggested delete line (not a generic placeholder)."""
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("-r", repo, "rm", "dsfasdfsdfsdf")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `delete` not `rm`:" in output
|
||||
assert "ARCHIVE_OR_AID" not in output
|
||||
assert f"\tborg -r {repo} delete dsfasdfsdfsdf" in output
|
||||
|
||||
|
||||
def test_rm_synonym_example_includes_argv_tail(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["python", "-m", "borg", "-r", "/tmp/borg/outC", "rm", "dsfasdfsdfsdf"])
|
||||
parser = ArgumentParser(prog="borg")
|
||||
message = "error: argument <command>: invalid choice: 'rm' (choose from 'delete', 'list')"
|
||||
hint = parser._top_command_choice_hint(message)
|
||||
assert hint is not None
|
||||
assert "Maybe you meant `delete` not `rm`:" in hint
|
||||
assert "ARCHIVE_OR_AID" not in hint
|
||||
assert "\tborg -r /tmp/borg/outC delete dsfasdfsdfsdf" in hint
|
||||
|
||||
|
||||
def test_lst_typo_example_includes_argv_tail(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["python", "-m", "borg", "-r", "/r", "lst", "my-archive"])
|
||||
parser = ArgumentParser(prog="borg")
|
||||
message = "error: argument <command>: invalid choice: 'lst' (choose from 'list', 'delete')"
|
||||
hint = parser._top_command_choice_hint(message)
|
||||
assert hint is not None
|
||||
assert "Maybe you meant `list` not `lst`:" in hint
|
||||
assert "ARCHIVE" not in hint
|
||||
assert "\tborg -r /r list my-archive" in hint
|
||||
|
||||
|
||||
def test_restore_synonym_example_includes_argv_tail(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["python", "-m", "borg", "-r", "/r", "restore", "arch1"])
|
||||
parser = ArgumentParser(prog="borg")
|
||||
message = "error: argument <command>: invalid choice: 'restore' (choose from 'undelete', 'list')"
|
||||
hint = parser._top_command_choice_hint(message)
|
||||
assert hint is not None
|
||||
assert "Maybe you meant `undelete` not `restore`:" in hint
|
||||
assert "…" not in hint
|
||||
assert "\tborg -r REPO undelete arch1" in hint
|
||||
|
||||
|
||||
def test_maybe_you_meant_rm_is_common_fix_bullet(cmd_fixture):
|
||||
"""Invalid-command hint must appear under Common fixes with a '- ' bullet."""
|
||||
exit_code, output = cmd_fixture("rm")
|
||||
assert exit_code == 2
|
||||
assert "Common fixes:" in output
|
||||
assert "- Maybe you meant `delete` not `rm`:" in output
|
||||
|
||||
|
||||
def test_maybe_you_meant_line_has_dash_prefix_before_maybe(cmd_fixture):
|
||||
"""Regression: '- ' must prefix the Maybe-you-meant line (not a bare paragraph before Common fixes)."""
|
||||
exit_code, output = cmd_fixture("rm")
|
||||
assert exit_code == 2
|
||||
assert "Common fixes:\n- Maybe you meant `delete` not `rm`:" in output
|
||||
|
||||
|
||||
def test_legacy_clean_synonym(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("clean")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `compact` not `clean`:" in output
|
||||
assert "\tborg compact" in output
|
||||
|
||||
|
||||
def test_legacy_restore_synonym(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("restore")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `undelete` not `restore`:" in output
|
||||
assert "\tborg undelete" in output
|
||||
|
||||
|
||||
def test_legacy_init_synonym(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "init", "-e", "none")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `init`:" in output
|
||||
assert f"\tborg --repo {repo} repo-create -e none" in output
|
||||
|
||||
|
||||
def test_legacy_rcreate_synonym(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "rcreate", "-e", "none")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `rcreate`:" in output
|
||||
assert f"\tborg --repo {repo} repo-create -e none" in output
|
||||
|
||||
|
||||
def test_legacy_repocreate_synonym(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repocreate", "-e", "none")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `repocreate`:" in output
|
||||
assert f"\tborg --repo {repo} repo-create -e none" in output
|
||||
|
||||
|
||||
def test_repo_create_missing_encryption_shows_available_modes(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repo-create")
|
||||
assert exit_code == 2
|
||||
assert "Use -e/--encryption to choose a mode" in output
|
||||
assert "Available encryption modes:" in output
|
||||
|
||||
|
||||
def test_repo_double_colon_syntax_shows_migration_hint(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo::archive")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repo-info")
|
||||
assert exit_code == 2
|
||||
assert "does not accept repo::archive syntax" in output
|
||||
assert "borg -r" in output
|
||||
assert "borg list archive" in output
|
||||
assert "borg repo-info" in output
|
||||
assert "export BORG_REPO=" in output
|
||||
|
||||
|
||||
def test_missing_repository_error_shows_create_example(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "missing-repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repo-info")
|
||||
assert exit_code == 2
|
||||
assert "does not exist." in output
|
||||
assert "Common fixes:" in output
|
||||
assert f'Specify Correct Path ("{repo}" does not exist).' in output
|
||||
assert "borg repo-info -r" not in output
|
||||
assert "Create repository (-r): borg repo-create" in output
|
||||
assert "Create repository (BORG_REPO):" in output
|
||||
assert "Available -e modes:" in output
|
||||
|
||||
|
||||
def test_repository_does_not_exist_common_fix_explains_missing_path():
|
||||
msg = Repository.DoesNotExist("/tmp/foo").get_message()
|
||||
assert 'Specify Correct Path ("/tmp/foo" does not exist).' in msg
|
||||
assert "borg repo-info -r" not in msg
|
||||
|
||||
|
||||
def test_repository_invalid_common_fix_explains_not_a_borg_repo():
|
||||
msg = Repository.InvalidRepository("/tmp/foo").get_message()
|
||||
assert 'Specify Correct Path ("/tmp/foo" is not a Borg repository).' in msg
|
||||
assert "borg repo-info -r" not in msg
|
||||
|
||||
|
||||
def test_list_name_none_common_fix_hint():
|
||||
parser = ArgumentParser(prog="borg")
|
||||
hints = parser._common_fix_hints("Validation failed: list.name is None")
|
||||
assert "For 'borg list', set repository via -r/--repo or BORG_REPO and pass an archive name." in hints
|
||||
|
||||
|
||||
def test_list_paths_required_shows_path_and_repo_creation_hints(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "does-not-exist")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "list")
|
||||
assert exit_code == 2
|
||||
assert "Option 'list.paths' is required but not provided" in output
|
||||
assert "borg list requires an archive NAME to list contents." in output
|
||||
assert "- Provide archive name: borg list NAME" in output
|
||||
assert "- To list archives in a repository, use: borg -r REPO repo-list" in output
|
||||
|
||||
|
||||
def test_argument_parser_error_accepts_jsonargparse_extra_arg():
|
||||
parser = ArgumentParser(prog="borg")
|
||||
with pytest.raises(SystemExit):
|
||||
parser.error("bad message", ValueError("wrapped"))
|
||||
|
||||
|
||||
def test_unrecognized_args_before_subcommand_shows_reordered_example(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("--stats", "create", "foo")
|
||||
assert exit_code == 2
|
||||
assert "Unrecognized arguments" in output
|
||||
assert "Common fixes:" in output
|
||||
assert "Put subcommand-specific options after `<command>`:" in output
|
||||
assert "create" in output and "--stats" in output
|
||||
|
||||
|
||||
def test_preprocess_prints_glob_archives_migration_hint(tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = exec_cmd("--repo", repo, "list", "dummy-archive", "--glob-archives", "sh:old", fork=False)
|
||||
assert exit_code == 2
|
||||
assert "Common fixes:" in output
|
||||
assert '- borg1 option "--glob-archives" is not used in borg2.' in output
|
||||
assert "--match-archives 'sh:PATTERN'" in output
|
||||
assert "- Example: borg list ARCHIVE --match-archives 'sh:old-*'" in output
|
||||
13
src/borg/testsuite/helpers/argparsing_test.py
Normal file
13
src/borg/testsuite/helpers/argparsing_test.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ...helpers.argparsing import _suggest_move_options_after_subcommand
|
||||
|
||||
|
||||
def test_suggest_reorder_unrecognized_args_before_subcommand(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["borg", "--stats", "create", "foo"])
|
||||
s = _suggest_move_options_after_subcommand("error: Unrecognized arguments: --stats")
|
||||
assert s is not None
|
||||
assert "create" in s and "--stats" in s
|
||||
assert s.index("create") < s.index("--stats")
|
||||
|
|
@ -913,7 +913,7 @@ def test_remote_rpc_exception_transport(remote_repository):
|
|||
remote_repository.call("inject_exception", {"kind": "DoesNotExist"})
|
||||
except LegacyRepository.DoesNotExist as e:
|
||||
assert len(e.args) == 1
|
||||
assert e.args[0] == remote_repository.location.processed
|
||||
assert remote_repository.location.processed in e.args[0]
|
||||
|
||||
try:
|
||||
remote_repository.call("inject_exception", {"kind": "AlreadyExists"})
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ def test_remote_rpc_exception_transport(remote_repository):
|
|||
remote_repository.call("inject_exception", {"kind": "DoesNotExist"})
|
||||
except Repository.DoesNotExist as e:
|
||||
assert len(e.args) == 1
|
||||
assert e.args[0] == remote_repository.location.processed
|
||||
assert remote_repository.location.processed in e.args[0]
|
||||
|
||||
try:
|
||||
remote_repository.call("inject_exception", {"kind": "AlreadyExists"})
|
||||
|
|
|
|||
Loading…
Reference in a new issue