FreeCAD

Форк
0
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

26
import functools
27
import os
28
from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR
29
import tempfile
30
import unittest
31

32
import FreeCAD
33

34
from addonmanager_uninstaller import AddonUninstaller, MacroUninstaller
35

36
from Addon import Addon
37
from AddonManagerTest.app.mocks import MockAddon, MockMacro
38

39

40
class TestAddonUninstaller(unittest.TestCase):
41
    """Test class for addonmanager_uninstaller.py non-GUI functionality"""
42

43
    MODULE = "test_uninstaller"  # file name without extension
44

45
    def setUp(self):
46
        """Initialize data needed for all tests"""
47
        self.test_data_dir = os.path.join(
48
            FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
49
        )
50
        self.mock_addon = MockAddon()
51
        self.signals_caught = []
52
        self.test_object = AddonUninstaller(self.mock_addon)
53

54
        self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))
55
        self.test_object.success.connect(functools.partial(self.catch_signal, "success"))
56
        self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))
57

58
    def tearDown(self):
59
        """Finalize the test."""
60

61
    def catch_signal(self, signal_name, *_):
62
        """Internal use: used to catch and log any emitted signals. Not called directly."""
63
        self.signals_caught.append(signal_name)
64

65
    def setup_dummy_installation(self, temp_dir) -> str:
66
        """Set up a dummy Addon in temp_dir"""
67
        toplevel_path = os.path.join(temp_dir, self.mock_addon.name)
68
        os.makedirs(toplevel_path)
69
        with open(os.path.join(toplevel_path, "README.md"), "w") as f:
70
            f.write("## Mock Addon ##\n\nFile created by the unit test code.")
71
        self.test_object.installation_path = temp_dir
72
        return toplevel_path
73

74
    def create_fake_macro(self, macro_directory, fake_macro_name, digest):
75
        """Create an FCMacro file and matching digest entry for later removal"""
76
        os.makedirs(macro_directory, exist_ok=True)
77
        fake_file_installed = os.path.join(macro_directory, fake_macro_name)
78
        with open(digest, "a", encoding="utf-8") as f:
79
            f.write("# The following files were created outside this installation:\n")
80
            f.write(fake_file_installed + "\n")
81
        with open(fake_file_installed, "w", encoding="utf-8") as f:
82
            f.write("# Fake macro data for unit testing")
83

84
    def test_uninstall_normal(self):
85
        """Test the integrated uninstall function under normal circumstances"""
86

87
        with tempfile.TemporaryDirectory() as temp_dir:
88
            toplevel_path = self.setup_dummy_installation(temp_dir)
89
            self.test_object.run()
90
            self.assertTrue(os.path.exists(temp_dir))
91
            self.assertFalse(os.path.exists(toplevel_path))
92
            self.assertNotIn("failure", self.signals_caught)
93
            self.assertIn("success", self.signals_caught)
94
            self.assertIn("finished", self.signals_caught)
95

96
    def test_uninstall_no_name(self):
97
        """Test the integrated uninstall function for an addon without a name"""
98

99
        with tempfile.TemporaryDirectory() as temp_dir:
100
            toplevel_path = self.setup_dummy_installation(temp_dir)
101
            self.mock_addon.name = None
102
            result = self.test_object.run()
103
            self.assertTrue(os.path.exists(temp_dir))
104
            self.assertIn("failure", self.signals_caught)
105
            self.assertNotIn("success", self.signals_caught)
106
            self.assertIn("finished", self.signals_caught)
107

108
    def test_uninstall_dangerous_name(self):
109
        """Test the integrated uninstall function for an addon with a dangerous name"""
110

111
        with tempfile.TemporaryDirectory() as temp_dir:
112
            toplevel_path = self.setup_dummy_installation(temp_dir)
113
            self.mock_addon.name = "./"
114
            result = self.test_object.run()
115
            self.assertTrue(os.path.exists(temp_dir))
116
            self.assertIn("failure", self.signals_caught)
117
            self.assertNotIn("success", self.signals_caught)
118
            self.assertIn("finished", self.signals_caught)
119

120
    def test_uninstall_unmatching_name(self):
121
        """Test the integrated uninstall function for an addon with a name that isn't installed"""
122

123
        with tempfile.TemporaryDirectory() as temp_dir:
124
            toplevel_path = self.setup_dummy_installation(temp_dir)
125
            self.mock_addon.name += "Nonexistent"
126
            result = self.test_object.run()
127
            self.assertTrue(os.path.exists(temp_dir))
128
            self.assertIn("failure", self.signals_caught)
129
            self.assertNotIn("success", self.signals_caught)
130
            self.assertIn("finished", self.signals_caught)
131

132
    def test_uninstall_addon_with_macros(self):
133
        """Tests that the uninstaller removes the macro files"""
134
        with tempfile.TemporaryDirectory() as temp_dir:
135
            toplevel_path = self.setup_dummy_installation(temp_dir)
136
            macro_directory = os.path.join(temp_dir, "Macros")
137
            self.create_fake_macro(
138
                macro_directory,
139
                "FakeMacro.FCMacro",
140
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
141
            )
142
            result = self.test_object.run()
143
            self.assertNotIn("failure", self.signals_caught)
144
            self.assertIn("success", self.signals_caught)
145
            self.assertIn("finished", self.signals_caught)
146
            self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro.FCMacro")))
147
            self.assertTrue(os.path.exists(macro_directory))
148

149
    def test_uninstall_calls_script(self):
150
        """Tests that the main uninstaller run function calls the uninstall.py script"""
151

152
        class Interceptor:
153
            def __init__(self):
154
                self.called = False
155
                self.args = []
156

157
            def func(self, *args):
158
                self.called = True
159
                self.args = args
160

161
        interceptor = Interceptor()
162
        with tempfile.TemporaryDirectory() as temp_dir:
163
            toplevel_path = self.setup_dummy_installation(temp_dir)
164
            self.test_object.run_uninstall_script = interceptor.func
165
            result = self.test_object.run()
166
            self.assertTrue(interceptor.called, "Failed to call uninstall script")
167

168
    def test_remove_extra_files_no_digest(self):
169
        """Tests that a lack of digest file is not an error, and nothing gets removed"""
170
        with tempfile.TemporaryDirectory() as temp_dir:
171
            self.test_object.remove_extra_files(temp_dir)  # Shouldn't throw
172
            self.assertTrue(os.path.exists(temp_dir))
173

174
    def test_remove_extra_files_empty_digest(self):
175
        """Test that an empty digest file is not an error, and nothing gets removed"""
176
        with tempfile.TemporaryDirectory() as temp_dir:
177
            with open("AM_INSTALLATION_DIGEST.txt", "w", encoding="utf-8") as f:
178
                f.write("")
179
            self.test_object.remove_extra_files(temp_dir)  # Shouldn't throw
180
            self.assertTrue(os.path.exists(temp_dir))
181

182
    def test_remove_extra_files_comment_only_digest(self):
183
        """Test that a digest file that contains only comment lines is not an error, and nothing
184
        gets removed"""
185
        with tempfile.TemporaryDirectory() as temp_dir:
186
            with open("AM_INSTALLATION_DIGEST.txt", "w", encoding="utf-8") as f:
187
                f.write("# Fake digest file for unit testing")
188
            self.test_object.remove_extra_files(temp_dir)  # Shouldn't throw
189
            self.assertTrue(os.path.exists(temp_dir))
190

191
    def test_remove_extra_files_repeated_files(self):
192
        """Test that a digest with the same file repeated removes it once, but doesn't error on
193
        later requests to remove it."""
194
        with tempfile.TemporaryDirectory() as temp_dir:
195
            toplevel_path = self.setup_dummy_installation(temp_dir)
196
            macro_directory = os.path.join(temp_dir, "Macros")
197
            self.create_fake_macro(
198
                macro_directory,
199
                "FakeMacro.FCMacro",
200
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
201
            )
202
            self.create_fake_macro(
203
                macro_directory,
204
                "FakeMacro.FCMacro",
205
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
206
            )
207
            self.create_fake_macro(
208
                macro_directory,
209
                "FakeMacro.FCMacro",
210
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
211
            )
212
            self.test_object.remove_extra_files(toplevel_path)  # Shouldn't throw
213
            self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro.FCMacro")))
214

215
    def test_remove_extra_files_normal_case(self):
216
        """Test that a digest that is a "normal" case removes the requested files"""
217
        with tempfile.TemporaryDirectory() as temp_dir:
218
            toplevel_path = self.setup_dummy_installation(temp_dir)
219
            macro_directory = os.path.join(temp_dir, "Macros")
220
            self.create_fake_macro(
221
                macro_directory,
222
                "FakeMacro1.FCMacro",
223
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
224
            )
225
            self.create_fake_macro(
226
                macro_directory,
227
                "FakeMacro2.FCMacro",
228
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
229
            )
230
            self.create_fake_macro(
231
                macro_directory,
232
                "FakeMacro3.FCMacro",
233
                os.path.join(toplevel_path, "AM_INSTALLATION_DIGEST.txt"),
234
            )
235

236
            # Make sure the setup worked as expected, otherwise the test is meaningless
237
            self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro1.FCMacro")))
238
            self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro2.FCMacro")))
239
            self.assertTrue(os.path.exists(os.path.join(macro_directory, "FakeMacro3.FCMacro")))
240

241
            self.test_object.remove_extra_files(toplevel_path)  # Shouldn't throw
242

243
            self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro1.FCMacro")))
244
            self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro2.FCMacro")))
245
            self.assertFalse(os.path.exists(os.path.join(macro_directory, "FakeMacro3.FCMacro")))
246

247
    def test_runs_uninstaller_script_successful(self):
248
        """Tests that the uninstall.py script is called"""
249
        with tempfile.TemporaryDirectory() as temp_dir:
250
            toplevel_path = self.setup_dummy_installation(temp_dir)
251
            with open(os.path.join(toplevel_path, "uninstall.py"), "w", encoding="utf-8") as f:
252
                double_escaped = temp_dir.replace("\\", "\\\\")
253
                f.write(
254
                    f"""# Mock uninstaller script
255
import os
256
path = '{double_escaped}'
257
with open(os.path.join(path,"RAN_UNINSTALLER.txt"),"w",encoding="utf-8") as f:
258
    f.write("File created by uninstall.py from unit tests")
259
"""
260
                )
261
            self.test_object.run_uninstall_script(toplevel_path)  # The exception does not leak out
262
            self.assertTrue(os.path.exists(os.path.join(temp_dir, "RAN_UNINSTALLER.txt")))
263

264
    def test_runs_uninstaller_script_failure(self):
265
        """Tests that exceptions in the uninstall.py script do not leak out"""
266
        with tempfile.TemporaryDirectory() as temp_dir:
267
            toplevel_path = self.setup_dummy_installation(temp_dir)
268
            with open(os.path.join(toplevel_path, "uninstall.py"), "w", encoding="utf-8") as f:
269
                f.write(
270
                    f"""# Mock uninstaller script
271
raise RuntimeError("Fake exception for unit testing")
272
"""
273
                )
274
            self.test_object.run_uninstall_script(toplevel_path)  # The exception does not leak out
275

276

277
class TestMacroUninstaller(unittest.TestCase):
278
    """Test class for addonmanager_uninstaller.py non-GUI functionality"""
279

280
    MODULE = "test_uninstaller"  # file name without extension
281

282
    def setUp(self):
283
        self.mock_addon = MockAddon()
284
        self.mock_addon.macro = MockMacro()
285
        self.test_object = MacroUninstaller(self.mock_addon)
286
        self.signals_caught = []
287

288
        self.test_object.finished.connect(functools.partial(self.catch_signal, "finished"))
289
        self.test_object.success.connect(functools.partial(self.catch_signal, "success"))
290
        self.test_object.failure.connect(functools.partial(self.catch_signal, "failure"))
291

292
    def tearDown(self):
293
        pass
294

295
    def catch_signal(self, signal_name, *_):
296
        """Internal use: used to catch and log any emitted signals. Not called directly."""
297
        self.signals_caught.append(signal_name)
298

299
    def test_remove_simple_macro(self):
300
        with tempfile.TemporaryDirectory() as temp_dir:
301
            self.test_object.installation_location = temp_dir
302
            self.mock_addon.macro.install(temp_dir)
303
            # Make sure the setup worked, otherwise the test is meaningless
304
            self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
305
            self.test_object.run()
306
            self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
307
            self.assertNotIn("failure", self.signals_caught)
308
            self.assertIn("success", self.signals_caught)
309
            self.assertIn("finished", self.signals_caught)
310

311
    def test_remove_macro_with_icon(self):
312
        with tempfile.TemporaryDirectory() as temp_dir:
313
            self.test_object.installation_location = temp_dir
314
            self.mock_addon.macro.icon = "mock_icon_test.svg"
315
            self.mock_addon.macro.install(temp_dir)
316
            # Make sure the setup worked, otherwise the test is meaningless
317
            self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
318
            self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.icon)))
319
            self.test_object.run()
320
            self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
321
            self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.icon)))
322
            self.assertNotIn("failure", self.signals_caught)
323
            self.assertIn("success", self.signals_caught)
324
            self.assertIn("finished", self.signals_caught)
325

326
    def test_remove_macro_with_xpm_data(self):
327
        with tempfile.TemporaryDirectory() as temp_dir:
328
            self.test_object.installation_location = temp_dir
329
            self.mock_addon.macro.xpm = "/*Fake XPM data*/"
330
            self.mock_addon.macro.install(temp_dir)
331
            # Make sure the setup worked, otherwise the test is meaningless
332
            self.assertTrue(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
333
            self.assertTrue(os.path.exists(os.path.join(temp_dir, "MockMacro_icon.xpm")))
334
            self.test_object.run()
335
            self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
336
            self.assertFalse(os.path.exists(os.path.join(temp_dir, "MockMacro_icon.xpm")))
337
            self.assertNotIn("failure", self.signals_caught)
338
            self.assertIn("success", self.signals_caught)
339
            self.assertIn("finished", self.signals_caught)
340

341
    def test_remove_macro_with_files(self):
342
        with tempfile.TemporaryDirectory() as temp_dir:
343
            self.test_object.installation_location = temp_dir
344
            self.mock_addon.macro.other_files = [
345
                "test_file_1.txt",
346
                "test_file_2.FCMacro",
347
                "subdir/test_file_3.txt",
348
            ]
349
            self.mock_addon.macro.install(temp_dir)
350
            # Make sure the setup worked, otherwise the test is meaningless
351
            for f in self.mock_addon.macro.other_files:
352
                self.assertTrue(
353
                    os.path.exists(os.path.join(temp_dir, f)),
354
                    f"Expected {f} to exist, and it does not",
355
                )
356
            self.test_object.run()
357
            for f in self.mock_addon.macro.other_files:
358
                self.assertFalse(
359
                    os.path.exists(os.path.join(temp_dir, f)),
360
                    f"Expected {f} to be removed, and it was not",
361
                )
362
            self.assertFalse(
363
                os.path.exists(os.path.join(temp_dir, "subdir")),
364
                "Failed to remove empty subdirectory",
365
            )
366
            self.assertNotIn("failure", self.signals_caught)
367
            self.assertIn("success", self.signals_caught)
368
            self.assertIn("finished", self.signals_caught)
369

370
    def test_remove_nonexistent_macro(self):
371
        with tempfile.TemporaryDirectory() as temp_dir:
372
            self.test_object.installation_location = temp_dir
373
            # Don't run the installer:
374

375
            self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
376
            self.test_object.run()  # Should not raise an exception
377
            self.assertFalse(os.path.exists(os.path.join(temp_dir, self.mock_addon.macro.filename)))
378
            self.assertNotIn("failure", self.signals_caught)
379
            self.assertIn("success", self.signals_caught)
380
            self.assertIn("finished", self.signals_caught)
381

382
    def test_remove_write_protected_macro(self):
383
        with tempfile.TemporaryDirectory() as temp_dir:
384
            self.test_object.installation_location = temp_dir
385
            self.mock_addon.macro.install(temp_dir)
386
            # Make sure the setup worked, otherwise the test is meaningless
387
            f = os.path.join(temp_dir, self.mock_addon.macro.filename)
388
            self.assertTrue(os.path.exists(f))
389
            os.chmod(f, S_IREAD | S_IRGRP | S_IROTH)
390
            self.test_object.run()
391

392
            if os.path.exists(f):
393
                os.chmod(f, S_IWUSR | S_IREAD)
394
                self.assertNotIn("success", self.signals_caught)
395
                self.assertIn("failure", self.signals_caught)
396
            else:
397
                # In some cases we managed to delete it anyway:
398
                self.assertIn("success", self.signals_caught)
399
                self.assertNotIn("failure", self.signals_caught)
400
            self.assertIn("finished", self.signals_caught)
401

402
    def test_cleanup_directories_multiple_empty(self):
403
        with tempfile.TemporaryDirectory() as temp_dir:
404
            empty_directories = set(["empty1", "empty2", "empty3"])
405
            full_paths = set()
406
            for directory in empty_directories:
407
                full_path = os.path.join(temp_dir, directory)
408
                os.mkdir(full_path)
409
                full_paths.add(full_path)
410

411
            for directory in full_paths:
412
                self.assertTrue(directory, "Test code failed to create {directory}")
413
            self.test_object._cleanup_directories(full_paths)
414
            for directory in full_paths:
415
                self.assertFalse(os.path.exists(directory))
416

417
    def test_cleanup_directories_none(self):
418
        with tempfile.TemporaryDirectory() as temp_dir:
419
            full_paths = set()
420
            self.test_object._cleanup_directories(full_paths)  # Shouldn't throw
421

422
    def test_cleanup_directories_not_empty(self):
423
        with tempfile.TemporaryDirectory() as temp_dir:
424
            empty_directories = set(["empty1", "empty2", "empty3"])
425
            full_paths = set()
426
            for directory in empty_directories:
427
                full_path = os.path.join(temp_dir, directory)
428
                os.mkdir(full_path)
429
                full_paths.add(full_path)
430
                with open(os.path.join(full_path, "test.txt"), "w", encoding="utf-8") as f:
431
                    f.write("Unit test dummy data\n")
432

433
            for directory in full_paths:
434
                self.assertTrue(directory, "Test code failed to create {directory}")
435
            self.test_object._cleanup_directories(full_paths)
436
            for directory in full_paths:
437
                self.assertTrue(os.path.exists(directory))
438

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.