summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/tests
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber/tests')
-rw-r--r--gerbonara/gerber/tests/conftest.py2
-rw-r--r--gerbonara/gerber/tests/image_support.py50
-rw-r--r--gerbonara/gerber/tests/test_rs274x.py211
3 files changed, 206 insertions, 57 deletions
diff --git a/gerbonara/gerber/tests/conftest.py b/gerbonara/gerber/tests/conftest.py
index c8fd475..c6a1221 100644
--- a/gerbonara/gerber/tests/conftest.py
+++ b/gerbonara/gerber/tests/conftest.py
@@ -8,8 +8,6 @@ def pytest_assertrepr_compare(op, left, right):
diff = left if isinstance(left, ImageDifference) else right
return [
f'Image difference assertion failed.',
- f' Reference: {diff.ref_path}',
- f' Actual: {diff.act_path}',
f' Calculated difference: {diff}', ]
# store report in node object so tmp_gbr can determine if the test failed.
diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py
index 8ce0d72..49217c2 100644
--- a/gerbonara/gerber/tests/image_support.py
+++ b/gerbonara/gerber/tests/image_support.py
@@ -3,6 +3,9 @@ from pathlib import Path
import tempfile
import os
from functools import total_ordering
+import shutil
+import bs4
+from contextlib import contextmanager
import numpy as np
from PIL import Image
@@ -35,9 +38,9 @@ def run_cargo_cmd(cmd, args, **kwargs):
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)
+ run_cargo_cmd('resvg', ['--dpi', '200', in_svg, out_png], check=True, stdout=subprocess.DEVNULL)
-def gbr_to_svg(in_gbr, out_svg, origin=(0, 0), size=(10, 10)):
+def gbr_to_svg(in_gbr, out_svg, origin=(0, 0), size=(6, 6)):
x, y = origin
w, h = size
cmd = ['gerbv', '-x', 'svg',
@@ -47,18 +50,51 @@ def gbr_to_svg(in_gbr, out_svg, origin=(0, 0), size=(10, 10)):
'-o', str(out_svg), str(in_gbr)]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-def gerber_difference(reference, actual, diff_out=None):
+@contextmanager
+def svg_soup(filename):
+ with open(filename, 'r') as f:
+ soup = bs4.BeautifulSoup(f.read(), 'xml')
+
+ yield soup
+
+ with open(filename, 'w') as f:
+ f.write(str(soup))
+
+def cleanup_clips(soup):
+ for group in soup.find_all('g'):
+ # gerbv uses Cairo's SVG canvas. Cairo's SVG canvas is kind of broken. It has no support for unit
+ # handling at all, which means the output files just end up being in pixels at 72 dpi. Further, it
+ # seems gerbv's aperture macro rendering interacts poorly with Cairo's SVG export. gerbv renders
+ # aperture macros into a new surface, which for some reason gets clipped by Cairo to the given
+ # canvas size. This is just wrong, so we just nuke the clip path from these SVG groups here.
+ #
+ # Apart from being graphically broken, this additionally causes very bad rendering performance.
+ del group['clip-path'] # remove broken clip
+
+def gerber_difference(reference, actual, diff_out=None, svg_transform=None, size=(10,10)):
with tempfile.NamedTemporaryFile(suffix='.svg') as act_svg,\
tempfile.NamedTemporaryFile(suffix='.svg') as ref_svg:
- gbr_to_svg(reference, ref_svg.name)
- gbr_to_svg(actual, act_svg.name)
+ gbr_to_svg(reference, ref_svg.name, size=size)
+ gbr_to_svg(actual, act_svg.name, size=size)
+
+ with svg_soup(ref_svg.name) as soup:
+ if svg_transform is not None:
+ soup.find('g', attrs={'id': 'surface1'})['transform'] = svg_transform
+ cleanup_clips(soup)
+
+ with svg_soup(act_svg.name) as soup:
+ cleanup_clips(soup)
+
+ # FIXME DEBUG
+ shutil.copyfile(act_svg.name, '/tmp/test-act.svg')
+ shutil.copyfile(ref_svg.name, '/tmp/test-ref.svg')
return svg_difference(ref_svg.name, act_svg.name, diff_out=diff_out)
def svg_difference(reference, actual, diff_out=None):
- with tempfile.NamedTemporaryFile(suffix='.png') as ref_png,\
- tempfile.NamedTemporaryFile(suffix='.png') as act_png:
+ with tempfile.NamedTemporaryFile(suffix='-ref.png') as ref_png,\
+ tempfile.NamedTemporaryFile(suffix='-act.png') as act_png:
svg_to_png(reference, ref_png.name)
svg_to_png(actual, act_png.name)
diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py
index 28ee891..d91609f 100644
--- a/gerbonara/gerber/tests/test_rs274x.py
+++ b/gerbonara/gerber/tests/test_rs274x.py
@@ -4,22 +4,28 @@
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
import os
import re
-import pytest
+import math
import functools
import tempfile
import shutil
from argparse import Namespace
+from itertools import chain
from pathlib import Path
+import pytest
+
from ..rs274x import GerberFile
+from ..cam import FileSettings
from .image_support import gerber_difference
+deg_to_rad = lambda a: a/180 * math.pi
+
fail_dir = Path('gerbonara_test_failures')
@pytest.fixture(scope='session', autouse=True)
def clear_failure_dir(request):
- for f in fail_dir.glob('*.gbr'):
+ for f in chain(fail_dir.glob('*.gbr'), fail_dir.glob('*.png')):
f.unlink()
reference_path = lambda reference: Path(__file__).parent / 'resources' / reference
@@ -42,61 +48,170 @@ def temp_files(request):
shutil.copy(tmp_out_gbr.name, perm_path_gbr)
shutil.copy(tmp_out_png.name, perm_path_png)
print(f'Failing output saved to {perm_path_gbr}')
- print(f'Difference image saved to {perm_path_png}')
print(f'Reference file is {reference_path(request.node.funcargs["reference"])}')
+ print(f'Difference image saved to {perm_path_png}')
+ print(f'gerbv command line:')
+ print(f'gerbv {perm_path_gbr} {reference_path(request.node.funcargs["reference"])}')
+
+to_gerbv_svg_units = lambda val, unit='mm': val*72 if unit == 'inch' else val/25.4*72
+
+REFERENCE_FILES = [ 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 ]
+
+MIN_REFERENCE_FILES = [
+ 'example_two_square_boxes.gbr',
+ 'example_outline_with_arcs.gbr',
+ 'example_flash_circle.gbr',
+ 'example_flash_polygon.gbr',
+ 'example_flash_rectangle.gbr',
+ 'example_simple_contour.gbr',
+ 'example_am_exposure_modifier.gbr',
+ 'bottom_copper.GBL',
+ 'bottom_silk.GBO',
+ 'eagle_files/copper_bottom_l4.gbr'
+ ]
@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
@pytest.mark.filterwarnings('ignore::SyntaxWarning')
-@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 ])
+@pytest.mark.parametrize('reference', REFERENCE_FILES)
def test_round_trip(temp_files, reference):
tmp_gbr, tmp_png = temp_files
ref = reference_path(reference)
+
GerberFile.open(ref).save(tmp_gbr)
+
mean, max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png)
assert mean < 1e-6
assert max < 0.1
+TEST_ANGLES = [90, 180, 270, 30, 1.5, 10, 360, 1024, -30, -90]
+TEST_OFFSETS = [(0, 0), (100, 0), (0, 100), (2, 0), (10, 100)]
+
+@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
+@pytest.mark.filterwarnings('ignore::SyntaxWarning')
+@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES)
+@pytest.mark.parametrize('angle', TEST_ANGLES)
+def test_rotation(temp_files, reference, angle):
+ if 'flash_rectangle' in reference and angle == 1024:
+ # gerbv's rendering of this is broken, the hole is missing.
+ return
+
+ tmp_gbr, tmp_png = temp_files
+ ref = reference_path(reference)
+
+ f = GerberFile.open(ref)
+ f.rotate(deg_to_rad(angle))
+ f.save(tmp_gbr)
+
+ cx, cy = 0, to_gerbv_svg_units(10, unit='inch')
+ mean, max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png, svg_transform=f'rotate({angle} {cx} {cy})')
+ assert mean < 1e-3 # relax mean criterion compared to above.
+ assert max < 0.9
+
+@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
+@pytest.mark.filterwarnings('ignore::SyntaxWarning')
+@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES)
+@pytest.mark.parametrize('angle', TEST_ANGLES)
+@pytest.mark.parametrize('center', [(0, 0), (-10, -10), (10, 10), (10, 0), (0, -10), (-10, 10), (10, 20)])
+def test_rotation_center(temp_files, reference, angle, center):
+ tmp_gbr, tmp_png = temp_files
+ ref = reference_path(reference)
+
+ f = GerberFile.open(ref)
+ f.rotate(deg_to_rad(angle), center=center)
+ f.save(tmp_gbr)
+
+ # calculate circle center in SVG coordinates
+ size = (10, 10) # inches
+ cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(10, 'inch')-to_gerbv_svg_units(center[1], 'mm')
+ mean, max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png,
+ svg_transform=f'rotate({angle} {cx} {cy})',
+ size=size)
+ assert mean < 1e-3
+ assert max < 0.9
+
+@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
+@pytest.mark.filterwarnings('ignore::SyntaxWarning')
+@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES)
+@pytest.mark.parametrize('offset', TEST_OFFSETS)
+def test_offset(temp_files, reference, offset):
+ tmp_gbr, tmp_png = temp_files
+ ref = reference_path(reference)
+
+ f = GerberFile.open(ref)
+ f.offset(*offset)
+ f.save(tmp_gbr, settings=FileSettings(unit=f.unit, number_format=(4,7)))
+
+ # flip y offset since svg's y axis is flipped compared to that of gerber
+ dx, dy = to_gerbv_svg_units(offset[0]), -to_gerbv_svg_units(offset[1])
+ mean, max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png, svg_transform=f'translate({dx} {dy})')
+ assert mean < 1e-4
+ assert max < 0.9
+
+@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
+@pytest.mark.filterwarnings('ignore::SyntaxWarning')
+@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES)
+@pytest.mark.parametrize('angle', TEST_ANGLES)
+@pytest.mark.parametrize('center', [(0, 0), (10, 0), (0, -10), (10, 20)])
+@pytest.mark.parametrize('offset', [(0, 0), (100, 0), (0, 100), (100, 100), (100, 10)])
+def test_combined(temp_files, reference, angle, center, offset):
+ tmp_gbr, tmp_png = temp_files
+ ref = reference_path(reference)
+
+ f = GerberFile.open(ref)
+ f.rotate(deg_to_rad(angle), center=center)
+ f.offset(*offset)
+ f.save(tmp_gbr)
+
+ size = (10, 10) # inches
+ cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(10, 'inch')-to_gerbv_svg_units(center[1], 'mm')
+ dx, dy = to_gerbv_svg_units(offset[0]), -to_gerbv_svg_units(offset[1])
+ mean, max = gerber_difference(ref, tmp_gbr, diff_out=tmp_png,
+ svg_transform=f'rotate({anlge} {cx} {cy}) translate({dx} {dy})',
+ size=size)
+ assert mean < 1e-4
+ assert max < 0.9
+