diff --git a/Makefile b/Makefile index a84b950..bcec8e9 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ test: @. venv/bin/activate @( \ cd tests/; \ - python3 -m unittest; \ + pytest; \ ) clean: distclean diff --git a/requirements.txt b/requirements.txt index 8ffe7ac..1368bdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pyinstaller PyQt5 borgbackup[fuse]==1.1.8 +pytest diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1c2e03c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,69 @@ +import json +import os +import pytest +import subprocess +from shutil import copyfile + +import context +import borg_interface as borg +from main_window import MainWindow +from helper import remove_path + + +def example_config(): + return '/tmp/test.conf' + + +@pytest.fixture +def mock_home(tmpdir): + envs = { + 'HOME': tmpdir.strpath, + } + yield envs + remove_path(envs['HOME']) + + +@pytest.fixture +def setup_config(): + tmp_path = '/tmp/test.conf' + dir_path = os.path.dirname(os.path.realpath(__file__)) + config_path = os.path.join(dir_path, + '../docs/borg_qt.conf.example') + copyfile(config_path, tmp_path) + yield tmp_path + os.remove(tmp_path) + + +@pytest.fixture +def form(setup_config, monkeypatch): + form = MainWindow() + monkeypatch.setattr(form.config, '_get_path', example_config) + form.config.read() + return form + + +@pytest.fixture(scope='session') +def repository(tmpdir_factory): + repository_path = tmpdir_factory.mktemp('test-borgqt') + os.environ['BORG_REPO'] = repository_path.strpath + os.environ['BORG_PASSPHRASE'] = 'foo' + os.environ['BORG_DISPLAY_PASSPHRASE'] = 'no' + subprocess.run(['borg', 'init', + '--encryption=repokey-blake2'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + yield + remove_path(repository_path) + +@pytest.fixture +def archives(repository): + backup_thread = borg.BackupThread(['.']) + backup_thread.run() + list_thread = borg.ListThread() + output = list_thread.run() + return output + +@pytest.fixture +def target_path(tmpdir): + yield str(tmpdir) + remove_path(str(tmpdir)) diff --git a/tests/test_borg.py b/tests/test_borg.py index 9405ce0..0d703c7 100644 --- a/tests/test_borg.py +++ b/tests/test_borg.py @@ -6,86 +6,55 @@ from time import strftime from PyQt5.QtWidgets import QApplication import context -from testcase import BorgInterfaceTest import borg_interface as borg -from helper import create_path, remove_path +from helper import create_path app = QApplication(sys.argv) -class BackupTestCase(BorgInterfaceTest): - def test_backup(self): - self.backup_thread = borg.BackupThread(['.']) - self.backup_thread.run() - output = subprocess.check_output(['borg', 'list'], encoding='utf8') - self.assertNotEqual(-1, output.find(strftime('%Y-%m-%d_%H:'))) - - def test_backup_with_prefix(self): - self.backup_thread = borg.BackupThread(['.'], prefix='test') - self.backup_thread.run() - output = subprocess.check_output(['borg', 'list'], encoding='utf8') - self.assertNotEqual(-1, output.find(strftime('test_%Y-%m-%d_%H:'))) +def test_backup(repository): + backup_thread = borg.BackupThread(['.']) + backup_thread.run() + output = subprocess.check_output(['borg', 'list'], encoding='utf8') + assert -1 != output.find(strftime('%Y-%m-%d_%H:')) -class RestoreTestCase(BorgInterfaceTest): - def setUp(self): - super().setUp() - self.backup_thread = borg.BackupThread(['.']) - self.backup_thread.run() - self.target_path = '/tmp/restore/' - self.list_thread = borg.ListThread() - repo_archives = self.list_thread.run() - self.archive_name = repo_archives[0]['name'] - self.restore_path = os.path.join(self.target_path, self.archive_name) - create_path(self.restore_path) - - def tearDown(self): - remove_path(self.target_path) - super().tearDown() - - def test_restore(self): - thread = borg.RestoreThread(self.archive_name, self.restore_path) - thread.run() - self.assertTrue(os.path.exists( - os.path.join(self.restore_path, os.path.realpath(__file__)))) +def test_backup_with_prefix(repository): + backup_thread = borg.BackupThread(['.'], prefix='test') + backup_thread.run() + output = subprocess.check_output(['borg', 'list'], encoding='utf8') + assert -1 != output.find(strftime('test_%Y-%m-%d_%H:')) -class DeleteTestCase(BorgInterfaceTest): - def setUp(self): - super().setUp() - self.backup_thread = borg.BackupThread(['.']) - self.backup_thread.run() - self.list_thread = borg.ListThread() - repo_archives = self.list_thread.run() - self.archive_name = repo_archives[0]['name'] - - def test_delete(self): - thread = borg.DeleteThread(self.archive_name) - thread.run() - self.list_thread = borg.ListThread() - repo_archives = self.list_thread.run() - self.assertEqual(repo_archives, []) +def test_restore(target_path, archives): + archive_list = archives + archive_name = archive_list[0]['name'] + restore_path = os.path.join(target_path, archive_name) + create_path(restore_path) + thread = borg.RestoreThread(archive_name, restore_path) + thread.run() + assert os.path.exists( + os.path.join(restore_path, os.path.realpath(__file__))) -class MountTestCase(BorgInterfaceTest): - def setUp(self): - super().setUp() - self.backup_thread = borg.BackupThread(['.']) - self.backup_thread.run() - self.list_thread = borg.ListThread() - repo_archives = self.list_thread.run() - self.archive_name = repo_archives[0]['name'] - self.mount_path = os.path.join('/tmp/', self.archive_name) - create_path(self.mount_path) +def test_delete(target_path, archives): + archive_list = archives + archive_name = archive_list[0]['name'] + thread = borg.DeleteThread(archive_name) + thread.run() + list_thread = borg.ListThread() + repo_archives = list_thread.run() + assert archive_name not in repo_archives - def tearDown(self): - os.system('borg umount ' + self.mount_path) - remove_path(self.mount_path) - super().tearDown() - def test_mount(self): - thread = borg.MountThread(self.archive_name, self.mount_path) - thread.run() - self.assertTrue(os.path.exists( - os.path.join(self.mount_path, os.path.realpath(__file__)))) +def test_mount(target_path, archives): + archive_list = archives + archive_name = archive_list[0]['name'] + mount_path = os.path.join(target_path, archive_name) + create_path(mount_path) + thread = borg.MountThread(archive_name, mount_path) + thread.run() + assert os.path.exists( + os.path.join(mount_path, os.path.realpath(__file__))) + os.system('borg umount ' + mount_path) diff --git a/tests/test_config.py b/tests/test_config.py index e72cf47..fa2e5e0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,147 +1,123 @@ import os import sys import configparser -import unittest -from unittest.mock import MagicMock, patch -import warnings -from shutil import copyfile + +import pytest from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication from PyQt5.QtTest import QTest -import context from main_window import MainWindow from helper import BorgException -from testcase import BorgQtTestCase app = QApplication(sys.argv) -class TestConfiguration(BorgQtTestCase): - def test_read_configuration(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - parser = configparser.ConfigParser() - parser.read(self.config_path) - self.assertEqual(parser, self.form.config.config) - - @patch('config.os.path') - def test_absent_config_file(self, mock_path): - mock_path.exists.return_value = False - with self.assertRaises(BorgException): - self.form.config._get_path() - - def test_empty_port(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - self.form.config.config['borgqt']['port'] = "" - with self.assertRaises(BorgException): - self.form.config._create_server_path() - - def test_absent_port(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - if 'port' in self.form.config.config['DEFAULT']: - self.form.config.config['DEFAULT'].pop('port', None) - if 'port' in self.form.config.config['borgqt']: - self.form.config.config['borgqt'].pop('port', None) - with self.assertRaises(BorgException): - self.form.config._create_server_path() - - def test_empty_user(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - self.form.config.config['borgqt']['user'] = "" - with self.assertRaises(BorgException): - self.form.config._create_server_path() - - def test_absent_user(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - if 'user' in self.form.config.config['DEFAULT']: - self.form.config.config['DEFAULT'].pop('user', None) - if 'user' in self.form.config.config['borgqt']: - self.form.config.config['borgqt'].pop('user', None) - with self.assertRaises(BorgException): - self.form.config._create_server_path() - - def test_apply_settings(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - self.form.config.apply_options() - self.assertIs(type(self.form.config.excludes), list) - self.assertEqual(self.form.config.full_path, - os.environ['BORG_REPO']) - self.assertEqual(self.form.config.password, - os.environ['BORG_PASSPHRASE']) +def test_read_configuration(form): + parser = configparser.ConfigParser() + parser.read(form.config.path) + assert parser == form.config.config -class TestWriteConfiguration(BorgQtTestCase): - def tearDown(self): - if os.path.exists(self.form.config.path): - os.remove(self.form.config.path) +def test_absent_config_file(monkeypatch): + def mock_path(path): + return False - def test_write_config(self): - self.form.config._get_path = MagicMock(return_value=self.config_path) - self.form.config.read() - self.form.config.path = '/tmp/test.conf' - self.form.config.write() - config = configparser.ConfigParser() - config.read(self.form.config.path) - for value in config['borgqt']: - self.assertEqual(self.form.config.config['borgqt'][value], - config['borgqt'][value]) + form = MainWindow() + monkeypatch.setattr(os.path, 'exists', mock_path) + with pytest.raises(BorgException): + form.config._get_path() -class TestGuiConfiguration(BorgQtTestCase): - def setUp(self): - super().setUp() - copyfile(self.config_path, '/tmp/test.conf') - self.form.config._get_path = MagicMock(return_value='/tmp/test.conf') - self.form.config.read() +def test_empty_port(form): + form.config.config['borgqt']['port'] = "" + with pytest.raises(BorgException): + form.config._create_server_path() - def tearDown(self): - if os.path.exists(self.form.config.path): - os.remove(self.form.config.path) - def test_set_form_values(self): - self.form.config.set_form_values() - self.assertEqual(self.form.config.password, - self.form.config.line_edit_password.text()) +def test_absent_port(form): + if 'port' in form.config.config['DEFAULT']: + form.config.config['DEFAULT'].pop('port', None) + if 'port' in form.config.config['borgqt']: + form.config.config['borgqt'].pop('port', None) + with pytest.raises(BorgException): + form.config._create_server_path() - def test_cancel_settings(self): - self.form.config.line_edit_password.clear() - QTest.keyClicks(self.form.config.line_edit_password, "bar") - cancel_button = self.form.config.button_box.button( - self.form.config.button_box.Cancel) - QTest.mouseClick(cancel_button, Qt.LeftButton) - self.assertEqual(self.form.config.password, - self.form.config.config['borgqt']['password']) - def test_ok_settings(self): - self.form.config.line_edit_password.clear() - QTest.keyClicks(self.form.config.line_edit_password, "bar") - ok_button = self.form.config.button_box.button( - self.form.config.button_box.Ok) - QTest.mouseClick(ok_button, Qt.LeftButton) - parser = configparser.ConfigParser() - parser.read(self.form.config.path) - self.assertEqual(self.form.config.line_edit_password.text(), - parser['borgqt']['password']) +def test_empty_user(form): + form.config.config['borgqt']['user'] = "" + with pytest.raises(BorgException): + form.config._create_server_path() - def test_include_remove(self): - self.form.config.set_form_values() - counter = self.form.config.list_include.count() - self.form.config.list_include.setCurrentRow(0) - self.form.config.remove_include() - self.assertGreaterEqual(counter, self.form.config.list_include.count()) - def test_exclude_remove(self): - self.form.config.set_form_values() - counter = self.form.config.list_exclude.count() - self.form.config.list_exclude.setCurrentRow(0) - self.form.config.remove_exclude() - self.assertGreaterEqual(counter, self.form.config.list_exclude.count()) +def test_absent_user(form): + if 'user' in form.config.config['DEFAULT']: + form.config.config['DEFAULT'].pop('user', None) + if 'user' in form.config.config['borgqt']: + form.config.config['borgqt'].pop('user', None) + with pytest.raises(BorgException): + form.config._create_server_path() + + +def test_apply_settings(form): + form.config.apply_options() + assert type(form.config.excludes) == list + assert form.config.full_path == os.environ['BORG_REPO'] + assert form.config.password == os.environ['BORG_PASSPHRASE'] + + +def test_write_config(form): + form.config.config['borgqt']['password'] == 'Test String' + form.config.write() + config = configparser.ConfigParser() + config.read(form.config.path) + for value in config['borgqt']: + assert (form.config.config['borgqt'][value] + == config['borgqt'][value]) + + +def test_set_form_values(form): + form.config.set_form_values() + assert (form.config.password + == form.config.line_edit_password.text()) + + +def test_cancel_settings(form): + form.config.line_edit_password.clear() + QTest.keyClicks(form.config.line_edit_password, "bar") + cancel_button = form.config.button_box.button( + form.config.button_box.Cancel) + QTest.mouseClick(cancel_button, Qt.LeftButton) + assert (form.config.password + == form.config.config['borgqt']['password']) + + +def test_ok_settings(form): + form.config.line_edit_password.clear() + QTest.keyClicks(form.config.line_edit_password, "bar") + ok_button = form.config.button_box.button( + form.config.button_box.Ok) + QTest.mouseClick(ok_button, Qt.LeftButton) + parser = configparser.ConfigParser() + parser.read(form.config.path) + assert (form.config.line_edit_password.text() + == parser['borgqt']['password']) + + +def test_include_remove(form): + form.config.set_form_values() + counter = form.config.list_include.count() + form.config.list_include.setCurrentRow(0) + form.config.remove_include() + assert (counter >= form.config.list_include.count()) + + +def test_exclude_remove(form): + form.config.set_form_values() + counter = form.config.list_exclude.count() + form.config.list_exclude.setCurrentRow(0) + form.config.remove_exclude() + assert (counter >= form.config.list_exclude.count()) diff --git a/tests/test_systemd.py b/tests/test_systemd.py index 38e247e..cb025c5 100644 --- a/tests/test_systemd.py +++ b/tests/test_systemd.py @@ -1,63 +1,64 @@ import os -import unittest -from testcase import TestSystemd - -import context from systemd import SystemdFile -class TestSystemdUnit(TestSystemd): - def setUp(self): - self.path = os.path.join(os.environ['HOME'], - '.config/systemd/user/borg_qt.service') - - def test_write_unit(self): - systemd_unit = SystemdFile('borg_qt.service') - systemd_unit.write() - self.assertTrue(os.path.exists(self.path)) +def test_write_unit(mock_home, monkeypatch): + monkeypatch.setattr(os, 'environ', mock_home) + systemd_unit = SystemdFile('borg_qt.service') + systemd_unit.write() + assert os.path.exists(systemd_unit.path) -class TestSystemdTimer(TestSystemd): - def setUp(self): - self.path = os.path.join(os.environ['HOME'], - '.config/systemd/user/borg_qt.timer') - - def test_write_timer(self): - systemd_timer = SystemdFile('borg_qt.timer') - systemd_timer.write() - self.assertTrue(os.path.exists(self.path)) +def test_write_timer(mock_home, monkeypatch): + monkeypatch.setattr(os, 'environ', mock_home) + systemd_timer = SystemdFile('borg_qt.timer') + systemd_timer.write() + assert os.path.exists(systemd_timer.path) -class TestSystemdEnabledTimer(TestSystemd): - def setUp(self): - self.symlink_path = os.path.join(os.environ['HOME'], - '.config/systemd/user/timers.target.wants/borg_qt.timer') - self.path = os.path.join(os.environ['HOME'], - '.config/systemd/user/borg_qt.timer') +# This test currently runs against the real home directory of the current user. +# Since this can cause problems with a productive Borg-Qt setup I'm commenting +# out this test for the moment. The long time target is to replace it with a +# test which mocks the call to systemd. +# +# class TestSystemdEnabledTimer(): +# def setup_method(self): +# self.unit_path = os.path.join(os.environ['HOME'], +# '.config/systemd/user/borg_qt.service') +# self.timer_path = os.path.join(os.environ['HOME'], +# '.config/systemd/user/borg_qt.timer') +# self.symlink_path = os.path.join(os.environ['HOME'], +# '.config/systemd/user/timers.target.wants/borg_qt.timer') - def tearDown(self): - super().tearDown() - os.remove(self.symlink_path) +# def teardown_method(self): +# if os.path.exists(self.unit_path): +# os.remove(self.unit_path) +# if os.path.exists(self.symlink_path): +# os.remove(self.symlink_path) +# if os.path.exists(self.timer_path): +# os.remove(self.timer_path) - def test_enable_timer(self): - systemd_unit = SystemdFile('borg_qt.service') - systemd_timer = SystemdFile('borg_qt.timer') - systemd_unit.content['Unit'] = {} - systemd_unit.content['Unit']['After'] = 'default.target' - systemd_unit.content['Install'] = {} - systemd_unit.content['Install']['Wanted'] = 'default.target' - systemd_unit.content['Service'] = {} - systemd_unit.content['Service']['Type'] = 'forking' - systemd_unit.content['Service']['ExecStart'] = '/bin/echo "test"' - systemd_unit.write() - systemd_timer.content['Unit'] = {} - systemd_timer.content['Unit']['Description'] = 'Test Timer' - systemd_timer.content['Timer'] = {} - systemd_timer.content['Timer']['OnCalendar'] = 'daily' - systemd_timer.content['Timer']['Persistent'] = 'true' - systemd_timer.content['Install'] = {} - systemd_timer.content['Install']['WantedBy'] = 'timers.target' - systemd_timer.write() - systemd_timer.enable() - self.assertTrue(os.path.exists(self.symlink_path)) +# def test_enable_timer(self): +# systemd_unit = SystemdFile('borg_qt.service') +# systemd_unit.path = self.unit_path +# systemd_timer = SystemdFile('borg_qt.timer') +# systemd_timer.path = self.timer_path +# systemd_unit.content['Unit'] = {} +# systemd_unit.content['Unit']['After'] = 'default.target' +# systemd_unit.content['Install'] = {} +# systemd_unit.content['Install']['Wanted'] = 'default.target' +# systemd_unit.content['Service'] = {} +# systemd_unit.content['Service']['Type'] = 'forking' +# systemd_unit.content['Service']['ExecStart'] = '/bin/echo "test"' +# systemd_unit.write() +# systemd_timer.content['Unit'] = {} +# systemd_timer.content['Unit']['Description'] = 'Test Timer' +# systemd_timer.content['Timer'] = {} +# systemd_timer.content['Timer']['OnCalendar'] = 'daily' +# systemd_timer.content['Timer']['Persistent'] = 'true' +# systemd_timer.content['Install'] = {} +# systemd_timer.content['Install']['WantedBy'] = 'timers.target' +# systemd_timer.write() +# systemd_timer.enable() +# assert os.path.exists(self.symlink_path) diff --git a/tests/testcase.py b/tests/testcase.py deleted file mode 100644 index ced8c72..0000000 --- a/tests/testcase.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import subprocess -import shutil - -import unittest -import warnings - -import context -from main_window import MainWindow -from helper import remove_path - - -def fxn(): - warnings.warn("deprecated", DeprecationWarning) - - -class BorgQtTestCase(unittest.TestCase): - def setUp(self): - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - fxn() - self.form = MainWindow() - - self.dir_path = os.path.dirname(os.path.realpath(__file__)) - self.config_path = os.path.join(self.dir_path, - '../docs/borg_qt.conf.example') - - -class BorgInterfaceTest(unittest.TestCase): - def setUp(self): - super().setUp() - self.repository_path = '/tmp/test-borgqt' - os.environ['BORG_REPO'] = self.repository_path - os.environ['BORG_PASSPHRASE'] = 'foo' - os.environ['BORG_DISPLAY_PASSPHRASE'] = 'no' - subprocess.run(['borg', 'init', - '--encryption=repokey-blake2'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - def tearDown(self): - remove_path(self.repository_path) - - -class TestSystemd(unittest.TestCase): - def tearDown(self): - os.remove(self.path)