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:
Chris Hennes
2022-11-18 18:51:04 -06:00
parent 403e0dc477
commit 89c191e160
46 changed files with 4012 additions and 1666 deletions

View File

@@ -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",
)

View File

@@ -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

View File

@@ -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:

View 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"))
)

View File

@@ -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)

View File

@@ -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")

View 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/>. *
# * *
# ***************************************************************************

View 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)

View 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()

View File

@@ -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

View File

@@ -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