From 3fb26e6940b5ae752308d8a33f2608d266795153 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 29 Dec 2021 19:58:20 +0100 Subject: Basic round-trip works --- gerbonara/gerber/tests/conftest.py | 22 ++++ gerbonara/gerber/tests/image_support.py | 63 ++++++++++ gerbonara/gerber/tests/panelize/test_rs274x.py | 70 ----------- .../tests/resources/example_outline_with_arcs.gbr | 33 ++++++ gerbonara/gerber/tests/test_rs274x.py | 131 +++++++++++++-------- 5 files changed, 201 insertions(+), 118 deletions(-) create mode 100644 gerbonara/gerber/tests/conftest.py create mode 100644 gerbonara/gerber/tests/image_support.py delete mode 100644 gerbonara/gerber/tests/panelize/test_rs274x.py create mode 100644 gerbonara/gerber/tests/resources/example_outline_with_arcs.gbr (limited to 'gerbonara/gerber/tests') diff --git a/gerbonara/gerber/tests/conftest.py b/gerbonara/gerber/tests/conftest.py new file mode 100644 index 0000000..0ad2555 --- /dev/null +++ b/gerbonara/gerber/tests/conftest.py @@ -0,0 +1,22 @@ + +import pytest + +from .image_support import ImageDifference + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_assertrepr_compare(op, left, right): + if isinstance(left, ImageDifference) or isinstance(right, ImageDifference): + diff = left if isinstance(left, ImageDifference) else right + return [ + f'Image difference assertion failed.', + f' Reference: {diff.ref_path}', + f' Actual: {diff.out_path}', + f' Calculated difference: {diff}', ] + +# store report in node object so tmp_gbr can determine if the test failed. +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + rep = outcome.get_result() + setattr(item, f'rep_{rep.when}', rep) + diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py new file mode 100644 index 0000000..ee8e6b9 --- /dev/null +++ b/gerbonara/gerber/tests/image_support.py @@ -0,0 +1,63 @@ +import subprocess +from pathlib import Path +import tempfile + +import numpy as np + +class ImageDifference(float): + def __init__(self, value, ref_path, out_path): + super().__init__(value) + self.ref_path, self.out_path = ref_path, out_path + +def run_cargo_cmd(cmd, args, **kwargs): + if cmd.upper() in os.environ: + return subprocess.run([os.environ[cmd.upper()], *args], **kwargs) + + try: + return subprocess.run([cmd, *args], **kwargs) + + except FileNotFoundError: + return subprocess.run([str(Path.home() / '.cargo' / 'bin' / cmd), *args], **kwargs) + +def svg_to_png(in_svg, out_png): + run_cargo_cmd('resvg', [in_svg, out_png], check=True, stdout=subprocess.DEVNULL) + +def gbr_to_svg(in_gbr, out_svg): + cmd = ['gerbv', '-x', 'svg', + '--border=0', + #f'--origin={origin_x:.6f}x{origin_y:.6f}', f'--window_inch={width:.6f}x{height:.6f}', + '--foreground=#ffffff', + '-o', str(out_svg), str(in_gbr)] + subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def gerber_difference(reference, actual): + with tempfile.NamedTemporaryFile(suffix='.svg') as out_svg,\ + tempfile.NamedTemporaryFile(suffix='.svg') as ref_svg: + + gbr_to_svg(reference, ref_svg.name) + gbr_to_svg(actual, act_svg.name) + + diff = svg_difference(ref_svg.name, act_svg.name) + diff.ref_path, diff.act_path = reference, actual + return diff + +def svg_difference(reference, actual): + with tempfile.NamedTemporaryFile(suffix='.png') as ref_png,\ + tempfile.NamedTemporaryFile(suffix='.png') as act_png: + + svg_to_png(reference, ref_png.name) + svg_to_png(actual, act_png.name) + + diff = image_difference(ref_png.name, act_png.name) + diff.ref_path, diff.act_path = reference, actual + return diff + +def image_difference(reference, actual): + ref = np.array(Image.open(reference)).astype(float) + out = np.array(Image.open(actual)).astype(float) + + ref, out = ref.mean(axis=2), out.mean(axis=2) # convert to grayscale + delta = np.abs(out - ref).astype(float) / 255 + return ImageDifference(delta.mean(), ref, out) + + diff --git a/gerbonara/gerber/tests/panelize/test_rs274x.py b/gerbonara/gerber/tests/panelize/test_rs274x.py deleted file mode 100644 index 73f3172..0000000 --- a/gerbonara/gerber/tests/panelize/test_rs274x.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama - -import os -import tempfile -from pathlib import Path -from contextlib import contextmanager -import unittest -from ...rs274x import read - -class TestRs274x(unittest.TestCase): - @classmethod - def setUpClass(cls): - here = Path(__file__).parent - cls.EXPECTSDIR = here / 'expects' - cls.METRIC_FILE = here / 'data' / 'ref_gerber_metric.gtl' - cls.INCH_FILE = here / 'data' / 'ref_gerber_inch.gtl' - cls.SQ_FILE = here / 'data' / 'ref_gerber_single_quadrant.gtl' - - @contextmanager - def _check_result(self, reference_fn): - with tempfile.NamedTemporaryFile('rb') as tmp_out: - yield tmp_out.name - - actual = tmp_out.read() - expected = (self.EXPECTSDIR / reference_fn).read_bytes() - - print('==== ACTUAL ===') - print(actual.decode()) - print() - print() - print('==== EXPECTED ===') - print(expected.decode()) - print() - print() - self.assertEqual(actual, expected) - - def test_save(self): - with self._check_result('RS2724x_save.gtl') as outfile: - gerber = read(self.METRIC_FILE) - gerber.write(outfile) - - def test_to_inch(self): - with self._check_result('RS2724x_to_inch.gtl') as outfile: - gerber = read(self.METRIC_FILE) - gerber.to_inch() - gerber.format = (2,5) - gerber.write(outfile) - - def test_to_metric(self): - with self._check_result('RS2724x_to_metric.gtl') as outfile: - gerber = read(self.INCH_FILE) - gerber.to_metric() - gerber.format = (3, 4) - gerber.write(outfile) - - def test_offset(self): - with self._check_result('RS2724x_offset.gtl') as outfile: - gerber = read(self.METRIC_FILE) - gerber.offset(11, 5) - gerber.write(outfile) - - def test_rotate(self): - with self._check_result('RS2724x_rotate.gtl') as outfile: - gerber = read(self.METRIC_FILE) - gerber.rotate(20, (10,10)) - gerber.write(outfile) - diff --git a/gerbonara/gerber/tests/resources/example_outline_with_arcs.gbr b/gerbonara/gerber/tests/resources/example_outline_with_arcs.gbr new file mode 100644 index 0000000..62c5693 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_outline_with_arcs.gbr @@ -0,0 +1,33 @@ +G04 Layer_Color=16711935* +%FSLAX25Y25*% +%MOIN*% +G70* +G01* +G75* +%ADD26C,0.01000*% +D26* +X354331Y177165D02* +G03* +X334646Y196850I-19685J0D01* +G01* +Y0D02* +G03* +X354331Y19685I0J19685D01* +G01* +X0D02* +G03* +X19685Y0I19685J0D01* +G01* +Y196850D02* +G03* +X0Y177165I0J-19685D01* +G01* +X354331Y19685D02* +Y177165D01* +X19685Y196850D02* +X334646D01* +X19685Y0D02* +X334646D01* +X0Y19685D02* +Y177165D01* +M02* diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py index e430f36..beaea11 100644 --- a/gerbonara/gerber/tests/test_rs274x.py +++ b/gerbonara/gerber/tests/test_rs274x.py @@ -4,52 +4,87 @@ # Author: Hamilton Kibbe import os import pytest +import functools +import tempfile +import shutil +from argparse import Namespace +from pathlib import Path + +from ..rs274x import GerberFile + +from .image_support import gerber_difference + + +fail_dir = Path('gerbonara_test_failures') +@pytest.fixture(scope='session', autouse=True) +def clear_failure_dir(request): + if fail_dir.is_dir(): + shutil.rmtree(fail_dir) + +@pytest.fixture +def tmp_gbr(request): + with tempfile.NamedTemporaryFile(suffix='.gbr') as tmp_out_gbr: + + yield Path(tmp_out_gbr.name) + + if request.node.rep_call.failed: + module, _, test_name = request.node.nodeid.rpartition('::') + _test, _, test_name = test_name.partition('_') + test_name = test_name.replace('[', '_').replace(']', '_') + fail_dir.mkdir(exist_ok=True) + perm_path = fail_dir / f'failure_{test_name}.gbr' + shutil.copy(tmp_out_gbr.name, perm_path) + print('Failing output saved to {perm_path}') + +@pytest.mark.parametrize('reference', [ l.strip() for l in ''' +board_outline.GKO +example_outline_with_arcs.gbr +''' +#example_two_square_boxes.gbr +#example_coincident_hole.gbr +#example_cutin.gbr +#example_cutin_multiple.gbr +#example_flash_circle.gbr +#example_flash_obround.gbr +#example_flash_polygon.gbr +#example_flash_rectangle.gbr +#example_fully_coincident.gbr +#example_guess_by_content.g0 +#example_holes_dont_clear.gbr +#example_level_holes.gbr +#example_not_overlapping_contour.gbr +#example_not_overlapping_touching.gbr +#example_overlapping_contour.gbr +#example_overlapping_touching.gbr +#example_simple_contour.gbr +#example_single_contour_1.gbr +#example_single_contour_2.gbr +#example_single_contour_3.gbr +#example_am_exposure_modifier.gbr +#bottom_copper.GBL +#bottom_mask.GBS +#bottom_silk.GBO +#eagle_files/copper_bottom_l4.gbr +#eagle_files/copper_inner_l2.gbr +#eagle_files/copper_inner_l3.gbr +#eagle_files/copper_top_l1.gbr +#eagle_files/profile.gbr +#eagle_files/silkscreen_bottom.gbr +#eagle_files/silkscreen_top.gbr +#eagle_files/soldermask_bottom.gbr +#eagle_files/soldermask_top.gbr +#eagle_files/solderpaste_bottom.gbr +#eagle_files/solderpaste_top.gbr +#multiline_read.ger +#test_fine_lines_x.gbr +#test_fine_lines_y.gbr +#top_copper.GTL +#top_mask.GTS +#top_silk.GTO +''' +'''.splitlines() if l ]) +def test_round_trip(tmp_gbr, reference): + ref = Path(__file__).parent / 'resources' / reference + GerberFile.open(ref).save(tmp_gbr) + assert gerber_difference(ref, tmp_gbr) < 0.02 -from ..rs274x import read, GerberFile - - -TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), "resources/top_copper.GTL") - -MULTILINE_READ_FILE = os.path.join( - os.path.dirname(__file__), "resources/multiline_read.ger" -) - - -def test_read(): - top_copper = read(TOP_COPPER_FILE) - assert isinstance(top_copper, GerberFile) - - -def test_multiline_read(): - multiline = read(MULTILINE_READ_FILE) - assert isinstance(multiline, GerberFile) - assert 11 == len(multiline.statements) - - -def test_comments_parameter(): - top_copper = read(TOP_COPPER_FILE) - assert top_copper.comments[0] == "This is a comment,:" - - -def test_size_parameter(): - top_copper = read(TOP_COPPER_FILE) - size = top_copper.size - pytest.approx(size[0], 2.256900, 6) - pytest.approx(size[1], 1.500000, 6) - - -def test_conversion(): - top_copper = read(TOP_COPPER_FILE) - assert top_copper.units == "inch" - top_copper_inch = read(TOP_COPPER_FILE) - top_copper.to_metric() - for statement in top_copper_inch.statements: - statement.to_metric() - for primitive in top_copper_inch.primitives: - primitive.to_metric() - assert top_copper.units == "metric" - for i, m in zip(top_copper.statements, top_copper_inch.statements): - assert i == m - - for i, m in zip(top_copper.primitives, top_copper_inch.primitives): - assert i == m -- cgit