KiBot/tests/utils/context.py

264 lines
9.6 KiB
Python

import os
import shutil
import tempfile
import logging
import subprocess
import re
import pytest
from glob import glob
from pty import openpty
COVERAGE_SCRIPT = 'python3-coverage'
KICAD_PCB_EXT = '.kicad_pcb'
KICAD_SCH_EXT = '.sch'
REF_DIR = 'N/A'
MODE_SCH = 1
MODE_PCB = 0
class TestContext(object):
def __init__(self, test_name, board_name, yaml_name):
# We are using PCBs
self.mode = MODE_PCB
# The name used for the test output dirs and other logging
self.test_name = test_name
# The name of the PCB board file
self.board_name = board_name
# The actual board file that will be loaded
self._get_board_file()
# The YAML file we'll use
self._get_yaml_name(yaml_name)
# The actual output dir for this run
self._set_up_output_dir(pytest.config.getoption('test_dir'))
# stdout and stderr from the run
self.out = None
self.err = None
self.proc = None
def _get_board_dir(self):
this_dir = os.path.dirname(os.path.realpath(__file__))
return os.path.join(this_dir, '../board_samples')
def _get_board_file(self):
self.board_file = os.path.abspath(os.path.join(self._get_board_dir(),
self.board_name +
(KICAD_PCB_EXT if self.mode == MODE_PCB else KICAD_SCH_EXT)))
logging.info('KiCad file: '+self.board_file)
assert os.path.isfile(self.board_file)
def _get_yaml_dir(self):
this_dir = os.path.dirname(os.path.realpath(__file__))
return os.path.join(this_dir, '../yaml_samples')
def _get_yaml_name(self, name):
self.yaml_file = os.path.abspath(os.path.join(self._get_yaml_dir(), name+'.kiplot.yaml'))
logging.info('YAML file: '+self.yaml_file)
assert os.path.isfile(self.yaml_file)
def _set_up_output_dir(self, test_dir):
if test_dir:
self.output_dir = os.path.join(test_dir, self.test_name)
os.makedirs(self.output_dir, exist_ok=True)
self._del_dir_after = False
else:
# create a tmp dir
self.output_dir = tempfile.mkdtemp(prefix='tmp-kiplot-'+self.test_name+'-')
self._del_dir_after = True
logging.info('Output dir: '+self.output_dir)
def clean_up(self):
logging.debug('Clean-up')
if self._del_dir_after:
logging.debug('Removing dir')
shutil.rmtree(self.output_dir)
def get_out_path(self, filename):
return os.path.join(self.output_dir, filename)
def get_gerber_job_filename(self, dir):
return os.path.join(dir, self.board_name+'-job.gbrjob')
def get_gerber_filename(self, dir, layer_slug, ext='.gbr'):
return os.path.join(dir, self.board_name+'-'+layer_slug+ext)
def get_pos_top_filename(self, dir):
return os.path.join(dir, self.board_name+'-top.pos')
def get_pos_bot_filename(self, dir):
return os.path.join(dir, self.board_name+'-bottom.pos')
def get_pos_both_filename(self, dir):
return os.path.join(dir, self.board_name+'-both.pos')
def expect_out_file(self, filename):
file = self.get_out_path(filename)
assert os.path.isfile(file)
assert os.path.getsize(file) > 0
return file
def dont_expect_out_file(self, filename):
file = self.get_out_path(filename)
assert not os.path.isfile(file)
def create_dummy_out_file(self, filename):
file = self.get_out_path(filename)
with open(file, 'w') as f:
f.write('Dummy file\n')
def run(self, ret_val=None, extra=None, use_a_tty=False, filename=None):
logging.debug('Running '+self.test_name)
# Change the command to be local and add the board and output arguments
cmd = [COVERAGE_SCRIPT, 'run', '-a']
cmd.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__))+'/../../src/kiplot'))
cmd = cmd+['-b', filename if filename else self.board_file]
cmd = cmd+['-c', self.yaml_file]
cmd = cmd+['-d', self.output_dir]
if extra is not None:
cmd = cmd+extra
logging.debug(cmd)
out_filename = self.get_out_path('output.txt')
err_filename = self.get_out_path('error.txt')
if use_a_tty:
# This is used to test the coloured logs, we need stderr to be a TTY
master, slave = openpty()
f_err = slave
f_out = slave
else:
# Redirect stdout and stderr to files
f_out = os.open(out_filename, os.O_RDWR | os.O_CREAT)
f_err = os.open(err_filename, os.O_RDWR | os.O_CREAT)
# Run the process
process = subprocess.Popen(cmd, stdout=f_out, stderr=f_err)
ret_code = process.wait()
logging.debug('ret_code '+str(ret_code))
if use_a_tty:
self.err = os.read(master, 10000)
self.err = self.err.decode()
self.out = self.err
exp_ret = 0 if ret_val is None else ret_val
assert ret_code == exp_ret
if use_a_tty:
os.close(master)
os.close(slave)
with open(out_filename, 'w') as f:
f.write(self.out)
with open(err_filename, 'w') as f:
f.write(self.out)
else:
# Read stdout
os.lseek(f_out, 0, os.SEEK_SET)
self.out = os.read(f_out, 10000)
os.close(f_out)
self.out = self.out.decode()
# Read stderr
os.lseek(f_err, 0, os.SEEK_SET)
self.err = os.read(f_err, 10000)
os.close(f_err)
self.err = self.err.decode()
def search_out(self, text):
m = re.search(text, self.out, re.MULTILINE)
return m
def search_err(self, text):
m = re.search(text, self.err, re.MULTILINE)
return m
def search_in_file(self, file, texts):
logging.debug('Searching in "'+file+'" output')
with open(self.get_out_path(file)) as f:
txt = f.read()
res = []
for t in texts:
logging.debug('- r"'+t+'"')
m = re.search(t, txt, re.MULTILINE)
assert m
# logging.debug(' '+m.group(0))
res.append(m.groups())
return res
def search_not_in_file(self, file, texts):
logging.debug('Searching not in "'+file+'" output')
with open(self.get_out_path(file)) as f:
txt = f.read()
for t in texts:
logging.debug('- r"'+t+'"')
m = re.search(t, txt, re.MULTILINE)
assert m is None
def compare_image(self, image, reference=None, diff='diff.png'):
""" For images and single page PDFs """
if reference is None:
reference = image
cmd = ['compare', '-metric', 'MSE',
self.get_out_path(image),
os.path.join(REF_DIR, reference),
self.get_out_path(diff)]
logging.debug('Comparing images with: '+str(cmd))
res = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
m = re.match(r'([\d\.]+) \(([\d\.]+)\)', res.decode())
assert m
logging.debug('MSE={} ({})'.format(m.group(1), m.group(2)))
assert float(m.group(2)) == 0.0
def compare_pdf(self, gen, reference=None, diff='diff-{}.png'):
""" For multi-page PDFs """
if reference is None:
reference = gen
logging.debug('Comparing PDFs: '+gen+' vs '+reference)
# Split the reference
logging.debug('Splitting '+reference)
cmd = ['convert', '-density', '150',
os.path.join(REF_DIR, reference),
self.get_out_path('ref-%d.png')]
subprocess.check_call(cmd)
# Split the generated
logging.debug('Splitting '+gen)
cmd = ['convert', '-density', '150',
self.get_out_path(gen),
self.get_out_path('gen-%d.png')]
subprocess.check_call(cmd)
# Chek number of pages
ref_pages = glob(self.get_out_path('ref-*.png'))
gen_pages = glob(self.get_out_path('gen-*.png'))
logging.debug('Pages {} vs {}'.format(len(gen_pages), len(ref_pages)))
assert len(ref_pages) == len(gen_pages)
# Compare each page
for page in range(len(ref_pages)):
cmd = ['compare', '-metric', 'MSE',
self.get_out_path('ref-'+str(page)+'.png'),
self.get_out_path('gen-'+str(page)+'.png'),
self.get_out_path(diff.format(page))]
logging.debug('Comparing images with: '+str(cmd))
res = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
m = re.match(r'([\d\.]+) \(([\d\.]+)\)', res.decode())
assert m
logging.debug('MSE={} ({})'.format(m.group(1), m.group(2)))
assert float(m.group(2)) == 0.0
def compare_txt(self, text, reference=None, diff='diff.txt'):
if reference is None:
reference = text
cmd = ['/bin/sh', '-c', 'diff -ub '+os.path.join(REF_DIR, reference)+' ' +
self.get_out_path(text)+' > '+self.get_out_path(diff)]
logging.debug('Comparing texts with: '+str(cmd))
res = subprocess.call(cmd)
assert res == 0
def filter_txt(self, file, pattern, repl):
fname = self.get_out_path(file)
with open(fname) as f:
txt = f.read()
with open(fname, 'w') as f:
f.write(re.sub(pattern, repl, txt))
class TestContextSCH(TestContext):
def __init__(self, test_name, board_name, yaml_name):
super().__init__(test_name, board_name, yaml_name)
self.mode = MODE_SCH
self._get_board_file()