diff --git a/.gitattributes b/.gitattributes index 61d299b5..f45a8178 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.py text eol=lf *.sh text eol=lf +*.svg text eol=lf diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index bfd40323..12361211 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -5,6 +5,7 @@ jobs: main: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: [ "3.8", "3.12" ] # Lowest and highest. steps: @@ -13,4 +14,6 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: "pip" # caching pip dependencies + - run: pip install -r requirements.txt + - run: pip install -r requirements_dev.txt - uses: pre-commit/action@v3.0.1 diff --git a/.gitignore b/.gitignore index 9e7f631d..84311f77 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ documentation/src/generators.inc *.ipynb_checkpoints venv/ .vscode/ +tracker.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 905984aa..6c0555fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,7 @@ ci: autofix_prs: true autoupdate_schedule: quarterly +exclude: '\.svg$' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -99,3 +100,13 @@ repos: hooks: - id: codespell exclude: ^(boxes|locale|po|static)/ # todo folder boxes + + - repo: local + hooks: + - id: pytest + name: pytest + entry: pytest + language: system + types: [ python ] + pass_filenames: false + always_run: true diff --git a/requirements_dev.txt b/requirements_dev.txt index ba521cb0..d53760d5 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,5 @@ -inkex +lxml mypy pre-commit +pytest>=8.1.1 types-Markdown diff --git a/scripts/boxes b/scripts/boxes old mode 100644 new mode 100755 index 4746e698..7d9fd6c8 --- a/scripts/boxes +++ b/scripts/boxes @@ -12,12 +12,12 @@ Options: -h --help Show this screen. --list List available generators. """ +from __future__ import annotations import gettext import os import sys from pathlib import Path -from typing import Type try: import boxes @@ -48,21 +48,17 @@ def print_grouped_generators() -> None: def create_example_every_generator() -> None: + print("Generating SVG examples for every possible generator.") for group in generator_groups(): - # print() - defaultStdout = sys.stdout - for boxExample in group.generators: boxName = boxExample.__name__ notTestGenerator = ('GridfinityTrayLayout', 'TrayLayout', 'TrayLayoutFile', 'TypeTray', 'Edges',) - brokenGenerator = ('WallConsole', 'HolePattern', 'FillTest',) # todo fix? + brokenGenerator = () avoidGenerator = notTestGenerator + brokenGenerator if boxName in avoidGenerator: print(f"SKIP: {boxName}") continue - # print(f"generate example for: {boxName}") - - # sys.stdout = buffer = StringIO() + print(f"Generate example for: {boxName}") box = boxExample() box.translations = get_translation() @@ -71,18 +67,10 @@ def create_example_every_generator() -> None: box.open() box.render() boxData = box.close() - # - # sys.stdout = defaultStdout - # output = buffer.getvalue() # Get the captured output - # if 0 < len(output): - # print("ERROR: " + boxExample.__name__) file = Path('examples') / (boxName + '.svg') file.write_bytes(boxData.getvalue()) - # with os.fdopen(sys.stdout.fileno(), "wb", closefd=False) if box.output == "-" else open(box.output, 'wb') as f: - # f.write(boxData.getvalue()) - def get_translation(): try: @@ -125,7 +113,7 @@ def group_generators(generators): return groups -def generators_by_name() -> dict[str, Type[boxes.Boxes]]: +def generators_by_name() -> dict[str, type[boxes.Boxes]]: all_generators = boxes.generators.getAllBoxGenerators() return { diff --git a/tests/data/.gitkeep b/tests/data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_svg.py b/tests/test_svg.py new file mode 100644 index 00000000..b608b524 --- /dev/null +++ b/tests/test_svg.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from lxml import etree + +try: + import boxes +except ImportError: + sys.path.append(Path(__file__).resolve().parent.parent.__str__()) + import boxes + +import boxes.generators + + +class TestSVG: + all_generators = boxes.generators.getAllBoxGenerators().values() + + notTestGenerator = ('GridfinityTrayLayout', 'TrayLayout', 'TrayLayoutFile', 'TypeTray', 'Edges',) + brokenGenerator = () + avoidGenerator = notTestGenerator + brokenGenerator + + def test_generators_available(self) -> None: + assert len(self.all_generators) != 0 + + # svgcheck currently do not allow inkscape custom tags. + # @staticmethod + # def is_valid_svg(file_path: str) -> bool: + # result = subprocess.run(['svgcheck', file_path], capture_output=True, text=True) + # return "INFO: File conforms to SVG requirements." in result.stdout + + @staticmethod + def is_valid_xml_by_lxml(xml_string: str) -> bool: + try: + etree.fromstring(xml_string) + return True + except etree.XMLSyntaxError: + return False + + @staticmethod + def idfunc(val) -> str: + return f"{val.__name__}" + + @pytest.mark.parametrize( + "generator", + all_generators, + ids=idfunc.__func__, + ) + def test_generator(self, generator: type[boxes.Boxes], capsys) -> None: + boxName = generator.__name__ + if boxName in self.avoidGenerator: + pytest.skip("Skipped generator") + box = generator() + box.parseArgs("") + box.metadata["reproducible"] = True + box.open() + box.render() + boxData = box.close() + + out, err = capsys.readouterr() + + assert 100 < boxData.__sizeof__(), "No data generated." + assert 0 == len(out), "Console output generated." + assert 0 == len(err), "Console error generated." + + # Use external library lxml as cross-check. + assert self.is_valid_xml_by_lxml(boxData.getvalue()) is True, "Invalid XML according to library lxml." + + file = Path(__file__).resolve().parent / 'data' / (boxName + '.svg') + file.write_bytes(boxData.getvalue()) + + # Use example data from repository as reference data. + referenceData = Path(__file__).resolve().parent.parent / 'examples' / (boxName + '.svg') + assert referenceData.exists() is True, "Reference file for comparison does not exist." + assert referenceData.is_file() is True, "Reference file for comparison does not exist." + assert referenceData.read_bytes() == boxData.getvalue(), "SVG files are not equal. If change is intended, please update example files."