Add info command

This commit is contained in:
Alexander Wainwright
2025-06-24 15:48:31 +10:00
parent 788e69ebd6
commit 5de63963a4
3 changed files with 155 additions and 0 deletions

46
src/locutus/info.py Normal file
View File

@@ -0,0 +1,46 @@
import argparse
import os
import subprocess
import sys
from locutus.config import LocutusConfig
def run_info(args: argparse.Namespace) -> int:
"""Show current config/profile summary and repository info."""
cfg = LocutusConfig(args.config, args.profile)
print(f'Config: {cfg.toml_path}')
print(f'Profile: {cfg.rc_path}')
repo = cfg.get_repo()
if repo:
print(f'Repo: {repo}')
else:
print('Repo: (not set)')
passphrase = cfg.get_passphrase()
if passphrase:
print('Passphrase: set')
else:
print('Passphrase: (not set)')
print('\nIncludes:')
for inc in cfg.includes:
print(f' {inc}')
print('\nExcludes:')
for exc in cfg.excludes:
print(f' {exc}')
if cfg.prune:
prune_str = ', '.join(f'{k}={v}' for k, v in cfg.prune.items())
print(f'\nPrune: {prune_str}')
print('\n[borg info output below]\n')
if repo:
env = {**os.environ, **cfg.env}
try:
subprocess.run(['borg', 'info', repo], env=env, check=True)
except Exception as e:
print(f'borg info failed: {e}', file=sys.stderr)
return 0

View File

@@ -73,6 +73,11 @@ def parse_args() -> tuple[argparse.Namespace, argparse.ArgumentParser]:
)
restore_parser.add_argument('targetdir', help='Directory to restore into')
# info
_info_parser = subparsers.add_parser(
'info', help='Show current config and repository info'
)
return parser.parse_args(), parser
@@ -104,6 +109,10 @@ def main() -> int:
from .restore import run_restore
return run_restore(args)
case 'info':
from .info import run_info
return run_info(args)
case _:
parser.print_help()
return 1

100
test/test_info.py Normal file
View File

@@ -0,0 +1,100 @@
import argparse
from unittest import mock
import subprocess
import pytest
from locutus.info import run_info
def make_args(config, profile):
args = argparse.Namespace()
args.config = config
args.profile = profile
return args
@mock.patch('locutus.info.LocutusConfig')
@mock.patch('subprocess.run')
def test_info_basic_output(mock_run, mock_config, capsys):
mock_config.return_value.toml_path = '/tmp/test.toml'
mock_config.return_value.rc_path = '/tmp/test.rc'
mock_config.return_value.get_repo.return_value = '/repo'
mock_config.return_value.get_passphrase.return_value = 'hunter2'
mock_config.return_value.includes = ['/etc', '/home']
mock_config.return_value.excludes = ['*.cache']
mock_config.return_value.prune = {'keep_last': 3, 'keep_daily': 1}
args = make_args('/tmp/test.toml', '/tmp/test.rc')
mock_run.return_value.returncode = 0
run_info(args)
out = capsys.readouterr().out
assert 'Config: /tmp/test.toml' in out
assert 'Profile: /tmp/test.rc' in out
assert 'Repo: /repo' in out
assert 'Passphrase: set' in out
assert 'Includes:' in out and '/etc' in out and '/home' in out
assert 'Excludes:' in out and '*.cache' in out
assert 'Prune: keep_last=3, keep_daily=1' in out
assert '[borg info output below]' in out
mock_run.assert_called_with(
['borg', 'info', '/repo'], env=mock.ANY, check=True
)
@mock.patch('locutus.info.LocutusConfig')
@mock.patch('subprocess.run')
def test_info_no_repo(mock_run, mock_config, capsys):
mock_config.return_value.toml_path = '/tmp/test.toml'
mock_config.return_value.rc_path = '/tmp/test.rc'
mock_config.return_value.get_repo.return_value = None
mock_config.return_value.get_passphrase.return_value = None
mock_config.return_value.includes = []
mock_config.return_value.excludes = []
mock_config.return_value.prune = {}
args = make_args('/tmp/test.toml', '/tmp/test.rc')
run_info(args)
out = capsys.readouterr().out
assert 'Repo: (not set)' in out
assert 'Passphrase: (not set)' in out
assert '[borg info output below]' in out
mock_run.assert_not_called()
@mock.patch('locutus.info.LocutusConfig')
@mock.patch(
'subprocess.run',
side_effect=subprocess.CalledProcessError(1, ['borg', 'info']),
)
def test_info_borg_info_fails(mock_run, mock_config, capsys):
mock_config.return_value.toml_path = '/tmp/test.toml'
mock_config.return_value.rc_path = '/tmp/test.rc'
mock_config.return_value.get_repo.return_value = '/repo'
mock_config.return_value.get_passphrase.return_value = 'hunter2'
mock_config.return_value.includes = []
mock_config.return_value.excludes = []
mock_config.return_value.prune = {}
args = make_args('/tmp/test.toml', '/tmp/test.rc')
run_info(args)
err = capsys.readouterr().err
assert 'borg info failed:' in err
@mock.patch('locutus.info.LocutusConfig')
@mock.patch('subprocess.run', side_effect=Exception('fail'))
def test_info_borg_info_generic_exception(mock_run, mock_config, capsys):
mock_config.return_value.toml_path = '/tmp/test.toml'
mock_config.return_value.rc_path = '/tmp/test.rc'
mock_config.return_value.get_repo.return_value = '/repo'
mock_config.return_value.get_passphrase.return_value = 'hunter2'
mock_config.return_value.includes = []
mock_config.return_value.excludes = []
mock_config.return_value.prune = {}
args = make_args('/tmp/test.toml', '/tmp/test.rc')
run_info(args)
err = capsys.readouterr().err
assert 'borg info failed: fail' in err