FreeCAD
633 строки · 26.0 Кб
1# SPDX-License-Identifier: LGPL-2.1-or-later
2# ***************************************************************************
3# * *
4# * Copyright (c) 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# ***************************************************************************
23import os24import sys25import tempfile26import unittest27import unittest.mock28
29Mock = unittest.mock.MagicMock30
31sys.path.append("../../")32
33
34class TestVersion(unittest.TestCase):35def setUp(self) -> None:36if "addonmanager_metadata" in sys.modules:37sys.modules.pop("addonmanager_metadata")38self.packaging_version = None39if "packaging.version" in sys.modules:40self.packaging_version = sys.modules["packaging.version"]41sys.modules.pop("packaging.version")42
43def tearDown(self) -> None:44if self.packaging_version is not None:45sys.modules["packaging.version"] = self.packaging_version46
47def test_init_from_string_manual(self):48import addonmanager_metadata as amm49
50version = amm.Version()51version._parse_string_to_tuple = unittest.mock.MagicMock()52version._init_from_string("1.2.3beta")53self.assertTrue(version._parse_string_to_tuple.called)54
55def test_init_from_list_good(self):56"""Initialization from a list works for good input"""57import addonmanager_metadata as amm58
59test_cases = [60{"input": (1,), "output": [1, 0, 0, ""]},61{"input": (1, 2), "output": [1, 2, 0, ""]},62{"input": (1, 2, 3), "output": [1, 2, 3, ""]},63{"input": (1, 2, 3, "b1"), "output": [1, 2, 3, "b1"]},64]65for test_case in test_cases:66with self.subTest(test_case=test_case):67v = amm.Version(from_list=test_case["input"])68self.assertListEqual(test_case["output"], v.version_as_list)69
70def test_parse_string_to_tuple_normal(self):71"""Parsing of complete version string works for normal cases"""72import addonmanager_metadata as amm73
74cases = {75"1": [1, 0, 0, ""],76"1.2": [1, 2, 0, ""],77"1.2.3": [1, 2, 3, ""],78"1.2.3beta": [1, 2, 3, "beta"],79"12_345.6_7.8pre-alpha": [12345, 67, 8, "pre-alpha"],80# The above test is mostly to point out that Python gets permits underscore81# characters in a number.82}83for inp, output in cases.items():84with self.subTest(inp=inp, output=output):85version = amm.Version()86version._parse_string_to_tuple(inp)87self.assertListEqual(version.version_as_list, output)88
89def test_parse_string_to_tuple_invalid(self):90"""Parsing of invalid version string raises an exception"""91import addonmanager_metadata as amm92
93cases = {"One", "1,2,3", "1-2-3", "1/2/3"}94for inp in cases:95with self.subTest(inp=inp):96with self.assertRaises(ValueError):97version = amm.Version()98version._parse_string_to_tuple(inp)99
100def test_parse_final_entry_normal(self):101"""Parsing of the final entry works for normal cases"""102import addonmanager_metadata as amm103
104cases = {105"3beta": (3, "beta"),106"42.alpha": (42, ".alpha"),107"123.45.6": (123, ".45.6"),108"98_delta": (98, "_delta"),109"1 and some words": (1, " and some words"),110}111for inp, output in cases.items():112with self.subTest(inp=inp, output=output):113number, text = amm.Version._parse_final_entry(inp)114self.assertEqual(number, output[0])115self.assertEqual(text, output[1])116
117def test_parse_final_entry_invalid(self):118"""Invalid input raises an exception"""119import addonmanager_metadata as amm120
121cases = ["beta", "", ["a", "b"]]122for case in cases:123with self.subTest(case=case):124with self.assertRaises(ValueError):125amm.Version._parse_final_entry(case)126
127def test_operators_internal(self):128"""Test internal (non-package) comparison operators"""129sys.modules["packaging.version"] = None130import addonmanager_metadata as amm131
132cases = self.given_comparison_cases()133for case in cases:134with self.subTest(case=case):135first = amm.Version(case[0])136second = amm.Version(case[1])137self.assertEqual(first < second, case[0] < case[1])138self.assertEqual(first > second, case[0] > case[1])139self.assertEqual(first <= second, case[0] <= case[1])140self.assertEqual(first >= second, case[0] >= case[1])141self.assertEqual(first == second, case[0] == case[1])142
143@staticmethod144def given_comparison_cases():145return [146("0.0.0alpha", "1.0.0alpha"),147("0.0.0alpha", "0.1.0alpha"),148("0.0.0alpha", "0.0.1alpha"),149("0.0.0alpha", "0.0.0beta"),150("0.0.0alpha", "0.0.0alpha"),151("1.0.0alpha", "0.0.0alpha"),152("0.1.0alpha", "0.0.0alpha"),153("0.0.1alpha", "0.0.0alpha"),154("0.0.0beta", "0.0.0alpha"),155]156
157
158class TestDependencyType(unittest.TestCase):159"""Ensure that the DependencyType dataclass converts to the correct strings"""160
161def setUp(self) -> None:162from addonmanager_metadata import DependencyType163
164self.DependencyType = DependencyType165
166def test_string_conversion_automatic(self):167self.assertEqual(str(self.DependencyType.automatic), "automatic")168
169def test_string_conversion_internal(self):170self.assertEqual(str(self.DependencyType.internal), "internal")171
172def test_string_conversion_addon(self):173self.assertEqual(str(self.DependencyType.addon), "addon")174
175def test_string_conversion_python(self):176self.assertEqual(str(self.DependencyType.python), "python")177
178
179class TestUrlType(unittest.TestCase):180"""Ensure that the UrlType dataclass converts to the correct strings"""181
182def setUp(self) -> None:183from addonmanager_metadata import UrlType184
185self.UrlType = UrlType186
187def test_string_conversion_website(self):188self.assertEqual(str(self.UrlType.website), "website")189
190def test_string_conversion_repository(self):191self.assertEqual(str(self.UrlType.repository), "repository")192
193def test_string_conversion_bugtracker(self):194self.assertEqual(str(self.UrlType.bugtracker), "bugtracker")195
196def test_string_conversion_readme(self):197self.assertEqual(str(self.UrlType.readme), "readme")198
199def test_string_conversion_documentation(self):200self.assertEqual(str(self.UrlType.documentation), "documentation")201
202def test_string_conversion_discussion(self):203self.assertEqual(str(self.UrlType.discussion), "discussion")204
205
206class TestMetadataAuxiliaryFunctions(unittest.TestCase):207def test_get_first_supported_freecad_version_simple(self):208from addonmanager_metadata import (209Metadata,210Version,211get_first_supported_freecad_version,212)213
214expected_result = Version(from_string="0.20.2beta")215metadata = self.given_metadata_with_freecadmin_set(expected_result)216first_version = get_first_supported_freecad_version(metadata)217self.assertEqual(expected_result, first_version)218
219@staticmethod220def given_metadata_with_freecadmin_set(min_version):221from addonmanager_metadata import Metadata222
223metadata = Metadata()224metadata.freecadmin = min_version225return metadata226
227def test_get_first_supported_freecad_version_with_content(self):228from addonmanager_metadata import (229Metadata,230Version,231get_first_supported_freecad_version,232)233
234expected_result = Version(from_string="0.20.2beta")235metadata = self.given_metadata_with_freecadmin_in_content(expected_result)236first_version = get_first_supported_freecad_version(metadata)237self.assertEqual(expected_result, first_version)238
239@staticmethod240def given_metadata_with_freecadmin_in_content(min_version):241from addonmanager_metadata import Metadata, Version242
243v_list = min_version.version_as_list244metadata = Metadata()245wb1 = Metadata()246wb1.freecadmin = Version(from_list=[v_list[0] + 1, v_list[1], v_list[2], v_list[3]])247wb2 = Metadata()248wb2.freecadmin = Version(from_list=[v_list[0], v_list[1] + 1, v_list[2], v_list[3]])249wb3 = Metadata()250wb3.freecadmin = Version(from_list=[v_list[0], v_list[1], v_list[2] + 1, v_list[3]])251m1 = Metadata()252m1.freecadmin = min_version253metadata.content = {"workbench": [wb1, wb2, wb3], "macro": [m1]}254return metadata255
256
257class TestMetadataReader(unittest.TestCase):258"""Test reading metadata from XML"""259
260def setUp(self) -> None:261if "xml.etree.ElementTree" in sys.modules:262sys.modules.pop("xml.etree.ElementTree")263if "addonmanager_metadata" in sys.modules:264sys.modules.pop("addonmanager_metadata")265
266def tearDown(self) -> None:267if "xml.etree.ElementTree" in sys.modules:268sys.modules.pop("xml.etree.ElementTree")269if "addonmanager_metadata" in sys.modules:270sys.modules.pop("addonmanager_metadata")271
272def test_from_file(self):273from addonmanager_metadata import MetadataReader274
275MetadataReader.from_bytes = Mock()276with tempfile.NamedTemporaryFile(delete=False) as temp:277temp.write(b"Some data")278temp.close()279MetadataReader.from_file(temp.name)280self.assertTrue(MetadataReader.from_bytes.called)281MetadataReader.from_bytes.assert_called_once_with(b"Some data")282os.unlink(temp.name)283
284@unittest.skip("Breaks other tests, needs to be fixed")285def test_from_bytes(self):286import xml.etree.ElementTree287
288with unittest.mock.patch("xml.etree.ElementTree") as element_tree_mock:289from addonmanager_metadata import MetadataReader290
291MetadataReader._process_element_tree = Mock()292MetadataReader.from_bytes(b"Some data")293element_tree_mock.parse.assert_called_once_with(b"Some data")294
295def test_process_element_tree(self):296from addonmanager_metadata import MetadataReader297
298MetadataReader._determine_namespace = Mock(return_value="")299element_tree_mock = Mock()300MetadataReader._create_node = Mock()301MetadataReader._process_element_tree(element_tree_mock)302MetadataReader._create_node.assert_called_once()303
304def test_determine_namespace_found_full(self):305from addonmanager_metadata import MetadataReader306
307root = Mock()308root.tag = "{https://wiki.freecad.org/Package_Metadata}package"309found_ns = MetadataReader._determine_namespace(root)310self.assertEqual(found_ns, "{https://wiki.freecad.org/Package_Metadata}")311
312def test_determine_namespace_found_empty(self):313from addonmanager_metadata import MetadataReader314
315root = Mock()316root.tag = "package"317found_ns = MetadataReader._determine_namespace(root)318self.assertEqual(found_ns, "")319
320def test_determine_namespace_not_found(self):321from addonmanager_metadata import MetadataReader322
323root = Mock()324root.find = Mock(return_value=False)325with self.assertRaises(RuntimeError):326MetadataReader._determine_namespace(root)327
328def test_parse_child_element_simple_strings(self):329from addonmanager_metadata import Metadata, MetadataReader330
331tags = ["name", "date", "description", "icon", "classname", "subdirectory"]332for tag in tags:333with self.subTest(tag=tag):334text = f"Test Data for {tag}"335child = self.given_mock_tree_node(tag, text)336mock_metadata = Metadata()337MetadataReader._parse_child_element("", child, mock_metadata)338self.assertEqual(mock_metadata.__dict__[tag], text)339
340def test_parse_child_element_version(self):341from addonmanager_metadata import Metadata, Version, MetadataReader342
343mock_metadata = Metadata()344child = self.given_mock_tree_node("version", "1.2.3")345MetadataReader._parse_child_element("", child, mock_metadata)346self.assertEqual(Version("1.2.3"), mock_metadata.version)347
348def test_parse_child_element_version_bad(self):349from addonmanager_metadata import Metadata, Version, MetadataReader350
351mock_metadata = Metadata()352child = self.given_mock_tree_node("version", "1-2-3")353MetadataReader._parse_child_element("", child, mock_metadata)354self.assertEqual(Version("0.0.0"), mock_metadata.version)355
356def test_parse_child_element_lists_of_strings(self):357from addonmanager_metadata import Metadata, MetadataReader358
359tags = ["file", "tag"]360for tag in tags:361with self.subTest(tag=tag):362mock_metadata = Metadata()363expected_results = []364for i in range(10):365text = f"Test {i} for {tag}"366expected_results.append(text)367child = self.given_mock_tree_node(tag, text)368MetadataReader._parse_child_element("", child, mock_metadata)369self.assertEqual(len(mock_metadata.__dict__[tag]), 10)370self.assertListEqual(mock_metadata.__dict__[tag], expected_results)371
372def test_parse_child_element_lists_of_contacts(self):373from addonmanager_metadata import Metadata, Contact, MetadataReader374
375tags = ["maintainer", "author"]376for tag in tags:377with self.subTest(tag=tag):378mock_metadata = Metadata()379expected_results = []380for i in range(10):381text = f"Test {i} for {tag}"382email = f"Email {i} for {tag}" if i % 2 == 0 else None383expected_results.append(Contact(name=text, email=email))384child = self.given_mock_tree_node(tag, text, {"email": email})385MetadataReader._parse_child_element("", child, mock_metadata)386self.assertEqual(len(mock_metadata.__dict__[tag]), 10)387self.assertListEqual(mock_metadata.__dict__[tag], expected_results)388
389def test_parse_child_element_list_of_licenses(self):390from addonmanager_metadata import Metadata, License, MetadataReader391
392mock_metadata = Metadata()393expected_results = []394tag = "license"395for i in range(10):396text = f"Test {i} for {tag}"397file = f"Filename {i} for {tag}" if i % 2 == 0 else None398expected_results.append(License(name=text, file=file))399child = self.given_mock_tree_node(tag, text, {"file": file})400MetadataReader._parse_child_element("", child, mock_metadata)401self.assertEqual(len(mock_metadata.__dict__[tag]), 10)402self.assertListEqual(mock_metadata.__dict__[tag], expected_results)403
404def test_parse_child_element_list_of_urls(self):405from addonmanager_metadata import Metadata, Url, UrlType, MetadataReader406
407mock_metadata = Metadata()408expected_results = []409tag = "url"410for i in range(10):411text = f"Test {i} for {tag}"412url_type = UrlType(i % len(UrlType))413type = str(url_type)414branch = ""415if type == "repository":416branch = f"Branch {i} for {tag}"417expected_results.append(Url(location=text, type=url_type, branch=branch))418child = self.given_mock_tree_node(tag, text, {"type": type, "branch": branch})419MetadataReader._parse_child_element("", child, mock_metadata)420self.assertEqual(len(mock_metadata.__dict__[tag]), 10)421self.assertListEqual(mock_metadata.__dict__[tag], expected_results)422
423def test_parse_child_element_lists_of_dependencies(self):424from addonmanager_metadata import (425Metadata,426Dependency,427DependencyType,428MetadataReader,429)430
431tags = ["depend", "conflict", "replace"]432attributes = {433"version_lt": "1.0.0",434"version_lte": "1.0.0",435"version_eq": "1.0.0",436"version_gte": "1.0.0",437"version_gt": "1.0.0",438"condition": "$BuildVersionMajor<1",439"optional": True,440}441
442for tag in tags:443for attribute, attr_value in attributes.items():444with self.subTest(tag=tag, attribute=attribute):445mock_metadata = Metadata()446expected_results = []447for i in range(10):448text = f"Test {i} for {tag}"449dependency_type = DependencyType(i % len(DependencyType))450dependency_type_str = str(dependency_type)451expected = Dependency(package=text, dependency_type=dependency_type)452expected.__dict__[attribute] = attr_value453expected_results.append(expected)454child = self.given_mock_tree_node(455tag,456text,457{"type": dependency_type_str, attribute: str(attr_value)},458)459MetadataReader._parse_child_element("", child, mock_metadata)460self.assertEqual(len(mock_metadata.__dict__[tag]), 10)461self.assertListEqual(mock_metadata.__dict__[tag], expected_results)462
463def test_parse_child_element_ignore_unknown_tag(self):464from addonmanager_metadata import Metadata, MetadataReader465
466tag = "invalid_tag"467text = "Shouldn't matter"468child = self.given_mock_tree_node(tag, text)469mock_metadata = Metadata()470MetadataReader._parse_child_element("", child, mock_metadata)471self.assertNotIn(tag, mock_metadata.__dict__)472
473def test_parse_child_element_versions(self):474from addonmanager_metadata import Metadata, Version, MetadataReader475
476tags = ["version", "freecadmin", "freecadmax", "pythonmin"]477for tag in tags:478with self.subTest(tag=tag):479mock_metadata = Metadata()480text = "3.4.5beta"481child = self.given_mock_tree_node(tag, text)482MetadataReader._parse_child_element("", child, mock_metadata)483self.assertEqual(mock_metadata.__dict__[tag], Version(from_string=text))484
485def given_mock_tree_node(self, tag, text, attributes=None):486class MockTreeNode:487def __init__(self):488self.tag = tag489self.text = text490self.attrib = attributes if attributes is not None else []491
492return MockTreeNode()493
494def test_parse_content_valid(self):495from addonmanager_metadata import MetadataReader496
497valid_content_items = ["workbench", "macro", "preferencepack"]498MetadataReader._create_node = Mock()499for content_type in valid_content_items:500with self.subTest(content_type=content_type):501tree_mock = [self.given_mock_tree_node(content_type, None)]502metadata_mock = Mock()503MetadataReader._parse_content("", metadata_mock, tree_mock)504MetadataReader._create_node.assert_called_once()505MetadataReader._create_node.reset_mock()506
507def test_parse_content_invalid(self):508from addonmanager_metadata import MetadataReader509
510MetadataReader._create_node = Mock()511content_item = "no_such_content_type"512tree_mock = [self.given_mock_tree_node(content_item, None)]513metadata_mock = Mock()514MetadataReader._parse_content("", metadata_mock, tree_mock)515MetadataReader._create_node.assert_not_called()516
517
518class TestMetadataReaderIntegration(unittest.TestCase):519"""Full-up tests of the MetadataReader class (no mocking)."""520
521def setUp(self) -> None:522self.test_data_dir = os.path.join(os.path.dirname(__file__), "..", "data")523remove_list = []524for key in sys.modules:525if "addonmanager_metadata" in key:526remove_list.append(key)527for key in remove_list:528print(f"Removing {key}")529sys.modules.pop(key)530
531def test_loading_simple_metadata_file(self):532from addonmanager_metadata import (533Contact,534Dependency,535License,536MetadataReader,537Url,538UrlType,539Version,540)541
542filename = os.path.join(self.test_data_dir, "good_package.xml")543metadata = MetadataReader.from_file(filename)544self.assertEqual("Test Workbench", metadata.name)545self.assertEqual("A package.xml file for unit testing.", metadata.description)546self.assertEqual(Version("1.0.1"), metadata.version)547self.assertEqual("2022-01-07", metadata.date)548self.assertEqual("Resources/icons/PackageIcon.svg", metadata.icon)549self.assertListEqual([License(name="LGPL-2.1", file="LICENSE")], metadata.license)550self.assertListEqual(551[Contact(name="FreeCAD Developer", email="developer@freecad.org")],552metadata.maintainer,553)554self.assertListEqual(555[556Url(557location="https://github.com/chennes/FreeCAD-Package",558type=UrlType.repository,559branch="main",560),561Url(562location="https://github.com/chennes/FreeCAD-Package/blob/main/README.md",563type=UrlType.readme,564),565],566metadata.url,567)568self.assertListEqual(["Tag0", "Tag1"], metadata.tag)569self.assertIn("workbench", metadata.content)570self.assertEqual(len(metadata.content["workbench"]), 1)571wb_metadata = metadata.content["workbench"][0]572self.assertEqual("MyWorkbench", wb_metadata.classname)573self.assertEqual("./", wb_metadata.subdirectory)574self.assertListEqual(["TagA", "TagB", "TagC"], wb_metadata.tag)575
576def test_multiple_workbenches(self):577from addonmanager_metadata import MetadataReader578
579filename = os.path.join(self.test_data_dir, "workbench_only.xml")580metadata = MetadataReader.from_file(filename)581self.assertIn("workbench", metadata.content)582self.assertEqual(len(metadata.content["workbench"]), 3)583expected_wb_classnames = [584"MyFirstWorkbench",585"MySecondWorkbench",586"MyThirdWorkbench",587]588for wb in metadata.content["workbench"]:589self.assertIn(wb.classname, expected_wb_classnames)590expected_wb_classnames.remove(wb.classname)591self.assertEqual(len(expected_wb_classnames), 0)592
593def test_multiple_macros(self):594from addonmanager_metadata import MetadataReader595
596filename = os.path.join(self.test_data_dir, "macro_only.xml")597metadata = MetadataReader.from_file(filename)598self.assertIn("macro", metadata.content)599self.assertEqual(len(metadata.content["macro"]), 2)600expected_wb_files = ["MyMacro.FCStd", "MyOtherMacro.FCStd"]601for wb in metadata.content["macro"]:602self.assertIn(wb.file[0], expected_wb_files)603expected_wb_files.remove(wb.file[0])604self.assertEqual(len(expected_wb_files), 0)605
606def test_multiple_preference_packs(self):607from addonmanager_metadata import MetadataReader608
609filename = os.path.join(self.test_data_dir, "prefpack_only.xml")610metadata = MetadataReader.from_file(filename)611self.assertIn("preferencepack", metadata.content)612self.assertEqual(len(metadata.content["preferencepack"]), 3)613expected_packs = ["MyFirstPack", "MySecondPack", "MyThirdPack"]614for wb in metadata.content["preferencepack"]:615self.assertIn(wb.name, expected_packs)616expected_packs.remove(wb.name)617self.assertEqual(len(expected_packs), 0)618
619def test_content_combination(self):620from addonmanager_metadata import MetadataReader621
622filename = os.path.join(self.test_data_dir, "combination.xml")623metadata = MetadataReader.from_file(filename)624self.assertIn("preferencepack", metadata.content)625self.assertEqual(len(metadata.content["preferencepack"]), 1)626self.assertIn("macro", metadata.content)627self.assertEqual(len(metadata.content["macro"]), 1)628self.assertIn("workbench", metadata.content)629self.assertEqual(len(metadata.content["workbench"]), 1)630
631
632if __name__ == "__main__":633unittest.main()634