# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * 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 * # * . * # * * # *************************************************************************** """Contains the unit test class for addonmanager_installer.py non-GUI functionality.""" import unittest from unittest.mock import Mock import os import shutil import tempfile from zipfile import ZipFile import sys sys.path.append("../../") # So the IDE can find the imports below import FreeCAD from addonmanager_installer import InstallationMethod, AddonInstaller, MacroInstaller from addonmanager_git import GitManager, initialize_git from addonmanager_metadata import MetadataReader from Addon import Addon from AddonManagerTest.app.mocks import MockAddon, MockMacro 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.""" addon = Mock() addon.name = "MockAddon" installer = AddonInstaller(addon, []) 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.real_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 = MetadataReader.from_file(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_non_github(self): """Ensure that zip files 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[:-4] 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") def test_finalize_zip_installation_github(self): with tempfile.TemporaryDirectory() as temp_dir: test_github_style_repo = os.path.join(self.test_data_dir, "test_github_style_repo.zip") self.mock_addon.url = "https://github.com/something/test_github_style_repo" self.mock_addon.branch = "master" 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_code_in_branch_subdirectory_true(self): """When there is a subdirectory with the branch name in it, find it""" self.mock_addon.url = "https://something.com/something_else/something" installer = AddonInstaller(self.mock_addon, []) with tempfile.TemporaryDirectory() as temp_dir: os.mkdir(os.path.join(temp_dir, f"something-{self.mock_addon.branch}")) result = installer._code_in_branch_subdirectory(temp_dir) self.assertTrue(result, "Failed to find ZIP subdirectory") def test_code_in_branch_subdirectory_false(self): """When there is not a subdirectory with the branch name in it, don't find one""" installer = AddonInstaller(self.mock_addon, []) with tempfile.TemporaryDirectory() as temp_dir: result = installer._code_in_branch_subdirectory(temp_dir) self.assertFalse(result, "Found ZIP subdirectory when there was none") def test_code_in_branch_subdirectory_more_than_one(self): """When there are multiple subdirectories, never find a branch subdirectory""" installer = AddonInstaller(self.mock_addon, []) with tempfile.TemporaryDirectory() as temp_dir: os.mkdir(os.path.join(temp_dir, f"{self.mock_addon.name}-{self.mock_addon.branch}")) os.mkdir(os.path.join(temp_dir, "AnotherSubdir")) result = installer._code_in_branch_subdirectory(temp_dir) self.assertFalse(result, "Found ZIP subdirectory when there were multiple subdirs") def test_move_code_out_of_subdirectory(self): """All files are moved out and the subdirectory is deleted""" self.mock_addon.url = "https://something.com/something_else/something" installer = AddonInstaller(self.mock_addon, []) with tempfile.TemporaryDirectory() as temp_dir: subdir = os.path.join(temp_dir, f"something-{self.mock_addon.branch}") os.mkdir(subdir) with open(os.path.join(subdir, "README.txt"), "w", encoding="utf-8") as f: f.write("# Test file for unit testing") with open(os.path.join(subdir, "AnotherFile.txt"), "w", encoding="utf-8") as f: f.write("# Test file for unit testing") installer._move_code_out_of_subdirectory(temp_dir) self.assertTrue(os.path.isfile(os.path.join(temp_dir, "README.txt"))) self.assertTrue(os.path.isfile(os.path.join(temp_dir, "AnotherFile.txt"))) self.assertFalse(os.path.isdir(subdir)) 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_copy(self): """Test which install methods are accepted for an https GitHub URL""" installer = AddonInstaller(self.mock_addon, []) installer.git_manager = True for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]: with self.subTest(site=site): 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") def test_determine_install_method_https_known_sites_git(self): """Test which install methods are accepted for an https GitHub URL""" installer = AddonInstaller(self.mock_addon, []) installer.git_manager = True for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]: with self.subTest(site=site): temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist! method = installer._determine_install_method(temp_file, InstallationMethod.GIT) self.assertEqual( method, InstallationMethod.GIT, f"Failed to allow git access to {site} URL", ) def test_determine_install_method_https_known_sites_zip(self): """Test which install methods are accepted for an https GitHub URL""" installer = AddonInstaller(self.mock_addon, []) installer.git_manager = True for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]: with self.subTest(site=site): temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist! method = installer._determine_install_method(temp_file, InstallationMethod.ZIP) self.assertEqual( method, InstallationMethod.ZIP, f"Failed to allow zip access to {site} URL", ) def test_determine_install_method_https_known_sites_any_gm(self): """Test which install methods are accepted for an https GitHub URL""" installer = AddonInstaller(self.mock_addon, []) installer.git_manager = True for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]: with self.subTest(site=site): temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist! method = installer._determine_install_method(temp_file, InstallationMethod.ANY) self.assertEqual( method, InstallationMethod.GIT, f"Failed to allow git access to {site} URL", ) def test_determine_install_method_https_known_sites_any_no_gm(self): """Test which install methods are accepted for an https GitHub URL""" installer = AddonInstaller(self.mock_addon, []) installer.git_manager = None for site in ["github.org", "gitlab.org", "framagit.org", "salsa.debian.org"]: with self.subTest(site=site): temp_file = f"https://{site}/dummy/dummy" # Doesn't have to actually exist! method = installer._determine_install_method(temp_file, InstallationMethod.ANY) 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): """Set up the mock objects""" self.mock = MockAddon() self.mock.macro = MockMacro() def test_installation(self): """Test the wrapper around the macro installer""" # Note that this doesn't test the underlying Macro object's install function, # it only tests whether that function is called appropriately by the # MacroInstaller wrapper. 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, self.mock.macro.filename)))