Addon Manager: Refactor local cache update check
This commit is contained in:
@@ -27,11 +27,9 @@
|
||||
import os
|
||||
import functools
|
||||
import tempfile
|
||||
import hashlib
|
||||
import threading
|
||||
import json
|
||||
import re # Needed for py 3.6 and earlier, can remove later, search for "re."
|
||||
from datetime import date, timedelta
|
||||
from datetime import date
|
||||
from typing import Dict
|
||||
|
||||
from PySide import QtGui, QtCore, QtWidgets
|
||||
@@ -59,6 +57,7 @@ from Addon import Addon
|
||||
from manage_python_dependencies import (
|
||||
PythonPackageManager,
|
||||
)
|
||||
from addonmanager_cache import local_cache_needs_update
|
||||
from addonmanager_devmode import DeveloperMode
|
||||
from addonmanager_firstrun import FirstRunDialog
|
||||
from addonmanager_connection_checker import ConnectionCheckerGUI
|
||||
@@ -179,7 +178,7 @@ class CommandAddonManager:
|
||||
self.packages_with_updates = set()
|
||||
self.startup_sequence = []
|
||||
self.cleanup_workers()
|
||||
self.determine_cache_update_status()
|
||||
self.update_cache = local_cache_needs_update()
|
||||
|
||||
# restore window geometry from stored state
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
@@ -295,79 +294,6 @@ class CommandAddonManager:
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
def determine_cache_update_status(self) -> None:
|
||||
"""Determine whether we need to update the cache, based on user preference, and previous
|
||||
cache update status. Sets self.update_cache to either True or False."""
|
||||
|
||||
# Figure out our cache update frequency: there is a combo box in the preferences dialog
|
||||
# with three options: never, daily, and weekly. Check that first, but allow it to be
|
||||
# overridden by a more specific DaysBetweenUpdates selection, if the user has provided it.
|
||||
# For that parameter we use:
|
||||
# -1: Only manual updates (default)
|
||||
# 0: Update every launch
|
||||
# >0: Update every n days
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
self.update_cache = False
|
||||
update_frequency = pref.GetInt("UpdateFrequencyComboEntry", 0)
|
||||
if update_frequency == 0:
|
||||
days_between_updates = -1
|
||||
elif update_frequency == 1:
|
||||
days_between_updates = 1
|
||||
elif update_frequency == 2:
|
||||
days_between_updates = 7
|
||||
days_between_updates = pref.GetInt("DaysBetweenUpdates", days_between_updates)
|
||||
last_cache_update_string = pref.GetString("LastCacheUpdate", "never")
|
||||
cache_path = FreeCAD.getUserCachePath()
|
||||
am_path = os.path.join(cache_path, "AddonManager")
|
||||
if last_cache_update_string == "never":
|
||||
self.update_cache = True
|
||||
elif days_between_updates > 0:
|
||||
if hasattr(date, "fromisoformat"):
|
||||
last_cache_update = date.fromisoformat(last_cache_update_string)
|
||||
else:
|
||||
# Python 3.6 and earlier don't have date.fromisoformat
|
||||
date_re = re.compile("([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])")
|
||||
matches = date_re.match(last_cache_update_string)
|
||||
last_cache_update = date(
|
||||
int(matches.group(1)), int(matches.group(2)), int(matches.group(3))
|
||||
)
|
||||
delta_update = timedelta(days=days_between_updates)
|
||||
if date.today() >= last_cache_update + delta_update:
|
||||
self.update_cache = True
|
||||
elif days_between_updates == 0:
|
||||
self.update_cache = True
|
||||
elif not os.path.isdir(am_path):
|
||||
self.update_cache = True
|
||||
stopfile = utils.get_cache_file_name("CACHE_UPDATE_INTERRUPTED")
|
||||
if os.path.exists(stopfile):
|
||||
self.update_cache = True
|
||||
os.remove(stopfile)
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Previous cache process was interrupted, restarting...\n",
|
||||
)
|
||||
)
|
||||
|
||||
# See if the user has changed the custom repos list since our last re-cache:
|
||||
stored_hash = pref.GetString("CustomRepoHash", "")
|
||||
custom_repos = pref.GetString("CustomRepositories", "")
|
||||
if custom_repos:
|
||||
hasher = hashlib.sha1()
|
||||
hasher.update(custom_repos.encode("utf-8"))
|
||||
new_hash = hasher.hexdigest()
|
||||
else:
|
||||
new_hash = ""
|
||||
if new_hash != stored_hash:
|
||||
stored_hash = pref.SetString("CustomRepoHash", new_hash)
|
||||
self.update_cache = True
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Custom repo list changed, forcing recache...\n",
|
||||
)
|
||||
)
|
||||
|
||||
def reject(self) -> None:
|
||||
"""called when the window has been closed"""
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class GitFailed(RuntimeError):
|
||||
|
||||
|
||||
class MockConsole:
|
||||
"""Mock for the FreeCAD.Console -- does NOT print anything out, just logs it."""
|
||||
"""Spy for the FreeCAD.Console -- does NOT print anything out, just logs it."""
|
||||
|
||||
def __init__(self):
|
||||
self.log = []
|
||||
@@ -401,3 +401,63 @@ class MockThread:
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MockPref:
|
||||
def __init__(self):
|
||||
self.prefs = {}
|
||||
self.pref_set_counter = {}
|
||||
self.pref_get_counter = {}
|
||||
|
||||
def set_prefs(self, pref_dict: dict) -> None:
|
||||
self.prefs = pref_dict
|
||||
|
||||
def GetInt(self, key: str, default: int) -> int:
|
||||
return self.Get(key, default)
|
||||
|
||||
def GetString(self, key: str, default: str) -> str:
|
||||
return self.Get(key, default)
|
||||
|
||||
def GetBool(self, key: str, default: bool) -> bool:
|
||||
return self.Get(key, default)
|
||||
|
||||
def Get(self, key: str, default):
|
||||
if key not in self.pref_set_counter:
|
||||
self.pref_get_counter[key] = 1
|
||||
else:
|
||||
self.pref_get_counter[key] += 1
|
||||
if key in self.prefs:
|
||||
return self.prefs[key]
|
||||
raise ValueError(f"Expected key not in mock preferences: {key}")
|
||||
|
||||
def SetInt(self, key: str, value: int) -> None:
|
||||
return self.Set(key, value)
|
||||
|
||||
def SetString(self, key: str, value: str) -> None:
|
||||
return self.Set(key, value)
|
||||
|
||||
def SetBool(self, key: str, value: bool) -> None:
|
||||
return self.Set(key, value)
|
||||
|
||||
def Set(self, key: str, value):
|
||||
if key not in self.pref_set_counter:
|
||||
self.pref_set_counter[key] = 1
|
||||
else:
|
||||
self.pref_set_counter[key] += 1
|
||||
self.prefs[key] = value
|
||||
|
||||
|
||||
class MockExists:
|
||||
def __init__(self, files: List[str] = None):
|
||||
"""Returns True for all files in files, and False for all others"""
|
||||
self.files = files
|
||||
self.files_checked = []
|
||||
|
||||
def exists(self, check_file: str):
|
||||
self.files_checked.append(check_file)
|
||||
if not self.files:
|
||||
return False
|
||||
for file in self.files:
|
||||
if check_file.endswith(file):
|
||||
return True
|
||||
return False
|
||||
|
||||
126
src/Mod/AddonManager/AddonManagerTest/app/test_cache.py
Normal file
126
src/Mod/AddonManager/AddonManagerTest/app/test_cache.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD 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 *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import datetime
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import date
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.append("../..")
|
||||
|
||||
import addonmanager_cache as cache
|
||||
from AddonManagerTest.app.mocks import MockPref, MockExists
|
||||
|
||||
|
||||
class TestCache(TestCase):
|
||||
@patch("addonmanager_freecad_interface.getUserCachePath")
|
||||
@patch("addonmanager_freecad_interface.ParamGet")
|
||||
@patch("os.remove", MagicMock())
|
||||
@patch("os.makedirs", MagicMock())
|
||||
def test_local_cache_needs_update(self, param_mock: MagicMock, cache_mock: MagicMock):
|
||||
cache_mock.return_value = ""
|
||||
param_mock.return_value = MockPref()
|
||||
default_prefs = {
|
||||
"UpdateFrequencyComboEntry": 0,
|
||||
"LastCacheUpdate": "2000-01-01",
|
||||
"CustomRepoHash": "",
|
||||
"CustomRepositories": "",
|
||||
}
|
||||
today = date.today().isoformat()
|
||||
yesterday = (date.today() - datetime.timedelta(1)).isoformat()
|
||||
|
||||
# Organize these as subtests because of all the patching that has to be done: once we are in this function,
|
||||
# the patch is complete, and we can just modify the return values of the fakes one by one
|
||||
tests = (
|
||||
{
|
||||
"case": "No existing cache",
|
||||
"files_that_exist": [],
|
||||
"prefs_to_set": {},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Last cache update was interrupted",
|
||||
"files_that_exist": ["CACHE_UPDATE_INTERRUPTED"],
|
||||
"prefs_to_set": {},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Cache exists and updating is blocked",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {},
|
||||
"expect": False,
|
||||
},
|
||||
{
|
||||
"case": "Daily updates set and last update was long ago",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Daily updates set and last update was today",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1, "LastCacheUpdate": today},
|
||||
"expect": False,
|
||||
},
|
||||
{
|
||||
"case": "Daily updates set and last update was yesterday",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1, "LastCacheUpdate": yesterday},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Weekly updates set and last update was long ago",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 1},
|
||||
"expect": True,
|
||||
},
|
||||
{
|
||||
"case": "Weekly updates set and last update was yesterday",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"UpdateFrequencyComboEntry": 2, "LastCacheUpdate": yesterday},
|
||||
"expect": False,
|
||||
},
|
||||
{
|
||||
"case": "Custom repo list changed",
|
||||
"files_that_exist": ["AddonManager"],
|
||||
"prefs_to_set": {"CustomRepositories": "NewRepo"},
|
||||
"expect": True,
|
||||
},
|
||||
)
|
||||
for test_case in tests:
|
||||
with self.subTest(test_case["case"]):
|
||||
case_prefs = default_prefs
|
||||
for pref, setting in test_case["prefs_to_set"].items():
|
||||
case_prefs[pref] = setting
|
||||
param_mock.return_value.set_prefs(case_prefs)
|
||||
exists_mock = MockExists(test_case["files_that_exist"])
|
||||
with patch("os.path.exists", exists_mock.exists):
|
||||
if test_case["expect"]:
|
||||
self.assertTrue(cache.local_cache_needs_update())
|
||||
else:
|
||||
self.assertFalse(cache.local_cache_needs_update())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -7,6 +7,7 @@ SET(AddonManager_SRCS
|
||||
Addon.py
|
||||
AddonManager.py
|
||||
AddonManager.ui
|
||||
addonmanager_cache.py
|
||||
addonmanager_connection_checker.py
|
||||
addonmanager_dependency_installer.py
|
||||
addonmanager_devmode.py
|
||||
@@ -85,6 +86,7 @@ SET(AddonManagerTestsApp_SRCS
|
||||
AddonManagerTest/app/__init__.py
|
||||
AddonManagerTest/app/mocks.py
|
||||
AddonManagerTest/app/test_addon.py
|
||||
AddonManagerTest/app/test_cache.py
|
||||
AddonManagerTest/app/test_dependency_installer.py
|
||||
AddonManagerTest/app/test_freecad_interface.py
|
||||
AddonManagerTest/app/test_git.py
|
||||
|
||||
@@ -30,6 +30,9 @@ from AddonManagerTest.app.test_utilities import (
|
||||
from AddonManagerTest.app.test_addon import (
|
||||
TestAddon as AddonManagerTestAddon,
|
||||
)
|
||||
from AddonManagerTest.app.test_cache import (
|
||||
TestCache as AddonManagerTestCache,
|
||||
)
|
||||
from AddonManagerTest.app.test_macro import (
|
||||
TestMacro as AddonManagerTestMacro,
|
||||
)
|
||||
@@ -74,6 +77,7 @@ except ImportError:
|
||||
loaded_gui_tests = [
|
||||
AddonManagerTestUtilities,
|
||||
AddonManagerTestAddon,
|
||||
AddonManagerTestCache,
|
||||
AddonManagerTestMacro,
|
||||
AddonManagerTestGit,
|
||||
AddonManagerTestAddonInstaller,
|
||||
|
||||
118
src/Mod/AddonManager/addonmanager_cache.py
Normal file
118
src/Mod/AddonManager/addonmanager_cache.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD 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 *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from datetime import date, timedelta
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import addonmanager_freecad_interface as fci
|
||||
import addonmanager_utilities as utils
|
||||
|
||||
translate = fci.translate
|
||||
|
||||
|
||||
def local_cache_needs_update() -> bool:
|
||||
"""Determine whether we need to update the cache, based on user preference, and previous
|
||||
cache update status. Returns either True or False."""
|
||||
|
||||
if not _cache_exists():
|
||||
return True
|
||||
|
||||
if _last_update_was_interrupted(reset_status=True):
|
||||
return True
|
||||
|
||||
if _custom_repo_list_changed():
|
||||
return True
|
||||
|
||||
# Figure out our cache update frequency: there is a combo box in the preferences dialog
|
||||
# with three options: never, daily, and weekly.
|
||||
days_between_updates = _days_between_updates()
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
last_cache_update_string = pref.GetString("LastCacheUpdate", "never")
|
||||
|
||||
if last_cache_update_string == "never":
|
||||
return True
|
||||
elif days_between_updates > 0:
|
||||
last_cache_update = date.fromisoformat(last_cache_update_string)
|
||||
delta_update = timedelta(days=days_between_updates)
|
||||
if date.today() >= last_cache_update + delta_update:
|
||||
return True
|
||||
elif days_between_updates == 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _days_between_updates() -> int:
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
update_frequency = pref.GetInt("UpdateFrequencyComboEntry", 0)
|
||||
if update_frequency == 0:
|
||||
return -1
|
||||
elif update_frequency == 1:
|
||||
return 1
|
||||
elif update_frequency == 2:
|
||||
return 7
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def _cache_exists() -> bool:
|
||||
cache_path = fci.getUserCachePath()
|
||||
am_path = os.path.join(cache_path, "AddonManager")
|
||||
return os.path.exists(am_path)
|
||||
|
||||
|
||||
def _last_update_was_interrupted(reset_status: bool) -> bool:
|
||||
flag_file = utils.get_cache_file_name("CACHE_UPDATE_INTERRUPTED")
|
||||
if os.path.exists(flag_file):
|
||||
if reset_status:
|
||||
os.remove(flag_file)
|
||||
fci.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Previous cache process was interrupted, restarting...\n",
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _custom_repo_list_changed() -> bool:
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
stored_hash = pref.GetString("CustomRepoHash", "")
|
||||
custom_repos = pref.GetString("CustomRepositories", "")
|
||||
if custom_repos:
|
||||
hasher = hashlib.sha1()
|
||||
hasher.update(custom_repos.encode("utf-8"))
|
||||
new_hash = hasher.hexdigest()
|
||||
else:
|
||||
new_hash = ""
|
||||
if new_hash != stored_hash:
|
||||
pref.SetString("CustomRepoHash", new_hash)
|
||||
fci.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Custom repo list changed, forcing recache...\n",
|
||||
)
|
||||
)
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user