Addon Manager: Refactor Macro parser
This commit is contained in:
committed by
Chris Hennes
parent
377c1564d1
commit
d6fc29f057
347
src/Mod/AddonManager/AddonManagerTest/app/test_macro_parser.py
Normal file
347
src/Mod/AddonManager/AddonManagerTest/app/test_macro_parser.py
Normal file
@@ -0,0 +1,347 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2023 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/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Tests for the MacroParser class"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.append("../../") # So the IDE can find the classes to run with
|
||||
|
||||
from addonmanager_macro_parser import MacroParser
|
||||
from AddonManagerTest.app.mocks import MockConsole, CallCatcher, MockThread
|
||||
|
||||
|
||||
# pylint: disable=protected-access, too-many-public-methods
|
||||
|
||||
|
||||
class TestMacroParser(unittest.TestCase):
|
||||
"""Test the MacroParser class"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.test_object = MacroParser("UnitTestMacro")
|
||||
self.test_object.console = MockConsole()
|
||||
self.test_object.current_thread = MockThread()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
pass
|
||||
|
||||
def test_fill_details_from_code_normal(self):
|
||||
"""Test to make sure _process_line gets called as expected"""
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_line = catcher.catch_call
|
||||
fake_macro_data = self.given_some_lines(20, 10)
|
||||
self.test_object.fill_details_from_code(fake_macro_data)
|
||||
self.assertEqual(catcher.call_count, 10)
|
||||
|
||||
def test_fill_details_from_code_too_many_lines(self):
|
||||
"""Test to make sure _process_line gets limited as expected"""
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_line = catcher.catch_call
|
||||
self.test_object.MAX_LINES_TO_SEARCH = 5
|
||||
fake_macro_data = self.given_some_lines(20, 10)
|
||||
self.test_object.fill_details_from_code(fake_macro_data)
|
||||
self.assertEqual(catcher.call_count, 5)
|
||||
|
||||
def test_fill_details_from_code_thread_interrupted(self):
|
||||
"""Test to make sure _process_line gets stopped as expected"""
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_line = catcher.catch_call
|
||||
self.test_object.current_thread.interrupt_after_n_calls = 6 # Stop on the 6th
|
||||
fake_macro_data = self.given_some_lines(20, 10)
|
||||
self.test_object.fill_details_from_code(fake_macro_data)
|
||||
self.assertEqual(catcher.call_count, 5)
|
||||
|
||||
@staticmethod
|
||||
def given_some_lines(num_lines, num_dunder_lines) -> str:
|
||||
"""Generate fake macro header data with the given number of lines and number of
|
||||
lines beginning with a double-underscore."""
|
||||
result = ""
|
||||
for i in range(num_lines):
|
||||
if i < num_dunder_lines:
|
||||
result += f"__something_{i}__ = 'Test{i}' # A line to be scanned\n"
|
||||
else:
|
||||
result += f"# Nothing to see on line {i}\n"
|
||||
return result
|
||||
|
||||
def test_process_line_known_lines(self):
|
||||
"""Lines starting with keys are processed"""
|
||||
test_lines = ["__known_key__ = 'Test'", "__another_known_key__ = 'Test'"]
|
||||
for line in test_lines:
|
||||
with self.subTest(line=line):
|
||||
self.test_object.remaining_item_map = {
|
||||
"__known_key__": "known_key",
|
||||
"__another_known_key__": "another_known_key",
|
||||
}
|
||||
content_lines = io.StringIO(line)
|
||||
read_in_line = content_lines.readline()
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_key = catcher.catch_call
|
||||
self.test_object._process_line(read_in_line, content_lines)
|
||||
self.assertTrue(
|
||||
catcher.called, "_process_key was not called for a known key"
|
||||
)
|
||||
|
||||
def test_process_line_unknown_lines(self):
|
||||
"""Lines starting with non-keys are not processed"""
|
||||
test_lines = [
|
||||
"# Just a line with a comment",
|
||||
"\n",
|
||||
"__dont_know_this_one__ = 'Who cares?'",
|
||||
"# __known_key__ = 'Aha, but it is commented out!'",
|
||||
]
|
||||
for line in test_lines:
|
||||
with self.subTest(line=line):
|
||||
self.test_object.remaining_item_map = {
|
||||
"__known_key__": "known_key",
|
||||
"__another_known_key__": "another_known_key",
|
||||
}
|
||||
content_lines = io.StringIO(line)
|
||||
read_in_line = content_lines.readline()
|
||||
catcher = CallCatcher()
|
||||
self.test_object._process_key = catcher.catch_call
|
||||
self.test_object._process_line(read_in_line, content_lines)
|
||||
self.assertFalse(
|
||||
catcher.called, "_process_key was called for an unknown key"
|
||||
)
|
||||
|
||||
def test_process_key_standard(self):
|
||||
"""Normal expected data is processed"""
|
||||
self.test_object._reset_map()
|
||||
in_memory_data = '__comment__ = "Test"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
self.test_object._process_key("__comment__", line, content_lines)
|
||||
self.assertTrue(self.test_object.parse_results["comment"], "Test")
|
||||
|
||||
def test_process_key_special(self):
|
||||
"""Special handling for version = date is processed"""
|
||||
self.test_object._reset_map()
|
||||
self.test_object.parse_results["date"] = "2001-01-01"
|
||||
in_memory_data = "__version__ = __date__"
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
self.test_object._process_key("__version__", line, content_lines)
|
||||
self.assertTrue(self.test_object.parse_results["version"], "2001-01-01")
|
||||
|
||||
def test_handle_backslash_continuation_no_backslashes(self):
|
||||
"""The backslash handling code doesn't change a line with no backslashes"""
|
||||
in_memory_data = '"Not a backslash in sight"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result = self.test_object._handle_backslash_continuation(line, content_lines)
|
||||
self.assertEqual(result, in_memory_data)
|
||||
|
||||
def test_handle_backslash_continuation(self):
|
||||
"""Lines ending in a backslash get stripped and concatenated"""
|
||||
in_memory_data = '"Line1\\\nLine2\\\nLine3\\\nLine4"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result = self.test_object._handle_backslash_continuation(line, content_lines)
|
||||
self.assertEqual(result, '"Line1Line2Line3Line4"')
|
||||
|
||||
def test_handle_triple_quoted_string_no_triple_quotes(self):
|
||||
"""The triple-quote handler leaves alone lines without triple-quotes"""
|
||||
in_memory_data = '"Line1"'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result, was_triple_quoted = self.test_object._handle_triple_quoted_string(
|
||||
line, content_lines
|
||||
)
|
||||
self.assertEqual(result, in_memory_data)
|
||||
self.assertFalse(was_triple_quoted)
|
||||
|
||||
def test_handle_triple_quoted_string(self):
|
||||
"""Data is extracted across multiple lines for a triple-quoted string"""
|
||||
in_memory_data = '"""Line1\nLine2\nLine3\nLine4"""\nLine5\n'
|
||||
content_lines = io.StringIO(in_memory_data)
|
||||
line = content_lines.readline()
|
||||
result, was_triple_quoted = self.test_object._handle_triple_quoted_string(
|
||||
line, content_lines
|
||||
)
|
||||
self.assertEqual(result, '"""Line1\nLine2\nLine3\nLine4"""')
|
||||
self.assertTrue(was_triple_quoted)
|
||||
|
||||
def test_strip_quotes_single(self):
|
||||
"""Single quotes are stripped from the final string"""
|
||||
expected = "test"
|
||||
quoted = f"'{expected}'"
|
||||
actual = self.test_object._strip_quotes(quoted)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strip_quotes_double(self):
|
||||
"""Double quotes are stripped from the final string"""
|
||||
expected = "test"
|
||||
quoted = f'"{expected}"'
|
||||
actual = self.test_object._strip_quotes(quoted)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strip_quotes_triple(self):
|
||||
"""Triple quotes are stripped from the final string"""
|
||||
expected = "test"
|
||||
quoted = f'"""{expected}"""'
|
||||
actual = self.test_object._strip_quotes(quoted)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strip_quotes_unquoted(self):
|
||||
"""Unquoted data results in None"""
|
||||
unquoted = "This has no quotation marks of any kind"
|
||||
actual = self.test_object._strip_quotes(unquoted)
|
||||
self.assertIsNone(actual)
|
||||
|
||||
def test_standard_extraction_string(self):
|
||||
"""String variables are extracted and stored"""
|
||||
string_keys = [
|
||||
"comment",
|
||||
"url",
|
||||
"wiki",
|
||||
"version",
|
||||
"author",
|
||||
"date",
|
||||
"icon",
|
||||
"xpm",
|
||||
]
|
||||
for key in string_keys:
|
||||
with self.subTest(key=key):
|
||||
self.test_object._standard_extraction(key, "test")
|
||||
self.assertEqual(self.test_object.parse_results[key], "test")
|
||||
|
||||
def test_standard_extraction_list(self):
|
||||
"""List variable is extracted and stored"""
|
||||
key = "other_files"
|
||||
self.test_object._standard_extraction(key, "test1, test2, test3")
|
||||
self.assertIn("test1", self.test_object.parse_results[key])
|
||||
self.assertIn("test2", self.test_object.parse_results[key])
|
||||
self.assertIn("test3", self.test_object.parse_results[key])
|
||||
|
||||
def test_apply_special_handling_version(self):
|
||||
"""If the tag is __version__, apply our special handling"""
|
||||
self.test_object._reset_map()
|
||||
self.test_object._apply_special_handling("__version__", 42)
|
||||
self.assertNotIn("__version__", self.test_object.remaining_item_map)
|
||||
self.assertEqual(self.test_object.parse_results["version"], "42")
|
||||
|
||||
def test_apply_special_handling_not_version(self):
|
||||
"""If the tag is not __version__, raise an error"""
|
||||
self.test_object._reset_map()
|
||||
with self.assertRaises(SyntaxError):
|
||||
self.test_object._apply_special_handling("__not_version__", 42)
|
||||
self.assertIn("__version__", self.test_object.remaining_item_map)
|
||||
|
||||
def test_process_noncompliant_version_date(self):
|
||||
"""Detect and allow __date__ for the __version__"""
|
||||
self.test_object.parse_results["date"] = "1/2/3"
|
||||
self.test_object._process_noncompliant_version("__date__")
|
||||
self.assertEqual(
|
||||
self.test_object.parse_results["version"],
|
||||
self.test_object.parse_results["date"],
|
||||
)
|
||||
|
||||
def test_process_noncompliant_version_float(self):
|
||||
"""Detect and allow floats for the __version__"""
|
||||
self.test_object._process_noncompliant_version(1.2)
|
||||
self.assertEqual(self.test_object.parse_results["version"], "1.2")
|
||||
|
||||
def test_process_noncompliant_version_int(self):
|
||||
"""Detect and allow integers for the __version__"""
|
||||
self.test_object._process_noncompliant_version(42)
|
||||
self.assertEqual(self.test_object.parse_results["version"], "42")
|
||||
|
||||
def test_detect_illegal_content_prefixed_string(self):
|
||||
"""Detect and raise an error for various kinds of prefixed strings"""
|
||||
illegal_strings = [
|
||||
"f'Some fancy {thing}'",
|
||||
'f"Some fancy {thing}"',
|
||||
"r'Some fancy {thing}'",
|
||||
'r"Some fancy {thing}"',
|
||||
"u'Some fancy {thing}'",
|
||||
'u"Some fancy {thing}"',
|
||||
"fr'Some fancy {thing}'",
|
||||
'fr"Some fancy {thing}"',
|
||||
"rf'Some fancy {thing}'",
|
||||
'rf"Some fancy {thing}"',
|
||||
]
|
||||
for test_string in illegal_strings:
|
||||
with self.subTest(test_string=test_string):
|
||||
with self.assertRaises(SyntaxError):
|
||||
MacroParser._detect_illegal_content(test_string)
|
||||
|
||||
def test_detect_illegal_content_not_a_string(self):
|
||||
"""Detect and raise an error for (some) non-strings"""
|
||||
illegal_strings = [
|
||||
"no quotes",
|
||||
"do_stuff()",
|
||||
'print("A function call sporting quotes!")',
|
||||
"__name__",
|
||||
"__version__",
|
||||
"1.2.3",
|
||||
]
|
||||
for test_string in illegal_strings:
|
||||
with self.subTest(test_string=test_string):
|
||||
with self.assertRaises(SyntaxError):
|
||||
MacroParser._detect_illegal_content(test_string)
|
||||
|
||||
def test_detect_illegal_content_no_failure(self):
|
||||
"""Recognize strings of various kinds, plus ints, and floats"""
|
||||
legal_strings = [
|
||||
'"Some legal value in double quotes"',
|
||||
"'Some legal value in single quotes'",
|
||||
'"""Some legal value in triple quotes"""',
|
||||
"__date__",
|
||||
"42",
|
||||
"4.2",
|
||||
]
|
||||
for test_string in legal_strings:
|
||||
with self.subTest(test_string=test_string):
|
||||
MacroParser._detect_illegal_content(test_string)
|
||||
|
||||
#####################
|
||||
# INTEGRATION TESTS #
|
||||
#####################
|
||||
|
||||
def test_macro_parser(self):
|
||||
"""INTEGRATION TEST: Given "real" data, ensure the parsing yields the expected results."""
|
||||
data_dir = os.path.join(os.path.dirname(__file__), "../data")
|
||||
macro_file = os.path.join(data_dir, "DoNothing.FCMacro")
|
||||
with open(macro_file, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
self.test_object.fill_details_from_code(code)
|
||||
self.assertEqual(len(self.test_object.console.errors), 0)
|
||||
self.assertEqual(len(self.test_object.console.warnings), 0)
|
||||
self.assertEqual(self.test_object.parse_results["author"], "Chris Hennes")
|
||||
self.assertEqual(self.test_object.parse_results["version"], "1.0")
|
||||
self.assertEqual(self.test_object.parse_results["date"], "2022-02-28")
|
||||
self.assertEqual(
|
||||
self.test_object.parse_results["comment"],
|
||||
"Do absolutely nothing. For Addon Manager integration tests.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.test_object.parse_results["url"], "https://github.com/FreeCAD/FreeCAD"
|
||||
)
|
||||
self.assertEqual(self.test_object.parse_results["icon"], "not_real.png")
|
||||
self.assertListEqual(
|
||||
self.test_object.parse_results["other_files"],
|
||||
["file1.py", "file2.py", "file3.py"],
|
||||
)
|
||||
self.assertNotEqual(self.test_object.parse_results["xpm"], "")
|
||||
Reference in New Issue
Block a user