Add a basic plot test
This commit is contained in:
parent
cc06a8ef66
commit
c4778e37bd
|
|
@ -27,3 +27,5 @@ MANIFEST
|
||||||
|
|
||||||
|
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
|
|
||||||
|
*.kicad_pcb-bak
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -50,6 +50,16 @@ export PYTHONPATH=~/local/kicad/lib/python2.7/site-packages
|
||||||
export LD_LIBRARY_PATH=~/local/kicad/lib64
|
export LD_LIBRARY_PATH=~/local/kicad/lib64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you've installed "normally", you should not need to do this.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
There are some tests. Run them with pytest:
|
||||||
|
|
||||||
|
```
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
# TODO list
|
# TODO list
|
||||||
|
|
||||||
There are some things that still need work:
|
There are some things that still need work:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import pcbnew
|
import pcbnew
|
||||||
|
|
||||||
from . import error
|
from . import error
|
||||||
|
|
@ -446,6 +448,27 @@ class PlotConfig(object):
|
||||||
def add_output(self, new_op):
|
def add_output(self, new_op):
|
||||||
self._outputs.append(new_op)
|
self._outputs.append(new_op)
|
||||||
|
|
||||||
|
def get_output_by_name(self, output_name):
|
||||||
|
"""
|
||||||
|
Gets an output with a given name.
|
||||||
|
|
||||||
|
@param output_name the name of the output to find
|
||||||
|
"""
|
||||||
|
for o in self.outputs:
|
||||||
|
|
||||||
|
if o.name == output_name:
|
||||||
|
return o
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_output_dir_for_name(self, output_name):
|
||||||
|
"""
|
||||||
|
Get the output dir for a given output name
|
||||||
|
"""
|
||||||
|
|
||||||
|
o = self.get_output_by_name(output_name)
|
||||||
|
return os.path.join(self.outdir, o.outdir) if o else None
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
|
||||||
errs = []
|
errs = []
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
(kicad_pcb (version 20171130) (host pcbnew "(5.0.0-rc2-76-gb5f63567d)")
|
||||||
|
|
||||||
|
(general
|
||||||
|
(thickness 1.6)
|
||||||
|
(drawings 5)
|
||||||
|
(tracks 4)
|
||||||
|
(zones 0)
|
||||||
|
(modules 1)
|
||||||
|
(nets 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
(page A4)
|
||||||
|
(title_block
|
||||||
|
(title "Simple Plotting Test")
|
||||||
|
(date 2018-06-04)
|
||||||
|
(rev A)
|
||||||
|
(company "KiPlot - KiCad Plotting Driver")
|
||||||
|
)
|
||||||
|
|
||||||
|
(layers
|
||||||
|
(0 F.Cu signal)
|
||||||
|
(31 B.Cu signal)
|
||||||
|
(32 B.Adhes user)
|
||||||
|
(33 F.Adhes user)
|
||||||
|
(34 B.Paste user)
|
||||||
|
(35 F.Paste user)
|
||||||
|
(36 B.SilkS user)
|
||||||
|
(37 F.SilkS user)
|
||||||
|
(38 B.Mask user)
|
||||||
|
(39 F.Mask user)
|
||||||
|
(40 Dwgs.User user)
|
||||||
|
(41 Cmts.User user)
|
||||||
|
(42 Eco1.User user)
|
||||||
|
(43 Eco2.User user)
|
||||||
|
(44 Edge.Cuts user)
|
||||||
|
(45 Margin user)
|
||||||
|
(46 B.CrtYd user)
|
||||||
|
(47 F.CrtYd user)
|
||||||
|
(48 B.Fab user)
|
||||||
|
(49 F.Fab user)
|
||||||
|
)
|
||||||
|
|
||||||
|
(setup
|
||||||
|
(last_trace_width 0.25)
|
||||||
|
(trace_clearance 0.2)
|
||||||
|
(zone_clearance 0.508)
|
||||||
|
(zone_45_only no)
|
||||||
|
(trace_min 0.2)
|
||||||
|
(segment_width 0.2)
|
||||||
|
(edge_width 0.15)
|
||||||
|
(via_size 0.8)
|
||||||
|
(via_drill 0.4)
|
||||||
|
(via_min_size 0.4)
|
||||||
|
(via_min_drill 0.3)
|
||||||
|
(uvia_size 0.3)
|
||||||
|
(uvia_drill 0.1)
|
||||||
|
(uvias_allowed no)
|
||||||
|
(uvia_min_size 0.2)
|
||||||
|
(uvia_min_drill 0.1)
|
||||||
|
(pcb_text_width 0.3)
|
||||||
|
(pcb_text_size 1.5 1.5)
|
||||||
|
(mod_edge_width 0.15)
|
||||||
|
(mod_text_size 1 1)
|
||||||
|
(mod_text_width 0.15)
|
||||||
|
(pad_size 1.524 1.524)
|
||||||
|
(pad_drill 0.762)
|
||||||
|
(pad_to_mask_clearance 0.2)
|
||||||
|
(aux_axis_origin 0 0)
|
||||||
|
(visible_elements FFFFFF7F)
|
||||||
|
(pcbplotparams
|
||||||
|
(layerselection 0x010fc_ffffffff)
|
||||||
|
(usegerberextensions false)
|
||||||
|
(usegerberattributes false)
|
||||||
|
(usegerberadvancedattributes false)
|
||||||
|
(creategerberjobfile false)
|
||||||
|
(excludeedgelayer true)
|
||||||
|
(linewidth 0.150000)
|
||||||
|
(plotframeref false)
|
||||||
|
(viasonmask false)
|
||||||
|
(mode 1)
|
||||||
|
(useauxorigin false)
|
||||||
|
(hpglpennumber 1)
|
||||||
|
(hpglpenspeed 20)
|
||||||
|
(hpglpendiameter 15.000000)
|
||||||
|
(psnegative false)
|
||||||
|
(psa4output false)
|
||||||
|
(plotreference true)
|
||||||
|
(plotvalue true)
|
||||||
|
(plotinvisibletext false)
|
||||||
|
(padsonsilk false)
|
||||||
|
(subtractmaskfromsilk false)
|
||||||
|
(outputformat 1)
|
||||||
|
(mirror false)
|
||||||
|
(drillshape 1)
|
||||||
|
(scaleselection 1)
|
||||||
|
(outputdirectory ""))
|
||||||
|
)
|
||||||
|
|
||||||
|
(net 0 "")
|
||||||
|
|
||||||
|
(net_class Default "This is the default net class."
|
||||||
|
(clearance 0.2)
|
||||||
|
(trace_width 0.25)
|
||||||
|
(via_dia 0.8)
|
||||||
|
(via_drill 0.4)
|
||||||
|
(uvia_dia 0.3)
|
||||||
|
(uvia_drill 0.1)
|
||||||
|
)
|
||||||
|
|
||||||
|
(module TestPoint:TestPoint_THTPad_2.0x2.0mm_Drill1.0mm (layer F.Cu) (tedit 5B1533F4) (tstamp 5B15541F)
|
||||||
|
(at 140 100)
|
||||||
|
(descr "THT rectangular pad as test Point, square 2.0mm_Drill1.0mm side length, hole diameter 1.0mm")
|
||||||
|
(tags "test point THT pad rectangle square")
|
||||||
|
(attr virtual)
|
||||||
|
(fp_text reference TP1 (at 0 -2) (layer F.SilkS)
|
||||||
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
|
)
|
||||||
|
(fp_text value TestPoint2mm (at 0 2.05) (layer F.Fab)
|
||||||
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
|
)
|
||||||
|
(fp_line (start 1.5 1.5) (end -1.5 1.5) (layer F.CrtYd) (width 0.05))
|
||||||
|
(fp_line (start 1.5 1.5) (end 1.5 -1.5) (layer F.CrtYd) (width 0.05))
|
||||||
|
(fp_line (start -1.5 -1.5) (end -1.5 1.5) (layer F.CrtYd) (width 0.05))
|
||||||
|
(fp_line (start -1.5 -1.5) (end 1.5 -1.5) (layer F.CrtYd) (width 0.05))
|
||||||
|
(fp_line (start -1.2 1.2) (end -1.2 -1.2) (layer F.SilkS) (width 0.12))
|
||||||
|
(fp_line (start 1.2 1.2) (end -1.2 1.2) (layer F.SilkS) (width 0.12))
|
||||||
|
(fp_line (start 1.2 -1.2) (end 1.2 1.2) (layer F.SilkS) (width 0.12))
|
||||||
|
(fp_line (start -1.2 -1.2) (end 1.2 -1.2) (layer F.SilkS) (width 0.12))
|
||||||
|
(fp_text user %R (at 0 -2) (layer F.Fab)
|
||||||
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
|
)
|
||||||
|
(pad 1 thru_hole rect (at 0 0) (size 2 2) (drill 1) (layers *.Cu *.Mask))
|
||||||
|
)
|
||||||
|
|
||||||
|
(gr_arc (start 145 98) (end 148 98) (angle -90) (layer Edge.Cuts) (width 0.2))
|
||||||
|
(gr_line (start 145 95) (end 132 95) (layer Edge.Cuts) (width 0.2))
|
||||||
|
(gr_line (start 148 106) (end 148 98) (layer Edge.Cuts) (width 0.2))
|
||||||
|
(gr_line (start 132 106) (end 148 106) (layer Edge.Cuts) (width 0.2))
|
||||||
|
(gr_line (start 132 95) (end 132 106) (layer Edge.Cuts) (width 0.2))
|
||||||
|
|
||||||
|
(segment (start 140 100) (end 143 100) (width 1) (layer F.Cu) (net 0))
|
||||||
|
(segment (start 140 100) (end 142 102) (width 1) (layer B.Cu) (net 0))
|
||||||
|
(segment (start 142 102) (end 143 102) (width 1) (layer B.Cu) (net 0))
|
||||||
|
(segment (start 134 99) (end 134 101) (width 1) (layer F.Cu) (net 0))
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""
|
||||||
|
Test configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--plot_dir", action="store", default=None,
|
||||||
|
help="the plot dir to use (omit to use a temp dir). "
|
||||||
|
"If given, plots will _not_ be cleared after testing.")
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
This directory contains tests for full testing of KiCad plots.
|
||||||
|
This tests:
|
||||||
|
|
||||||
|
* KiPlot's config parsing and driving of KiCad
|
||||||
|
* KiCad's own plotting code
|
||||||
|
|
||||||
|
Generally, boards are drawn from `../board_samples` and configs from
|
||||||
|
`yaml_samples`. Sometimes, the YAML samples are modified by the test
|
||||||
|
runners to avoid having hundreds of them!
|
||||||
|
|
||||||
|
Bug that should be tested for:
|
||||||
|
|
||||||
|
* https://bugs.launchpad.net/kicad/+bug/1775037
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import logging
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from kiplot import kiplot
|
||||||
|
from kiplot import config_reader
|
||||||
|
|
||||||
|
|
||||||
|
KICAD_PCB_EXT = '.kicad_pcb'
|
||||||
|
|
||||||
|
|
||||||
|
class KiPlotTestContext(object):
|
||||||
|
|
||||||
|
def __init__(self, test_name):
|
||||||
|
self.cfg = None
|
||||||
|
|
||||||
|
# The name used for the test output dirs and other logging
|
||||||
|
self.test_name = test_name
|
||||||
|
|
||||||
|
# The name of the PCB board file (will be interpolated into the plot
|
||||||
|
# files by pcbnewm so we need to know
|
||||||
|
self.board_name = None
|
||||||
|
|
||||||
|
# The actual board file that will be loaded
|
||||||
|
self.board_file = None
|
||||||
|
|
||||||
|
# The directory under which to place plots (None: use a temp dir)
|
||||||
|
self.plot_dir = pytest.config.getoption('plot_dir')
|
||||||
|
|
||||||
|
# The actual output dir for this plot run
|
||||||
|
self._output_dir = None
|
||||||
|
# Clean the output dir afterwards (true for temp dirs)
|
||||||
|
self._del_dir_after = self.plot_dir is None
|
||||||
|
|
||||||
|
def _get_text_cfg_dir(self):
|
||||||
|
|
||||||
|
this_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
return os.path.join(this_dir, '../yaml_samples')
|
||||||
|
|
||||||
|
def _get_board_cfg_dir(self):
|
||||||
|
|
||||||
|
this_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
return os.path.join(this_dir, '../board_samples')
|
||||||
|
|
||||||
|
def load_yaml_config_file(self, filename):
|
||||||
|
"""
|
||||||
|
Reads a config from a YAML file
|
||||||
|
"""
|
||||||
|
|
||||||
|
cfg_file = os.path.join(self._get_text_cfg_dir(), filename)
|
||||||
|
|
||||||
|
cr = config_reader.CfgYamlReader()
|
||||||
|
|
||||||
|
with open(cfg_file) as cf_file:
|
||||||
|
cfg = cr.read(cf_file)
|
||||||
|
|
||||||
|
self.cfg = cfg
|
||||||
|
|
||||||
|
def _load_board_file(self, filename=None):
|
||||||
|
"""
|
||||||
|
Load the named board.
|
||||||
|
|
||||||
|
@param filename: a filename to load, or None to load the relevant
|
||||||
|
board name from the board sample dir
|
||||||
|
"""
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
self.board_file = os.path.join(self._get_board_cfg_dir(),
|
||||||
|
self.board_name + KICAD_PCB_EXT)
|
||||||
|
else:
|
||||||
|
self.board_file = filename
|
||||||
|
|
||||||
|
assert os.path.isfile(self.board_file)
|
||||||
|
|
||||||
|
def _set_up_output_dir(self):
|
||||||
|
|
||||||
|
if not self.plot_dir:
|
||||||
|
# create a tmp dir
|
||||||
|
self.output_dir = tempfile.mkdtemp(
|
||||||
|
prefix='tmp_kiplot_{}'.format(self.test_name))
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.output_dir = os.path.join(self.plot_dir, self.test_name)
|
||||||
|
# just create the dir
|
||||||
|
if os.path.isdir(self.output_dir):
|
||||||
|
# exists, that's OK
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.makedirs(self.output_dir)
|
||||||
|
|
||||||
|
self.cfg.outdir = self.output_dir
|
||||||
|
logging.info(self.output_dir)
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
|
||||||
|
if self._del_dir_after:
|
||||||
|
shutil.rmtree(self.output_dir)
|
||||||
|
|
||||||
|
def do_plot(self):
|
||||||
|
|
||||||
|
self.cfg.validate()
|
||||||
|
|
||||||
|
self._load_board_file(self.board_file)
|
||||||
|
|
||||||
|
self._set_up_output_dir()
|
||||||
|
|
||||||
|
plotter = kiplot.Plotter(self.cfg)
|
||||||
|
plotter.plot(self.board_file)
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
"""
|
||||||
|
Tests of simple 2-layer PCBs
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import plotting_test_utils
|
||||||
|
|
||||||
|
import os
|
||||||
|
import mmap
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def expect_file_at(filename):
|
||||||
|
|
||||||
|
assert(os.path.isfile(filename))
|
||||||
|
|
||||||
|
|
||||||
|
def get_gerber_filename(board_name, layer_slug, ext='.gbr'):
|
||||||
|
return board_name + '-' + layer_slug + ext
|
||||||
|
|
||||||
|
|
||||||
|
def find_gerber_aperture(s, ap_desc):
|
||||||
|
|
||||||
|
m = re.search(r'%AD(.*)' + ap_desc + r'\*%', s)
|
||||||
|
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def expect_gerber_has_apertures(gbr_data, ap_list):
|
||||||
|
|
||||||
|
aps = []
|
||||||
|
|
||||||
|
for ap in ap_list:
|
||||||
|
|
||||||
|
# find the circular aperture for the outline
|
||||||
|
ap_no = find_gerber_aperture(gbr_data, ap)
|
||||||
|
|
||||||
|
assert ap_no is not None
|
||||||
|
|
||||||
|
# apertures from D10 to D999
|
||||||
|
assert len(ap_no) in [2, 3]
|
||||||
|
|
||||||
|
aps.append(ap_no)
|
||||||
|
|
||||||
|
logging.debug("Found apertures {}".format(aps))
|
||||||
|
return aps
|
||||||
|
|
||||||
|
|
||||||
|
def expect_gerber_flash_at(gbr_data, pos):
|
||||||
|
"""
|
||||||
|
Check for a gerber flash at a given point
|
||||||
|
(it's hard to check that aperture is right without a real gerber parser
|
||||||
|
"""
|
||||||
|
|
||||||
|
repat = r'^X{x}Y{y}D03\*$'.format(
|
||||||
|
x=int(pos[0] * 100000),
|
||||||
|
y=int(pos[1] * 100000)
|
||||||
|
)
|
||||||
|
|
||||||
|
m = re.search(repat, gbr_data, re.MULTILINE)
|
||||||
|
|
||||||
|
assert(m)
|
||||||
|
|
||||||
|
logging.debug("Gerber flash found: " + repat)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mmapped_data(filename):
|
||||||
|
|
||||||
|
with open(filename) as fo:
|
||||||
|
return mmap.mmap(fo.fileno(), 0, access=mmap.ACCESS_READ)
|
||||||
|
|
||||||
|
|
||||||
|
# content of test_sample.py
|
||||||
|
def test_2layer():
|
||||||
|
|
||||||
|
ctx = plotting_test_utils.KiPlotTestContext('simple_2layer')
|
||||||
|
|
||||||
|
ctx.load_yaml_config_file('simple_2layer.kiplot.yaml')
|
||||||
|
ctx.board_name = 'simple_2layer'
|
||||||
|
|
||||||
|
ctx.do_plot()
|
||||||
|
|
||||||
|
gbr_dir = ctx.cfg.resolve_output_dir_for_name('gerbers')
|
||||||
|
|
||||||
|
f_cu_gbr = os.path.join(gbr_dir,
|
||||||
|
get_gerber_filename(ctx.board_name, "F_Cu"))
|
||||||
|
|
||||||
|
expect_file_at(f_cu_gbr)
|
||||||
|
|
||||||
|
f_cu_data = get_mmapped_data(f_cu_gbr)
|
||||||
|
|
||||||
|
ap_ids = expect_gerber_has_apertures(f_cu_data, [
|
||||||
|
"C,0.200000",
|
||||||
|
"R,2.000000X2.000000",
|
||||||
|
"C,1.000000"])
|
||||||
|
|
||||||
|
# expect a flash for the square pad
|
||||||
|
expect_gerber_flash_at(f_cu_data, (140, -100))
|
||||||
|
|
||||||
|
ctx.clean_up()
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Example KiPlot config file for a basic 2-layer board
|
||||||
|
kiplot:
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
|
||||||
|
- name: 'gerbers'
|
||||||
|
comment: "Gerbers for the Gerber god"
|
||||||
|
type: gerber
|
||||||
|
dir: gerberdir
|
||||||
|
options:
|
||||||
|
# generic layer options
|
||||||
|
exclude_edge_layer: false
|
||||||
|
exclude_pads_from_silkscreen: false
|
||||||
|
use_aux_axis_as_origin: false
|
||||||
|
plot_sheet_reference: false
|
||||||
|
plot_footprint_refs: true
|
||||||
|
plot_footprint_values: true
|
||||||
|
force_plot_invisible_refs_vals: false
|
||||||
|
tent_vias: true
|
||||||
|
check_zone_fills: true
|
||||||
|
|
||||||
|
# gerber options
|
||||||
|
line_width: 0.15
|
||||||
|
subtract_mask_from_silk: true
|
||||||
|
use_protel_extensions: false
|
||||||
|
gerber_precision: 4.5
|
||||||
|
create_gerber_job_file: true
|
||||||
|
use_gerber_x2_attributes: true
|
||||||
|
use_gerber_net_attributes: false
|
||||||
|
|
||||||
|
layers:
|
||||||
|
- layer: F.Cu
|
||||||
|
suffix: F_Cu
|
||||||
|
- layer: F.SilkS
|
||||||
|
suffix: F_SilkS
|
||||||
Loading…
Reference in New Issue