mirror of
ssh://devgit.janware.com/janware/proj/jw-devtest
synced 2026-10-30 00:37:34 +01:00
First commit
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
commit
4b912741cb
63 changed files with 3753 additions and 0 deletions
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
|
||||
16
src/python/devtest/os/TcDummy.py
Normal file
16
src/python/devtest/os/TcDummy.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from jwutils.log import *
|
||||
from .TestCase import TestCase
|
||||
|
||||
class TcDummy(TestCase):
|
||||
|
||||
@property
|
||||
def is_dummy(self):
|
||||
return True
|
||||
|
||||
async def _run(self, env, machine, phase):
|
||||
slog(NOTICE, "running dummy test case in phase >%s<" % phase.name)
|
||||
#return None
|
||||
# return an error message string if this test fails
|
||||
return "Stumbled over chair"
|
||||
49
src/python/devtest/os/TestCase.py
Normal file
49
src/python/devtest/os/TestCase.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from abc import abstractmethod, ABC
|
||||
from jwutils.misc import *
|
||||
from .TestPhases import TestPhases
|
||||
|
||||
# could be derived from Python unittest.TestCase, I currently don't see the
|
||||
# benefit, though
|
||||
class TestCase: # export
|
||||
|
||||
def __init__(self):
|
||||
self.__name = object_builtin_name(self)
|
||||
self.__factory = None
|
||||
self.__phases = { TestPhases.Phase.Up }
|
||||
|
||||
def __format__(self, fmt):
|
||||
return self.name
|
||||
|
||||
@abstractmethod
|
||||
async def _run(self, env, machine, phase):
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__name
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
self.__name = value
|
||||
|
||||
@property
|
||||
def factory(self):
|
||||
return self.__factory
|
||||
@factory.setter
|
||||
def factory(self, value):
|
||||
self.__factory = value
|
||||
|
||||
@property
|
||||
def phases(self):
|
||||
return self.__phases
|
||||
@phases.setter
|
||||
def phases(self, value):
|
||||
self.__phases = value
|
||||
|
||||
@property
|
||||
def is_dummy(self):
|
||||
return False
|
||||
|
||||
async def run(self, env, machine, phase):
|
||||
return await self._run(env, machine, phase)
|
||||
12
src/python/devtest/os/TestCaseFactory.py
Normal file
12
src/python/devtest/os/TestCaseFactory.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
class TestCaseFactory(ABC): # export
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
@abstractmethod
|
||||
def test_cases(self):
|
||||
pass
|
||||
60
src/python/devtest/os/TestCases.py
Normal file
60
src/python/devtest/os/TestCases.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from abc import abstractmethod, ABC
|
||||
import sys
|
||||
import re
|
||||
import inspect
|
||||
from jwutils.log import *
|
||||
from jwutils.misc import *
|
||||
from .TestCase import TestCase
|
||||
from .TestCaseFactory import TestCaseFactory
|
||||
from .tcf import *
|
||||
|
||||
class TestCases: # export
|
||||
|
||||
def __init__(self, path, dummies=False):
|
||||
|
||||
self.__path = path.split(':')
|
||||
self.__dummies = dummies
|
||||
self.__idx = 0
|
||||
self.__cases = []
|
||||
factories = set()
|
||||
# use list() to avoid expansion of sys.modules during iteration
|
||||
for name, mod in list(sys.modules.items()):
|
||||
members = inspect.getmembers(mod, inspect.isclass)
|
||||
for name, c in members:
|
||||
if inspect.isabstract(c):
|
||||
continue
|
||||
if TestCaseFactory in inspect.getmro(c):
|
||||
factories.add(c)
|
||||
continue
|
||||
slog(NOTICE, "using test case factories:", ', '.join([f.__name__ for f in factories]))
|
||||
for factory in factories:
|
||||
slog(INFO, "checking test case factory", str(factory))
|
||||
cases = factory(self.__path).test_cases()
|
||||
for c in cases:
|
||||
if c.is_dummy and not self.__dummies:
|
||||
continue
|
||||
c.factory = factory
|
||||
self.__cases.append(c)
|
||||
|
||||
# -- fancy log found test cases
|
||||
delim = "-" * 60 + " found test cases"
|
||||
fmt = "| {:>30} | {:<10} | {}"
|
||||
slog(NOTICE, "," + delim + " >")
|
||||
slog(NOTICE, fmt.format('Factory', 'Phases', 'Test Case'))
|
||||
slog(NOTICE, "+" + re.sub('[^-]', '-', delim) + "--")
|
||||
for c in self.__cases:
|
||||
slog(NOTICE, fmt.format(c.factory.__name__, ', '.join([p.name for p in c.phases]), c.name))
|
||||
slog(NOTICE, "`" + delim + " <")
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
self.__idx += 1
|
||||
try:
|
||||
return self.__cases[self.__idx-1]
|
||||
except IndexError:
|
||||
self.__idx = 0
|
||||
raise StopIteration # Done iterating.
|
||||
39
src/python/devtest/os/TestPhases.py
Normal file
39
src/python/devtest/os/TestPhases.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import IntEnum
|
||||
import itertools
|
||||
|
||||
class TestPhases: # export
|
||||
|
||||
class Phase(IntEnum):
|
||||
Pre = 1
|
||||
Boot = 2
|
||||
Up = 3
|
||||
Shutdown = 4
|
||||
Post = 5
|
||||
|
||||
@staticmethod
|
||||
def __parse(st):
|
||||
return TestPhases.Phase[st.capitalize()]
|
||||
|
||||
@staticmethod
|
||||
def parse(spec):
|
||||
r = set()
|
||||
terms = spec.split('|')
|
||||
for t in terms:
|
||||
interval = t.split('-')
|
||||
if len(interval) == 1:
|
||||
r.add(TestPhases.__parse(interval[0]))
|
||||
continue
|
||||
for p in itertools.islice(
|
||||
TestPhases.Phase,
|
||||
TestPhases.__parse(interval[0])-1,
|
||||
TestPhases.__parse(interval[1])
|
||||
):
|
||||
r.add(p)
|
||||
return r
|
||||
|
||||
if __name__ == '__main__':
|
||||
for p in [ 'Pre-Post', 'Pre', 'Boot', 'Shutdown-Post', 'Blah' ]:
|
||||
print("{} = {}".format(p, TestPhases.parse(p)))
|
||||
143
src/python/devtest/os/TestResults.py
Normal file
143
src/python/devtest/os/TestResults.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import IntEnum
|
||||
from termcolor import colored
|
||||
from texttable import Texttable
|
||||
from jwutils.log import *
|
||||
|
||||
class TestResults: # export
|
||||
|
||||
class Field(IntEnum):
|
||||
Case = 0
|
||||
Phase = 1
|
||||
Result = 2
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.__deco = {
|
||||
'border': Texttable.BORDER,
|
||||
'header': Texttable.HEADER,
|
||||
'hlines': Texttable.HLINES,
|
||||
'vlines': Texttable.VLINES
|
||||
}
|
||||
|
||||
self.__cols = {
|
||||
'Case': {
|
||||
'bits': set(['case']),
|
||||
'fmt': self.__fmt_case
|
||||
},
|
||||
'Phase': {
|
||||
'bits': set(['phase']),
|
||||
'fmt': self.__fmt_phase
|
||||
},
|
||||
'Result': {
|
||||
'bits': set(['result']),
|
||||
'fmt': self.__fmt_result
|
||||
},
|
||||
'Message': {
|
||||
'bits': set(['msg']),
|
||||
'fmt': self.__fmt_msg
|
||||
},
|
||||
}
|
||||
|
||||
self.__col_aliases = {
|
||||
'all': [
|
||||
'Case',
|
||||
'Phase',
|
||||
'Result',
|
||||
'Message',
|
||||
]
|
||||
}
|
||||
self.__records = []
|
||||
|
||||
def ___cols(self, args):
|
||||
cols = args.result_cols
|
||||
if cols in self.__col_aliases:
|
||||
return self.__col_aliases[cols]
|
||||
return [c.strip(' ') for c in cols.split(',')]
|
||||
|
||||
def ___records(self, args):
|
||||
# hook to apply filters defined in args
|
||||
return self.__records
|
||||
|
||||
def __fmt_case(self, record):
|
||||
return record[TestResults.Field.Case].name
|
||||
|
||||
def __fmt_phase(self, record):
|
||||
return record[TestResults.Field.Phase].name
|
||||
|
||||
def __fmt_result(self, record):
|
||||
rr = record[TestResults.Field.Result]
|
||||
if rr is None:
|
||||
prio = NOTICE
|
||||
color = 'green'
|
||||
r = "PASS"
|
||||
else:
|
||||
prio = ERR
|
||||
color = 'red'
|
||||
r = "FAIL"
|
||||
return r
|
||||
# currently disabled: Texttable doesn't support colored cell contents
|
||||
#c_on, c_off = console_color_chars(prio)
|
||||
#return c_on + r + c_off
|
||||
#return colored(r, color)
|
||||
|
||||
def __fmt_msg(self, record):
|
||||
rr = record[TestResults.Field.Result]
|
||||
if rr is None:
|
||||
return ""
|
||||
return rr
|
||||
|
||||
def add_arguments(self, p):
|
||||
p.add_argument("--result-cols", help="Comma-separated list of output columns, " +
|
||||
"possible columns are: " + ', '.join(self.__cols.keys()), default="all")
|
||||
p.add_argument("--result-deco", help="Comma-separated list of output table decoration flags, " +
|
||||
"possible values are: " + ', '.join(self.__deco.keys()), default="all")
|
||||
return p
|
||||
|
||||
def add(self, case, phase, result):
|
||||
self.__records.append([case, phase, result])
|
||||
|
||||
def n_failed(self):
|
||||
return len(list(filter(None, [rec[TestResults.Field.Result] for rec in self.__records])))
|
||||
|
||||
def dump(self, args):
|
||||
|
||||
t = Texttable(max_width=160)
|
||||
|
||||
# -- deco
|
||||
deco_keys = args.result_deco
|
||||
if deco_keys == 'all':
|
||||
deco_keys = self.__deco.keys()
|
||||
else:
|
||||
deco_keys = filter(None, args.deco.split(','))
|
||||
deco = 0x0
|
||||
for d in deco_keys:
|
||||
deco |= self.__deco[d]
|
||||
t.set_deco(deco)
|
||||
|
||||
cols = self.___cols(args)
|
||||
#slog(DEBUG, 'Columns: ' + ' '.join(cols))
|
||||
records = self.___records(args)
|
||||
bits = set()
|
||||
for c in cols:
|
||||
bits |= self.__cols[c]['bits']
|
||||
#info = dict()
|
||||
#out = dict()
|
||||
|
||||
rows = []
|
||||
row = []
|
||||
if 'header' in deco_keys:
|
||||
for c in cols:
|
||||
row.append(c)
|
||||
rows.append(row)
|
||||
#slog(DEBUG, 'Header: ' + ' '.join(row))
|
||||
for record in records:
|
||||
row = []
|
||||
for c in cols:
|
||||
row.append(self.__cols[c]['fmt'](record))
|
||||
#slog(DEBUG, 'case-row: ' + ' '.join(row))
|
||||
rows.append(row)
|
||||
#slog(DEBUG, rows)
|
||||
t.add_rows(rows)
|
||||
print(t.draw())
|
||||
4
src/python/devtest/os/be/Makefile
Normal file
4
src/python/devtest/os/be/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
TOPDIR = ../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-mod.mk
|
||||
235
src/python/devtest/os/be/qemu/Invocation.py
Normal file
235
src/python/devtest/os/be/qemu/Invocation.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
import shutil
|
||||
from jwutils.log import *
|
||||
from ...Connection import Connection
|
||||
|
||||
class Invocation(ABC):
|
||||
|
||||
# -- private stuff
|
||||
|
||||
def __init__(self, machine):
|
||||
self.__cl = None
|
||||
self.__cf = ""
|
||||
self.__mach = machine
|
||||
|
||||
@property
|
||||
def mach(self):
|
||||
return self.__mach
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
return self.__mach.env
|
||||
|
||||
def __val(self, func_name, defval=None):
|
||||
func_name = '_impl_' + func_name
|
||||
method = getattr(self, func_name, None)
|
||||
if not method:
|
||||
if defval is not None:
|
||||
return defval
|
||||
raise Exception("tried to invoke unknown method >%s<" % func_name)
|
||||
return method()
|
||||
|
||||
def __do_add_cl(self, stuff):
|
||||
if isinstance(stuff, list):
|
||||
self.__cl.extend(stuff)
|
||||
else:
|
||||
self.__cl.append(stuff)
|
||||
|
||||
def __add_cl(self, func_name, opt_name=None):
|
||||
val = self.__val(func_name)
|
||||
if not val:
|
||||
return
|
||||
arr = []
|
||||
if opt_name:
|
||||
arr.append(opt_name)
|
||||
if isinstance(val, list):
|
||||
arr.extend(val)
|
||||
else:
|
||||
arr.append(val)
|
||||
self.__do_add_cl(arr)
|
||||
|
||||
def __add_cf(self, func_name, key):
|
||||
val = self.__val(func_name)
|
||||
if not val:
|
||||
return
|
||||
self.__cf += '{}="{}"\n'.format(key, val)
|
||||
|
||||
# -- protected API methods
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_brctl_exe(self):
|
||||
r = shutil.which("brctl", path="/bin:/usr/bin:/sbin:/usr/sbin")
|
||||
if r is None:
|
||||
raise Exception("no brctl executable found")
|
||||
return r
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_bridge_name(self):
|
||||
return "brdev0"
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_bridge_net(self):
|
||||
return "192.168.100.0/24"
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_bridge_addr(self):
|
||||
return "192.168.100.1"
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_mac_addr(self):
|
||||
return "00:0B:DC:9B:D6:DA"
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_nic_model(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _impl_qemu_exe(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _impl_machine(self):
|
||||
pass
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_memory(self):
|
||||
return "1G"
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_opts_interactive(self):
|
||||
console = False
|
||||
r = []
|
||||
for conn in self.env.connections:
|
||||
if conn.info.proto == Connection.Proto.Console:
|
||||
slog(INFO, "found console: {}".format(conn.info))
|
||||
#r.extend(['-serial pipe:/tmp/qemu-{}-console'.format(self.env.args.platform), '-display none'])
|
||||
r.extend([
|
||||
'-chardev', 'pipe,id=cpipe,path={},delay=off,wait=off,mux=off'.format(self.mach.fifo_base("console")),
|
||||
'-serial', 'chardev:cpipe',
|
||||
'-display', 'none'
|
||||
])
|
||||
console = True
|
||||
r.extend([
|
||||
'-chardev', 'pipe,id=cmon,path={},delay=off,wait=off'.format(self.mach.fifo_base("monitor")),
|
||||
'-mon', 'chardev=cmon,mode=readline',
|
||||
#'-mon', 'chardev=cmon,mode=control,pretty=off',
|
||||
#'-monitor', 'stdio',
|
||||
'-S', # start with CPU stopped, need "cont" command on monitor to start
|
||||
])
|
||||
if not console:
|
||||
r.extend(["-nographic"])
|
||||
return r
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_kernel(self):
|
||||
if self.env.args.qe_kernel:
|
||||
return self.env.args.qe_kernel
|
||||
return None
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_qemu_opts(self):
|
||||
if self.env.args.qe_opts:
|
||||
slog(NOTICE, "qemu_opts=>" + self.env.args.qe_opts + "<")
|
||||
#os.exit(1)
|
||||
return self.env.args.qe_opts.split()
|
||||
return None
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_dtb(self):
|
||||
if self.env.args.qe_dtb:
|
||||
return self.env.args.qe_dtb
|
||||
return None
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_append(self):
|
||||
pass
|
||||
|
||||
def _impl_append_wrapper(self):
|
||||
r = self._impl_append()
|
||||
ct = "devtest.qemu=1"
|
||||
if not r or not len(r):
|
||||
return r
|
||||
if isinstance(r, list):
|
||||
r.append(ct)
|
||||
return r
|
||||
return r + ' ' + ct
|
||||
|
||||
##@abstractmethod
|
||||
def _impl_audio_dev(self):
|
||||
return "none,id=blah"
|
||||
|
||||
#@abstractmethod
|
||||
def _impl_disks(self):
|
||||
pass
|
||||
|
||||
# -- public API
|
||||
|
||||
def typename(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@classmethod
|
||||
def create(cls, platform):
|
||||
mod_name = "Invocation_" + platform
|
||||
mod = importlib.import_module("devtest.os.be.qemu." + mod_name)
|
||||
|
||||
@property
|
||||
def cmdline(self):
|
||||
if self.__cl is None:
|
||||
self.__cl = []
|
||||
self.__do_add_cl([shutil.which("sudo"), "-E"])
|
||||
self.__add_cl('qemu_exe')
|
||||
self.__add_cl('opts_interactive')
|
||||
self.__add_cl('machine', '-M')
|
||||
self.__add_cl('kernel', '-kernel')
|
||||
self.__add_cl('append_wrapper', '-append')
|
||||
self.__add_cl('dtb', '-dtb')
|
||||
self.__add_cl('memory', '-m')
|
||||
self.__add_cl('audio_dev', '-audiodev')
|
||||
self.__do_add_cl(['-rtc', 'base=utc'])
|
||||
self.__add_cl('disks')
|
||||
nic="nic"
|
||||
val = self.__val('nic_model', '')
|
||||
if val is not None and len(val):
|
||||
nic += ',model=' + val
|
||||
val = self.__val('mac_addr', '')
|
||||
if val is not None and len(val):
|
||||
nic += ',macaddr=' + val
|
||||
self.__do_add_cl(["-net", nic, "-net", "tap,id=blah0,ifname=tap11"
|
||||
+ ",script=" + self.__mach.net_helper_path(True)
|
||||
+ ",downscript=" + self.__mach.net_helper_path(False)
|
||||
])
|
||||
self.__add_cl('qemu_opts')
|
||||
|
||||
return self.__cl
|
||||
|
||||
@property
|
||||
def helper_config(self):
|
||||
if not self.__cf:
|
||||
keys = [
|
||||
"append",
|
||||
#"base",
|
||||
"brctl_exe",
|
||||
"bridge_addr",
|
||||
"bridge_name",
|
||||
"bridge_net",
|
||||
#"dns_domain",
|
||||
#"hostname",
|
||||
#"short_hostname",
|
||||
"kernel",
|
||||
"dtb",
|
||||
"mac_addr",
|
||||
"machine",
|
||||
#"nic_model",
|
||||
"qemu_exe",
|
||||
"qemu_opts",
|
||||
#"root_dev",
|
||||
#"root_part_number",
|
||||
#"dev_platform"
|
||||
]
|
||||
for k in keys:
|
||||
self.__add_cf(k, k)
|
||||
return self.__cf
|
||||
23
src/python/devtest/os/be/qemu/Invocation_q3beige.py
Normal file
23
src/python/devtest/os/be/qemu/Invocation_q3beige.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from abc import abstractmethod
|
||||
import shutil
|
||||
from .Invocation import Invocation
|
||||
|
||||
class Invocation_q3beige(Invocation): # export
|
||||
|
||||
def __init__(self, machine):
|
||||
super().__init__(machine)
|
||||
self.__qemu_exe = shutil.which("qemu-system-ppc")
|
||||
|
||||
def _impl_qemu_exe(self):
|
||||
return self.__qemu_exe
|
||||
|
||||
def _impl_machine(self):
|
||||
return "g3beige"
|
||||
|
||||
def _impl_append(self):
|
||||
return "console=ttyS0 root=/dev/hda2"
|
||||
|
||||
def _impl_disks(self):
|
||||
return ["-hda", self.mach.env.args.medium]
|
||||
23
src/python/devtest/os/be/qemu/Invocation_sabrelite.py
Normal file
23
src/python/devtest/os/be/qemu/Invocation_sabrelite.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from abc import abstractmethod
|
||||
import shutil
|
||||
from .Invocation import Invocation
|
||||
|
||||
class Invocation_sabrelite(Invocation): # export
|
||||
|
||||
def __init__(self, machine):
|
||||
super().__init__(machine)
|
||||
self.__qemu_exe = shutil.which("qemu-system-arm")
|
||||
|
||||
def _impl_qemu_exe(self):
|
||||
return self.__qemu_exe
|
||||
|
||||
def _impl_machine(self):
|
||||
return "sabrelite"
|
||||
|
||||
def _impl_append(self):
|
||||
return "root=/dev/mmcblk3p2 console=ttymxc0,115200 nomodeset vga=current video=vesafb:off locale.LANG=C locale.LANGUAGE=C locale.LC_MESSAGES=C locale.LC_CTYPE=C TERM=dumb"
|
||||
|
||||
def _impl_disks(self):
|
||||
return ["-drive", "if=none,id=mydrive,file={},format=raw".format(self.mach.env.args.medium), "-device", "sd-card,id=sd0,drive=mydrive"]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue