Addon Manager: Refactor installation code
Improve testability of installation code by refactoring it to completely separate the GUI and non-GUI code, and to provide more robust support for non-GUI access to some type of Addon Manager activity.
This commit is contained in:
@@ -29,17 +29,25 @@ import FreeCAD
|
||||
from Addon import Addon, INTERNAL_WORKBENCHES
|
||||
from addonmanager_macro import Macro
|
||||
|
||||
|
||||
class TestAddon(unittest.TestCase):
|
||||
|
||||
MODULE = "test_addon" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = os.path.join(FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data")
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
def test_display_name(self):
|
||||
|
||||
# Case 1: No display name set elsewhere: name == display_name
|
||||
addon = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
self.assertEqual(addon.name, "FreeCAD")
|
||||
self.assertEqual(addon.display_name, "FreeCAD")
|
||||
|
||||
@@ -49,51 +57,68 @@ class TestAddon(unittest.TestCase):
|
||||
self.assertEqual(addon.display_name, "Test Workbench")
|
||||
|
||||
def test_metadata_loading(self):
|
||||
addon = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "good_package.xml"))
|
||||
|
||||
# Generic tests:
|
||||
self.assertIsNotNone(addon.metadata)
|
||||
self.assertEqual(addon.metadata.Version, "1.0.1")
|
||||
self.assertEqual(addon.metadata.Description, "A package.xml file for unit testing.")
|
||||
self.assertEqual(
|
||||
addon.metadata.Description, "A package.xml file for unit testing."
|
||||
)
|
||||
|
||||
maintainer_list = addon.metadata.Maintainer
|
||||
self.assertEqual(len(maintainer_list),1,"Wrong number of maintainers found")
|
||||
self.assertEqual(maintainer_list[0]["name"],"FreeCAD Developer")
|
||||
self.assertEqual(maintainer_list[0]["email"],"developer@freecad.org")
|
||||
self.assertEqual(len(maintainer_list), 1, "Wrong number of maintainers found")
|
||||
self.assertEqual(maintainer_list[0]["name"], "FreeCAD Developer")
|
||||
self.assertEqual(maintainer_list[0]["email"], "developer@freecad.org")
|
||||
|
||||
license_list = addon.metadata.License
|
||||
self.assertEqual(len(license_list),1,"Wrong number of licenses found")
|
||||
self.assertEqual(license_list[0]["name"],"LGPLv2.1")
|
||||
self.assertEqual(license_list[0]["file"],"LICENSE")
|
||||
self.assertEqual(len(license_list), 1, "Wrong number of licenses found")
|
||||
self.assertEqual(license_list[0]["name"], "LGPLv2.1")
|
||||
self.assertEqual(license_list[0]["file"], "LICENSE")
|
||||
|
||||
url_list = addon.metadata.Urls
|
||||
self.assertEqual(len(url_list),2,"Wrong number of urls found")
|
||||
self.assertEqual(url_list[0]["type"],"repository")
|
||||
self.assertEqual(url_list[0]["location"],"https://github.com/chennes/FreeCAD-Package")
|
||||
self.assertEqual(url_list[0]["branch"],"main")
|
||||
self.assertEqual(url_list[1]["type"],"readme")
|
||||
self.assertEqual(url_list[1]["location"],"https://github.com/chennes/FreeCAD-Package/blob/main/README.md")
|
||||
self.assertEqual(len(url_list), 2, "Wrong number of urls found")
|
||||
self.assertEqual(url_list[0]["type"], "repository")
|
||||
self.assertEqual(
|
||||
url_list[0]["location"], "https://github.com/chennes/FreeCAD-Package"
|
||||
)
|
||||
self.assertEqual(url_list[0]["branch"], "main")
|
||||
self.assertEqual(url_list[1]["type"], "readme")
|
||||
self.assertEqual(
|
||||
url_list[1]["location"],
|
||||
"https://github.com/chennes/FreeCAD-Package/blob/main/README.md",
|
||||
)
|
||||
|
||||
contents = addon.metadata.Content
|
||||
self.assertEqual(len(contents),1,"Wrong number of content catetories found")
|
||||
self.assertEqual(len(contents["workbench"]),1,"Wrong number of workbenches found")
|
||||
self.assertEqual(len(contents), 1, "Wrong number of content catetories found")
|
||||
self.assertEqual(
|
||||
len(contents["workbench"]), 1, "Wrong number of workbenches found"
|
||||
)
|
||||
|
||||
def test_git_url_cleanup(self):
|
||||
base_url = "https://github.com/FreeCAD/FreeCAD"
|
||||
test_urls = [f" {base_url} ",
|
||||
f"{base_url}.git",
|
||||
f" {base_url}.git "]
|
||||
test_urls = [f" {base_url} ", f"{base_url}.git", f" {base_url}.git "]
|
||||
for url in test_urls:
|
||||
addon = Addon("FreeCAD", url, Addon.Status.NOT_INSTALLED, "master")
|
||||
self.assertEqual(addon.url, base_url)
|
||||
|
||||
def test_tag_extraction(self):
|
||||
addon = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "good_package.xml"))
|
||||
|
||||
tags = addon.tags
|
||||
self.assertEqual(len(tags),5)
|
||||
self.assertEqual(len(tags), 5)
|
||||
expected_tags = set()
|
||||
expected_tags.add("Tag0")
|
||||
expected_tags.add("Tag1")
|
||||
@@ -107,41 +132,79 @@ class TestAddon(unittest.TestCase):
|
||||
# Test package.xml combinations:
|
||||
|
||||
# Workbenches
|
||||
addon_with_workbench = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon_with_workbench.load_metadata_file(os.path.join(self.test_dir, "workbench_only.xml"))
|
||||
addon_with_workbench = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_workbench.load_metadata_file(
|
||||
os.path.join(self.test_dir, "workbench_only.xml")
|
||||
)
|
||||
self.assertTrue(addon_with_workbench.contains_workbench())
|
||||
self.assertFalse(addon_with_workbench.contains_macro())
|
||||
self.assertFalse(addon_with_workbench.contains_preference_pack())
|
||||
|
||||
# Macros
|
||||
addon_with_macro = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon_with_macro.load_metadata_file(os.path.join(self.test_dir, "macro_only.xml"))
|
||||
addon_with_macro = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_macro.load_metadata_file(
|
||||
os.path.join(self.test_dir, "macro_only.xml")
|
||||
)
|
||||
self.assertFalse(addon_with_macro.contains_workbench())
|
||||
self.assertTrue(addon_with_macro.contains_macro())
|
||||
self.assertFalse(addon_with_macro.contains_preference_pack())
|
||||
|
||||
# Preference Packs
|
||||
addon_with_prefpack = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon_with_prefpack.load_metadata_file(os.path.join(self.test_dir, "prefpack_only.xml"))
|
||||
addon_with_prefpack = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_prefpack.load_metadata_file(
|
||||
os.path.join(self.test_dir, "prefpack_only.xml")
|
||||
)
|
||||
self.assertFalse(addon_with_prefpack.contains_workbench())
|
||||
self.assertFalse(addon_with_prefpack.contains_macro())
|
||||
self.assertTrue(addon_with_prefpack.contains_preference_pack())
|
||||
|
||||
# Combination
|
||||
addon_with_all = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon_with_all.load_metadata_file(os.path.join(self.test_dir, "combination.xml"))
|
||||
addon_with_all = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_with_all.load_metadata_file(
|
||||
os.path.join(self.test_dir, "combination.xml")
|
||||
)
|
||||
self.assertTrue(addon_with_all.contains_workbench())
|
||||
self.assertTrue(addon_with_all.contains_macro())
|
||||
self.assertTrue(addon_with_all.contains_preference_pack())
|
||||
|
||||
# Now do the simple, explicitly-set cases
|
||||
addon_wb = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon_wb = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_wb.repo_type = Addon.Kind.WORKBENCH
|
||||
self.assertTrue(addon_wb.contains_workbench())
|
||||
self.assertFalse(addon_wb.contains_macro())
|
||||
self.assertFalse(addon_wb.contains_preference_pack())
|
||||
|
||||
addon_m = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon_m = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon_m.repo_type = Addon.Kind.MACRO
|
||||
self.assertFalse(addon_m.contains_workbench())
|
||||
self.assertTrue(addon_m.contains_macro())
|
||||
@@ -157,18 +220,25 @@ class TestAddon(unittest.TestCase):
|
||||
addon = Addon.from_macro(macro)
|
||||
|
||||
self.assertEqual(addon.repo_type, Addon.Kind.MACRO)
|
||||
self.assertEqual(addon.name,"DoNothing")
|
||||
self.assertEqual(addon.macro.comment,"Do absolutely nothing. For Addon Manager unit tests.")
|
||||
self.assertEqual(addon.url,"https://github.com/FreeCAD/FreeCAD")
|
||||
self.assertEqual(addon.macro.version,"1.0")
|
||||
self.assertEqual(len(addon.macro.other_files),3)
|
||||
self.assertEqual(addon.macro.author,"Chris Hennes")
|
||||
self.assertEqual(addon.macro.date,"2022-02-28")
|
||||
self.assertEqual(addon.macro.icon,"not_real.png")
|
||||
self.assertEqual(addon.macro.xpm,"")
|
||||
self.assertEqual(addon.name, "DoNothing")
|
||||
self.assertEqual(
|
||||
addon.macro.comment, "Do absolutely nothing. For Addon Manager unit tests."
|
||||
)
|
||||
self.assertEqual(addon.url, "https://github.com/FreeCAD/FreeCAD")
|
||||
self.assertEqual(addon.macro.version, "1.0")
|
||||
self.assertEqual(len(addon.macro.other_files), 3)
|
||||
self.assertEqual(addon.macro.author, "Chris Hennes")
|
||||
self.assertEqual(addon.macro.date, "2022-02-28")
|
||||
self.assertEqual(addon.macro.icon, "not_real.png")
|
||||
self.assertEqual(addon.macro.xpm, "")
|
||||
|
||||
def test_cache(self):
|
||||
addon = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
cache_data = addon.to_cache()
|
||||
second_addon = Addon.from_cache(cache_data)
|
||||
|
||||
@@ -176,10 +246,30 @@ class TestAddon(unittest.TestCase):
|
||||
|
||||
def test_dependency_resolution(self):
|
||||
|
||||
addonA = Addon("AddonA","https://github.com/FreeCAD/FakeAddonA", Addon.Status.NOT_INSTALLED, "master")
|
||||
addonB = Addon("AddonB","https://github.com/FreeCAD/FakeAddonB", Addon.Status.NOT_INSTALLED, "master")
|
||||
addonC = Addon("AddonC","https://github.com/FreeCAD/FakeAddonC", Addon.Status.NOT_INSTALLED, "master")
|
||||
addonD = Addon("AddonD","https://github.com/FreeCAD/FakeAddonD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addonA = Addon(
|
||||
"AddonA",
|
||||
"https://github.com/FreeCAD/FakeAddonA",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addonB = Addon(
|
||||
"AddonB",
|
||||
"https://github.com/FreeCAD/FakeAddonB",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addonC = Addon(
|
||||
"AddonC",
|
||||
"https://github.com/FreeCAD/FakeAddonC",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addonD = Addon(
|
||||
"AddonD",
|
||||
"https://github.com/FreeCAD/FakeAddonD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
|
||||
addonA.requires.add("AddonB")
|
||||
addonB.requires.add("AddonC")
|
||||
@@ -191,30 +281,69 @@ class TestAddon(unittest.TestCase):
|
||||
addonB.name: addonB,
|
||||
addonC.name: addonC,
|
||||
addonD.name: addonD,
|
||||
}
|
||||
}
|
||||
|
||||
deps = Addon.Dependencies()
|
||||
addonA.walk_dependency_tree(all_addons, deps)
|
||||
|
||||
self.assertEqual(len(deps.required_external_addons),3)
|
||||
self.assertEqual(len(deps.required_external_addons), 3)
|
||||
addon_strings = [addon.name for addon in deps.required_external_addons]
|
||||
self.assertTrue("AddonB" in addon_strings, "AddonB not in required dependencies, and it should be.")
|
||||
self.assertTrue("AddonC" in addon_strings, "AddonC not in required dependencies, and it should be.")
|
||||
self.assertTrue("AddonD" in addon_strings, "AddonD not in required dependencies, and it should be.")
|
||||
self.assertTrue("Path" in deps.internal_workbenches, "Path not in workbench dependencies, and it should be.")
|
||||
self.assertTrue(
|
||||
"AddonB" in addon_strings,
|
||||
"AddonB not in required dependencies, and it should be.",
|
||||
)
|
||||
self.assertTrue(
|
||||
"AddonC" in addon_strings,
|
||||
"AddonC not in required dependencies, and it should be.",
|
||||
)
|
||||
self.assertTrue(
|
||||
"AddonD" in addon_strings,
|
||||
"AddonD not in required dependencies, and it should be.",
|
||||
)
|
||||
self.assertTrue(
|
||||
"Path" in deps.internal_workbenches,
|
||||
"Path not in workbench dependencies, and it should be.",
|
||||
)
|
||||
|
||||
def test_internal_workbench_list(self):
|
||||
addon = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "depends_on_all_workbenches.xml"))
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(
|
||||
os.path.join(self.test_dir, "depends_on_all_workbenches.xml")
|
||||
)
|
||||
deps = Addon.Dependencies()
|
||||
addon.walk_dependency_tree({}, deps)
|
||||
self.assertEqual(len(deps.internal_workbenches), len(INTERNAL_WORKBENCHES))
|
||||
|
||||
def test_version_check(self):
|
||||
addon = Addon("FreeCAD","https://github.com/FreeCAD/FreeCAD", Addon.Status.NOT_INSTALLED, "master")
|
||||
addon.load_metadata_file(os.path.join(self.test_dir, "test_version_detection.xml"))
|
||||
addon = Addon(
|
||||
"FreeCAD",
|
||||
"https://github.com/FreeCAD/FreeCAD",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
addon.load_metadata_file(
|
||||
os.path.join(self.test_dir, "test_version_detection.xml")
|
||||
)
|
||||
|
||||
self.assertEqual(len(addon.tags),1, "Wrong number of tags found: version requirements should have restricted to only one")
|
||||
self.assertFalse("TagA" in addon.tags, "Found 'TagA' in tags, it should have been excluded by version requirement")
|
||||
self.assertTrue("TagB" in addon.tags, "Failed to find 'TagB' in tags, it should have been included")
|
||||
self.assertFalse("TagC" in addon.tags, "Found 'TagA' in tags, it should have been excluded by version requirement")
|
||||
self.assertEqual(
|
||||
len(addon.tags),
|
||||
1,
|
||||
"Wrong number of tags found: version requirements should have restricted to only one",
|
||||
)
|
||||
self.assertFalse(
|
||||
"TagA" in addon.tags,
|
||||
"Found 'TagA' in tags, it should have been excluded by version requirement",
|
||||
)
|
||||
self.assertTrue(
|
||||
"TagB" in addon.tags,
|
||||
"Failed to find 'TagB' in tags, it should have been included",
|
||||
)
|
||||
self.assertFalse(
|
||||
"TagC" in addon.tags,
|
||||
"Found 'TagA' in tags, it should have been excluded by version requirement",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 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 functools
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from time import sleep
|
||||
import unittest
|
||||
|
||||
from addonmanager_dependency_installer import DependencyInstaller
|
||||
|
||||
|
||||
class CompleteProcessMock(subprocess.CompletedProcess):
|
||||
def __init__(self):
|
||||
super().__init__(["fake_arg"], 0)
|
||||
self.stdout = "Mock suprocess call stdout result"
|
||||
|
||||
|
||||
class SubprocessMock:
|
||||
def __init__(self):
|
||||
self.arg_log = []
|
||||
self.called = False
|
||||
self.call_count = 0
|
||||
self.delay = 0
|
||||
self.succeed = True
|
||||
|
||||
def subprocess_interceptor(self, args):
|
||||
self.arg_log.append(args)
|
||||
self.called = True
|
||||
self.call_count += 1
|
||||
sleep(self.delay)
|
||||
if self.succeed:
|
||||
return CompleteProcessMock()
|
||||
raise subprocess.CalledProcessError(1, " ".join(args), "Unit test mock output")
|
||||
|
||||
|
||||
class FakeFunction:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.call_count = 0
|
||||
self.return_value = None
|
||||
self.arg_log = []
|
||||
|
||||
def func_call(self, *args):
|
||||
self.arg_log.append(args)
|
||||
self.called = True
|
||||
self.call_count += 1
|
||||
return self.return_value
|
||||
|
||||
|
||||
class TestDependencyInstaller(unittest.TestCase):
|
||||
"""Test the dependency installation class"""
|
||||
|
||||
def setUp(self):
|
||||
self.subprocess_mock = SubprocessMock()
|
||||
self.test_object = DependencyInstaller(
|
||||
[], ["required_py_package"], ["optional_py_package"]
|
||||
)
|
||||
self.test_object._subprocess_wrapper = (
|
||||
self.subprocess_mock.subprocess_interceptor
|
||||
)
|
||||
self.signals_caught = []
|
||||
self.test_object.failure.connect(
|
||||
functools.partial(self.catch_signal, "failure")
|
||||
)
|
||||
self.test_object.finished.connect(
|
||||
functools.partial(self.catch_signal, "finished")
|
||||
)
|
||||
self.test_object.no_pip.connect(functools.partial(self.catch_signal, "no_pip"))
|
||||
self.test_object.no_python_exe.connect(
|
||||
functools.partial(self.catch_signal, "no_python_exe")
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def catch_signal(self, signal_name, *_):
|
||||
self.signals_caught.append(signal_name)
|
||||
|
||||
def test_run_no_pip(self):
|
||||
self.test_object._verify_pip = lambda: False
|
||||
self.test_object.run()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
|
||||
def test_run_with_pip(self):
|
||||
ff = FakeFunction()
|
||||
self.test_object._verify_pip = lambda: True
|
||||
self.test_object._install_python_packages = ff.func_call
|
||||
self.test_object.run()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
self.assertTrue(ff.called)
|
||||
|
||||
def test_run_with_no_packages(self):
|
||||
ff = FakeFunction()
|
||||
self.test_object._verify_pip = lambda: True
|
||||
self.test_object._install_python_packages = ff.func_call
|
||||
self.test_object.python_requires = []
|
||||
self.test_object.python_optional = []
|
||||
self.test_object.run()
|
||||
self.assertIn("finished", self.signals_caught)
|
||||
self.assertFalse(ff.called)
|
||||
|
||||
def test_install_python_packages_new_location(self):
|
||||
ff_required = FakeFunction()
|
||||
ff_optional = FakeFunction()
|
||||
self.test_object._install_required = ff_required.func_call
|
||||
self.test_object._install_optional = ff_optional.func_call
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.test_object.location = os.path.join(td, "UnitTestLocation")
|
||||
self.test_object._install_python_packages()
|
||||
self.assertTrue(ff_required.called)
|
||||
self.assertTrue(ff_optional.called)
|
||||
self.assertTrue(os.path.exists(self.test_object.location))
|
||||
|
||||
def test_install_python_packages_existing_location(self):
|
||||
ff_required = FakeFunction()
|
||||
ff_optional = FakeFunction()
|
||||
self.test_object._install_required = ff_required.func_call
|
||||
self.test_object._install_optional = ff_optional.func_call
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.test_object.location = td
|
||||
self.test_object._install_python_packages()
|
||||
self.assertTrue(ff_required.called)
|
||||
self.assertTrue(ff_optional.called)
|
||||
|
||||
def test_verify_pip_no_python(self):
|
||||
self.test_object._get_python = lambda: None
|
||||
should_continue = self.test_object._verify_pip()
|
||||
self.assertFalse(should_continue)
|
||||
self.assertEqual(len(self.signals_caught), 0)
|
||||
|
||||
def test_verify_pip_no_pip(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = False
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
result = self.test_object._verify_pip()
|
||||
self.assertFalse(result)
|
||||
self.assertIn("no_pip", self.signals_caught)
|
||||
|
||||
def test_verify_pip_with_pip(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = True
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
result = self.test_object._verify_pip()
|
||||
self.assertTrue(result)
|
||||
self.assertNotIn("no_pip", self.signals_caught)
|
||||
|
||||
def test_install_required_loops(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = True
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_requires = ["test1", "test2", "test3"]
|
||||
self.test_object._install_required("vendor_path")
|
||||
self.assertEqual(sm.call_count, 3)
|
||||
|
||||
def test_install_required_failure(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = False
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_requires = ["test1", "test2", "test3"]
|
||||
self.test_object._install_required("vendor_path")
|
||||
self.assertEqual(sm.call_count, 1)
|
||||
self.assertIn("failure", self.signals_caught)
|
||||
|
||||
def test_install_optional_loops(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = True
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_optional = ["test1", "test2", "test3"]
|
||||
self.test_object._install_optional("vendor_path")
|
||||
self.assertEqual(sm.call_count, 3)
|
||||
|
||||
def test_install_optional_failure(self):
|
||||
sm = SubprocessMock()
|
||||
sm.succeed = False
|
||||
self.test_object._subprocess_wrapper = sm.subprocess_interceptor
|
||||
self.test_object._get_python = lambda: "fake_python"
|
||||
self.test_object.python_optional = ["test1", "test2", "test3"]
|
||||
self.test_object._install_optional("vendor_path")
|
||||
self.assertEqual(sm.call_count, 3)
|
||||
|
||||
def test_run_pip(self):
|
||||
pass
|
||||
@@ -69,7 +69,7 @@ class TestGit(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
"""Clean up after the test"""
|
||||
os.chdir(self.cwd)
|
||||
#self._rmdir(self.test_dir)
|
||||
# self._rmdir(self.test_dir)
|
||||
os.rename(self.test_dir, self.test_dir + ".old." + str(time.time()))
|
||||
|
||||
def test_clone(self):
|
||||
@@ -77,7 +77,9 @@ class TestGit(unittest.TestCase):
|
||||
checkout_dir = self._clone_test_repo()
|
||||
self.assertTrue(os.path.exists(checkout_dir))
|
||||
self.assertTrue(os.path.exists(os.path.join(checkout_dir, ".git")))
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
self.assertEqual(
|
||||
os.getcwd(), self.cwd, "We should be left in the same CWD we started"
|
||||
)
|
||||
|
||||
def test_checkout(self):
|
||||
"""Test git checkout"""
|
||||
@@ -88,7 +90,9 @@ class TestGit(unittest.TestCase):
|
||||
expected_status = "## HEAD (no branch)"
|
||||
self.assertEqual(status, expected_status)
|
||||
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
self.assertEqual(
|
||||
os.getcwd(), self.cwd, "We should be left in the same CWD we started"
|
||||
)
|
||||
|
||||
def test_update(self):
|
||||
"""Test using git to update the local repo"""
|
||||
@@ -98,7 +102,9 @@ class TestGit(unittest.TestCase):
|
||||
self.assertTrue(self.git.update_available(checkout_dir))
|
||||
self.git.update(checkout_dir)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
self.assertEqual(
|
||||
os.getcwd(), self.cwd, "We should be left in the same CWD we started"
|
||||
)
|
||||
|
||||
def test_tag_and_branch(self):
|
||||
"""Test checking the currently checked-out tag"""
|
||||
@@ -122,21 +128,25 @@ class TestGit(unittest.TestCase):
|
||||
self.assertEqual(found_branch, expected_branch)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
self.assertEqual(
|
||||
os.getcwd(), self.cwd, "We should be left in the same CWD we started"
|
||||
)
|
||||
|
||||
def test_get_remote(self):
|
||||
""" Test getting the remote location """
|
||||
"""Test getting the remote location"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
expected_remote = self.test_repo_remote
|
||||
returned_remote = self.git.get_remote(checkout_dir)
|
||||
self.assertEqual(expected_remote, returned_remote)
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
self.assertEqual(
|
||||
os.getcwd(), self.cwd, "We should be left in the same CWD we started"
|
||||
)
|
||||
|
||||
def test_repair(self):
|
||||
""" Test the repair feature (and some exception throwing) """
|
||||
"""Test the repair feature (and some exception throwing)"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
remote = self.git.get_remote(checkout_dir)
|
||||
git_dir = os.path.join(checkout_dir,".git")
|
||||
git_dir = os.path.join(checkout_dir, ".git")
|
||||
self.assertTrue(os.path.exists(git_dir))
|
||||
self._rmdir(git_dir)
|
||||
|
||||
@@ -146,9 +156,10 @@ class TestGit(unittest.TestCase):
|
||||
|
||||
self.git.repair(remote, checkout_dir)
|
||||
status = self.git.status(checkout_dir)
|
||||
self.assertEqual(status,"## main...origin/main\n")
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
self.assertEqual(status, "## main...origin/main\n")
|
||||
self.assertEqual(
|
||||
os.getcwd(), self.cwd, "We should be left in the same CWD we started"
|
||||
)
|
||||
|
||||
def _rmdir(self, path):
|
||||
try:
|
||||
|
||||
386
src/Mod/AddonManager/AddonManagerTest/app/test_installer.py
Normal file
386
src/Mod/AddonManager/AddonManagerTest/app/test_installer.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 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/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Contains the unit test class for addonmanager_installer.py non-GUI functionality."""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
import FreeCAD
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from addonmanager_installer import InstallationMethod, AddonInstaller, MacroInstaller
|
||||
|
||||
from addonmanager_git import GitManager, initialize_git
|
||||
|
||||
from Addon import Addon
|
||||
|
||||
|
||||
class MockAddon:
|
||||
def __init__(self):
|
||||
self.name = "TestAddon"
|
||||
self.url = "https://github.com/FreeCAD/FreeCAD-addons"
|
||||
self.branch = "master"
|
||||
|
||||
|
||||
class TestAddonInstaller(unittest.TestCase):
|
||||
"""Test class for addonmanager_installer.py non-GUI functionality"""
|
||||
|
||||
MODULE = "test_installer" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize data needed for all tests"""
|
||||
# self.start_time = time.perf_counter()
|
||||
self.test_data_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
self.real_addon = Addon(
|
||||
"TestAddon",
|
||||
"https://github.com/FreeCAD/FreeCAD-addons",
|
||||
Addon.Status.NOT_INSTALLED,
|
||||
"master",
|
||||
)
|
||||
self.mock_addon = MockAddon()
|
||||
|
||||
def tearDown(self):
|
||||
"""Finalize the test."""
|
||||
# end_time = time.perf_counter()
|
||||
# print(f"Test '{self.id()}' ran in {end_time-self.start_time:.4f} seconds")
|
||||
|
||||
def test_validate_object(self):
|
||||
"""An object is valid if it has a name, url, and branch attribute."""
|
||||
|
||||
AddonInstaller._validate_object(self.real_addon) # Won't raise
|
||||
AddonInstaller._validate_object(self.mock_addon) # Won't raise
|
||||
|
||||
class NoName:
|
||||
def __init__(self):
|
||||
self.url = "https://github.com/FreeCAD/FreeCAD-addons"
|
||||
self.branch = "master"
|
||||
|
||||
no_name = NoName()
|
||||
with self.assertRaises(RuntimeError):
|
||||
AddonInstaller._validate_object(no_name)
|
||||
|
||||
class NoUrl:
|
||||
def __init__(self):
|
||||
self.name = "TestAddon"
|
||||
self.branch = "master"
|
||||
|
||||
no_url = NoUrl()
|
||||
with self.assertRaises(RuntimeError):
|
||||
AddonInstaller._validate_object(no_url)
|
||||
|
||||
class NoBranch:
|
||||
def __init__(self):
|
||||
self.name = "TestAddon"
|
||||
self.url = "https://github.com/FreeCAD/FreeCAD-addons"
|
||||
|
||||
no_branch = NoBranch()
|
||||
with self.assertRaises(RuntimeError):
|
||||
AddonInstaller._validate_object(no_branch)
|
||||
|
||||
def test_update_metadata(self):
|
||||
"""If a metadata file exists in the installation location, it should be loaded."""
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer.installation_path = temp_dir
|
||||
addon_dir = os.path.join(temp_dir, self.mock_addon.name)
|
||||
os.mkdir(addon_dir)
|
||||
shutil.copy(
|
||||
os.path.join(self.test_data_dir, "good_package.xml"),
|
||||
os.path.join(addon_dir, "package.xml"),
|
||||
)
|
||||
installer._update_metadata() # Does nothing, but should not crash
|
||||
|
||||
installer = AddonInstaller(self.real_addon, [], [])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer.installation_path = temp_dir
|
||||
installer._update_metadata()
|
||||
addon_dir = os.path.join(temp_dir, self.mock_addon.name)
|
||||
os.mkdir(addon_dir)
|
||||
shutil.copy(
|
||||
os.path.join(self.test_data_dir, "good_package.xml"),
|
||||
os.path.join(addon_dir, "package.xml"),
|
||||
)
|
||||
good_metadata = FreeCAD.Metadata(os.path.join(addon_dir, "package.xml"))
|
||||
installer._update_metadata()
|
||||
self.assertEqual(self.real_addon.installed_version, good_metadata.Version)
|
||||
|
||||
def test_finalize_zip_installation(self):
|
||||
"""Ensure that zipfiles are correctly extracted."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_simple_repo = os.path.join(self.test_data_dir, "test_simple_repo.zip")
|
||||
non_gh_mock = MockAddon()
|
||||
non_gh_mock.url = test_simple_repo
|
||||
non_gh_mock.name = "NonGitHubMock"
|
||||
installer = AddonInstaller(non_gh_mock, [], [])
|
||||
installer.installation_path = temp_dir
|
||||
installer._finalize_zip_installation(test_simple_repo)
|
||||
expected_location = os.path.join(temp_dir, non_gh_mock.name, "README")
|
||||
self.assertTrue(
|
||||
os.path.isfile(expected_location), "Non-GitHub zip extraction failed"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_github_style_repo = os.path.join(
|
||||
self.test_data_dir, "test_github_style_repo.zip"
|
||||
)
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
installer.installation_path = temp_dir
|
||||
installer._finalize_zip_installation(test_github_style_repo)
|
||||
expected_location = os.path.join(temp_dir, self.mock_addon.name, "README")
|
||||
self.assertTrue(
|
||||
os.path.isfile(expected_location), "GitHub zip extraction failed"
|
||||
)
|
||||
|
||||
def test_install_by_git(self):
|
||||
"""Test using git to install. Depends on there being a local git installation: the test
|
||||
is skipped if there is no local git."""
|
||||
git_manager = initialize_git()
|
||||
if not git_manager:
|
||||
self.skipTest("git not found, skipping git installer tests")
|
||||
return
|
||||
|
||||
# Our test git repo has to be in a zipfile, otherwise it cannot itself be stored in git,
|
||||
# since it has a .git subdirectory.
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
git_repo_zip = os.path.join(self.test_data_dir, "test_repo.zip")
|
||||
with ZipFile(git_repo_zip, "r") as zip_repo:
|
||||
zip_repo.extractall(temp_dir)
|
||||
|
||||
mock_addon = MockAddon()
|
||||
mock_addon.url = os.path.join(temp_dir, "test_repo")
|
||||
mock_addon.branch = "main"
|
||||
installer = AddonInstaller(mock_addon, [], [])
|
||||
installer.installation_path = os.path.join(temp_dir, "installed_addon")
|
||||
installer._install_by_git()
|
||||
|
||||
self.assertTrue(os.path.exists(installer.installation_path))
|
||||
addon_name_dir = os.path.join(installer.installation_path, mock_addon.name)
|
||||
self.assertTrue(os.path.exists(addon_name_dir))
|
||||
readme = os.path.join(addon_name_dir, "README.md")
|
||||
self.assertTrue(os.path.exists(readme))
|
||||
|
||||
def test_install_by_copy(self):
|
||||
"""Test using a simple filesystem copy to install an addon."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
git_repo_zip = os.path.join(self.test_data_dir, "test_repo.zip")
|
||||
with ZipFile(git_repo_zip, "r") as zip_repo:
|
||||
zip_repo.extractall(temp_dir)
|
||||
|
||||
mock_addon = MockAddon()
|
||||
mock_addon.url = os.path.join(temp_dir, "test_repo")
|
||||
mock_addon.branch = "main"
|
||||
installer = AddonInstaller(mock_addon, [], [])
|
||||
installer.addon_to_install = mock_addon
|
||||
installer.installation_path = os.path.join(temp_dir, "installed_addon")
|
||||
installer._install_by_copy()
|
||||
|
||||
self.assertTrue(os.path.exists(installer.installation_path))
|
||||
addon_name_dir = os.path.join(installer.installation_path, mock_addon.name)
|
||||
self.assertTrue(os.path.exists(addon_name_dir))
|
||||
readme = os.path.join(addon_name_dir, "README.md")
|
||||
self.assertTrue(os.path.exists(readme))
|
||||
|
||||
def test_determine_install_method_local_path(self):
|
||||
"""Test which install methods are accepted for a local path"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.COPY
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
git_manager = initialize_git()
|
||||
if git_manager:
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.GIT
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.GIT)
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.ZIP
|
||||
)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.ANY
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
|
||||
def test_determine_install_method_file_url(self):
|
||||
"""Test which install methods are accepted for a file:// url"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
temp_dir = "file://" + temp_dir.replace(os.path.sep, "/")
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.COPY
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
git_manager = initialize_git()
|
||||
if git_manager:
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.GIT
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.GIT)
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.ZIP
|
||||
)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(
|
||||
temp_dir, InstallationMethod.ANY
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.COPY)
|
||||
|
||||
def test_determine_install_method_local_zip(self):
|
||||
"""Test which install methods are accepted for a local path to a zipfile"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
temp_file = os.path.join(temp_dir, "dummy.zip")
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.COPY
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.GIT
|
||||
)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.ZIP
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.ANY
|
||||
)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
|
||||
def test_determine_install_method_remote_zip(self):
|
||||
"""Test which install methods are accepted for a remote path to a zipfile"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
|
||||
temp_file = "https://freecad.org/dummy.zip" # Doesn't have to actually exist!
|
||||
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.COPY)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.GIT)
|
||||
self.assertIsNone(method)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ZIP)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
method = installer._determine_install_method(temp_file, InstallationMethod.ANY)
|
||||
self.assertEqual(method, InstallationMethod.ZIP)
|
||||
|
||||
def test_determine_install_method_https_known_sites(self):
|
||||
"""Test which install methods are accepted for an https github URL"""
|
||||
|
||||
installer = AddonInstaller(self.mock_addon, [], [])
|
||||
|
||||
for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]:
|
||||
temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist!
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.COPY
|
||||
)
|
||||
self.assertIsNone(method, f"Allowed copying from {site} URL")
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.GIT
|
||||
)
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.GIT,
|
||||
f"Failed to allow git access to {site} URL",
|
||||
)
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.ZIP
|
||||
)
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.ZIP,
|
||||
f"Failed to allow zip access to {site} URL",
|
||||
)
|
||||
method = installer._determine_install_method(
|
||||
temp_file, InstallationMethod.ANY
|
||||
)
|
||||
git_manager = initialize_git()
|
||||
if git_manager:
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.GIT,
|
||||
f"Failed to allow git access to {site} URL",
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
method,
|
||||
InstallationMethod.ZIP,
|
||||
f"Failed to allow zip access to {site} URL",
|
||||
)
|
||||
|
||||
def test_fcmacro_copying(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_addon = MockAddon()
|
||||
mock_addon.url = os.path.join(
|
||||
self.test_data_dir, "test_addon_with_fcmacro.zip"
|
||||
)
|
||||
installer = AddonInstaller(mock_addon, [], [])
|
||||
installer.installation_path = temp_dir
|
||||
installer.macro_installation_path = os.path.join(temp_dir, "Macros")
|
||||
installer.run()
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, "Macros", "TestMacro.FCMacro")),
|
||||
"FCMacro file was not copied to macro installation location",
|
||||
)
|
||||
|
||||
|
||||
class TestMacroInstaller(unittest.TestCase):
|
||||
|
||||
MODULE = "test_installer" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
class MacroMock:
|
||||
def install(self, location: os.PathLike):
|
||||
with open(
|
||||
os.path.join(location, "MACRO_INSTALLATION_TEST"),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
f.write("Test file for macro installation unit tests")
|
||||
return True, []
|
||||
|
||||
class AddonMock:
|
||||
def __init__(self):
|
||||
self.macro = MacroMock()
|
||||
|
||||
self.mock = AddonMock()
|
||||
|
||||
def test_installation(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
installer = MacroInstaller(self.mock)
|
||||
installer.installation_path = temp_dir
|
||||
installation_succeeded = installer.run()
|
||||
self.assertTrue(installation_succeeded)
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, "MACRO_INSTALLATION_TEST"))
|
||||
)
|
||||
@@ -31,12 +31,15 @@ from typing import Dict
|
||||
|
||||
from addonmanager_macro import Macro
|
||||
|
||||
|
||||
class TestMacro(unittest.TestCase):
|
||||
|
||||
MODULE = "test_macro" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = os.path.join(FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data")
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
def test_basic_metadata(self):
|
||||
replacements = {
|
||||
@@ -100,7 +103,7 @@ class TestMacro(unittest.TestCase):
|
||||
if "VERSION" in line:
|
||||
line = "__Version__ = __Date__"
|
||||
output_lines.append(line)
|
||||
with open(outfile,"w") as f:
|
||||
with open(outfile, "w") as f:
|
||||
f.write("\n".join(output_lines))
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
@@ -115,7 +118,7 @@ class TestMacro(unittest.TestCase):
|
||||
if "VERSION" in line:
|
||||
line = "__Version__ = 1.23"
|
||||
output_lines.append(line)
|
||||
with open(outfile,"w") as f:
|
||||
with open(outfile, "w") as f:
|
||||
f.write("\n".join(output_lines))
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
@@ -130,7 +133,7 @@ class TestMacro(unittest.TestCase):
|
||||
if "VERSION" in line:
|
||||
line = "__Version__ = 1"
|
||||
output_lines.append(line)
|
||||
with open(outfile,"w") as f:
|
||||
with open(outfile, "w") as f:
|
||||
f.write("\n".join(output_lines))
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
@@ -153,28 +156,27 @@ static char * blarg_xpm[] = {
|
||||
};"""
|
||||
with open(outfile) as f:
|
||||
contents = f.read()
|
||||
contents += f"\n__xpm__ = \"\"\"{xpm_data}\"\"\"\n"
|
||||
contents += f'\n__xpm__ = """{xpm_data}"""\n'
|
||||
|
||||
with open(outfile,"w") as f:
|
||||
with open(outfile, "w") as f:
|
||||
f.write(contents)
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
self.assertEqual(m.xpm, xpm_data)
|
||||
|
||||
|
||||
def generate_macro_file(self, replacements:Dict[str,str] = {}) -> os.PathLike:
|
||||
with open(os.path.join(self.test_dir,"macro_template.FCStd")) as f:
|
||||
def generate_macro_file(self, replacements: Dict[str, str] = {}) -> os.PathLike:
|
||||
with open(os.path.join(self.test_dir, "macro_template.FCStd")) as f:
|
||||
lines = f.readlines()
|
||||
outfile = tempfile.NamedTemporaryFile(mode="wt",delete=False)
|
||||
outfile = tempfile.NamedTemporaryFile(mode="wt", delete=False)
|
||||
for line in lines:
|
||||
for key,value in replacements.items():
|
||||
line = line.replace(key,value)
|
||||
for key, value in replacements.items():
|
||||
line = line.replace(key, value)
|
||||
|
||||
outfile.write(line)
|
||||
outfile.close()
|
||||
return outfile.name
|
||||
|
||||
def generate_macro(self, replacements:Dict[str,str] = {}) -> Macro:
|
||||
def generate_macro(self, replacements: Dict[str, str] = {}) -> Macro:
|
||||
outfile = self.generate_macro_file(replacements)
|
||||
m = Macro("Unit Test Macro")
|
||||
m.fill_details_from_file(outfile)
|
||||
|
||||
@@ -41,7 +41,9 @@ class TestUtilities(unittest.TestCase):
|
||||
MODULE = "test_utilities" # file name without extension
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = os.path.join(FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data")
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
def test_recognized_git_location(self):
|
||||
recognized_urls = [
|
||||
@@ -51,9 +53,7 @@ class TestUtilities(unittest.TestCase):
|
||||
"https://salsa.debian.org/science-team/freecad",
|
||||
]
|
||||
for url in recognized_urls:
|
||||
repo = Addon(
|
||||
"Test Repo", url, Addon.Status.NOT_INSTALLED, "branch"
|
||||
)
|
||||
repo = Addon("Test Repo", url, Addon.Status.NOT_INSTALLED, "branch")
|
||||
self.assertTrue(
|
||||
recognized_git_location(repo), f"{url} was unexpectedly not recognized"
|
||||
)
|
||||
@@ -65,9 +65,7 @@ class TestUtilities(unittest.TestCase):
|
||||
"https://github.com.malware.com/",
|
||||
]
|
||||
for url in unrecognized_urls:
|
||||
repo = Addon(
|
||||
"Test Repo", url, Addon.Status.NOT_INSTALLED, "branch"
|
||||
)
|
||||
repo = Addon("Test Repo", url, Addon.Status.NOT_INSTALLED, "branch")
|
||||
self.assertFalse(
|
||||
recognized_git_location(repo), f"{url} was unexpectedly recognized"
|
||||
)
|
||||
@@ -90,18 +88,14 @@ class TestUtilities(unittest.TestCase):
|
||||
for url in github_urls:
|
||||
branch = "branchname"
|
||||
expected_result = f"{url}/raw/{branch}/README.md"
|
||||
repo = Addon(
|
||||
"Test Repo", url, Addon.Status.NOT_INSTALLED, branch
|
||||
)
|
||||
repo = Addon("Test Repo", url, Addon.Status.NOT_INSTALLED, branch)
|
||||
actual_result = get_readme_url(repo)
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
for url in gitlab_urls:
|
||||
branch = "branchname"
|
||||
expected_result = f"{url}/-/raw/{branch}/README.md"
|
||||
repo = Addon(
|
||||
"Test Repo", url, Addon.Status.NOT_INSTALLED, branch
|
||||
)
|
||||
repo = Addon("Test Repo", url, Addon.Status.NOT_INSTALLED, branch)
|
||||
actual_result = get_readme_url(repo)
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
@@ -139,4 +133,4 @@ class TestUtilities(unittest.TestCase):
|
||||
|
||||
empty_file = os.path.join(self.test_dir, "missing_macro_metadata.FCStd")
|
||||
version = get_macro_version_from_file(empty_file)
|
||||
self.assertEqual(version, "", "Missing version did not yield empty string")
|
||||
self.assertEqual(version, "", "Missing version did not yield empty string")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
src/Mod/AddonManager/AddonManagerTest/data/test_simple_repo.zip
Normal file
BIN
src/Mod/AddonManager/AddonManagerTest/data/test_simple_repo.zip
Normal file
Binary file not shown.
22
src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py
Normal file
22
src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 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/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
750
src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py
Normal file
750
src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py
Normal file
@@ -0,0 +1,750 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 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 os
|
||||
import tempfile
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class DialogWatcher(QtCore.QObject):
|
||||
def __init__(self, dialog_to_watch_for, button):
|
||||
super().__init__()
|
||||
self.dialog_found = False
|
||||
self.has_run = False
|
||||
self.dialog_to_watch_for = dialog_to_watch_for
|
||||
self.button = button
|
||||
|
||||
def run(self):
|
||||
widget = QtWidgets.QApplication.activeModalWidget()
|
||||
if widget:
|
||||
# Is this the widget we are looking for?
|
||||
if (
|
||||
hasattr(widget, "windowTitle")
|
||||
and callable(widget.windowTitle)
|
||||
and widget.windowTitle() == self.dialog_to_watch_for
|
||||
):
|
||||
# Found the dialog we are looking for: now try to "click" the appropriate button
|
||||
self.click_button(widget)
|
||||
self.dialog_found = True
|
||||
self.has_run = True
|
||||
|
||||
def click_button(self, widget):
|
||||
buttons = widget.findChildren(QtWidgets.QPushButton)
|
||||
for button in buttons:
|
||||
text = button.text().replace("&", "")
|
||||
if text == self.button:
|
||||
button.click()
|
||||
|
||||
|
||||
class DialogInteractor(DialogWatcher):
|
||||
def __init__(self, dialog_to_watch_for, interaction):
|
||||
"""Takes the title of the dialog, a button string, and a callable."""
|
||||
super().__init__(dialog_to_watch_for, None)
|
||||
self.interaction = interaction
|
||||
|
||||
def run(self):
|
||||
widget = QtWidgets.QApplication.activeModalWidget()
|
||||
if widget:
|
||||
# Is this the widget we are looking for?
|
||||
if (
|
||||
hasattr(widget, "windowTitle")
|
||||
and callable(widget.windowTitle)
|
||||
and widget.windowTitle() == self.dialog_to_watch_for
|
||||
):
|
||||
# Found the dialog we are looking for: now try to "click" the appropriate button
|
||||
self.dialog_found = True
|
||||
if self.dialog_found:
|
||||
self.has_run = True
|
||||
if self.interaction is not None and callable(self.interaction):
|
||||
self.interaction(widget)
|
||||
|
||||
|
||||
class TestInstallerGui(unittest.TestCase):
|
||||
|
||||
MODULE = "test_installer_gui" # file name without extension
|
||||
|
||||
class MockAddon:
|
||||
def __init__(self):
|
||||
test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
self.name = "MockAddon"
|
||||
self.display_name = "Mock Addon"
|
||||
self.url = os.path.join(test_dir, "test_simple_repo.zip")
|
||||
self.branch = "main"
|
||||
|
||||
def setUp(self):
|
||||
self.addon_to_install = TestInstallerGui.MockAddon()
|
||||
self.installer_gui = AddonInstallerGUI(self.addon_to_install)
|
||||
self.finalized_thread = False
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_success_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to an OK click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Success"), translate("AddonsInstaller", "OK")
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._installation_succeeded()
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_failure_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a Cancel click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Installation Failed"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._installation_failed(
|
||||
self.addon_to_install, "Test of installation failure"
|
||||
)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_no_python_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot execute Python"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_no_python_exe()
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_no_pip_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot execute pip"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_no_pip("pip not actually run, this was a test")
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_dependency_failure_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Package installation failed"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_dependency_failure(
|
||||
"Unit test", "Nothing really failed, this is a test of the dialog box"
|
||||
)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_install(self):
|
||||
# Run the installation code and make sure it puts the directory in place
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.installer_gui.installer.installation_path = temp_dir
|
||||
self.installer_gui.install() # This does not block
|
||||
self.installer_gui.installer.success.disconnect(
|
||||
self.installer_gui._installation_succeeded
|
||||
)
|
||||
self.installer_gui.installer.failure.disconnect(
|
||||
self.installer_gui._installation_failed
|
||||
)
|
||||
while not self.installer_gui.worker_thread.isFinished():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, "MockAddon")),
|
||||
"Installed directory not found",
|
||||
)
|
||||
|
||||
def test_handle_disallowed_python(self):
|
||||
disallowed_packages = ["disallowed_package_name"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._handle_disallowed_python(disallowed_packages)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_handle_disallowed_python_long_list(self):
|
||||
"""A separate test for when there are MANY packages, which takes a separate code path."""
|
||||
disallowed_packages = []
|
||||
for i in range(50):
|
||||
disallowed_packages.append(f"disallowed_package_name_{i}")
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._handle_disallowed_python(disallowed_packages)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_report_missing_workbenches_single(self):
|
||||
"""Test only missing one workbench"""
|
||||
wbs = ["OneMissingWorkbench"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_missing_workbenches(wbs)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_report_missing_workbenches_multiple(self):
|
||||
"""Test only missing one workbench"""
|
||||
wbs = ["FirstMissingWorkbench", "SecondMissingWorkbench"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_missing_workbenches(wbs)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_resolve_dependencies_then_install(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.external_addons = ["addon_1", "addon_2"]
|
||||
self.python_requires = ["py_req_1", "py_req_2"]
|
||||
self.python_optional = ["py_opt_1", "py_opt_2"]
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Resolve Dependencies"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._resolve_dependencies_then_install(missing)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_check_python_version_bad(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_min_version = {"major": 3, "minor": 9999}
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Incompatible Python version"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
stop_installing = self.installer_gui._check_python_version(missing)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
self.assertTrue(
|
||||
stop_installing, "Failed to halt installation on bad Python version"
|
||||
)
|
||||
|
||||
def test_check_python_version_good(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_min_version = {"major": 3, "minor": 0}
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
stop_installing = self.installer_gui._check_python_version(missing)
|
||||
self.assertFalse(
|
||||
stop_installing, "Failed to continue installation on good Python version"
|
||||
)
|
||||
|
||||
def test_clean_up_optional(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_optional = [
|
||||
"allowed_packages_1",
|
||||
"allowed_packages_2",
|
||||
"disallowed_package",
|
||||
]
|
||||
|
||||
allowed_packages = ["allowed_packages_1", "allowed_packages_2"]
|
||||
missing = MissingDependenciesMock()
|
||||
self.installer_gui.installer.allowed_packages = set(allowed_packages)
|
||||
self.installer_gui._clean_up_optional(missing)
|
||||
self.assertTrue("allowed_packages_1" in missing.python_optional)
|
||||
self.assertTrue("allowed_packages_2" in missing.python_optional)
|
||||
self.assertFalse("disallowed_package" in missing.python_optional)
|
||||
|
||||
def intercept_run_dependency_installer(
|
||||
self, addons, python_requires, python_optional
|
||||
):
|
||||
self.assertEqual(python_requires, ["py_req_1", "py_req_2"])
|
||||
self.assertEqual(python_optional, ["py_opt_1", "py_opt_2"])
|
||||
self.assertEqual(addons[0].name, "addon_1")
|
||||
self.assertEqual(addons[1].name, "addon_2")
|
||||
|
||||
def test_dependency_dialog_yes_clicked(self):
|
||||
class DialogMock:
|
||||
class ListWidgetMock:
|
||||
class ListWidgetItemMock:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def text(self):
|
||||
return self.name
|
||||
|
||||
def checkState(self):
|
||||
return QtCore.Qt.Checked
|
||||
|
||||
def __init__(self, items):
|
||||
self.list = []
|
||||
for item in items:
|
||||
self.list.append(
|
||||
DialogMock.ListWidgetMock.ListWidgetItemMock(item)
|
||||
)
|
||||
|
||||
def count(self):
|
||||
return len(self.list)
|
||||
|
||||
def item(self, i):
|
||||
return self.list[i]
|
||||
|
||||
def __init__(self):
|
||||
self.listWidgetAddons = DialogMock.ListWidgetMock(
|
||||
["addon_1", "addon_2"]
|
||||
)
|
||||
self.listWidgetPythonRequired = DialogMock.ListWidgetMock(
|
||||
["py_req_1", "py_req_2"]
|
||||
)
|
||||
self.listWidgetPythonOptional = DialogMock.ListWidgetMock(
|
||||
["py_opt_1", "py_opt_2"]
|
||||
)
|
||||
|
||||
class AddonMock:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
self.installer_gui.dependency_dialog = DialogMock()
|
||||
self.installer_gui.addons = [AddonMock("addon_1"), AddonMock("addon_2")]
|
||||
self.installer_gui._run_dependency_installer = (
|
||||
self.intercept_run_dependency_installer
|
||||
)
|
||||
self.installer_gui._dependency_dialog_yes_clicked()
|
||||
|
||||
|
||||
class TestMacroInstallerGui(unittest.TestCase):
|
||||
class MockMacroAddon:
|
||||
class MockMacro:
|
||||
def __init__(self):
|
||||
self.install_called = False
|
||||
self.install_result = (
|
||||
True # External code can change to False to test failed install
|
||||
)
|
||||
self.name = "MockMacro"
|
||||
self.filename = "mock_macro_no_real_file.FCMacro"
|
||||
self.comment = "This is a mock macro for unit testing"
|
||||
self.icon = None
|
||||
self.xpm = None
|
||||
|
||||
def install(self):
|
||||
self.install_called = True
|
||||
return self.install_result
|
||||
|
||||
def __init__(self):
|
||||
self.macro = TestMacroInstallerGui.MockMacroAddon.MockMacro()
|
||||
self.name = self.macro.name
|
||||
self.display_name = self.macro.name
|
||||
|
||||
class MockParameter:
|
||||
"""Mock the parameter group to allow simplified behavior and introspection."""
|
||||
|
||||
def __init__(self):
|
||||
self.params = {}
|
||||
self.groups = {}
|
||||
self.accessed_parameters = {} # Dict is param name: default value
|
||||
|
||||
types = ["Bool", "String", "Int", "UInt", "Float"]
|
||||
for t in types:
|
||||
setattr(self, f"Get{t}", self.get)
|
||||
setattr(self, f"Set{t}", self.set)
|
||||
setattr(self, f"Rem{t}", self.rem)
|
||||
|
||||
def get(self, p, default=None):
|
||||
self.accessed_parameters[p] = default
|
||||
if p in self.params:
|
||||
return self.params[p]
|
||||
else:
|
||||
return default
|
||||
|
||||
def set(self, p, value):
|
||||
self.params[p] = value
|
||||
|
||||
def rem(self, p):
|
||||
if p in self.params:
|
||||
self.params.erase(p)
|
||||
|
||||
def GetGroup(self, name):
|
||||
if name not in self.groups:
|
||||
self.groups[name] = TestMacroInstallerGui.MockParameter()
|
||||
return self.groups[name]
|
||||
|
||||
def GetGroups(self):
|
||||
return self.groups.keys()
|
||||
|
||||
class ToolbarIntercepter:
|
||||
def __init__(self):
|
||||
self.ask_for_toolbar_called = False
|
||||
self.install_macro_to_toolbar_called = False
|
||||
self.tb = None
|
||||
self.custom_group = TestMacroInstallerGui.MockParameter()
|
||||
self.custom_group.set("Name", "MockCustomToolbar")
|
||||
|
||||
def _ask_for_toolbar(self, _):
|
||||
self.ask_for_toolbar_called = True
|
||||
return self.custom_group
|
||||
|
||||
def _install_macro_to_toolbar(self, tb):
|
||||
self.install_macro_to_toolbar_called = True
|
||||
self.tb = tb
|
||||
|
||||
class InstallerInterceptor:
|
||||
def __init__(self):
|
||||
self.ccc_called = False
|
||||
|
||||
def _create_custom_command(
|
||||
self,
|
||||
toolbar,
|
||||
filename,
|
||||
menuText,
|
||||
tooltipText,
|
||||
whatsThisText,
|
||||
statustipText,
|
||||
pixmapText,
|
||||
):
|
||||
self.ccc_called = True
|
||||
self.toolbar = toolbar
|
||||
self.filename = filename
|
||||
self.menuText = menuText
|
||||
self.tooltipText = tooltipText
|
||||
self.whatsThisText = whatsThisText
|
||||
self.statustipText = statustipText
|
||||
self.pixmapText = pixmapText
|
||||
|
||||
def setUp(self):
|
||||
self.mock_macro = TestMacroInstallerGui.MockMacroAddon()
|
||||
self.installer = MacroInstallerGUI(self.mock_macro)
|
||||
self.installer.addon_params = TestMacroInstallerGui.MockParameter()
|
||||
self.installer.toolbar_params = TestMacroInstallerGui.MockParameter()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_ask_for_toolbar_no_dialog_default_exists(self):
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", False)
|
||||
self.installer.addon_params.set("CustomToolbarName", "UnitTestCustomToolbar")
|
||||
utct = self.installer.toolbar_params.GetGroup("UnitTestCustomToolbar")
|
||||
utct.set("Name", "UnitTestCustomToolbar")
|
||||
utct.set("Active", True)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UnitTestCustomToolbar")
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_cancelled(self):
|
||||
|
||||
# First test: the user cancels the dialog
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", True)
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_defaults(self):
|
||||
|
||||
# Second test: the user leaves the dialog at all default values, so:
|
||||
# - The checkbox "Ask every time" is unchecked
|
||||
# - The selected toolbar option is "Create new toolbar", which triggers a search for
|
||||
# a new custom toolbar name by calling _create_new_custom_toolbar, which we mock.
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
fake_custom_toolbar_group = TestMacroInstallerGui.MockParameter()
|
||||
fake_custom_toolbar_group.set("Name", "UnitTestCustomToolbar")
|
||||
self.installer._create_new_custom_toolbar = lambda: fake_custom_toolbar_group
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
translate("AddonsInstaller", "OK"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UnitTestCustomToolbar")
|
||||
self.assertIn("alwaysAskForToolbar", self.installer.addon_params.params)
|
||||
self.assertFalse(self.installer.addon_params.get("alwaysAskForToolbar", True))
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_selection(self):
|
||||
|
||||
# Third test: the user selects a custom toolbar in the dialog, and checks the box to always
|
||||
# ask.
|
||||
dialog_interactor = DialogInteractor(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
self.interactor_selection_option_and_checkbox,
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_interactor.run)
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
result = self.installer._ask_for_toolbar(["UT_TB_1", "UT_TB_2", "UT_TB_3"])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UT_TB_3")
|
||||
self.assertIn("alwaysAskForToolbar", self.installer.addon_params.params)
|
||||
self.assertTrue(self.installer.addon_params.get("alwaysAskForToolbar", False))
|
||||
|
||||
def interactor_selection_option_and_checkbox(self, parent):
|
||||
|
||||
boxes = parent.findChildren(QtWidgets.QComboBox)
|
||||
self.assertEqual(len(boxes), 1) # Just to make sure...
|
||||
box = boxes[0]
|
||||
box.setCurrentIndex(box.count() - 2) # Select the last thing but one
|
||||
|
||||
checkboxes = parent.findChildren(QtWidgets.QCheckBox)
|
||||
self.assertEqual(len(checkboxes), 1) # Just to make sure...
|
||||
checkbox = checkboxes[0]
|
||||
checkbox.setChecked(True)
|
||||
|
||||
parent.accept()
|
||||
|
||||
def test_macro_button_exists_no_command(self):
|
||||
# Test 1: No command for this macro
|
||||
self.installer._find_custom_command = lambda _: None
|
||||
button_exists = self.installer._macro_button_exists()
|
||||
self.assertFalse(button_exists)
|
||||
|
||||
def test_macro_button_exists_true(self):
|
||||
# Test 2: Macro is in the list of buttons
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestCommand")
|
||||
ut_tb_1.set(
|
||||
"UnitTestCommand", "FreeCAD"
|
||||
) # This is what the real thing looks like...
|
||||
self.installer._find_custom_command = lambda _: "UnitTestCommand"
|
||||
self.assertTrue(self.installer._macro_button_exists())
|
||||
|
||||
def test_macro_button_exists_false(self):
|
||||
# Test 3: Macro is not in the list of buttons
|
||||
self.installer._find_custom_command = lambda _: "UnitTestCommand"
|
||||
self.assertFalse(self.installer._macro_button_exists())
|
||||
|
||||
def test_ask_to_install_toolbar_button_disabled(self):
|
||||
self.installer.addon_params.SetBool("dontShowAddMacroButtonDialog", True)
|
||||
self.installer._ask_to_install_toolbar_button()
|
||||
# This should NOT block when dontShowAddMacroButtonDialog is True
|
||||
|
||||
def test_ask_to_install_toolbar_button_enabled_no(self):
|
||||
self.installer.addon_params.SetBool("dontShowAddMacroButtonDialog", False)
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Add button?"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer._ask_to_install_toolbar_button() # Blocks until killed by watcher
|
||||
self.assertTrue(dialog_watcher.dialog_found)
|
||||
|
||||
def test_get_toolbar_with_name_found(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestToolbar")
|
||||
ut_tb_1.set("Name", "Unit Test Toolbar")
|
||||
ut_tb_1.set("UnitTestParam", True)
|
||||
tb = self.installer._get_toolbar_with_name("Unit Test Toolbar")
|
||||
self.assertIsNotNone(tb)
|
||||
self.assertTrue(tb.get("UnitTestParam", False))
|
||||
|
||||
def test_get_toolbar_with_name_not_found(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestToolbar")
|
||||
ut_tb_1.set("Name", "Not the Unit Test Toolbar")
|
||||
tb = self.installer._get_toolbar_with_name("Unit Test Toolbar")
|
||||
self.assertIsNone(tb)
|
||||
|
||||
def test_create_new_custom_toolbar_no_existing(self):
|
||||
tb = self.installer._create_new_custom_toolbar()
|
||||
self.assertEqual(tb.get("Name", ""), "Auto-Created Macro Toolbar")
|
||||
self.assertTrue(tb.get("Active", False), True)
|
||||
|
||||
def test_create_new_custom_toolbar_one_existing(self):
|
||||
_ = self.installer._create_new_custom_toolbar()
|
||||
tb = self.installer._create_new_custom_toolbar()
|
||||
self.assertEqual(tb.get("Name", ""), "Auto-Created Macro Toolbar (2)")
|
||||
self.assertTrue(tb.get("Active", False), True)
|
||||
|
||||
def test_check_for_toolbar_true(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.assertTrue(self.installer._check_for_toolbar("UT_TB_1"))
|
||||
|
||||
def test_check_for_toolbar_false(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.assertFalse(self.installer._check_for_toolbar("Not UT_TB_1"))
|
||||
|
||||
def test_install_toolbar_button_first_custom_toolbar(self):
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertTrue("Custom_1" in self.installer.toolbar_params.GetGroups())
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_1(self):
|
||||
# There is an existing custom toolbar, and we should use it
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_1")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "UT_TB_1")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_2(self):
|
||||
# There are multiple existing custom toolbars, and we should use one of them
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_3")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "UT_TB_3")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_3(self):
|
||||
# There are multiple existing custom toolbars, but none of them match
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_4")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertTrue(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "MockCustomToolbar")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_4(self):
|
||||
# There are multiple existing custom toolbars, one of them matches, but we have set
|
||||
# "alwaysAskForToolbar" to True
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_3")
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", True)
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertTrue(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "MockCustomToolbar")
|
||||
|
||||
def test_install_macro_to_toolbar_icon_abspath(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.NamedTemporaryFile() as ntf:
|
||||
self.mock_macro.macro.icon = ntf.name
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, ntf.name)
|
||||
|
||||
def test_install_macro_to_toolbar_icon_relpath(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.mock_macro.macro.icon = "RelativeIconPath.png"
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, os.path.join(td, "RelativeIconPath.png"))
|
||||
|
||||
def test_install_macro_to_toolbar_xpm(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.mock_macro.macro.xpm = "Not really xpm data, don't try to use it!"
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, os.path.join(td, "MockMacro_icon.xpm"))
|
||||
self.assertTrue(os.path.exists(os.path.join(td, "MockMacro_icon.xpm")))
|
||||
|
||||
def test_install_macro_to_toolbar_no_icon(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertIsNone(ii.pixmapText)
|
||||
246
src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py
Normal file
246
src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py
Normal file
@@ -0,0 +1,246 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 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 time import sleep
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
from Addon import Addon
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
from addonmanager_update_all_gui import UpdateAllGUI, AddonStatus
|
||||
|
||||
|
||||
class MockUpdater(QtCore.QObject):
|
||||
success = QtCore.Signal(object)
|
||||
failure = QtCore.Signal(object)
|
||||
finished = QtCore.Signal()
|
||||
|
||||
def __init__(self, addon, addons=[]):
|
||||
super().__init__()
|
||||
self.addon_to_install = addon
|
||||
self.addons = addons
|
||||
self.has_run = False
|
||||
self.emit_success = True
|
||||
self.work_function = (
|
||||
None # Set to some kind of callable to make this function take time
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.has_run = True
|
||||
if self.work_function is not None and callable(self.work_function):
|
||||
self.work_function()
|
||||
if self.emit_success:
|
||||
self.success.emit(self.addon_to_install)
|
||||
else:
|
||||
self.failure.emit(self.addon_to_install)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class MockUpdaterFactory:
|
||||
def __init__(self, addons):
|
||||
self.addons = addons
|
||||
self.work_function = None
|
||||
self.updater = None
|
||||
|
||||
def get_updater(self, addon):
|
||||
self.updater = MockUpdater(addon, self.addons)
|
||||
self.updater.work_function = self.work_function
|
||||
return self.updater
|
||||
|
||||
|
||||
class MockAddon:
|
||||
def __init__(self, name):
|
||||
self.display_name = name
|
||||
self.name = name
|
||||
self.macro = None
|
||||
|
||||
def status(self):
|
||||
return Addon.Status.UPDATE_AVAILABLE
|
||||
|
||||
|
||||
class CallInterceptor:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.args = None
|
||||
|
||||
def intercept(self, *args):
|
||||
self.called = True
|
||||
self.args = args
|
||||
|
||||
|
||||
class TestUpdateAllGui(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.addons = []
|
||||
for i in range(3):
|
||||
self.addons.append(MockAddon(f"Mock Addon {i}"))
|
||||
self.factory = MockUpdaterFactory(self.addons)
|
||||
self.test_object = UpdateAllGUI(self.addons)
|
||||
self.test_object.updater_factory = self.factory
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_run(self):
|
||||
self.factory.work_function = lambda: sleep(0.1)
|
||||
self.test_object.run()
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
|
||||
def test_setup_dialog(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.assertIsNotNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
)
|
||||
self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 3)
|
||||
|
||||
def test_cancelling_installation(self):
|
||||
self.factory.work_function = lambda: sleep(0.1)
|
||||
self.test_object.run()
|
||||
cancel_timer = QtCore.QTimer()
|
||||
cancel_timer.timeout.connect(
|
||||
self.test_object.dialog.buttonBox.button(
|
||||
QtWidgets.QDialogButtonBox.Cancel
|
||||
).click
|
||||
)
|
||||
cancel_timer.start(90)
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 10)
|
||||
self.assertGreater(len(self.test_object.addons_with_update), 0)
|
||||
|
||||
def test_add_addon_to_table(self):
|
||||
mock_addon = MockAddon("MockAddon")
|
||||
self.test_object.dialog.tableWidget.clear()
|
||||
self.test_object._add_addon_to_table(mock_addon)
|
||||
self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 1)
|
||||
|
||||
def test_update_addon_status(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_addon_status(0, AddonStatus.WAITING)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.WAITING.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.INSTALLING)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.SUCCEEDED)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.SUCCEEDED.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.FAILED)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.FAILED.ui_string(),
|
||||
)
|
||||
|
||||
def test_process_next_update(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._launch_active_installer = lambda: None
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(1, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(2, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
|
||||
def test_launch_active_installer(self):
|
||||
self.test_object.active_installer = self.factory.get_updater(self.addons[0])
|
||||
self.test_object._update_succeeded = lambda _: None
|
||||
self.test_object._update_failed = lambda _: None
|
||||
self.test_object.process_next_update = lambda: None
|
||||
self.test_object._launch_active_installer()
|
||||
# The above call does not block, so spin until it has completed (basically instantly in testing)
|
||||
while self.test_object.worker_thread.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
|
||||
def test_update_succeeded(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_succeeded(self.addons[0])
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.SUCCEEDED.ui_string(),
|
||||
)
|
||||
|
||||
def test_update_failed(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_failed(self.addons[0])
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.FAILED.ui_string(),
|
||||
)
|
||||
|
||||
def test_update_finished(self):
|
||||
self.test_object._setup_dialog()
|
||||
call_interceptor = CallInterceptor()
|
||||
self.test_object.worker_thread = QtCore.QThread()
|
||||
self.test_object.worker_thread.start()
|
||||
self.test_object._process_next_update = call_interceptor.intercept
|
||||
self.test_object.active_installer = self.factory.get_updater(self.addons[0])
|
||||
self.test_object._update_finished()
|
||||
self.assertFalse(self.test_object.worker_thread.isRunning())
|
||||
self.test_object.worker_thread.terminate()
|
||||
self.assertTrue(call_interceptor.called)
|
||||
self.test_object.worker_thread.wait()
|
||||
|
||||
def test_finalize(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object.worker_thread = QtCore.QThread()
|
||||
self.test_object.worker_thread.start()
|
||||
self.test_object._finalize()
|
||||
self.assertFalse(self.test_object.worker_thread.isRunning())
|
||||
self.test_object.worker_thread.terminate()
|
||||
self.test_object.worker_thread.wait()
|
||||
self.assertFalse(self.test_object.running)
|
||||
self.assertIsNotNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Close)
|
||||
)
|
||||
self.assertIsNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
)
|
||||
|
||||
def test_is_running(self):
|
||||
self.assertFalse(self.test_object.is_running())
|
||||
self.test_object.run()
|
||||
self.assertTrue(self.test_object.is_running())
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
@@ -1,211 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of the FreeCAD CAx development system. *
|
||||
# * *
|
||||
# * This library 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. *
|
||||
# * *
|
||||
# * This library 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 this library; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
# * 02110-1301 USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
import unittest
|
||||
import FreeCAD
|
||||
from addonmanager_git import initialize_git
|
||||
|
||||
from PySide2 import QtCore
|
||||
|
||||
import NetworkManager
|
||||
from Addon import Addon
|
||||
from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
UpdateChecker,
|
||||
)
|
||||
from addonmanager_workers_installation import InstallWorkbenchWorker
|
||||
|
||||
|
||||
class TestWorkersInstallation(unittest.TestCase):
|
||||
|
||||
MODULE = "test_workers_installation" # file name without extension
|
||||
|
||||
addon_list = (
|
||||
[]
|
||||
) # Cache at the class level so only the first test has to download it
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test"""
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
self.saved_mod_directory = Addon.mod_directory
|
||||
self.saved_cache_directory = Addon.cache_directory
|
||||
Addon.mod_directory = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Mod"
|
||||
)
|
||||
Addon.cache_directory = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Cache"
|
||||
)
|
||||
|
||||
os.makedirs(Addon.mod_directory, mode=0o777, exist_ok=True)
|
||||
os.makedirs(Addon.cache_directory, mode=0o777, exist_ok=True)
|
||||
|
||||
url = "https://api.github.com/zen"
|
||||
NetworkManager.InitializeNetworkManager()
|
||||
result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
|
||||
if result is None:
|
||||
self.skipTest("No active internet connection detected")
|
||||
|
||||
self.macro_counter = 0
|
||||
self.workbench_counter = 0
|
||||
self.prefpack_counter = 0
|
||||
self.addon_from_cache_counter = 0
|
||||
self.macro_from_cache_counter = 0
|
||||
|
||||
self.package_cache = {}
|
||||
self.macro_cache = []
|
||||
|
||||
self.package_cache_filename = os.path.join(
|
||||
Addon.cache_directory, "packages.json"
|
||||
)
|
||||
self.macro_cache_filename = os.path.join(Addon.cache_directory, "macros.json")
|
||||
|
||||
if not TestWorkersInstallation.addon_list:
|
||||
self._create_addon_list()
|
||||
|
||||
# Workbench: use the FreeCAD-Help workbench for testing purposes
|
||||
self.help_addon = None
|
||||
for addon in self.addon_list:
|
||||
if addon.name == "Help":
|
||||
self.help_addon = addon
|
||||
break
|
||||
if not self.help_addon:
|
||||
print("Unable to locate the FreeCAD-Help addon to test with")
|
||||
self.skipTest("No active internet connection detected")
|
||||
|
||||
# Store the user's preference for whether git is enabled or disabled
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
self.saved_git_disabled_status = pref.GetBool("disableGit", False)
|
||||
|
||||
def tearDown(self):
|
||||
mod_dir = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
if os.path.exists(mod_dir):
|
||||
self._rmdir(mod_dir)
|
||||
macro_dir = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
if os.path.exists(macro_dir):
|
||||
self._rmdir(macro_dir)
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetBool("disableGit", self.saved_git_disabled_status)
|
||||
|
||||
def test_workbench_installation(self):
|
||||
addon_location = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Mod", self.help_addon.name
|
||||
)
|
||||
worker = InstallWorkbenchWorker(self.help_addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
self.assertTrue(os.path.exists(addon_location))
|
||||
self.assertTrue(os.path.exists(os.path.join(addon_location, "package.xml")))
|
||||
|
||||
def test_workbench_installation_git_disabled(self):
|
||||
"""If the testing user has git enabled, also test the addon manager with git disabled"""
|
||||
if self.saved_git_disabled_status:
|
||||
self.skipTest("Git is disabled, this test is redundant")
|
||||
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetBool("disableGit", True)
|
||||
|
||||
self.test_workbench_installation()
|
||||
|
||||
pref.SetBool("disableGit", False)
|
||||
|
||||
def test_workbench_update_checker(self):
|
||||
|
||||
git_manager = initialize_git()
|
||||
|
||||
if not git_manager:
|
||||
return
|
||||
|
||||
# Workbench: use the FreeCAD-Help workbench for testing purposes
|
||||
help_addon = None
|
||||
for addon in self.addon_list:
|
||||
if addon.name == "Help":
|
||||
help_addon = addon
|
||||
break
|
||||
if not help_addon:
|
||||
print("Unable to locate the FreeCAD-Help addon to test with")
|
||||
return
|
||||
|
||||
addon_location = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Mod", self.help_addon.name
|
||||
)
|
||||
worker = InstallWorkbenchWorker(addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
self.assertEqual(help_addon.status(), Addon.Status.PENDING_RESTART)
|
||||
|
||||
# Back up one revision
|
||||
git_manager.reset(addon_location, ["--hard", "HEAD~1"])
|
||||
|
||||
# At this point the addon should be "out of date", checked out to one revision behind
|
||||
# the most recent.
|
||||
|
||||
worker = UpdateChecker()
|
||||
worker.override_mod_directory(
|
||||
os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
)
|
||||
worker.check_workbench(help_addon) # Synchronous call
|
||||
self.assertEqual(help_addon.status(), Addon.Status.UPDATE_AVAILABLE)
|
||||
|
||||
# Now try to "update" it (which is really done via the install worker)
|
||||
worker = InstallWorkbenchWorker(addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
self.assertEqual(help_addon.status(), Addon.Status.PENDING_RESTART)
|
||||
|
||||
def _rmdir(self, path):
|
||||
try:
|
||||
shutil.rmtree(path, onerror=self._remove_readonly)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def _remove_readonly(self, func, path, _) -> None:
|
||||
"""Remove a read-only file."""
|
||||
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
def _create_addon_list(self):
|
||||
"""Create the list of addons"""
|
||||
worker = CreateAddonListWorker()
|
||||
worker.addon_repo.connect(self._addon_added)
|
||||
worker.start()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
def _addon_added(self, addon: Addon):
|
||||
"""Callback for adding an Addon: tracks the list, and counts the various types"""
|
||||
print(f"Addon added: {addon.name}")
|
||||
TestWorkersInstallation.addon_list.append(addon)
|
||||
if addon.contains_workbench():
|
||||
self.workbench_counter += 1
|
||||
if addon.contains_macro():
|
||||
self.macro_counter += 1
|
||||
if addon.contains_preference_pack():
|
||||
self.prefpack_counter += 1
|
||||
@@ -27,8 +27,6 @@ import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from addonmanager_git import initialize_git
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from PySide2 import QtCore
|
||||
@@ -39,11 +37,6 @@ from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
LoadPackagesFromCacheWorker,
|
||||
LoadMacrosFromCacheWorker,
|
||||
CheckSingleUpdateWorker,
|
||||
)
|
||||
|
||||
from addonmanager_workers_installation import (
|
||||
InstallWorkbenchWorker,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,7 +124,6 @@ class TestWorkersStartup(unittest.TestCase):
|
||||
f.write(json.dumps(self.macro_cache, indent=" "))
|
||||
|
||||
original_macro_counter = self.macro_counter
|
||||
original_workbench_counter = self.workbench_counter
|
||||
original_addon_list = self.addon_list.copy()
|
||||
self.macro_counter = 0
|
||||
self.workbench_counter = 0
|
||||
|
||||
Reference in New Issue
Block a user