mirror of
ssh://devgit.janware.com/janware/proj/jw-devtest
synced 2026-10-27 12:17:34 +01:00
First commit
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
commit
4b912741cb
73 changed files with 3753 additions and 0 deletions
4
Makefile
Normal file
4
Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = .
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/topdir.mk
|
||||||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
1.0.0-0-dev
|
||||||
4
make/Makefile
Normal file
4
make/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/make.mk
|
||||||
4
make/java-defs.mk
Normal file
4
make/java-defs.mk
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
ifeq ($(OS_NAME),ubuntu)
|
||||||
|
export JAVA_HOME = $(shell PATH=/usr/sbin:$(PATH) update-alternatives --query javac | \
|
||||||
|
sed '/^Alternative:.*java-8/ !d; s/Alternative: *//; s,/bin/javac$$,,')
|
||||||
|
endif
|
||||||
9
make/proj.mk
Normal file
9
make/proj.mk
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# to be included from inside the project directory
|
||||||
|
|
||||||
|
DEV_PROJECTS_DIR ?= $(TOPDIR)/..
|
||||||
|
JWBDIR ?= $(firstword $(wildcard $(DEV_PROJECTS_DIR)/jw-build $(BUILD_TOOLS_PREFIX)/opt/$(FLAVOUR_PATH_PREFIX)jw-build))
|
||||||
|
|
||||||
|
include $(JWBDIR)/make/projects.mk
|
||||||
|
|
||||||
|
USE_PROJECT_LIB = false
|
||||||
|
HDRDIR_SCOPE_SUFFIX = $(PROJECT)
|
||||||
46
make/project.conf
Normal file
46
make/project.conf
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
[summary]
|
||||||
|
janware GmbH test utilities
|
||||||
|
|
||||||
|
[description]
|
||||||
|
This package provides utilities used for testing embedded devices
|
||||||
|
|
||||||
|
[global]
|
||||||
|
group = Development/Tools/Building
|
||||||
|
subpackages = run
|
||||||
|
license = LGPL
|
||||||
|
jw-maintainer = jan
|
||||||
|
|
||||||
|
[build]
|
||||||
|
libname = none
|
||||||
|
|
||||||
|
[pkg.run.post]
|
||||||
|
case "$1" in
|
||||||
|
0)
|
||||||
|
;;
|
||||||
|
1|2)
|
||||||
|
/usr/bin/systemctl enable atd
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
udevadm control --reload
|
||||||
|
|
||||||
|
[pkg.requires.os]
|
||||||
|
run = bridge-utils, at, = python3-texttable
|
||||||
|
#build = doxygen, graphviz, gcc, cmake, cpio, unzip, rsync, bc, python3-texttable, \
|
||||||
|
# gpg, bison, flex, gettext, texinfo, subversion, pkg-config, maven, \
|
||||||
|
# mtools, dosfstools, parted, mtd-utils, wget, curl, xmlto, iproute2, \
|
||||||
|
# bc, python3-networkx, udev, systemd
|
||||||
|
|
||||||
|
#[pkg.requires.ubuntu]
|
||||||
|
#build = openjdk-8-jdk, fdupes,libboost-system-dev, libboost-filesystem-dev, ncurses-dev, \
|
||||||
|
# squashfs-tools, kmod, repo, openbios-ppc, python3.7, python3-networkx, libc6-i386, \
|
||||||
|
# xxd, g++, qemu-system-arm, qemu-system-ppc, libnl-cli-3-dev, libiw-dev, libnm-dev
|
||||||
|
|
||||||
|
[pkg.requires.suse]
|
||||||
|
build = squashfs, ncurses-devel, python3-GitPython, gcc-c++, qemu-arm, qemu-ppc, \
|
||||||
|
vim, glibc-32bit, python3-click, python3-itsdangerous, python3-lxml, python3-Jinja2, \
|
||||||
|
python3-MarkupSafe, python3-Werkzeug
|
||||||
|
|
||||||
|
[pkg.requires.jw]
|
||||||
|
run = jw-python-run = VERSION
|
||||||
|
devel = jw-devtest-run = VERSION-REVISION, jw-build-devel = VERSION
|
||||||
|
build = jw-build-devel, jw-python-devel
|
||||||
1
make/py-test-case-phase.mk
Normal file
1
make/py-test-case-phase.mk
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
include $(JWBDIR)/make/py-mod.mk
|
||||||
4
scripts/Makefile
Normal file
4
scripts/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/scripts.mk
|
||||||
248
scripts/file-utils.py
Normal file
248
scripts/file-utils.py
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import jwutils
|
||||||
|
from jwutils.log import *
|
||||||
|
|
||||||
|
HPC = 255
|
||||||
|
SPT = 63
|
||||||
|
BPS = 512
|
||||||
|
|
||||||
|
class CmdMem2Disk(jwutils.Cmd):
|
||||||
|
|
||||||
|
class Chunk:
|
||||||
|
def __init__(self, size, offset=None, name=None):
|
||||||
|
self.name = name
|
||||||
|
self.size = size
|
||||||
|
self.offset = offset
|
||||||
|
def __str__(self):
|
||||||
|
return "n:{}, s:{}, o:{}".format(self.name, self.size, self.offset)
|
||||||
|
|
||||||
|
class Part(Chunk):
|
||||||
|
def __init__(self, rhs):
|
||||||
|
super().__init__(rhs.size, rhs.offset, rhs.name)
|
||||||
|
if isinstance(rhs, self.__class__):
|
||||||
|
self.addr = rhs.addr
|
||||||
|
self.aligned_size = rhs.aligned_size
|
||||||
|
else:
|
||||||
|
self.addr = None
|
||||||
|
self.aligned_size = CmdMem2Disk.align_to_block(self.size)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return super().__str__() + ", a:{}, S:{}".format(self.addr, self.aligned_size)
|
||||||
|
|
||||||
|
def __to_int(self, s):
|
||||||
|
if isinstance(s, int):
|
||||||
|
return s
|
||||||
|
if not s or not len(s):
|
||||||
|
return None
|
||||||
|
if s.find("0x") != -1:
|
||||||
|
return int(s, 16)
|
||||||
|
return int(s)
|
||||||
|
|
||||||
|
def __parse_mtdparts(self, spec, total_size=None):
|
||||||
|
#flash:0x80000@0x50000000(u-boot),0x800000@0x2000000(fb),0x1600000(rootfs),0x180000(data)
|
||||||
|
rx_partdef = re.compile("((0x)*[0-9a-f]+|[0-9]+|-)(@((0x)*[0-9a-f]+|[0-9]+))*(\(([a-zA-Z_-]+)\))*")
|
||||||
|
r = []
|
||||||
|
rest = total_size
|
||||||
|
mtdparts = spec.split(';')
|
||||||
|
count = 1
|
||||||
|
for mtddef_spec in mtdparts:
|
||||||
|
mtddef = mtddef_spec.split(':')
|
||||||
|
mtdid = mtddef[0]
|
||||||
|
for partdef_spec in mtddef[1].split(','):
|
||||||
|
match = rx_partdef.search(partdef_spec)
|
||||||
|
if not match:
|
||||||
|
raise Exception("Unparseable part definition >{}<".format(partdef_spec))
|
||||||
|
for i in range(0, len(match.groups())):
|
||||||
|
slog(DEBUG, "match", i, "=", match.group(i))
|
||||||
|
size = match.group(1)
|
||||||
|
offset = match.group(4)
|
||||||
|
name = match.group(7)
|
||||||
|
slog(DEBUG, "size={}, offset={}, name={}".format(size, offset, name))
|
||||||
|
if not name or not len(name):
|
||||||
|
name = str(count)
|
||||||
|
if rest and rest <= 0:
|
||||||
|
raise Exception("Partition \"{}\" starts after end of input".format(name))
|
||||||
|
if size == '-':
|
||||||
|
if rest is None:
|
||||||
|
raise Exception("Can't calculate length of partition \"{}\" with total space undefined".format(name))
|
||||||
|
size = rest
|
||||||
|
else:
|
||||||
|
size = self.__to_int(size)
|
||||||
|
offset = self.__to_int(offset)
|
||||||
|
r.append(self.Chunk(size, offset, name))
|
||||||
|
if rest:
|
||||||
|
rest -= size
|
||||||
|
count += 1
|
||||||
|
return r
|
||||||
|
|
||||||
|
def __lba(self, c, h, s):
|
||||||
|
return (c * HPC + h) * SPT + (s - 1)
|
||||||
|
|
||||||
|
def __chs(self, addr):
|
||||||
|
lba = addr / BPS
|
||||||
|
c = int(lba / (HPC * SPT))
|
||||||
|
h = int(lba / SPT) % HPC
|
||||||
|
s = int(lba % SPT) + 1
|
||||||
|
return (c, h, s)
|
||||||
|
|
||||||
|
def __chs_bytes(self, addr):
|
||||||
|
(c, h, s) = self.__chs(addr)
|
||||||
|
c_byte = c & 0xFF
|
||||||
|
s_byte = ((c >> 2) & 0xC0) + s
|
||||||
|
slog(DEBUG, "chs=(%d,%d,%d)" % (c, h, s))
|
||||||
|
slog(DEBUG, "chs-bytes=(%d,%d,%d)" % (c_byte, h, s_byte))
|
||||||
|
#return (0, 0, 0)
|
||||||
|
return (c_byte, h, s_byte)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(name='mem2disk', help='Create partitioned disk image from raw memory dump')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def align_to_block(cls, n):
|
||||||
|
if n % BPS == 0:
|
||||||
|
return n
|
||||||
|
return BPS * (int(n/BPS)+1)
|
||||||
|
|
||||||
|
# C in [0,1023], H in [0,255], S in [1,63]
|
||||||
|
# C/H/S
|
||||||
|
# 0/0/1 MBR
|
||||||
|
# 0/1/1 BR1
|
||||||
|
# 0/1/2 Data1
|
||||||
|
# X/0/1 BRN
|
||||||
|
# X/0/2 Data1
|
||||||
|
def align_to_cylinder(self, addr, reserve_boot_sector = True):
|
||||||
|
lowest = self.__lba(0, 1, 1) * BPS
|
||||||
|
if addr < lowest:
|
||||||
|
addr = lowest
|
||||||
|
elif addr != lowest:
|
||||||
|
(c, h, s) = self.__chs(addr)
|
||||||
|
lba = self.__lba(c, h, s) * BPS
|
||||||
|
if lba != addr:
|
||||||
|
assert lba < addr
|
||||||
|
addr = self.__lba(c + 1, 0, 1) * BPS
|
||||||
|
if reserve_boot_sector:
|
||||||
|
addr += BPS
|
||||||
|
return addr
|
||||||
|
|
||||||
|
def append_part(self, chunk, cur):
|
||||||
|
if cur is None:
|
||||||
|
cur = bytearray()
|
||||||
|
start_ = len(cur)
|
||||||
|
start = self.align_to_cylinder(start_, True)
|
||||||
|
cur.extend(bytearray(start - start_))
|
||||||
|
cur[start-BPS, start] = create_part_header(start, len(chunk))
|
||||||
|
cur.extend(chunk)
|
||||||
|
return cur
|
||||||
|
|
||||||
|
def create_part_table_entry(self, part):
|
||||||
|
slog(DEBUG, "--- creating table entry >{}<".format(part))
|
||||||
|
start_sec = int(part.addr / BPS + .5)
|
||||||
|
size_sec = int(part.aligned_size / BPS + .5)
|
||||||
|
assert start_sec == part.addr / BPS
|
||||||
|
slog(DEBUG, "size_sec = ", size_sec, "part.aligned_size = ", part.aligned_size, "part.aligned_size / BPS = ", part.aligned_size / BPS)
|
||||||
|
assert size_sec == part.aligned_size / BPS
|
||||||
|
chs_start = self.__chs_bytes(part.addr)
|
||||||
|
chs_end = self.__chs_bytes(part.addr + part.aligned_size - 1)
|
||||||
|
slog(DEBUG, "chs_start=", chs_start)
|
||||||
|
slog(DEBUG, "chs_end=", chs_end)
|
||||||
|
return struct.pack("<BBBBBBBBLL",
|
||||||
|
0x0, # not bootable
|
||||||
|
chs_start[1], chs_start[2], chs_start[0],
|
||||||
|
0x83, # fstype
|
||||||
|
chs_end[1], chs_end[2], chs_end[0],
|
||||||
|
start_sec, size_sec)
|
||||||
|
#1, 2)
|
||||||
|
|
||||||
|
def create_mbr(self, parts):
|
||||||
|
mbr = bytearray(446)
|
||||||
|
for i in range(0, 4):
|
||||||
|
mbr.extend(self.create_part_table_entry(parts[i]) if i < len(parts) else bytes(16))
|
||||||
|
for magic in [0x55, 0xaa]:
|
||||||
|
mbr.extend(struct.pack('<B', magic))
|
||||||
|
return mbr
|
||||||
|
|
||||||
|
def add_parser(self, parsers):
|
||||||
|
p = super().add_parser(parsers)
|
||||||
|
p.add_argument("--mtdparts", help="Partitioning definition of the memory conforming to mtdparts-syntax")
|
||||||
|
p.add_argument("--no-boot-sectors", help="Don't add (empty) boot sectors at the beginning of each partition",
|
||||||
|
action='store_true', default=False)
|
||||||
|
p.add_argument("--pad-byte", help="Byte value to pad empty areas with", type=int, default=0x0)
|
||||||
|
p.add_argument("input", help="Path to input memory chunk")
|
||||||
|
p.add_argument("output", help="Path to output disk image file")
|
||||||
|
|
||||||
|
def write(self, f, data):
|
||||||
|
n = len(data)
|
||||||
|
if not n:
|
||||||
|
return 0
|
||||||
|
left = n
|
||||||
|
while True:
|
||||||
|
rr = f.write(data)
|
||||||
|
if not rr:
|
||||||
|
raise Exception("Failed to write data to file")
|
||||||
|
left -= rr
|
||||||
|
assert left >= 0
|
||||||
|
if not left:
|
||||||
|
return n
|
||||||
|
|
||||||
|
def copy(self, fin, fout, n):
|
||||||
|
max_bufsize = 1024 * 1024
|
||||||
|
left = n
|
||||||
|
slog(DEBUG, "== copying ", n, "to output file offset", fout.tell())
|
||||||
|
while left:
|
||||||
|
slog(DEBUG, "left={}".format(left))
|
||||||
|
bufsize = left if left <= max_bufsize else max_bufsize
|
||||||
|
data = fin.read(bufsize)
|
||||||
|
if not data or not len(data):
|
||||||
|
raise Exception("Failed to read %d bytes from file" % bufsize)
|
||||||
|
#slog(DEBUG, " data = {}".format(data[0:15]))
|
||||||
|
self.write(fout, data)
|
||||||
|
left -= len(data)
|
||||||
|
assert left >= 0
|
||||||
|
return n
|
||||||
|
|
||||||
|
async def run(self, args):
|
||||||
|
|
||||||
|
def empty(size):
|
||||||
|
return bytearray([self.__to_int(args.pad_byte)]) * size
|
||||||
|
|
||||||
|
if args.mtdparts:
|
||||||
|
if args.input in [ '/dev/zero' ]:
|
||||||
|
total_size = None
|
||||||
|
else:
|
||||||
|
total_size = os.stat(args.input).st_size
|
||||||
|
chunks = self.__parse_mtdparts(args.mtdparts, total_size=total_size)
|
||||||
|
# else if args.xxx: possible other partition layout definition formats go here
|
||||||
|
else:
|
||||||
|
raise Exception("No memory layout definition given")
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
parts = []
|
||||||
|
for chunk in chunks:
|
||||||
|
part = self.Part(chunk)
|
||||||
|
part.addr = self.align_to_cylinder(pos, False)
|
||||||
|
parts.append(part)
|
||||||
|
pos = part.addr + part.aligned_size + BPS
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
with open(args.output, mode='wb') as fout:
|
||||||
|
with open(args.input, mode='rb') as fin:
|
||||||
|
pos += self.write(fout, self.create_mbr(parts))
|
||||||
|
for part in parts:
|
||||||
|
if pos < part.addr:
|
||||||
|
pos += self.write(fout, empty(part.addr - pos))
|
||||||
|
if not args.no_boot_sectors:
|
||||||
|
pos += self.write(fout, empty(BPS)) # empty boot sector
|
||||||
|
pos += self.copy(fin, fout, part.size)
|
||||||
|
pad = part.aligned_size - part.size
|
||||||
|
assert pad >= 0
|
||||||
|
if pad > 0:
|
||||||
|
pos += self.write(fout, empty(pad))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
exit(jwutils.run_sub_commands('File utilities') != 0)
|
||||||
10
scripts/jw-devtest.py
Normal file
10
scripts/jw-devtest.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import jwutils
|
||||||
|
from devtest.os import *
|
||||||
|
|
||||||
|
jwutils.log.set_filename_length(30)
|
||||||
|
|
||||||
|
if jwutils.run_sub_commands('Control Device under Test'):
|
||||||
|
exit(1)
|
||||||
|
exit(0)
|
||||||
4
src/Makefile
Normal file
4
src/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/dirs.mk
|
||||||
4
src/python/Makefile
Normal file
4
src/python/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-mods.mk
|
||||||
4
src/python/devtest/Makefile
Normal file
4
src/python/devtest/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ../../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-mod.mk
|
||||||
127
src/python/devtest/os/CmdTestOs.py
Normal file
127
src/python/devtest/os/CmdTestOs.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import jwutils
|
||||||
|
from jwutils.log import *
|
||||||
|
|
||||||
|
from .Machine import Machine
|
||||||
|
from .MachineCmd import MachineCmd
|
||||||
|
from .TestPhases import TestPhases
|
||||||
|
from .TestCases import TestCases
|
||||||
|
from .TestResults import TestResults
|
||||||
|
|
||||||
|
class CmdTestOs(MachineCmd): # export
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('test-os', help='Run batches of tests against a machine')
|
||||||
|
self.__results = TestResults()
|
||||||
|
|
||||||
|
# -- phase trigger methods
|
||||||
|
async def __trigger_boot(self, env, machine):
|
||||||
|
slog(INFO, "requesting power-on")
|
||||||
|
await machine.request_power_on(env)
|
||||||
|
|
||||||
|
async def __trigger_up(self, env, machine):
|
||||||
|
await machine.wait_up(env)
|
||||||
|
|
||||||
|
async def __trigger_shutdown(self, env, machine):
|
||||||
|
await machine.request_shutdown(env)
|
||||||
|
|
||||||
|
async def __trigger_post(self, env, machine):
|
||||||
|
await machine.wait_poweroff(env)
|
||||||
|
|
||||||
|
# -- phase filtered batches
|
||||||
|
async def __establish_connections(self, phase, env, machine):
|
||||||
|
for conn in env.connections:
|
||||||
|
if not phase in conn.info.phases:
|
||||||
|
if conn.instance is None:
|
||||||
|
continue
|
||||||
|
slog(INFO, 'Phase "{}": closing connection "{}" with phases [{}]'.format(phase.name, conn.info, conn.info.phases_str))
|
||||||
|
await machine.unregister_connection(conn.instance)
|
||||||
|
conn.instance = None
|
||||||
|
continue
|
||||||
|
slog(DEBUG, " phase matches")
|
||||||
|
if conn.instance is not None:
|
||||||
|
continue
|
||||||
|
slog(INFO, 'Phase "{}": opening connection "{}" with phases [{}]'.format(phase.name, conn.info, conn.info.phases_str))
|
||||||
|
conn.instance = machine.register_connection(env, conn.info)
|
||||||
|
|
||||||
|
async def __run_test_cases(self, phase, env, machine, cases):
|
||||||
|
connections = [ c.instance for c in env.connections if c.instance ]
|
||||||
|
for case in cases:
|
||||||
|
if not phase in case.phases:
|
||||||
|
continue
|
||||||
|
delim = '--- running test case "{}"'.format(case)
|
||||||
|
prio = NOTICE
|
||||||
|
prefix = ' {}|{}'.format(*console_color_chars(prio))
|
||||||
|
slog(prio, ',' + delim)
|
||||||
|
jwutils.log.append_to_prefix(prefix)
|
||||||
|
try:
|
||||||
|
if not machine.clear_for_tests():
|
||||||
|
raise Exception("machine is not clear for running tests")
|
||||||
|
rr = await case.run(env, machine, phase)
|
||||||
|
if rr:
|
||||||
|
slog(ERR, "FAIL: ", rr)
|
||||||
|
except Exception:
|
||||||
|
rr = sys.exc_info()[1]
|
||||||
|
slog(ERR, "FAIL: ", rr)
|
||||||
|
slog_m(ERR, traceback.format_exc())
|
||||||
|
self.__results.add(case, phase, rr)
|
||||||
|
jwutils.log.remove_from_prefix(len(prefix))
|
||||||
|
slog(prio, '`' + delim)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_trigger(cls, phase):
|
||||||
|
return getattr(cls, '_{}__trigger_{}'.format(cls.__name__, phase.name.lower()), None)
|
||||||
|
|
||||||
|
# override MachineCmd._run()
|
||||||
|
async def _run(self, env):
|
||||||
|
machine = None
|
||||||
|
try:
|
||||||
|
machine = await Machine.create(env)
|
||||||
|
test_cases = TestCases(env.args.test_case_path, dummies=env.args.dummy_tests)
|
||||||
|
for phase in TestPhases.Phase:
|
||||||
|
if not machine.clear_for_tests():
|
||||||
|
raise Exception("machine is not clear for running tests")
|
||||||
|
delim = '-' * 60 + " Phase {} -- ".format(phase.name)
|
||||||
|
prio = NOTICE
|
||||||
|
prefix = ' {}|{}'.format(*console_color_chars(prio))
|
||||||
|
slog(prio, ',' + delim + '>')
|
||||||
|
jwutils.log.append_to_prefix(prefix)
|
||||||
|
try:
|
||||||
|
trigger_method = self.__get_trigger(phase)
|
||||||
|
if trigger_method is not None:
|
||||||
|
await trigger_method(phase, env, machine)
|
||||||
|
await self.__establish_connections(phase, env, machine)
|
||||||
|
await self.__run_test_cases(phase, env, machine, test_cases)
|
||||||
|
finally:
|
||||||
|
jwutils.log.remove_from_prefix(len(prefix))
|
||||||
|
slog(prio, '`' + delim + '<')
|
||||||
|
except Exception as e:
|
||||||
|
slog(ERR, "failed to run tests against machine {}: {}".format(machine, e))
|
||||||
|
slog_m(ERR, traceback.format_exc())
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if machine is not None:
|
||||||
|
await machine.request_shutdown(env)
|
||||||
|
await machine.wait_poweroff(env)
|
||||||
|
await machine.cleanup(env)
|
||||||
|
for conn in env.connections:
|
||||||
|
if conn.instance:
|
||||||
|
await machine.unregister_connection(conn.instance)
|
||||||
|
conn.instance = None
|
||||||
|
del machine
|
||||||
|
self.__results.dump(env.args)
|
||||||
|
n_failed = self.__results.n_failed()
|
||||||
|
if (n_failed):
|
||||||
|
slog(ERR, "%d test%s failed" % (n_failed, "" if n_failed == 1 else "s"))
|
||||||
|
return -1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def add_parser(self, parsers):
|
||||||
|
p = super().add_parser(parsers)
|
||||||
|
self.__results.add_arguments(p)
|
||||||
|
return p
|
||||||
136
src/python/devtest/os/Connection.py
Normal file
136
src/python/devtest/os/Connection.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import Enum, IntFlag
|
||||||
|
import asyncio
|
||||||
|
import types
|
||||||
|
from jwutils.log import *
|
||||||
|
from .TestPhases import TestPhases
|
||||||
|
|
||||||
|
# TODO: Make this a valid abstract python stream
|
||||||
|
class Connection(ABC): # export
|
||||||
|
|
||||||
|
class Proto(Enum):
|
||||||
|
Serial = 1
|
||||||
|
Console = 2
|
||||||
|
CAN = 3
|
||||||
|
HTTP = 4
|
||||||
|
Fifos = 5
|
||||||
|
|
||||||
|
class Flags(IntFlag):
|
||||||
|
FailOnTimeout = 0x01
|
||||||
|
ReadStripNewline = 0x02
|
||||||
|
ReadDecodeToString = 0x04
|
||||||
|
|
||||||
|
class Info:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_attr_proto(rhs):
|
||||||
|
if not rhs in Connection.Proto._member_names_:
|
||||||
|
raise Exception("Unknown connection protocol >" + rhs + "<")
|
||||||
|
return Connection.Proto[rhs]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_attr_phases(rhs):
|
||||||
|
return TestPhases.parse(rhs)
|
||||||
|
|
||||||
|
def __getitem__(self, key_):
|
||||||
|
key, default = (key_[0], key_[1]) if isinstance(key_, tuple) else (key_, None)
|
||||||
|
if key in self.__spec.keys():
|
||||||
|
return self.__spec[key]
|
||||||
|
return default
|
||||||
|
|
||||||
|
#p.add_argument("-C", "--connection", help="Connection specification of the form 'proto=console,phase=pre|up-shutdown'", action='append', default=[])
|
||||||
|
def __init(self, spec):
|
||||||
|
self.spec = spec
|
||||||
|
self.phases = set()
|
||||||
|
self.proto = None
|
||||||
|
tokens = spec.split(',')
|
||||||
|
if not len(tokens):
|
||||||
|
raise Exeption("Empty connection spec")
|
||||||
|
if tokens[0].find("=") == -1:
|
||||||
|
tokens[0] = 'proto=' + tokens[0]
|
||||||
|
self.__spec = {}
|
||||||
|
for tok in tokens:
|
||||||
|
assignment = tok.split('=')
|
||||||
|
lhs = assignment[0]
|
||||||
|
rhs = assignment[1] if len(assignment) > 1 else True
|
||||||
|
func = getattr(Connection.Info, '_parse_attr_' + lhs, None)
|
||||||
|
if not isinstance(func, types.FunctionType):
|
||||||
|
#setattr(self, lhs, rhs) # FIXME: This is overkill
|
||||||
|
self.__spec[lhs] = rhs
|
||||||
|
else:
|
||||||
|
setattr(self, lhs, func(rhs))
|
||||||
|
self.__spec[lhs] = func(rhs)
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.__init(spec)
|
||||||
|
|
||||||
|
def __format__(self, fmt):
|
||||||
|
return self.spec
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_spec(spec):
|
||||||
|
return ConInfo(spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def phases_names(self):
|
||||||
|
return [p.name for p in self.phases]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def phases_str(self):
|
||||||
|
return ', '.join(self.phases_names)
|
||||||
|
|
||||||
|
def __init__(self, env, spec):
|
||||||
|
self.env = env
|
||||||
|
if isinstance(spec, self.Info):
|
||||||
|
self.__info = spec
|
||||||
|
elif isinstance(spec, str):
|
||||||
|
self.__info = Connection.Info(spec)
|
||||||
|
else:
|
||||||
|
raise TypeError('Invalid type "{}" of spec "{}", should be string.'.format(type(spec), spec))
|
||||||
|
|
||||||
|
def __format__(self, fmt):
|
||||||
|
return self.__info.spec
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _read(self, act_timeout, flags):
|
||||||
|
return await self._read(act_timeout, flags)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _readline(self, timeout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _write(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self):
|
||||||
|
return self.__info
|
||||||
|
@info.setter
|
||||||
|
def info(self, value):
|
||||||
|
raise Exception("Tried to set immutable attribute info")
|
||||||
|
|
||||||
|
async def read(self, act_timeout=None, flags=Flags.ReadDecodeToString):
|
||||||
|
r = await self._read(act_timeout, flags)
|
||||||
|
if r is not None:
|
||||||
|
if flags & self.Flags.ReadDecodeToString:
|
||||||
|
r = r.decode('utf-8')
|
||||||
|
if flags & self.Flags.ReadStripNewline:
|
||||||
|
if len(r) and r[-1] == '\n':
|
||||||
|
r = r[0:-1]
|
||||||
|
return r
|
||||||
|
|
||||||
|
async def readline(self, timeout=None):
|
||||||
|
return await self._readline(timeout)
|
||||||
|
|
||||||
|
async def write(self, data):
|
||||||
|
return await self._write(data)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
return await self._close()
|
||||||
43
src/python/devtest/os/Connections.py
Normal file
43
src/python/devtest/os/Connections.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .Connection import Connection
|
||||||
|
|
||||||
|
class Connections: # export
|
||||||
|
|
||||||
|
class Instance:
|
||||||
|
def __init__(self, info, conn = None):
|
||||||
|
self.info = info
|
||||||
|
self.instance = conn
|
||||||
|
|
||||||
|
def __init__(self, rhs=None):
|
||||||
|
if rhs:
|
||||||
|
self.__contents = rhs.__contents
|
||||||
|
else:
|
||||||
|
self.__contents = []
|
||||||
|
self.__idx = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
self.__idx += 1
|
||||||
|
try:
|
||||||
|
return self.__contents[self.__idx-1]
|
||||||
|
except IndexError:
|
||||||
|
self.__idx = 0
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.__contents)
|
||||||
|
|
||||||
|
def append(self, stuff):
|
||||||
|
if isinstance(stuff, Connection.Info):
|
||||||
|
self.__contents.append(Connections.Instance(stuff, None))
|
||||||
|
elif isinstance(stuff, Connection):
|
||||||
|
self.__contents.append(Connections.Instance(stuff.info, stuff))
|
||||||
|
elif isinstance(stuff, Connections.Instance):
|
||||||
|
self.__contents.append(stuff)
|
||||||
|
elif isinstance(stuff, str):
|
||||||
|
self.__contents.append(Connections.Instance(Connection.Info(stuff), None))
|
||||||
|
else:
|
||||||
|
raise Exception("tried to append connection of incompatible type ", stuff)
|
||||||
18
src/python/devtest/os/Environment.py
Normal file
18
src/python/devtest/os/Environment.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .Connections import Connections
|
||||||
|
from jwutils import Options
|
||||||
|
|
||||||
|
class Environment:
|
||||||
|
|
||||||
|
def __init__(self, cmd, args):
|
||||||
|
self.args = args
|
||||||
|
self.cmd = cmd
|
||||||
|
self.connections = Connections()
|
||||||
|
self.features = Options(args.features)
|
||||||
|
for spec in args.connection:
|
||||||
|
self.connections.append(spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eloop(self):
|
||||||
|
return self.cmd.cmds.eloop
|
||||||
129
src/python/devtest/os/Machine.py
Normal file
129
src/python/devtest/os/Machine.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import Enum
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from jwutils.log import *
|
||||||
|
from jwutils.misc import *
|
||||||
|
|
||||||
|
from .Connection import Connection
|
||||||
|
from .conn.Fifos import Fifos as ConnFifos
|
||||||
|
|
||||||
|
class Machine(ABC): # export
|
||||||
|
|
||||||
|
def __init__(self, env):
|
||||||
|
# -- readonly stuff
|
||||||
|
self.__env = env
|
||||||
|
self.__name = env.args.platform + '@' + env.args.backend
|
||||||
|
self.__exe_path = None
|
||||||
|
self.__tmpdir = None
|
||||||
|
self.__connections = {}
|
||||||
|
|
||||||
|
def __format__(self, fmt):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
def _register_connection(self, conn):
|
||||||
|
if conn.info.spec in self.__connections.keys():
|
||||||
|
raise Exception("Connection {} is already up".format(conn))
|
||||||
|
self.__connections[conn.info.spec] = conn
|
||||||
|
|
||||||
|
async def _unregister_connection(self, conn):
|
||||||
|
spec = conn.info.spec
|
||||||
|
if spec in self.__connections.keys():
|
||||||
|
del self.__connections[conn.info.spec]
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, env):
|
||||||
|
backend = env.args.backend
|
||||||
|
if backend.find('.') == -1:
|
||||||
|
backend = 'devtest.os.be.' + backend
|
||||||
|
slog(NOTICE, "loading machine ", backend)
|
||||||
|
mod = importlib.import_module(backend)
|
||||||
|
r = mod.Machine(env)
|
||||||
|
await r.init()
|
||||||
|
return r
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.__name
|
||||||
|
@name.setter
|
||||||
|
def name(self, value):
|
||||||
|
self.__name = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def env(self):
|
||||||
|
return self.__env
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exe_path(self):
|
||||||
|
if not self.__exe_path:
|
||||||
|
self.__exe_path = os.path.abspath(sys.argv[0])
|
||||||
|
return self.__exe_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exe_basename(self):
|
||||||
|
return re.sub('\.py$', '', os.path.basename(self.exe_path))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tmpdir(self):
|
||||||
|
if self.env.args.persistent_tmpdir:
|
||||||
|
if not self.__tmpdir:
|
||||||
|
self.__tmpdir = self.env.args.persistent_tmpdir
|
||||||
|
shutil.rmtree(self.__tmpdir, ignore_errors=True)
|
||||||
|
os.mkdir(self.__tmpdir)
|
||||||
|
return self.__tmpdir
|
||||||
|
if not self.__tmpdir:
|
||||||
|
self.__tmpdir = tempfile.TemporaryDirectory(prefix=self.exe_basename+'-')
|
||||||
|
return self.__tmpdir.name
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cleanup(self, env):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def clear_for_tests(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def register_connection(self, env, info):
|
||||||
|
slog(NOTICE, 'opening connection "{}" to platform "{}"'.format(info, self.env.args.platform))
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module('devtest.os.conn.' + info.proto.name) # untested
|
||||||
|
classes = get_derived_classes(mod, Connection)
|
||||||
|
if not len(classes):
|
||||||
|
raise Exception("Tried to establish connection {} of unsupported type {}".format(info, info.proto))
|
||||||
|
slog(DEBUG, 'constructing connection with info "{}"'.format(info))
|
||||||
|
r = classes[0](env, info)
|
||||||
|
slog(DEBUG, "found connection definition class >%s<" % type(r))
|
||||||
|
self._register_connection(r)
|
||||||
|
except Exception as e:
|
||||||
|
slog(ERR, "failed to open connection {}: {}".format(info, e))
|
||||||
|
raise
|
||||||
|
return r
|
||||||
|
|
||||||
|
async def unregister_connection(self, conn):
|
||||||
|
await self._unregister_connection(conn)
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def request_power_on(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def wait_up(self, timeout):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def request_shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def wait_poweroff(self, timeout):
|
||||||
|
return True
|
||||||
44
src/python/devtest/os/MachineCmd.py
Normal file
44
src/python/devtest/os/MachineCmd.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
import jwutils
|
||||||
|
from jwutils.log import *
|
||||||
|
from .Environment import Environment
|
||||||
|
|
||||||
|
class MachineCmd(jwutils.Cmd): # export
|
||||||
|
|
||||||
|
def __init__(self, name, help):
|
||||||
|
super().__init__(name, help=help)
|
||||||
|
|
||||||
|
def add_parser(self, parsers):
|
||||||
|
p = super().add_parser(parsers)
|
||||||
|
p.add_argument("-B", "--backend", help="Machine backend", default='qemu')
|
||||||
|
p.add_argument("-P", "--platform", help="Target platform type", required=True)
|
||||||
|
p.add_argument("-C", "--connection", help="Connection specification of the form 'proto=console,phase=pre|up-shutdown'", action='append', default=[])
|
||||||
|
p.add_argument("-T", "--test-case-path", help="Test case search path, directories separated by colons", required=True)
|
||||||
|
p.add_argument( "--shutdown-timeout", help="Timeout for a machine to complete an ACPI shutdown request in ms", default=60000)
|
||||||
|
p.add_argument( "--poweroff-timeout", help="Timeout for a machine to complete a poweroff request in ms", default=100)
|
||||||
|
p.add_argument( "--features", help="Comma-separated list of features that are passed to the test cases for interpretation", default='')
|
||||||
|
|
||||||
|
# TODO: Don't pollute list of generic machine options with Qemu specific stuff
|
||||||
|
p.add_argument("-R", "--root-part-number", help="Number of root partition (starting from 1)")
|
||||||
|
p.add_argument("--medium", help="Boot medium (file system path or \"net\")")
|
||||||
|
|
||||||
|
p.add_argument( "--persistent-tmpdir", help="Temporary directory for storing runtime data and scripts, by default a random one is created", default=None)
|
||||||
|
p.add_argument( "--dummy-tests", help="Add hard-coded dummy test cases to the test run", action='store_true', default=False)
|
||||||
|
|
||||||
|
# TODO: Don't pollute list of generic machine options with Qemu specific stuff
|
||||||
|
# TODO: Remove short options, currently only there for qemu.sh compatibility
|
||||||
|
p.add_argument("-S", "--qe-network-script", help="Script to be called for network bringup / teardown, builtin version is used by default", default=None)
|
||||||
|
p.add_argument("-k", "--qe-kernel", help="Kernel for Qemu to use, none by default", default=None)
|
||||||
|
p.add_argument("-D", "--qe-dtb", help="Device-tree blob for the kernel to use, requires --qe-kernel, none by default", default=None)
|
||||||
|
p.add_argument("-o", "--qe-opts", help="Additional arguments to Qemu", default=None)
|
||||||
|
return p
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _run(self, env):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def run(self, args):
|
||||||
|
env = Environment(self, args)
|
||||||
|
return await self._run(env)
|
||||||
4
src/python/devtest/os/Makefile
Normal file
4
src/python/devtest/os/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ../../../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-mod.mk
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue