Extend CLI tests
This commit is contained in:
parent
70179a4178
commit
d43eff8b49
4 changed files with 123 additions and 30 deletions
|
|
@ -29,7 +29,7 @@ from pathlib import Path
|
|||
from .utils import MM, Inch
|
||||
from .cam import FileSettings
|
||||
from .rs274x import GerberFile
|
||||
from .layers import LayerStack, NamingScheme
|
||||
from . import layers as lyr
|
||||
from . import __version__
|
||||
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ def apply_transform(transform, unit, layer_or_stack):
|
|||
layer_or_stack.scale(factor)
|
||||
|
||||
def rotate(angle, cx=0, cy=0):
|
||||
layer_or_stack.rotate(math.radians(angle), (cx, cy), unit)
|
||||
layer_or_stack.rotate(math.radians(angle), cx, cy, unit)
|
||||
|
||||
(x_min, y_min), (x_max, y_max) = layer_or_stack.bounding_box(unit, default=((0, 0), (0, 0)))
|
||||
width, height = x_max - x_min, y_max - y_min
|
||||
|
|
@ -76,7 +76,7 @@ class Coordinate(click.ParamType):
|
|||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
coords = map(float, value.split(','))
|
||||
coords = [float(e) for e in value.split(',')]
|
||||
if len(coords) != self.dimension:
|
||||
raise ValueError()
|
||||
return coords
|
||||
|
|
@ -89,7 +89,7 @@ class Rotation(click.ParamType):
|
|||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
coords = map(float, value.split(','))
|
||||
coords = [float(e) for e in value.split(',')]
|
||||
if len(coords) not in (1, 3):
|
||||
raise ValueError()
|
||||
|
||||
|
|
@ -115,10 +115,10 @@ class NamingScheme(click.Choice):
|
|||
name = 'naming_scheme'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__([n for n in dir(NamingScheme) if not n.startswith('_')])
|
||||
super().__init__([n for n in dir(lyr.NamingScheme) if not n.startswith('_')])
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
return getattr(NamingScheme, super().convert(value, param, ctx))
|
||||
return getattr(lyr.NamingScheme, super().convert(value, param, ctx))
|
||||
|
||||
|
||||
@click.group()
|
||||
|
|
@ -161,9 +161,9 @@ def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules,
|
|||
with warnings.catch_warnings():
|
||||
warnings.simplefilter(format_warnings)
|
||||
if force_zip:
|
||||
stack = LayerStack.open_zip(inpath, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
stack = lyr.LayerStack.open_zip(inpath, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
else:
|
||||
stack = LayerStack.open(inpath, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
stack = lyr.LayerStack.open(inpath, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
|
||||
if force_bounds:
|
||||
min_x, min_y, max_x, max_y = list(map(float, force_bounds.split(',')))
|
||||
|
|
@ -210,10 +210,10 @@ def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules,
|
|||
@click.argument('outfile')
|
||||
def rewrite(transform, command_line_units, number_format, units, zero_suppression, keep_comments, output_format,
|
||||
input_number_format, input_units, input_zero_suppression, infile, outfile, format_warnings):
|
||||
""" Parse a gerber file, apply transformations, and re-serialize it into a new gerber file. Without transformations,
|
||||
this command can be used to convert a gerber file to use different settings (e.g. units, precision), but can also be
|
||||
used to "normalize" gerber files in a weird format into a more standards-compatible one as gerbonara's gerber parser
|
||||
is significantly more robust for weird inputs than others. """
|
||||
""" Parse a single gerber file, apply transformations, and re-serialize it into a new gerber file. Without
|
||||
transformations, this command can be used to convert a gerber file to use different settings (e.g. units,
|
||||
precision), but can also be used to "normalize" gerber files in a weird format into a more standards-compatible one
|
||||
as gerbonara's gerber parser is significantly more robust for weird inputs than others. """
|
||||
|
||||
input_settings = FileSettings()
|
||||
if input_number_format:
|
||||
|
|
@ -232,12 +232,13 @@ def rewrite(transform, command_line_units, number_format, units, zero_suppressio
|
|||
if transform:
|
||||
apply_transform(transform, command_line_units or MM, f)
|
||||
|
||||
output_format = FileSettings() if output_format == 'reuse' else FileSettings.defaults()
|
||||
output_format = f.import_settings if output_format == 'reuse' else FileSettings.defaults()
|
||||
if number_format:
|
||||
a, _, b = number_format.partition('.')
|
||||
output_format.number_format = (int(a), int(b))
|
||||
|
||||
output_format.unit = units
|
||||
if units:
|
||||
output_format.unit = units
|
||||
|
||||
if zero_suppression:
|
||||
output_format.zeros = None if zero_suppression == 'off' else zero_suppression
|
||||
|
|
@ -286,13 +287,13 @@ def transform(transform, units, output_format, inpath, outpath,
|
|||
with warnings.catch_warnings():
|
||||
warnings.simplefilter(format_warnings)
|
||||
if force_zip:
|
||||
stack = LayerStack.open_zip(path, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
stack = lyr.LayerStack.open_zip(path, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
else:
|
||||
stack = LayerStack.open(path, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
stack = lyr.LayerStack.open(path, overrides=overrides, autoguess=use_builtin_name_rules)
|
||||
|
||||
apply_transform(transform, units, stack)
|
||||
|
||||
output_format = FileSettings() if output_format == 'reuse' else FileSettings.defaults()
|
||||
output_format = None if output_format == 'reuse' else FileSettings.defaults()
|
||||
stack.save_to_directory(outpath, naming_scheme=output_naming_scheme or {},
|
||||
gerber_settings=output_format,
|
||||
excellon_settings=dataclasses.replace(output_format, zeros=None))
|
||||
|
|
@ -343,13 +344,13 @@ 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)
|
||||
theta, cx, cy = rotation or 0, 0, 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)
|
||||
stack = lyr.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 or MM)
|
||||
|
|
@ -366,7 +367,7 @@ def merge(inpath, outpath, offset, rotation, input_map, command_line_units, outp
|
|||
if not output_naming_scheme:
|
||||
warnings.warn('--output-board-name given without --output-naming-scheme. This will be ignored.')
|
||||
target.board_name = output_board_name
|
||||
output_format = FileSettings() if output_format == 'reuse' else FileSettings.defaults()
|
||||
output_format = None if output_format == 'reuse' else FileSettings.defaults()
|
||||
target.save_to_directory(outpath, naming_scheme=output_naming_scheme or {},
|
||||
gerber_settings=output_format,
|
||||
excellon_settings=dataclasses.replace(output_format, zeros=None))
|
||||
|
|
@ -406,17 +407,19 @@ def bounding_box(infile, format_warnings, input_number_format, input_units, inpu
|
|||
|
||||
|
||||
@cli.command()
|
||||
@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('--force-zip', is_flag=True, help='Force treating input path as zip file (default: guess file type from extension and contents)')
|
||||
@click.argument('path', type=click.Path(exists=True))
|
||||
def layers(path, force_zip, format_warnings):
|
||||
""" Read layers from a directory or zip with Gerber files and list the found layer / path assignment. """
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter(format_warnings)
|
||||
if force_zip:
|
||||
stack = LayerStack.open_zip(path)
|
||||
stack = lyr.LayerStack.open_zip(path)
|
||||
else:
|
||||
stack = LayerStack.open(path)
|
||||
stack = lyr.LayerStack.open(path)
|
||||
|
||||
print(f'Detected board name: {stack.board_name}')
|
||||
print(f'Probably exported by: {stack.generator or "Unknown"}')
|
||||
|
|
@ -441,6 +444,7 @@ def layers(path, force_zip, format_warnings):
|
|||
|
||||
|
||||
@cli.command()
|
||||
@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']), help='''Enable or
|
||||
disable file format warnings during parsing (default: on)''')
|
||||
@click.option('--force-zip', is_flag=True, help='Force treating input path as zip file (default: guess file type from extension and contents)')
|
||||
|
|
@ -452,9 +456,9 @@ def meta(path, force_zip, format_warnings):
|
|||
with warnings.catch_warnings():
|
||||
warnings.simplefilter(format_warnings)
|
||||
if force_zip:
|
||||
stack = LayerStack.open_zip(path)
|
||||
stack = lyr.LayerStack.open_zip(path)
|
||||
else:
|
||||
stack = LayerStack.open(path)
|
||||
stack = lyr.LayerStack.open(path)
|
||||
|
||||
out = {}
|
||||
out['board_name'] = stack.board_name
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ class GerberFile(CamFile):
|
|||
for obj in self.objects:
|
||||
obj.offset(dx, dy, unit)
|
||||
|
||||
def rotate(self, angle:'radian', center=(0,0), unit=MM):
|
||||
def rotate(self, angle:'radian', cx=0, cy=0, unit=MM):
|
||||
if math.isclose(angle % (2*math.pi), 0):
|
||||
return
|
||||
|
||||
|
|
@ -302,7 +302,7 @@ class GerberFile(CamFile):
|
|||
ap.rotation += angle
|
||||
|
||||
for obj in self.objects:
|
||||
obj.rotate(angle, *center, unit)
|
||||
obj.rotate(angle, cx, cy, unit)
|
||||
|
||||
def invert_polarity(self):
|
||||
""" Invert the polarity (color) of each object in this file. """
|
||||
|
|
|
|||
|
|
@ -21,18 +21,34 @@ from pathlib import Path
|
|||
import re
|
||||
import tempfile
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .utils import *
|
||||
from ..cli import render, rewrite, transform, merge, bounding_box, layers, meta
|
||||
from .. import cli
|
||||
from ..utils import MM
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def file_mock():
|
||||
old = cli.GerberFile
|
||||
c_obj = cli.GerberFile = mock.Mock()
|
||||
i_obj = c_obj.open.return_value = mock.Mock()
|
||||
i_obj.bounding_box.return_value = (0, 0), (50, 100)
|
||||
yield i_obj
|
||||
cli.GerberFile = old
|
||||
|
||||
|
||||
class TestRender:
|
||||
def invoke(self, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(render, list(map(str, args)))
|
||||
res = runner.invoke(cli.render, list(map(str, args)))
|
||||
print(res.output)
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
|
|
@ -115,3 +131,76 @@ class TestRender:
|
|||
assert len(colors_without) == len(colors_with)
|
||||
assert colors_with - {'#67890a'} == set(test_colorscheme.values()) - {'#67890abc'}
|
||||
|
||||
|
||||
class TestRewrite:
|
||||
def invoke(self, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(cli.rewrite, list(map(str, args)))
|
||||
print(res.output)
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
def test_basic(self):
|
||||
assert self.invoke('--version').startswith('Version ')
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_transforms(self, reference, file_mock):
|
||||
with tempfile.NamedTemporaryFile() as tmpout:
|
||||
self.invoke(reference, tmpout.name, '--transform', 'rotate(90); translate(10, 10); rotate(-45.5); scale(2)')
|
||||
file_mock.rotate.assert_has_calls([
|
||||
mock.call(math.radians(90), 0, 0, MM),
|
||||
mock.call(math.radians(-45.5), 0, 0, MM)])
|
||||
file_mock.offset.assert_called_with(10, 10, MM)
|
||||
file_mock.scale.assert_called_with(2)
|
||||
assert file_mock.save.called
|
||||
assert file_mock.save.call_args[0][0] == tmpout.name
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_real_invocation(self, reference):
|
||||
with tempfile.NamedTemporaryFile() as tmpout:
|
||||
self.invoke(reference, tmpout.name, '--transform', 'rotate(45); translate(10, 0)')
|
||||
assert tmpout.read()
|
||||
|
||||
|
||||
class TestMerge:
|
||||
def invoke(self, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(cli.merge, list(map(str, args)))
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
def test_basic(self):
|
||||
assert self.invoke('--version').startswith('Version ')
|
||||
|
||||
@pytest.mark.parametrize('file_a', ['kicad-older'])
|
||||
@pytest.mark.parametrize('file_b', ['eagle-newer'])
|
||||
def test_real_invocation(self, file_a, file_b):
|
||||
with tempfile.TemporaryDirectory() as outdir:
|
||||
self.invoke(reference_path(file_a), '--rotation', '90', '--offset', '0,0',
|
||||
reference_path(file_b), '--offset', '100,100', '--rotation', '0',
|
||||
outdir, '--output-naming-scheme', 'kicad', '--output-board-name', 'foobar',
|
||||
'--warnings', 'ignore')
|
||||
assert (Path(outdir) / 'foobar-F.Cu.gbr').exists()
|
||||
|
||||
|
||||
class TestMeta:
|
||||
def invoke(self, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(cli.meta, list(map(str, args)))
|
||||
print(res.output)
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
def test_basic(self):
|
||||
assert self.invoke('--version').startswith('Version ')
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_real_invocation(self, reference):
|
||||
j = json.loads(self.invoke(reference, '--warnings', 'ignore'))
|
||||
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ def test_rotation_center(reference, angle, center, tmpfile):
|
|||
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
||||
|
||||
f = GerberFile.open(reference)
|
||||
f.rotate(math.radians(angle), center=center)
|
||||
f.rotate(math.radians(angle), *center)
|
||||
f.save(tmp_gbr)
|
||||
|
||||
# calculate circle center in SVG coordinates
|
||||
|
|
@ -379,7 +379,7 @@ def test_combined(reference, angle, center, offset, tmpfile):
|
|||
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
||||
|
||||
f = GerberFile.open(reference)
|
||||
f.rotate(math.radians(angle), center=center)
|
||||
f.rotate(math.radians(angle), *center)
|
||||
f.offset(*offset)
|
||||
f.save(tmp_gbr, settings=FileSettings(unit=f.unit, number_format=(4,7)))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue