summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/tests
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber/tests')
-rw-r--r--gerbonara/gerber/tests/conftest.py22
-rw-r--r--gerbonara/gerber/tests/image_support.py63
-rw-r--r--gerbonara/gerber/tests/panelize/test_rs274x.py70
-rw-r--r--gerbonara/gerber/tests/resources/example_outline_with_arcs.gbr33
-rw-r--r--gerbonara/gerber/tests/test_rs274x.py131
5 files changed, 201 insertions, 118 deletions
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 <opiopan@gmail.com>
-
-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 <ham@hamiltonkib.be>
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