FreeCAD
437 строк · 21.3 Кб
1# SPDX-License-Identifier: LGPL-2.1-or-later
2# ***************************************************************************
3# * *
4# * Copyright (c) 2022 FreeCAD Project Association *
5# * *
6# * This file is part of FreeCAD. *
7# * *
8# * FreeCAD is free software: you can redistribute it and/or modify it *
9# * under the terms of the GNU Lesser General Public License as *
10# * published by the Free Software Foundation, either version 2.1 of the *
11# * License, or (at your option) any later version. *
12# * *
13# * FreeCAD is distributed in the hope that it will be useful, but *
14# * WITHOUT ANY WARRANTY; without even the implied warranty of *
15# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16# * Lesser General Public License for more details. *
17# * *
18# * You should have received a copy of the GNU Lesser General Public *
19# * License along with FreeCAD. If not, see *
20# * <https://www.gnu.org/licenses/>. *
21# * *
22# ***************************************************************************
23
24"""Contains the unit test class for addonmanager_uninstaller.py non-GUI functionality."""
25
26import functools27import os28from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR29import tempfile30import unittest31
32import FreeCAD33
34from addonmanager_uninstaller import AddonUninstaller, MacroUninstaller35
36from Addon import Addon37from AddonManagerTest.app.mocks import MockAddon, MockMacro38
39
40class TestAddonUninstaller(unittest.TestCase):41"""Test class for addonmanager_uninstaller.py non-GUI functionality"""42
43MODULE = "test_uninstaller" # file name without extension44
45def setUp(self):46"""Initialize data needed for all tests"""47self.test_data_dir = os.path.join(48FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"49)50self.mock_addon = MockAddon()51self.signals_caught = []52self.test_object = AddonUninstaller(self.mock_addon)53
54self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))55self.test_object.success.connect(functools.partial(self.catch_signal, "success"))56self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))57
58def tearDown(self):59"""Finalize the test."""60
61def catch_signal(self, signal_name, *_):62"""Internal use: used to catch and log any emitted signals. Not called directly."""63self.signals_caught.append(signal_name)64
65def setup_dummy_installation(self, temp_dir) -> str:66"""Set up a dummy Addon in temp_dir"""67toplevel_path = os.path.join(temp_dir, self.mock_addon.name)68os.makedirs(toplevel_path)69with open(os.path.join(toplevel_path, "README.md"), "w") as f:70f.write("## Mock Addon ##\n\nFile created by the unit test code.")71self.test_object.installation_path = temp_dir72return toplevel_path73
74def create_fake_macro(self, macro_directory, fake_macro_name, digest):75"""Create an FCMacro file and matching digest entry for later removal"""76os.makedirs(macro_directory, exist_ok=True)77fake_file_installed = os.path.join(macro_directory, fake_macro_name)78with open(digest, "a", encoding="utf-8") as f:79f.write("# The following files were created outside this installation:\n")80f.write(fake_file_installed + "\n")81with open(fake_file_installed, "w", encoding="utf-8") as f:82f.write("# Fake macro data for unit testing")83
84def test_uninstall_normal(self):85"""Test the integrated uninstall function under normal circumstances"""86
87with tempfile.TemporaryDirectory() as temp_dir:88toplevel_path = self.setup_dummy_installation(temp_dir)89self.test_object.run()90self.assertTrue(os.path.exists(temp_dir))91self.assertFalse(os.path.exists(toplevel_path))92self.assertNotIn("failure", self.signals_caught)93self.assertIn("success", self.signals_caught)94self.assertIn("finished", self.signals_caught)95
96def test_uninstall_no_name(self):97"""Test the integrated uninstall function for an addon without a name"""98
99with tempfile.TemporaryDirectory() as temp_dir:100toplevel_path = self.setup_dummy_installation(temp_dir)101self.mock_addon.name = None102result = self.test_object.run()103self.assertTrue(os.path.exists(temp_dir))104self.assertIn("failure", self.signals_caught)105self.assertNotIn("success", self.signals_caught)106self.assertIn("finished", self.signals_caught)107
108def test_uninstall_dangerous_name(self):109"""Test the integrated uninstall function for an addon with a dangerous name"""110
111with tempfile.TemporaryDirectory() as temp_dir:112toplevel_path = self.setup_dummy_installation(temp_dir)113self.mock_addon.name = "./"114result = self.test_object.run()115self.assertTrue(os.path.exists(temp_dir))116self.assertIn("failure", self.signals_caught)117self.assertNotIn("success", self.signals_caught)118self.assertIn("finished", self.signals_caught)119
120def test_uninstall_unmatching_name(self):121"""Test the integrated uninstall function for an addon with a name that isn't installed"""122
123with tempfile.TemporaryDirectory() as temp_dir:124toplevel_path = self.setup_dummy_installation(temp_dir)125self.mock_addon.name += "Nonexistent"126result = self.test_object.run()127self.assertTrue(os.path.exists(temp_dir))128self.assertIn("failure", self.signals_caught)129self.assertNotIn("success", self.signals_caught)130self.assertIn("finished", self.signals_caught)131
132def test_uninstall_addon_with_macros(self):133"""Tests that the uninstaller removes the macro files"""134with tempfile.TemporaryDirectory() as temp_dir:135toplevel_path = self.setup_dummy_installation(temp_dir)136macro_directory = os.path.join(temp_dir, "Macros")137self.create_fake_macro(138macro_directory,139"FakeMacro.FCMacro",140os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),141)142result = self.test_object.run()143self.assertNotIn("failure", self.signals_caught)144self.assertIn("success", self.signals_caught)145self.assertIn("finished", self.signals_caught)146self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro.FCMacro")))147self.assertTrue(os.path.exists(macro_directory))148
149def test_uninstall_calls_script(self):150"""Tests that the main uninstaller run function calls the uninstall.py script"""151
152class Interceptor:153def __init__(self):154self.called = False155self.args = []156
157def func(self, *args):158self.called = True159self.args = args160
161interceptor = Interceptor()162with tempfile.TemporaryDirectory() as temp_dir:163toplevel_path = self.setup_dummy_installation(temp_dir)164self.test_object.run_uninstall_script = interceptor.func165result = self.test_object.run()166self.assertTrue(interceptor.called, "Failed to call uninstall script")167
168def test_remove_extra_files_no_digest(self):169"""Tests that a lack of digest file is not an error, and nothing gets removed"""170with tempfile.TemporaryDirectory() as temp_dir:171self.test_object.remove_extra_files(temp_dir) # Shouldn't throw172self.assertTrue(os.path.exists(temp_dir))173
174def test_remove_extra_files_empty_digest(self):175"""Test that an empty digest file is not an error, and nothing gets removed"""176with tempfile.TemporaryDirectory() as temp_dir:177with open("AM_INSTALLATION_DIGEST.txt", "w", encoding="utf-8") as f:178f.write("")179self.test_object.remove_extra_files(temp_dir) # Shouldn't throw180self.assertTrue(os.path.exists(temp_dir))181
182def test_remove_extra_files_comment_only_digest(self):183"""Test that a digest file that contains only comment lines is not an error, and nothing184gets removed"""
185with tempfile.TemporaryDirectory() as temp_dir:186with open("AM_INSTALLATION_DIGEST.txt", "w", encoding="utf-8") as f:187f.write("# Fake digest file for unit testing")188self.test_object.remove_extra_files(temp_dir) # Shouldn't throw189self.assertTrue(os.path.exists(temp_dir))190
191def test_remove_extra_files_repeated_files(self):192"""Test that a digest with the same file repeated removes it once, but doesn't error on193later requests to remove it."""
194with tempfile.TemporaryDirectory() as temp_dir:195toplevel_path = self.setup_dummy_installation(temp_dir)196macro_directory = os.path.join(temp_dir, "Macros")197self.create_fake_macro(198macro_directory,199"FakeMacro.FCMacro",200os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),201)202self.create_fake_macro(203macro_directory,204"FakeMacro.FCMacro",205os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),206)207self.create_fake_macro(208macro_directory,209"FakeMacro.FCMacro",210os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),211)212self.test_object.remove_extra_files(toplevel_path) # Shouldn't throw213self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro.FCMacro")))214
215def test_remove_extra_files_normal_case(self):216"""Test that a digest that is a "normal" case removes the requested files"""217with tempfile.TemporaryDirectory() as temp_dir:218toplevel_path = self.setup_dummy_installation(temp_dir)219macro_directory = os.path.join(temp_dir, "Macros")220self.create_fake_macro(221macro_directory,222"FakeMacro1.FCMacro",223os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),224)225self.create_fake_macro(226macro_directory,227"FakeMacro2.FCMacro",228os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),229)230self.create_fake_macro(231macro_directory,232"FakeMacro3.FCMacro",233os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),234)235
236# Make sure the setup worked as expected, otherwise the test is meaningless237self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro1.FCMacro")))238self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro2.FCMacro")))239self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro3.FCMacro")))240
241self.test_object.remove_extra_files(toplevel_path) # Shouldn't throw242
243self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro1.FCMacro")))244self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro2.FCMacro")))245self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro3.FCMacro")))246
247def test_runs_uninstaller_script_successful(self):248"""Tests that the uninstall.py script is called"""249with tempfile.TemporaryDirectory() as temp_dir:250toplevel_path = self.setup_dummy_installation(temp_dir)251with open(os.path.join(toplevel_path, "uninstall.py"), "w", encoding="utf-8") as f:252double_escaped = temp_dir.replace("\\", "\\\\")253f.write(254f"""# Mock uninstaller script255import os
256path = '{double_escaped}'257with open(os.path.join(path,"RAN_UNINSTALLER.txt"),"w",encoding="utf-8") as f:
258f.write("File created by uninstall.py from unit tests")
259"""
260)261self.test_object.run_uninstall_script(toplevel_path) # The exception does not leak out262self.assertTrue(os.path.exists(os.path.join(temp_dir, "RAN_UNINSTALLER.txt")))263
264def test_runs_uninstaller_script_failure(self):265"""Tests that exceptions in the uninstall.py script do not leak out"""266with tempfile.TemporaryDirectory() as temp_dir:267toplevel_path = self.setup_dummy_installation(temp_dir)268with open(os.path.join(toplevel_path, "uninstall.py"), "w", encoding="utf-8") as f:269f.write(270f"""# Mock uninstaller script271raise RuntimeError("Fake exception for unit testing")
272"""
273)274self.test_object.run_uninstall_script(toplevel_path) # The exception does not leak out275
276
277class TestMacroUninstaller(unittest.TestCase):278"""Test class for addonmanager_uninstaller.py non-GUI functionality"""279
280MODULE = "test_uninstaller" # file name without extension281
282def setUp(self):283self.mock_addon = MockAddon()284self.mock_addon.macro = MockMacro()285self.test_object = MacroUninstaller(self.mock_addon)286self.signals_caught = []287
288self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))289self.test_object.success.connect(functools.partial(self.catch_signal, "success"))290self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))291
292def tearDown(self):293pass294
295def catch_signal(self, signal_name, *_):296"""Internal use: used to catch and log any emitted signals. Not called directly."""297self.signals_caught.append(signal_name)298
299def test_remove_simple_macro(self):300with tempfile.TemporaryDirectory() as temp_dir:301self.test_object.installation_location = temp_dir302self.mock_addon.macro.install(temp_dir)303# Make sure the setup worked, otherwise the test is meaningless304self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))305self.test_object.run()306self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))307self.assertNotIn("failure", self.signals_caught)308self.assertIn("success", self.signals_caught)309self.assertIn("finished", self.signals_caught)310
311def test_remove_macro_with_icon(self):312with tempfile.TemporaryDirectory() as temp_dir:313self.test_object.installation_location = temp_dir314self.mock_addon.macro.icon = "mock_icon_test.svg"315self.mock_addon.macro.install(temp_dir)316# Make sure the setup worked, otherwise the test is meaningless317self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))318self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.icon)))319self.test_object.run()320self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))321self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.icon)))322self.assertNotIn("failure", self.signals_caught)323self.assertIn("success", self.signals_caught)324self.assertIn("finished", self.signals_caught)325
326def test_remove_macro_with_xpm_data(self):327with tempfile.TemporaryDirectory() as temp_dir:328self.test_object.installation_location = temp_dir329self.mock_addon.macro.xpm = "/*Fake XPM data*/"330self.mock_addon.macro.install(temp_dir)331# Make sure the setup worked, otherwise the test is meaningless332self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))333self.assertTrue(os.path.exists(os.path.join(temp_dir, "MockMacro_icon.xpm")))334self.test_object.run()335self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))336self.assertFalse(os.path.exists(os.path.join(temp_dir, "MockMacro_icon.xpm")))337self.assertNotIn("failure", self.signals_caught)338self.assertIn("success", self.signals_caught)339self.assertIn("finished", self.signals_caught)340
341def test_remove_macro_with_files(self):342with tempfile.TemporaryDirectory() as temp_dir:343self.test_object.installation_location = temp_dir344self.mock_addon.macro.other_files = [345"test_file_1.txt",346"test_file_2.FCMacro",347"subdir/test_file_3.txt",348]349self.mock_addon.macro.install(temp_dir)350# Make sure the setup worked, otherwise the test is meaningless351for f in self.mock_addon.macro.other_files:352self.assertTrue(353os.path.exists(os.path.join(temp_dir, f)),354f"Expected {f} to exist, and it does not",355)356self.test_object.run()357for f in self.mock_addon.macro.other_files:358self.assertFalse(359os.path.exists(os.path.join(temp_dir, f)),360f"Expected {f} to be removed, and it was not",361)362self.assertFalse(363os.path.exists(os.path.join(temp_dir, "subdir")),364"Failed to remove empty subdirectory",365)366self.assertNotIn("failure", self.signals_caught)367self.assertIn("success", self.signals_caught)368self.assertIn("finished", self.signals_caught)369
370def test_remove_nonexistent_macro(self):371with tempfile.TemporaryDirectory() as temp_dir:372self.test_object.installation_location = temp_dir373# Don't run the installer:374
375self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))376self.test_object.run() # Should not raise an exception377self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))378self.assertNotIn("failure", self.signals_caught)379self.assertIn("success", self.signals_caught)380self.assertIn("finished", self.signals_caught)381
382def test_remove_write_protected_macro(self):383with tempfile.TemporaryDirectory() as temp_dir:384self.test_object.installation_location = temp_dir385self.mock_addon.macro.install(temp_dir)386# Make sure the setup worked, otherwise the test is meaningless387f = os.path.join(temp_dir, self.mock_addon.macro.filename)388self.assertTrue(os.path.exists(f))389os.chmod(f, S_IREAD | S_IRGRP | S_IROTH)390self.test_object.run()391
392if os.path.exists(f):393os.chmod(f, S_IWUSR | S_IREAD)394self.assertNotIn("success", self.signals_caught)395self.assertIn("failure", self.signals_caught)396else:397# In some cases we managed to delete it anyway:398self.assertIn("success", self.signals_caught)399self.assertNotIn("failure", self.signals_caught)400self.assertIn("finished", self.signals_caught)401
402def test_cleanup_directories_multiple_empty(self):403with tempfile.TemporaryDirectory() as temp_dir:404empty_directories = set(["empty1", "empty2", "empty3"])405full_paths = set()406for directory in empty_directories:407full_path = os.path.join(temp_dir, directory)408os.mkdir(full_path)409full_paths.add(full_path)410
411for directory in full_paths:412self.assertTrue(directory, "Test code failed to create {directory}")413self.test_object._cleanup_directories(full_paths)414for directory in full_paths:415self.assertFalse(os.path.exists(directory))416
417def test_cleanup_directories_none(self):418with tempfile.TemporaryDirectory() as temp_dir:419full_paths = set()420self.test_object._cleanup_directories(full_paths) # Shouldn't throw421
422def test_cleanup_directories_not_empty(self):423with tempfile.TemporaryDirectory() as temp_dir:424empty_directories = set(["empty1", "empty2", "empty3"])425full_paths = set()426for directory in empty_directories:427full_path = os.path.join(temp_dir, directory)428os.mkdir(full_path)429full_paths.add(full_path)430with open(os.path.join(full_path, "test.txt"), "w", encoding="utf-8") as f:431f.write("Unit test dummy data\n")432
433for directory in full_paths:434self.assertTrue(directory, "Test code failed to create {directory}")435self.test_object._cleanup_directories(full_paths)436for directory in full_paths:437self.assertTrue(os.path.exists(directory))438