diff --git a/.travis.yml b/.travis.yml index ce6f446a..eb5c4dba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,8 @@ dist: trusty env: global: - SETUP_XVFB=true - - PYTHON=3.6.3 + - PYTHON36=3.6.3 + - PYTHON37=3.7.1 matrix: include: @@ -48,19 +49,20 @@ matrix: install: - | if [ $TRAVIS_OS_NAME = "linux" ]; then - #curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - #git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv - #git clone git://github.com/pyenv/pyenv-update.git $(pyenv root)/plugins/pyenv-update - #export PATH="/home/travis/.pyenv/shims:${PATH}" export DISPLAY=:99.0 /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset sleep 3 + # cd /opt/pyenv && git pull origin master + pyenv install -s $PYTHON36 + eval "$(pyenv init -)" + pyenv shell $PYTHON36 elif [ $TRAVIS_OS_NAME = "osx" ]; then brew upgrade pyenv + pyenv install -s $PYTHON37 + pyenv install -s $PYTHON36 + eval "$(pyenv init -)" + pyenv shell $PYTHON36 $PYTHON37 fi - pyenv install -s $PYTHON - eval "$(pyenv init -)" - pyenv shell $PYTHON - pip install -U setuptools pip - pip install . @@ -69,12 +71,10 @@ install: before_script: - if [ $TRAVIS_OS_NAME = "linux" ]; then (herbstluftwm )& fi -- if [ $TRAVIS_OS_NAME = "osx" ]; then (sudo Xvfb :99 -ac -screen 0 1024x768x8 )& fi - sleep 3 script: -- pytest --forked -- if [ $TRAVIS_OS_NAME = "linux" ]; then tox -e flake8; fi +- tox #after_script: #- | diff --git a/setup.cfg b/setup.cfg index 729b4d7c..0c777a43 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,7 +68,8 @@ max-line-length = 120 exclude = build,dist,.git,.idea,.cache,.tox,.eggs [tox:tox] -envlist = py36,flake8 +envlist = py36,py37,flake8 +skip_missing_interpreters = true [testenv] deps = @@ -78,6 +79,7 @@ deps = pytest-xdist pytest-faulthandler commands=pytest +passenv = DISPLAY [testenv:flake8] deps = diff --git a/src/vorta/__main__.py b/src/vorta/__main__.py index a4fc1696..72d07bfe 100644 --- a/src/vorta/__main__.py +++ b/src/vorta/__main__.py @@ -8,9 +8,22 @@ from vorta.config import SETTINGS_DIR from vorta.updater import get_updater import vorta.sentry import vorta.log +from vorta.utils import parse_args def main(): + args = parse_args() + + frozen_binary = getattr(sys, 'frozen', False) + + # Don't fork if user specifies it or when running from onedir app bundle on macOS. + if args.foreground or (frozen_binary and sys.platform == 'darwin'): + pass + else: + print('Forking to background (see system tray).') + if os.fork(): + sys.exit() + # Send crashes to Sentry. if not os.environ.get('NO_SENTRY', False): vorta.sentry.init() @@ -21,6 +34,7 @@ def main(): app = VortaApp(sys.argv, single_app=True) app.updater = get_updater() + sys.exit(app.exec_()) diff --git a/src/vorta/application.py b/src/vorta/application.py index 0cb5d420..b19c2ff6 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -4,14 +4,13 @@ import fcntl from PyQt5 import QtCore from PyQt5.QtWidgets import QApplication -from PyQt5.QtGui import QIcon from .tray_menu import TrayMenu from .scheduler import VortaScheduler from .models import BackupProfileModel from .borg.create import BorgCreateThread from .views.main_window import MainWindow -from .utils import get_asset +from .utils import parse_args, set_tray_icon from vorta.config import SETTINGS_DIR @@ -28,7 +27,7 @@ class VortaApp(QApplication): backup_cancelled_event = QtCore.pyqtSignal() backup_log_event = QtCore.pyqtSignal(str) - def __init__(self, args, single_app=False): + def __init__(self, args_raw, single_app=False): # Ensure only one app instance is running. # From https://stackoverflow.com/questions/220525/ @@ -43,14 +42,17 @@ class VortaApp(QApplication): print('An instance of Vorta is already running.') sys.exit(1) - super().__init__(args) + super().__init__(args_raw) self.setQuitOnLastWindowClosed(False) self.scheduler = VortaScheduler(self) # Prepare tray and main window self.tray = TrayMenu(self) self.main_window = MainWindow(self) - # self.main_window.show() + + args = parse_args() + if args.foreground: + self.main_window.show() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) @@ -73,14 +75,11 @@ class VortaApp(QApplication): self.main_window.raise_() def backup_started_event_response(self): - icon = QIcon(get_asset('icons/hdd-o-active.png')) - self.tray.setIcon(icon) + set_tray_icon(self.tray, active=True) def backup_finished_event_response(self): - icon = QIcon(get_asset('icons/hdd-o.png')) - self.tray.setIcon(icon) + set_tray_icon(self.tray) self.main_window.scheduleTab._draw_next_scheduled_backup() def backup_cancelled_event_response(self): - icon = QIcon(get_asset('icons/hdd-o.png')) - self.tray.setIcon(icon) + set_tray_icon(self.tray) diff --git a/src/vorta/assets/UI/mainwindow.ui b/src/vorta/assets/UI/mainwindow.ui index fb3c7564..4df3a95b 100644 --- a/src/vorta/assets/UI/mainwindow.ui +++ b/src/vorta/assets/UI/mainwindow.ui @@ -54,7 +54,7 @@ - 5 + 12 0 @@ -92,7 +92,7 @@ - + :/icons/edit.svg:/icons/edit.svg @@ -109,7 +109,7 @@ ... - + :/icons/trash.svg:/icons/trash.svg @@ -132,7 +132,7 @@ - 3 + 4 false @@ -166,6 +166,11 @@ Archives + + + Misc + + @@ -181,7 +186,7 @@ 0 - 45 + 55 diff --git a/src/vorta/assets/UI/misctab.ui b/src/vorta/assets/UI/misctab.ui new file mode 100644 index 00000000..e83988e1 --- /dev/null +++ b/src/vorta/assets/UI/misctab.ui @@ -0,0 +1,75 @@ + + + Form + + + + 0 + 0 + 791 + 497 + + + + Form + + + + + + 10 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Version: + + + + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + diff --git a/src/vorta/assets/UI/repoadd.ui b/src/vorta/assets/UI/repoadd.ui index 4fcbf439..cd03369b 100644 --- a/src/vorta/assets/UI/repoadd.ui +++ b/src/vorta/assets/UI/repoadd.ui @@ -17,6 +17,19 @@ true + + + + + 75 + true + + + + Initialize New Backup Repository + + + @@ -139,24 +152,6 @@ - - - - - - Add - - - - - - - Cancel - - - - - @@ -193,21 +188,26 @@ + + + + + + Add + + + + + + + Cancel + + + + + - - - - - 75 - true - - - - Initialize New Backup Repository - - - diff --git a/src/vorta/assets/UI/repotab.ui b/src/vorta/assets/UI/repotab.ui index 2cb6d50c..631c9656 100644 --- a/src/vorta/assets/UI/repotab.ui +++ b/src/vorta/assets/UI/repotab.ui @@ -24,39 +24,108 @@ - - - - 0 - 0 - - - - Configure your backup repository (you can add a new or existing repository). For remote repositories, you will need a SSH key to log in without a password (if you already have a key, just keep it at the default). - - - true - - - - - - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - 10 - + - 5 + 0 - - 25 - - + + + + + + + + + + + + 0 + 0 + + + + + 12 + + + + margin-bottom: 10 + + + <html><head/><body><p>Remote or local backup repository. For secure remote backups, try <a href="https://www.borgbase.com/?utm_source=vorta&utm_medium=app"><span style=" text-decoration: underline; color:#0000ff;">BorgBase</span></a>. 100GB free during Beta.</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + + + + + 0 + 0 + + + + + 12 + + + + margin-bottom: 10 + + + To securely access remote repositories. Keep default to use all your existing keys. Or create new key. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Compression: + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + Repository: @@ -64,10 +133,46 @@ + + + 0 + + + + + + + + Copy public SSH key to clipboard. + + + + + + Copy + + + + :/icons/copy.svg:/icons/copy.svg + + + false + + + Qt::NoArrow + + + + + + 0 + + 0 + @@ -102,97 +207,28 @@ - - - - - 12 - - - - margin-bottom: 10 - - - <html><head/><body><p>Remote or local backup repository. For secure remote backups, try <a href="https://www.borgbase.com/?utm_source=vorta&utm_medium=app"><span style=" text-decoration: underline; color:#0000ff;">BorgBase</span></a>. 100GB free during Beta.</p></body></html> - - - true - - - true - - - - - - - Compression: - - - - - - - + SSH Key: - - - - 0 - - - - - - - - Copy public SSH key to clipboard. - - - - - - Copy - - - - :/icons/copy.svg:/icons/copy.svg - - - false - - - Qt::NoArrow - - - - - - - - - - 12 - - - - margin-bottom: 10 - - - To securely access remote repositories. Keep default to use all your existing keys. Or create new key. - - - true - - - + + + + Qt::Vertical + + + + 20 + 40 + + + + diff --git a/src/vorta/assets/UI/scheduletab.ui b/src/vorta/assets/UI/scheduletab.ui index 2eb1c905..9bf53f1a 100644 --- a/src/vorta/assets/UI/scheduletab.ui +++ b/src/vorta/assets/UI/scheduletab.ui @@ -29,7 +29,7 @@ font-weight: bold; } - 2 + 1 @@ -353,7 +353,7 @@ font-weight: bold; - + Allowed Networks: diff --git a/src/vorta/assets/icons/hdd-o-active.png b/src/vorta/assets/icons/hdd-o-active-dark.png similarity index 100% rename from src/vorta/assets/icons/hdd-o-active.png rename to src/vorta/assets/icons/hdd-o-active-dark.png diff --git a/src/vorta/assets/icons/hdd-o-active-light.png b/src/vorta/assets/icons/hdd-o-active-light.png new file mode 100644 index 00000000..bc6fb3bf Binary files /dev/null and b/src/vorta/assets/icons/hdd-o-active-light.png differ diff --git a/src/vorta/assets/icons/hdd-o.png b/src/vorta/assets/icons/hdd-o-dark.png similarity index 100% rename from src/vorta/assets/icons/hdd-o.png rename to src/vorta/assets/icons/hdd-o-dark.png diff --git a/src/vorta/assets/icons/hdd-o-light.png b/src/vorta/assets/icons/hdd-o-light.png new file mode 100644 index 00000000..0a80c621 Binary files /dev/null and b/src/vorta/assets/icons/hdd-o-light.png differ diff --git a/src/vorta/borg/create.py b/src/vorta/borg/create.py index e6a22402..17f8f33e 100644 --- a/src/vorta/borg/create.py +++ b/src/vorta/borg/create.py @@ -103,7 +103,7 @@ class BorgCreateThread(BorgThread): cmd.extend(['--exclude-if-present', f.strip()]) # Add repo url and source dirs. - cmd.append(f"{profile.repo.url}::{platform.node()}-{profile.id}-{dt.now().isoformat(timespec='seconds')}") + cmd.append(f"{profile.repo.url}::{platform.node()}-{profile.slug()}-{dt.now().isoformat(timespec='seconds')}") for f in SourceDirModel.select().where(SourceDirModel.profile == profile.id): cmd.append(f.dir) diff --git a/src/vorta/borg/prune.py b/src/vorta/borg/prune.py index 235b76a9..b79a56db 100644 --- a/src/vorta/borg/prune.py +++ b/src/vorta/borg/prune.py @@ -32,7 +32,7 @@ class BorgPruneThread(BorgThread): '--keep-weekly', str(profile.prune_week), '--keep-monthly', str(profile.prune_month), '--keep-yearly', str(profile.prune_year), - '--prefix', f'{platform.node()}-' + '--prefix', f'{platform.node()}-{profile.slug()}' ] if profile.prune_keep_within: pruning_opts += ['--keep-within', profile.prune_keep_within] diff --git a/src/vorta/models.py b/src/vorta/models.py index 7914fd35..a261c608 100644 --- a/src/vorta/models.py +++ b/src/vorta/models.py @@ -4,10 +4,12 @@ This module provides the app's data store using Peewee with SQLite. At the bottom there is a simple schema migration system. """ -import peewee as pw +import sys import json +import peewee as pw from datetime import datetime, timedelta from playhouse.migrate import SqliteMigrator, migrate +from vorta.utils import slugify SCHEMA_VERSION = 8 @@ -82,6 +84,9 @@ class BackupProfileModel(pw.Model): def refresh(self): return type(self).get(self._pk_expr()) + def slug(self): + return slugify(self.name) + class Meta: database = db @@ -148,16 +153,21 @@ class SchemaVersion(pw.Model): database = db +class SettingsModel(pw.Model): + """App settings unrelated to a single profile or repo""" + key = pw.CharField(unique=True) + value = pw.BooleanField() + label = pw.CharField() + type = pw.CharField() + + class Meta: + database = db + + class BackupProfileMixin: """Extend to support multiple profiles later.""" def profile(self): return BackupProfileModel.get(id=self.window().current_profile.id) - # app = QApplication.instance() - # main_window = hasattr(app, 'main_window') - # if main_window: - # return app.main_window.current_profile - # else: - # return BackupProfileModel.select().first() def _apply_schema_update(current_schema, version_after, *operations): @@ -171,13 +181,33 @@ def _apply_schema_update(current_schema, version_after, *operations): def init_db(con): db.initialize(con) db.connect() - db.create_tables([RepoModel, RepoPassword, BackupProfileModel, SourceDirModel, + db.create_tables([RepoModel, RepoPassword, BackupProfileModel, SourceDirModel, SettingsModel, ArchiveModel, WifiSettingModel, EventLogModel, SchemaVersion]) if BackupProfileModel.select().count() == 0: default_profile = BackupProfileModel(name='Default Profile') default_profile.save() + # Default settings + settings = [ + {'key': 'use_light_icon', 'value': False, 'type': 'checkbox', + 'label': 'Use light system tray icon (applies after restart, useful for dark themes).'} + ] + if sys.platform == 'darwin': + settings += [ + {'key': 'autostart', 'value': False, 'type': 'checkbox', + 'label': 'Add Vorta to Login Items in Preferences > Users and Groups > Login Items.'}, + {'key': 'enable_notifications', 'value': True, 'type': 'checkbox', + 'label': 'Display notifications when background tasks fail.'}, + {'key': 'check_for_updates', 'value': True, 'type': 'checkbox', + 'label': 'Check for updates on startup.'}, + ] + + for setting in settings: # Create missing settings and update labels. + s, created = SettingsModel.get_or_create(key=setting['key'], defaults=setting) + s.label = setting['label'] + s.save() + # Delete old log entries after 3 months. three_months_ago = datetime.now() - timedelta(days=180) EventLogModel.delete().where(EventLogModel.start_time < three_months_ago) diff --git a/src/vorta/tray_menu.py b/src/vorta/tray_menu.py index 5dcba6dd..7a41257d 100644 --- a/src/vorta/tray_menu.py +++ b/src/vorta/tray_menu.py @@ -1,16 +1,15 @@ from PyQt5.QtWidgets import QMenu, QSystemTrayIcon -from PyQt5.QtGui import QIcon -from .utils import get_asset from .borg.borg_thread import BorgThread from .models import BackupProfileModel +from .utils import set_tray_icon class TrayMenu(QSystemTrayIcon): def __init__(self, parent=None): - icon = QIcon(get_asset('icons/hdd-o.png')) - QSystemTrayIcon.__init__(self, icon, parent) + QSystemTrayIcon.__init__(self, parent) self.app = parent + set_tray_icon(self) menu = QMenu() # Workaround to get `activated` signal on Unity: https://stackoverflow.com/a/43683895/3983708 diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 5c12c4dd..99c41e97 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -1,6 +1,9 @@ import os import sys import plistlib +import argparse +import unicodedata +import re from collections import defaultdict from functools import reduce @@ -11,10 +14,10 @@ from paramiko.ecdsakey import ECDSAKey from paramiko.ed25519key import Ed25519Key from paramiko import SSHException from PyQt5.QtWidgets import QFileDialog +from PyQt5.QtGui import QIcon from PyQt5 import QtCore import subprocess import keyring -from .models import WifiSettingModel class VortaKeyring(keyring.backend.KeyringBackend): @@ -140,6 +143,8 @@ def get_asset(path): def get_sorted_wifis(profile): """Get SSIDs from OS and merge with settings in DB.""" + from vorta.models import WifiSettingModel + if sys.platform == 'darwin': plist_path = '/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist' plist_file = open(plist_path, 'rb') @@ -185,3 +190,32 @@ def get_current_wifi(): split_line = line.strip().split(':') if split_line[0] == 'SSID': return split_line[1].strip() + + +def parse_args(): + parser = argparse.ArgumentParser(description='Vorta Backup GUI for Borg.') + parser.add_argument('--foreground', '-f', + action='store_true', + help="Don't fork into background and open main window on startup.") + return parser.parse_args() + + +def slugify(value): + """ + Converts to lowercase, removes non-word characters (alphanumerics and + underscores) and converts spaces to hyphens. Also strips leading and + trailing whitespace. + + Copied from Django. + """ + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') + value = re.sub(r'[^\w\s-]', '', value).strip().lower() + return re.sub(r'[-\s]+', '-', value) + + +def set_tray_icon(tray, active=False): + from vorta.models import SettingsModel + use_light_style = SettingsModel.get(key='use_light_icon').value + icon_name = f"icons/hdd-o{'-active' if active else ''}-{'light' if use_light_style else 'dark'}.png" + icon = QIcon(get_asset(icon_name)) + tray.setIcon(icon) diff --git a/src/vorta/views/main_window.py b/src/vorta/views/main_window.py index 5f8c992e..f7afc0bc 100644 --- a/src/vorta/views/main_window.py +++ b/src/vorta/views/main_window.py @@ -1,3 +1,4 @@ +import sys from PyQt5.QtWidgets import QShortcut from PyQt5 import uic, QtCore from PyQt5.QtGui import QKeySequence @@ -6,6 +7,7 @@ from .repo_tab import RepoTab from .source_tab import SourceTab from .archive_tab import ArchiveTab from .schedule_tab import ScheduleTab +from .misc_tab import MiscTab from .profile_add_edit_dialog import AddProfileWindow, EditProfileWindow from ..utils import get_asset from ..models import BackupProfileModel @@ -30,6 +32,7 @@ class MainWindow(MainWindowBase, MainWindowUI): self.sourceTab = SourceTab(self.sourceTabSlot) self.archiveTab = ArchiveTab(self.archiveTabSlot) self.scheduleTab = ScheduleTab(self.scheduleTabSlot) + self.miscTabSlot = MiscTab(self.miscTabSlot) self.tabWidget.setCurrentIndex(0) self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile) @@ -56,6 +59,14 @@ class MainWindow(MainWindowBase, MainWindowUI): self.profileRenameButton.clicked.connect(self.profile_rename_action) self.profileDeleteButton.clicked.connect(self.profile_delete_action) + # OS-specific startup options: + if sys.platform != 'darwin': + # Hide Wifi-rule section in schedule tab. + self.scheduleTab.wifiListLabel.hide() + self.scheduleTab.wifiListWidget.hide() + self.scheduleTab.page_2.hide() + self.scheduleTab.toolBox.removeItem(1) + # Connect to existing thread. if BorgThread.is_running(): self.createStartBtn.setEnabled(False) diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py new file mode 100644 index 00000000..39b28792 --- /dev/null +++ b/src/vorta/views/misc_tab.py @@ -0,0 +1,28 @@ +from PyQt5 import uic +from PyQt5.QtWidgets import QCheckBox +from vorta.utils import get_asset +from vorta.models import SettingsModel +from vorta._version import __version__ + +uifile = get_asset('UI/misctab.ui') +MiscTabUI, MiscTabBase = uic.loadUiType(uifile, from_imports=True, import_from='vorta.views') + + +class MiscTab(MiscTabBase, MiscTabUI): + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(parent) + self.versionLabel.setText(__version__) + + for setting in SettingsModel.select().where(SettingsModel.type == 'checkbox'): + b = QCheckBox(setting.label) + b.setCheckState(setting.value) + b.setTristate(False) + b.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v)) + self.checkboxLayout.addWidget(b) + + def save_setting(self, key, new_value): + setting = SettingsModel.get(key=key) + setting.value = bool(new_value) + setting.save() diff --git a/src/vorta/views/profile_add_edit_dialog.py b/src/vorta/views/profile_add_edit_dialog.py index b50cdf8d..838a6d63 100644 --- a/src/vorta/views/profile_add_edit_dialog.py +++ b/src/vorta/views/profile_add_edit_dialog.py @@ -34,7 +34,7 @@ class AddProfileWindow(AddProfileBase, AddProfileUI): def validate(self): name = self.profileNameField.text() - # Name as entered? + # A name was entered? if len(name) == 0: self._set_status('Please enter a profile name.') return False diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py index 74ab77b4..104aa38e 100644 --- a/src/vorta/views/source_tab.py +++ b/src/vorta/views/source_tab.py @@ -45,7 +45,6 @@ class SourceTab(SourceBase, SourceUI, BackupProfileMixin): item = "directory" if want_folder else "file" dialog = choose_folder_dialog(self, "Choose %s to back up" % item, want_folder=want_folder) - self._file_dialog = dialog # for pytest dialog.open(receive) def source_remove(self): diff --git a/tests/conftest.py b/tests/conftest.py index 99f94b39..ac28e80a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,10 +28,11 @@ def app(tmpdir, qtbot): test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() - source_dir = SourceDirModel(dir='/tmp', repo=new_repo) + source_dir = SourceDirModel(dir='/tmp/another', repo=new_repo) source_dir.save() app = VortaApp([]) + app.main_window.show() qtbot.addWidget(app.main_window) return app @@ -39,7 +40,7 @@ def app(tmpdir, qtbot): @pytest.fixture def choose_folder_dialog(*args): class MockFileDialog: - def __init__(self, *args): + def __init__(self, *args, **kwargs): pass def open(self, func): diff --git a/tests/test_archives.py b/tests/test_archives.py index 8ee22d87..31993e29 100644 --- a/tests/test_archives.py +++ b/tests/test_archives.py @@ -98,7 +98,7 @@ def test_archive_mount(app, qtbot, mocker, borg_json_output, monkeypatch, choose ) qtbot.mouseClick(tab.mountButton, QtCore.Qt.LeftButton) - qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Mounted'), timeout=1000) + qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Mounted'), timeout=5000) qtbot.mouseClick(tab.mountButton, QtCore.Qt.LeftButton) # qtbot.waitUntil(lambda: tab.mountErrors.text() == 'No active Borg mounts found.') @@ -127,7 +127,7 @@ def test_archive_extract(app, qtbot, mocker, borg_json_output, monkeypatch): mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result) qtbot.mouseClick(tab.extractButton, QtCore.Qt.LeftButton) - qtbot.waitUntil(lambda: hasattr(tab, '_window')) + qtbot.waitUntil(lambda: hasattr(tab, '_window'), timeout=5000) assert tab._window.treeView.model().rootItem.childItems[0].data(0) == 'Users' tab._window.treeView.model().rootItem.childItems[0].load_children() diff --git a/tests/test_repo.py b/tests/test_repo.py index 944f6deb..a40fc494 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -1,6 +1,7 @@ import os +import uuid from PyQt5 import QtCore -from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtWidgets import QMessageBox import vorta.borg.borg_thread import vorta.models @@ -9,10 +10,12 @@ from vorta.views.ssh_dialog import SSHAddWindow from vorta.models import EventLogModel, RepoModel, ArchiveModel -def test_repo_add(app, qtbot, mocker, borg_json_output): +def test_repo_add_failures(app, qtbot, mocker, borg_json_output): # Add new repo window main = app.main_window - add_repo_window = AddRepoWindow(main.repoTab) + add_repo_window = AddRepoWindow(main) + qtbot.addWidget(add_repo_window) + qtbot.keyClicks(add_repo_window.repoURL, 'aaa') qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.LeftButton) assert add_repo_window.errorText.text().startswith('Please enter a valid') @@ -21,6 +24,15 @@ def test_repo_add(app, qtbot, mocker, borg_json_output): qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.LeftButton) assert add_repo_window.errorText.text() == 'Please use a longer password.' + +def test_repo_add_success(app, qtbot, mocker, borg_json_output): + # Add new repo window + main = app.main_window + add_repo_window = AddRepoWindow(main) + qtbot.addWidget(add_repo_window) + test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain + + qtbot.keyClicks(add_repo_window.repoURL, test_repo_url) qtbot.keyClicks(add_repo_window.passwordLineEdit, 'long-password-long') stdout, stderr = borg_json_output('info') @@ -34,8 +46,9 @@ def test_repo_add(app, qtbot, mocker, borg_json_output): main.repoTab.process_new_repo(blocker.args[0]) - # assert EventLogModel.select().count() == 2 - assert RepoModel.get(id=2).url == 'aaabbb.com:repo' + qtbot.waitUntil(lambda: EventLogModel.select().count() == 2) + assert EventLogModel.select().count() == 2 + assert RepoModel.get(id=2).url == test_repo_url def test_repo_unlink(app, qtbot, monkeypatch): @@ -70,9 +83,6 @@ def test_ssh_dialog(qtbot, tmpdir): assert pub_tmpfile_content.startswith('ssh-ed25519') qtbot.waitUntil(lambda: ssh_dialog.errors.text().startswith('New key was copied')) - clipboard = QApplication.clipboard() - assert clipboard.text().startswith('ssh-ed25519') - qtbot.mouseClick(ssh_dialog.generateButton, QtCore.Qt.LeftButton) qtbot.waitUntil(lambda: ssh_dialog.errors.text().startswith('Key file already')) diff --git a/tests/test_source.py b/tests/test_source.py index ef39234b..6820525d 100644 --- a/tests/test_source.py +++ b/tests/test_source.py @@ -1,18 +1,18 @@ import logging from PyQt5 import QtCore import vorta.models +import vorta.views -def test_add_folder(app, qtbot, tmpdir): +def test_add_folder(app, qtbot, tmpdir, monkeypatch, choose_folder_dialog): + monkeypatch.setattr( + vorta.views.source_tab, "choose_folder_dialog", choose_folder_dialog + ) main = app.main_window main.tabWidget.setCurrentIndex(1) tab = main.sourceTab qtbot.mouseClick(tab.sourceAddFolder, QtCore.Qt.LeftButton) - - qtbot.waitUntil(lambda: len(tab._file_dialog.selectedFiles()) > 0, timeout=3000) - tab._file_dialog.accept() - qtbot.waitUntil(lambda: tab.sourceDirectoriesWidget.count() == 2) for src in vorta.models.SourceDirModel.select():