# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf-8 -*-
#
# Copyright 2002 Ben Escoto <ben@emerose.org>
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
#
# This file is part of duplicity.
#
# Duplicity 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.
#
# Duplicity 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 duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import copy
import shlex
import unittest
import pytest
from duplicity import cli_main
from duplicity import gpg
from duplicity.cli_data import *
from duplicity.cli_util import *
from testing.unit import UnitTestCase
[docs]@unittest.skipIf(os.environ.get("USER", "") == "buildd", "Skip test on Launchpad")
class CommandlineTest(UnitTestCase):
"""
Test parse_commandline_options
"""
good_args = {
"count": "5",
"remove_time": "100",
"source_path": "foo/bar",
"source_url": "file://duptest",
"target_dir": "foo/bar",
"target_url": "file://duptest",
}
[docs] def setUp(self):
super().setUp()
log.setup()
config.gpg_profile = gpg.GPGProfile()
os.makedirs("foo/bar", exist_ok=True)
os.makedirs("inc", exist_ok=True)
os.makedirs("full", exist_ok=True)
[docs] def tearDown(self):
log.shutdown()
os.removedirs("foo/bar")
os.removedirs("inc")
os.removedirs("full")
[docs] def run_all_commands_with_errors(self, new_args, err_msg):
"""
Test all commands with the supplied argument list.
Only test command if new_args contains needed arg.
"""
test_args = copy.copy(self.good_args)
test_args.update(new_args)
for var in DuplicityCommands.__dict__.keys():
if var.startswith("__"):
continue
cmd = var2cmd(var)
runtest = False
args = DuplicityCommands.__dict__[var]
cline = [cmd]
for arg in args:
cline.append(test_args[arg])
if arg in new_args:
runtest = True
if runtest:
with self.assertRaisesRegex(cli_main.CommandLineError, err_msg) as cm:
cli_main.process_command_line(cline)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_full_command(self):
"""
test backup, restore, verify with explicit commands
"""
for cmd in ["cleanup"] + cli_main.CommandAliases.cleanup:
cli_main.process_command_line(f"{cmd} file://duptest".split())
self.assertEqual(config.action, "cleanup")
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["collection-status"] + cli_main.CommandAliases.collection_status:
cli_main.process_command_line(f"{cmd} file://duptest".split())
self.assertEqual(config.action, "collection-status")
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["full"] + cli_main.CommandAliases.full:
cli_main.process_command_line(f"{cmd} foo/bar file://duptest".split())
self.assertEqual(config.action, "full")
self.assertTrue(config.source_path.endswith("foo/bar"))
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["incremental"] + cli_main.CommandAliases.incremental:
cli_main.process_command_line(f"{cmd} foo/bar file://duptest".split())
self.assertEqual(config.action, "inc")
self.assertTrue(config.source_path.endswith("foo/bar"))
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["list-current-files"] + cli_main.CommandAliases.list_current_files:
cli_main.process_command_line(f"{cmd} file://duptest".split())
self.assertEqual(config.action, "list-current-files")
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["remove-all-but-n-full"] + cli_main.CommandAliases.remove_all_but_n_full:
cli_main.process_command_line(f"{cmd} 5 file://duptest".split())
self.assertEqual(config.action, "remove-all-but-n-full")
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["remove-all-inc-of-but-n-full"] + cli_main.CommandAliases.remove_all_inc_of_but_n_full:
cli_main.process_command_line(f"{cmd} 5 file://duptest".split())
self.assertEqual(config.action, "remove-all-inc-of-but-n-full")
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["remove-older-than"] + cli_main.CommandAliases.remove_older_than:
cli_main.process_command_line(f"{cmd} 100 file://duptest".split())
self.assertEqual(config.action, "remove-older-than")
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["restore"] + cli_main.CommandAliases.restore:
cli_main.process_command_line(f"{cmd} file://duptest foo/bar".split())
self.assertEqual(config.action, "restore")
self.assertTrue(config.source_path.endswith("foo/bar"))
self.assertEqual(config.target_url, "file://duptest")
for cmd in ["verify"] + cli_main.CommandAliases.verify:
cli_main.process_command_line(f"{cmd} file://duptest foo/bar".split())
self.assertEqual(config.action, "verify")
self.assertTrue(config.source_path.endswith("foo/bar"))
self.assertEqual(config.target_url, "file://duptest")
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_full_command_errors_reversed_args(self):
"""
test backup, restore, verify with explicit commands - reversed arg
"""
new_args = {
"source_path": "file://duptest",
"source_url": "foo/bar",
"target_dir": "file://duptest",
"target_url": "foo/bar",
}
err_msg = "should be url|should be pathname"
self.run_all_commands_with_errors(new_args, err_msg)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_full_command_errors_bad_url(self):
"""
test backup, restore, verify with explicit commands - bad url
"""
new_args = {
"source_url": "file:/duptest",
"target_url": "file:/duptest",
}
err_msg = "should be url"
self.run_all_commands_with_errors(new_args, err_msg)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_full_command_errors_bad_integer(self):
"""
test backup, restore, verify with explicit commands - bad integer
"""
new_args = {
"count": "foo",
}
err_msg = "not an int"
self.run_all_commands_with_errors(new_args, err_msg)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_full_command_errors_bad_time_string(self):
"""
test backup, restore, verify with explicit commands - bad time string
"""
new_args = {
"remove_time": "foo",
}
err_msg = "Bad time string"
self.run_all_commands_with_errors(new_args, err_msg)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_option_aliases(self):
"""
test short option aliases
"""
cline = "ib foo/bar file:///target_url -v 9".split()
cli_main.process_command_line(cline)
self.assertEqual(log.getverbosity(), log.DEBUG)
cline = "rb file:///source_url foo/bar -t 10000".split()
cli_main.process_command_line(cline)
self.assertEqual(config.restore_time, 10000)
cline = "rb file:///source_url foo/bar --time 10000".split()
cli_main.process_command_line(cline)
self.assertEqual(config.restore_time, 10000)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_encryption_options(self):
"""
test encrypt/sign key handling
"""
start = "ib foo/bar file:///target_url "
keys = (
"DEADDEAD",
"DEADDEADDEADDEAD",
"DEADDEADDEADDEADDEADDEADDEADDEADDEADDEAD",
)
for key in keys:
cline = f"{start} --encrypt-key={key}".split()
cli_main.process_command_line(cline)
self.assertEqual(config.gpg_profile.recipients, [key])
cline = f"{start} --encrypt-sign-key={key}".split()
cli_main.process_command_line(cline)
self.assertEqual(config.gpg_profile.recipients, [key])
self.assertEqual(config.gpg_profile.sign_key, key)
cline = f"{start} --hidden-encrypt-key={key}".split()
cli_main.process_command_line(cline)
self.assertEqual(config.gpg_profile.hidden_recipients, [key])
cline = f"{start} --sign-key={key}".split()
cli_main.process_command_line(cline)
self.assertEqual(config.gpg_profile.sign_key, key)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_bad_encryption_options(self):
"""
test short option aliases
"""
start = "inc foo/bar file:///target_url "
keys = (
"DEADFOO",
"DEADDEADDEADFOO",
"DEADDEADDEADDEADDEADDEADDEADDEADDEADFOO",
)
for key in keys:
with self.assertRaises(CommandLineError) as cm:
cline = f"{start} --encrypt-key={key}".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = f"{start} --hidden-encrypt-key={key}".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = f"{start} --sign-key={key}".split()
cli_main.process_command_line(cline)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_implied_commands(self):
"""
test implied commands
"""
cline = "foo/bar file:///target_url".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.source_path, "foo/bar")
self.assertEqual(config.target_url, "file:///target_url")
cline = "file:///source_url foo/bar".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "restore")
self.assertEqual(config.source_url, "file:///source_url")
self.assertEqual(config.target_dir, "foo/bar")
cline = "-v9 foo/bar file:///target_url".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.source_path, "foo/bar")
self.assertEqual(config.target_url, "file:///target_url")
cline = "-v9 file:///source_url foo/bar".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "restore")
self.assertEqual(config.source_url, "file:///source_url")
self.assertEqual(config.target_dir, "foo/bar")
cline = "foo/bar -v9 file:///target_url".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.source_path, "foo/bar")
self.assertEqual(config.target_url, "file:///target_url")
cline = "file:///source_url -v9 foo/bar".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "restore")
self.assertEqual(config.source_url, "file:///source_url")
self.assertEqual(config.target_dir, "foo/bar")
cline = "--verbosity n foo/bar file:///target_url".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.source_path, "foo/bar")
self.assertEqual(config.target_url, "file:///target_url")
cline = "--verbosity n file:///source_url foo/bar".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "restore")
self.assertEqual(config.source_url, "file:///source_url")
self.assertEqual(config.target_dir, "foo/bar")
cline = "foo/bar --verbosity n file:///target_url".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.source_path, "foo/bar")
self.assertEqual(config.target_url, "file:///target_url")
cline = "file:///source_url --verbosity n foo/bar".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "restore")
self.assertEqual(config.source_url, "file:///source_url")
self.assertEqual(config.target_dir, "foo/bar")
# this incremental misses the path argument
with self.assertRaises(CommandLineError) as cm:
cline = "inc file:///target_url".split()
cli_main.process_command_line(cline)
# this full backup lacks the path argument
with self.assertRaises(CommandLineError) as cm:
cline = "full file:///target_url".split()
cli_main.process_command_line(cline)
# implied inc works if '/' supplied
cline = "inc/ file:///target_url".split()
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.source_path, "inc/")
self.assertEqual(config.target_url, "file:///target_url")
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_miscellaneous(self):
"""
test miscellaneous parameters
"""
start = "ib foo/bar file:///target_url"
# check defaults, might add more asserts here
cline = start.split()
cli_main.process_command_line(cline)
self.assertEqual(config.print_statistics, True)
cline = f"{start} --no-print-statistics".split()
cli_main.process_command_line(cline)
self.assertEqual(config.print_statistics, False)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_integer_args(self):
"""
test implied commands
"""
cline = "foo/bar file:///target_url --copy-blocksize=1024 --volsize=1024".split()
cli_main.process_command_line(cline)
self.assertEqual(config.copy_blocksize, 1024 * 1024)
self.assertEqual(config.volsize, 1024 * 1024 * 1024)
with self.assertRaises(CommandLineError) as cm:
cline = "foo/bar file:///target_url --copy-blocksize=foo --volsize=1024".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = "foo/bar file:///target_url --copy-blocksize=1024 --volsize=foo".split()
cli_main.process_command_line(cline)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_bad_command(self):
"""
test bad commands
"""
with self.assertRaises(CommandLineError) as cm:
cline = "fbx foo/bar file:///target_url".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = "rbx file:///target_url foo/bar".split()
cli_main.process_command_line(cline)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_too_many_positionals(self):
"""
test bad commands
"""
with self.assertRaises(CommandLineError) as cm:
cline = "fb foo/bar file:///target_url extra".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = "rb file:///target_url foo/bar extra".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = "foo/bar file:///target_url extra".split()
cli_main.process_command_line(cline)
with self.assertRaises(CommandLineError) as cm:
cline = "file:///target_url foo/bar extra".split()
cli_main.process_command_line(cline)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_list_commands(self):
"""
test list commands like ssh_options, etc.
"""
cline = shlex.split("inc foo/bar file:///target_url --ssh-options='--foo'")
cli_main.process_command_line(cline)
self.assertEqual(config.ssh_options, "--foo")
cline = shlex.split("inc foo/bar file:///target_url --ssh-options='--foo' --ssh-options='--bar'")
cli_main.process_command_line(cline)
self.assertEqual(config.ssh_options, "--foo --bar")
cline = shlex.split("inc foo/bar file:///target_url --ssh-options='--foo --bar'")
cli_main.process_command_line(cline)
self.assertEqual(config.ssh_options, "--foo --bar")
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_help_commands(self):
"""
Test -h/--help
"""
with self.assertRaises(SystemExit) as cm:
cli_main.process_command_line(shlex.split("-h"))
self.assertTrue(check_main_help(cm.content))
with self.assertRaises(SystemExit) as cm:
cli_main.process_command_line(shlex.split(f"--help"))
self.assertTrue(check_main_help(cm.content))
for cmd in [var2cmd(v) for v in DuplicityCommands.__dict__.keys() if not v.startswith("__")]:
with self.assertRaises(SystemExit) as cm:
cli_main.process_command_line(shlex.split(f"{cmd} -h"))
with self.assertRaises(SystemExit) as cm:
cli_main.process_command_line(shlex.split(f"{cmd} --help"))
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_log_options(self):
"""
test log options.
"""
log.setup()
# TODO: this fails although running duplicity, cli_main return the correct default loglevel
# default level is notice
# self.assertEqual(log.getverbosity(), log.NOTICE)
# setting custom level
cline = shlex.split("foo/bar file:///target_url --verbosity Debug")
cli_main.process_command_line(cline)
self.assertEqual(log.getverbosity(), log.DEBUG)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_changed_removed(self):
"""
test changed/removed in odd places.
"""
# removed option with command
with self.assertRaises(cli_main.CommandLineError) as cm:
cline = shlex.split("--gio backup foo/bar file:///target_url")
cli_main.process_command_line(cline)
# removed option without command
with self.assertRaises(CommandLineError) as cm:
cline = shlex.split("--gio foo/bar file:///target_url")
cli_main.process_command_line(cline)
# changed option with command
with self.assertRaises(CommandLineError) as cm:
cline = shlex.split("restore --file-to-restore foo/bar file://source_url path")
cli_main.process_command_line(cline)
# changed option without command
with self.assertRaises(CommandLineError) as cm:
cline = shlex.split("--file-to-restore foo/bar file://source_url path")
cli_main.process_command_line(cline)
# removed backup option with command
with self.assertRaises(CommandLineError) as cm:
cline = shlex.split("--time-separator _ backup source_dir file://target_url")
cli_main.process_command_line(cline)
# removed backup option without command
with self.assertRaises(CommandLineError) as cm:
cline = shlex.split("--time-separator _ source_dir file://target_url")
cli_main.process_command_line(cline)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_intermixed_args(self):
"""
test intermixed args.
"""
# Issue 766 -- intermixed -- explicit
cline = shlex.split(
"--archive-dir /tmp/backup-metadata/archive/ --tempdir /tmp/backup-metadata/temp/ "
"--allow-source-mismatch --encrypt-sign-key DEADDEAD --volsize 4096 --progress -v 4 "
"incr --full-if-older-than 30D foo/bar --log-file /tmp/log.txt boto3+s3://foo"
)
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.allow_source_mismatch, True)
self.assertEqual(config.archive_dir, b"/tmp/backup-metadata/archive/")
self.assertEqual(config.full_if_older_than, 2592000)
self.assertEqual(config.progress, True)
self.assertEqual(config.temproot, b"/tmp/backup-metadata/temp/")
self.assertEqual(config.volsize, 4294967296)
# Issue 766 -- intermixed -- implicit
cline = shlex.split(
"--archive-dir /tmp/backup-metadata/archive/ --tempdir /tmp/backup-metadata/temp/ "
"--allow-source-mismatch --encrypt-sign-key DEADDEAD --volsize 4096 --progress -v 4 "
"--full-if-older-than 30D foo/bar --log-file /tmp/log.txt boto3+s3://foo"
)
cli_main.process_command_line(cline)
self.assertEqual(config.action, "inc")
self.assertEqual(config.allow_source_mismatch, True)
self.assertEqual(config.archive_dir, b"/tmp/backup-metadata/archive/")
self.assertEqual(config.full_if_older_than, 2592000)
self.assertEqual(config.progress, True)
self.assertEqual(config.temproot, b"/tmp/backup-metadata/temp/")
self.assertEqual(config.volsize, 4294967296)
[docs] @pytest.mark.usefixtures("redirect_stdin")
def test_regression_issues(self):
"""
test regression issues.
"""
# Issue 759 - --asynchronous-upload
cline = shlex.split("backup --asynchronous-upload / file://target_url")
cli_main.process_command_line(cline)
self.assertEqual(config.async_concurrency, 1)
# Issue 764 - --full-if-older-than -- explicit
cline = shlex.split("backup --full-if-older-than 30D foo/bar file://target_url")
cli_main.process_command_line(cline)
self.assertEqual(config.full_if_older_than, 2592000)
# Issue 764 - --full-if-older-than -- implied
cline = shlex.split("--full-if-older-than 30D foo/bar file://target_url")
cli_main.process_command_line(cline)
self.assertEqual(config.full_if_older_than, 2592000)
# Issue 773 - --exclude-device-files
config.select_opts = []
cline = shlex.split("backup --exclude-device-files / file://target_url")
cli_main.process_command_line(cline)
self.assertListEqual(config.select_opts, [("--exclude-device-files", [])])
# Issue 773 - --exclude-other-fileystems
config.select_opts = []
cline = shlex.split("backup --exclude-other-filesystems / file://target_url")
cli_main.process_command_line(cline)
self.assertListEqual(config.select_opts, [("--exclude-other-filesystems", [])])
# Issue 795/816 - invalid option error using --gpg-options - unbound - argparse bug
with self.assertRaises(CommandLineError) as cm:
cline = shlex.split("backup --gpg-options '--homedir=/home/user' foo/bar file://target_url")
cli_main.process_command_line(cline)
self.assertIn("design error in argparse", cm.exception)
# Issue 795/816 - invalid option error using --gpg-options - bound
cline = shlex.split("backup --gpg-options='--homedir=/home/user' foo/bar file://target_url")
cli_main.process_command_line(cline)
self.assertEqual(config.gpg_options, "--homedir=/home/user")