FreeCAD

Форк
0
344 строки · 15.3 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# ***************************************************************************
3
# *                                                                         *
4
# *   Copyright (c) 2022-2023 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
"""Tests for the MacroParser class"""
25

26
import io
27
import os
28
import sys
29
import unittest
30

31
sys.path.append("../../")  # So the IDE can find the classes to run with
32

33
from addonmanager_macro_parser import MacroParser
34
from AddonManagerTest.app.mocks import MockConsole, CallCatcher, MockThread
35

36

37
# pylint: disable=protected-access, too-many-public-methods
38

39

40
class TestMacroParser(unittest.TestCase):
41
    """Test the MacroParser class"""
42

43
    def setUp(self) -> None:
44
        self.test_object = MacroParser("UnitTestMacro")
45
        self.test_object.console = MockConsole()
46
        self.test_object.current_thread = MockThread()
47

48
    def tearDown(self) -> None:
49
        pass
50

51
    def test_fill_details_from_code_normal(self):
52
        """Test to make sure _process_line gets called as expected"""
53
        catcher = CallCatcher()
54
        self.test_object._process_line = catcher.catch_call
55
        fake_macro_data = self.given_some_lines(20, 10)
56
        self.test_object.fill_details_from_code(fake_macro_data)
57
        self.assertEqual(catcher.call_count, 10)
58

59
    def test_fill_details_from_code_too_many_lines(self):
60
        """Test to make sure _process_line gets limited as expected"""
61
        catcher = CallCatcher()
62
        self.test_object._process_line = catcher.catch_call
63
        self.test_object.MAX_LINES_TO_SEARCH = 5
64
        fake_macro_data = self.given_some_lines(20, 10)
65
        self.test_object.fill_details_from_code(fake_macro_data)
66
        self.assertEqual(catcher.call_count, 5)
67

68
    def test_fill_details_from_code_thread_interrupted(self):
69
        """Test to make sure _process_line gets stopped as expected"""
70
        catcher = CallCatcher()
71
        self.test_object._process_line = catcher.catch_call
72
        self.test_object.current_thread.interrupt_after_n_calls = 6  # Stop on the 6th
73
        fake_macro_data = self.given_some_lines(20, 10)
74
        self.test_object.fill_details_from_code(fake_macro_data)
75
        self.assertEqual(catcher.call_count, 5)
76

77
    @staticmethod
78
    def given_some_lines(num_lines, num_dunder_lines) -> str:
79
        """Generate fake macro header data with the given number of lines and number of
80
        lines beginning with a double-underscore."""
81
        result = ""
82
        for i in range(num_lines):
83
            if i < num_dunder_lines:
84
                result += f"__something_{i}__ = 'Test{i}'  # A line to be scanned\n"
85
            else:
86
                result += f"# Nothing to see on line {i}\n"
87
        return result
88

89
    def test_process_line_known_lines(self):
90
        """Lines starting with keys are processed"""
91
        test_lines = ["__known_key__ = 'Test'", "__another_known_key__ = 'Test'"]
92
        for line in test_lines:
93
            with self.subTest(line=line):
94
                self.test_object.remaining_item_map = {
95
                    "__known_key__": "known_key",
96
                    "__another_known_key__": "another_known_key",
97
                }
98
                content_lines = io.StringIO(line)
99
                read_in_line = content_lines.readline()
100
                catcher = CallCatcher()
101
                self.test_object._process_key = catcher.catch_call
102
                self.test_object._process_line(read_in_line, content_lines)
103
                self.assertTrue(catcher.called, "_process_key was not called for a known key")
104

105
    def test_process_line_unknown_lines(self):
106
        """Lines starting with non-keys are not processed"""
107
        test_lines = [
108
            "# Just a line with a comment",
109
            "\n",
110
            "__dont_know_this_one__ = 'Who cares?'",
111
            "# __known_key__ = 'Aha, but it is commented out!'",
112
        ]
113
        for line in test_lines:
114
            with self.subTest(line=line):
115
                self.test_object.remaining_item_map = {
116
                    "__known_key__": "known_key",
117
                    "__another_known_key__": "another_known_key",
118
                }
119
                content_lines = io.StringIO(line)
120
                read_in_line = content_lines.readline()
121
                catcher = CallCatcher()
122
                self.test_object._process_key = catcher.catch_call
123
                self.test_object._process_line(read_in_line, content_lines)
124
                self.assertFalse(catcher.called, "_process_key was called for an unknown key")
125

126
    def test_process_key_standard(self):
127
        """Normal expected data is processed"""
128
        self.test_object._reset_map()
129
        in_memory_data = '__comment__ = "Test"'
130
        content_lines = io.StringIO(in_memory_data)
131
        line = content_lines.readline()
132
        self.test_object._process_key("__comment__", line, content_lines)
133
        self.assertTrue(self.test_object.parse_results["comment"], "Test")
134

135
    def test_process_key_special(self):
136
        """Special handling for version = date is processed"""
137
        self.test_object._reset_map()
138
        self.test_object.parse_results["date"] = "2001-01-01"
139
        in_memory_data = "__version__ = __date__"
140
        content_lines = io.StringIO(in_memory_data)
141
        line = content_lines.readline()
142
        self.test_object._process_key("__version__", line, content_lines)
143
        self.assertTrue(self.test_object.parse_results["version"], "2001-01-01")
144

145
    def test_handle_backslash_continuation_no_backslashes(self):
146
        """The backslash handling code doesn't change a line with no backslashes"""
147
        in_memory_data = '"Not a backslash in sight"'
148
        content_lines = io.StringIO(in_memory_data)
149
        line = content_lines.readline()
150
        result = self.test_object._handle_backslash_continuation(line, content_lines)
151
        self.assertEqual(result, in_memory_data)
152

153
    def test_handle_backslash_continuation(self):
154
        """Lines ending in a backslash get stripped and concatenated"""
155
        in_memory_data = '"Line1\\\nLine2\\\nLine3\\\nLine4"'
156
        content_lines = io.StringIO(in_memory_data)
157
        line = content_lines.readline()
158
        result = self.test_object._handle_backslash_continuation(line, content_lines)
159
        self.assertEqual(result, '"Line1Line2Line3Line4"')
160

161
    def test_handle_triple_quoted_string_no_triple_quotes(self):
162
        """The triple-quote handler leaves alone lines without triple-quotes"""
163
        in_memory_data = '"Line1"'
164
        content_lines = io.StringIO(in_memory_data)
165
        line = content_lines.readline()
166
        result, was_triple_quoted = self.test_object._handle_triple_quoted_string(
167
            line, content_lines
168
        )
169
        self.assertEqual(result, in_memory_data)
170
        self.assertFalse(was_triple_quoted)
171

172
    def test_handle_triple_quoted_string(self):
173
        """Data is extracted across multiple lines for a triple-quoted string"""
174
        in_memory_data = '"""Line1\nLine2\nLine3\nLine4"""\nLine5\n'
175
        content_lines = io.StringIO(in_memory_data)
176
        line = content_lines.readline()
177
        result, was_triple_quoted = self.test_object._handle_triple_quoted_string(
178
            line, content_lines
179
        )
180
        self.assertEqual(result, '"""Line1\nLine2\nLine3\nLine4"""')
181
        self.assertTrue(was_triple_quoted)
182

183
    def test_strip_quotes_single(self):
184
        """Single quotes are stripped from the final string"""
185
        expected = "test"
186
        quoted = f"'{expected}'"
187
        actual = self.test_object._strip_quotes(quoted)
188
        self.assertEqual(actual, expected)
189

190
    def test_strip_quotes_double(self):
191
        """Double quotes are stripped from the final string"""
192
        expected = "test"
193
        quoted = f'"{expected}"'
194
        actual = self.test_object._strip_quotes(quoted)
195
        self.assertEqual(actual, expected)
196

197
    def test_strip_quotes_triple(self):
198
        """Triple quotes are stripped from the final string"""
199
        expected = "test"
200
        quoted = f'"""{expected}"""'
201
        actual = self.test_object._strip_quotes(quoted)
202
        self.assertEqual(actual, expected)
203

204
    def test_strip_quotes_unquoted(self):
205
        """Unquoted data results in None"""
206
        unquoted = "This has no quotation marks of any kind"
207
        actual = self.test_object._strip_quotes(unquoted)
208
        self.assertIsNone(actual)
209

210
    def test_standard_extraction_string(self):
211
        """String variables are extracted and stored"""
212
        string_keys = [
213
            "comment",
214
            "url",
215
            "wiki",
216
            "version",
217
            "author",
218
            "date",
219
            "icon",
220
            "xpm",
221
        ]
222
        for key in string_keys:
223
            with self.subTest(key=key):
224
                self.test_object._standard_extraction(key, "test")
225
                self.assertEqual(self.test_object.parse_results[key], "test")
226

227
    def test_standard_extraction_list(self):
228
        """List variable is extracted and stored"""
229
        key = "other_files"
230
        self.test_object._standard_extraction(key, "test1, test2, test3")
231
        self.assertIn("test1", self.test_object.parse_results[key])
232
        self.assertIn("test2", self.test_object.parse_results[key])
233
        self.assertIn("test3", self.test_object.parse_results[key])
234

235
    def test_apply_special_handling_version(self):
236
        """If the tag is __version__, apply our special handling"""
237
        self.test_object._reset_map()
238
        self.test_object._apply_special_handling("__version__", 42)
239
        self.assertNotIn("__version__", self.test_object.remaining_item_map)
240
        self.assertEqual(self.test_object.parse_results["version"], "42")
241

242
    def test_apply_special_handling_not_version(self):
243
        """If the tag is not __version__, raise an error"""
244
        self.test_object._reset_map()
245
        with self.assertRaises(SyntaxError):
246
            self.test_object._apply_special_handling("__not_version__", 42)
247
        self.assertIn("__version__", self.test_object.remaining_item_map)
248

249
    def test_process_noncompliant_version_date(self):
250
        """Detect and allow __date__ for the __version__"""
251
        self.test_object.parse_results["date"] = "1/2/3"
252
        self.test_object._process_noncompliant_version("__date__")
253
        self.assertEqual(
254
            self.test_object.parse_results["version"],
255
            self.test_object.parse_results["date"],
256
        )
257

258
    def test_process_noncompliant_version_float(self):
259
        """Detect and allow floats for the __version__"""
260
        self.test_object._process_noncompliant_version(1.2)
261
        self.assertEqual(self.test_object.parse_results["version"], "1.2")
262

263
    def test_process_noncompliant_version_int(self):
264
        """Detect and allow integers for the __version__"""
265
        self.test_object._process_noncompliant_version(42)
266
        self.assertEqual(self.test_object.parse_results["version"], "42")
267

268
    def test_detect_illegal_content_prefixed_string(self):
269
        """Detect and raise an error for various kinds of prefixed strings"""
270
        illegal_strings = [
271
            "f'Some fancy {thing}'",
272
            'f"Some fancy {thing}"',
273
            "r'Some fancy {thing}'",
274
            'r"Some fancy {thing}"',
275
            "u'Some fancy {thing}'",
276
            'u"Some fancy {thing}"',
277
            "fr'Some fancy {thing}'",
278
            'fr"Some fancy {thing}"',
279
            "rf'Some fancy {thing}'",
280
            'rf"Some fancy {thing}"',
281
        ]
282
        for test_string in illegal_strings:
283
            with self.subTest(test_string=test_string):
284
                with self.assertRaises(SyntaxError):
285
                    MacroParser._detect_illegal_content(test_string)
286

287
    def test_detect_illegal_content_not_a_string(self):
288
        """Detect and raise an error for (some) non-strings"""
289
        illegal_strings = [
290
            "no quotes",
291
            "do_stuff()",
292
            'print("A function call sporting quotes!")',
293
            "__name__",
294
            "__version__",
295
            "1.2.3",
296
        ]
297
        for test_string in illegal_strings:
298
            with self.subTest(test_string=test_string):
299
                with self.assertRaises(SyntaxError):
300
                    MacroParser._detect_illegal_content(test_string)
301

302
    def test_detect_illegal_content_no_failure(self):
303
        """Recognize strings of various kinds, plus ints, and floats"""
304
        legal_strings = [
305
            '"Some legal value in double quotes"',
306
            "'Some legal value in single quotes'",
307
            '"""Some legal value in triple quotes"""',
308
            "__date__",
309
            "42",
310
            "4.2",
311
        ]
312
        for test_string in legal_strings:
313
            with self.subTest(test_string=test_string):
314
                MacroParser._detect_illegal_content(test_string)
315

316
    #####################
317
    # INTEGRATION TESTS #
318
    #####################
319

320
    def test_macro_parser(self):
321
        """INTEGRATION TEST: Given "real" data, ensure the parsing yields the expected results."""
322
        data_dir = os.path.join(os.path.dirname(__file__), "../data")
323
        macro_file = os.path.join(data_dir, "DoNothing.FCMacro")
324
        with open(macro_file, "r", encoding="utf-8") as f:
325
            code = f.read()
326
        self.test_object.fill_details_from_code(code)
327
        self.assertEqual(len(self.test_object.console.errors), 0)
328
        self.assertEqual(len(self.test_object.console.warnings), 0)
329
        self.assertEqual(self.test_object.parse_results["author"], "Chris Hennes")
330
        self.assertEqual(self.test_object.parse_results["version"], "1.0")
331
        self.assertEqual(self.test_object.parse_results["date"], "2022-02-28")
332
        self.assertEqual(
333
            self.test_object.parse_results["comment"],
334
            "Do absolutely nothing. For Addon Manager integration tests.",
335
        )
336
        self.assertEqual(
337
            self.test_object.parse_results["url"], "https://github.com/FreeCAD/FreeCAD"
338
        )
339
        self.assertEqual(self.test_object.parse_results["icon"], "not_real.png")
340
        self.assertListEqual(
341
            self.test_object.parse_results["other_files"],
342
            ["file1.py", "file2.py", "file3.py"],
343
        )
344
        self.assertNotEqual(self.test_object.parse_results["xpm"], "")
345

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

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

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

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