Add a basic plot test

This commit is contained in:
John Beard 2018-06-04 13:48:46 +01:00
parent cc06a8ef66
commit c4778e37bd
10 changed files with 458 additions and 2 deletions

4
.gitignore vendored
View File

@ -26,4 +26,6 @@ wheels/
MANIFEST MANIFEST
.pytest_cache .pytest_cache
*.kicad_pcb-bak

View File

@ -50,10 +50,20 @@ 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:
* DRC checking - that can't be done over the Python interface yet. If/when * DRC checking - that can't be done over the Python interface yet. If/when
this is added to KiCad, KiPlot will be able to also be used for DRC this is added to KiCad, KiPlot will be able to also be used for DRC
functional tests instead of a complex additonal test harness in C++. functional tests instead of a complex additonal test harness in C++.

View File

@ -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 = []

View File

@ -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))
)

9
tests/conftest.py Normal file
View File

@ -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.")

13
tests/test_plot/README.md Normal file
View File

@ -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

View File

View File

@ -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)

View 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()

View File

@ -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