Fix tests

This commit is contained in:
jaseg 2023-02-21 22:48:14 +01:00
parent 9a6bc691cb
commit 6231f67139
9 changed files with 68 additions and 34 deletions

View file

@ -67,6 +67,21 @@ class Coordinate(click.ParamType):
except ValueError:
self.fail(f'{value!r} is not a valid coordinate. A coordinate consists of exactly {self.dimension} comma-separate floating-point numbers.')
class Rotation(click.ParamType):
name = 'rotation'
def convert(self, value, param, ctx):
try:
coords = map(float, value.split(','))
if len(coords) not in (1, 3):
raise ValueError()
theta, x, y, *_rest = *coords, 0, 0
return theta, x, y
except ValueError:
self.fail(f'{value!r} is not a valid rotation. A rotation is either a floating point angle ("[theta]"), or the same angle followed by comma-separated X and Y coordinates of the rotation center ("[theta],[cx],[cy]").')
class Unit(click.Choice):
name = 'unit'
@ -109,7 +124,8 @@ def cli():
@click.option('--force-zip', is_flag=True, help='''Force treating input path as a zip file (default: guess file type
from extension and contents)''')
@click.option('--top/--bottom', default=True, help='Which side of the board to render')
@click.option('--command-line-units', type=Unit(), default=MM, help='Units for values given in other options. Default: millimeter')
@click.option('--command-line-units', type=Unit(), default=MM, help='''Units for values given in other options. Default:
millimeter''')
@click.option('--margin', type=float, default=0.0, help='Add space around the board inside the viewport')
@click.option('--force-bounds', help='Force SVG bounding box to value given as "min_x,min_y,max_x,max_y"')
@click.option('--inkscape/--standard-svg', default=True, help='Export in Inkscape SVG format with layers and stuff.')
@ -147,24 +163,31 @@ def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules,
@click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True)
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('-t', '--transform', help='''Execute python transformation script on input. You have access to the functions
translate(x, y), scale(factor) and rotate(angle, center_x?, center_y?), the bounding box variables x_min,
y_min, x_max, y_max, width and height, and everything from python\'s built-in math module (e.g. pi, sqrt,
sin). As convenience methods, center() and origin() are provided to center the board resp. move its
bottom-left corner to the origin. Coordinates are given in --command-line-units, angles in degrees, and
scale as a scale factor (as opposed to a percentage). Example: "translate(-10, 0); rotate(45, 0, 5)"''')
@click.option('--command-line-units', type=Unit(), default=MM, help='Units for values given in other options. Default: millimeter')
@click.option('-n', '--number-format', help='Override number format to use during export in "[integer digits].[decimal digits]" notation, e.g. "2.6".')
@click.option('-t', '--transform', help='''Execute python transformation script on input. You have access to the
functions translate(x, y), scale(factor) and rotate(angle, center_x?, center_y?), the bounding box
variables x_min, y_min, x_max, y_max, width and height, and everything from python\'s built-in math module
(e.g. pi, sqrt, sin). As convenience methods, center() and origin() are provided to center the board resp.
move its bottom-left corner to the origin. Coordinates are given in --command-line-units, angles in
degrees, and scale as a scale factor (as opposed to a percentage). Example: "translate(-10, 0); rotate(45,
0, 5)"''')
@click.option('--command-line-units', type=Unit(), default=MM, help='''Units for values given in other options. Default:
millimeter''')
@click.option('-n', '--number-format', help='''Override number format to use during export in "[integer digits].[decimal
digits]" notation, e.g. "2.6".''')
@click.option('-u', '--units', type=Unit(), help='Override export file units')
@click.option('-z', '--zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='Override export zero suppression setting. Note: The meaning of this value is like in the Gerber spec for both Gerber and Excellon files!')
@click.option('--keep-comments/--drop-comments', help='Keep gerber comments. Note: Comments will be prepended to the start of file, and will not occur in their old position.')
@click.option('-z', '--zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='''Override export
zero suppression setting. Note: The meaning of this value is like in the Gerber spec for both Gerber and
Excellon files!''')
@click.option('--keep-comments/--drop-comments', help='''Keep gerber comments. Note: Comments will be prepended to the
start of file, and will not occur in their old position.''')
@click.option('--reuse-input-settings', 'output_format', flag_value='reuse', help='''Use the same export settings as the
input file instead of sensible defaults.''')
@click.option('--default-settings', 'output_format', default=True, flag_value='defaults', help='''Use sensible defaults
for the output file format settings (default).''')
@click.option('--input-number-format', help='Override number format of input file (mostly useful for Excellon files)')
@click.option('--input-units', type=Unit(), help='Override units of input file')
@click.option('--input-zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='Override zero suppression setting of input file')
@click.option('--input-zero-suppression', type=click.Choice(['off', 'leading', 'trailing']), help='''Override zero
suppression setting of input file''')
@click.argument('infile')
@click.argument('outfile')
def rewrite(transform, command_line_units, number_format, units, zero_suppression, keep_comments, output_format,
@ -258,15 +281,16 @@ def transform(transform, units, output_format, inpath, outpath,
@cli.command()
@click.option('--command-line-units', type=click.Choice(['metric', 'us-customary']), default='metric', help='Units for values given in --transform. Default: millimeter')
@click.option('--command-line-units', type=Unit(), default=MM, help='Units for values given in --transform. Default:
millimeter')
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
help='''Enable or disable file format warnings during parsing (default: on)''')
@click.option('--offset', multiple=True, type=Coordinate(), help="""Offset for the n'th file as a "x,y" string in unit
given by --command-line-units (default: millimeter). Can be given multiple times, and the first option
affects the first input, the second option affects the second input, and so on.""")
@click.option('--rotation', multiple=True, type=int, help="""Rotation for the n'th file in degrees clockwise. Can be
given multiple times, and the first option affects the first input, the second option affects the second
input, and so on.""")
@click.option('--rotation', multiple=True, type=Rotation(), help="""Rotation for the n'th file in degrees clockwise,
optionally followed by comma-separated rotation center X and Y coordinates. Can be given multiple times,
and the first option affects the first input, the second option affects the second input, and so on.""")
@click.option('-m', '--input-map', type=click.Path(exists=True, path_type=Path), multiple=True, help='''Extend or
override layer name mapping with name map from JSON file. This option can be given multiple times, in
which case the n'th option affects only the n'th input, like with --offset and --rotation. The JSON file
@ -300,13 +324,20 @@ def merge(inpath, outpath, offset, rotation, input_map, command_line_units, outp
raise click.UsageError('More --offset, --rotation or --input-map options than input files')
offset = offset or (0, 0)
rotation = rotation or 0
theta, cx, cy = rotation or 0, 0, 0
overrides = json.loads(input_map.read_bytes()) if input_map else None
with warnings.catch_warnings():
warnings.simplefilter(format_warnings)
stack = LayerStack.open(p, overrides=overrides, autoguess=use_builtin_name_rules)
if not math.isclose(offset[0], 0, abs_tol=1e-3) and math.isclose(offset[1], 0, abs_tol=1e-3):
stack.offset(*offset, command_line_units)
if not math.isclose(theta, 0, abs_tol=1e-2):
stack.rotate(theta, cx, cy)
if target is None:
target = stack
else:

View file

@ -326,17 +326,17 @@ class NetlistParser(object):
if name == 'UNITS':
if value in ('CUST', 'CUST 0'):
self.settings.units = Inch
self.settings.unit = Inch
self.settings.angle_unit = 'degree'
self.has_unit = True
elif value == 'CUST 1':
self.settings.units = MM
self.settings.unit = MM
self.settings.angle_unit = 'degree'
self.has_unit = True
elif value == 'CUST 2':
self.settings.units = Inch
self.settings.unit = Inch
self.settings.angle_unit = 'radian'
self.has_unit = True

View file

@ -154,7 +154,7 @@ def common_prefix(l):
return sorted(out, key=len)[-1]
def autoguess(filenames):
def do_autoguess(filenames):
prefix = common_prefix([f.name for f in filenames])
matches = {}
@ -313,7 +313,7 @@ class LayerStack:
if sum(len(files) for files in filemap.values()) < 6:
warnings.warn('Ambiguous gerber filenames. Trying last-resort autoguesser.')
generator = None
filemap = autoguess(files)
filemap = do_autoguess(files)
if len(filemap) < 6:
raise ValueError('Cannot figure out gerber file mapping. Partial map is: ', filemap)
@ -338,13 +338,13 @@ class LayerStack:
# Ignore if we can't find the param file -- maybe the user has convinced Allegro to actually put this
# information into a comment, or maybe they have made Allegro just use decimal points like XNC does.
filemap = autoguess([ f for files in filemap.values() for f in files ])
filemap = do_autoguess([ f for files in filemap.values() for f in files ])
if len(filemap) < 6:
raise SystemError('Cannot figure out gerber file mapping')
# FIXME use layer metadata from comments and ipc file if available
elif generator == 'zuken':
filemap = autoguess([ f for files in filemap.values() for f in files ])
filemap = do_autoguess([ f for files in filemap.values() for f in files ])
if len(filemap) < 6:
raise SystemError('Cannot figure out gerber file mapping')
# FIXME use layer metadata from comments and ipc file if available

View file

@ -88,7 +88,7 @@ class GerberFile(CamFile):
# dedup apertures
new_apertures = {}
replace_apertures = {}
mock_settings = FileSettings()
mock_settings = FileSettings.defaults()
for ap in self.apertures + other.apertures:
gbr = ap.to_gerber(mock_settings)
if gbr not in new_apertures:

View file

@ -3,7 +3,7 @@ from pathlib import Path
import pytest
from .image_support import ImageDifference
from .image_support import ImageDifference, run_cargo_cmd
def pytest_assertrepr_compare(op, left, right):
if isinstance(left, ImageDifference) or isinstance(right, ImageDifference):
@ -28,3 +28,8 @@ def pytest_sessionstart(session):
# on coordinator
for f in chain(fail_dir.glob('*.gbr'), fail_dir.glob('*.png')):
f.unlink()
try:
run_cargo_cmd('resvg', '--help')
except FileNotFoundError:
pytest.exit('resvg binary not found, aborting test.', 2)

View file

@ -104,6 +104,7 @@ def gerbv_export(in_gbr, out_svg, export_format='svg', origin=(0, 0), size=(6, 6
cachefile = cachedir / f'{digest}.svg'
if not cachefile.is_file():
print(f'Building cache for {Path(in_gbr).name}')
# NOTE: gerbv seems to always export 'clear' polarity apertures as white, irrespective of --foreground, --background
# and project file color settings.
# TODO: File issue upstream.
@ -138,6 +139,8 @@ def gerbv_export(in_gbr, out_svg, export_format='svg', origin=(0, 0), size=(6, 6
f'--foreground={fg}',
'-o', str(cachefile), '-p', f.name]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
print(f'Re-using cache for {Path(in_gbr).name}')
shutil.copy(cachefile, out_svg)
@contextmanager

View file

@ -1,7 +1,6 @@
G04 ex2: overlapping*
%FSLAX24Y24*%
%MOMM*%
%SRX1Y1I0.000J0.000*%
%ADD10C,1.00000*%
G01*
%LPD*%
@ -21,4 +20,4 @@ X10000Y50000D01*
G04 second fully coincident linear segment*
X0D01*
G37*
M02*
M02*

View file

@ -31,8 +31,8 @@ from .utils import *
from ..utils import Inch, MM
REFERENCE_FILES = {
'easyeda/Gerber_Drill_NPTH.DRL': (None, None),
'easyeda/Gerber_Drill_PTH.DRL': (None, 'easyeda/Gerber_TopLayer.GTL'),
'easyeda/Gerber_Drill_NPTH.DRL': (('inch', 'leading', 4), None),
'easyeda/Gerber_Drill_PTH.DRL': (('inch', 'leading', 4), 'easyeda/Gerber_TopLayer.GTL'),
# Altium uses an excellon format specification format that gerbv doesn't understand, so we have to fix that.
'altium-composite-drill/NC Drill/LimeSDR-QPCIe_1v2-SlotHoles.TXT': (('mm', 'trailing', 4), None),
'altium-composite-drill/NC Drill/LimeSDR-QPCIe_1v2-RoundHoles.TXT': (('mm', 'trailing', 4), 'altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GTL'),
@ -57,7 +57,6 @@ REFERENCE_FILES = {
def test_round_trip(reference, tmpfile):
reference, (unit_spec, _) = reference
tmp = tmpfile('Output excellon', '.drl')
print('unit spec', unit_spec)
f = ExcellonFile.open(reference)
f.save(tmp)
@ -79,7 +78,6 @@ def test_first_level_idempotence_svg(reference, tmpfile):
tmp = tmpfile('Output excellon', '.drl')
ref_svg = tmpfile('Reference SVG render', '.svg')
out_svg = tmpfile('Output SVG render', '.svg')
print('unit spec', unit_spec)
a = ExcellonFile.open(reference)
a.save(tmp)

View file

@ -150,7 +150,6 @@ REFERENCE_FILES = [ l.strip() for l in '''
pcb-rnd/power-art.gko
pcb-rnd/power-art.ast
pcb-rnd/power-art.gtl
pcb-rnd/power-art.lht
pcb-rnd/power-art.gto
pcb-rnd/power-art.gtp
pcb-rnd/power-art.asb
@ -206,7 +205,6 @@ REFERENCE_FILES = [ l.strip() for l in '''
siemens-2/Gerber/SolderPasteBottom.gdo
siemens-2/Gerber/SolderPasteTop.gdo
siemens-2/Gerber/EtchLayerBottom.gdo
siemens-2/Gerber/GerberPlot.gpf
siemens-2/Gerber/BoardOutlline.gdo
upverter/design_export.gko
upverter/design_export.gtl