Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting for number format in archive tab #1719

Merged
merged 17 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/vorta/store/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ def get_misc_settings() -> List[Dict[str, str]]:
'label': trans_late('settings', 'Get statistics of file/folder when added'),
'tooltip': trans_late('settings', 'When adding a new source, calculate its size and the number of files.'),
},
{
'key': 'enable_fixed_units',
'value': False,
'type': 'checkbox',
'group': information,
'label': trans_late('settings', 'Use the same unit of measurement for archive sizes'),
'tooltip': trans_late(
'settings',
'When enabled, all archive sizes will use the same unit of measurement, '
'such as KB or MB. This can make archive sizes easier to compare.',
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the word "compare" instead of "read", since that might to be closer to the original intention of the setting.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that!

),
},
{
'key': 'use_system_keyring',
'value': True,
Expand Down
12 changes: 3 additions & 9 deletions src/vorta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
# Used to store whether a user wanted to override the
# default directory for the --development flag
DEFAULT_DIR_FLAG = object()
METRIC_UNITS = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
NONMETRIC_UNITS = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']

borg_compat = BorgCompatibility()
_network_status_monitor = None
Expand Down Expand Up @@ -140,14 +142,10 @@ def get_network_status_monitor():

def get_path_datasize(path, exclude_patterns):
file_info = QFileInfo(path)
data_size = 0

if file_info.isDir():
data_size, files_count = get_directory_size(file_info.absoluteFilePath(), exclude_patterns)
# logger.info("path (folder) %s %u elements size now=%u (%s)",
# file_info.absoluteFilePath(), files_count, data_size, pretty_bytes(data_size))
else:
# logger.info("path (file) %s size=%u", file_info.path(), file_info.size())
data_size = file_info.size()
files_count = 1

Expand Down Expand Up @@ -279,11 +277,7 @@ def pretty_bytes(
if not isinstance(size, int):
return ''
prefix = '+' if sign and size > 0 else ''
power, units = (
(10**3, ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'])
if metric
else (2**10, ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'])
)
power, units = (10**3, METRIC_UNITS) if metric else (2**10, NONMETRIC_UNITS)
if fixed_unit is None:
n = find_best_unit_for_size(size, metric=metric, precision=precision)
else:
Expand Down
11 changes: 7 additions & 4 deletions src/vorta/views/archive_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from vorta.borg.rename import BorgRenameJob
from vorta.borg.umount import BorgUmountJob
from vorta.i18n import translate
from vorta.store.models import ArchiveModel, BackupProfileMixin
from vorta.store.models import ArchiveModel, BackupProfileMixin, SettingsModel
from vorta.utils import (
borg_compat,
choose_file_dialog,
Expand Down Expand Up @@ -291,9 +291,12 @@ def populate_from_profile(self):

formatted_time = archive.time.strftime('%Y-%m-%d %H:%M')
self.archiveTable.setItem(row, 0, QTableWidgetItem(formatted_time))
self.archiveTable.setItem(
row, 1, SizeItem(pretty_bytes(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS))
)

# format units based on user settings for 'dynamic' or 'fixed' units
fixed_unit = best_unit if SettingsModel.get(key='enable_fixed_units').value else None
size = pretty_bytes(archive.size, fixed_unit=fixed_unit, precision=SIZE_DECIMAL_DIGITS)
self.archiveTable.setItem(row, 1, SizeItem(size))

if archive.duration is not None:
formatted_duration = str(timedelta(seconds=round(archive.duration)))
else:
Expand Down
1 change: 1 addition & 0 deletions src/vorta/views/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(self, parent=None):
self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile)
self.repoTab.repo_changed.connect(self.scheduleTab.populate_from_profile)
self.repoTab.repo_added.connect(self.archiveTab.refresh_archive_list)
self.miscTab.refresh_archive.connect(self.archiveTab.populate_from_profile)

self.createStartBtn.clicked.connect(self.app.create_backup_action)
self.cancelButton.clicked.connect(self.app.backup_cancelled_event.emit)
Expand Down
6 changes: 5 additions & 1 deletion src/vorta/views/misc_tab.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from PyQt6 import uic
from PyQt6 import QtCore, uic
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication,
Expand Down Expand Up @@ -28,6 +28,8 @@


class MiscTab(MiscTabBase, MiscTabUI, BackupProfileMixin):
refresh_archive = QtCore.pyqtSignal()

def __init__(self, parent=None):
"""Init."""
super().__init__(parent)
Expand Down Expand Up @@ -101,6 +103,8 @@ def populate(self):
cb.setCheckState(Qt.CheckState(setting.value))
cb.setTristate(False)
cb.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v))
if setting.key == 'enable_fixed_units':
cb.stateChanged.connect(self.refresh_archive.emit)

tb = ToolTipButton()
tb.setToolTip(setting.tooltip)
Expand Down
67 changes: 45 additions & 22 deletions tests/unit/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

def test_autostart(qapp, qtbot):
"""Check if file exists only on Linux, otherwise just check it doesn't crash"""

setting = "Automatically start Vorta at login"

_click_toggle_setting(setting, qapp, qtbot)
Expand All @@ -34,10 +33,41 @@ def test_autostart(qapp, qtbot):
assert not os.path.exists(autostart_path)


def test_enable_fixed_units(qapp, qtbot, mocker):
"""Tests the 'enable fixed units' setting to ensure the archive tab sizes are displayed correctly."""
tab = qapp.main_window.archiveTab
setting = "Use the same unit of measurement for archive sizes"

# set mocks
mock_setting = mocker.patch.object(vorta.views.archive_tab.SettingsModel, "get", return_value=Mock(value=True))
mock_pretty_bytes = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes")

# with setting enabled, fixed units should be determined and passed to pretty_bytes as an 'int'
tab.populate_from_profile()
mock_pretty_bytes.assert_called()
kwargs_list = mock_pretty_bytes.call_args_list[0].kwargs
assert 'fixed_unit' in kwargs_list
assert isinstance(kwargs_list['fixed_unit'], int)

# disable setting and reset mock
mock_setting.return_value = Mock(value=False)
mock_pretty_bytes.reset_mock()

# with setting disabled, pretty_bytes should be called with fixed units set to 'None'
tab.populate_from_profile()
mock_pretty_bytes.assert_called()
kwargs_list = mock_pretty_bytes.call_args_list[0].kwargs
assert 'fixed_unit' in kwargs_list
assert kwargs_list['fixed_unit'] is None

# use the qt bot to click the setting and see that the refresh_archive emit works as intended.
with qtbot.waitSignal(qapp.main_window.miscTab.refresh_archive, timeout=5000):
_click_toggle_setting(setting, qapp, qtbot)


@pytest.mark.skipif(sys.platform != 'darwin', reason="Full Disk Access check only on Darwin")
def test_check_full_disk_access(qapp, qtbot, mocker):
"""Enables/disables 'Check for Full Disk Access on startup' setting and ensures functionality"""

setting = "Check for Full Disk Access on startup"

# Set mocks for setting enabled
Expand All @@ -64,23 +94,16 @@ def test_check_full_disk_access(qapp, qtbot, mocker):


def _click_toggle_setting(setting, qapp, qtbot):
"""Click toggle setting in the misc tab"""

main = qapp.main_window
main.tabWidget.setCurrentIndex(4)
tab = main.miscTab

for x in range(0, tab.checkboxLayout.count()):
item = tab.checkboxLayout.itemAt(x, QFormLayout.ItemRole.FieldRole)
if not item:
continue
checkbox = item.itemAt(0).widget()
checkbox.__class__ = QCheckBox

if checkbox.text() == setting:
# Have to use pos to click checkbox correctly
# https://stackoverflow.com/questions/19418125/pysides-qtest-not-checking-box/24070484#24070484
qtbot.mouseClick(
checkbox, QtCore.Qt.MouseButton.LeftButton, pos=QtCore.QPoint(2, int(checkbox.height() / 2))
)
break
"""Toggle setting checkbox in the misc tab"""
miscTab = qapp.main_window.miscTab

for x in range(miscTab.checkboxLayout.count()):
item = miscTab.checkboxLayout.itemAt(x, QFormLayout.ItemRole.FieldRole)
if item is not None:
checkbox = item.itemAt(0).widget()
if checkbox.text() == setting and isinstance(checkbox, QCheckBox):
# Have to use pos to click checkbox correctly
# https://stackoverflow.com/questions/19418125/pysides-qtest-not-checking-box/24070484#24070484
pos = QtCore.QPoint(2, int(checkbox.height() / 2))
qtbot.mouseClick(checkbox, QtCore.Qt.MouseButton.LeftButton, pos=pos)
break
126 changes: 60 additions & 66 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import uuid

import pytest
from vorta.keyring.abc import VortaKeyring
from vorta.utils import find_best_unit_for_sizes, pretty_bytes
from vorta.utils import (
find_best_unit_for_sizes,
pretty_bytes,
)


def test_keyring():
Expand All @@ -13,70 +17,60 @@ def test_keyring():
assert keyring.get_password("vorta-repo", REPO) == UNICODE_PW


def test_best_size_unit_precision0():
@pytest.mark.parametrize(
"precision, expected_unit",
[
(0, 1), # return units as "1" (represents KB), min=100KB
(1, 2), # return units as "2" (represents MB), min=0.1MB
(2, 2), # still returns KB, since 0.1MB < min=0.001 GB to allow for GB to be best_unit
],
)
def test_best_unit_for_sizes_precision(precision, expected_unit):
MB = 1000000
sizes = [int(0.1 * MB), 100 * MB, 2000 * MB]
unit = find_best_unit_for_sizes(sizes, metric=True, precision=0)
assert unit == 1 # KB, min=100KB


def test_best_size_unit_precision1():
MB = 1000000
sizes = [int(0.1 * MB), 100 * MB, 2000 * MB]
unit = find_best_unit_for_sizes(sizes, metric=True, precision=1)
assert unit == 2 # MB, min=0.1MB


def test_best_size_unit_empty():
sizes = []
unit = find_best_unit_for_sizes(sizes, metric=True, precision=1)
assert unit == 0 # bytes


def test_best_size_unit_precision3():
MB = 1000000
sizes = [1 * MB, 100 * MB, 2000 * MB]
unit = find_best_unit_for_sizes(sizes, metric=True, precision=3)
assert unit == 3 # GB, min=0.001 GB


def test_best_size_unit_nonmetric1():
sizes = [102]
unit = find_best_unit_for_sizes(sizes, metric=False, precision=1)
assert unit == 0 # 102 < 0.1KB


def test_best_size_unit_nonmetric2():
sizes = [103]
unit = find_best_unit_for_sizes(sizes, metric=False, precision=1)
assert unit == 1 # 103bytes == 0.1KB


def test_pretty_bytes_metric_fixed1():
s = pretty_bytes(1000000, metric=True, precision=0, fixed_unit=2)
assert s == "1 MB"


def test_pretty_bytes_metric_fixed2():
s = pretty_bytes(1000000, metric=True, precision=1, fixed_unit=2)
assert s == "1.0 MB"


def test_pretty_bytes_metric_fixed3():
s = pretty_bytes(100000, metric=True, precision=1, fixed_unit=2)
assert s == "0.1 MB"


def test_pretty_bytes_nonmetric_fixed1():
s = pretty_bytes(1024 * 1024, metric=False, precision=1, fixed_unit=2)
assert s == "1.0 MiB"


def test_pretty_bytes_metric_nonfixed2():
s = pretty_bytes(1000000, metric=True, precision=1)
assert s == "1.0 MB"


def test_pretty_bytes_metric_large():
s = pretty_bytes(10**30, metric=True, precision=1)
assert s == "1000000.0 YB"
best_unit = find_best_unit_for_sizes(sizes, metric=True, precision=precision)
assert best_unit == expected_unit


@pytest.mark.parametrize(
"sizes, expected_unit",
[
([], 0), # no sizes given but should still return "0" (represents bytes) as best representation
([102], 0), # non-metric size 102 < 0.1KB (102 < 0.1 * 1024), so it will return 0 instead of 1
([103], 1), # non-metric size 103 > 0.1KB (103 < 0.1 * 1024), so it will return 1
],
)
def test_best_unit_for_sizes_nonmetric(sizes, expected_unit):
best_unit = find_best_unit_for_sizes(sizes, metric=False, precision=1)
assert best_unit == expected_unit


@pytest.mark.parametrize(
"size, metric, precision, fixed_unit, expected_output",
[
(10**5, True, 1, 2, "0.1 MB"), # 100KB, metric, precision 1, fixed unit "2" (MB)
(10**6, True, 0, 2, "1 MB"), # 1MB, metric, precision 0, fixed unit "2" (MB)
(10**6, True, 1, 2, "1.0 MB"), # 1MB, metric, precision 1, fixed unit "2" (MB)
(1024 * 1024, False, 1, 2, "1.0 MiB"), # 1MiB, nonmetric, precision 1, fixed unit "2" (MiB)
],
)
def test_pretty_bytes_fixed_units(size, metric, precision, fixed_unit, expected_output):
# test pretty bytes when specifying a fixed unit of measurement
output = pretty_bytes(size, metric=metric, precision=precision, fixed_unit=fixed_unit)
assert output == expected_output


@pytest.mark.parametrize(
"size, metric, expected_output",
[
(10**6, True, "1.0 MB"), # 1MB, metric
(10**24, True, "1.0 YB"), # 1YB, metric
(10**30, True, "1000000.0 YB"), # test huge number, metric
(1024 * 1024, False, "1.0 MiB"), # 1MiB, nonmetric
(2**40 * 2**40, False, "1.0 YiB"), # 1YiB, nonmetric
],
)
def test_pretty_bytes_nonfixed_units(size, metric, expected_output):
# test pretty bytes when NOT specifying a fixed unit of measurement
output = pretty_bytes(size, metric=metric, precision=1)
assert output == expected_output