Added Internal BoM + KiCost integration

- Currently very basic, but you get "Costs" and "Costs (DNF)" work
  sheets in the XLSX output when the xlsx.kicost option is enabled.
This commit is contained in:
Salvador E. Tropea 2021-04-15 11:14:37 -03:00
parent 3a3e88ec83
commit 16ddb9465f
18 changed files with 574 additions and 12 deletions

View File

@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New KiCost variant style.
- `skip_if_no_field` and `invert` options to the regex used in the generic
filter.
- Basic KiCost support.
- Basic KiCost support (experimental).
- Basic internal BoM and KiCost integration (experimental)
- Experimental mechanism to change 3D models according to the variant.
### Changed

View File

@ -705,9 +705,10 @@ Next time you need this list just use an alias, like this:
- `hide_pcb_info`: [boolean=false] Hide project information.
- `hide_stats_info`: [boolean=false] Hide statistics information.
- `highlight_empty`: [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`.
- `kicost`: [boolean=false] Enable KiCost worksheet creation.
- `logo`: [string|boolean=''] PNG file to use as logo, use false to remove.
- `max_col_width`: [number=60] [20,999] Maximum column width (characters).
- `style`: [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic..
- `style`: [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic.
- `title`: [string='KiBot Bill of Materials'] BoM title.
* Archiver (files compressor)

View File

@ -173,11 +173,13 @@ outputs:
hide_stats_info: false
# [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`
highlight_empty: true
# [boolean=false] Enable KiCost worksheet creation
kicost: false
# [string|boolean=''] PNG file to use as logo, use false to remove
logo: ''
# [number=60] [20,999] Maximum column width (characters)
max_col_width: 60
# [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic.
# [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic
style: 'modern-blue'
# [string='KiBot Bill of Materials'] BoM title
title: 'KiBot Bill of Materials'

View File

@ -9,11 +9,13 @@
XLSX Writer: Generates an XLSX BoM file.
"""
import io
import pprint
from textwrap import wrap
from base64 import b64decode
from .columnlist import ColumnList
from .kibot_logo import KIBOT_LOGO
from .. import log
from ..misc import W_NOKICOST
try:
from xlsxwriter import Workbook
XLSX_SUPPORT = True
@ -22,6 +24,13 @@ except ModuleNotFoundError:
class Workbook():
pass
try:
from kicost.kicost import query_part_info
from kicost.spreadsheet import create_worksheet, Spreadsheet
import kicost.global_vars as kvar
KICOST_SUPPORT = True
except ModuleNotFoundError:
KICOST_SUPPORT = False
logger = log.get_logger(__name__)
BG_GEN = "#E6FFEE"
@ -274,6 +283,50 @@ def write_info(cfg, r_info_start, worksheet, column_widths, col1, fmt_info, fmt_
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", prj.comp_build)
class Part(object):
def __init__(self):
super().__init__()
def create_kicost_sheet(workbook, groups, cfg):
if not KICOST_SUPPORT:
logger.warning(W_NOKICOST, 'KiCost sheet requested but failed to load KiCost support')
if cfg.debug_level > 2:
logger.debug("Groups exported to KiCost:")
for g in groups:
logger.debug(pprint.pformat(g.__dict__))
logger.debug("-- Components")
for c in g.components:
logger.debug(pprint.pformat(c.__dict__))
# Force KiCost to use our logger
kvar.logger = logger
# Create the projects information structure
prj_info = [{'title': p.name, 'company': p.sch.company, 'date': p.sch.date} for p in cfg.aggregate]
# Create the worksheets
ws_names = ['Costs', 'Costs (DNF)']
for ws in range(2):
# Second pass is DNF
dnf = ws == 1
# Should we generate the DNF?
if dnf and (not cfg.xlsx.generate_dnf or cfg.n_total == cfg.n_fitted):
break
# Create the parts structure from the groups
parts = []
for g in groups:
if (cfg.ignore_dnf and not g.is_fitted()) != dnf:
continue
part = Part()
part.refs = [c.ref for c in g.components]
part.fields = g.fields
parts.append(part)
# Get the prices
query_part_info(parts)
# Create a class to hold the spreadsheet parameters
ss = Spreadsheet(workbook, ws_names[ws])
# Add a worksheet with costs to the spreadsheet
create_worksheet(ss, logger, parts, prj_info)
def write_xlsx(filename, groups, col_fields, head_names, cfg):
"""
Write BoM out to a XLSX file
@ -395,6 +448,10 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
# Add a sheet for the color references
create_color_ref(workbook, cfg.xlsx.col_colors, hl_empty, fmt_cols)
# Optionally add KiCost information
if cfg.xlsx.kicost:
create_kicost_sheet(workbook, groups, cfg)
workbook.close()
return True

View File

@ -196,6 +196,7 @@ W_BADFIELD = '(W062) '
W_UNKDIST = '(W063) '
W_UNKCUR = '(W064) '
W_NONETLIST = '(W065) '
W_NOKICOST = '(W066) '
class Rect(object):

View File

@ -156,7 +156,9 @@ class BoMXLSX(BoMLinkable):
self.max_col_width = 60
""" [20,999] Maximum column width (characters) """
self.style = 'modern-blue'
""" Head style: modern-blue, modern-green, modern-red and classic. """
""" Head style: modern-blue, modern-green, modern-red and classic """
self.kicost = False
""" Enable KiCost worksheet creation """
def config(self, parent):
super().config(parent)

1
tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.local

View File

@ -0,0 +1,82 @@
EESchema Schematic File Version 4
EELAYER 30 0
EELAYER END
$Descr A4 11693 8268
encoding utf-8
Sheet 1 1
Title "KiCost Test Schematic"
Date "2021-04-06"
Rev "A"
Comp "INTI - MyNT"
Comment1 ""
Comment2 ""
Comment3 ""
Comment4 ""
$EndDescr
Text Notes 500 600 0 79 ~ 0
This schematic serves as a test-file for the KiBot export script.\n
Text Notes 5950 2600 0 118 ~ 0
The test tests the following \nvariants matrix:\n production test default\nC1 X\nC2 X X\nR1 X X X\nR2 X X\n
$Comp
L Device:C C1
U 1 1 5F43BEC2
P 1000 1700
F 0 "C1" H 1115 1746 50 0000 L CNN
F 1 "1nF" H 1115 1655 50 0000 L CNN
F 2 "" H 1038 1550 50 0001 C CNN
F 3 "~" H 1000 1700 50 0001 C CNN
F 4 "-production,+test" H 1000 1700 50 0001 C CNN "Config"
F 5 "Samsung" H 1000 1700 50 0001 C CNN "manf"
F 6 "CL10B102KC8NNNC" H 1000 1700 50 0001 C CNN "manf#"
F 7 "1276-1131-1-ND" H 1000 1700 50 0001 C CNN "digikey#"
1 1000 1700
1 0 0 -1
$EndComp
$Comp
L Device:C C2
U 1 1 5F43CE1C
P 1450 1700
F 0 "C2" H 1565 1746 50 0000 L CNN
F 1 "1000 pF" H 1565 1655 50 0000 L CNN
F 2 "" H 1488 1550 50 0001 C CNN
F 3 "~" H 1450 1700 50 0001 C CNN
F 4 "+production,+test" H 1450 1700 50 0001 C CNN "Config"
F 5 "Samsung" H 1000 1700 50 0001 C CNN "manf"
F 6 "CL10B102KC8NNNC" H 1000 1700 50 0001 C CNN "manf#"
F 7 "1276-1131-1-ND" H 1000 1700 50 0001 C CNN "digikey#"
1 1450 1700
1 0 0 -1
$EndComp
$Comp
L Device:R R1
U 1 1 5F43D144
P 2100 1700
F 0 "R1" H 2170 1746 50 0000 L CNN
F 1 "1k" H 2170 1655 50 0000 L CNN
F 2 "" V 2030 1700 50 0001 C CNN
F 3 "~" H 2100 1700 50 0001 C CNN
F 4 "3k3" H 2100 1700 50 0001 C CNN "test:Value"
F 5 "Bourns" H 1000 1700 50 0001 C CNN "manf"
F 6 "CR0603-JW-102ELF" H 1000 1700 50 0001 C CNN "manf#"
F 7 "CR0603-JW-102ELFCT-ND" H 1000 1700 50 0001 C CNN "digikey#"
F 9 "CR0603-JW-332ELF" H 1000 1700 50 0001 C CNN "test:manf#"
F 10 "CR0603-JW-332ELFCT-ND" H 1000 1700 50 0001 C CNN "test:digikey#"
1 2100 1700
1 0 0 -1
$EndComp
$Comp
L Device:R R2
U 1 1 5F43D4BB
P 2500 1700
F 0 "R2" H 2570 1746 50 0000 L CNN
F 1 "1000" H 2570 1655 50 0000 L CNN
F 2 "" V 2430 1700 50 0001 C CNN
F 3 "~" H 2500 1700 50 0001 C CNN
F 4 "-test" H 2500 1700 50 0001 C CNN "Config"
F 5 "Bourns" H 1000 1700 50 0001 C CNN "manf"
F 6 "CR0603-JW-102ELF" H 1000 1700 50 0001 C CNN "manf#"
F 7 "CR0603-JW-102ELFCT-ND" H 1000 1700 50 0001 C CNN "digikey#"
1 2500 1700
1 0 0 -1
$EndComp
$EndSCHEMATC

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<export version="D">
<design>
<source>/home/salvador/0Data/Eccosur/kibot/tests/board_samples/kicad_5/1/kibom-variant_2c.sch</source>
<date>mar 06 abr 2021 14:20:00</date>
<tool>Eeschema 5.1.9+dfsg1-1</tool>
<sheet number="1" name="/" tstamps="/">
<title_block>
<title>KiCost Test Schematic</title>
<company>INTI - MyNT</company>
<rev>A</rev>
<date>2021-04-06</date>
<source>kibom-variant_2c.sch</source>
<comment number="1" value=""/>
<comment number="2" value=""/>
<comment number="3" value=""/>
<comment number="4" value=""/>
</title_block>
</sheet>
</design>
<components>
<comp ref="C1">
<value>1nF</value>
<datasheet>~</datasheet>
<fields>
<field name="Config">-production,+test</field>
<field name="digikey#">1276-1131-1-ND</field>
<field name="manf">Samsung</field>
<field name="manf#">CL10B102KC8NNNC</field>
</fields>
<libsource lib="Device" part="C" description="Unpolarized capacitor"/>
<sheetpath names="/" tstamps="/"/>
<tstamp>5F43BEC2</tstamp>
</comp>
<comp ref="C2">
<value>1000 pF</value>
<datasheet>~</datasheet>
<fields>
<field name="Config">+production,+test</field>
<field name="digikey#">1276-1131-1-ND</field>
<field name="manf">Samsung</field>
<field name="manf#">CL10B102KC8NNNC</field>
</fields>
<libsource lib="Device" part="C" description="Unpolarized capacitor"/>
<sheetpath names="/" tstamps="/"/>
<tstamp>5F43CE1C</tstamp>
</comp>
<comp ref="R1">
<value>1k</value>
<datasheet>~</datasheet>
<fields>
<field name="digikey#">CR0603-JW-102ELFCT-ND</field>
<field name="manf">Bourns</field>
<field name="manf#">CR0603-JW-102ELF</field>
<field name="test:Value">3k3</field>
<field name="test:digikey#">CR0603-JW-332ELFCT-ND</field>
<field name="test:manf#">CR0603-JW-332ELF</field>
</fields>
<libsource lib="Device" part="R" description="Resistor"/>
<sheetpath names="/" tstamps="/"/>
<tstamp>5F43D144</tstamp>
</comp>
<comp ref="R2">
<value>1000</value>
<datasheet>~</datasheet>
<fields>
<field name="Config">-test</field>
<field name="digikey#">CR0603-JW-102ELFCT-ND</field>
<field name="manf">Bourns</field>
<field name="manf#">CR0603-JW-102ELF</field>
</fields>
<libsource lib="Device" part="R" description="Resistor"/>
<sheetpath names="/" tstamps="/"/>
<tstamp>5F43D4BB</tstamp>
</comp>
</components>
<libparts>
<libpart lib="Device" part="C">
<description>Unpolarized capacitor</description>
<docs>~</docs>
<footprints>
<fp>C_*</fp>
</footprints>
<fields>
<field name="Reference">C</field>
<field name="Value">C</field>
</fields>
<pins>
<pin num="1" name="~" type="passive"/>
<pin num="2" name="~" type="passive"/>
</pins>
</libpart>
<libpart lib="Device" part="R">
<description>Resistor</description>
<docs>~</docs>
<footprints>
<fp>R_*</fp>
</footprints>
<fields>
<field name="Reference">R</field>
<field name="Value">R</field>
</fields>
<pins>
<pin num="1" name="~" type="passive"/>
<pin num="2" name="~" type="passive"/>
</pins>
</libpart>
</libparts>
<libraries>
<library logical="Device">
<uri>/usr/share/kicad/library/Device.lib</uri>
</library>
</libraries>
<nets>
<net code="1" name="Net-(C1-Pad1)">
<node ref="C1" pin="1"/>
</net>
<net code="2" name="Net-(C1-Pad2)">
<node ref="C1" pin="2"/>
</net>
<net code="3" name="Net-(C2-Pad1)">
<node ref="C2" pin="1"/>
</net>
<net code="4" name="Net-(C2-Pad2)">
<node ref="C2" pin="2"/>
</net>
<net code="5" name="Net-(R1-Pad1)">
<node ref="R1" pin="1"/>
</net>
<net code="6" name="Net-(R1-Pad2)">
<node ref="R1" pin="2"/>
</net>
<net code="7" name="Net-(R2-Pad1)">
<node ref="R2" pin="1"/>
</net>
<net code="8" name="Net-(R2-Pad2)">
<node ref="R2" pin="2"/>
</net>
</nets>
</export>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
<gesmes:subject>Reference rates</gesmes:subject>
<gesmes:Sender>
<gesmes:name>European Central Bank</gesmes:name>
</gesmes:Sender>
<Cube>
<Cube time='2021-04-08'>
<Cube currency='USD' rate='1.1873'/>
<Cube currency='JPY' rate='129.71'/>
<Cube currency='BGN' rate='1.9558'/>
<Cube currency='CZK' rate='25.875'/>
<Cube currency='DKK' rate='7.4377'/>
<Cube currency='GBP' rate='0.86290'/>
<Cube currency='HUF' rate='358.65'/>
<Cube currency='PLN' rate='4.5513'/>
<Cube currency='RON' rate='4.9198'/>
<Cube currency='SEK' rate='10.2073'/>
<Cube currency='CHF' rate='1.1021'/>
<Cube currency='ISK' rate='151.00'/>
<Cube currency='NOK' rate='10.0780'/>
<Cube currency='HRK' rate='7.5835'/>
<Cube currency='RUB' rate='91.4618'/>
<Cube currency='TRY' rate='9.6799'/>
<Cube currency='AUD' rate='1.5539'/>
<Cube currency='BRL' rate='6.6545'/>
<Cube currency='CAD' rate='1.4947'/>
<Cube currency='CNY' rate='7.7749'/>
<Cube currency='HKD' rate='9.2356'/>
<Cube currency='IDR' rate='17257.41'/>
<Cube currency='ILS' rate='3.8994'/>
<Cube currency='INR' rate='88.5885'/>
<Cube currency='KRW' rate='1324.91'/>
<Cube currency='MXN' rate='23.9497'/>
<Cube currency='MYR' rate='4.9125'/>
<Cube currency='NZD' rate='1.6855'/>
<Cube currency='PHP' rate='57.713'/>
<Cube currency='SGD' rate='1.5916'/>
<Cube currency='THB' rate='37.347'/>
<Cube currency='ZAR' rate='17.2677'/>
</Cube>
</Cube>
</gesmes:Envelope>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
Prj:,kibom-variant_2c,,,,,Board Qty:,100,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Co.:,INTI - MyNT,,,,,Unit Cost:,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Global Part Info,,,,,,,,Arrow,,,,,Digi-Key,,,,,Farnell,,,,,LCSC,,,,,Mouser,,,,,Newark,,,,,RS Components,,,,,TME,,,,,test,,,,
Refs,Value,Footprint,Manf,Manf#,Qty,Unit$,Ext$,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#
"R1,R2",1k,,Bourns,CR0603-JW-102ELF,200,0,0,,,,,,51387,,0,0,CR0603-JW-102ELFCT-ND,55000,,0,0,2333561,,,,,,52251,,0,0,652CR0603JW102ELF,110000,,0,0,02J2284,,,,,,,,,,,,,,,
,USD($)/GBP(£):,1.375941592305018,,,,Purchase description:,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,
,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1 Prj: kibom-variant_2c Board Qty: 100
2 Co.: INTI - MyNT Unit Cost: 0
3 Global Part Info Arrow Digi-Key Farnell LCSC Mouser Newark RS Components TME test
4 Refs Value Footprint Manf Manf# Qty Unit$ Ext$ Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat#
5 R1,R2 1k Bourns CR0603-JW-102ELF 200 0 0 51387 0 0 CR0603-JW-102ELFCT-ND 55000 0 0 2333561 52251 0 0 652CR0603JW102ELF 110000 0 0 02J2284
6 USD($)/GBP(£): 1.375941592305018 Purchase description: 0 0 0 0 0 0 0 0 0
7 0

View File

@ -0,0 +1,9 @@
Prj:,kibom-variant_2c,,,,,Board Qty:,100,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Co.:,INTI - MyNT,,,,,Unit Cost:,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Global Part Info,,,,,,,,Arrow,,,,,Digi-Key,,,,,Farnell,,,,,LCSC,,,,,Mouser,,,,,Newark,,,,,RS Components,,,,,TME,,,,,test,,,,
Refs,Value,Footprint,Manf,Manf#,Qty,Unit$,Ext$,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#
"C1,C2",1nF,,Samsung,CL10B102KC8NNNC,200,0,0,,,,,,NonStk,,0,0,1276-1131-1-ND,3860,,0,0,3013404,542250,,0,0,C153291,NonStk,,0,0,187CL10B102KC8NNNC,19600,,0,0,82AC9311,NonStk,,0,0,7665480,5789,,0,0,CL10B102KC8NNNC,,,,,
,USD($)/EUR(€):,1.1873,,,,Purchase description:,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,
,USD($)/GBP(£):,1.375941592305018,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1 Prj: kibom-variant_2c Board Qty: 100
2 Co.: INTI - MyNT Unit Cost: 0
3 Global Part Info Arrow Digi-Key Farnell LCSC Mouser Newark RS Components TME test
4 Refs Value Footprint Manf Manf# Qty Unit$ Ext$ Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat# Avail Purch Unit$ Ext$ Cat#
5 C1,C2 1nF Samsung CL10B102KC8NNNC 200 0 0 NonStk 0 0 1276-1131-1-ND 3860 0 0 3013404 542250 0 0 C153291 NonStk 0 0 187CL10B102KC8NNNC 19600 0 0 82AC9311 NonStk 0 0 7665480 5789 0 0 CL10B102KC8NNNC
6 USD($)/EUR(€): 1.1873 Purchase description: 0 0 0 0 0 0 0 0 0
7 USD($)/GBP(£): 1.375941592305018 0

View File

@ -5,10 +5,10 @@ For debug information use:
pytest-3 --log-cli-level debug
"""
import os
import os.path as op
import sys
# Look for the 'utils' module from where the script is running
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
prev_dir = op.dirname(op.dirname(op.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
@ -20,10 +20,16 @@ import subprocess
OUT_DIR = 'KiCost'
def conver2csv(xlsx):
def convert2csv(xlsx, skip_empty=False, sheet=None):
csv = xlsx[:-4]+'csv'
logging.debug('Converting to CSV')
p1 = subprocess.Popen(['xlsx2csv', '--skipemptycolumns', xlsx], stdout=subprocess.PIPE)
cmd = ['xlsx2csv']
if skip_empty:
cmd.append('--skipemptycolumns')
if sheet:
cmd.extend(['-n', sheet])
cmd.append(xlsx)
p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE)
with open(csv, 'w') as f:
p2 = subprocess.Popen(['egrep', '-i', '-v', r'( date|kicost|Total purchase)'], stdin=p1.stdout, stdout=f)
p2.communicate()[0]
@ -32,10 +38,10 @@ def conver2csv(xlsx):
def check_simple(ctx, variant):
if variant:
variant = '_'+variant
name = os.path.join(OUT_DIR, 'simple'+variant+'.xlsx')
name = op.join(OUT_DIR, 'simple'+variant+'.xlsx')
ctx.expect_out_file(name)
xlsx = ctx.get_out_path(name)
conver2csv(xlsx)
convert2csv(xlsx, skip_empty=True)
ctx.compare_txt(name[:-4]+'csv')
@ -48,3 +54,17 @@ def test_kicost_simple(test_dir):
check_simple(ctx, 'production')
check_simple(ctx, 'test')
ctx.clean_up()
def test_kicost_bom_simple(test_dir):
prj = 'kibom-variant_2c'
ctx = context.TestContextSCH(test_dir, 'test_kicost_bom_simple', prj, 'int_bom_kicost_simple_xlsx', OUT_DIR)
ctx.run(kicost=True) # , extra_debug=True
output = op.join(OUT_DIR, prj+'-bom.xlsx')
ctx.expect_out_file(output)
convert2csv(ctx.get_out_path(output), sheet='Costs')
csv = output[:-4]+'csv'
ctx.compare_txt(csv)
convert2csv(ctx.get_out_path(output), sheet='Costs (DNF)')
ctx.compare_txt(csv, output[:-5]+'_dnf.csv')
ctx.clean_up()

View File

@ -356,6 +356,7 @@ def test_help_filters(test_dir):
def test_help_output_plugin_1(test_dir, monkeypatch):
ctx = context.TestContext(test_dir, 'test_help_output_plugin_1', '3Rs', 'pre_and_position', POS_DIR)
ctx.home_local_link()
with monkeypatch.context() as m:
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
logging.debug('HOME='+os.environ['HOME'])
@ -371,6 +372,7 @@ def test_help_output_plugin_1(test_dir, monkeypatch):
def test_help_output_plugin_2(test_dir, monkeypatch):
ctx = context.TestContext(test_dir, 'test_help_output_plugin_2', '3Rs', 'pre_and_position', POS_DIR)
ctx.home_local_link()
with monkeypatch.context() as m:
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
logging.debug('HOME='+os.environ['HOME'])
@ -384,6 +386,7 @@ def test_help_output_plugin_2(test_dir, monkeypatch):
def test_help_output_plugin_3(test_dir, monkeypatch):
ctx = context.TestContext(test_dir, 'test_help_output_plugin_3', '3Rs', 'pre_and_position', POS_DIR)
ctx.home_local_link()
with monkeypatch.context() as m:
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
logging.debug('HOME='+os.environ['HOME'])
@ -394,6 +397,7 @@ def test_help_output_plugin_3(test_dir, monkeypatch):
def test_help_output_plugin_4(test_dir, monkeypatch):
ctx = context.TestContext(test_dir, 'test_help_output_plugin_4', '3Rs', 'pre_and_position', POS_DIR)
ctx.home_local_link()
with monkeypatch.context() as m:
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
logging.debug('HOME='+os.environ['HOME'])
@ -475,6 +479,7 @@ def test_example_6(test_dir):
def test_example_7(test_dir, monkeypatch):
""" With dummy plug-ins """
ctx = context.TestContext(test_dir, 'Example7', '3Rs', 'pre_and_position', '')
ctx.home_local_link()
with monkeypatch.context() as m:
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
ctx.run(extra=['--example'], no_verbose=True, no_yaml_file=True, no_board_file=True)
@ -796,6 +801,7 @@ def test_empty_zip(test_dir):
def test_compress_fail_deps(test_dir, monkeypatch):
ctx = context.TestContext(test_dir, 'test_compress_fail_deps', '3Rs', 'compress_fail_deps', 'Test')
ctx.home_local_link()
with monkeypatch.context() as m:
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
ctx.run(INTERNAL_ERROR)

View File

@ -21,6 +21,8 @@ KICAD_VERSION_5_99 = 5099000
KICAD_VERSION_5_1_7 = 5001007
MODE_SCH = 1
MODE_PCB = 0
# Defined as True to collect real world queries
ADD_QUERY_TO_KNOWN = False
ng_ver = os.environ.get('KIAUS_USE_NIGHTLY')
if ng_ver:
@ -280,7 +282,7 @@ class TestContext(object):
self.err = self.err.decode()
def run(self, ret_val=None, extra=None, use_a_tty=False, filename=None, no_out_dir=False, no_board_file=False,
no_yaml_file=False, chdir_out=False, no_verbose=False, extra_debug=False, do_locale=False):
no_yaml_file=False, chdir_out=False, no_verbose=False, extra_debug=False, do_locale=False, kicost=False):
logging.debug('Running '+self.test_name)
# Change the command to be local and add the board and output arguments
cmd = [os.path.abspath(os.path.dirname(os.path.abspath(__file__))+'/../../src/kibot')]
@ -311,7 +313,29 @@ class TestContext(object):
os.environ['LANG'] = do_locale
logging.debug('LOCPATH='+os.environ['LOCPATH'])
logging.debug('LANG='+os.environ['LANG'])
self.do_run(cmd, ret_val, use_a_tty, chdir_out)
# KiCost fake environment setup
if kicost:
# Always fake the currency rates
os.environ['KICOST_CURRENCY_RATES'] = 'tests/data/currency_rates.xml'
if ADD_QUERY_TO_KNOWN:
queries_file = 'tests/data/kitspace_queries.txt'
os.environ['KICOST_LOG_HTTP'] = queries_file
with open(queries_file, 'at') as f:
f.write('# ' + self.board_name + '\n')
server = None
else:
os.environ['KICOST_KITSPACE_URL'] = 'http://localhost:8000'
fo = open(self.get_out_path('server_stdout.txt'), 'at')
fe = open(self.get_out_path('server_stderr.txt'), 'at')
server = subprocess.Popen('./tests/utils/dummy-web-server.py', stdout=fo, stderr=fe)
try:
self.do_run(cmd, ret_val, use_a_tty, chdir_out)
finally:
# Always kill the fake web server
if kicost and server is not None:
server.terminate()
fo.close()
fe.close()
# Do we need to restore the locale?
if do_locale:
if old_LOCPATH:
@ -689,6 +713,16 @@ class TestContext(object):
targets[parts[0].strip()] = parts[1].strip()
return targets
def home_local_link(self):
""" Make sure that ./tests can be used as a replacement for HOME.
Currently just links ~/.local """
home = os.environ.get('HOME')
if home is not None:
local = os.path.join(home, '.local')
fake_local = os.path.join('tests', '.local')
if os.path.isdir(local) and not os.path.isdir(fake_local):
os.symlink(local, fake_local)
class TestContextSCH(TestContext):

138
tests/utils/dummy-web-server.py Executable file
View File

@ -0,0 +1,138 @@
#!/usr/bin/python3
"""
Very simple HTTP server in python (Updated for Python 3.7)
Usage:
./dummy-web-server.py -h
./dummy-web-server.py -l localhost -p 8000
Send a GET request:
curl http://localhost:8000
Send a HEAD request:
curl -I http://localhost:8000
Send a POST request:
curl -d 'foo=bar&bin=baz' http://localhost:8000
This code is available for use under the MIT license.
----
Copyright 2021 Brad Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the 'Software'), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import argparse
import os.path as op
import sys
from urllib.parse import unquote
from http.server import HTTPServer, BaseHTTPRequestHandler
queries = {}
comments = {}
class S(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
def _html(self, message):
"""This just generates an HTML document that includes `message`
in the body. Override, or re-write this do do more interesting stuff.
"""
content = "<html><body><h1>{}</h1></body></html>".format(message)
return content.encode("utf8") # NOTE: must return a bytes object!
def do_GET(self):
self._set_headers()
self.wfile.write(self._html("hi!"))
def do_HEAD(self):
self._set_headers()
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length).decode('utf8')
self._set_headers()
if post_data in queries:
self.wfile.write(queries[post_data].encode("utf8"))
print("Known query "+comments[post_data])
else:
data = unquote(post_data.replace('+', ' '))
print('Unknown query, len={}\n{}\n{}'.format(content_length, post_data, data))
content = "<html><body><h1>POST!</h1><pre>{}</pre></body></html>".format(post_data)
self.wfile.write(content.encode("utf8"))
sys.stdout.flush()
def load_queries(file):
global queries
with open(file, 'rt') as f:
is_query = True
last_comment = None
id = 1
for line in f:
line = line[:-1]
if line[0] == '#':
last_comment = line[1:].strip()
id = 1
elif is_query:
query = line
is_query = False
else:
# print(query)
# print(len(query))
queries[query] = line
comments[query] = '{} ({})'.format(last_comment, id)
id += 1
is_query = True
def run(server_class=HTTPServer, handler_class=S, addr="localhost", port=8000):
server_address = (addr, port)
httpd = server_class(server_address, handler_class)
load_queries(op.join(op.dirname(__file__), '../data/kitspace_queries.txt'))
print("Starting httpd server on {}:{}".format(addr, port))
httpd.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run a simple HTTP server")
parser.add_argument(
"-l",
"--listen",
default="localhost",
help="Specify the IP address on which the server listens",
)
parser.add_argument(
"-p",
"--port",
type=int,
default=8000,
help="Specify the port on which the server listens",
)
args = parser.parse_args()
run(addr=args.listen, port=args.port)

View File

@ -0,0 +1,12 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in HTML format"
type: bom
dir: KiCost
options:
xlsx:
kicost: true