from decimal import Decimal as D import os import re import pytest import coverage import logging import subprocess import sys from . import context from kibot.layer import Layer from kibot.pre_base import BasePreFlight from kibot.out_base import BaseOutput from kibot.gs import GS from kibot.kiplot import load_actions, _import, load_board, generate_makefile from kibot.dep_downloader import search_as_plugin from kibot.registrable import RegOutput, RegFilter from kibot.misc import (WRONG_INSTALL, BOM_ERROR, DRC_ERROR, ERC_ERROR, PDF_PCB_PRINT, KICAD2STEP_ERR) from kibot.bom.columnlist import ColumnList from kibot.bom.units import get_prefix, comp_match import kibot.bom.units as units from kibot.bom.electro_grammar import parse from kibot.__main__ import detect_kicad from kibot.kicad.config import KiConf from kibot.globals import Globals from kibot.PcbDraw.unit import read_resistance cov = coverage.Coverage() mocked_check_output_FNF = True mocked_check_output_retOK = '' mocked_call_enabled = False # Important note: # - We can't load the plug-ins twice, the import fails. # - Once we patched them using monkey patch the patch isn't reverted unless we load them again. # I don't know the real reason, may be related to the way we load plug-ins. # For this reason this patch is used for more than one case. def mocked_check_output(cmd, stderr=None, text=False): logging.debug('mocked_check_output called') if mocked_check_output_FNF: raise FileNotFoundError() else: if mocked_check_output_retOK: return mocked_check_output_retOK e = subprocess.CalledProcessError(10, 'rar') e.output = b'THE_ERROR' raise e def mocked_call(cmd, exit_with=None): if mocked_call_enabled: logging.debug('Forcing fail on '+str(cmd)) if exit_with is not None: logging.error(cmd[0]+' returned %d', 5) sys.exit(exit_with) return 5 return subprocess.call(cmd) def patch_functions(m): m.setattr("subprocess.check_output", mocked_check_output) m.setattr('kibot.kiplot.exec_with_retry', mocked_call) GS.exec_with_retry = mocked_call def init_globals(): glb = Globals() glb.set_tree({}) glb.config(None) def run_compress(ctx, test_import_fail=False): with context.cover_it(cov): # Load the plug-ins load_actions() init_globals() # Create a compress object with the dummy file as source out = RegOutput.get_class_for('compress')() out.set_tree({'type': 'compress', 'options': {'format': 'RAR', 'files': [{'source': ctx.get_out_path('*')}]}}) out.config(None) # Setup the GS output dir, needed for the output path GS.out_dir = '.' # Run the compression and catch the error with pytest.raises(SystemExit) as pytest_wrapped_e: if test_import_fail: _import('out_bogus', os.path.abspath(os.path.join(os.path.dirname(__file__), 'fake_plugin/out_bogus.py'))) else: out.run('') return pytest_wrapped_e # No longer possible, we trust in check_tool_dep, it won't return an unexistent file name, so we don't catch FileNoFound # def test_no_rar(test_dir, caplog, monkeypatch): # global mocked_check_output_FNF # mocked_check_output_FNF = True # # Create a silly context to get the output path # ctx = context.TestContext(test_dir, 'test_v5', 'empty_zip', '') # # The file we pretend to compress # ctx.create_dummy_out_file('Test.txt') # # We will patch subprocess.check_output to make rar fail # with monkeypatch.context() as m: # patch_functions(m) # pytest_wrapped_e = run_compress(ctx) # # Check we exited because rar isn't installed # assert pytest_wrapped_e.type == SystemExit # assert pytest_wrapped_e.value.code == MISSING_TOOL # assert "Missing `rar` command" in caplog.text @pytest.mark.indep def test_rar_fail(test_dir, caplog, monkeypatch): global mocked_check_output_FNF mocked_check_output_FNF = False # Create a silly context to get the output path ctx = context.TestContext(test_dir, 'test_v5', 'empty_zip', '') # The file we pretend to compress ctx.create_dummy_out_file('Test.txt') # We will patch subprocess.check_output to make rar fail with monkeypatch.context() as m: patch_functions(m) pytest_wrapped_e = run_compress(ctx) pytest_wrapped_e2 = run_compress(ctx, test_import_fail=True) # Check we exited because rar isn't installed assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == WRONG_INSTALL assert "Failed to invoke rar command, error 10" in caplog.text # Not in the docker image ... pytest issue? (TODO) # assert "THE_ERROR" in caplog.text # Check we exited because the import failed assert pytest_wrapped_e2.type == SystemExit assert pytest_wrapped_e2.value.code == WRONG_INSTALL assert "Unable to import plug-ins:" in caplog.text class NoGetTargets(BaseOutput): def __init__(self): self.options = True self.comment = 'Fake' self.name = 'dummy' self.type = 'none' self._sch_related = True class DummyPre(BasePreFlight): def __init__(self): super().__init__('dummy', True) self._sch_related = True @pytest.mark.indep def test_no_get_targets(caplog): test = NoGetTargets() test_pre = DummyPre() # Also check the dependencies fallback GS.sch = None GS.sch_file = 'fake' with context.cover_it(cov): test.get_targets('') files = test.get_dependencies() files_pre = test_pre.get_dependencies() assert "Output 'Fake' (dummy) [none] doesn't implement get_targets(), please report it" in caplog.text assert files == [GS.sch_file] assert files_pre == [GS.sch_file] @pytest.mark.indep def test_ibom_parse_fail(test_dir, caplog, monkeypatch): global mocked_check_output_FNF mocked_check_output_FNF = False global mocked_check_output_retOK mocked_check_output_retOK = b'ERROR Parsing failed' # We will patch subprocess.check_output to make ibom fail with monkeypatch.context() as m: patch_functions(m) os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = 'True' with context.cover_it(cov): detect_kicad() # Load the plug-ins load_actions() init_globals() # Create an ibom object out = RegOutput.get_class_for('ibom')() out.set_tree({}) out.type = 'ibom' out.config(None) with pytest.raises(SystemExit) as pytest_wrapped_e: out.run('') assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == BOM_ERROR # logging.debug(caplog.text) assert "Failed to create BoM" in caplog.text mocked_check_output_retOK = '' @pytest.mark.indep def test_var_rename_no_variant(): with context.cover_it(cov): # Load the plug-ins load_actions() # Create an ibom object filter = RegFilter.get_class_for('var_rename')() GS.variant = None # Should just return filter.filter(None) @pytest.mark.indep def test_bom_no_sch(): with context.cover_it(cov): # Load the plug-ins load_actions() # Create an ibom object GS.sch = None out = RegOutput.get_class_for('bom')() (columns, extra) = out.options._get_columns() assert columns == ColumnList.COLUMNS_DEFAULT out = RegOutput.get_class_for('kibom')() options = out.options() columns = options.conf._get_columns() assert columns == ColumnList.COLUMNS_DEFAULT @pytest.mark.indep def test_pre_xrc_fail(test_dir, caplog, monkeypatch): ctx = context.TestContext(test_dir, 'test_v5', 'empty_zip', '') global mocked_call_enabled mocked_call_enabled = True with monkeypatch.context() as m: patch_functions(m) with context.cover_it(cov): load_actions() GS.set_pcb(ctx.board_file) sch = ctx.board_file GS.set_sch(sch.replace('.kicad_pcb', context.KICAD_SCH_EXT)) GS.out_dir = test_dir init_globals() pre_drc = BasePreFlight.get_class_for('run_drc')('run_drc', True) with pytest.raises(SystemExit) as e1: pre_drc.run() pre_erc = BasePreFlight.get_class_for('run_erc')('run_erc', True) with pytest.raises(SystemExit) as e2: pre_erc.run() out = RegOutput.get_class_for('pdf_pcb_print')() out.set_tree({'layers': 'all'}) out.config(None) out.type = 'pdf_pcb_print' with pytest.raises(SystemExit) as e3: out.run('') assert e1.type == SystemExit assert e1.value.code == DRC_ERROR assert e2.type == SystemExit assert e2.value.code == ERC_ERROR assert e3.type == SystemExit assert e3.value.code == PDF_PCB_PRINT assert 'pcbnew_do returned 5' in caplog.text ctx.clean_up() mocked_call_enabled = False @pytest.mark.indep def test_unimplemented_layer(caplog): with context.cover_it(cov): with pytest.raises(AssertionError) as e: Layer.solve(1) assert e.type == AssertionError assert e.value.args[0] == "Unimplemented layer type " @pytest.mark.indep def test_step_fail(test_dir, caplog, monkeypatch): global mocked_check_output_FNF mocked_check_output_FNF = False global mocked_call_enabled mocked_call_enabled = True # Create a silly context to get the output path ctx = context.TestContext(test_dir, 'test_v5', 'empty_zip', '') # We will patch subprocess.check_output to make rar fail with monkeypatch.context() as m: patch_functions(m) with context.cover_it(cov): detect_kicad() load_actions() init_globals() GS.set_pcb(ctx.board_file) GS.board = None KiConf.loaded = False load_board() # Create a compress object with the dummy file as source out = RegOutput.get_class_for('step')() out.set_tree({}) out.config(None) out.type = 'step' with pytest.raises(SystemExit) as e: out.run('') # Check we exited because rar isn't installed assert e.type == SystemExit assert e.value.code == KICAD2STEP_ERR assert "kicad2step_do returned 5" in caplog.text mocked_call_enabled = False @pytest.mark.indep def test_unknown_prefix(caplog): with context.cover_it(cov): get_prefix(1, 'y') assert 'Unknown prefix, please report' in caplog.text def test_search_as_plugin_ok(test_dir, caplog): ctx = context.TestContext(test_dir, 'test_v5', 'empty_zip', '') with context.cover_it(cov): detect_kicad() load_actions() dir_fake = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') GS.kicad_plugins_dirs.append(dir_fake) logging.debug('GS.kicad_plugins_dirs: '+str(GS.kicad_plugins_dirs)) fname = search_as_plugin('fake', ['fake_plugin']) logging.debug('fname: '+fname) with open(ctx.get_out_path('error.txt'), 'wt') as f: f.write(caplog.text) # Fails to collect caplog on docker image (TODO) # This is a bizarre case, the test alone works, all tests in test_misc* together work. # But when running all the tests this one fails to get caplog. # The test_rar_fail has a similar problem. # assert re.search(r"Using `(.*)data/fake_plugin/fake` for `fake` \(fake_plugin\)", caplog.text) is not None assert re.search(r"(.*)data/fake_plugin/fake", fname) is not None ctx.clean_up() def test_search_as_plugin_fail(test_dir, caplog): with context.cover_it(cov): detect_kicad() load_actions() dir_fake = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') GS.kicad_plugins_dirs.append(dir_fake) fname = search_as_plugin('fake', ['']) assert fname is None @pytest.mark.indep def test_layer_no_id(): with context.cover_it(cov): la = Layer() la.layer = 'F.Cu' la.description = 'Top' la.suffix = 'F_Cu' assert str(la) == "F.Cu ('Top' F_Cu)" @pytest.mark.indep def test_makefile_kibot_sys(test_dir): ctx = context.TestContext(test_dir, 'test_v5', 'empty_zip', '') GS.sch_file = 'foo.sch' GS.pcb_file = 'bar.kicad_pcb' GS.out_dir = ctx.get_out_path('') with context.cover_it(cov): generate_makefile(ctx.get_out_path('Makefile'), 'pp', [], kibot_sys=True) ctx.search_in_file('Makefile', [r'KIBOT\?=kibot']) ctx.clean_up() @pytest.mark.indep def test_units_1(): with context.cover_it(cov): # Test for ',' as decimal point units.decimal_point = ',' assert str(comp_match("3,3 pF", 'C')) == "3.3 pF" a = comp_match("0,1uf 10% 0402 50v x7r", 'C') assert str(a) == "100 nF" assert a.extra['tolerance'] == 10 assert a.extra['size'] == '0402' assert a.extra['voltage_rating'] == 50 assert a.extra['characteristic'] == 'X7R' a = comp_match("0.01uf, 50v, cog, 5%, 0603", 'C') assert str(a) == "10 nF" assert a.extra['tolerance'] == 5 assert a.extra['size'] == '0603' assert a.extra['voltage_rating'] == 50 assert a.extra['characteristic'] == 'C0G' a = comp_match("0,01uf; 50v; cog; 5%; 0603", 'C') assert str(a) == "10 nF" assert a.extra['tolerance'] == 5 assert a.extra['size'] == '0603' assert a.extra['voltage_rating'] == 50 assert a.extra['characteristic'] == 'C0G' units.decimal_point = '' assert str(comp_match("1", 'R')) == "1 Ω" assert str(comp_match("1000", 'R')) == "1 kΩ" assert str(comp_match("1000000", 'R')) == "1 MΩ" assert str(comp_match("1000000000", 'R')) == "1 GΩ" assert str(comp_match("3.3 pF", 'C')) == "3.3 pF" assert str(comp_match("0.0033 nF", 'C')) == "3.3 pF" assert str(comp_match("3p3", 'C')) == "3.3 pF" a = comp_match("3k3 1% 0805", 'R') assert str(a) == "3.3 kΩ" assert a.extra['tolerance'] == 1 assert a.extra['size'] == '0805' a = comp_match("0.01 1%", 'R') assert str(a) == "10 mΩ" assert a.extra['tolerance'] == 1 @pytest.mark.indep def test_read_resistance(): with context.cover_it(cov): assert read_resistance("4k7")[0] == D("4700") assert read_resistance("4k7")[0] == D("4700") assert read_resistance("4.7R")[0] == D("4.7") assert read_resistance("4R7")[0] == D("4.7") assert read_resistance("0R47")[0] == D("0.47") assert read_resistance("4700k")[0] == D("4700000") assert read_resistance("470m")[0] == D("0.47") assert read_resistance("470M")[0] == D("470000000") assert read_resistance("4M7")[0] == D("4700000") assert read_resistance("470")[0] == D("470") assert read_resistance("470Ω")[0] == D("470") assert read_resistance("470 Ω")[0] == D("470") assert read_resistance("470Ohm")[0] == D("470") assert read_resistance("470 Ohms")[0] == D("470") assert read_resistance("R47")[0] == D("0.47") assert read_resistance("1G")[0] == D("1000000000") assert read_resistance("4k7000")[0] == D("4700") @pytest.mark.indep def test_electro_grammar_1(): with context.cover_it(cov): C2UF_0603_30P = {'type': 'capacitor', 'capacitance': 2e-6, 'size': '0603', 'tolerance': 30} C2UF_0603 = {'type': 'capacitor', 'capacitance': 2e-6, 'size': '0603'} C10UF_0402 = {'type': 'capacitor', 'capacitance': 10e-6, 'size': '0402'} C100NF_0603 = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603'} C100NF_0603_X7R = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'X7R'} C100NF_0603_Z5U = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'Z5U'} C100NF_0603_Y5V = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'Y5V'} C100NF_0603_C0G = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'C0G'} C100NF_0603_25V = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'voltage_rating': 25} C100NF_0603_6V3 = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'voltage_rating': 6.3} C100UF_0603 = {'type': 'capacitor', 'capacitance': 100e-6, 'size': '0603'} C100UF_0603_X7R = {'type': 'capacitor', 'capacitance': 100e-6, 'size': '0603', 'characteristic': 'X7R'} C1N5_0603_X7R = {'type': 'capacitor', 'capacitance': 1.5e-9, 'size': '0603', 'characteristic': 'X7R'} C1F_0603_25V = {'type': 'capacitor', 'capacitance': 1, 'size': '0603', 'voltage_rating': 25} C_01005 = {'type': 'capacitor', 'size': '01005'} C_0201 = {'type': 'capacitor', 'size': '0201'} C_0402 = {'type': 'capacitor', 'size': '0402'} C_0603 = {'type': 'capacitor', 'size': '0603'} C_0805 = {'type': 'capacitor', 'size': '0805'} C_1206 = {'type': 'capacitor', 'size': '1206'} C_TESTS = ((('this is total rubbish', ''), {}), (('2uF 0603',), C2UF_0603), (('2uF 0603 30%', '2uF 0603 +/-30%', '2uF 0603 ±30%', '2uF 0603 +-30%'), C2UF_0603_30P), (('10uF 0402', '10 micro Farad 0402', '10 \u03BC''F 0402', '10 \u00B5''F 0402', '10𝛍F 0402', '10𝜇F 0402', '10𝝁 F 0402', '10 𝝻F 0402', '10𝞵F 0402'), C10UF_0402), (('100nF 0603 kajdlkja alkdjlkajd', 'adjalkjd 100nF akjdlkjda 0603 kajdlkja alkdjlkajd', 'capacitor 100nF 0603, warehouse 5', 'adjalkjd 0603 akjdlkjda 100nF kajdlkja alkdjlkajd', 'C 100n 0603', 'Capacitor 100n 0603', 'cap 100n 0603'), C100NF_0603), (('1n5F 0603 X7R',), C1N5_0603_X7R), (('100NF 0603 X7R', '100nF 0603 X7R', '100nF 0603 x7r'), C100NF_0603_X7R), (('100UF 0603 X7R',), C100UF_0603_X7R), (('100nF 0603 Z5U',), C100NF_0603_Z5U), (('100nF 0603 Y5V',), C100NF_0603_Y5V), (('100nF 0603 C0G', '100nF 0603 NP0', '100nF 0603 np0', '100nF 0603 c0g', '100nF 0603 cog', '100nF 0603 npO', '100nF 0603 COG', '100nF 0603 C0G/NP0'), C100NF_0603_C0G), (('1F 0603 25V', '1f 0603 25V', '1 Farad 0603 25V'), C1F_0603_25V), (('100nF 0603 25V', '100nF 0603 25 v'), C100NF_0603_25V), (('100nF 0603 6v3', '100nF 0603 6V3', '100nF 0603 6.3V', '100nF 0603 6.3v'), C100NF_0603_6V3), (('0603 0.0001F', '0603 0.0001 F', '0603 0.1mF'), C100UF_0603), (('capacitor 01005',), C_01005), (('capacitor 0201',), C_0201), (('capacitor 0402',), C_0402), (('capacitor 0603',), C_0603), (('capacitor 0805',), C_0805), (('capacitor 1206',), C_1206)) R1K_0603 = {'type': 'resistor', 'size': '0603', 'resistance': 1000} R1K_0805_5P = {'type': 'resistor', 'size': '0805', 'resistance': 1000, 'tolerance': 5} R1K_0805_01P = {'type': 'resistor', 'size': '0805', 'resistance': 1000, 'tolerance': 0.1} R1K_0805_5P_100MW = {'type': 'resistor', 'size': '0805', 'resistance': 1000, 'tolerance': 5, 'power_rating': 0.1} R1K_0201_500MW = {'type': 'resistor', 'size': '0201', 'resistance': 1000, 'power_rating': 0.5} R0_0201_125MW = {'type': 'resistor', 'size': '0201', 'resistance': 0, 'power_rating': 0.125} R1M_0603 = {'type': 'resistor', 'size': '0603', 'resistance': 1e6} R1M = {'type': 'resistor', 'resistance': 1e6} R1M1_0603 = {'type': 'resistor', 'size': '0603', 'resistance': 1.1e6} R100 = {'type': 'resistor', 'resistance': 100} R10K_0805 = {'type': 'resistor', 'size': '0805', 'resistance': 10000} R1 = {'type': 'resistor', 'resistance': 1} R1_0402 = {'type': 'resistor', 'resistance': 1, 'size': '0402'} R1_0805 = {'type': 'resistor', 'resistance': 1, 'size': '0805'} R1K5_0402 = {'type': 'resistor', 'resistance': 1500, 'size': '0402'} R2_7_0402 = {'type': 'resistor', 'resistance': 2.7, 'size': '0402'} R1MILI = {'type': 'resistor', 'resistance': 0.001} R100U = {'type': 'resistor', 'resistance': 0.0001} R_01005 = {'type': 'resistor', 'size': '01005'} R_0201 = {'type': 'resistor', 'size': '0201'} R_0402 = {'type': 'resistor', 'size': '0402'} R_0603 = {'type': 'resistor', 'size': '0603'} R_0805 = {'type': 'resistor', 'size': '0805'} R_1206 = {'type': 'resistor', 'size': '1206'} R_TESTS = ((('R 0.01 1%',), {'type': 'resistor', 'resistance': 0.01, 'tolerance': 1}), (('1k 0603', '1k ohm 0603', '1K ohms 0603'), R1K_0603), (('resistor 100', '100R', '100 R'), R100), (('r 10000 0805',), R10K_0805), (('res or whatever 1',), R1), (('1 ohm 0402',), R1_0402), (('1Ω 0805', '1Ω 0805'), R1_0805), (('1MEG 0603', '1M 0603'), R1M_0603), (('1M1 ohms 0603',), R1M1_0603), (('1k5 0402', '1.5k 0402'), R1K5_0402), (('2r7 0402', '2R7 0402'), R2_7_0402), (('1 mOhm',), R1MILI), (('1 MOhm',), R1M), (('100 uΩ',), R100U), (('1k 0805 5%',), R1K_0805_5P), (('1k 0805 0.1%',), R1K_0805_01P), (('1k 0805 5% 100mW',), R1K_0805_5P_100MW), (('0 ohm 0201 0.125W', '0 ohm 0201 1/8W'), R0_0201_125MW), (('resistor 1k 0201 1/2 watts',), R1K_0201_500MW), (('resistor 01005',), R_01005), (('resistor 0201',), R_0201), (('resistor 0402',), R_0402), (('resistor 0603',), R_0603), (('resistor 0805',), R_0805), (('resistor 1206',), R_1206)) LED_TEST = ((('led red 0603',), {'type': 'led', 'size': '0603', 'color': 'red'}), (('SMD LED GREEN 0805', 'GREEN 0805 LED'), {'type': 'led', 'size': '0805', 'color': 'green'})) L_TEST = ((('L 100 0805', 'IND 100 0805', 'Inductor 100 0805'), {'type': 'inductor', 'inductance': 100, 'size': '0805'}), (('3n3 H', '3n3H', '3.3 nH', '3300pH', '3.3 nano Henry', 'This is a 3.3 nH inductor'), {'type': 'inductor', 'inductance': 3.3e-9})) TESTS = C_TESTS+R_TESTS+L_TEST+LED_TEST for test in TESTS: ref = test[1] for c in test[0]: res = parse(c) assert res == ref, "For `{}` got:\n{}\nExpected:\n{}".format(c, res, ref) logging.debug(c+" Ok")