From 3b6c5f2d3c68a5dca0af1ac1aae20f26189baf51 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 23 Mar 2020 07:20:09 +0100 Subject: [PATCH] Rewrite of borg diff core (#373) - It now heavily relies on regex. This was done in order to make the code more stable and easier to maintain. - Share common code for file tree view between extract and diff. --- src/vorta/views/archive_tab.py | 11 +- src/vorta/views/diff_dialog.py | 5 +- src/vorta/views/diff_result.py | 267 ++++------------- src/vorta/views/extract_dialog.py | 238 ++------------- src/vorta/views/tree_view.py | 273 ++++++++++++++++++ .../diff_archives_stderr.json | 0 .../diff_archives_stdout.json | 2 + tests/conftest.py | 3 + tests/test_archives.py | 38 ++- tests/test_repo.py | 4 +- 10 files changed, 408 insertions(+), 433 deletions(-) create mode 100644 src/vorta/views/tree_view.py create mode 100644 tests/borg_json_output/diff_archives_stderr.json create mode 100644 tests/borg_json_output/diff_archives_stdout.json diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index c6c53b4e..ca0ea40c 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -76,6 +76,8 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): self.populate_from_profile() + self.selected_archives = None + def _set_status(self, text): self.mountErrors.setText(text) self.mountErrors.repaint() @@ -429,9 +431,10 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): window.show() if window.exec_(): - selected_archives = window.selected_archives - archive_cell_newer = self.archiveTable.item(selected_archives[0], 4) - archive_cell_older = self.archiveTable.item(selected_archives[1], 4) + if window.selected_archives: + self.selected_archives = window.selected_archives + archive_cell_newer = self.archiveTable.item(self.selected_archives[0], 4) + archive_cell_older = self.archiveTable.item(self.selected_archives[1], 4) if archive_cell_older and archive_cell_newer: archive_name_newer = archive_cell_newer.text() archive_name_older = archive_cell_older.text() @@ -457,5 +460,5 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): window = DiffResult(result['data'], archive_newer, archive_older) self._toggle_all_buttons(True) window.setParent(self, QtCore.Qt.Sheet) - self._window = window # for testing + self._resultwindow = window # for testing window.show() diff --git a/src/vorta/views/diff_dialog.py b/src/vorta/views/diff_dialog.py index cf657c0c..0770bc8e 100644 --- a/src/vorta/views/diff_dialog.py +++ b/src/vorta/views/diff_dialog.py @@ -3,12 +3,11 @@ from PyQt5.QtWidgets import QHeaderView, QTableView, QTableWidgetItem from vorta.utils import get_asset -uifile = get_asset('UI/diffdialog.ui') +uifile = get_asset("UI/diffdialog.ui") DiffDialogUI, DiffDialogBase = uic.loadUiType(uifile) class DiffDialog(DiffDialogBase, DiffDialogUI): - def __init__(self, archiveTable): super().__init__() self.setupUi(self) @@ -38,7 +37,7 @@ class DiffDialog(DiffDialogBase, DiffDialogUI): text = archiveTable.item(row, column).text() self.archiveTable.setItem(row, column, QTableWidgetItem(text)) except AttributeError: - self.archiveTable.setItem(row, column, QTableWidgetItem('')) + self.archiveTable.setItem(row, column, QTableWidgetItem("")) self.diffButton.setEnabled(False) diff --git a/src/vorta/views/diff_result.py b/src/vorta/views/diff_result.py index 1da7cbc9..2e5033c0 100644 --- a/src/vorta/views/diff_result.py +++ b/src/vorta/views/diff_result.py @@ -1,34 +1,28 @@ import os +import re from PyQt5 import uic -from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, QVariant +from PyQt5.QtCore import Qt, QVariant from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QHeaderView -from vorta.utils import (get_asset, get_dict_from_list, nested_dict, - pretty_bytes) +from vorta.utils import (get_asset, get_dict_from_list, nested_dict) + +from vorta.views.tree_view import TreeModel uifile = get_asset('UI/diffresult.ui') DiffResultUI, DiffResultBase = uic.loadUiType(uifile) -files_with_attributes = None -nested_file_list = None -selected_files_folders = None - class DiffResult(DiffResultBase, DiffResultUI): def __init__(self, fs_data, archive_newer, archive_older): super().__init__() self.setupUi(self) - global files_with_attributes, nested_file_list, selected_files_folders - # Clear global file lists files_with_attributes = [] nested_file_list = nested_dict() - selected_files_folders = set() def parse_line(line): - if line: line_split = line.split() else: @@ -36,59 +30,48 @@ class DiffResult(DiffResultBase, DiffResultUI): if line_split[0] == 'added' or line_split[0] == 'removed': change_type = line_split[0] - size = line_split[1] - unit = line_split[2] + if line_split[1] in ['directory', 'link']: + size = 0 + full_path = re.search(r'^\w+ \w+ +(.*)', line).group(1) + else: + significand = line_split[1] + unit = line_split[2] + size = calc_size(significand, unit) + full_path = re.search(r'^\w+ +\S+ \w?B (.*)', line).group(1) else: - change_type = "modified" - size = line_split[0] - unit = line_split[1] - # If present remove '+' or '-' sign at the front - if size[0] in ('+', '-'): - size = size[1:] + size_change = re.search(r' *[\+-]?(\d+\.*\d*) (\w?B) +[\+-]?.+\w?B ', line) + if size_change: + significand = size_change.group(1) + unit = size_change.group(2) + size = calc_size(significand, unit) + full_path_index = size_change.end(0) + else: + size = 0 - if line_split[0].startswith("["): - size = 0 - change_type = line[:line.find(line_split[3])] - full_path = line[line.find(line_split[3]):] - dir, name = os.path.split(full_path) - # add to nested dict of folders to find nested dirs. - d = get_dict_from_list(nested_file_list, full_path.split('/')) - elif line_split[1] not in ['directory', 'link']: - if unit == 'B': - size = int(size) - elif unit == 'kB': - size = int(float(size) * 10**3) - elif unit == 'MB': - size = int(float(size) * 10**6) - elif unit == 'GB': - size = int(float(size) * 10**9) - elif unit == 'TB': - size = int(float(size) * 10**12) + permission_change = re.search(r' *(\[.{24}\]) ', line) + if permission_change: + change_type = permission_change.group(1) + full_path_index = permission_change.end(0) + else: + change_type = "modified" - if change_type == 'added' or change_type == 'removed': - full_path = line[line.find(line_split[3]):] - elif change_type == "modified": - full_path = line[line.find(line_split[4]):] + if size_change and permission_change: + full_path_index = max(size_change.end(0), permission_change.end(0)) + full_path = line[full_path_index:] - dir, name = os.path.split(full_path) - # add to nested dict of folders to find nested dirs. - d = get_dict_from_list(nested_file_list, dir.split('/')) - if name not in d: - d[name] = {} - else: - size = 0 - full_path = line[line.find(line_split[2]):] + dir, name = os.path.split(full_path) - dir, name = os.path.split(full_path) - # add to nested dict of folders to find nested dirs. - d = get_dict_from_list(nested_file_list, full_path.split('/')) + # add to nested dict of folders to find nested dirs. + d = get_dict_from_list(nested_file_list, dir.split('/')) + if name not in d: + d[name] = {} return size, change_type, name, dir for l in fs_data.split('\n'): files_with_attributes.append(parse_line(l)) - model = TreeModel() + model = DiffTree(files_with_attributes, nested_file_list) view = self.treeView view.setAlternatingRowColors(True) @@ -103,113 +86,31 @@ class DiffResult(DiffResultBase, DiffResultUI): self.archiveNameLabel_1.setText(f'{archive_newer.name}') self.archiveNameLabel_2.setText(f'{archive_older.name}') self.okButton.clicked.connect(self.accept) - self.selected = selected_files_folders -class FolderItem: - def __init__(self, path, name, modified, parent=None): - self.parentItem = parent - self.path = path - self.itemData = [name, modified] - self.childItems = [] - - # Pre-filter children - self._filtered_children = [] - search_path = os.path.join(self.path, name) - if parent is None: # Find path for root folder - for root_folder in nested_file_list.keys(): - self._filtered_children.append((0, '', root_folder, '', )) - else: - - # This adds direct children. - self._filtered_children = [f for f in files_with_attributes if search_path == f[3]] - - # Add nested folders. - for immediate_child in get_dict_from_list(nested_file_list, search_path.split('/')).keys(): - if not [True for child in self._filtered_children if child[2] == immediate_child]: - self._filtered_children.append((0, '', immediate_child, search_path)) - - self.is_loaded = False - - def load_children(self): - for child_item in self._filtered_children: - if child_item[0] > 0: # This is a file - self.childItems.append(FileItem( - name=child_item[2], - modified=child_item[1], - size=child_item[0], - parent=self)) - else: # Folder - self.childItems.append( - FolderItem( - path=child_item[3], - name=child_item[2], - modified=child_item[1], - parent=self)) - self.is_loaded = True - - def child(self, row): - return self.childItems[row] - - def childCount(self): - return len(self._filtered_children) - - def columnCount(self): - return 3 - - def data(self, column): - if column <= 1: - return self.itemData[column] - else: - return None - - def parent(self): - return self.parentItem - - def row(self): - if self.parentItem: - return self.parentItem.childItems.index(self) - - return 0 +def calc_size(significand, unit): + if unit == 'B': + return int(significand) + elif unit == 'kB': + return int(float(significand) * 10**3) + elif unit == 'MB': + return int(float(significand) * 10**6) + elif unit == 'GB': + return int(float(significand) * 10**9) + elif unit == 'TB': + return int(float(significand) * 10**12) -class FileItem(FolderItem): - def __init__(self, name, modified, size, parent=None): - self.parentItem = parent - self.itemData = [name, modified, size] - - def childCount(self): - return 0 - - def columnCount(self): - return 3 - - def data(self, column): - if column == 1: - return self.itemData[column] - elif column == 2: - return pretty_bytes(self.itemData[column]) - elif column == 0: - return self.itemData[column] - - def parent(self): - return self.parentItem - - def row(self): - return self.parentItem.childItems.index(self) - - -class TreeModel(QAbstractItemModel): - column_names = ['Name', 'Modified', 'Size'] - - def __init__(self, parent=None): - super(TreeModel, self).__init__(parent) - - self.rootItem = FolderItem(path='', name='', modified=None) - self.rootItem.load_children() - - def columnCount(self, parent): - return 3 +class DiffTree(TreeModel): + def __init__( + self, + files_with_attributes, + nested_file_list, + parent=None, + ): + super().__init__( + files_with_attributes, nested_file_list, parent=parent + ) def data(self, index, role): if not index.isValid(): @@ -230,61 +131,7 @@ class TreeModel(QAbstractItemModel): else: return None - def canFetchMore(self, index): - if not index.isValid(): - return False - item = index.internalPointer() - return not item.is_loaded - - def fetchMore(self, index): - item = index.internalPointer() - item.load_children() - def flags(self, index): if not index.isValid(): return Qt.NoItemFlags return Qt.ItemIsEnabled - - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self.column_names[section] - - return None - - def index(self, row, column, parent): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - if not parent.isValid(): - parentItem = self.rootItem - else: - parentItem = parent.internalPointer() - - childItem = parentItem.child(row) - if childItem: - return self.createIndex(row, column, childItem) - else: - return QModelIndex() - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - - childItem = index.internalPointer() - parentItem = childItem.parent() - - if parentItem == self.rootItem: - return QModelIndex() - - return self.createIndex(parentItem.row(), 0, parentItem) - - def rowCount(self, parent): - if parent.column() > 0: - return 0 - - if not parent.isValid(): - parentItem = self.rootItem - else: - parentItem = parent.internalPointer() - - return parentItem.childCount() diff --git a/src/vorta/views/extract_dialog.py b/src/vorta/views/extract_dialog.py index 3a13fe07..a42c7683 100644 --- a/src/vorta/views/extract_dialog.py +++ b/src/vorta/views/extract_dialog.py @@ -4,50 +4,45 @@ import datetime from collections import namedtuple from PyQt5 import uic -from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication, QHeaderView -from vorta.utils import get_asset, pretty_bytes, get_dict_from_list, nested_dict +from vorta.utils import get_asset, get_dict_from_list, nested_dict +from vorta.views.tree_view import TreeModel -uifile = get_asset('UI/extractdialog.ui') +uifile = get_asset("UI/extractdialog.ui") ExtractDialogUI, ExtractDialogBase = uic.loadUiType(uifile) -ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' - -files_with_attributes = None -nested_file_list = None -selected_files_folders = None +ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" class ExtractDialog(ExtractDialogBase, ExtractDialogUI): def __init__(self, fs_data, archive): super().__init__() self.setupUi(self) - global files_with_attributes, nested_file_list, selected_files_folders - # Clear global file lists files_with_attributes = [] nested_file_list = nested_dict() - selected_files_folders = set() + self.selected = set() def parse_line(line): - size, modified, full_path = line.split('\t') + size, modified, full_path = line.split("\t") size = int(size) dir, name = os.path.split(full_path) # add to nested dict of folders to find nested dirs. - d = get_dict_from_list(nested_file_list, dir.split('/')) + d = get_dict_from_list(nested_file_list, dir.split("/")) if name not in d: d[name] = {} return size, modified, name, dir - for l in fs_data.split('\n'): + for l in fs_data.split("\n"): try: files_with_attributes.append(parse_line(l)) except ValueError: pass - model = TreeModel() + model = ExtractTree(files_with_attributes, nested_file_list, self.selected) view = self.treeView view.setAlternatingRowColors(True) @@ -59,141 +54,22 @@ class ExtractDialog(ExtractDialogBase, ExtractDialogUI): header.setSectionResizeMode(2, QHeaderView.ResizeToContents) header.setSectionResizeMode(0, QHeaderView.Stretch) - self.archiveNameLabel.setText(f'{archive.name}, {archive.time}') + self.archiveNameLabel.setText(f"{archive.name}, {archive.time}") self.cancelButton.clicked.connect(self.close) self.extractButton.clicked.connect(self.accept) - self.selected = selected_files_folders -class FolderItem: - def __init__(self, path, name, modified, parent=None): - self.parentItem = parent - self.path = path - self.itemData = [name, modified] - self.childItems = [] - self.checkedState = False - - # Pre-filter children - self._filtered_children = [] - search_path = os.path.join(self.path, name) - if parent is None: # Find path for root folder - for root_folder in nested_file_list.keys(): - self._filtered_children.append((0, '', root_folder, '', )) - else: - self.checkedState = parent.checkedState # If there is a parent, use its checked-status. - - # This adds direct children. - self._filtered_children = [f for f in files_with_attributes if search_path == f[3]] - - # Add nested folders. - for immediate_child in get_dict_from_list(nested_file_list, search_path.split('/')).keys(): - if not [True for child in self._filtered_children if child[2] == immediate_child]: - self._filtered_children.append((0, '', immediate_child, search_path)) - - self.is_loaded = False - - def load_children(self): - for child_item in self._filtered_children: - if child_item[0] > 0: # This is a file - self.childItems.append(FileItem( - name=child_item[2], - modified=child_item[1], - size=child_item[0], - parent=self)) - else: # Folder - self.childItems.append( - FolderItem( - path=child_item[3], - name=child_item[2], - modified=child_item[1], - parent=self)) - - self.is_loaded = True - - def setCheckedState(self, value): - if value == 2: - self.checkedState = True - selected_files_folders.add( - os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0])) - else: - self.checkedState = False - path_to_remove = os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0]) - if path_to_remove in selected_files_folders: - selected_files_folders.remove(path_to_remove) - - if hasattr(self, 'childItems'): - for child in self.childItems: - child.setCheckedState(value) - - def getCheckedState(self): - if self.checkedState: - return Qt.Checked - else: - return Qt.Unchecked - - def child(self, row): - return self.childItems[row] - - def childCount(self): - return len(self._filtered_children) - - def columnCount(self): - return 3 - - def data(self, column): - if column <= 1: - return self.itemData[column] - else: - return None - - def parent(self): - return self.parentItem - - def row(self): - if self.parentItem: - return self.parentItem.childItems.index(self) - - return 0 - - -class FileItem(FolderItem): - def __init__(self, name, modified, size, parent=None): - self.parentItem = parent - self.itemData = [name, modified, size] # dt.strptime(modified, ISO_FORMAT) - self.checkedState = parent.checkedState - - def childCount(self): - return 0 - - def columnCount(self): - return 3 - - def data(self, column): - if column == 1: - return self.itemData[column] # .strftime('%Y-%m-%dT%H:%M') - elif column == 2: - return pretty_bytes(self.itemData[column]) - elif column == 0: - return self.itemData[column] - - def parent(self): - return self.parentItem - - def row(self): - return self.parentItem.childItems.index(self) - - -class TreeModel(QAbstractItemModel): - column_names = ['Name', 'Modified', 'Size'] - - def __init__(self, parent=None): - super(TreeModel, self).__init__(parent) - - self.rootItem = FolderItem(path='', name='', modified=None) - self.rootItem.load_children() - - def columnCount(self, parent): - return 3 +class ExtractTree(TreeModel): + def __init__( + self, + files_with_attributes, + nested_file_list, + selected_files_folders, + parent=None, + ): + super().__init__( + files_with_attributes, nested_file_list, selected_files_folders, parent + ) def data(self, index, role): if not index.isValid(): @@ -208,86 +84,24 @@ class TreeModel(QAbstractItemModel): else: return None - def setData(self, index, value, role=Qt.EditRole): - if role == Qt.CheckStateRole: - item = index.internalPointer() - item.setCheckedState(value) - self.dataChanged.emit(QModelIndex(), QModelIndex(), []) - - return True - - def canFetchMore(self, index): - if not index.isValid(): - return False - item = index.internalPointer() - return not item.is_loaded - - def fetchMore(self, index): - item = index.internalPointer() - item.load_children() - def flags(self, index): if not index.isValid(): return Qt.NoItemFlags return Qt.ItemIsEnabled | Qt.ItemIsUserCheckable - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self.column_names[section] - return None - - def index(self, row, column, parent): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - if not parent.isValid(): - parentItem = self.rootItem - else: - parentItem = parent.internalPointer() - - childItem = parentItem.child(row) - if childItem: - return self.createIndex(row, column, childItem) - else: - return QModelIndex() - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - - childItem = index.internalPointer() - parentItem = childItem.parent() - - if parentItem == self.rootItem: - return QModelIndex() - - return self.createIndex(parentItem.row(), 0, parentItem) - - def rowCount(self, parent): - if parent.column() > 0: - return 0 - - if not parent.isValid(): - parentItem = self.rootItem - else: - parentItem = parent.internalPointer() - - return parentItem.childCount() - - -if __name__ == '__main__': +if __name__ == "__main__": """ For local testing: borg list --progress --info --log-json --format="{size:8d}{TAB}{mtime}{TAB}{path}{NL}" """ - FakeArchive = namedtuple('Archive', ['name', 'time']) + FakeArchive = namedtuple("Archive", ["name", "time"]) app = QApplication(sys.argv) - test_list = open('/Users/manu/Downloads/nyx2-list.txt').read() + test_list = open("/Users/manu/Downloads/nyx2-list.txt").read() - archive = FakeArchive('test-archive', datetime.datetime.now()) + archive = FakeArchive("test-archive", datetime.datetime.now()) view = ExtractDialog(test_list, archive) view.show() sys.exit(app.exec_()) diff --git a/src/vorta/views/tree_view.py b/src/vorta/views/tree_view.py new file mode 100644 index 00000000..01cd05e2 --- /dev/null +++ b/src/vorta/views/tree_view.py @@ -0,0 +1,273 @@ +from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt + +import os +import abc + +from vorta.utils import get_dict_from_list, pretty_bytes + + +class FolderItem: + def __init__( + self, + path, + name, + modified, + files_with_attributes, + nested_file_list, + selected_files_folders=None, + parent=None, + ): + self.parentItem = parent + self.path = path + self.itemData = [name, modified] + self.childItems = [] + self.checkedState = False + self.files_with_attributes = files_with_attributes + self.nested_file_list = nested_file_list + self.selected_files_folders = selected_files_folders + + # Pre-filter children + self._filtered_children = [] + search_path = os.path.join(self.path, name) + if parent is None: # Find path for root folder + for root_folder in nested_file_list.keys(): + self._filtered_children.append((0, "", root_folder, "",)) + else: + self.checkedState = ( + parent.checkedState + ) # If there is a parent, use its checked-status. + + # This adds direct children. + self._filtered_children = [ + f for f in files_with_attributes if search_path == f[3] + ] + + # Add nested folders. + for immediate_child in get_dict_from_list( + nested_file_list, search_path.split("/") + ).keys(): + if not [ + True + for child in self._filtered_children + if child[2] == immediate_child + ]: + self._filtered_children.append( + (0, "", immediate_child, search_path) + ) + + self.is_loaded = False + + def load_children(self): + for child_item in self._filtered_children: + if child_item[0] > 0: # This is a file + self.childItems.append( + FileItem( + name=child_item[2], + modified=child_item[1], + size=child_item[0], + files_with_attributes=self.files_with_attributes, + nested_file_list=self.nested_file_list, + selected_files_folders=self.selected_files_folders, + parent=self, + ) + ) + else: # Folder + self.childItems.append( + FolderItem( + path=child_item[3], + name=child_item[2], + modified=child_item[1], + files_with_attributes=self.files_with_attributes, + nested_file_list=self.nested_file_list, + selected_files_folders=self.selected_files_folders, + parent=self, + ) + ) + self.is_loaded = True + + def setCheckedState(self, value): + if value == 2: + self.checkedState = True + self.selected_files_folders.add( + os.path.join( + self.parentItem.path, self.parentItem.data(0), self.itemData[0] + ) + ) + else: + self.checkedState = False + path_to_remove = os.path.join( + self.parentItem.path, self.parentItem.data(0), self.itemData[0] + ) + if path_to_remove in self.selected_files_folders: + self.selected_files_folders.remove(path_to_remove) + + if hasattr(self, "childItems"): + for child in self.childItems: + child.setCheckedState(value) + + def getCheckedState(self): + if self.checkedState: + return Qt.Checked + else: + return Qt.Unchecked + + def child(self, row): + return self.childItems[row] + + def childCount(self): + return len(self._filtered_children) + + def columnCount(self): + return 3 + + def data(self, column): + if column <= 1: + return self.itemData[column] + else: + return None + + def parent(self): + return self.parentItem + + def row(self): + if self.parentItem: + return self.parentItem.childItems.index(self) + + return 0 + + +class FileItem(FolderItem): + def __init__( + self, + name, + modified, + size, + files_with_attributes, + nested_file_list, + selected_files_folders=None, + parent=None, + ): + self.parentItem = parent + self.itemData = [name, modified, size] + self.checkedState = parent.checkedState + self.files_with_attributes = files_with_attributes + self.nested_file_list = nested_file_list + self.selected_files_folders = selected_files_folders + + def childCount(self): + return 0 + + def columnCount(self): + return 3 + + def data(self, column): + if column == 1: + return self.itemData[column] + elif column == 2: + return pretty_bytes(self.itemData[column]) + elif column == 0: + return self.itemData[column] + + def parent(self): + return self.parentItem + + def row(self): + return self.parentItem.childItems.index(self) + + +class TreeModel(QAbstractItemModel): + __metaclass__ = abc.ABCMeta + + column_names = ["Name", "Modified", "Size"] + + def __init__( + self, + files_with_attributes, + nested_file_list, + selected_files_folders=None, + parent=None, + ): + super(TreeModel, self).__init__(parent) + + self.rootItem = FolderItem( + path="", + name="", + files_with_attributes=files_with_attributes, + nested_file_list=nested_file_list, + selected_files_folders=selected_files_folders, + modified=None, + ) + self.rootItem.load_children() + + def columnCount(self, parent): + return 3 + + @abc.abstractmethod + def data(self, index, role): + return + + def setData(self, index, value, role=Qt.EditRole): + if role == Qt.CheckStateRole: + item = index.internalPointer() + item.setCheckedState(value) + self.dataChanged.emit(QModelIndex(), QModelIndex(), []) + + return True + + def canFetchMore(self, index): + if not index.isValid(): + return False + item = index.internalPointer() + return not item.is_loaded + + def fetchMore(self, index): + item = index.internalPointer() + item.load_children() + + @abc.abstractmethod + def flags(self, index): + return + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.column_names[section] + + return None + + def index(self, row, column, parent): + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if not parent.isValid(): + parentItem = self.rootItem + else: + parentItem = parent.internalPointer() + + childItem = parentItem.child(row) + if childItem: + return self.createIndex(row, column, childItem) + else: + return QModelIndex() + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + + childItem = index.internalPointer() + parentItem = childItem.parent() + + if parentItem == self.rootItem: + return QModelIndex() + + return self.createIndex(parentItem.row(), 0, parentItem) + + def rowCount(self, parent): + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parentItem = self.rootItem + else: + parentItem = parent.internalPointer() + + return parentItem.childCount() diff --git a/tests/borg_json_output/diff_archives_stderr.json b/tests/borg_json_output/diff_archives_stderr.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/borg_json_output/diff_archives_stdout.json b/tests/borg_json_output/diff_archives_stdout.json new file mode 100644 index 00000000..28f49522 --- /dev/null +++ b/tests/borg_json_output/diff_archives_stdout.json @@ -0,0 +1,2 @@ + +7 B 0 B [-rw-rw-r-- -> -rw-rw-rw-] test/hallo +added 0 B test/tschüss diff --git a/tests/conftest.py b/tests/conftest.py index fcc95da6..8022ed9e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,9 @@ def init_db(qapp): test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() + test_archive1 = ArchiveModel(snapshot_id='99998', name='test-archive1', time=dt(2000, 1, 1, 0, 0), repo=1) + test_archive1.save() + source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo) source_dir.save() diff --git a/tests/test_archives.py b/tests/test_archives.py index a9738053..9cbc5cfc 100644 --- a/tests/test_archives.py +++ b/tests/test_archives.py @@ -115,7 +115,7 @@ def test_archive_extract(qapp, qtbot, mocker, borg_json_output, monkeypatch): main.tabWidget.setCurrentIndex(3) tab.populate_from_profile() - qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 1) + qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2) monkeypatch.setattr( vorta.views.extract_dialog.ExtractDialog, "exec_", lambda *args: True @@ -131,6 +131,40 @@ def test_archive_extract(qapp, qtbot, mocker, borg_json_output, monkeypatch): assert tab._window.treeView.model().rootItem.childItems[0].data(0) == 'Users' tab._window.treeView.model().rootItem.childItems[0].load_children() - assert tab._window.archiveNameLabel.text().startswith('test-archive, 2000') tab._window.accept() + + +def test_archive_diff(qapp, qtbot, mocker, borg_json_output, monkeypatch): + main = qapp.main_window + tab = main.archiveTab + main.tabWidget.setCurrentIndex(3) + + tab.populate_from_profile() + qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2) + + monkeypatch.setattr( + vorta.views.diff_dialog.DiffDialog, "exec_", lambda *args: True + ) + + monkeypatch.setattr( + tab, "selected_archives", (0, 1) + ) + + stdout, stderr = borg_json_output('diff_archives') + popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) + mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result) + + qtbot.mouseClick(tab.diffButton, QtCore.Qt.LeftButton) + qtbot.waitUntil(lambda: hasattr(tab, '_window'), timeout=5000) + + monkeypatch.setattr( + vorta.views.diff_result.DiffResult, "exec_", lambda *args: True + ) + qtbot.waitUntil(lambda: hasattr(tab, '_resultwindow'), timeout=5000) + + assert tab._resultwindow.treeView.model().rootItem.childItems[0].data(0) == 'test' + tab._resultwindow.treeView.model().rootItem.childItems[0].load_children() + + assert tab._resultwindow.archiveNameLabel_1.text() == 'test-archive' + tab._resultwindow.accept() diff --git a/tests/test_repo.py b/tests/test_repo.py index 92a5f7eb..95f17158 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -101,8 +101,8 @@ def test_create(qapp, borg_json_output, mocker, qtbot): qtbot.waitUntil(lambda: main.createProgressText.text().startswith('Backup finished.'), timeout=3000) qtbot.waitUntil(lambda: main.createStartBtn.isEnabled(), timeout=3000) assert EventLogModel.select().count() == 1 - assert ArchiveModel.select().count() == 2 + assert ArchiveModel.select().count() == 3 assert RepoModel.get(id=1).unique_size == 15520474 assert main.createStartBtn.isEnabled() - assert main.archiveTab.archiveTable.rowCount() == 2 + assert main.archiveTab.archiveTable.rowCount() == 3 assert main.scheduleTab.logTableWidget.rowCount() == 1