Project restart
This commit is contained in:
1
src/VERSION
Normal file
1
src/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
0.1
|
||||
38
src/pyproject.toml
Normal file
38
src/pyproject.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name="testium"
|
||||
requires-python = ">=3.11"
|
||||
authors = [
|
||||
{name = "François Dausseur", email = "francois@beafrancois.fr"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Programming Language :: Python"
|
||||
]
|
||||
dependencies = [
|
||||
"setuptools",
|
||||
"pyside6",
|
||||
"pyyaml",
|
||||
"pyserial",
|
||||
"colorama",
|
||||
"matplotlib",
|
||||
"telnetlib3",
|
||||
"jinja2",
|
||||
"pexpect",
|
||||
"gitpython",
|
||||
"junit-xml",
|
||||
"lxml",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.scripts]
|
||||
testium = "testium:main"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
docpkg = ["*.pdf"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {file = ["VERSION"]}
|
||||
12
src/requirements.txt
Normal file
12
src/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
setuptools
|
||||
pyside6
|
||||
pyserial
|
||||
telnetlib3
|
||||
pyyaml
|
||||
pexpect
|
||||
gitpython
|
||||
jinja2
|
||||
colorama
|
||||
matplotlib
|
||||
junit-xml
|
||||
lxml
|
||||
140
src/testium/__init__.py
Executable file
140
src/testium/__init__.py
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
|
||||
ourpath = Path(__file__)
|
||||
ourpath = ourpath.resolve()
|
||||
sys.path.append(os.path.abspath(ourpath.parent))
|
||||
from interpreter.utils.eval import evaluate
|
||||
|
||||
import interpreter.utils.constants as cst
|
||||
|
||||
def main():
|
||||
# This line sets the method for the "Process" function. It is required for Linux
|
||||
# support of the test dialogs.
|
||||
multiprocessing.set_start_method('spawn')
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--version",
|
||||
help="Returns the version of testium", action='store_true')
|
||||
parser.add_argument("-b", "--batch-execution",
|
||||
help="Executes the test in batch mode", action='store_true')
|
||||
parser.add_argument("-m", "--terminal",
|
||||
help="Starts terminal mode", action='store_true')
|
||||
parser.add_argument("-o", "--no-color",
|
||||
help="Deactivates stdout colors in batch and terminal mode", action='store_true')
|
||||
parser.add_argument("-c", "--config-file", help="Configuration file",
|
||||
nargs='+',
|
||||
default=[])
|
||||
parser.add_argument("-r", "--run-and-close", action='store_true',
|
||||
help="Runs the test then closes the application",
|
||||
required=False)
|
||||
parser.add_argument("-l", "--log-file", help="log file name", default='')
|
||||
parser.add_argument("-d", "--define",
|
||||
help="Configuration passed to the executed tests.",
|
||||
nargs='+',
|
||||
type=str,
|
||||
action='append',
|
||||
default=[])
|
||||
parser.add_argument("-p", "--report-file",
|
||||
help="report file name", default='')
|
||||
parser.add_argument("-t", "--report-type", help="report file type",
|
||||
choices=cst.REP_TYPES,
|
||||
default='')
|
||||
parser.add_argument("-n", "--report-pattern", help="report file pattern",
|
||||
nargs='+',
|
||||
default=[])
|
||||
parser.add_argument("-i", "--include-path",
|
||||
help="Python modules search path",
|
||||
nargs='+',
|
||||
default=[])
|
||||
parser.add_argument("-g", "--debug", action='store_true',
|
||||
help="GUI debug mode",
|
||||
required=False)
|
||||
|
||||
parser.add_argument(
|
||||
'test_file', help='the test script file', nargs='?', default='')
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(args.include_path)>0:
|
||||
for p in args.include_path:
|
||||
sys.path.append(p)
|
||||
|
||||
defines = {}
|
||||
defs = []
|
||||
for define in args.define:
|
||||
defs += define
|
||||
for define in defs:
|
||||
d = define.split('=', 1)
|
||||
if d[0].strip() != '':
|
||||
if len(d) > 1:
|
||||
_, edef = evaluate(d[1])
|
||||
defines.update({d[0].strip(): edef})
|
||||
else:
|
||||
defines.update({d[0].strip(): True})
|
||||
|
||||
cf = []
|
||||
for c in args.config_file:
|
||||
conf = c.strip('\"').strip("\'")
|
||||
if not os.path.isabs(conf):
|
||||
conf = os.path.join(os.getcwd(), conf)
|
||||
cf.append(conf)
|
||||
tf = args.test_file.strip('\"').strip("\'")
|
||||
rf = args.report_file.strip('\"').strip("\'")
|
||||
lf = args.log_file.strip('\"').strip("\'")
|
||||
pn = []
|
||||
for p in args.report_pattern:
|
||||
pn.append(p.strip('\"').strip("\'"))
|
||||
|
||||
if args.version:
|
||||
# initilization of the settings (used to know if git supported)
|
||||
import interpreter.utils.settings as prefs
|
||||
prefs.init()
|
||||
|
||||
from interpreter.utils.version import get_testium_version
|
||||
print(get_testium_version())
|
||||
|
||||
elif args.terminal:
|
||||
import select
|
||||
from interpreter.terminal import Terminal
|
||||
|
||||
if (lf != '') or (rf != '') or (tf != '') or (pn != []):
|
||||
print('"-l", "-p", "-t", "-n" options are not supported in this mode.')
|
||||
|
||||
t = Terminal(os.getcwd(), cf, defines, args.no_color)
|
||||
|
||||
loop = 1
|
||||
while loop:
|
||||
try:
|
||||
loop = 0
|
||||
t.cmdloop()
|
||||
except KeyboardInterrupt:
|
||||
print("\n<ctrl-c>")
|
||||
loop = 1
|
||||
except Exception as exc:
|
||||
if str(exc) == 'quit':
|
||||
break
|
||||
print(exc)
|
||||
loop = 1
|
||||
|
||||
|
||||
elif args.batch_execution:
|
||||
if (lf != ''):
|
||||
print('"-l" option is not supported in this mode.')
|
||||
|
||||
from interpreter.batch import Batch
|
||||
b = Batch(tf, cf, defines, rf, args.report_type, pn, args.no_color)
|
||||
|
||||
else:
|
||||
from main_win.testium_win import MainWin
|
||||
MainWin(tf, config_files=cf,
|
||||
run=args.run_and_close,
|
||||
log_file=lf,
|
||||
defines=defines,
|
||||
report=rf,
|
||||
report_type=args.report_type,
|
||||
report_pattern=pn,
|
||||
debug=args.debug)
|
||||
25
src/testium/__main__.py
Normal file
25
src/testium/__main__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import os, sys
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.ERROR,
|
||||
filename=os.path.join(os.path.normpath(os.getcwd()), "crash.txt"),
|
||||
format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
def exception_handler(typ_exc, value, trbk):
|
||||
"""Testium Exception handling"""
|
||||
logging.error("An unmanaged exception occured", exc_info=(typ_exc, value, trbk))
|
||||
print(f"Critical failure : '{value}'.")
|
||||
tb = traceback.format_exception(typ_exc, value, trbk)
|
||||
print("".join(tb[-4:]))
|
||||
|
||||
sys.excepthook = exception_handler
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
|
||||
from testium import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
src/testium/interpreter/__init__.py
Normal file
0
src/testium/interpreter/__init__.py
Normal file
98
src/testium/interpreter/batch.py
Normal file
98
src/testium/interpreter/batch.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
from time import sleep
|
||||
from signal import signal, SIGINT
|
||||
from queue import Empty
|
||||
from multiprocessing import Queue
|
||||
|
||||
from interpreter.process import TestProcess
|
||||
from interpreter.utils.test_ctrl import TestSetController
|
||||
from interpreter.utils.tum_except import ETUMFileError
|
||||
from interpreter.utils.stdout_redirect import stdio_redir
|
||||
|
||||
|
||||
class Batch:
|
||||
def __init__(
|
||||
self,
|
||||
test_file,
|
||||
config_files,
|
||||
defines,
|
||||
report_file,
|
||||
report_type,
|
||||
report_pattern,
|
||||
no_color,
|
||||
):
|
||||
try:
|
||||
try:
|
||||
file_name = os.path.abspath(test_file)
|
||||
initial_dir = os.path.dirname(file_name)
|
||||
|
||||
if not os.path.isdir(initial_dir):
|
||||
raise ETUMFileError("Could not find %s directory" % (initial_dir))
|
||||
if not os.path.isfile(file_name):
|
||||
raise ETUMFileError("Could not find %s file" % (file_name))
|
||||
|
||||
if not file_name:
|
||||
raise ETUMFileError("No file to load")
|
||||
|
||||
outstream = sys.stdout
|
||||
if "Linux" in platform.system() and not no_color:
|
||||
try:
|
||||
from interpreter.utils.termlog import TermLog
|
||||
|
||||
outstream = TermLog(sys.stdout)
|
||||
stdio_redir.redirect(outstream)
|
||||
except ModuleNotFoundError:
|
||||
print(
|
||||
"Colored console not supported by the system."
|
||||
+ " If you want it, please install colorama module"
|
||||
)
|
||||
|
||||
signal(SIGINT, self.sigint_handler)
|
||||
|
||||
msg_queue = Queue()
|
||||
self.tst_ctrl = TestSetController()
|
||||
tst_proc = TestProcess(
|
||||
file_name,
|
||||
msg_queue,
|
||||
self.tst_ctrl,
|
||||
config_files,
|
||||
defines,
|
||||
)
|
||||
tst_proc.start()
|
||||
|
||||
while not self.tst_ctrl.control("loaded"):
|
||||
sleep(0.1)
|
||||
|
||||
self.tst_ctrl.control(
|
||||
"report",
|
||||
rep_path=report_file,
|
||||
rep_type=report_type,
|
||||
pattern=report_pattern,
|
||||
)
|
||||
# Start test execution
|
||||
self.tst_ctrl.control("execute")
|
||||
|
||||
# Wait for the "finished" signal
|
||||
while True:
|
||||
try:
|
||||
m = msg_queue.get(timeout=0.2)
|
||||
if m.get("id", None) is None:
|
||||
# No id -> finished
|
||||
break
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
# Close the process and wait for termination
|
||||
self.tst_ctrl.control("close")
|
||||
tst_proc.join()
|
||||
|
||||
except Exception as e:
|
||||
print("Exception encountered:")
|
||||
print(str(e))
|
||||
finally:
|
||||
stdio_redir.restore()
|
||||
|
||||
def sigint_handler(self, signal_received, frame):
|
||||
self.tst_ctrl.control("stop")
|
||||
230
src/testium/interpreter/process.py
Normal file
230
src/testium/interpreter/process.py
Normal file
@@ -0,0 +1,230 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Queue, Pipe
|
||||
from queue import Empty
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
import traceback
|
||||
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.params import expanse
|
||||
from interpreter.utils.string_queue import StringQueue
|
||||
from interpreter.utils.test_ctrl import TestSetController
|
||||
from interpreter.utils.test_init import (
|
||||
env_init,
|
||||
load_test,
|
||||
test_run_init,
|
||||
test_run_header,
|
||||
locate_report_file,
|
||||
backup_gd,
|
||||
restore_gd,
|
||||
)
|
||||
from interpreter.test_set import TestSet
|
||||
from interpreter.utils.stdout_redirect import stdio_redir
|
||||
from interpreter.utils.tum_except import print_exception
|
||||
from interpreter.utils.func_exec import func_call_init
|
||||
from interpreter.utils.api_srv import api_request
|
||||
|
||||
|
||||
class TestProcess(Process):
|
||||
def __init__(
|
||||
self,
|
||||
file_name,
|
||||
status_queue: Queue,
|
||||
tst_control: TestSetController,
|
||||
config_files,
|
||||
defines,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.__fname = file_name
|
||||
self.__squeue = status_queue
|
||||
self.__tctrl = tst_control
|
||||
self.__cfgf = config_files
|
||||
self.__defs = defines
|
||||
self.__exec = False
|
||||
self.__loaded = False
|
||||
self.__closed = False
|
||||
self.__pconn = self.redirect_stdout()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
try:
|
||||
# Thread for stdout redirection
|
||||
in_stream = StringQueue()
|
||||
self.redir = Thread(target=self.send_stdout, args=[in_stream])
|
||||
self.redir.daemon = True
|
||||
stdio_redir.redirect(in_stream)
|
||||
self.redir.start()
|
||||
test_dir = os.path.dirname(os.path.abspath(self.__fname))
|
||||
|
||||
env_init()
|
||||
|
||||
# Load the test file
|
||||
test_dict, cfg_files = load_test(
|
||||
self.__fname, test_dir, self.__cfgf, self.__defs)
|
||||
|
||||
# Backup the global dict in case of restart of the test
|
||||
gdict = backup_gd()
|
||||
|
||||
# The path of the test file is included in PYTHONPATH
|
||||
sys.path.append(os.path.dirname(self.__fname))
|
||||
|
||||
# Now create the test structure and objects
|
||||
test_set = TestSet(self.__fname, test_dict, self.__squeue)
|
||||
|
||||
# Thread for incoming control commands
|
||||
self.init_commands(test_set)
|
||||
self.cmd_th = Thread(
|
||||
target=self.process_control_commands, args=[self.__tctrl])
|
||||
self.cmd_th.daemon = True
|
||||
self.cmd_th.start()
|
||||
|
||||
test_set.report_path = locate_report_file(test_set.report_path)
|
||||
|
||||
# Python functions call subprocess initialization
|
||||
fproc = func_call_init(tm.gd("python_path", ""), api_request)
|
||||
|
||||
self.__loaded = True
|
||||
|
||||
while True:
|
||||
# waiting for a control command
|
||||
while (not self.__exec) and (not self.__closed):
|
||||
sleep(0.2)
|
||||
# if close is required
|
||||
if self.__closed:
|
||||
break
|
||||
# Test is started
|
||||
try:
|
||||
try:
|
||||
try:
|
||||
test_run_init()
|
||||
print(test_run_header())
|
||||
fproc.start()
|
||||
fproc.wait_ready()
|
||||
test_set.execute()
|
||||
finally:
|
||||
if test_set.success():
|
||||
print("Test run success.")
|
||||
else:
|
||||
print("Test run failed.")
|
||||
|
||||
test_set.run_post_exec()
|
||||
finally:
|
||||
# Stop function execution process
|
||||
fproc.stop()
|
||||
fproc.join()
|
||||
self.__exec = False
|
||||
# Sends signal to the GUI
|
||||
self.send_finished()
|
||||
restore_gd(gdict)
|
||||
except Exception as e:
|
||||
print_exception(e)
|
||||
|
||||
except Exception as e:
|
||||
print_exception(e)
|
||||
|
||||
finally:
|
||||
self.exit()
|
||||
|
||||
def init_commands(self, test_set: TestSet):
|
||||
self.__cmds = {
|
||||
"pause": test_set.pause,
|
||||
"cont": test_set.cont,
|
||||
"tree": test_set.tree,
|
||||
"report": test_set.set_report,
|
||||
"stop": test_set.stop,
|
||||
"loaded": self.loaded,
|
||||
"execute": self.execute,
|
||||
"add_breakpoint": test_set.addBreakpoint,
|
||||
"del_breakpoint": test_set.delBreakpoint,
|
||||
"skipped_state": test_set.getSkippedState,
|
||||
"enabled_state": test_set.getEnabledState,
|
||||
"process_param": self.process_param,
|
||||
"set_test_outputs": self.set_test_outputs,
|
||||
"set_enabled_state": test_set.setEnabledState,
|
||||
"check_uncheck_all": test_set.checkUncheckAll,
|
||||
"get_folded": test_set.getFolded,
|
||||
"close": self.close,
|
||||
}
|
||||
|
||||
def exit(self):
|
||||
self.__closed = True
|
||||
if hasattr(self, "cmd_th"):
|
||||
self.cmd_th.join()
|
||||
self.redir.join()
|
||||
stdio_redir.restore()
|
||||
stdio_redir.stop()
|
||||
|
||||
def send_finished(self):
|
||||
status = {'id': None,
|
||||
'name': "test_process",
|
||||
'status': 'finished'}
|
||||
self.__squeue.put(status)
|
||||
|
||||
def execute(self):
|
||||
self.__exec = True
|
||||
|
||||
def loaded(self):
|
||||
return self.__loaded
|
||||
|
||||
def close(self):
|
||||
self.__closed = True
|
||||
|
||||
def process_param(self, param):
|
||||
return expanse(param)
|
||||
|
||||
def set_test_outputs(self, outputs: list):
|
||||
tm.setgd("test_outputs", outputs)
|
||||
|
||||
def process_control_commands(self, tctrl):
|
||||
term = False
|
||||
while (not term) and (not self.__closed):
|
||||
cmd = ""
|
||||
res = None
|
||||
args = {}
|
||||
try:
|
||||
qcontent = tctrl.ctrl.get(timeout=0.2)
|
||||
try:
|
||||
cmd = list(qcontent.keys())[0]
|
||||
args = qcontent[cmd]
|
||||
if cmd == "exit":
|
||||
term = True
|
||||
break
|
||||
try:
|
||||
if isinstance(args, dict):
|
||||
res = {cmd: self.__cmds[cmd](**args)}
|
||||
elif args is None:
|
||||
res = {cmd: self.__cmds[cmd]()}
|
||||
except:
|
||||
res = (None, "function unknown or call failed")
|
||||
except:
|
||||
res = (None, "Malformed command")
|
||||
tctrl.resp.put(res)
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
def redirect_stdout(self):
|
||||
pipe = pconn, cconn = Pipe()
|
||||
redir = Thread(target=self.capture_stdout, args=(cconn,))
|
||||
redir.daemon = True
|
||||
redir.start()
|
||||
return pconn
|
||||
|
||||
def send_stdout(self, stream):
|
||||
while not self.__closed:
|
||||
try:
|
||||
data = stream.read(block=True, timeout=0.2)
|
||||
if data != "":
|
||||
self.__pconn.send(data)
|
||||
except RuntimeError:
|
||||
continue
|
||||
|
||||
def capture_stdout(self, cconn):
|
||||
while True:
|
||||
try:
|
||||
# read the pipe data
|
||||
data = cconn.recv()
|
||||
print(data, end="")
|
||||
except EOFError:
|
||||
# exit the loop is the pipe is closed
|
||||
break
|
||||
243
src/testium/interpreter/terminal.py
Normal file
243
src/testium/interpreter/terminal.py
Normal file
@@ -0,0 +1,243 @@
|
||||
try:
|
||||
import readline
|
||||
except:
|
||||
pass
|
||||
from cmd import Cmd
|
||||
import os
|
||||
import sys
|
||||
from yaml import load, Loader
|
||||
import functools
|
||||
import platform
|
||||
import types
|
||||
import inspect
|
||||
|
||||
# test modules
|
||||
from interpreter.utils.test_init import (
|
||||
env_init, prepare_global, set_standard_gd_keys,
|
||||
update_global, test_run_init, test_run_header, load_test)
|
||||
from interpreter.utils.globdict import (global_dict)
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.test_report.test_report import TestReport
|
||||
|
||||
|
||||
class FakeQueue:
|
||||
def put(self, arg):
|
||||
pass
|
||||
|
||||
|
||||
def func(self, args):
|
||||
if not args.startswith("{"):
|
||||
args = "{"+args+"}"
|
||||
y = load(args, Loader)
|
||||
obj = self.current_item(y, status_queue=FakeQueue())
|
||||
obj.report = self.report
|
||||
res = obj.execute()
|
||||
if not (res.value is None):
|
||||
print('result : {}'.format(res.value))
|
||||
print(res.test_result)
|
||||
|
||||
|
||||
class Terminal(Cmd):
|
||||
SUPPORTED_TESTS = [
|
||||
cst.TYPE_SLEEP,
|
||||
cst.TYPE_LET,
|
||||
cst.TYPE_FUNCTION,
|
||||
cst.TYPE_CONSOLE,
|
||||
cst.TYPE_IMAGE_DLG,
|
||||
cst.TYPE_MESSAGE_DLG,
|
||||
cst.TYPE_QUESTION_DLG,
|
||||
cst.TYPE_VALUE_DLG,
|
||||
]
|
||||
|
||||
SUPPORTED_GROUPS = [
|
||||
cst.TYPE_GROUP,
|
||||
cst.TYPE_CYCLE
|
||||
]
|
||||
|
||||
def __init__(self, working_dir, config_files, defines, no_color):
|
||||
super().__init__()
|
||||
self.working_dir = working_dir
|
||||
self.config_files = config_files
|
||||
self.current_item = None
|
||||
report = TestReport(None)
|
||||
self.report = report
|
||||
|
||||
env_init()
|
||||
prepare_global()
|
||||
# Define the builtin variables
|
||||
set_standard_gd_keys("Unnamed", self.working_dir, '', config_files)
|
||||
update_global([], defines)
|
||||
|
||||
# creation of the functions
|
||||
for tst in self.SUPPORTED_TESTS:
|
||||
meth_name = "do_" + tst.item_cmd
|
||||
# copy of the function
|
||||
f = types.FunctionType(func.__code__, func.__globals__, name=meth_name,
|
||||
argdefs=func.__defaults__,
|
||||
closure=func.__closure__)
|
||||
f = functools.update_wrapper(f, func)
|
||||
f.__kwdefaults__ = func.__kwdefaults__
|
||||
f.__doc__ = tst.item_class.__doc__
|
||||
setattr(self, meth_name, types.MethodType(f, self))
|
||||
|
||||
test_run_init()
|
||||
self.prompt = "(testium)~ "
|
||||
|
||||
# display header
|
||||
print(test_run_header())
|
||||
# redirect output
|
||||
|
||||
if 'Linux' in platform.system() and not no_color:
|
||||
from interpreter.utils.stdout_redirect import stdio_redir
|
||||
try:
|
||||
from interpreter.utils.termlog import TermLog
|
||||
stdio_redir.redirect(TermLog(sys.stdout))
|
||||
except ModuleNotFoundError:
|
||||
tm.print_info('Colored console not supported by the system.' +
|
||||
' If you want it, please install colorama module')
|
||||
|
||||
def precmd(self, line: str) -> str:
|
||||
c = line.split(" ", 1)[0].strip()
|
||||
self.current_item = None
|
||||
for tst in self.SUPPORTED_TESTS:
|
||||
if c == tst.item_cmd:
|
||||
self.current_item = tst.item_class
|
||||
break
|
||||
return line
|
||||
|
||||
def load_test_recursively(self, tree_parent, parent_seq, status_queue):
|
||||
try:
|
||||
parent_seq_name = parent_seq['name']
|
||||
except KeyError:
|
||||
parent_seq['name'] = "sequence"
|
||||
except TypeError:
|
||||
raise Exception("Syntax error in an item of type {} which is a child of {}".format(
|
||||
tree_parent.type(), tree_parent.parent().name()))
|
||||
try:
|
||||
parent_seq_actions = parent_seq['steps']
|
||||
except KeyError:
|
||||
raise Exception(' No action list found for "%s" sequence'
|
||||
% (parent_seq_name))
|
||||
# if action is a dictionary , we assume it is a single action
|
||||
# that has not been nested in a list, so do it
|
||||
if isinstance(parent_seq_actions, (dict)):
|
||||
parent_seq_actions = [parent_seq_actions]
|
||||
if not isinstance(parent_seq_actions, (list, tuple)):
|
||||
raise Exception('Actions list not valid.')
|
||||
# first we merged to the same level 'sequence dict entries and list within the list
|
||||
counter = 0
|
||||
test_dir = tm.gd('test_directory')
|
||||
while (counter < len(parent_seq_actions)):
|
||||
action = parent_seq_actions[counter]
|
||||
# if action is a list raise up to the the same level,
|
||||
# ie insert action element into the parent_seq_actions
|
||||
if isinstance(action, (list, tuple)):
|
||||
parent_seq_actions[counter:counter+1] = action
|
||||
continue
|
||||
# if action is a NoneType skip and continue
|
||||
# (when pointing to an unused alias for instance)
|
||||
if action is None:
|
||||
counter += 1
|
||||
continue
|
||||
# if action is a sequence we insert its entry into the action list
|
||||
if 'sequence' in action:
|
||||
parent_seq_actions[counter:counter+1] = action['sequence']
|
||||
continue
|
||||
else:
|
||||
executed = False
|
||||
for it in [*self.SUPPORTED_TESTS, *self.SUPPORTED_GROUPS]:
|
||||
if it.item_cmd in action:
|
||||
executed = True
|
||||
item = (it.item_class)(action[it.item_cmd],
|
||||
tree_parent,
|
||||
status_queue)
|
||||
# check for sequence type:
|
||||
if it.item_cmd == cst.TYPE_UNITTEST_FILE.item_cmd:
|
||||
item.setTestDir(test_dir)
|
||||
item.load()
|
||||
elif ((it.item_cmd == cst.TYPE_CYCLE.item_cmd) or
|
||||
(it.item_cmd == cst.TYPE_GROUP.item_cmd)):
|
||||
self.load_test_recursively(
|
||||
item, action[it.item_cmd], status_queue)
|
||||
|
||||
if not executed:
|
||||
raise Exception('action type is not known "{}"'.format(
|
||||
list(action.keys())[0]))
|
||||
|
||||
counter += 1
|
||||
|
||||
def __setReportRecursively(self, parent):
|
||||
for i in range(parent.childCount()):
|
||||
parent.child(i).report = self.report
|
||||
self.__setReportRecursively(parent.child(i))
|
||||
|
||||
def setReport(self, root_item):
|
||||
root_item.report = self.report
|
||||
self.__setReportRecursively(root_item)
|
||||
|
||||
def get_names(self):
|
||||
memb = inspect.getmembers(self)
|
||||
return [n[0] for n in memb if (inspect.ismethod(n[1]) and n[0].startswith("do_"))]
|
||||
|
||||
def do_load(self, args):
|
||||
"""load function.
|
||||
|
||||
This function loads and executes a testium sub-script.
|
||||
|
||||
The loaded sequence can't be a main testium script ("testium -b" option is
|
||||
defined for such a usage).
|
||||
|
||||
Accepted files are with extension "*.tum".
|
||||
|
||||
usage:
|
||||
load path/to/my/sequence.tum
|
||||
"""
|
||||
file = args.strip()
|
||||
suff = file[-4:]
|
||||
if not suff in ['.tum']:
|
||||
raise Exception('Wrong input file extension')
|
||||
|
||||
if not (os.path.exists(file) and os.path.isfile(file)):
|
||||
raise Exception(
|
||||
'"{}" does not exist or is not a file.'.format(file))
|
||||
|
||||
d, _ = load_test(file)
|
||||
if not isinstance(d, list):
|
||||
raise Exception(
|
||||
"The file root object must be a list. A \"main\" tum can't be loaded from here (use batch mode instead).")
|
||||
|
||||
if (len(d) == 1) and isinstance(d[0], dict) and (not d[0].get('sequence', None) is None):
|
||||
d = d[0]['sequence']
|
||||
|
||||
sq = FakeQueue()
|
||||
root_item = (cst.TYPE_ROOT.item_class)(
|
||||
dict_item={'steps': d}, status_queue=sq)
|
||||
self.load_test_recursively(root_item, {'steps': d}, sq)
|
||||
self.setReport(root_item)
|
||||
res = root_item.execute()
|
||||
if not (res.value is None):
|
||||
print('"{}" execution overall result: {}'.format(file, res.value))
|
||||
print(res.test_result)
|
||||
|
||||
def do_gd(self, args):
|
||||
"""Variables lists and values.
|
||||
|
||||
usage:
|
||||
gd
|
||||
gd home
|
||||
"""
|
||||
if args != '':
|
||||
res = tm.gd(args, None)
|
||||
if res is None:
|
||||
raise Exception(
|
||||
'the variable: "{}" has not been found.'.format(args))
|
||||
print(res)
|
||||
return
|
||||
|
||||
for k in global_dict.keys():
|
||||
print('{}: {}'.format(str(k), str(global_dict[k])))
|
||||
|
||||
def do_quit(self, args):
|
||||
'''Quit the application.'''
|
||||
raise Exception('quit')
|
||||
0
src/testium/interpreter/test_items/__init__.py
Normal file
0
src/testium/interpreter/test_items/__init__.py
Normal file
@@ -0,0 +1,254 @@
|
||||
import sys
|
||||
import os
|
||||
from multiprocessing import freeze_support
|
||||
from itertools import chain
|
||||
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from PySide6.QtWidgets import QApplication, QDialog, QDialogButtonBox
|
||||
from PySide6.QtCore import Qt, QSettings, QSize
|
||||
from PySide6.QtGui import QFont, QFontInfo
|
||||
from PySide6.QtWidgets import QTreeWidgetItem
|
||||
|
||||
# try:
|
||||
from interpreter.test_items.dialog_choices_files import choices_dialog_win
|
||||
|
||||
# except:
|
||||
# import choices_dialog_win
|
||||
|
||||
|
||||
def __iter__QTreeWidgetItem(self):
|
||||
for item in chain(*map(iter, self.children())):
|
||||
yield item
|
||||
yield self
|
||||
|
||||
|
||||
def childrenQTreeWidgetItem(self):
|
||||
return [self.child(i) for i in range(self.childCount())]
|
||||
|
||||
|
||||
QTreeWidgetItem.name = ""
|
||||
QTreeWidgetItem.__iter__ = __iter__QTreeWidgetItem
|
||||
QTreeWidgetItem.children = childrenQTreeWidgetItem
|
||||
|
||||
|
||||
class ChoicesTreeItem(QTreeWidgetItem):
|
||||
|
||||
def __init__(self, parent, dic, default_icon):
|
||||
super().__init__()
|
||||
self.name = dic.get("name", "")
|
||||
self.setFlags(self.flags() | Qt.ItemIsUserCheckable)
|
||||
self.setCheckState(0, Qt.Checked)
|
||||
parent.addChild(self)
|
||||
self._default_icon = default_icon
|
||||
self.setRowIcon(dic.get("icon", ""))
|
||||
|
||||
def setRowIcon(self, icon_path):
|
||||
icon = None
|
||||
if icon_path != "":
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
pmap = QPixmap(icon_path)
|
||||
icon = QIcon(pmap)
|
||||
self.setIcon(0, icon)
|
||||
except:
|
||||
# we don't want to crash for an icon
|
||||
print(f"WARN Impossible to load '{icon_path}' icon.")
|
||||
if (icon is None) and (self._default_icon is not None):
|
||||
self.setIcon(0, self._default_icon)
|
||||
|
||||
|
||||
class ChoicesDialog(QDialog, choices_dialog_win.Ui_Dialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._default_icon = None
|
||||
self.setupUi(self)
|
||||
self.choicesView.setColumnCount(2)
|
||||
self.choicesView.setAlternatingRowColors(True)
|
||||
self.choicesView.setIconSize(QSize(24, 24))
|
||||
font = QFont()
|
||||
font.setPointSize(12)
|
||||
self.choicesView.setFont(font)
|
||||
self.choicesView.setAlternatingRowColors(True)
|
||||
self.choicesView.header().setVisible(True)
|
||||
self.choicesView.header().setDefaultSectionSize(50)
|
||||
self.choicesView.header().setMinimumSectionSize(50)
|
||||
self.choicesView.header().setStretchLastSection(False)
|
||||
|
||||
self.choicesView.headerItem().setText(0, "name")
|
||||
self.choicesView.setColumnWidth(0, 300)
|
||||
self.choicesView.headerItem().setText(1, "description")
|
||||
self.choicesView.setColumnWidth(1, 800)
|
||||
self.root = self.choicesView.invisibleRootItem()
|
||||
|
||||
def connect_checked(self):
|
||||
self.choicesView.itemChanged.connect(self.on_testChecked)
|
||||
|
||||
def apply_default_icon(self, path):
|
||||
if (path is not None) and os.path.exists(path):
|
||||
try:
|
||||
pmap = QPixmap(path)
|
||||
self._default_icon = QIcon(pmap)
|
||||
except:
|
||||
# we don't want to crash for an icon
|
||||
print(f"WARN Impossible to load '{path}' icon.")
|
||||
elif path is not None:
|
||||
print("Icon not loaded since it is not a valid path.")
|
||||
|
||||
def populate_tree(self, parent, choices):
|
||||
if not isinstance(choices, list):
|
||||
return
|
||||
|
||||
for choice in choices:
|
||||
name = choice.get("name", "")
|
||||
desc = choice.get("description", "")
|
||||
if name == "":
|
||||
continue
|
||||
tree_item = ChoicesTreeItem(parent, choice, self._default_icon)
|
||||
tree_item.setText(0, name)
|
||||
tree_item.setText(1, desc)
|
||||
sub_choices = choice.get("choices", None)
|
||||
if sub_choices is not None:
|
||||
self.populate_tree(tree_item, sub_choices)
|
||||
|
||||
def __foldRecursively(self, tree_item, is_fold):
|
||||
for i in range(tree_item.childCount()):
|
||||
if tree_item.child(i).childCount() > 0:
|
||||
tree_item.child(i).setExpanded(not is_fold)
|
||||
self.__foldRecursively(tree_item.child(i), is_fold)
|
||||
|
||||
def foldAll(self, is_fold):
|
||||
self.__foldRecursively(self.root, is_fold)
|
||||
|
||||
def on_testChecked(self, item, index):
|
||||
self.updateTreeCheckState(item, Qt.Checked == item.checkState(0))
|
||||
|
||||
def updateTreeCheckState(self, tree_item, is_checked):
|
||||
# treat the case of the invisible root
|
||||
if tree_item is self.root:
|
||||
for i in range(self.root.childCount()):
|
||||
self.updateTreeCheckState(self.root.child(i), is_checked)
|
||||
else:
|
||||
if is_checked:
|
||||
tree_item.setCheckState(0, Qt.Checked)
|
||||
else:
|
||||
tree_item.setCheckState(0, Qt.Unchecked)
|
||||
|
||||
for i in range(tree_item.childCount()):
|
||||
self.updateTreeCheckState(tree_item.child(i), is_checked)
|
||||
|
||||
def checked_state(self, parent=None):
|
||||
if parent is None:
|
||||
return self.checked_state(self.root)
|
||||
|
||||
sub_choices = []
|
||||
for i in range(parent.childCount()):
|
||||
sub_choices.append(self.checked_state(parent.child(i)))
|
||||
|
||||
if parent is self.root:
|
||||
res = sub_choices
|
||||
else:
|
||||
res = {
|
||||
"name": parent.name,
|
||||
"checked": Qt.Checked == parent.checkState(0),
|
||||
}
|
||||
if len(sub_choices) > 0:
|
||||
res.update({"choices": sub_choices})
|
||||
|
||||
return res
|
||||
|
||||
def apply_checked(self, choice, parent=None):
|
||||
if parent is None:
|
||||
self.apply_checked(choice, self.root)
|
||||
return
|
||||
|
||||
if not isinstance(choice, list):
|
||||
return
|
||||
|
||||
if len(choice) != parent.childCount():
|
||||
return
|
||||
|
||||
for i in range(parent.childCount()):
|
||||
if not isinstance(choice[i], dict):
|
||||
return
|
||||
if choice[i].get("checked", True) == True:
|
||||
parent.child(i).setCheckState(0, Qt.Checked)
|
||||
else:
|
||||
parent.child(i).setCheckState(0, Qt.Unchecked)
|
||||
|
||||
sub_choices = choice[i].get("choices", None)
|
||||
if sub_choices is not None:
|
||||
self.apply_checked(sub_choices, parent.child(i))
|
||||
|
||||
|
||||
def main(args, conn=None):
|
||||
SettingsCompagny = "Testium"
|
||||
SettingsApplication = "testium_choices_dlg_" + args[0]
|
||||
SettingsLastChoices = "last_choice"
|
||||
success = True
|
||||
app = QApplication()
|
||||
d = ChoicesDialog()
|
||||
d.setFixedSize(800, 600)
|
||||
d.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
d.setWindowTitle(args[0])
|
||||
d.labelDialog.setText(args[1])
|
||||
d.labelDialog.setAlignment(Qt.AlignCenter)
|
||||
d.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
|
||||
d.apply_default_icon(args[3])
|
||||
d.populate_tree(d.root, args[2])
|
||||
d.foldAll(False)
|
||||
|
||||
settings = QSettings(SettingsCompagny, SettingsApplication)
|
||||
last_choice = settings.value(SettingsLastChoices, "")
|
||||
|
||||
d.apply_checked(last_choice)
|
||||
|
||||
d.connect_checked()
|
||||
|
||||
d.choicesView.setFocus()
|
||||
dres = d.exec()
|
||||
|
||||
if dres == QDialog.Rejected:
|
||||
success = False
|
||||
|
||||
# build the answer:
|
||||
|
||||
result = d.checked_state()
|
||||
|
||||
if conn:
|
||||
settings.setValue(SettingsLastChoices, result)
|
||||
conn.send([result, success])
|
||||
conn.close()
|
||||
else:
|
||||
print(result, end="")
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
# all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
"""dummyStream behaves like a stream but does nothing."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def write(self, data):
|
||||
pass
|
||||
|
||||
def read(self, data):
|
||||
pass
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'choices_dialog_win.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||
QHeaderView, QLabel, QSizePolicy, QTreeWidget,
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
if not Dialog.objectName():
|
||||
Dialog.setObjectName(u"Dialog")
|
||||
Dialog.resize(481, 386)
|
||||
Dialog.setModal(True)
|
||||
self.verticalLayout_2 = QVBoxLayout(Dialog)
|
||||
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
||||
self.verticalLayout = QVBoxLayout()
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.labelDialog = QLabel(Dialog)
|
||||
self.labelDialog.setObjectName(u"labelDialog")
|
||||
font = QFont()
|
||||
font.setPointSize(22)
|
||||
self.labelDialog.setFont(font)
|
||||
|
||||
self.verticalLayout.addWidget(self.labelDialog)
|
||||
|
||||
self.choicesView = QTreeWidget(Dialog)
|
||||
self.choicesView.setObjectName(u"choicesView")
|
||||
self.choicesView.setColumnCount(0)
|
||||
|
||||
self.verticalLayout.addWidget(self.choicesView)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(Dialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
|
||||
self.verticalLayout.addWidget(self.buttonBox)
|
||||
|
||||
|
||||
self.verticalLayout_2.addLayout(self.verticalLayout)
|
||||
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(Dialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None))
|
||||
self.labelDialog.setText(QCoreApplication.translate("Dialog", u"TextLabel", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>481</width>
|
||||
<height>386</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelDialog">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>22</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="choicesView">
|
||||
<property name="columnCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>240</x>
|
||||
<y>362</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>240</x>
|
||||
<y>192</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>240</x>
|
||||
<y>362</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>240</x>
|
||||
<y>192</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,72 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import (Qt)
|
||||
from PySide6.QtWidgets import (QApplication, QDialog)
|
||||
from PySide6 import (QtGui)
|
||||
|
||||
try:
|
||||
from interpreter.test_items.dialog_image_files import dialog_image_win
|
||||
except:
|
||||
import dialog_image_win
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
class TestDialogWindow(QDialog, dialog_image_win.Ui_Dialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
def main(args, conn):
|
||||
success = True
|
||||
app = QApplication(args)
|
||||
d = TestDialogWindow()
|
||||
d.setFixedSize(700,600)
|
||||
d.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
d.setWindowTitle(args[0])
|
||||
d.labelDialog.setText(args[1])
|
||||
|
||||
image = QtGui.QImage(args[2])
|
||||
|
||||
if image.isNull():
|
||||
print('Image %s could not be loaded...' % (args[2]))
|
||||
success = False
|
||||
|
||||
else:
|
||||
image2 = image.scaled(d.labelImage.width(), d.labelImage.height(),
|
||||
aspectMode=Qt.KeepAspectRatio)
|
||||
|
||||
d.labelImage.setPixmap(QtGui.QPixmap.fromImage(image2))
|
||||
|
||||
dres = d.exec()
|
||||
|
||||
if dres == QDialog.Rejected:
|
||||
success = False
|
||||
|
||||
if conn is not None:
|
||||
conn.send(success)
|
||||
conn.close()
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:], None)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'dialog_image_win.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||
QLabel, QSizePolicy, QWidget)
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
if not Dialog.objectName():
|
||||
Dialog.setObjectName(u"Dialog")
|
||||
Dialog.setWindowModality(Qt.WindowModal)
|
||||
Dialog.resize(700, 600)
|
||||
Dialog.setSizeGripEnabled(False)
|
||||
Dialog.setModal(True)
|
||||
self.buttonBox = QDialogButtonBox(Dialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setGeometry(QRect(10, 560, 681, 32))
|
||||
self.buttonBox.setOrientation(Qt.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||
self.labelDialog = QLabel(Dialog)
|
||||
self.labelDialog.setObjectName(u"labelDialog")
|
||||
self.labelDialog.setGeometry(QRect(10, 10, 681, 71))
|
||||
font = QFont()
|
||||
font.setPointSize(20)
|
||||
self.labelDialog.setFont(font)
|
||||
self.labelDialog.setAlignment(Qt.AlignCenter)
|
||||
self.labelDialog.setWordWrap(True)
|
||||
self.labelImage = QLabel(Dialog)
|
||||
self.labelImage.setObjectName(u"labelImage")
|
||||
self.labelImage.setGeometry(QRect(10, 80, 681, 471))
|
||||
self.labelImage.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(Dialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None))
|
||||
self.labelDialog.setText(QCoreApplication.translate("Dialog", u"TextLabel", None))
|
||||
self.labelImage.setText("")
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>700</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>560</y>
|
||||
<width>681</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="labelDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>681</width>
|
||||
<height>71</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>20</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="labelImage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>80</y>
|
||||
<width>681</width>
|
||||
<height>471</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,36 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PySide6.QtWidgets import (QApplication, QDialog)
|
||||
from PySide6.QtCore import (Qt)
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
def main(args):
|
||||
app = QApplication(sys.argv)
|
||||
reply = QMessageBox.information(None, args[0], args[1], QMessageBox.Ok)
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'dialog_note_win.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||
QGridLayout, QLabel, QSizePolicy, QTextEdit,
|
||||
QWidget)
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
if not Dialog.objectName():
|
||||
Dialog.setObjectName(u"Dialog")
|
||||
Dialog.resize(501, 337)
|
||||
Dialog.setModal(True)
|
||||
self.gridLayout = QGridLayout(Dialog)
|
||||
self.gridLayout.setObjectName(u"gridLayout")
|
||||
self.labelDialog = QLabel(Dialog)
|
||||
self.labelDialog.setObjectName(u"labelDialog")
|
||||
font = QFont()
|
||||
font.setPointSize(20)
|
||||
self.labelDialog.setFont(font)
|
||||
self.labelDialog.setAlignment(Qt.AlignCenter)
|
||||
self.labelDialog.setWordWrap(True)
|
||||
|
||||
self.gridLayout.addWidget(self.labelDialog, 0, 0, 1, 1)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(Dialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setOrientation(Qt.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||
|
||||
self.gridLayout.addWidget(self.buttonBox, 2, 0, 1, 1)
|
||||
|
||||
self.textEdit = QTextEdit(Dialog)
|
||||
self.textEdit.setObjectName(u"textEdit")
|
||||
|
||||
self.gridLayout.addWidget(self.textEdit, 1, 0, 1, 1)
|
||||
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(Dialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None))
|
||||
self.labelDialog.setText(QCoreApplication.translate("Dialog", u"TextLabel", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>501</width>
|
||||
<height>337</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelDialog">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>20</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QTextEdit" name="textEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,54 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PySide6.QtWidgets import (QApplication, QDialog)
|
||||
from PySide6.QtCore import (Qt)
|
||||
from interpreter.test_items.dialog_note_files import dialog_note_win
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
class TestDialogWindow(QDialog, dialog_note_win.Ui_Dialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
|
||||
def main(args, conn=None):
|
||||
success = True
|
||||
app = QApplication(args)
|
||||
d = TestDialogWindow()
|
||||
d.setFixedSize(387,224)
|
||||
d.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
d.setWindowTitle(args[0])
|
||||
d.labelDialog.setText(args[1])
|
||||
d.textEdit.setFocus()
|
||||
dres = d.exec()
|
||||
|
||||
if dres == QDialog.Rejected:
|
||||
success = False
|
||||
if conn:
|
||||
conn.send([d.textEdit.toPlainText(), success])
|
||||
conn.close()
|
||||
else:
|
||||
print(d.textEdit.text(), end='')
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,32 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PySide6.QtWidgets import (QApplication, QDialog)
|
||||
from PySide6.QtCore import (Qt)
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
def main(args, conn):
|
||||
app = QApplication(sys.argv)
|
||||
reply = QMessageBox.question(None, args[0], args[1], QMessageBox.Yes|QMessageBox.No)
|
||||
|
||||
conn.send(reply)
|
||||
conn.close()
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
@@ -0,0 +1,81 @@
|
||||
import sys
|
||||
import os
|
||||
from PySide6.QtCore import (Qt, QTimer, QTime)
|
||||
from PySide6.QtWidgets import (QApplication, QDialog)
|
||||
|
||||
from interpreter.test_items.dialog_sleep_files import dialog_sleep_win
|
||||
|
||||
class DialogSleepWindow(QDialog, dialog_sleep_win.Ui_SleepDialogWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self.timeEdit.setDisplayFormat("HH:mm:ss")
|
||||
self.timer = QTimer()
|
||||
self.timer.setSingleShot(False)
|
||||
self.timer.stop()
|
||||
self.timer.timeout.connect(self.on_timerEvent)
|
||||
|
||||
def time(self, secs):
|
||||
hrs = secs//(3600)
|
||||
min = (secs - (hrs * 3600))//60
|
||||
s = secs - (hrs * 3600) - (min * 60)
|
||||
return QTime(hrs, min, s)
|
||||
|
||||
def setupTimer(self, timeout):
|
||||
self.timeout = int(timeout)
|
||||
|
||||
# time settings ...
|
||||
self.timeEdit.setTime(self.time(self.timeout))
|
||||
self.timer.setSingleShot(False)
|
||||
self.timer.setInterval(1000)
|
||||
self.timer.start()
|
||||
|
||||
def on_timerEvent(self):
|
||||
self.timeout = self.timeout - 1
|
||||
if self.timeout <= 0:
|
||||
self.accept()
|
||||
else:
|
||||
self.timeEdit.setTime(self.time(self.timeout))
|
||||
|
||||
def main(args, conn=None):
|
||||
success = True
|
||||
app = QApplication(sys.argv)
|
||||
d = DialogSleepWindow()
|
||||
d.setFixedSize(379,129)
|
||||
d.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
d.setModal(True)
|
||||
d.setWindowTitle(args[0])
|
||||
d.setupTimer(float(args[1]))
|
||||
|
||||
dres = d.exec()
|
||||
if dres == QDialog.Rejected:
|
||||
success = False
|
||||
res = -1
|
||||
if success:
|
||||
res = 0
|
||||
|
||||
if conn:
|
||||
conn.send(success)
|
||||
conn.close()
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'dialog_sleep_win.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QAbstractSpinBox, QApplication, QDateTimeEdit,
|
||||
QDialog, QDialogButtonBox, QHBoxLayout, QLabel,
|
||||
QLayout, QSizePolicy, QSpacerItem, QTimeEdit,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_SleepDialogWindow(object):
|
||||
def setupUi(self, SleepDialogWindow):
|
||||
if not SleepDialogWindow.objectName():
|
||||
SleepDialogWindow.setObjectName(u"SleepDialogWindow")
|
||||
SleepDialogWindow.resize(493, 124)
|
||||
font = QFont()
|
||||
font.setFamilies([u"Sans"])
|
||||
SleepDialogWindow.setFont(font)
|
||||
SleepDialogWindow.setModal(True)
|
||||
self.verticalLayout = QVBoxLayout(SleepDialogWindow)
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.verticalLayout.setSizeConstraint(QLayout.SetMinimumSize)
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||
self.label = QLabel(SleepDialogWindow)
|
||||
self.label.setObjectName(u"label")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
|
||||
self.label.setSizePolicy(sizePolicy)
|
||||
font1 = QFont()
|
||||
font1.setFamilies([u"Sans"])
|
||||
font1.setPointSize(21)
|
||||
self.label.setFont(font1)
|
||||
|
||||
self.horizontalLayout.addWidget(self.label)
|
||||
|
||||
self.timeEdit = QTimeEdit(SleepDialogWindow)
|
||||
self.timeEdit.setObjectName(u"timeEdit")
|
||||
font2 = QFont()
|
||||
font2.setFamilies([u"Sans"])
|
||||
font2.setPointSize(24)
|
||||
self.timeEdit.setFont(font2)
|
||||
self.timeEdit.setFrame(False)
|
||||
self.timeEdit.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
|
||||
self.timeEdit.setReadOnly(True)
|
||||
self.timeEdit.setButtonSymbols(QAbstractSpinBox.NoButtons)
|
||||
self.timeEdit.setCurrentSection(QDateTimeEdit.HourSection)
|
||||
|
||||
self.horizontalLayout.addWidget(self.timeEdit)
|
||||
|
||||
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
|
||||
self.horizontalLayout_2 = QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
|
||||
self.horizontalLayout_2.addItem(self.horizontalSpacer)
|
||||
|
||||
self.label_4 = QLabel(SleepDialogWindow)
|
||||
self.label_4.setObjectName(u"label_4")
|
||||
font3 = QFont()
|
||||
font3.setFamilies([u"Sans"])
|
||||
font3.setPointSize(10)
|
||||
self.label_4.setFont(font3)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.label_4)
|
||||
|
||||
self.label_3 = QLabel(SleepDialogWindow)
|
||||
self.label_3.setObjectName(u"label_3")
|
||||
self.label_3.setFont(font3)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.label_3)
|
||||
|
||||
self.label_2 = QLabel(SleepDialogWindow)
|
||||
self.label_2.setObjectName(u"label_2")
|
||||
self.label_2.setFont(font3)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.label_2)
|
||||
|
||||
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_2)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(SleepDialogWindow)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setOrientation(Qt.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel)
|
||||
|
||||
self.verticalLayout.addWidget(self.buttonBox)
|
||||
|
||||
|
||||
self.retranslateUi(SleepDialogWindow)
|
||||
self.buttonBox.accepted.connect(SleepDialogWindow.accept)
|
||||
self.buttonBox.rejected.connect(SleepDialogWindow.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(SleepDialogWindow)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, SleepDialogWindow):
|
||||
SleepDialogWindow.setWindowTitle(QCoreApplication.translate("SleepDialogWindow", u"Dialog", None))
|
||||
self.label.setText(QCoreApplication.translate("SleepDialogWindow", u"Remaining time", None))
|
||||
self.timeEdit.setDisplayFormat(QCoreApplication.translate("SleepDialogWindow", u"HH:mm:ss", None))
|
||||
self.label_4.setText(QCoreApplication.translate("SleepDialogWindow", u"hr", None))
|
||||
self.label_3.setText(QCoreApplication.translate("SleepDialogWindow", u"min", None))
|
||||
self.label_2.setText(QCoreApplication.translate("SleepDialogWindow", u"sec", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SleepDialogWindow</class>
|
||||
<widget class="QDialog" name="SleepDialogWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>493</width>
|
||||
<height>124</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
<pointsize>21</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remaining time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTimeEdit" name="timeEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
<pointsize>24</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
</property>
|
||||
<property name="currentSection">
|
||||
<enum>QDateTimeEdit::HourSection</enum>
|
||||
</property>
|
||||
<property name="displayFormat">
|
||||
<string>HH:mm:ss</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>hr</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>sec</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SleepDialogWindow</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SleepDialogWindow</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'dialog_value_win.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||
QLabel, QLineEdit, QSizePolicy, QWidget)
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
if not Dialog.objectName():
|
||||
Dialog.setObjectName(u"Dialog")
|
||||
Dialog.resize(387, 224)
|
||||
Dialog.setModal(True)
|
||||
self.buttonBox = QDialogButtonBox(Dialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setGeometry(QRect(20, 180, 351, 32))
|
||||
self.buttonBox.setOrientation(Qt.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||
self.labelDialog = QLabel(Dialog)
|
||||
self.labelDialog.setObjectName(u"labelDialog")
|
||||
self.labelDialog.setGeometry(QRect(10, 10, 371, 111))
|
||||
font = QFont()
|
||||
font.setPointSize(20)
|
||||
self.labelDialog.setFont(font)
|
||||
self.labelDialog.setAlignment(Qt.AlignCenter)
|
||||
self.labelDialog.setWordWrap(True)
|
||||
self.lineEdit = QLineEdit(Dialog)
|
||||
self.lineEdit.setObjectName(u"lineEdit")
|
||||
self.lineEdit.setGeometry(QRect(20, 130, 351, 40))
|
||||
self.lineEdit.setFont(font)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(Dialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None))
|
||||
self.labelDialog.setText(QCoreApplication.translate("Dialog", u"TextLabel", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>387</width>
|
||||
<height>224</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>180</y>
|
||||
<width>351</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="labelDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>371</width>
|
||||
<height>111</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>20</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLineEdit" name="lineEdit">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>130</y>
|
||||
<width>351</width>
|
||||
<height>40</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>20</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,59 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PySide6.QtWidgets import (QApplication, QDialog)
|
||||
from PySide6.QtCore import (Qt)
|
||||
|
||||
from interpreter.test_items.dialog_value_files import dialog_value_win
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
class TestDialogWindow(QDialog, dialog_value_win.Ui_Dialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
|
||||
def main(args, conn=None):
|
||||
success = True
|
||||
app = QApplication(args)
|
||||
d = TestDialogWindow()
|
||||
d.setFixedSize(387,224)
|
||||
d.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
d.setWindowTitle(args[0])
|
||||
d.labelDialog.setText(args[1])
|
||||
d.lineEdit.setText(args[2])
|
||||
d.lineEdit.setFocus()
|
||||
dres = d.exec()
|
||||
|
||||
if dres == QDialog.Rejected:
|
||||
success = False
|
||||
if conn:
|
||||
conn.send([d.lineEdit.text(), success])
|
||||
conn.close()
|
||||
else:
|
||||
print(d.lineEdit.text(), end='')
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
|
||||
118
src/testium/interpreter/test_items/item_actions/__init__.py
Normal file
118
src/testium/interpreter/test_items/item_actions/__init__.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.test_items.test_item import TestItem, test_run, test_data
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
from interpreter.test_items.item_actions.action import TestItemAction
|
||||
|
||||
|
||||
class TestItemActions(TestItem):
|
||||
def __init__(
|
||||
self, item_type, dict_actions, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
self._name = item_type.item_name
|
||||
super().__init__(dict_actions, parent, status_queue, filename=filename)
|
||||
self._type = item_type
|
||||
self.is_container = False
|
||||
self.action_classes = {}
|
||||
self.actions_token = None
|
||||
self.actions = []
|
||||
try:
|
||||
self.dict_actions = dict_actions["steps"]
|
||||
except KeyError:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has no action list",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
def register_actions(self, **args: TestItemAction):
|
||||
for action_name, action_class in args.items():
|
||||
self.action_classes.update({action_name: action_class})
|
||||
|
||||
def load(self):
|
||||
ret = {}
|
||||
for action in self.dict_actions:
|
||||
# Action should be only dict of length 1
|
||||
if not isinstance(action, dict) or (not len(action) == 1):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' action should be only dict of length = 1.",
|
||||
self.seqFilename()
|
||||
)
|
||||
action_name = list(action.keys())[0]
|
||||
if not (action_name in self.action_classes.keys()):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has an unknown action '{action.keys()[0]}'.",
|
||||
self.seqFilename()
|
||||
)
|
||||
|
||||
item = (self.action_classes[action_name])(
|
||||
action_name,
|
||||
action[action_name],
|
||||
self,
|
||||
self.status_queue,
|
||||
filename=self.seqFilename(),
|
||||
)
|
||||
self.actions.append(item)
|
||||
ret.update(test_data(item, {}))
|
||||
return ret
|
||||
|
||||
def __run(self):
|
||||
results = []
|
||||
i = 0
|
||||
to_be_stopped = False
|
||||
while (
|
||||
(not self.isStopped()) and (i < self.childCount()) and (not to_be_stopped)
|
||||
):
|
||||
result = self.child(i).execute()
|
||||
results.append(result)
|
||||
if result.test_result == TestValue.FAILURE and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
i = i + 1
|
||||
|
||||
if self.isStopped() or to_be_stopped:
|
||||
for j in range(self.childCount()):
|
||||
if self.child(j).executedOnStop() and (j >= i):
|
||||
self.child(j).execute()
|
||||
|
||||
test_success = TestValue.SUCCESS
|
||||
for res in results:
|
||||
if res.test_result == TestValue.FAILURE:
|
||||
test_success = TestValue.FAILURE
|
||||
break
|
||||
|
||||
result = TestResult(None, test_success, "Group iteration")
|
||||
return result
|
||||
|
||||
def setSeqFilename(self, filename):
|
||||
super().setSeqFilename(filename)
|
||||
for action in self.actions:
|
||||
action.setSeqFilename(filename)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
results = []
|
||||
to_be_stopped = False
|
||||
if (not self.isStopped()) and (not to_be_stopped):
|
||||
result = self.__run()
|
||||
|
||||
# Test results
|
||||
results.append(result)
|
||||
|
||||
if result.test_result == TestValue.FAILURE and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
|
||||
# end of loop test
|
||||
if self.isStopped() or to_be_stopped:
|
||||
if to_be_stopped:
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
f"'{self._name}' item execution aborted on failure",
|
||||
)
|
||||
else:
|
||||
self.result.set(
|
||||
TestValue.NORUN,
|
||||
f"'{self._name}' item execution aborted on user request",
|
||||
)
|
||||
else:
|
||||
self.result.set(TestValue.SUCCESS, "")
|
||||
for res in results:
|
||||
if not res.success:
|
||||
self.result.set(TestValue.FAILURE, "")
|
||||
39
src/testium/interpreter/test_items/item_actions/action.py
Normal file
39
src/testium/interpreter/test_items/item_actions/action.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from interpreter.test_items.test_item import TestItem, LOG_TEST_START, LOG_TEST_STOP
|
||||
|
||||
|
||||
class TestItemAction(TestItem):
|
||||
def __init__(
|
||||
self,
|
||||
action_name,
|
||||
item_type,
|
||||
dict_item: dict,
|
||||
parent: TestItem,
|
||||
status_queue,
|
||||
filename="",
|
||||
):
|
||||
if dict_item is None:
|
||||
dict_item = {}
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._dict_name = self._name
|
||||
self._name = (
|
||||
action_name + " - " + self._name if self._name != "" else action_name
|
||||
)
|
||||
self._type = item_type
|
||||
|
||||
self.banner = ""
|
||||
self.footer = ""
|
||||
if self._dict_name != "":
|
||||
self.banner = LOG_TEST_START.format(self._name)
|
||||
self.footer = LOG_TEST_STOP.format(self._name)
|
||||
|
||||
def write_banner(self):
|
||||
if self.banner != "":
|
||||
super().write_banner()
|
||||
|
||||
def write_footer(self):
|
||||
if self.banner != "":
|
||||
super().write_footer()
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self._parent.actions_token
|
||||
509
src/testium/interpreter/test_items/test_item.py
Normal file
509
src/testium/interpreter/test_items/test_item.py
Normal file
@@ -0,0 +1,509 @@
|
||||
from functools import wraps
|
||||
from time import sleep
|
||||
import yaml
|
||||
from copy import deepcopy
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.params import TestItemParams
|
||||
from interpreter.utils.constants import TestItemType as cst_type
|
||||
from interpreter.utils.eval import eval_to_boolean, evaluate, post_evaluate
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
|
||||
LOG_TEST_STOP = '<----- step "{}" finished'
|
||||
LOG_TEST_START = '-----> step "{}" started'
|
||||
|
||||
|
||||
class TestItem:
|
||||
pass
|
||||
|
||||
|
||||
def test_run(f):
|
||||
@wraps(f)
|
||||
def wrapper(self):
|
||||
if not self.skipped:
|
||||
if self.enabled:
|
||||
self.run_test_init()
|
||||
# Conditional execution
|
||||
raw_condition = self._prms.getParam(
|
||||
"condition", default=None, processed=False
|
||||
)
|
||||
if raw_condition is None:
|
||||
condition = True
|
||||
else:
|
||||
c = self._prms.expanse(raw_condition)
|
||||
if isinstance(c, bool):
|
||||
condition = c
|
||||
elif isinstance(c, (str, bytes)):
|
||||
is_evaluated, condition = evaluate(c)
|
||||
if not is_evaluated:
|
||||
print("eval with c: {}".format(c))
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a 'condition' impossible to evaluate",
|
||||
self.seqFilename(),
|
||||
)
|
||||
else:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a 'condition' result ({c}) which is not string or bool",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
msg = '"{}" --> "{}"'.format(raw_condition, c)
|
||||
|
||||
# Do we have to skip the test because of a true condition ?
|
||||
if condition:
|
||||
if not raw_condition is None:
|
||||
msg = "condition met: " + msg
|
||||
self.result.reported = {"input_condition": msg}
|
||||
print(msg)
|
||||
# Test preparation
|
||||
self.run_before_test()
|
||||
# Test execution
|
||||
f(self)
|
||||
else:
|
||||
msg = "condition not met: " + msg
|
||||
self.result.set(TestValue.NORUN, msg)
|
||||
self.result.reported = {"input_condition": msg}
|
||||
self.run_test_end()
|
||||
else:
|
||||
self.result.set(TestValue.NORUN, "test disabled")
|
||||
print("Test is disabled.")
|
||||
else:
|
||||
self.result.set(TestValue.NORUN, "test skipped")
|
||||
print("Test is skipped.")
|
||||
|
||||
return self.result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def test_data(item: TestItem, child: dict) -> dict:
|
||||
return {
|
||||
item.id(): {
|
||||
"id": item.id(),
|
||||
"name": item.name(),
|
||||
"type": item.type(),
|
||||
"doc": None if (item.doc() == "") or (item.doc() == None) else item.doc(),
|
||||
"content": item.content(),
|
||||
"folded": item.is_folded,
|
||||
"seq_filename": item.seqFilename(),
|
||||
"child": child,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestItem:
|
||||
def __init__(
|
||||
self, dict_item: dict = None, parent: TestItem = None, status_queue=None, filename = ""
|
||||
):
|
||||
self.enabled = True
|
||||
self.skipped = False
|
||||
self.is_container = True
|
||||
self.is_folded = False
|
||||
self._children = []
|
||||
self._parent = parent
|
||||
self._id = id(self)
|
||||
self._type = cst_type.TYPE_ROOT
|
||||
self._report_key = None
|
||||
self._reported = None
|
||||
self.status_queue = status_queue
|
||||
self._execute_on_stop = False
|
||||
self._post_eval = None
|
||||
self._expected_result = None
|
||||
self._no_fail = None
|
||||
self._is_stopped = False
|
||||
self._is_running = False
|
||||
self._is_breakpoint = False
|
||||
self._is_paused = False
|
||||
self._stop_on_failure = False
|
||||
self._doc = ""
|
||||
self._name = ""
|
||||
self.report = None
|
||||
self._dict_item = self._filter_dict_item(dict_item)
|
||||
self._seq_filename = filename
|
||||
|
||||
if parent is not None:
|
||||
parent.addChild(self)
|
||||
|
||||
if dict_item is not None:
|
||||
# creation of the params object
|
||||
self._prms = TestItemParams(dict_item, parent)
|
||||
|
||||
# getting parameters for the test item
|
||||
try:
|
||||
self._name = self._prms.getParam("name", default="", processed=True)
|
||||
# robustness if "name:" followed by an empty string in the yaml.
|
||||
if self._name == None:
|
||||
self._name = ""
|
||||
s = self._prms.getParam("skipped", default=None, processed=True)
|
||||
if s:
|
||||
try:
|
||||
self.skipped = eval_to_boolean(s)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"'{self.cmd()}' test item named '{self.name()}':\nskipped expresion can only be a static expression as it is evaluated during loading of TUM : {s}",
|
||||
self.seqFilename(),
|
||||
)
|
||||
# This allow disabling test item directly by using its name inside param.xml file
|
||||
elif self._name in tm.gd("skipped_test_item", []):
|
||||
self.skipped = True
|
||||
else:
|
||||
self.skipped = False
|
||||
|
||||
self._report_key = self._prms.getParam("key", default=None)
|
||||
self._stop_on_failure = self._prms.getParam(
|
||||
"stop_on_failure", default=False, processed=True
|
||||
)
|
||||
self._doc = self._prms.getParam("doc", default="", processed=True)
|
||||
#
|
||||
self._execute_on_stop = self._prms.getParam(
|
||||
"execute_on_stop", default=False, processed=True
|
||||
)
|
||||
|
||||
if "process_result" in dict_item:
|
||||
self._post_eval = dict_item["process_result"]
|
||||
|
||||
if "expected_result" in dict_item:
|
||||
self._expected_result = dict_item["expected_result"]
|
||||
|
||||
if "no_fail" in dict_item:
|
||||
self._no_fail = dict_item["no_fail"]
|
||||
|
||||
self.banner = LOG_TEST_START.format(self._name)
|
||||
self.footer = LOG_TEST_STOP.format(self._name)
|
||||
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
self.result = TestResult(self, TestValue.FAILURE, "Failure by default")
|
||||
|
||||
def _filter_dict_item(self, dict_item):
|
||||
# Stores the content of the step to be displayed
|
||||
# in the GUI
|
||||
c = {}
|
||||
if isinstance(dict_item, dict):
|
||||
for k, v in dict_item.items():
|
||||
if k == "steps" or k == "name" or k == "doc" or k == "seq_filename":
|
||||
continue
|
||||
if isinstance(v, (list, dict)):
|
||||
val = deepcopy(v)
|
||||
else:
|
||||
val = v
|
||||
c.update({k: val})
|
||||
else:
|
||||
c = str(dict_item)
|
||||
|
||||
return c
|
||||
|
||||
# default behavior... must be overloaded by children
|
||||
# this is mostly used by root item
|
||||
@test_run
|
||||
def execute(self):
|
||||
test_results = []
|
||||
i = 0
|
||||
to_be_stopped = False
|
||||
while (not self.isStopped()) and (i < self.childCount()) and not to_be_stopped:
|
||||
test_res = self.child(i).execute()
|
||||
test_results.append(test_res)
|
||||
i = i + 1
|
||||
if test_res.test_result == TestValue.FAILURE and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
if self.isStopped() or to_be_stopped:
|
||||
for j in range(self.childCount()):
|
||||
if self.child(j).executedOnStop() and (j >= i):
|
||||
self.child(j).execute()
|
||||
if to_be_stopped:
|
||||
self.result.set(TestValue.FAILURE, "test stopped on failure")
|
||||
else:
|
||||
test_success = TestValue.SUCCESS
|
||||
for res in test_results:
|
||||
if res.test_result != TestValue.SUCCESS:
|
||||
test_success = TestValue.FAILURE
|
||||
break
|
||||
self.result.test_result = test_success
|
||||
else:
|
||||
test_success = TestValue.SUCCESS
|
||||
for res in test_results:
|
||||
if res.test_result != TestValue.SUCCESS:
|
||||
test_success = TestValue.FAILURE
|
||||
break
|
||||
self.result.test_result = test_success
|
||||
self.result.message = "Test run failed"
|
||||
|
||||
def write_banner(self):
|
||||
if self.parent() is not None:
|
||||
s = self.banner
|
||||
s = (s + "{:>" + str(max(1, 80 - len(s))) + "}").format(
|
||||
str("@@{}@@".format(self.t0))
|
||||
)
|
||||
print(s)
|
||||
|
||||
def write_footer(self):
|
||||
if self.parent() is not None:
|
||||
print(self.result.message)
|
||||
print(self.footer + f": {str(self.result.test_result)}")
|
||||
|
||||
def run_test_init(self):
|
||||
"""Common test items execution initialization."""
|
||||
self.t0 = tm.timestamp()
|
||||
if self._name != "":
|
||||
tm.setgd("ts_start_" + self._name, self.t0)
|
||||
self.duration = -1
|
||||
self.write_banner()
|
||||
self._is_running = True
|
||||
self._sendStatusStarted()
|
||||
if self._is_breakpoint:
|
||||
self._is_paused = True
|
||||
while self._is_paused:
|
||||
sleep(0.2)
|
||||
|
||||
if self.is_container:
|
||||
self.report.incLevel()
|
||||
|
||||
self._reported = self._prms.getParam("report", default=None, processed=False)
|
||||
|
||||
def run_before_test(self):
|
||||
"""Peace of code executed just before the test is
|
||||
executed.
|
||||
"""
|
||||
pass
|
||||
|
||||
def run_test_end(self):
|
||||
"""Common test items execution closure."""
|
||||
if self.is_container:
|
||||
self.report.decLevel()
|
||||
|
||||
while self._is_paused:
|
||||
sleep(0.2)
|
||||
|
||||
# Post evaluation of the test result
|
||||
self.process_result()
|
||||
# expected_result treatment
|
||||
self.result_expected()
|
||||
# Case of the no_fail true parameter
|
||||
self.process_no_fail()
|
||||
|
||||
self.result.sendStatus(self.status_queue)
|
||||
if not self.result.value is None:
|
||||
tm.setgd("last_test_result", str(self.result.value))
|
||||
else:
|
||||
tm.setgd("last_test_result", str(self.result.test_result))
|
||||
self.write_footer()
|
||||
self._is_running = False
|
||||
self._is_stopped = False
|
||||
self.t1 = tm.timestamp()
|
||||
self.duration = self.t1 - self.t0
|
||||
if self._name != "":
|
||||
tm.setgd("ts_end_" + self._name, self.t1)
|
||||
tm.setgd("ts_duration_" + self._name, tm.timestamp_as_sec(self.duration))
|
||||
rk = self._prms.expanse(self._report_key)
|
||||
|
||||
# Report value export
|
||||
if hasattr(self.report, "value") and self.report.value is not None:
|
||||
self.result.reported = {"result": self.report.value}
|
||||
|
||||
if not self._reported is None:
|
||||
self.process_report(self._reported)
|
||||
self.report.addTest(self, self.result, rk)
|
||||
self._sendStatusFinished()
|
||||
|
||||
def process_result(self):
|
||||
if self._post_eval is None:
|
||||
return
|
||||
print(f"Post-processed the test result:")
|
||||
r = self.result.value
|
||||
pe = self._prms.expanse(self._post_eval)
|
||||
try:
|
||||
self.result.value = self.post_evaluate(pe)
|
||||
print(f" was: {r}")
|
||||
print(f" is: {str(self.result.value)}")
|
||||
except Exception as e:
|
||||
print(" Result processing failed!")
|
||||
print(e)
|
||||
self.result.set(TestValue.FAILURE, "Result processing failed")
|
||||
|
||||
if isinstance(self.result.value, bool):
|
||||
if self.result.value:
|
||||
self.result.set(TestValue.SUCCESS, "Processing result returned 'True'")
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, "Processing result returned 'False'")
|
||||
|
||||
def process_report(self, report_eval):
|
||||
tm.print_debug(f"Export reported values:")
|
||||
rep_eval = self._prms.expanse(report_eval)
|
||||
if isinstance(rep_eval, dict):
|
||||
self.result.reported = rep_eval
|
||||
if tm.debug_enabled():
|
||||
for k, v in rep_eval.items():
|
||||
tm.print_debug(f" {k}: {v}")
|
||||
else:
|
||||
tm.print_debug(" Failed: the reported value must be a dictionnary.")
|
||||
|
||||
def result_expected(self):
|
||||
res = self.result.value
|
||||
|
||||
# if a result is expected
|
||||
e = None
|
||||
eres = None
|
||||
|
||||
if not self._expected_result is None:
|
||||
e = self._prms.expanse(self._expected_result)
|
||||
_, eres = evaluate(e)
|
||||
|
||||
if not eres is None:
|
||||
if not res is None:
|
||||
print("Compare the result to expected:")
|
||||
print(" Result = " + str(res))
|
||||
msg = " Expected = " + str(self._expected_result)
|
||||
if self._expected_result != eres:
|
||||
msg = msg + " -> " + str(eres)
|
||||
print(msg)
|
||||
self.result.reported = {"expected": eres}
|
||||
|
||||
if eres == res:
|
||||
self.result.set(TestValue.SUCCESS, f"Expected result met.")
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, f"Expected result not met.")
|
||||
else:
|
||||
if str(eres).lower() != str(self.result.test_result).lower():
|
||||
self.result.set(
|
||||
TestValue.FAILURE, "Expected result not met : {}.".format(e)
|
||||
)
|
||||
else:
|
||||
self.result.set(
|
||||
TestValue.SUCCESS, "Expected result met: {}.".format(e)
|
||||
)
|
||||
|
||||
def process_no_fail(self):
|
||||
# Treatment of the no_fail parameters
|
||||
if self._no_fail is None:
|
||||
return
|
||||
|
||||
no_fail = False
|
||||
no_fail_exp = self._prms.expanse(self._no_fail)
|
||||
try:
|
||||
no_fail = bool(no_fail_exp)
|
||||
except:
|
||||
tm.print_debug(
|
||||
f"The 'no_fail' parameter evaluation did not lead to a boolean value: '{no_fail}'"
|
||||
)
|
||||
tm.print_warn(
|
||||
"The 'no_fail' parameter is ignored due to evaluation error."
|
||||
)
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a 'no_fail' parameter impossible to evaluate",
|
||||
self.seqFilename(),
|
||||
)
|
||||
if no_fail:
|
||||
if self.result.test_result == TestValue.FAILURE:
|
||||
tm.print_info(f"'no_fail' is True. Test forced to PASS.")
|
||||
self.result.test_result = TestValue.SUCCESS
|
||||
|
||||
def post_evaluate(self, post_eval):
|
||||
res = self.result.value
|
||||
if self.result.value is None:
|
||||
res = self.result.test_result
|
||||
return post_evaluate(post_eval, res)
|
||||
|
||||
def doc(self) -> str:
|
||||
return self._doc
|
||||
|
||||
def _sendStatusStarted(self):
|
||||
status = {
|
||||
"id": self._id,
|
||||
"name": self._name,
|
||||
"status": "started",
|
||||
"timestamp": self.t0,
|
||||
}
|
||||
self.status_queue.put(status)
|
||||
|
||||
def _sendStatusFinished(self):
|
||||
status = {
|
||||
"id": self._id,
|
||||
"name": self._name,
|
||||
"status": "finished",
|
||||
"duration": self.duration,
|
||||
}
|
||||
self.status_queue.put(status)
|
||||
|
||||
def sendMessage(self, msg):
|
||||
status = {"id": self._id, "name": self._name, "message": msg}
|
||||
self.status_queue.put(status)
|
||||
|
||||
def isRunning(self):
|
||||
return self._is_running
|
||||
|
||||
def isStopped(self):
|
||||
return self._is_stopped
|
||||
|
||||
def stop(self):
|
||||
self._is_stopped = True
|
||||
|
||||
def pause(self):
|
||||
self._is_paused = True
|
||||
|
||||
def addBreakpoint(self):
|
||||
self._is_breakpoint = True
|
||||
|
||||
def delBreakpoint(self):
|
||||
self._is_breakpoint = False
|
||||
|
||||
def cont(self):
|
||||
self._is_paused = False
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def content(self):
|
||||
ret = (
|
||||
yaml.dump(
|
||||
{self.cmd(): self._dict_item}, allow_unicode=True, sort_keys=False
|
||||
)
|
||||
if len(self._dict_item) != 0
|
||||
else ""
|
||||
)
|
||||
return ret
|
||||
|
||||
def type(self):
|
||||
return self._type.item_name
|
||||
|
||||
def cmd(self):
|
||||
return self._type.item_cmd
|
||||
|
||||
def childCount(self):
|
||||
return len(self._children)
|
||||
|
||||
def setId(self, id):
|
||||
self._id = id
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def setEnabled(self):
|
||||
self.enabled = True
|
||||
|
||||
def executedOnStop(self):
|
||||
return self._execute_on_stop
|
||||
|
||||
def addChild(self, child):
|
||||
self._children.append(child)
|
||||
|
||||
def hasChildren(self):
|
||||
return self.childCount() > 0
|
||||
|
||||
def parent(self):
|
||||
return self._parent
|
||||
|
||||
def child(self, index):
|
||||
return self._children[index]
|
||||
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def setSeqFilename(self, filename):
|
||||
self._seq_filename = filename
|
||||
|
||||
def seqFilename(self):
|
||||
return self._seq_filename
|
||||
59
src/testium/interpreter/test_items/test_item_check.py
Normal file
59
src/testium/interpreter/test_items/test_item_check.py
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import TestValue
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.eval import evaluate
|
||||
|
||||
class TestItemCheckValue(TestItem):
|
||||
"""check item usage.
|
||||
check usage:{check: {name: check my func output, steps: ['$(fn_echo) < 5']}}
|
||||
"""
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_CHECK.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_CHECK
|
||||
self.is_container = False
|
||||
try:
|
||||
self._action_list = self._prms.getParamAll('steps', default=[], required=False)
|
||||
if len(self._action_list) > 0:
|
||||
tm.print_warn("'steps' argument of check test item is deprecated and is replaced by 'values'")
|
||||
self._action_list += self._prms.getParamAll('values', default=[], required=False)
|
||||
if len(self._action_list) <= 0:
|
||||
raise ETUMSyntaxError(
|
||||
f" The '{self.cmd()}' test item named '{self.name()}' must have a 'values' parameter",
|
||||
self.seqFilename()
|
||||
)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' (a child of: '{self.parent().name()}') has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
if isinstance(self._action_list, str):
|
||||
self._action_list = [self._action_list]
|
||||
|
||||
is_success = True
|
||||
#test core function
|
||||
for v in self._action_list:
|
||||
val = self._prms.expanse(v)
|
||||
is_evaluated, ev = evaluate(val)
|
||||
if not is_evaluated:
|
||||
self.result.set(TestValue.FAILURE, "Error evaluating: '{}'".format(val))
|
||||
return
|
||||
|
||||
if not isinstance(ev, bool):
|
||||
self.result.set(TestValue.FAILURE, "The check of '{}' must result in a boolean: ".format(v))
|
||||
return
|
||||
|
||||
print("Evaluation of '{}' --> '{}' is {}.".format(v, val, str(ev)))
|
||||
if not ev:
|
||||
is_success = False
|
||||
|
||||
if is_success:
|
||||
self.result.set(TestValue.SUCCESS, 'Check passed')
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, 'Check failed')
|
||||
@@ -0,0 +1,50 @@
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import TestItem, test_run
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
from interpreter.test_items.dialog_choices_files import choices_dialog
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
|
||||
|
||||
class TestItemChoicesDialog(TestItem):
|
||||
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_CHOICES_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_CHOICES_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam("question", required=True)
|
||||
self._choices = self._prms.getParam("choices", required=True)
|
||||
self._default_icon = self._prms.getParam(
|
||||
"icon", required=False, default=None
|
||||
)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' (a child of: '{self.parent().name()}') has a missing or wrong parameter",
|
||||
self.seqFilename()
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
q = self._prms.expanse(self._question)
|
||||
choices = self._prms.expanse(self._choices)
|
||||
icon = self._prms.expanse(self._default_icon)
|
||||
parent_conn, child_conn = Pipe()
|
||||
p = Process(
|
||||
target=choices_dialog.main, args=([self.name(), q, choices, icon], child_conn)
|
||||
)
|
||||
p.start()
|
||||
val, succ = parent_conn.recv()
|
||||
p.join()
|
||||
|
||||
self.result.value = val
|
||||
|
||||
if succ:
|
||||
# The result of the test item is put into the global dict
|
||||
tm.setgd("cs_" + self._name, val)
|
||||
self.result.set(TestValue.SUCCESS, str(val))
|
||||
else:
|
||||
tm.delgd("cs_" + self._name)
|
||||
self.result.set(TestValue.FAILURE, str(val))
|
||||
361
src/testium/interpreter/test_items/test_item_console.py
Normal file
361
src/testium/interpreter/test_items/test_item_console.py
Normal file
@@ -0,0 +1,361 @@
|
||||
import sys
|
||||
import os
|
||||
import importlib
|
||||
import traceback
|
||||
|
||||
from libs import testium as tm
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.stdout_redirect import stdio_redir
|
||||
from interpreter.test_items.test_item import test_run
|
||||
from interpreter.test_items.item_actions import TestItemActions
|
||||
from interpreter.test_items.item_actions.action import TestItemAction
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
|
||||
|
||||
class TestItemConsoleAction(TestItemAction):
|
||||
|
||||
def get_console(self):
|
||||
cname = self._prms.expanse(self.token["console_name"])
|
||||
return tm.console(cname)
|
||||
|
||||
|
||||
class TestItemConsoleOpen(TestItemConsoleAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_CONSOLE_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
self._protocol = self._prms.getParam("protocol", required=True)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
self._protocol = self._prms.expanse(self._protocol)
|
||||
if not (self._protocol in ["telnet", "ssh", "rawtcp", "serial", "terminal"]):
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
'"protocol" can only be "telnet", "ssh", "rawtcp", "serial" or "terminal"',
|
||||
)
|
||||
return
|
||||
|
||||
cname = self._prms.expanse(self.token["console_name"])
|
||||
write_delay = (
|
||||
self._prms.getParam("write_delay", default=0, processed=True) / 1000.0
|
||||
)
|
||||
log = self._prms.getParam("log", processed=True)
|
||||
erase_log = self._prms.getParam("overwrite_log", default=True, processed=True)
|
||||
|
||||
if self._protocol == "telnet":
|
||||
telnet_host = self._prms.getParam(
|
||||
"telnet_host", required=True, processed=True
|
||||
)
|
||||
telnet_port = self._prms.getParam("telnet_port", default=69)
|
||||
|
||||
elif self._protocol == "ssh":
|
||||
if sys.platform.startswith("win"):
|
||||
self.result.set(
|
||||
TestValue.FAILURE, "SSH protocol not supported on Windows"
|
||||
)
|
||||
return
|
||||
ssh_host = self._prms.getParam("ssh_host", required=True, processed=True)
|
||||
ssh_user = self._prms.getParam("ssh_user", required=True, processed=True)
|
||||
ssh_pwd = self._prms.getParam(
|
||||
"ssh_pwd", required=False, default=None, processed=True
|
||||
)
|
||||
|
||||
elif self._protocol == "rawtcp":
|
||||
rawtcp_host = self._prms.getParam("tcp_host", required=True, processed=True)
|
||||
rawtcp_port = self._prms.getParam("tcp_port", required=True, processed=True)
|
||||
|
||||
elif self._protocol == "serial":
|
||||
serial_port = self._prms.getParam(
|
||||
"serial_port", required=True, processed=True
|
||||
)
|
||||
serial_bauds = self._prms.getParam(
|
||||
"serial_baudrate", required=True, processed=True
|
||||
)
|
||||
buffered = self._prms.getParam(
|
||||
"buffered", default=True, required=False, processed=True
|
||||
)
|
||||
|
||||
else:
|
||||
terminal_path = self._prms.getParam("terminal_path", processed=True)
|
||||
if terminal_path is not None:
|
||||
terminal_path = os.path.normpath(terminal_path)
|
||||
terminal_shell = self._prms.getParam(
|
||||
"shell", default="/usr/bin/env bash", required=False, processed=True
|
||||
)
|
||||
|
||||
try:
|
||||
if self._protocol == "telnet":
|
||||
if log:
|
||||
cons = console.TelnetLoggedConsole(
|
||||
name=cname,
|
||||
host=telnet_host,
|
||||
port=telnet_port,
|
||||
overwriteFile=erase_log,
|
||||
logPath=log,
|
||||
write_delay=write_delay,
|
||||
)
|
||||
else:
|
||||
cons = console.TelnetConsole(
|
||||
name=cname,
|
||||
host=telnet_host,
|
||||
port=telnet_port,
|
||||
write_delay=write_delay,
|
||||
)
|
||||
|
||||
elif self._protocol == "ssh":
|
||||
if sys.platform.startswith("win"):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' does not support SSH protocol on Windows",
|
||||
self.seqFilename()
|
||||
)
|
||||
if log:
|
||||
tm.print_warn(
|
||||
f"Warning : For '{self.cmd()}' test item named '{self.name()}', logging of {self._protocol} is not yet supported"
|
||||
)
|
||||
cons = console_ssh.SshConsole(
|
||||
name=cname,
|
||||
host=ssh_host,
|
||||
user=ssh_user,
|
||||
password=ssh_pwd,
|
||||
echoOn=True,
|
||||
)
|
||||
|
||||
elif self._protocol == "rawtcp":
|
||||
if log:
|
||||
tm.print_warn(
|
||||
"Warning : logging of {} is not yet supported".format(
|
||||
self._protocol
|
||||
)
|
||||
)
|
||||
cons = raw_tcp_console.RawTCPConsole(
|
||||
name=cname,
|
||||
address=rawtcp_host,
|
||||
port=rawtcp_port,
|
||||
echoOn=True,
|
||||
write_delay=write_delay,
|
||||
)
|
||||
|
||||
elif self._protocol == "serial":
|
||||
if log:
|
||||
cons = console.SerialLoggedConsole(
|
||||
name=cname,
|
||||
baudrate=serial_bauds,
|
||||
port=serial_port,
|
||||
overwriteFile=erase_log,
|
||||
logPath=log,
|
||||
echoOn=False,
|
||||
write_delay=write_delay,
|
||||
)
|
||||
else:
|
||||
cons = console.SerialConsole(
|
||||
name=cname,
|
||||
baudrate=int(serial_bauds),
|
||||
port=serial_port,
|
||||
bufferize=bool(buffered),
|
||||
echoOn=False,
|
||||
write_delay=write_delay,
|
||||
)
|
||||
|
||||
else:
|
||||
if log:
|
||||
print(
|
||||
"Warning : logging of {} is not yet supported".format(
|
||||
self._protocol
|
||||
)
|
||||
)
|
||||
if terminal_path and not os.path.exists(terminal_path):
|
||||
raise ETUMSyntaxError(
|
||||
f"'{self.cmd()}' test item named '{self.name()}' (console '{cname}'): terminal path is not mandatory but must exist when provided: {terminal_path}",
|
||||
self.seqFilename()
|
||||
)
|
||||
cons = termconsole.TermConsole(
|
||||
name=cname,
|
||||
project_path=terminal_path,
|
||||
cust_shell=terminal_shell,
|
||||
echoOn=True,
|
||||
write_delay=write_delay,
|
||||
)
|
||||
|
||||
cons.stream = stdio_redir.stream
|
||||
# record the console instance in the global dict as consolename instance
|
||||
# and consolename key entry in the dictionnary if it exists
|
||||
tm.add_console(cons)
|
||||
cons.open()
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
except Exception as e:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message="Impossible to open the console ({}) (exception: {})".format(
|
||||
cname, e
|
||||
),
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
|
||||
|
||||
class TestItemConsoleClose(TestItemConsoleAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_CONSOLE_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
cons = self.get_console()
|
||||
try:
|
||||
cons.close()
|
||||
tm.remove_console(self._prms.expanse(self.token["console_name"]))
|
||||
except:
|
||||
pass
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemConsoleWrite(TestItemConsoleAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_CONSOLE_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
try:
|
||||
msg = self._prms.expanse(self._prms.getData())
|
||||
cons = self.get_console()
|
||||
cons.write(str(msg))
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
self.result.reported = {"data": msg}
|
||||
except:
|
||||
test_res = TestResult(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Console '{self.token['console_name']}': impossible to write",
|
||||
)
|
||||
|
||||
|
||||
class TestItemConsoleWriteLn(TestItemConsoleAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_CONSOLE_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
try:
|
||||
msg = self._prms.expanse(self._prms.getData())
|
||||
cons = self.get_console()
|
||||
cons.write(str(msg) + "\n")
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
self.result.reported = {"data": msg}
|
||||
except:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Console '{self.token['console_name']}': impossible to write",
|
||||
)
|
||||
|
||||
|
||||
class TestItemConsoleReadUntil(TestItemConsoleAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_CONSOLE_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
self._read_until = self._prms.getParam("expected", required=True)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
cons = self.get_console()
|
||||
ru = self._prms.expanse(self._read_until)
|
||||
read_timeout = int(self._prms.getParam("timeout", default=-1, processed=True))
|
||||
mute = self._prms.getParam("mute", default=False, processed=True)
|
||||
if read_timeout < 0:
|
||||
read_timeout = None
|
||||
|
||||
try:
|
||||
status, data = cons.read_until(
|
||||
ru, timeout=read_timeout, return_data=True, mute=mute
|
||||
)
|
||||
if status == 0:
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
self.result.value = data
|
||||
else:
|
||||
self.result.set(result=TestValue.FAILURE, message="No matching text")
|
||||
if mute:
|
||||
self.result.reported = {"data": ""}
|
||||
else:
|
||||
self.result.reported = {"data": data}
|
||||
# The result is put in global dir
|
||||
tm.setgd("cn_" + self.parent()._name, data)
|
||||
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Console '{self.token['console_name']}': impossible to read",
|
||||
)
|
||||
|
||||
|
||||
class TestItemConsole(TestItemActions):
|
||||
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
|
||||
super().__init__(
|
||||
cst.TYPE_CONSOLE, dict_item, parent, status_queue, filename=filename
|
||||
)
|
||||
|
||||
self.register_actions(
|
||||
open=TestItemConsoleOpen,
|
||||
close=TestItemConsoleClose,
|
||||
write=TestItemConsoleWrite,
|
||||
writeln=TestItemConsoleWriteLn,
|
||||
read_until=TestItemConsoleReadUntil,
|
||||
)
|
||||
self.actions_token = {}
|
||||
|
||||
global console
|
||||
console = importlib.import_module("libs.console")
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
global console_ssh
|
||||
console_ssh = importlib.import_module("libs.console_ssh")
|
||||
|
||||
global termconsole
|
||||
termconsole = importlib.import_module("libs.termconsole")
|
||||
|
||||
global raw_tcp_console
|
||||
raw_tcp_console = importlib.import_module("libs.raw_tcp_console")
|
||||
|
||||
self.actions_token["console_name"] = self._prms.getParam(
|
||||
"console_name", required=True
|
||||
)
|
||||
263
src/testium/interpreter/test_items/test_item_cycle.py
Normal file
263
src/testium/interpreter/test_items/test_item_cycle.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import traceback
|
||||
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||
from interpreter.utils.func_exec import func_exec
|
||||
from interpreter.test_items.test_item import TestItem, test_run
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.params import TestItemParams
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.eval import evaluate
|
||||
|
||||
|
||||
class TestItemCycle(TestItem):
|
||||
def __init__(self, dict_cycle, parent=None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_CYCLE.item_name
|
||||
super().__init__(dict_cycle, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_CYCLE
|
||||
self.is_container = True
|
||||
self._exit_file = None
|
||||
self._exit_func = None
|
||||
self._exit_time = None
|
||||
self._exit_condition = None
|
||||
self._start_time = None
|
||||
self._niter = None
|
||||
|
||||
if "iterator" in dict_cycle:
|
||||
self._iter = dict_cycle["iterator"]
|
||||
|
||||
if isinstance(self._iter, str):
|
||||
self._iter = self._prms.expanse(self._iter)
|
||||
|
||||
else:
|
||||
self._iter = None
|
||||
|
||||
if "exit_condition" in dict_cycle:
|
||||
if not isinstance(dict_cycle["exit_condition"], dict):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has an error in its exit condition",
|
||||
self.seqFilename()
|
||||
)
|
||||
|
||||
exit_params = TestItemParams(dict_cycle["exit_condition"], self._parent)
|
||||
self._exit_time = exit_params.getParam("time", processed=False)
|
||||
self._exit_condition = exit_params.getParam("value", processed=False)
|
||||
|
||||
req = False
|
||||
if (self._exit_time is None) and (self._exit_condition is None):
|
||||
req = True
|
||||
|
||||
self._exit_file = exit_params.getParam("file", required=req)
|
||||
self._exit_func = exit_params.getParam("func_name", required=req)
|
||||
self._exit_func_param = exit_params.getParam("param")
|
||||
self._exit_eval = exit_params.getParam("eval", default="")
|
||||
|
||||
def __runALoop(self):
|
||||
failcount = 0
|
||||
i = 0
|
||||
to_be_stopped = False
|
||||
while (
|
||||
(not self.isStopped()) and (i < self.childCount()) and (not to_be_stopped)
|
||||
):
|
||||
result = self.child(i).execute()
|
||||
if result.test_result == TestValue.FAILURE:
|
||||
failcount = failcount + 1
|
||||
if self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
i = i + 1
|
||||
|
||||
if self.isStopped() or to_be_stopped:
|
||||
for j in range(self.childCount()):
|
||||
if self.child(j).executedOnStop() and (j >= i):
|
||||
self.child(j).execute()
|
||||
|
||||
test_success = TestValue.SUCCESS
|
||||
if failcount > 0:
|
||||
test_success = TestValue.FAILURE
|
||||
|
||||
result = TestResult(None, test_success, "Cycle iteration")
|
||||
return result
|
||||
|
||||
def nbLoops(self, iter):
|
||||
if iter is None:
|
||||
# infinite number of loop
|
||||
self._niter = float("inf")
|
||||
elif isinstance(iter, int):
|
||||
self._niter = iter
|
||||
else:
|
||||
self._niter = len(iter)
|
||||
return self._niter
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
failcount = 0
|
||||
iter = self._iter
|
||||
if iter is not None:
|
||||
if isinstance(iter, str):
|
||||
iter = self._prms.expanse(iter)
|
||||
|
||||
if not isinstance(iter, (list, tuple, int)):
|
||||
_, iter = evaluate(iter)
|
||||
if not isinstance(iter, (list, tuple, int)):
|
||||
self.result.set(TestValue.FAILURE, f"unrecognized type for iterator '{str(iter)}'")
|
||||
return
|
||||
if not isinstance(iter, int):
|
||||
r = []
|
||||
for i in iter:
|
||||
r.append(self._prms.expanse(i))
|
||||
iter = r
|
||||
|
||||
|
||||
# test core function
|
||||
self._niter = self.nbLoops(iter)
|
||||
i = 1
|
||||
to_be_stopped = False
|
||||
self._start_time = tm.timestamp_as_sec()
|
||||
self.result.set(TestValue.SUCCESS, "Initial cycle setup")
|
||||
while (i <= self._niter) and (not self.isStopped()) and (not to_be_stopped):
|
||||
try:
|
||||
msg = ""
|
||||
if isinstance(iter, int) or iter is None:
|
||||
msg = "{}/{}".format(i, self._niter)
|
||||
self.sendMessage("Cycle " + msg)
|
||||
msg = 'Cycle "' + self._name + '" iteration ' + msg + "."
|
||||
else:
|
||||
msg = '{}/{} - Current: "{}"'.format(i, self._niter, str(iter[i - 1]))
|
||||
self.sendMessage("Cycle " + msg)
|
||||
msg = 'Cycle "' + self._name + '" iteration ' + msg + "."
|
||||
print(msg)
|
||||
|
||||
# store the current loop params
|
||||
self._currentIter = i - 1
|
||||
self._currentInverseIter = self._niter - i - 1
|
||||
if isinstance(iter, int) or iter is None:
|
||||
self._currentLoop = i
|
||||
else:
|
||||
self._currentLoop = iter[i - 1]
|
||||
# Cycle loop execution
|
||||
res_loop = self.__runALoop()
|
||||
|
||||
if not res_loop.success:
|
||||
failcount = failcount + 1
|
||||
self.result.set(
|
||||
TestValue.FAILURE, "(Cycle {}/{})".format(i - 1, self._niter)
|
||||
)
|
||||
# Cycle time exit condition check
|
||||
if res_loop.success or (
|
||||
(not res_loop.success) and (not self._stop_on_failure)
|
||||
):
|
||||
if self._exit_time is not None:
|
||||
ela = tm.timestamp_as_sec()
|
||||
etime = self._prms.expanse(self._exit_time)
|
||||
if (ela - self._start_time) > float(etime) * 60:
|
||||
self.result.reported = {
|
||||
"exit": "time elapsed",
|
||||
"timeout": etime,
|
||||
"elapsed": (ela - self._start_time) / 60,
|
||||
"count": self._currentIter,
|
||||
}
|
||||
print(
|
||||
"Exiting loop: {:.1f} minutes elapsed (defined: {}).".format(
|
||||
(ela - self._start_time) / 60, etime
|
||||
)
|
||||
)
|
||||
break
|
||||
else:
|
||||
print(
|
||||
"loop: {:.1f} minutes elapsed (exiting when > {}).".format(
|
||||
(ela - self._start_time) / 60, etime
|
||||
)
|
||||
)
|
||||
|
||||
# Cycle value exit condition check
|
||||
if self._exit_condition is not None:
|
||||
exit_val = self._prms.expanse(
|
||||
self._exit_condition
|
||||
)
|
||||
_, exit_val = evaluate(exit_val)
|
||||
if exit_val:
|
||||
# exit condition is True
|
||||
self.result.reported = {
|
||||
"exit": "condition",
|
||||
"condition": self._exit_condition,
|
||||
"count": self._currentIter,
|
||||
}
|
||||
print(
|
||||
'Exiting loop: "{}" is True.'.format(
|
||||
self._exit_condition
|
||||
)
|
||||
)
|
||||
break
|
||||
else:
|
||||
print(
|
||||
'Continuing. Condition "{}" not met.'.format(
|
||||
self._exit_condition
|
||||
)
|
||||
)
|
||||
|
||||
if self._exit_func:
|
||||
file = self._prms.expanse(self._exit_file)
|
||||
func = self._prms.expanse(self._exit_func)
|
||||
post_eval = self._prms.expanse(self._exit_eval)
|
||||
if self._exit_func_param:
|
||||
param_list = self._prms.getParamFromList(self._exit_func_param)
|
||||
pl = self._prms.expanse(param_list)
|
||||
else:
|
||||
pl = [self._currentLoop]
|
||||
fsucc, res = func_exec(file, func, pl)
|
||||
if fsucc == TestValue.SUCCESS:
|
||||
fres, _ = res
|
||||
if fres:
|
||||
# function returned True
|
||||
self.result.reported = {
|
||||
"exit": "returned value",
|
||||
"returned": fres,
|
||||
"count": self._currentIter,
|
||||
}
|
||||
print("Exiting loop: exit function condition met.")
|
||||
break
|
||||
else:
|
||||
print("Exiting condition not met : \"{}\"".format(fres))
|
||||
else:
|
||||
raise ETUMRuntimeError(f"Loop exiting function failed: \"{res}\"")
|
||||
|
||||
if post_eval:
|
||||
print(f"Evaluation: \"{post_eval}\"")
|
||||
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
self.result.set(TestValue.FAILURE, "(Cycle {}/{})".format(i - 1, self._niter))
|
||||
to_be_stopped = True
|
||||
|
||||
if (self.result.test_result == TestValue.FAILURE) and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
i = i + 1
|
||||
|
||||
# end of loop test exit condition
|
||||
if self.isStopped() or to_be_stopped:
|
||||
if to_be_stopped:
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
"(Cycle {}/{}) execution aborted on failure".format(i - 1, self._niter),
|
||||
)
|
||||
else:
|
||||
if self._exit_func:
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
"(Cycle {}/{}) execution aborted on user request".format(
|
||||
i - 1, self._niter
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.result.set(
|
||||
TestValue.SUCCESS, "(Cycle {}/{})".format(self._niter, self._niter)
|
||||
)
|
||||
if failcount > 0:
|
||||
self.result.set(
|
||||
TestValue.FAILURE, "(Cycle {}/{})".format(i - 1, self._niter)
|
||||
)
|
||||
|
||||
else:
|
||||
self.result.set(TestValue.SUCCESS, "(Cycle {}/{})".format(self._niter, self._niter))
|
||||
if failcount > 0:
|
||||
self.result.set(TestValue.FAILURE, "(Cycle {}/{})".format(i - 1, self._niter))
|
||||
77
src/testium/interpreter/test_items/test_item_func.py
Normal file
77
src/testium/interpreter/test_items/test_item_func.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pprint
|
||||
import textwrap
|
||||
|
||||
from interpreter.test_items.test_item import TestItem, test_run
|
||||
from interpreter.test_items.test_result import TestValue
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.func_exec import func_exec
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
|
||||
|
||||
class TestItemFunc(TestItem):
|
||||
"""py_func item usage.
|
||||
func file: func_file.py, func_name: func, param: [$(variable1), [1, 2, 3], true]
|
||||
"""
|
||||
|
||||
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_FUNCTION.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_FUNCTION
|
||||
self.is_container = False
|
||||
try:
|
||||
self.file_name = self._prms.getParam("file", required=True)
|
||||
self.func_name = self._prms.getParam("func_name", required=True)
|
||||
self.params = self._prms.getParamAll("param")
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' (child of '{self.parent.name()}') has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
self.result.set(
|
||||
TestValue.FAILURE, "an exception occured during function execution."
|
||||
)
|
||||
try:
|
||||
self.file_name = self._prms.expanse(self.file_name)
|
||||
self.func_name = self._prms.expanse(self.func_name)
|
||||
param_list = self._prms.getParamFromList(self.params)
|
||||
pl = self._prms.expanse(param_list)
|
||||
if tm.debug_enabled():
|
||||
tm.print_debug("Parameters list:")
|
||||
tm.print_debug(textwrap.indent(pprint.pformat(pl), " |"))
|
||||
success, ret = func_exec(self.file_name, self.func_name, pl)
|
||||
|
||||
if success == TestValue.SUCCESS:
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
res, reported_values = ret
|
||||
reported_values = {**reported_values, "returned": res}
|
||||
self.result.reported = ret[1]
|
||||
|
||||
if tm.debug_enabled():
|
||||
tm.print_debug("Returned value:")
|
||||
tm.print_debug(textwrap.indent(pprint.pformat(res), " |"))
|
||||
|
||||
# The result of the func test item is put in global dir and result
|
||||
tm.setgd("fn_" + self._name, res)
|
||||
self.result.value = res
|
||||
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, ret)
|
||||
if tm.debug_enabled():
|
||||
tm.print_debug("Failed:")
|
||||
tm.print_debug(textwrap.indent(pprint.pformat(ret), " |"))
|
||||
|
||||
return
|
||||
|
||||
except:
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
'Unrecoverable "py_func" item error from {}'.format(self.func_name),
|
||||
)
|
||||
37
src/testium/interpreter/test_items/test_item_git.py
Normal file
37
src/testium/interpreter/test_items/test_item_git.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.tum_except import ETUMParamError, ETUMSyntaxError
|
||||
import interpreter.utils.version as git
|
||||
|
||||
class TestItemGit(TestItem):
|
||||
"""
|
||||
This item expect only one parameter which is a string or list of string being the path to the git folder
|
||||
"""
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_GIT.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_GIT
|
||||
self.is_container = False
|
||||
self.repo = self._prms.getParamAll('repo', processed=True, required=True)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ret=''
|
||||
if isinstance(self.repo[0], str):
|
||||
repo = self._prms.expanse(self.repo[0])
|
||||
ret = git.get_version(repo)
|
||||
elif isinstance(self.repo, list):
|
||||
for r in self.repo:
|
||||
repo = self._prms.expanse(r)
|
||||
ret += git.get_version(repo) + '\n'
|
||||
else:
|
||||
ETUMSyntaxError(f"The '{self.cmd()}' test item named '{self.name()}' expected a string or list but has '{self.repo}'",
|
||||
self.seqFilename())
|
||||
|
||||
if "Warning" in ret:
|
||||
res = TestValue.FAILURE
|
||||
else:
|
||||
res = TestValue.SUCCESS
|
||||
|
||||
self.result.set(res, ret)
|
||||
62
src/testium/interpreter/test_items/test_item_group.py
Normal file
62
src/testium/interpreter/test_items/test_item_group.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
import libs.testium as tm
|
||||
|
||||
class TestItemGroup(TestItem):
|
||||
def __init__(self, dict_cycle, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_GROUP.item_name
|
||||
super().__init__(dict_cycle, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_GROUP
|
||||
self.is_container = True
|
||||
|
||||
def __runALoop(self):
|
||||
results = []
|
||||
i = 0
|
||||
to_be_stopped = False
|
||||
while (not self.isStopped()) and (i < self.childCount()) and (not to_be_stopped):
|
||||
result = self.child(i).execute()
|
||||
results.append(result)
|
||||
if result.test_result == TestValue.FAILURE and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
i = i + 1
|
||||
|
||||
if self.isStopped() or to_be_stopped:
|
||||
for j in range(self.childCount()):
|
||||
if self.child(j).executedOnStop() and (j >= i):
|
||||
self.child(j).execute()
|
||||
|
||||
test_success = TestValue.SUCCESS
|
||||
for res in results:
|
||||
if res.test_result == TestValue.FAILURE:
|
||||
test_success = TestValue.FAILURE
|
||||
break
|
||||
|
||||
result = TestResult(None, test_success, 'Group iteration')
|
||||
return result
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
results = []
|
||||
to_be_stopped = False
|
||||
if (not self.isStopped()) and (not to_be_stopped):
|
||||
result = self.__runALoop()
|
||||
|
||||
# Test results
|
||||
results.append(result)
|
||||
|
||||
if result.test_result == TestValue.FAILURE and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
|
||||
# end of loop test
|
||||
if self.isStopped() or to_be_stopped:
|
||||
if to_be_stopped:
|
||||
self.result.set(TestValue.FAILURE, 'Group execution aborted on failure')
|
||||
else:
|
||||
self.result.set(TestValue.NORUN, 'Group execution aborted on user request')
|
||||
else:
|
||||
self.result.set(TestValue.SUCCESS, '')
|
||||
for res in results:
|
||||
if not res.success:
|
||||
self.result.set(TestValue.FAILURE, '')
|
||||
71
src/testium/interpreter/test_items/test_item_image_dialog.py
Normal file
71
src/testium/interpreter/test_items/test_item_image_dialog.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import TestItem, test_run
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
from interpreter.test_items.dialog_image_files import dialog_image
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
|
||||
|
||||
class TestItemImageDialog(TestItem):
|
||||
"""dialog_image item usage.
|
||||
dialog_image name: Nice image, question: could you press the red button, filename: img.jpg
|
||||
"""
|
||||
|
||||
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_IMAGE_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_IMAGE_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam("question", required=True)
|
||||
self._filename = self._prms.getParam("filename", required=True)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ourpath = __file__
|
||||
test_file = os.path.join(
|
||||
os.path.dirname(ourpath), "dialog_image_files", "dialog_image.py"
|
||||
)
|
||||
|
||||
q = self._prms.expanse(self._question)
|
||||
image_path = self._prms.expanse(self._filename)
|
||||
print("Image Displayed:\n" + q + "\n" + image_path)
|
||||
if not os.path.isfile(image_path):
|
||||
image_path = os.path.normpath(
|
||||
os.path.join(tm.gd("test_directory"), image_path)
|
||||
)
|
||||
|
||||
parent_conn, child_conn = Pipe()
|
||||
p = Process(
|
||||
target=dialog_image.main, args=([self.name(), q, image_path], child_conn)
|
||||
)
|
||||
p.start()
|
||||
succ = parent_conn.recv()
|
||||
p.join()
|
||||
if succ:
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE)
|
||||
|
||||
|
||||
def mypath():
|
||||
if hasattr(sys, "frozen"):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
|
||||
from multiprocessing import Process
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = Process(target=test_dialog.main, args=(["bob", "bab"],))
|
||||
p.start()
|
||||
p.join()
|
||||
@@ -0,0 +1,246 @@
|
||||
import sys
|
||||
import traceback
|
||||
from functools import wraps
|
||||
from random import randint
|
||||
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.test_items.test_item import TestItem, test_run
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
|
||||
from interpreter.test_items.item_actions import TestItemActions
|
||||
from interpreter.test_items.item_actions.action import TestItemAction
|
||||
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.eval import evaluate
|
||||
|
||||
from interpreter.test_items.test_item_json_rpc.jsonrpc_adapters import (
|
||||
JrpcAdapter,
|
||||
JrpcConsoleAdapter,
|
||||
JrpcUdpAdapter,
|
||||
)
|
||||
|
||||
|
||||
class TestItemJSRPCActionOpen(TestItemAction):
|
||||
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_JSON_RPC_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
try:
|
||||
self.token.open()
|
||||
except Exception as e:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Error while performing the JSONRPC '{self._name}' action (exception: {e})",
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemJSRPCActionClose(TestItemAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_JSON_RPC_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
try:
|
||||
self.token.close()
|
||||
except Exception as e:
|
||||
test_res = TestResult(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Error while performing the JSONRPC '{self._name}' action (exception: {e})",
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemJSRPCActionQuery(TestItemAction):
|
||||
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_JSON_RPC_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
self._meth = self._prms.getParam("method", required=True)
|
||||
self._obj = self._prms.getParam("params", required=False)
|
||||
if self._obj is None:
|
||||
self._obj = list()
|
||||
self._jrpc_id = self._prms.getParam("id", required=False, default="rand")
|
||||
self._send_only = self._prms.getParam("no_wait", required=False, default=False)
|
||||
self._timeout = self._prms.getParam("timeout", required=False, default=None)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
meth = self._prms.expanse(self._meth)
|
||||
obj = self._prms.expanse(self._obj)
|
||||
jrpc_id = self._prms.expanse(self._jrpc_id)
|
||||
if isinstance(jrpc_id, str) and jrpc_id.lower().startswith("rand"):
|
||||
jrpc_id = randint(1, (2**32) - 1)
|
||||
send_only = self._prms.expanse(self._send_only)
|
||||
timeout = self._prms.expanse(self._timeout)
|
||||
try:
|
||||
success, result = self.token.query(
|
||||
meth, obj, jrpc_id, send_only, timeout=timeout
|
||||
)
|
||||
except Exception as e:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Error while performing the JSONRPC '{self._name}' action (exception: {e})",
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
# in case the action returned without error, we
|
||||
# set the test result value to the data returned by the action.
|
||||
if not self._send_only:
|
||||
self.result.value = result
|
||||
if self._send_only or success:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
else:
|
||||
self.result.set(result=TestValue.FAILURE, message=str(result))
|
||||
|
||||
|
||||
class TestItemJSRPCActionReceive(TestItemAction):
|
||||
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_JSON_RPC_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
self._timeout = self._prms.getParam("timeout", required=False, default=None)
|
||||
self._jrpc_id = self._prms.getParam("id", required=True)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
timeout = self._prms.expanse(self._timeout)
|
||||
jrpc_id = self._prms.expanse(self._jrpc_id)
|
||||
|
||||
try:
|
||||
success, result = self.token.receive(jrpc_id, timeout)
|
||||
except Exception as e:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message=f"Error while performing the JSONRPC '{self._name}' action (exception: {e})",
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
# in case the action returned without error, we
|
||||
# set the test result value to the data returned by the action.
|
||||
self.result.value = result
|
||||
if success:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
else:
|
||||
self.result.set(result=TestValue.FAILURE, message=str(result))
|
||||
|
||||
|
||||
class TestItemJSON_RPC(TestItemActions):
|
||||
"""
|
||||
This item TBD
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, dict_item: dict, parent: TestItem = None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
cst.TYPE_JSON_RPC, dict_item, parent, status_queue, filename=filename
|
||||
)
|
||||
|
||||
self.register_actions(
|
||||
open=TestItemJSRPCActionOpen,
|
||||
close=TestItemJSRPCActionClose,
|
||||
query=TestItemJSRPCActionQuery,
|
||||
receive=TestItemJSRPCActionReceive,
|
||||
)
|
||||
|
||||
# Console specific params
|
||||
self._console = self._prms.getParam("console", required=False)
|
||||
# UDP specific params
|
||||
self._udp = self._prms.getParam("udp", required=False)
|
||||
# Common params
|
||||
self._jrpc_version = self._prms.getParam(
|
||||
"version", required=False, default="1.0"
|
||||
)
|
||||
self._timeout = self._prms.getParam("timeout", required=True)
|
||||
self._mute = self._prms.getParam("mute", required=False, default=False)
|
||||
|
||||
if (self._console is None) and (self._udp is None):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' must have a 'console' or 'udp' parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
self._is_console = False
|
||||
if not self._console is None:
|
||||
self._is_console = True
|
||||
|
||||
def run_before_test(self):
|
||||
jrpc_version = self._prms.expanse(self._jrpc_version)
|
||||
mute = self._prms.expanse(self._mute)
|
||||
timeout = self._prms.expanse(self._timeout)
|
||||
if self._is_console:
|
||||
console = self._prms.expanse(self._console)
|
||||
console_name = console.get("name")
|
||||
console_prompt = console.get("prompt", "\n")
|
||||
if console_name is None:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' 'console' configuration needs member 'name' defined",
|
||||
self.seqFilename(),
|
||||
)
|
||||
jrpc_adapter = JrpcConsoleAdapter(
|
||||
console_name, console_prompt, timeout, jrpc_version, mute
|
||||
)
|
||||
else:
|
||||
udp = self._prms.expanse(self._udp)
|
||||
if udp is None or not isinstance(udp, dict):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' UDP configuration needs 'udp' parameters define",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
server = udp.get("server")
|
||||
snd_port = udp.get("snd_port")
|
||||
rcv_port = udp.get("rcv_port")
|
||||
bufsize = udp.get("bufsize", 1450)
|
||||
if server is None or snd_port is None or rcv_port is None:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' UDP configuration needs 'server', 'snd_port' and 'rcv_port' defined",
|
||||
self.seqFilename(),
|
||||
)
|
||||
jrpc_adapter = JrpcUdpAdapter(
|
||||
server, snd_port, rcv_port, bufsize, timeout, jrpc_version, mute
|
||||
)
|
||||
|
||||
self.actions_token = jrpc_adapter
|
||||
@@ -0,0 +1,355 @@
|
||||
import json
|
||||
import socket
|
||||
import re
|
||||
import struct
|
||||
|
||||
from interpreter.utils.tum_except import ETUMRuntimeError
|
||||
import libs.testium as tm
|
||||
from libs.console import Console
|
||||
|
||||
|
||||
def is_ip_address(address):
|
||||
ip_regex = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"
|
||||
return re.match(ip_regex, address) is not None
|
||||
|
||||
|
||||
def is_ip_multicast(ip):
|
||||
# convert the IP as an integer
|
||||
ip_int = struct.unpack("!I", socket.inet_aton(ip))[0]
|
||||
# Checks if it is in a multicast range
|
||||
return 0xE0000000 <= ip_int <= 0xEFFFFFFF
|
||||
|
||||
|
||||
def jrpc_query(version: str, method: str, obj, jrpc_id: int):
|
||||
req = {
|
||||
"1.0": {
|
||||
"method": method,
|
||||
},
|
||||
"2.0": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
},
|
||||
}
|
||||
if not version in ["1.0", "2.0"]:
|
||||
raise ETUMRuntimeError("JSONRPC frame creation with bad version value.")
|
||||
req = req[version]
|
||||
req["params"] = obj
|
||||
req["id"] = jrpc_id
|
||||
return json.dumps(req)
|
||||
|
||||
|
||||
class JrpcAdapter:
|
||||
"""Base class for defining a JSONRPC messages handler for the jsonrpc test item."""
|
||||
|
||||
def __init__(self, timeout: float = 1.0, version="1.0", mute=False) -> None:
|
||||
self._jrpc_version = version
|
||||
self._mute = mute
|
||||
self._timeout = timeout
|
||||
if not (version == "1.0" or version == "2.0"):
|
||||
raise ETUMRuntimeError("Invalid JSONRPC version passed.")
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self._timeout
|
||||
|
||||
def check_answer(self, obj, jrpc_id: int) -> None:
|
||||
if "1.0" == self._jrpc_version:
|
||||
if not ("error" in obj.keys()):
|
||||
raise ETUMRuntimeError(
|
||||
"Malformed JSONRPC 1.0 answer. 'error' required."
|
||||
)
|
||||
if not ("result" in obj.keys()):
|
||||
raise ETUMRuntimeError(
|
||||
"Malformed JSONRPC 1.0 answer. 'result' required."
|
||||
)
|
||||
|
||||
if obj["result"] is not None and obj["error"] is not None:
|
||||
raise ETUMRuntimeError(
|
||||
"Malformed JSONRPC 1.0 answer. If 'result' is not null, 'error' must be null."
|
||||
)
|
||||
|
||||
if not ("id" in obj.keys()):
|
||||
raise ETUMRuntimeError(
|
||||
"Malformed JSONRPC 1.0 answer. 'id' must be defined."
|
||||
)
|
||||
else:
|
||||
if "2.0" != obj.get("jsonrpc", ""):
|
||||
raise ETUMRuntimeError(
|
||||
"Malformed JSONRPC 2.0 answer. 'jsonrpc' required."
|
||||
)
|
||||
|
||||
is_error = True
|
||||
is_result = True
|
||||
if not ("error" in obj.keys()):
|
||||
is_error = False
|
||||
if not ("result" in obj.keys()):
|
||||
is_result = False
|
||||
|
||||
if not (is_error ^ is_result):
|
||||
raise ETUMRuntimeError(
|
||||
"Malformed JSONRPC 2.0 answer. 'result' and 'result' can't exist together."
|
||||
)
|
||||
|
||||
if not ("id" in obj.keys()):
|
||||
raise ETUMRuntimeError("The JSONRPC answer 'id' must be defined.")
|
||||
if obj["id"] != jrpc_id:
|
||||
raise ETUMRuntimeError(
|
||||
"The JSONRPC answer ID does not correspond to the request"
|
||||
)
|
||||
|
||||
def _build_query(self, method: str, obj, jrpc_id: int):
|
||||
return jrpc_query(self._jrpc_version, method, obj, jrpc_id)
|
||||
|
||||
def _send(self, message: str):
|
||||
pass
|
||||
|
||||
def _receive(self, timeout: float) -> str:
|
||||
pass
|
||||
|
||||
def _open(self):
|
||||
pass
|
||||
|
||||
def _close(self):
|
||||
pass
|
||||
|
||||
def query(
|
||||
self,
|
||||
method: str,
|
||||
obj,
|
||||
jrpc_id="rand",
|
||||
send_only: bool = False,
|
||||
timeout: float = None,
|
||||
):
|
||||
"""This performs a jsonrpc query to a jsonrpc server.
|
||||
The returned value is a tuple of size 2:
|
||||
success, data
|
||||
|
||||
if send_only is true, the function returns immediately after sending the request.
|
||||
None is returned.
|
||||
|
||||
if timeout is None:
|
||||
the inherited timeout is used.
|
||||
|
||||
if timeout <= 0:
|
||||
If the response does not come before the end of the timeout, it fails with an exception.
|
||||
if the id doesn't match, an exception is raised.
|
||||
success depends on content of the jsonrpc response.
|
||||
data is the error code if the success if false, otherwise it is the returned value.
|
||||
|
||||
if timeout > 0:
|
||||
If the response does not come before the end of the timeout, it fails with an exception.
|
||||
success depends on content of the jsonrpc response.
|
||||
data is the error code if the success if false, otherwise it is the returned value.
|
||||
"""
|
||||
tmout = self._timeout if timeout is None else timeout
|
||||
self._send(self._build_query(method, obj, jrpc_id))
|
||||
if not send_only:
|
||||
return self.receive(jrpc_id, tmout)
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def receive(self, jrpc_id: int, timeout: float = None) -> tuple:
|
||||
"""This function only receives an answer from a jsonrpc request.
|
||||
The values returned are :
|
||||
success, data
|
||||
|
||||
if timeout is None:
|
||||
the inherited timeout is used.
|
||||
|
||||
if timeout <= 0:
|
||||
if no data is available on the port/console, an exception is raised.
|
||||
if the id doesn't match, an exception is raised.
|
||||
success depends on content of the jsonrpc response.
|
||||
data is the error code if the success if false, otherwise it is the returned value.
|
||||
|
||||
if timeout > 0:
|
||||
If the response does not come before the end of the timeout, it fails with an exception.
|
||||
success depends on content of the jsonrpc response.
|
||||
data is the error code if the success if false, otherwise it is the returned value.
|
||||
|
||||
"""
|
||||
tmout = self._timeout if timeout is None else timeout
|
||||
obj = json.loads(self._receive(tmout))
|
||||
self.check_answer(obj, jrpc_id)
|
||||
|
||||
if self._jrpc_version == "1.0":
|
||||
success = obj["error"] is None
|
||||
else:
|
||||
success = not obj.get("error", None) is None
|
||||
if success:
|
||||
data = obj["result"]
|
||||
else:
|
||||
data = obj["error"]
|
||||
|
||||
return success, data
|
||||
|
||||
def open(self):
|
||||
self._open()
|
||||
|
||||
def close(self):
|
||||
self._close()
|
||||
|
||||
|
||||
class JrpcUdpAdapter(JrpcAdapter):
|
||||
description = "JSONRPC UDP adapter"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server: str,
|
||||
snd_port: int = -1,
|
||||
rcv_port: int = -1,
|
||||
bufsize: int = 1450,
|
||||
timeout: float = 1.0,
|
||||
version: str = "1.0",
|
||||
mute: bool = False,
|
||||
) -> None:
|
||||
"""server: hostname or ip of the UDP server to which we'll send requests.
|
||||
snd_port: port to which we'll send requests.
|
||||
rcv_port: port on which we'll wait for responses.
|
||||
bufsize: max size of the data to receive
|
||||
version: jsonrpc version
|
||||
"""
|
||||
super().__init__(timeout, version, mute)
|
||||
self._bufsize = bufsize
|
||||
self._server = server
|
||||
self._multicast = False
|
||||
self._rcv_port = rcv_port
|
||||
self._snd_port = snd_port
|
||||
|
||||
@property
|
||||
def sock(self):
|
||||
return tm.gd(f"jrpc_udp_rcv_port_{self._rcv_port}")
|
||||
|
||||
@sock.setter
|
||||
def sock(self, s):
|
||||
tm.setgd(f"jrpc_udp_rcv_port_{self._rcv_port}", s)
|
||||
|
||||
def del_global_sock(self):
|
||||
tm.delgd(f"jrpc_udp_rcv_port_{self._rcv_port}")
|
||||
|
||||
def _send(self, message: str):
|
||||
|
||||
# gets the address from the hostname if necessary
|
||||
srv = (self._server, self._snd_port)
|
||||
if not is_ip_address(self._server):
|
||||
try:
|
||||
socket.gethostbyname(self._server)
|
||||
addrinfo = socket.getaddrinfo(
|
||||
self._server, self._snd_port, socket.AF_INET, socket.SOCK_DGRAM
|
||||
)
|
||||
srv = addrinfo[0][4]
|
||||
except socket.gaierror as e:
|
||||
raise ETUMRuntimeError("JSONRPC udp send unknown address.")
|
||||
|
||||
# Sends the message to the server
|
||||
self.sock.sendto(message.encode(), srv)
|
||||
|
||||
# Don't log if mute
|
||||
if not self._mute:
|
||||
print(f" | sent to @{self._server}:{self._snd_port}")
|
||||
|
||||
def _receive(self, timeout: float) -> str:
|
||||
|
||||
# configures the reception timeout
|
||||
self.sock.settimeout(timeout)
|
||||
|
||||
# Receives the answer from the server
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(self._bufsize)
|
||||
|
||||
# In case of buffer overload we chose to complain
|
||||
if len(data) >= self._bufsize:
|
||||
raise ETUMRuntimeError(
|
||||
"JSONRPC udp answer size overflow. Try to increase the bufsize"
|
||||
)
|
||||
|
||||
# Converts binary to string
|
||||
res = data.decode()
|
||||
|
||||
# Don't log if mute
|
||||
if not self._mute:
|
||||
print(f" | UDP answer: '{res}'")
|
||||
print(f" | received from @{addr[0]}:{addr[1]}")
|
||||
|
||||
except socket.timeout:
|
||||
raise ETUMRuntimeError(
|
||||
"JSONRPC udp answer took too long. Try to increase the timeout."
|
||||
)
|
||||
return res
|
||||
|
||||
def _build_query(self, method: str, obj, jrpc_id: int):
|
||||
# Overload of the super build query to allow display of the sent message
|
||||
message = super()._build_query(method, obj, jrpc_id)
|
||||
print(f" | UDP query: '{message}'")
|
||||
return message
|
||||
|
||||
def _open(self):
|
||||
# Complain if the socket already exists
|
||||
if not tm.gd(f"jrpc_udp_rcv_port_{self._rcv_port}") is None:
|
||||
raise ETUMRuntimeError(
|
||||
f"A unclosed socket exists for the current reception port ({self._rcv_port})"
|
||||
)
|
||||
|
||||
if is_ip_address(self._server) and is_ip_multicast(self._server):
|
||||
self._multicast = True
|
||||
|
||||
# Creates the socket and bind to the reception port
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
if self._multicast:
|
||||
ttl = struct.pack("b", 1)
|
||||
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
|
||||
|
||||
self.sock.settimeout(self.timeout)
|
||||
self.sock.bind(("", self._rcv_port))
|
||||
|
||||
def _close(self):
|
||||
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
self.del_global_sock()
|
||||
|
||||
|
||||
class JrpcConsoleAdapter(JrpcAdapter):
|
||||
description = "JSONRPC console adapter"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cons_name: str,
|
||||
endswith: str = "\n",
|
||||
timeout: float = 1.0,
|
||||
version: str = "1.0",
|
||||
mute: bool = False,
|
||||
) -> None:
|
||||
""" """
|
||||
super().__init__(timeout, version, mute)
|
||||
self._endswith = endswith
|
||||
self._json_regexp = re.compile(r"^\s*{", re.MULTILINE)
|
||||
|
||||
# if the console is not defined in global we complain
|
||||
self._cons = tm.console(cons_name)
|
||||
if self._cons is None:
|
||||
raise ETUMRuntimeError(
|
||||
f"The '{cons_name}' console can't be found in global directory."
|
||||
)
|
||||
|
||||
def _send(self, message: str):
|
||||
self._cons.write(message + "\n")
|
||||
|
||||
def _receive(self, timeout: float) -> str:
|
||||
status, data = self._cons.read_until(
|
||||
self._endswith, timeout, return_data=True, mute=self._mute
|
||||
)
|
||||
|
||||
# if we did not receive anything, we complain
|
||||
if not status == 0:
|
||||
raise ETUMRuntimeError(
|
||||
f"The '{self._cons.name}' console did not answer in the requested time."
|
||||
)
|
||||
|
||||
res = list(self._json_regexp.finditer(data))
|
||||
if len(res) <= 0:
|
||||
raise ETUMRuntimeError("Not found JSON '^{'")
|
||||
|
||||
return data[res[-1].start() : -len(self._endswith)]
|
||||
70
src/testium/interpreter/test_items/test_item_let.py
Normal file
70
src/testium/interpreter/test_items/test_item_let.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import random
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.eval import evaluate
|
||||
|
||||
class TestItemLet(TestItem):
|
||||
"""let item usage.
|
||||
let values: {variable1: a, variable2: /dev/ttyUSB0, variable3: 115200}
|
||||
let eval: {conditional_exec: "random.randint(1, 4)"}
|
||||
"""
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_LET.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_LET
|
||||
self.is_container = False
|
||||
try:
|
||||
self._values_list = self._prms.getParamAll('values', default=[], required=False)
|
||||
self._eval_list = self._prms.getParamAll('eval', default=[], required=False)
|
||||
if (len(self._values_list) <= 0) and (len(self._eval_list) <= 0):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' must have a 'values' or 'eval' parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
if isinstance(self._values_list, dict):
|
||||
l = []
|
||||
for k in self._values_list.keys():
|
||||
l.append({k: self._values_list[k]})
|
||||
self._values_list = l
|
||||
if isinstance(self._eval_list, dict):
|
||||
l = []
|
||||
for k in self._eval_list.keys():
|
||||
l.append({k: self._eval_list[k]})
|
||||
self._eval_list = l
|
||||
#test core function
|
||||
for i in self._values_list:
|
||||
for k, v in i.items():
|
||||
key = self._prms.expanse(k)
|
||||
ev = self._prms.expanse(v)
|
||||
tm.setgd(key, ev)
|
||||
self.result.reported = {key: ev}
|
||||
print('global value "{}" set to "{}"'.format(key, ev))
|
||||
|
||||
for i in self._eval_list:
|
||||
for k, v in i.items():
|
||||
key = self._prms.expanse(k)
|
||||
val = self._prms.expanse(v)
|
||||
is_evaluated, ev = evaluate(val)
|
||||
if not is_evaluated:
|
||||
self.result.set(TestValue.FAILURE, "Error evaluating: '{}'".format(val))
|
||||
return
|
||||
tm.setgd(key, ev)
|
||||
self.result.reported = {key: ev}
|
||||
print('global value "{}" set to "{}"'.format(key, ev))
|
||||
|
||||
self.result.set(TestValue.SUCCESS, 'Variable set')
|
||||
54
src/testium/interpreter/test_items/test_item_msg_dialog.py
Normal file
54
src/testium/interpreter/test_items/test_item_msg_dialog.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
from interpreter.test_items.dialog_msg_files import msg_dialog
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
|
||||
class TestItemMsgDialog(TestItem):
|
||||
"""dialog_message item usage.
|
||||
dialog_message name: Nice message, question: Open the door and press OK
|
||||
"""
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_MESSAGE_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_MESSAGE_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam('question', required = True)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ourpath = __file__
|
||||
test_file = os.path.join(os.path.dirname(ourpath),
|
||||
'dialog_msg_files',
|
||||
'msg_dialog.py')
|
||||
|
||||
q = self._prms.expanse(self._question)
|
||||
print("Message Displayed:\n" + q)
|
||||
parent_conn, child_conn = Pipe()
|
||||
p=Process(target=msg_dialog.main,
|
||||
args=([self.name(), q],))
|
||||
p.start()
|
||||
p.join()
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
|
||||
def mypath():
|
||||
if hasattr(sys, "frozen"):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
from multiprocessing import Process
|
||||
|
||||
if __name__=='__main__':
|
||||
p=Process(target=msg_dialog.main, args=(['bob', 'bab'],))
|
||||
p.start()
|
||||
p.join()
|
||||
62
src/testium/interpreter/test_items/test_item_note_dialog.py
Normal file
62
src/testium/interpreter/test_items/test_item_note_dialog.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.test_items.dialog_note_files import test_dialog
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
|
||||
class TestItemNoteDialog(TestItem):
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_NOTE_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_NOTE_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam('question', required = True)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ourpath = __file__
|
||||
test_file = os.path.join(os.path.dirname(ourpath),
|
||||
'dialog_note_files',
|
||||
'test_dialog.py')
|
||||
|
||||
q = self._prms.expanse(self._question)
|
||||
print("Question:\n" + q)
|
||||
parent_conn, child_conn = Pipe()
|
||||
p=Process(target=test_dialog.main, args=([self.name(), q],child_conn))
|
||||
p.start()
|
||||
val, succ = parent_conn.recv()
|
||||
p.join()
|
||||
tm.setgd(self.name(), val)
|
||||
print("\n" + ("-" * 80) + "\n")
|
||||
print("- Test note\n")
|
||||
print("-" * 80 + "\n")
|
||||
print(val)
|
||||
print("-" * 80 + "\n")
|
||||
self.result.reported = {'note': val}
|
||||
if succ:
|
||||
self.result.set(TestValue.SUCCESS, val)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, val)
|
||||
|
||||
def mypath():
|
||||
if hasattr(sys, "frozen"):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
from multiprocessing import Process
|
||||
|
||||
if __name__=='__main__':
|
||||
p=Process(target=test_dialog.main, args=(['bob', 'bab'],))
|
||||
p.start()
|
||||
p.join()
|
||||
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.test_items.dialog_question_files import question_dialog
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
|
||||
class TestItemQuestionDialog(TestItem):
|
||||
"""dialog_question item usage.
|
||||
dialog_question name: Nice question, question: "If OK, press OK, If not, press cancel"
|
||||
"""
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_QUESTION_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_QUESTION_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam('question', required = True)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ourpath = __file__
|
||||
test_file = os.path.join(os.path.dirname(ourpath),
|
||||
'dialog_question_files',
|
||||
'question_dialog.py')
|
||||
|
||||
q = self._prms.expanse(self._question)
|
||||
print('Question asked:\n' + q + '\n')
|
||||
parent_conn, child_conn = Pipe()
|
||||
p=Process(target=question_dialog.main,
|
||||
args=([self.name(), q],child_conn))
|
||||
p.start()
|
||||
succ = parent_conn.recv()
|
||||
p.join()
|
||||
if succ == QMessageBox.Yes:
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
print('Answer: YES\n')
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE)
|
||||
print('Answer: NO\n')
|
||||
|
||||
def mypath():
|
||||
if hasattr(sys, "frozen"):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
from multiprocessing import Process
|
||||
|
||||
if __name__=='__main__':
|
||||
p=Process(target=test_dialog.main, args=(['bob', 'bab'],))
|
||||
p.start()
|
||||
p.join()
|
||||
41
src/testium/interpreter/test_items/test_item_report.py
Normal file
41
src/testium/interpreter/test_items/test_item_report.py
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.test_report.test_report import Export
|
||||
|
||||
class TestItemReport(TestItem):
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_REPORT.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_REPORT
|
||||
self.is_container = False
|
||||
|
||||
if not 'export' in dict_item:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' needs an 'export' section",
|
||||
self.seqFilename()
|
||||
)
|
||||
|
||||
self.tum_report = dict_item['export']
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
self.result.set(TestValue.FAILURE, 'an exception occured during report execution.')
|
||||
|
||||
dict_rep = self._prms.expanse(self.tum_report)
|
||||
if not isinstance(dict_rep, list):
|
||||
self.result.set(TestValue.FAILURE, 'Report item needs a "report" section')
|
||||
return
|
||||
rep_name = self._prms.expanse(self._name)
|
||||
|
||||
reports = []
|
||||
for exp in dict_rep:
|
||||
reports.append(Export(exp))
|
||||
|
||||
success = TestValue.SUCCESS
|
||||
for rep in reports:
|
||||
rep.exec(self.report.db_connection, rep_name, no_header=True)
|
||||
|
||||
self.result.set(success)
|
||||
127
src/testium/interpreter/test_items/test_item_run.py
Normal file
127
src/testium/interpreter/test_items/test_item_run.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import os
|
||||
from posixpath import splitext
|
||||
import sys
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
import traceback
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||
|
||||
|
||||
def nowInBetween(start, end):
|
||||
"""
|
||||
Check wether current time is within boundaries
|
||||
"""
|
||||
now = datetime.now().time()
|
||||
if start <= end:
|
||||
return start <= now < end
|
||||
else:
|
||||
return start <= now or now < end
|
||||
|
||||
|
||||
class TestItemRun(TestItem):
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_RUN.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_RUN
|
||||
self.is_container = False
|
||||
try:
|
||||
self.tum_fime = self._prms.getParam('tum_fime', required=True)
|
||||
self.param_file = self._prms.getParam('param_file', default='')
|
||||
self.python_path = self._prms.getParam('python_path', default='')
|
||||
self.testium_path = self._prms.getParam('testium_path', default='')
|
||||
self.log_path = self._prms.getParam('log_file', default='')
|
||||
self.report_path = self._prms.getParam('report_file', default='')
|
||||
self.start_time = self._prms.getParam('start_time')
|
||||
self.end_time = self._prms.getParam('end_time')
|
||||
self.wait_for_exec = self._prms.getParam('wait_for_exec')
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
res = -1
|
||||
try:
|
||||
file_path = self._prms.expanse(self.tum_fime)
|
||||
if not os.path.exists(file_path) and not os.path.isabs(file_path):
|
||||
file_path = os.path.join(tm.gd('test_directory'), self.tum_fime)
|
||||
if not os.path.isfile(file_path):
|
||||
raise ETUMRuntimeError(
|
||||
'"{}" file could not be found'.format(file_path))
|
||||
self.tum_fime = file_path
|
||||
pf = self._prms.expanse(self.param_file)
|
||||
pp = self._prms.expanse(self.python_path)
|
||||
sp = self._prms.expanse(self.testium_path)
|
||||
lp = self._prms.expanse(self.log_path)
|
||||
rp = self._prms.expanse(self.report_path)
|
||||
cmd = []
|
||||
if pp != '':
|
||||
cmd.append(pp)
|
||||
if sp == '':
|
||||
sp = os.path.join(tm.get_main_dir(), "testium.pyw")
|
||||
cmd.append(sp)
|
||||
if lp == '':
|
||||
lp = os.path.splitext(self.tum_fime)[0] + "_" + \
|
||||
datetime.utcnow().isoformat(timespec='seconds') + '.log'
|
||||
cmd.append("-r")
|
||||
if pf != '':
|
||||
cmd.append("-c")
|
||||
cmd.append('"' + pf + '"')
|
||||
cmd.append("-l")
|
||||
cmd.append('"' + lp + '"')
|
||||
if rp != '':
|
||||
cmd.append("-p")
|
||||
cmd.append('"' + rp + '"')
|
||||
cmd.append(self.tum_fime)
|
||||
for c in cmd:
|
||||
print(c, end = ' ')
|
||||
|
||||
if self.start_time is not None:
|
||||
self.start_time = datetime.strptime(
|
||||
self.start_time, '%H:%M').time()
|
||||
|
||||
if self.end_time is not None:
|
||||
self.end_time = datetime.strptime(self.end_time, '%H:%M').time()
|
||||
|
||||
if self.wait_for_exec and (self.start_time is None or self.end_time is None):
|
||||
raise ETUMRuntimeError(
|
||||
'"wait_for_exec" set but not start_time or end_time')
|
||||
|
||||
if self.wait_for_exec:
|
||||
while not nowInBetween(self.start_time, self.end_time):
|
||||
sleep(60)
|
||||
r = subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
elif self.start_time is not None and self.end_time is not None:
|
||||
if nowInBetween(self.start_time, self.end_time):
|
||||
r = subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
elif self.start_time is not None:
|
||||
if self.start_time < datetime.now().time():
|
||||
r = subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
else:
|
||||
r = subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if isinstance(r, subprocess.CompletedProcess):
|
||||
print((r.stdout).decode())
|
||||
print(r.stderr.decode())
|
||||
res = r.returncode
|
||||
if res >= 0:
|
||||
self.result.set(TestValue.SUCCESS)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE,
|
||||
'Test execution returned negative value.')
|
||||
except:
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
self.result.set(TestValue.FAILURE, 'Unrecoverable "run" item error')
|
||||
|
||||
|
||||
243
src/testium/interpreter/test_items/test_item_runtime_plot.py
Normal file
243
src/testium/interpreter/test_items/test_item_runtime_plot.py
Normal file
@@ -0,0 +1,243 @@
|
||||
import sys
|
||||
import importlib
|
||||
import traceback
|
||||
from functools import wraps
|
||||
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.test_items.test_item import TestItem, test_run
|
||||
from interpreter.test_items.test_result import TestResult, TestValue
|
||||
from interpreter.test_items.item_actions import TestItemActions
|
||||
from interpreter.test_items.item_actions.action import TestItemAction
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.eval import evaluate
|
||||
|
||||
|
||||
class TestItemPlotAction(TestItemAction):
|
||||
|
||||
def get_plot(self):
|
||||
gname = self._prms.expanse(self.token)
|
||||
return gname, tm.plot(gname)
|
||||
|
||||
|
||||
class TestItemPlotActionOpen(TestItemPlotAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_GRAPH_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
self._log_path = self._prms.getParam("log_path", None, required=False)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
try:
|
||||
gname = self._prms.expanse(self.token)
|
||||
lpath = self._prms.expanse(self._log_path)
|
||||
gr = runtime_plot.RuntimePlot(gname, lpath)
|
||||
tm.add_plot(gr)
|
||||
|
||||
except Exception as e:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message="Impossible to open the plot ({}) (exception: {})".format(
|
||||
self._plot_name, e
|
||||
),
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemPlotActionClose(TestItemPlotAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_GRAPH_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
self._wait_dialog_exit = self._prms.getParam("wait_dialog_exit", False)
|
||||
self._timeout = self._prms.getParam("timeout", -1)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
gname, gr = self.get_plot()
|
||||
wait_exit = self._prms.expanse(self._wait_dialog_exit)
|
||||
tmout = self._prms.expanse(self._timeout)
|
||||
try:
|
||||
if wait_exit:
|
||||
gr.close_wait_dialog_exit(tmout)
|
||||
else:
|
||||
gr.close()
|
||||
except Exception as e:
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE,
|
||||
message="Impossible to close the plot ({}) (exception: {})".format(
|
||||
gname, e
|
||||
),
|
||||
)
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
tm.remove_plot(gname)
|
||||
|
||||
|
||||
class TestItemPlotActionPeriodic(TestItemPlotAction):
|
||||
def __init__(
|
||||
self, action_name, dict_item, parent=None, status_queue=None, filename=""
|
||||
):
|
||||
super().__init__(
|
||||
action_name,
|
||||
cst.TYPE_GRAPH_ACTION,
|
||||
dict_item,
|
||||
parent,
|
||||
status_queue,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
# Periodic function call
|
||||
try:
|
||||
self.period = self._prms.getParam("period", required=True)
|
||||
self.file_name = self._prms.getParam("file", required=True)
|
||||
self.func_name = self._prms.getParam("func_name", required=True)
|
||||
self.params = self._prms.getParamAll("param")
|
||||
self.post_eval = self._prms.getParam("eval", default="")
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' 'periodic' action settings syntax error",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
gname, gr = self.get_plot()
|
||||
try:
|
||||
file = self._prms.expanse(self.file_name)
|
||||
func_name = self._prms.expanse(self.func_name)
|
||||
param_list = self._prms.getParamFromList(self.params)
|
||||
pl = self._prms.expanse(param_list)
|
||||
post_eval = self._prms.expanse(self.post_eval)
|
||||
gr.add_periodic(self.period, file, func_name, pl, post_eval)
|
||||
|
||||
except:
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
self.result.set(
|
||||
result=TestValue.FAILURE, message='Unrecoverable "plot" item error'
|
||||
)
|
||||
else:
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemPlotActionAdd(TestItemPlotAction):
|
||||
def __init__(self, action_name, dict_item, parent=None, status_queue=None, filename=""):
|
||||
super().__init__(
|
||||
action_name, cst.TYPE_GRAPH_ACTION, dict_item, parent, status_queue, filename=filename
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
gname, gr = self.get_plot()
|
||||
input = self._prms.getData()
|
||||
data = {}
|
||||
if isinstance(input, str):
|
||||
input = self._prms.expanse(input)
|
||||
|
||||
if isinstance(input, dict):
|
||||
for k, v in input.items():
|
||||
v = self._prms.expanse(v)
|
||||
_, v = evaluate(v)
|
||||
data.update({k: v})
|
||||
|
||||
gr.add(data)
|
||||
|
||||
else:
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
f"Plot item ({self._name}) 'add' content must be a dict.",
|
||||
)
|
||||
return
|
||||
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemPlotActionLastValues(TestItemPlotAction):
|
||||
def __init__(self, action_name, dict_item, parent=None, status_queue=None, filename=""):
|
||||
super().__init__(
|
||||
action_name, cst.TYPE_GRAPH_ACTION, dict_item, parent, status_queue, filename=filename
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
gname, gr = self.get_plot()
|
||||
test_res = {}
|
||||
keys = self._prms.getParam("name", [], processed=True)
|
||||
if isinstance(keys, list):
|
||||
last_values = gr.last_values()
|
||||
for k in keys:
|
||||
test_res.update({k: last_values.get(k, None)})
|
||||
else:
|
||||
self.result.set(
|
||||
TestValue.FAILURE,
|
||||
f"Plot item ({self._name}) 'name' parameter of 'last_value' action must be a list.",
|
||||
)
|
||||
return
|
||||
|
||||
tm.setgd("plv_" + gname, test_res)
|
||||
self.result.value = test_res
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemPlotActionExport(TestItemPlotAction):
|
||||
def __init__(self, action_name, dict_item, parent=None, status_queue=None, filename=""):
|
||||
super().__init__(
|
||||
action_name, cst.TYPE_GRAPH_ACTION, dict_item, parent, status_queue, filename=filename
|
||||
)
|
||||
|
||||
self.file_name = self._prms.getData()
|
||||
if not isinstance(self.file_name, str):
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' 'export' parameter must be a file name",
|
||||
self.seqFilename()
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
gname, gr = self.get_plot()
|
||||
fn = self._prms.expanse(self.file_name)
|
||||
if gr is not None:
|
||||
gr.save(fn)
|
||||
print(f"Saved '{gname}' plot in '{fn}'")
|
||||
self.result.set(result=TestValue.SUCCESS)
|
||||
|
||||
|
||||
class TestItemPlot(TestItemActions):
|
||||
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
|
||||
super().__init__(
|
||||
cst.TYPE_GRAPH, dict_item, parent, status_queue, filename=filename
|
||||
)
|
||||
|
||||
self.register_actions(
|
||||
open=TestItemPlotActionOpen,
|
||||
close=TestItemPlotActionClose,
|
||||
periodic=TestItemPlotActionPeriodic,
|
||||
add=TestItemPlotActionAdd,
|
||||
last_value=TestItemPlotActionLastValues,
|
||||
export=TestItemPlotActionExport,
|
||||
)
|
||||
|
||||
self.actions_token = self._prms.getParam("plot_name", required=True)
|
||||
|
||||
global runtime_plot
|
||||
runtime_plot = importlib.import_module("libs.runtime_plot")
|
||||
71
src/testium/interpreter/test_items/test_item_sleep.py
Normal file
71
src/testium/interpreter/test_items/test_item_sleep.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import re
|
||||
from time import sleep
|
||||
from datetime import timedelta
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
from interpreter.test_items.dialog_sleep_files import dialog_sleep
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||
|
||||
class TestItemSleep(TestItem):
|
||||
"""sleep item usage.
|
||||
sleep timeout: 10
|
||||
"""
|
||||
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_SLEEP.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_SLEEP
|
||||
self.is_container = False
|
||||
try:
|
||||
self._timeout = self._prms.getParam('timeout', required = True)
|
||||
self._has_dialog = self._prms.getParam('dialog', default=False)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
|
||||
timeout = self._prms.expanse(self._timeout)
|
||||
|
||||
if isinstance(timeout, str) and timeout.isnumeric():
|
||||
timeout = float(timeout)
|
||||
elif isinstance(timeout, str):
|
||||
m = re.search(r"((?P<day>\d+)d)?\s*((?P<hour>\d+)h)?\s*((?P<minute>\d+)m)?\s*((?P<second>\d+)s)?", timeout, flags=re.IGNORECASE)
|
||||
if m.lastindex is not None :
|
||||
day = int(m.group("day")) if m.group("day") is not None else 0
|
||||
hour = int(m.group("hour")) if m.group("hour") is not None else 0
|
||||
minute = int(m.group("minute")) if m.group("minute") is not None else 0
|
||||
second = int(m.group("second")) if m.group("second") is not None else 0
|
||||
timeout = timedelta(days=day, hours=hour, minutes=minute, seconds=second).total_seconds()
|
||||
|
||||
has_dialog = self._prms.expanse(self._has_dialog)
|
||||
|
||||
#test core function
|
||||
if has_dialog:
|
||||
parent_conn, child_conn = Pipe()
|
||||
p=Process(target=dialog_sleep.main, args=([self.name(), timeout],child_conn))
|
||||
p.start()
|
||||
succ = parent_conn.recv()
|
||||
p.join()
|
||||
|
||||
if succ:
|
||||
mesg = 'Sleep %s sec' % (str(timeout))
|
||||
res = TestValue.SUCCESS
|
||||
else:
|
||||
mesg = 'Sleep aborted'
|
||||
print("Aborted")
|
||||
res = TestValue.FAILURE
|
||||
|
||||
self.result.set(res, mesg)
|
||||
|
||||
else:
|
||||
if not isinstance(timeout, (int, float)):
|
||||
raise ETUMRuntimeError(f"Timeout value of sleep test item \"{self.name}\" is not valid: \"{timeout}\".")
|
||||
sleep(timeout)
|
||||
self.result.set(TestValue.SUCCESS, 'Sleep %s sec' % (str(timeout)))
|
||||
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.test_items.tested_references_files import tested_refs_dialog
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
|
||||
class TestItemTestedRefsDialog(TestItem):
|
||||
def __init__(self, dict_item, parent=None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_REFERENCE_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_REFERENCE_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam('question', required=True)
|
||||
self._init_values = self._prms.getParamAll('reference', required=False, processed=True)
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ourpath=__file__
|
||||
test_file=os.path.join(os.path.dirname(ourpath),
|
||||
'tested_references_files',
|
||||
'tested_refs_dialog.py')
|
||||
|
||||
q=self._prms.expanse(self._question)
|
||||
parent_conn, child_conn=Pipe()
|
||||
init_values=','.join(self._init_values)
|
||||
p=Process(target=tested_refs_dialog.main,
|
||||
args=([self.name(), q, init_values],
|
||||
child_conn))
|
||||
p.start()
|
||||
val, succ=parent_conn.recv()
|
||||
p.join()
|
||||
|
||||
titems=[]
|
||||
if len(val) > 0:
|
||||
i = 0
|
||||
for sitem in val.split(','):
|
||||
titem={}
|
||||
telems=sitem.split('/')
|
||||
titem['reference']=telems[0]
|
||||
titem['revision']=telems[1]
|
||||
titem['serial']=telems[2]
|
||||
print("Identification:\n" + str(titem))
|
||||
titems.append(titem)
|
||||
self.result.reported = {'reference_{}'.format(i): titem}
|
||||
i = i + 1
|
||||
self.result.value = titems
|
||||
tm.setgd('tested_items', titems)
|
||||
if len(val) > 0:
|
||||
if succ:
|
||||
self.result.set(TestValue.SUCCESS, val)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, val)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, 'The dialog did not return any value')
|
||||
|
||||
def mypath():
|
||||
if hasattr(sys, "frozen"):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
from multiprocessing import Process
|
||||
|
||||
if __name__ == '__main__':
|
||||
p=Process(target=test_dialog.main, args=(['bob', 'bab'],))
|
||||
p.start()
|
||||
p.join()
|
||||
220
src/testium/interpreter/test_items/test_item_unittest.py
Normal file
220
src/testium/interpreter/test_items/test_item_unittest.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest import (TestCase, TestSuite, TextTestRunner,
|
||||
TextTestResult)
|
||||
from unittest.loader import defaultTestLoader
|
||||
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import (ETUMFileError)
|
||||
from interpreter.utils.modules import load_source
|
||||
from interpreter.test_items.test_item import (TestItem, test_run, LOG_TEST_STOP, LOG_TEST_START)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.test_items.test_item import test_data
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
from interpreter.utils.stdout_redirect import stdio_redir
|
||||
|
||||
class UnittestResult(TextTestResult):
|
||||
"""Test result adapted for unittest test"""
|
||||
_status_queue = None
|
||||
reported_values = {}
|
||||
|
||||
def __init__(self, stream, descriptions, verbosity):
|
||||
super().__init__(stdio_redir.stream, descriptions, verbosity)
|
||||
self.separator2 = ""
|
||||
|
||||
@classmethod
|
||||
def setStatusQueue(self, status_queue):
|
||||
self._status_queue = status_queue
|
||||
|
||||
def __sendStatus(self, test, result, msg=''):
|
||||
if hasattr(test, '_id'):
|
||||
self.res = TestResult(result=result, message = msg)
|
||||
self.res.test_id = test._id
|
||||
self.res.sendStatus(self._status_queue)
|
||||
self.duration = tm.timestamp() - self._timestamp
|
||||
|
||||
def __sendStatusStarted(self, test):
|
||||
self._status_queue.put({'id':test._id, 'status':'started',
|
||||
'timestamp':self._timestamp})
|
||||
|
||||
def __sendStatusStopped(self, test):
|
||||
self._status_queue.put({'id':test._id, 'status':'finished', 'duration': self.duration})
|
||||
|
||||
def stop(self):
|
||||
super().stop()
|
||||
|
||||
def addSuccess(self, test):
|
||||
super().addSuccess(test)
|
||||
self.__sendStatus(test, TestValue.SUCCESS)
|
||||
|
||||
def addError(self, test, err):
|
||||
super().addError(test, err)
|
||||
self.__sendStatus(test, TestValue.FAILURE, str(err[1]))
|
||||
|
||||
def addFailure(self, test, err):
|
||||
super().addFailure(test, err)
|
||||
self.__sendStatus(test, TestValue.FAILURE, str(err[1]))
|
||||
|
||||
def addSkip(self, test, reason):
|
||||
super().addSkip(test, reason)
|
||||
self.__sendStatus(test, TestValue.NORUN)
|
||||
|
||||
def addExpectedFailure(self, test, err):
|
||||
super().addExpectedFailure(test, err)
|
||||
self.__sendStatus(test, TestValue.FAILURE, str(err[1]))
|
||||
|
||||
def addUnexpectedSuccess(self, test):
|
||||
super().addUnexpectedSuccess(test)
|
||||
self.__sendStatus(test, TestValue.SUCCESS)
|
||||
|
||||
def startTest(self, test):
|
||||
"""Called when the given test is about to be run.
|
||||
"""
|
||||
self._timestamp = test.t0
|
||||
s = LOG_TEST_START.format(test._item_name)
|
||||
s = (s + '{:>'+str(max(1, 80-len(s))) +
|
||||
'}').format(str('@@{}@@'.format(test.t0)))
|
||||
print(s)
|
||||
self.__sendStatusStarted(test)
|
||||
super().startTest(test)
|
||||
|
||||
def stopTest(self, test):
|
||||
"Called when the given test is about to be run"
|
||||
super().stopTest(test)
|
||||
print(LOG_TEST_STOP.format(test._item_name) + ": " + str(self.res.test_result))
|
||||
self.__sendStatusStopped(test)
|
||||
|
||||
class TestItemUnittestElement(TestItem):
|
||||
def __init__(self, name, parent = None, status_queue=None, filename=""):
|
||||
super().__init__(None, parent, status_queue, filename=filename)
|
||||
self.is_container = False
|
||||
self._name = name
|
||||
self._type = cst.TYPE_UNITTEST_STEP
|
||||
self.banner = ""
|
||||
self.footer = ""
|
||||
|
||||
|
||||
class TestItemUnittestFile(TestItem):
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_UNITTEST_FILE.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self.is_container = True
|
||||
self._type = cst.TYPE_UNITTEST_FILE
|
||||
self._fileName = self._prms.getParam('test_file', required = True, processed = True)
|
||||
self._testDir = ''
|
||||
self._test_methods = self._prms.getParamAll('test_method', processed=True)
|
||||
|
||||
def setTestDir(self, dir):
|
||||
self._testDir=dir
|
||||
|
||||
def __runALoop(self):
|
||||
results = []
|
||||
i = 0
|
||||
to_be_stopped = False
|
||||
while (not self.isStopped()) and (i < self.childCount()) and (not to_be_stopped):
|
||||
if not self.child(i).enabled:
|
||||
res = TestResult(self.child(i), TestValue.NORUN)
|
||||
else:
|
||||
ts = TestSuite()
|
||||
test = self.child(i).test
|
||||
test.t0 = tm.timestamp()
|
||||
test._item_name = self.child(i).name()
|
||||
ts.addTest(test)
|
||||
self.child(i).t0 = test.t0
|
||||
try:
|
||||
try:
|
||||
result = self.test_runner.run(ts)
|
||||
finally:
|
||||
self.child(i).duration = tm.timestamp() - self.child(i).t0
|
||||
except:
|
||||
res = TestResult(self.child(i), TestValue.FAILURE, '"{}" crashed.'.format(test._item_name))
|
||||
else:
|
||||
if len(result.failures)>0 or len(result.errors)>0:
|
||||
res = TestResult(self.child(i), TestValue.FAILURE)
|
||||
elif (len(result.skipped)>0):
|
||||
res = TestResult(self.child(i), TestValue.NORUN)
|
||||
else:
|
||||
res = TestResult(self.child(i), TestValue.SUCCESS)
|
||||
self.report.addTest(self.child(i), res)
|
||||
if res.test_result == TestValue.FAILURE and self._stop_on_failure:
|
||||
to_be_stopped = True
|
||||
results.append(res)
|
||||
i = i + 1
|
||||
|
||||
test_success = TestValue.SUCCESS
|
||||
for res in results:
|
||||
if res.test_result == TestValue.FAILURE:
|
||||
test_success = TestValue.FAILURE
|
||||
break
|
||||
|
||||
result = TestResult(None, test_success, 'Unittest file')
|
||||
return result
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
# set the queue where steps have to send their results
|
||||
self.test_runner.resultclass.setStatusQueue(self.status_queue)
|
||||
|
||||
# Execute the tests
|
||||
result = self.__runALoop()
|
||||
|
||||
if self.isStopped():
|
||||
self.result.set(TestValue.NORUN, 'Group execution aborted on user request')
|
||||
else:
|
||||
self.result.set(result.test_result, 'unittest file ' + str(result.test_result))
|
||||
|
||||
def load(self):
|
||||
ret = {}
|
||||
if self._fileName == '':
|
||||
raise ETUMFileError('A file name is expected but got "None"')
|
||||
|
||||
if not os.path.isabs(self._fileName):
|
||||
self._fileName = os.path.normpath(os.path.join(self._testDir, self._fileName))
|
||||
|
||||
if not os.path.isfile(self._fileName):
|
||||
raise ETUMFileError('File "%s" is not found' % (self._fileName))
|
||||
|
||||
sys.path.append(os.path.dirname(self._fileName))
|
||||
|
||||
self.test_runner = TextTestRunner(verbosity=2,
|
||||
resultclass=UnittestResult,
|
||||
failfast=self._stop_on_failure)
|
||||
self.test_loader = defaultTestLoader
|
||||
|
||||
test_suites = []
|
||||
modulename = os.path.basename(self._fileName).split('.')[0]
|
||||
module = load_source(modulename, os.path.abspath(self._fileName))
|
||||
testnames = []
|
||||
for name in dir(module):
|
||||
try:
|
||||
obj = getattr(module, name)
|
||||
if (isinstance(obj, type) and issubclass(obj, TestCase)):
|
||||
tcn = self.test_loader.getTestCaseNames(obj)
|
||||
testnames = [*testnames, *tcn]
|
||||
test_suites.append(TestSuite(list(map(obj, tcn))))
|
||||
except ImportError:
|
||||
# case where the module in scope can't be imported for any reason
|
||||
pass
|
||||
|
||||
for test_method in self._test_methods:
|
||||
if not test_method in testnames:
|
||||
raise ETUMFileError('Test method "%s" is not found in "%s"' % (
|
||||
test_method, self._fileName))
|
||||
|
||||
for tests in test_suites:
|
||||
for test in tests:
|
||||
test_name = (str(test).split('(')[0]).strip()
|
||||
if (test_name in self._test_methods) or (len(self._test_methods) == 0):
|
||||
item = TestItemUnittestElement(test_name, self)
|
||||
# set the test_item id in the test_step instance for
|
||||
# later status sending
|
||||
test._id = item.id()
|
||||
test.reported_values = {}
|
||||
item.test = test
|
||||
item._doc = test._testMethodDoc
|
||||
if item._doc is None:
|
||||
item._doc = ''
|
||||
|
||||
ret.update(test_data(item, {}))
|
||||
|
||||
return ret
|
||||
67
src/testium/interpreter/test_items/test_item_value_dialog.py
Normal file
67
src/testium/interpreter/test_items/test_item_value_dialog.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from interpreter.test_items.test_item import (TestItem, test_run)
|
||||
from interpreter.test_items.test_result import (TestResult, TestValue)
|
||||
from interpreter.test_items.dialog_value_files import test_dialog
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
|
||||
class TestItemValueDialog(TestItem):
|
||||
"""dialog_value item usage.
|
||||
dialog_value name: Enter value, question: "Which value did you measure?"
|
||||
"""
|
||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||
self._name = cst.TYPE_VALUE_DLG.item_name
|
||||
super().__init__(dict_item, parent, status_queue, filename=filename)
|
||||
self._type = cst.TYPE_VALUE_DLG
|
||||
self.is_container = False
|
||||
try:
|
||||
self._question = self._prms.getParam('question', required = True)
|
||||
self._default = self._prms.getParam('default', '')
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"The '{self.cmd()}' test item named '{self.name()}' has a missing or wrong parameter",
|
||||
self.seqFilename(),
|
||||
)
|
||||
|
||||
@test_run
|
||||
def execute(self):
|
||||
ourpath = __file__
|
||||
test_file = os.path.join(os.path.dirname(ourpath),
|
||||
'dialog_value_files',
|
||||
'test_dialog.py')
|
||||
|
||||
q = self._prms.expanse(self._question)
|
||||
d = self._prms.expanse(self._default)
|
||||
print("Question:\n" + q)
|
||||
parent_conn, child_conn = Pipe()
|
||||
p=Process(target=test_dialog.main, args=([self.name(), q, d],child_conn))
|
||||
p.start()
|
||||
val, succ = parent_conn.recv()
|
||||
p.join()
|
||||
tm.setgd(self.name(), val)
|
||||
print("Answer: " + val)
|
||||
if len(val) > 0:
|
||||
self.result.reported = {'question': q, 'answer': val}
|
||||
self.result.value = val
|
||||
if succ:
|
||||
self.result.set(TestValue.SUCCESS, val)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, val)
|
||||
else:
|
||||
self.result.set(TestValue.FAILURE, 'The dialog did not return any value')
|
||||
|
||||
def mypath():
|
||||
if hasattr(sys, "frozen"):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
from multiprocessing import Process
|
||||
|
||||
if __name__=='__main__':
|
||||
p=Process(target=test_dialog.main, args=(['bob', 'bab'],))
|
||||
p.start()
|
||||
p.join()
|
||||
83
src/testium/interpreter/test_items/test_result.py
Normal file
83
src/testium/interpreter/test_items/test_result.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from interpreter.utils.tum_except import (ETUMRuntimeError)
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
class TestValue(Enum):
|
||||
SUCCESS = 0
|
||||
FAILURE = -1
|
||||
NORUN = -2
|
||||
|
||||
def __str__(self):
|
||||
r = ''
|
||||
if self == self.SUCCESS:
|
||||
r = 'PASS'
|
||||
if self == self.FAILURE:
|
||||
r = 'FAIL'
|
||||
if self == self.NORUN:
|
||||
r = 'SKIP'
|
||||
return r
|
||||
|
||||
class TestResult:
|
||||
def __init__(self, test=None, result=None, message=''):
|
||||
|
||||
self.test_name = ''
|
||||
self.id = -1
|
||||
self.test_id = -1
|
||||
self.value = None # Optional : used to handle values to
|
||||
# be evaluated if success of failure (function item for ex.)
|
||||
|
||||
if test is not None:
|
||||
self.test_name = test.name()
|
||||
self.test_id = test.id()
|
||||
|
||||
self.__reported_values = {}
|
||||
self.set(result, message)
|
||||
|
||||
def set(self, result, message = ''):
|
||||
self.test_result = result
|
||||
if not (message == ''):
|
||||
self.message = message
|
||||
else:
|
||||
self.message = str(self.test_result)
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return TestValue.SUCCESS == self.test_result
|
||||
|
||||
@property
|
||||
def test_result(self):
|
||||
return self._result
|
||||
|
||||
@test_result.setter
|
||||
def test_result(self, result):
|
||||
if (isinstance(result, TestValue)) or (result is None):
|
||||
self._result = result
|
||||
else:
|
||||
raise(ETUMRuntimeError('Test result (for reporting) must be a "TestValue" class instance'))
|
||||
|
||||
@property
|
||||
def reported(self):
|
||||
return self.__reported_values
|
||||
|
||||
@reported.setter
|
||||
def reported(self, value):
|
||||
self.__reported_values.update(value)
|
||||
|
||||
def reportedJSON(self):
|
||||
return json.dumps(self.__reported_values)
|
||||
|
||||
def sendStatus(self, status_queue):
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
date_str = '[{}]'.format(date_str)
|
||||
status = {'id':self.test_id,
|
||||
'name':self.test_name,
|
||||
'value':self.test_result.value,
|
||||
'message':self.message,
|
||||
'date':date_str}
|
||||
if status_queue is not None:
|
||||
status_queue.put(status)
|
||||
else:
|
||||
raise(ETUMRuntimeError("TestResult can't send status. status_queue is 'None'"))
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import sys
|
||||
import os
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
from PySide6.QtWidgets import (QApplication, QDialog, QTableWidgetItem)
|
||||
from PySide6.QtCore import (Qt, QSettings)
|
||||
|
||||
try:
|
||||
from interpreter.test_items.tested_references_files import tested_refs_win
|
||||
except:
|
||||
import tested_refs_win
|
||||
|
||||
class TestedRefsWindow(QDialog, tested_refs_win.Ui_Dialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
def main(args, conn=None):
|
||||
SettingsCompagny = 'Testium'
|
||||
SettingsApplication = 'testium_ref_item'
|
||||
SettingsLastReference = 'lastReference'
|
||||
success = True
|
||||
app = QApplication(args)
|
||||
d = TestedRefsWindow()
|
||||
d.setFixedSize(481,386)
|
||||
d.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
d.setWindowTitle(args[0])
|
||||
d.labelDialog.setText(args[1])
|
||||
d.tableReferences.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
settings = QSettings(SettingsCompagny, SettingsApplication)
|
||||
last_reference = settings.value(SettingsLastReference, '')
|
||||
|
||||
last_rows_content = last_reference.split(sep=',')
|
||||
args_rows_content = args[2].split(sep=',')
|
||||
|
||||
d.tableReferences.setRowCount(len(args_rows_content))
|
||||
i = 0
|
||||
for row in args_rows_content:
|
||||
j = 0
|
||||
for val in row.split('/'):
|
||||
d.tableReferences.setItem(i, j, QTableWidgetItem(val))
|
||||
j += 1
|
||||
j = 0
|
||||
if i < len(last_rows_content):
|
||||
last_row = last_rows_content[i]
|
||||
for val in last_row.split('/'):
|
||||
if d.tableReferences.item(i, j) is None:
|
||||
d.tableReferences.setItem(i, j, QTableWidgetItem(val))
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
d.tableReferences.setFocus()
|
||||
dres = d.exec()
|
||||
|
||||
if dres == QDialog.Rejected:
|
||||
success = False
|
||||
|
||||
#build the answer:
|
||||
row_items=[]
|
||||
for i in range(d.tableReferences.rowCount()):
|
||||
col_items=[]
|
||||
for j in range(d.tableReferences.columnCount()):
|
||||
try:
|
||||
col_items.append(d.tableReferences.item(i,j).text())
|
||||
except:
|
||||
col_items.append('')
|
||||
|
||||
row_items.append('/'.join(col_items))
|
||||
|
||||
result=','.join(row_items)
|
||||
|
||||
if conn:
|
||||
settings.setValue(SettingsLastReference, result)
|
||||
conn.send([result, success])
|
||||
conn.close()
|
||||
else:
|
||||
print(result, end='')
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
#all standard streams are replaced by dummy one to avoid cx_freeze flushing bug.
|
||||
class dummyStream:
|
||||
''' dummyStream behaves like a stream but does nothing. '''
|
||||
def __init__(self): pass
|
||||
def write(self,data): pass
|
||||
def read(self,data): pass
|
||||
def flush(self): pass
|
||||
def close(self): pass
|
||||
|
||||
# and now redirect all default streams to this dummyStream:
|
||||
sys.stdout = dummyStream()
|
||||
sys.stderr = dummyStream()
|
||||
sys.stdin = dummyStream()
|
||||
sys.__stdout__ = dummyStream()
|
||||
sys.__stderr__ = dummyStream()
|
||||
sys.__stdin__ = dummyStream()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'tested_refs_win.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||
QHeaderView, QLabel, QSizePolicy, QTableWidget,
|
||||
QTableWidgetItem, QWidget)
|
||||
|
||||
class Ui_Dialog(object):
|
||||
def setupUi(self, Dialog):
|
||||
if not Dialog.objectName():
|
||||
Dialog.setObjectName(u"Dialog")
|
||||
Dialog.resize(481, 386)
|
||||
Dialog.setModal(True)
|
||||
self.buttonBox = QDialogButtonBox(Dialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setGeometry(QRect(10, 350, 461, 32))
|
||||
self.buttonBox.setOrientation(Qt.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||
self.labelDialog = QLabel(Dialog)
|
||||
self.labelDialog.setObjectName(u"labelDialog")
|
||||
self.labelDialog.setGeometry(QRect(10, 10, 461, 111))
|
||||
font = QFont()
|
||||
font.setPointSize(20)
|
||||
self.labelDialog.setFont(font)
|
||||
self.labelDialog.setAlignment(Qt.AlignCenter)
|
||||
self.labelDialog.setWordWrap(True)
|
||||
self.tableReferences = QTableWidget(Dialog)
|
||||
if (self.tableReferences.columnCount() < 3):
|
||||
self.tableReferences.setColumnCount(3)
|
||||
font1 = QFont()
|
||||
font1.setPointSize(10)
|
||||
__qtablewidgetitem = QTableWidgetItem()
|
||||
__qtablewidgetitem.setFont(font1);
|
||||
self.tableReferences.setHorizontalHeaderItem(0, __qtablewidgetitem)
|
||||
__qtablewidgetitem1 = QTableWidgetItem()
|
||||
__qtablewidgetitem1.setFont(font1);
|
||||
self.tableReferences.setHorizontalHeaderItem(1, __qtablewidgetitem1)
|
||||
__qtablewidgetitem2 = QTableWidgetItem()
|
||||
__qtablewidgetitem2.setFont(font1);
|
||||
self.tableReferences.setHorizontalHeaderItem(2, __qtablewidgetitem2)
|
||||
self.tableReferences.setObjectName(u"tableReferences")
|
||||
self.tableReferences.setGeometry(QRect(10, 130, 461, 211))
|
||||
self.tableReferences.setMinimumSize(QSize(461, 0))
|
||||
self.tableReferences.setFont(font1)
|
||||
self.tableReferences.setAlternatingRowColors(True)
|
||||
self.tableReferences.setRowCount(0)
|
||||
|
||||
self.retranslateUi(Dialog)
|
||||
self.buttonBox.accepted.connect(Dialog.accept)
|
||||
self.buttonBox.rejected.connect(Dialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(Dialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, Dialog):
|
||||
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None))
|
||||
self.labelDialog.setText(QCoreApplication.translate("Dialog", u"TextLabel", None))
|
||||
___qtablewidgetitem = self.tableReferences.horizontalHeaderItem(0)
|
||||
___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Reference", None));
|
||||
___qtablewidgetitem1 = self.tableReferences.horizontalHeaderItem(1)
|
||||
___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Revision", None));
|
||||
___qtablewidgetitem2 = self.tableReferences.horizontalHeaderItem(2)
|
||||
___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Serial number", None));
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>481</width>
|
||||
<height>386</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>350</y>
|
||||
<width>461</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="labelDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>461</width>
|
||||
<height>111</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>20</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QTableWidget" name="tableReferences">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>130</y>
|
||||
<width>461</width>
|
||||
<height>211</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>461</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Reference</string>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Revision</string>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Serial number</string>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
0
src/testium/interpreter/test_report/__init__.py
Normal file
0
src/testium/interpreter/test_report/__init__.py
Normal file
100
src/testium/interpreter/test_report/report_export.py
Normal file
100
src/testium/interpreter/test_report/report_export.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
|
||||
import interpreter.test_report.test_report as tr
|
||||
from interpreter.utils.paths import prepare_file_to_save
|
||||
import interpreter.utils.constants as cst
|
||||
import libs.testium as tm
|
||||
|
||||
|
||||
class ReportExport:
|
||||
KEY_SUCCESS = 'success'
|
||||
KEY_TITLE = 'title'
|
||||
KEY_MESSAGE = 'message'
|
||||
KEY_DURATION = 'duration'
|
||||
KEY_LOG = 'log'
|
||||
ROW_TEXTS = [
|
||||
['Test title', KEY_TITLE],
|
||||
['Message', KEY_MESSAGE],
|
||||
['Duration (s)', KEY_DURATION],
|
||||
['Test Result', KEY_SUCCESS]
|
||||
]
|
||||
HEADER_TEXTS = {
|
||||
'test_file': 'Test file name',
|
||||
'test_name': 'Test name',
|
||||
'testrun_date': 'Date of the test',
|
||||
'testrun_time': 'Time of the test',
|
||||
'test_revision': 'Git revision of the test',
|
||||
'report_version': 'Report tool version',
|
||||
}
|
||||
TEXT_INDEX = 0
|
||||
KEY_INDEX = 1
|
||||
|
||||
def __init__(self, name, report_db, report_file, pattern, key):
|
||||
self.name = name
|
||||
self.pattern = pattern
|
||||
self._report_file = report_file
|
||||
self.key = key
|
||||
self._con = report_db
|
||||
self.header = {}
|
||||
for row in self._con.execute('SELECT * FROM header'):
|
||||
self.header.update({row[0]: row[1]})
|
||||
|
||||
def process_tests(self):
|
||||
req = 'SELECT * FROM tests '
|
||||
lp = len(self.pattern)
|
||||
lk = len(self.key)
|
||||
|
||||
# If key or patterns are defined
|
||||
# the query is adapted
|
||||
if (lp != 0) or (lk != 0):
|
||||
|
||||
req = req + 'WHERE '
|
||||
|
||||
for i in range(lp):
|
||||
pat = self.pattern[i]
|
||||
req = req + cst.DB_TEST_NAME + ' LIKE '
|
||||
req = req + '"' + pat + '" ' + 'OR '
|
||||
|
||||
for i in range(lk):
|
||||
k = self.key[i]
|
||||
req = req + cst.DB_TEST_KEY + ' LIKE '
|
||||
req = req + '"' + k + '" ' + 'OR '
|
||||
|
||||
req = req[:-len('OR ')] + ' '
|
||||
|
||||
req = req + 'ORDER BY ' + cst.DB_TEST_TIMESTAMP_START
|
||||
for row in self._con.execute(req):
|
||||
self.testsIterate(row)
|
||||
|
||||
def testsIterate(self, row):
|
||||
pass
|
||||
|
||||
def rowData(self, row, name):
|
||||
return row[tr.TestReport.indexOf(name)]
|
||||
|
||||
def prepareFile(self, file_ext=''):
|
||||
self._file_name = prepare_file_to_save(self._report_file, file_ext)
|
||||
|
||||
def extract_info(self, row):
|
||||
ret = {}
|
||||
ret[self.KEY_SUCCESS] = self.rowData(row, cst.DB_TEST_RESULT)
|
||||
ret[self.KEY_MESSAGE] = self.rowData(row, cst.DB_TEST_MESSAGE)
|
||||
ret[self.KEY_TITLE] = self.rowData(row, cst.DB_TEST_NAME)
|
||||
ret[self.KEY_DURATION] = tm.timestamp_as_sec(self.rowData(
|
||||
row, cst.DB_TEST_DURATION))
|
||||
log = self.rowData(row, cst.DB_TEST_LOG)
|
||||
if (log is None) or (log == ''):
|
||||
ret[self.KEY_LOG] = ''
|
||||
else:
|
||||
ret[self.KEY_LOG] = log
|
||||
|
||||
return ret
|
||||
|
||||
def extract_test(self, row):
|
||||
ret = {}
|
||||
for key in cst.DB_TEST_FIELDS:
|
||||
r = self.rowData(row, key)
|
||||
if isinstance(r, bytes):
|
||||
r = r.decode()
|
||||
ret[key] = r
|
||||
return ret
|
||||
72
src/testium/interpreter/test_report/report_export_html.py
Normal file
72
src/testium/interpreter/test_report/report_export_html.py
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
from lxml import (etree, html)
|
||||
import interpreter.test_report.report_export as rpe
|
||||
import interpreter.test_report.test_report as tr
|
||||
import interpreter.utils.constants as cst
|
||||
|
||||
class ReportExportHTML(rpe.ReportExport):
|
||||
|
||||
def __init__(self, name, report_db, report_file, pattern, key, no_header=False):
|
||||
super().__init__(name, report_db, report_file, pattern, key)
|
||||
|
||||
self.prepareFile()
|
||||
self.create_base()
|
||||
self.process_tests()
|
||||
with open(self._file_name, 'w') as f:
|
||||
f.write(html.tostring(self.root, pretty_print=True).decode())
|
||||
|
||||
def testsIterate(self, row):
|
||||
super().testsIterate(row)
|
||||
rdata = self.extract_info(row)
|
||||
trow = etree.SubElement(self.table, 'tr')
|
||||
for r in self.ROW_TEXTS:
|
||||
rh = etree.SubElement(trow, 'td')
|
||||
if r[self.KEY_INDEX] == self.KEY_DURATION:
|
||||
rh.text = '{:.4f}'.format(rdata[r[self.KEY_INDEX]])
|
||||
else:
|
||||
rh.text = rdata[r[self.KEY_INDEX]]
|
||||
|
||||
if rdata[self.KEY_LOG] != '':
|
||||
h2 = etree.SubElement(self.logsection, 'h3')
|
||||
h2.text = rdata[self.KEY_TITLE]
|
||||
for l in rdata[self.KEY_LOG].splitlines():
|
||||
p = etree.SubElement(self.logsection, 'p')
|
||||
p.text = l
|
||||
|
||||
def create_base(self):
|
||||
repname = self.header[cst.DB_TEST_SET_NAME]
|
||||
if self.name != '':
|
||||
repname = self.name
|
||||
|
||||
self.root = etree.Element('html', lang='en')
|
||||
head = etree.SubElement(self.root, 'head')
|
||||
title = etree.SubElement(head, 'title')
|
||||
title.text = repname
|
||||
self.body = etree.SubElement(self.root, 'body')
|
||||
h1 = etree.SubElement(self.body, 'h1')
|
||||
h1.text = repname
|
||||
|
||||
div = etree.SubElement(self.body, 'div')
|
||||
h2 = etree.SubElement(div, 'h2')
|
||||
h2.text = 'Test conditions'
|
||||
|
||||
for k in self.HEADER_TEXTS.keys():
|
||||
if k in self.header.keys():
|
||||
h = etree.SubElement(div, 'h3')
|
||||
h.text = self.HEADER_TEXTS[k]
|
||||
p = etree.SubElement(div, 'p')
|
||||
p.text = self.header[k]
|
||||
|
||||
div = etree.SubElement(self.body, 'div')
|
||||
h2 = etree.SubElement(div, 'h2')
|
||||
h2.text = 'Test results'
|
||||
|
||||
self.table = etree.SubElement(self.body, 'table')
|
||||
row = etree.SubElement(self.table, 'tr')
|
||||
for r in self.ROW_TEXTS:
|
||||
rh = etree.SubElement(row, 'th')
|
||||
rh.text = r[self.TEXT_INDEX]
|
||||
|
||||
self.logsection = etree.SubElement(self.body, 'div')
|
||||
h2 = etree.SubElement(self.logsection, 'h2')
|
||||
h2.text = 'Logs'
|
||||
32
src/testium/interpreter/test_report/report_export_json.py
Normal file
32
src/testium/interpreter/test_report/report_export_json.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
import interpreter.test_report.report_export as rpe
|
||||
import interpreter.utils.constants as cst
|
||||
|
||||
|
||||
class ReportExportJSON(rpe.ReportExport):
|
||||
|
||||
def __init__(self, name, report_db, report_file, pattern, key, no_header=False):
|
||||
super().__init__(name, report_db, report_file, pattern, key)
|
||||
|
||||
self._no_header = no_header
|
||||
self._tests = []
|
||||
self.prepareFile()
|
||||
self.process_tests()
|
||||
if no_header:
|
||||
if self.name != "":
|
||||
json_export = {self.name: self._tests}
|
||||
else:
|
||||
tests_name = "tests"
|
||||
if self.name != "":
|
||||
tests_name = self.name
|
||||
json_export = {"header": self.header}
|
||||
json_export.update({tests_name: self._tests})
|
||||
with open(self._file_name, "w", encoding="utf-8") as fd:
|
||||
fd.write(json.dumps(json_export, indent=4))
|
||||
|
||||
def testsIterate(self, row):
|
||||
super().testsIterate(row)
|
||||
r = self.extract_test(row)
|
||||
if r[cst.DB_TEST_DATA].strip().startswith("{"):
|
||||
r[cst.DB_TEST_DATA] = json.loads(r[cst.DB_TEST_DATA])
|
||||
self._tests.append(r)
|
||||
45
src/testium/interpreter/test_report/report_export_junit.py
Normal file
45
src/testium/interpreter/test_report/report_export_junit.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from junit_xml import (TestSuite, TestCase)
|
||||
import libs.testium as tm
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
import interpreter.test_report.report_export as rpe
|
||||
import interpreter.test_report.test_report as tr
|
||||
import interpreter.utils.constants as cst
|
||||
|
||||
|
||||
class ReportExportJUnit(rpe.ReportExport):
|
||||
|
||||
def __init__(self, name, report_db, report_file, pattern, key, no_header=False):
|
||||
super().__init__(name, report_db, report_file, pattern, key)
|
||||
|
||||
self.prepareFile()
|
||||
self.test_cases = []
|
||||
repname = self.header[cst.DB_TEST_SET_NAME]
|
||||
if self.name != '':
|
||||
repname = self.name
|
||||
self.process_tests()
|
||||
|
||||
ts = TestSuite(repname, test_cases=self.test_cases,
|
||||
hostname=tm.gd('host_ip'))
|
||||
with open(self._file_name, 'w') as f:
|
||||
TestSuite.to_file(f, [ts])
|
||||
|
||||
def testsIterate(self, row):
|
||||
super().testsIterate(row)
|
||||
rdata = self.extract_info(row)
|
||||
log = rdata[self.KEY_LOG]
|
||||
if log == '':
|
||||
log = rdata[self.KEY_MESSAGE]
|
||||
try:
|
||||
tc = TestCase(rdata[self.KEY_TITLE], elapsed_sec=rdata[self.KEY_DURATION],
|
||||
log=log, status=rdata[self.KEY_SUCCESS])
|
||||
# Workaround for old versions of os.
|
||||
except TypeError:
|
||||
tc = TestCase(rdata[self.KEY_TITLE], elapsed_sec=rdata[self.KEY_DURATION], stdout=log)
|
||||
if rdata[self.KEY_SUCCESS] == str(TestValue.FAILURE):
|
||||
m = rdata[self.KEY_MESSAGE]
|
||||
if m == '':
|
||||
m = 'test failure'
|
||||
tc.add_failure_info(output=m)
|
||||
elif rdata[self.KEY_SUCCESS] == str(TestValue.NORUN):
|
||||
tc.add_skipped_info('test skipped')
|
||||
self.test_cases.append(tc)
|
||||
127
src/testium/interpreter/test_report/report_export_txt.py
Normal file
127
src/testium/interpreter/test_report/report_export_txt.py
Normal file
@@ -0,0 +1,127 @@
|
||||
|
||||
from interpreter.test_items.test_result import (TestValue)
|
||||
import interpreter.test_report.report_export as rpe
|
||||
import interpreter.test_report.test_report as tr
|
||||
from interpreter.test_report.report_interface import (adapt_json, convert_json)
|
||||
from interpreter.utils.constants import TestItemType as cst_type
|
||||
import interpreter.utils.constants as cst
|
||||
|
||||
class ReportExportTxt(rpe.ReportExport):
|
||||
no_value_types = [cst_type.TYPE_CONSOLE.item_name, cst_type.TYPE_SLEEP.item_name,
|
||||
cst_type.TYPE_IMAGE_DLG.item_name, cst_type.TYPE_LET.item_name, cst_type.TYPE_CHECK,
|
||||
cst_type.TYPE_CYCLE.item_name, cst_type.TYPE_GROUP.item_name,
|
||||
cst_type.TYPE_UNITTEST_FILE.item_name, cst_type.TYPE_MESSAGE_DLG.item_name,
|
||||
cst_type.TYPE_QUESTION_DLG.item_name]
|
||||
|
||||
def __init__(self, name, report_db, report_file, pattern, key, no_header=False):
|
||||
super().__init__(name, report_db, report_file, pattern, key)
|
||||
|
||||
self.prepareFile()
|
||||
self._file_descriptor = open(self._file_name, 'w', encoding="utf-8")
|
||||
|
||||
if not no_header:
|
||||
self.write_header()
|
||||
self.process_tests()
|
||||
self.write_footer()
|
||||
|
||||
self._file_descriptor.close()
|
||||
|
||||
def testsIterate(self, row):
|
||||
super().testsIterate(row)
|
||||
level = self.rowData(row, cst.DB_TEST_LEVEL)
|
||||
if level > 0:
|
||||
succ = self.rowData(row, cst.DB_TEST_RESULT)
|
||||
msg = self.rowData(row, cst.DB_TEST_MESSAGE)
|
||||
tiname = self.rowData(row, cst.DB_TEST_NAME)
|
||||
j = self.rowData(row, cst.DB_TEST_DATA)
|
||||
if succ == str(TestValue.SUCCESS):
|
||||
msg = ''
|
||||
if succ != str(TestValue.NORUN):
|
||||
self.line_result(tiname, succ, msg, level)
|
||||
|
||||
ty = self.rowData(row, cst.DB_TEST_TYPE)
|
||||
if ty in self.no_value_types:
|
||||
pass
|
||||
else:
|
||||
if isinstance(j, bytes):
|
||||
j = convert_json(j)
|
||||
if isinstance(j, dict):
|
||||
for k, v in j.items():
|
||||
self.line_value(tiname, k, '=', v, level)
|
||||
|
||||
def addtxt(self, str):
|
||||
self._file_descriptor.write(str)
|
||||
|
||||
def separator(self):
|
||||
self.addtxt('=' * 60 + '\n')
|
||||
|
||||
def banner(self, level):
|
||||
if level <= 0:
|
||||
b = '='
|
||||
elif level == 1:
|
||||
b = '-'
|
||||
else:
|
||||
b = '- '
|
||||
|
||||
sstart = self.line_start(0)
|
||||
line = sstart + b * int((60 - len(sstart))/len(b))
|
||||
self.addtxt(line + '\n')
|
||||
|
||||
def write_header(self):
|
||||
repname = self.header[cst.DB_TEST_SET_NAME]
|
||||
if self.name != '':
|
||||
repname = self.name
|
||||
self.addtxt('Testium' + '\n')
|
||||
self.addtxt('{:^96}'.format(repname)+'\n')
|
||||
self.addtxt('{:^96}'.format(
|
||||
self.header[cst.DB_TESTRUN_DATE] + ' ' +
|
||||
self.header[cst.DB_TESTRUN_TIME]) + '\n\n\n')
|
||||
|
||||
def write_footer(self):
|
||||
|
||||
self.separator()
|
||||
self.addtxt('\n')
|
||||
succ = 'Not finished'
|
||||
if cst.DB_TEST_SET_RESULT in self.header:
|
||||
succ = self.header[cst.DB_TEST_SET_RESULT]
|
||||
|
||||
self.addtxt('{:<40}'.format('Overall test status')
|
||||
+ '{:>55}'.format(succ) + '\n\n\n')
|
||||
|
||||
self.addtxt('{:<40}'.format('Operator:')
|
||||
+ '{:<40}'.format('signature:') + '\n\n\n')
|
||||
|
||||
def line_text(self, text, level):
|
||||
self.addtxt('{:.<45}'.format(self.line_start(level))
|
||||
+ ': ' + text + '\n')
|
||||
|
||||
def line_begin(self, ti_name):
|
||||
sstart = self.line_start(0) + ' ' + ti_name
|
||||
self.addtxt('{:.<45}'.format(sstart) + ': test Begins' + '\n')
|
||||
|
||||
def line_result(self, ti_name, tresult, tmessage, level):
|
||||
sstart = ''
|
||||
if len(self.pattern) == 0:
|
||||
sstart = self.line_start(level) + ' '
|
||||
sstart = sstart + ti_name
|
||||
|
||||
if tresult == str(TestValue.SUCCESS) or tresult == str(TestValue.FAILURE):
|
||||
self.addtxt('{:.<45}'.format(sstart)
|
||||
+ ': {:<43}{:>5}'.format(tmessage,
|
||||
tresult) + '\n')
|
||||
|
||||
def line_value(self, title, key, sep, value, level):
|
||||
sstart = ''
|
||||
if len(self.pattern) == 0:
|
||||
sstart = self.line_start(level) + ' '
|
||||
sstart = '{:.<45}'.format(sstart + ' ' + title)
|
||||
self.addtxt('{:}: {:} {:} {:}'.format(sstart,
|
||||
str(key),
|
||||
str(sep),
|
||||
str(value)) + '\n')
|
||||
|
||||
def line_start(self, level):
|
||||
sstart = ''
|
||||
pat = ' |'
|
||||
sstart = pat * (level-1)
|
||||
return sstart
|
||||
7
src/testium/interpreter/test_report/report_interface.py
Normal file
7
src/testium/interpreter/test_report/report_interface.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import json
|
||||
|
||||
def adapt_json(data):
|
||||
return (json.dumps(data, sort_keys=True)).encode()
|
||||
|
||||
def convert_json(blob):
|
||||
return json.loads(blob.decode())
|
||||
344
src/testium/interpreter/test_report/test_report.py
Normal file
344
src/testium/interpreter/test_report/test_report.py
Normal file
@@ -0,0 +1,344 @@
|
||||
import os
|
||||
from functools import wraps
|
||||
import sqlite3
|
||||
from time import (time, sleep)
|
||||
import traceback
|
||||
from interpreter.utils.tum_except import (ETUMRuntimeError, ETUMSyntaxError)
|
||||
from interpreter.utils.stdout_redirect import stdio_redir
|
||||
from interpreter.utils.params import (expanse)
|
||||
from interpreter.utils.paths import prepare_file_to_save
|
||||
import interpreter.utils.constants as cst
|
||||
from interpreter.utils.constants import TestItemType as cst_type
|
||||
from interpreter.test_report.report_interface import (adapt_json, convert_json)
|
||||
|
||||
sqlite3.register_adapter(dict, adapt_json)
|
||||
sqlite3.register_adapter(list, adapt_json)
|
||||
sqlite3.register_adapter(tuple, adapt_json)
|
||||
sqlite3.register_converter('JSON', convert_json)
|
||||
|
||||
TEST_REPORT_FILE_REV = '0.1'
|
||||
|
||||
|
||||
def tr_procedure(f):
|
||||
@wraps(f)
|
||||
def wrapper(self, *args, **kwds):
|
||||
if not self._active:
|
||||
return
|
||||
return f(self, *args, **kwds)
|
||||
return wrapper
|
||||
|
||||
|
||||
class Export:
|
||||
|
||||
def __init__(self, dict_export, con=None):
|
||||
if (not isinstance(dict_export, dict)) or (len(dict_export) != 1):
|
||||
raise ETUMSyntaxError(
|
||||
'Syntax error in the report export description')
|
||||
|
||||
self.con = con
|
||||
self.type = list(dict_export.keys())[0]
|
||||
self.tum_pattern = dict_export[self.type].get('pattern', [])
|
||||
self.tum_key = dict_export[self.type].get('key', [])
|
||||
self.path = dict_export[self.type].get('path', '')
|
||||
self.filename = dict_export[self.type].get('file_name', '')
|
||||
|
||||
if len(self.tum_pattern) > 0:
|
||||
if not isinstance(self.tum_pattern, (list, str)):
|
||||
raise ETUMSyntaxError(
|
||||
'pattern must be a string or a list of string')
|
||||
if isinstance(self.tum_pattern, (str)):
|
||||
self.tum_pattern = [self.tum_pattern]
|
||||
|
||||
if len(self.tum_key) > 0:
|
||||
if not isinstance(self.tum_key, (list, str)):
|
||||
raise ETUMSyntaxError(
|
||||
'pattern must be a string or a list of string')
|
||||
if isinstance(self.tum_key, (str)):
|
||||
self.tum_key = [self.tum_key]
|
||||
|
||||
def exec(self, con=None, name : str ='', no_header: bool = False):
|
||||
if con is None:
|
||||
con = self.con
|
||||
|
||||
if con is None:
|
||||
return
|
||||
|
||||
pats = []
|
||||
for p in self.tum_pattern:
|
||||
pats.append(expanse(p))
|
||||
|
||||
keys = []
|
||||
for k in self.tum_key:
|
||||
keys.append(expanse(k))
|
||||
|
||||
et = expanse(self.type)
|
||||
path = expanse(self.path)
|
||||
fname = expanse(self.filename)
|
||||
if fname != '' and path == '':
|
||||
path = fname
|
||||
elif fname == '' and path != '':
|
||||
pass
|
||||
else:
|
||||
path = os.path.join(path, fname)
|
||||
|
||||
if et == cst.REP_TYPE_TEXT:
|
||||
from interpreter.test_report.report_export_txt import ReportExportTxt
|
||||
ReportExportTxt(name, con, path, pats, keys, no_header)
|
||||
elif et == cst.REP_TYPE_JSON:
|
||||
from interpreter.test_report.report_export_json import ReportExportJSON
|
||||
ReportExportJSON(name, con, path, pats, keys, no_header)
|
||||
elif et == cst.REP_TYPE_JUNIT:
|
||||
try:
|
||||
from interpreter.test_report.report_export_junit import ReportExportJUnit
|
||||
ReportExportJUnit(name, con, path, pats, keys, no_header)
|
||||
except ModuleNotFoundError:
|
||||
raise ETUMRuntimeError('"junit_xml" module not available')
|
||||
elif et == cst.REP_TYPE_HTML:
|
||||
try:
|
||||
from interpreter.test_report.report_export_html import ReportExportHTML
|
||||
ReportExportHTML(name, con, path, pats, keys, no_header)
|
||||
except ModuleNotFoundError:
|
||||
raise ETUMRuntimeError('"lxml" module not available')
|
||||
elif et == cst.REP_TYPE_SQLITE:
|
||||
pass
|
||||
else:
|
||||
raise ETUMSyntaxError('Report export not recognized')
|
||||
|
||||
class TestReport:
|
||||
TEST_COLS = [[cst.DB_TEST_TIMESTAMP_START, 'INT'],
|
||||
[cst.DB_TEST_ID, 'INT NOT NULL'],
|
||||
[cst.DB_TEST_PARENT_ID, 'INT'],
|
||||
[cst.DB_TEST_LEVEL, 'INT'],
|
||||
[cst.DB_TEST_NAME, 'TEXT'],
|
||||
[cst.DB_TEST_TYPE, 'TEXT'],
|
||||
[cst.DB_TEST_KEY, 'TEXT'],
|
||||
[cst.DB_TEST_RESULT, 'TEXT'],
|
||||
[cst.DB_TEST_MESSAGE, 'TEXT'],
|
||||
[cst.DB_TEST_DURATION, 'INT'],
|
||||
[cst.DB_TEST_LOG, 'TEXT'],
|
||||
[cst.DB_TEST_DATA, 'JSON'],
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def indexOf(cls, name):
|
||||
i = 0
|
||||
for l in cls.TEST_COLS:
|
||||
if l[0] == name:
|
||||
break
|
||||
i = i + 1
|
||||
return i
|
||||
|
||||
@classmethod
|
||||
def export_to_dict(cls, etype, filename, path, pattern, key):
|
||||
return {etype: {'file_name': filename, 'path': path,
|
||||
'pattern': pattern, 'key': key}}
|
||||
|
||||
def __init__(self, dict_report):
|
||||
self._path = ""
|
||||
self.tum_path = ''
|
||||
self.has_sqlite = False
|
||||
self._active = True
|
||||
self.export = []
|
||||
self.tum_export = []
|
||||
self._level = 0
|
||||
self._log_stored = False
|
||||
self._con = None
|
||||
|
||||
if dict_report is None:
|
||||
self._active = False
|
||||
return
|
||||
|
||||
# Process parameters
|
||||
a = expanse(dict_report.get('enabled', True))
|
||||
if isinstance(a, bool):
|
||||
self._active = a
|
||||
else:
|
||||
if str(a).lower() == 'false':
|
||||
self._active = False
|
||||
|
||||
if self._active:
|
||||
self.dict_report = dict_report
|
||||
ls = expanse(dict_report.get('log_stored', False))
|
||||
if isinstance(ls, bool):
|
||||
self._log_stored = ls
|
||||
else:
|
||||
if str(ls).lower() == 'true':
|
||||
self._log_stored = True
|
||||
|
||||
exports = self.dict_report.get('export', [])
|
||||
if isinstance(exports, dict):
|
||||
exports = [{k: v} for k, v in exports.items()]
|
||||
for exp in exports:
|
||||
self.add_export(self.tum_export, exp)
|
||||
|
||||
if self._log_stored:
|
||||
stdio_redir.intercept()
|
||||
|
||||
# Path
|
||||
@property
|
||||
def path(self):
|
||||
ret = self.tum_path
|
||||
if self._path != '':
|
||||
ret = self._path
|
||||
return ret
|
||||
|
||||
@path.setter
|
||||
def path(self, value):
|
||||
self._path = value
|
||||
if (self._path != '') and (self._active == False):
|
||||
self._log_stored = True
|
||||
self._active = True
|
||||
stdio_redir.intercept()
|
||||
|
||||
for exp in self.exports:
|
||||
exp.path = self.path
|
||||
|
||||
# export
|
||||
@property
|
||||
def exports(self):
|
||||
ret = self.tum_export
|
||||
if len(self.export) > 0:
|
||||
ret = self.export
|
||||
return ret
|
||||
|
||||
@exports.setter
|
||||
def exports(self, exp):
|
||||
self.add_export(self.export, exp)
|
||||
if (len(self.export) > 0):
|
||||
self._active = True
|
||||
|
||||
def add_export(self, elist, exp):
|
||||
e = Export(exp)
|
||||
elist.append(e)
|
||||
if e.type == cst.REP_TYPE_SQLITE:
|
||||
self.has_sqlite = True
|
||||
self.tum_path = e.path
|
||||
if e.filename != '':
|
||||
self.tum_path = os.path.join(self.tum_path, e.filename)
|
||||
|
||||
@property
|
||||
def db_connection(self):
|
||||
return self._con
|
||||
|
||||
@tr_procedure
|
||||
def open(self, header):
|
||||
|
||||
rep_path = self.path
|
||||
if not self.has_sqlite:
|
||||
rep_path = ':memory:'
|
||||
else:
|
||||
rep_path = expanse(rep_path)
|
||||
prepare_file_to_save(rep_path)
|
||||
if not os.path.exists(os.path.dirname(rep_path)):
|
||||
raise ETUMRuntimeError("Report path does not exist: " + rep_path)
|
||||
self._con = sqlite3.connect(rep_path)
|
||||
self.createHeader(header)
|
||||
self.createTestTable()
|
||||
self._con.commit()
|
||||
|
||||
@tr_procedure
|
||||
def close(self, header):
|
||||
try:
|
||||
try:
|
||||
for k, v in header.items():
|
||||
self._con.execute(
|
||||
"INSERT INTO header VALUES('{}', '{}')".format(k, v))
|
||||
self._con.commit()
|
||||
|
||||
# stop stdout interception thread
|
||||
stdio_redir.stop()
|
||||
|
||||
for export in self.exports:
|
||||
export.exec(self._con)
|
||||
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
finally:
|
||||
self._con.close()
|
||||
|
||||
@tr_procedure
|
||||
def createHeader(self, header):
|
||||
self._con.execute("CREATE TABLE header(key TEXT, value TEXT)")
|
||||
self._con.execute("INSERT INTO header VALUES(?, ?)", (cst.DB_REPORT_VERSION,
|
||||
TEST_REPORT_FILE_REV))
|
||||
for k, v in header.items():
|
||||
self._con.execute(
|
||||
"INSERT INTO header VALUES('{}', '{}')".format(k, v))
|
||||
|
||||
@tr_procedure
|
||||
def createTestTable(self):
|
||||
req = ''
|
||||
for l in self.TEST_COLS:
|
||||
req = req + l[0] + ' ' + l[1] + ','
|
||||
req = req[:-1]
|
||||
self._con.execute('CREATE TABLE tests(' + req + ')')
|
||||
|
||||
@tr_procedure
|
||||
def addTest(self, test_item, result, key=None):
|
||||
p = test_item.parent()
|
||||
pid = None
|
||||
if p is not None:
|
||||
pid = p.id()
|
||||
param = ()
|
||||
for l in self.TEST_COLS:
|
||||
if l[0] == cst.DB_TEST_TIMESTAMP_START:
|
||||
param = param + (test_item.t0,)
|
||||
elif l[0] == cst.DB_TEST_ID:
|
||||
param = param + (test_item.id(),)
|
||||
elif l[0] == cst.DB_TEST_PARENT_ID:
|
||||
param = param + (pid,)
|
||||
elif l[0] == cst.DB_TEST_NAME:
|
||||
param = param + (test_item.name(),)
|
||||
elif l[0] == cst.DB_TEST_TYPE:
|
||||
param = param + (test_item.type(),)
|
||||
elif l[0] == cst.DB_TEST_KEY:
|
||||
skey = key
|
||||
if isinstance(key, list):
|
||||
skey = ""
|
||||
for k in key:
|
||||
skey += f"{k}, "
|
||||
skey = skey if len(key) == 0 else skey[:-len(", ")]
|
||||
param = param + (skey,)
|
||||
elif l[0] == cst.DB_TEST_RESULT:
|
||||
param = param + (str(result.test_result),)
|
||||
elif l[0] == cst.DB_TEST_MESSAGE:
|
||||
param = param + (str(result.message),)
|
||||
elif l[0] == cst.DB_TEST_DURATION:
|
||||
param = param + (test_item.duration,)
|
||||
elif l[0] == cst.DB_TEST_DATA:
|
||||
param = param + (result.reported,)
|
||||
elif l[0] == cst.DB_TEST_LEVEL:
|
||||
param = param + (self._level,)
|
||||
elif l[0] == cst.DB_TEST_LOG:
|
||||
if self._log_stored and (test_item.type() != cst_type.TYPE_ROOT.item_name):
|
||||
lo = ''
|
||||
pat = test_item.footer
|
||||
t0 = time()
|
||||
while pat != "":
|
||||
lo = lo + stdio_redir.read()
|
||||
if (pat in lo):
|
||||
break
|
||||
if (time() - t0) < 10.0:
|
||||
sleep(0.1)
|
||||
else:
|
||||
break
|
||||
|
||||
param = param + (lo,)
|
||||
else:
|
||||
param = param + ('',)
|
||||
else:
|
||||
raise ETUMRuntimeError('unknow database key')
|
||||
|
||||
req = 'INSERT INTO tests VALUES('
|
||||
for l in self.TEST_COLS:
|
||||
req = req + '?,'
|
||||
req = req[:-1] + ')'
|
||||
|
||||
self._con.execute(req, param)
|
||||
|
||||
def incLevel(self):
|
||||
self._level = self._level + 1
|
||||
|
||||
def decLevel(self):
|
||||
if self._level > 0:
|
||||
self._level = self._level - 1
|
||||
491
src/testium/interpreter/test_set.py
Normal file
491
src/testium/interpreter/test_set.py
Normal file
@@ -0,0 +1,491 @@
|
||||
import os
|
||||
import datetime
|
||||
from queue import Queue
|
||||
from interpreter.utils.params import expanse
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import (
|
||||
ETUMSyntaxError,
|
||||
)
|
||||
import interpreter.utils.settings as prefs
|
||||
from interpreter.test_report.test_report import TestReport
|
||||
from interpreter.utils.func_exec import func_exec
|
||||
from interpreter.utils.constants import TestItemType as cst_type
|
||||
import interpreter.utils.constants as cst
|
||||
from interpreter.utils.constants import TEST_TYPE_LIST
|
||||
from interpreter.test_items.test_item import test_data
|
||||
from interpreter.test_items.item_actions import TestItemActions
|
||||
from interpreter.test_items.test_result import TestValue
|
||||
|
||||
|
||||
class TestSet:
|
||||
def __init__(
|
||||
self,
|
||||
tum_fime: str,
|
||||
test_dict: dict,
|
||||
status_queue: Queue,
|
||||
):
|
||||
self._test_file = tum_fime
|
||||
self.post_exec_file = None
|
||||
|
||||
self._report = None
|
||||
self._success = False
|
||||
self.status_queue = status_queue
|
||||
self.report_path = ""
|
||||
self.report_type = ""
|
||||
self.report_pattern = []
|
||||
self._testdict = test_dict
|
||||
|
||||
self._tree = self.__loadTestTree(tum_fime)
|
||||
self.dict_report = self._testdict.get("report", None)
|
||||
self.set_post_exec()
|
||||
|
||||
def execute(self):
|
||||
self._report = TestReport(self.dict_report)
|
||||
report_header = {
|
||||
cst.DB_TEST_FILE: os.path.abspath(self._test_file),
|
||||
cst.DB_TEST_SET_NAME: os.path.splitext(os.path.split(self._test_file)[1])[
|
||||
0
|
||||
],
|
||||
cst.DB_TEST_REVISION: tm.gd("test_version"),
|
||||
cst.DB_SEQUENCER_VERSION: tm.gd("testium_version"),
|
||||
cst.DB_TESTRUN_DATE: tm.gd("testrun_date"),
|
||||
cst.DB_TESTRUN_TIME: tm.gd("testrun_time"),
|
||||
}
|
||||
if self.report_type != "":
|
||||
rep = TestReport.export_to_dict(
|
||||
self.report_type, "", self.report_path, self.report_pattern, []
|
||||
)
|
||||
self._report.exports = rep
|
||||
self._report.open(report_header)
|
||||
self.setReport()
|
||||
|
||||
res = None
|
||||
try:
|
||||
a_test_is_skipped = self.__aTestIsSkipped(self._rootItem)
|
||||
a_test_is_disabled = self.__aTestIsDisabled(self._rootItem)
|
||||
res = self._rootItem.execute()
|
||||
finally:
|
||||
self._end_test_date = datetime.datetime.now()
|
||||
self._test_duration = self._end_test_date - tm.gd("start_test_date")
|
||||
|
||||
# report ending
|
||||
d = {}
|
||||
if res is not None:
|
||||
self._success = res.test_result == TestValue.SUCCESS
|
||||
d.update({cst.DB_TEST_SET_RESULT: str(res.test_result)})
|
||||
d.update({cst.DB_TEST_SET_DURATION: self._test_duration})
|
||||
self._report.close(d)
|
||||
|
||||
# updating global dict with report output
|
||||
outs = tm.gd("test_outputs", None)
|
||||
if outs is not None:
|
||||
outs.append(self._report.path)
|
||||
else:
|
||||
outs = [self._report.path]
|
||||
|
||||
# test cleanup
|
||||
del self._report
|
||||
|
||||
# updating global dict with outputs
|
||||
tm.setgd("test_outputs", outs)
|
||||
|
||||
tm.cleanup_instances("console")
|
||||
tm.cleanup_instances("plot")
|
||||
|
||||
if a_test_is_skipped or a_test_is_disabled:
|
||||
tm.print_warn("A test has been skipped or disabled in this test run.")
|
||||
|
||||
def set_report(self, rep_path: str, rep_type: str, pattern: list):
|
||||
if rep_path != "":
|
||||
self.report_path = rep_path
|
||||
self.report_type = rep_type
|
||||
self.report_pattern = pattern
|
||||
|
||||
def success(self) -> bool:
|
||||
"Returns if the test has been a success"
|
||||
return self._success
|
||||
|
||||
def extractReportPath(self):
|
||||
r = ""
|
||||
if self.dict_report is None:
|
||||
return r
|
||||
|
||||
n = self.dict_report.get("file_name", "")
|
||||
if n == "":
|
||||
return r
|
||||
|
||||
n = expanse(n)
|
||||
|
||||
f = expanse(self.dict_report.get("path", ""))
|
||||
if f == "":
|
||||
f = expanse(prefs.settings.report_path)
|
||||
|
||||
if not os.path.isabs(f):
|
||||
f = os.path.abspath(f)
|
||||
if not os.path.exists(f):
|
||||
os.makedirs(f)
|
||||
|
||||
f = os.path.join(f, n)
|
||||
return f
|
||||
|
||||
def __stopRunningTestsRecursively(self, parent):
|
||||
for i in range(parent.childCount()):
|
||||
if parent.child(i).isRunning():
|
||||
parent.child(i).stop()
|
||||
self.__stopRunningTestsRecursively(parent.child(i))
|
||||
|
||||
def stop(self):
|
||||
self._rootItem.stop()
|
||||
self.__stopRunningTestsRecursively(self._rootItem)
|
||||
|
||||
def __pauseTestsRecursively(self, parent):
|
||||
for i in range(parent.childCount()):
|
||||
if parent.child(i).isRunning():
|
||||
parent.child(i).pause()
|
||||
self.__pauseTestsRecursively(parent.child(i))
|
||||
|
||||
def pause(self):
|
||||
self._rootItem.pause()
|
||||
self.__pauseTestsRecursively(self._rootItem)
|
||||
|
||||
def __setReportRecursively(self, parent):
|
||||
for i in range(parent.childCount()):
|
||||
parent.child(i).report = self._report
|
||||
self.__setReportRecursively(parent.child(i))
|
||||
|
||||
def setReport(self):
|
||||
self._rootItem.report = self._report
|
||||
self.__setReportRecursively(self._rootItem)
|
||||
|
||||
def addBreakpoint(self, item_id):
|
||||
item = self.__findItemById(item_id)
|
||||
item.addBreakpoint()
|
||||
|
||||
def delBreakpoint(self, item_id):
|
||||
item = self.__findItemById(item_id)
|
||||
item.delBreakpoint()
|
||||
|
||||
def __continueTestsRecursively(self, parent):
|
||||
for i in range(parent.childCount()):
|
||||
if parent.child(i).isRunning():
|
||||
parent.child(i).cont()
|
||||
self.__continueTestsRecursively(parent.child(i))
|
||||
|
||||
def cont(self):
|
||||
self._rootItem.cont()
|
||||
self.__continueTestsRecursively(self._rootItem)
|
||||
|
||||
def updateParentsState(self, child):
|
||||
parent = child.parent()
|
||||
if parent is not None:
|
||||
n = parent.childCount()
|
||||
all_unchecked = True
|
||||
one_checked = False
|
||||
for i in range(n):
|
||||
if parent.child(i).enabled:
|
||||
all_unchecked = False
|
||||
else:
|
||||
one_checked = True
|
||||
if (n > 0) and all_unchecked:
|
||||
parent.enabled = False
|
||||
self.updateParentsState(parent)
|
||||
|
||||
elif n > 0:
|
||||
parent.enabled = True
|
||||
self.updateParentsState(parent)
|
||||
|
||||
def __aTestIsSkipped(self, parent):
|
||||
res = False
|
||||
i = 0
|
||||
while (res is False) and (i < parent.childCount()):
|
||||
if parent.child(i).skipped:
|
||||
res = True
|
||||
i = i + 1
|
||||
|
||||
i = 0
|
||||
while (res is False) and (i < parent.childCount()):
|
||||
res = self.__aTestIsSkipped(parent.child(i))
|
||||
i = i + 1
|
||||
|
||||
return res
|
||||
|
||||
def __aTestIsDisabled(self, parent):
|
||||
res = False
|
||||
i = 0
|
||||
while (res is False) and (i < parent.childCount()):
|
||||
if not parent.child(i).enabled:
|
||||
res = True
|
||||
i = i + 1
|
||||
|
||||
i = 0
|
||||
while (res is False) and (i < parent.childCount()):
|
||||
res = self.__aTestIsDisabled(parent.child(i))
|
||||
i = i + 1
|
||||
|
||||
return res
|
||||
|
||||
def __findItemByIdRecursively(self, item_id, parent):
|
||||
res = None
|
||||
i = 0
|
||||
while (res is None) and (i < parent.childCount()):
|
||||
if parent.child(i).id() == item_id:
|
||||
res = parent.child(i)
|
||||
i = i + 1
|
||||
|
||||
i = 0
|
||||
while (res is None) and (i < parent.childCount()):
|
||||
res = self.__findItemByIdRecursively(item_id, parent.child(i))
|
||||
i = i + 1
|
||||
|
||||
return res
|
||||
|
||||
def __findItemById(self, item_id):
|
||||
item = self.__findItemByIdRecursively(item_id, self._rootItem)
|
||||
return item
|
||||
|
||||
def getEnabledState(self, item_id):
|
||||
"""Return True if the item is enabled, False otherwise."""
|
||||
item = self.__findItemById(item_id)
|
||||
return item.enabled
|
||||
|
||||
def getSkippedState(self, item_id):
|
||||
"""Return True if the item is skipped, False otherwise."""
|
||||
item = self.__findItemById(item_id)
|
||||
return item.skipped
|
||||
|
||||
def getItemDoc(self, item_id):
|
||||
item = self.__findItemById(item_id)
|
||||
return item.doc()
|
||||
|
||||
def getFolded(self, item_id):
|
||||
item = self.__findItemById(item_id)
|
||||
return item.is_folded
|
||||
|
||||
def setEnabledState(self, item_id, enabled_state, unitary=False):
|
||||
"""Set the item_id item enabled attributes to enabled_state."""
|
||||
parent = self.__findItemById(item_id)
|
||||
parent.enabled = enabled_state
|
||||
if not unitary:
|
||||
for i in range(parent.childCount()):
|
||||
parent.child(i).enabled = enabled_state
|
||||
self.enableDisableAll(parent.child(i), enabled_state)
|
||||
self.updateParentsState(parent)
|
||||
|
||||
def checkUncheckAll(self, checked: bool):
|
||||
self.enableDisableAll(self._rootItem, checked)
|
||||
|
||||
def enableDisableAll(self, parent, enabled_state):
|
||||
"""If enabled_state, enable all the child of parent item, else disable them."""
|
||||
if enabled_state:
|
||||
for i in range(parent.childCount()):
|
||||
parent.child(i).enabled = True
|
||||
self.enableDisableAll(parent.child(i), enabled_state)
|
||||
else:
|
||||
for i in range(parent.childCount()):
|
||||
parent.child(i).enabled = False
|
||||
self.enableDisableAll(parent.child(i), enabled_state)
|
||||
|
||||
def __loadTestTree(self, filename):
|
||||
try:
|
||||
dict_main = self._testdict["main"]
|
||||
except:
|
||||
raise ETUMSyntaxError(
|
||||
f"the 'main' root item of the principal 'tum' file could not be found.",
|
||||
filename
|
||||
)
|
||||
|
||||
self._rootItem = (cst_type.TYPE_ROOT.item_class)(
|
||||
dict_item=dict_main, status_queue=self.status_queue
|
||||
)
|
||||
ret = self.load_test_recursively(self._rootItem, dict_main, filename)
|
||||
self.set_post_exec()
|
||||
return ret
|
||||
|
||||
def set_post_exec(self):
|
||||
post_exec = self._testdict.get("post_execution", None)
|
||||
if post_exec is None:
|
||||
if self.post_exec_file is not None:
|
||||
self.post_exec_file = None
|
||||
return
|
||||
|
||||
postexec_file = post_exec["file_name"]
|
||||
|
||||
if not os.path.isfile(os.path.join(self._testDir, postexec_file)):
|
||||
raise ETUMSyntaxError(f"Post execution file '{postexec_file}' not found")
|
||||
|
||||
self.post_exec_file = postexec_file
|
||||
|
||||
def run_post_exec(self):
|
||||
|
||||
post_exec_file = self.post_exec_file
|
||||
test_dir = tm.gd("test_directory")
|
||||
|
||||
if post_exec_file is None:
|
||||
post_exec_file = os.path.join(test_dir, "post_execution.py")
|
||||
|
||||
if not os.path.isfile(post_exec_file):
|
||||
tm.print_info(f"No post exec in this test.")
|
||||
tm.print_debug(f' No file: "{post_exec_file}".')
|
||||
return
|
||||
|
||||
tm.print_debug(f'Post-execution from: "{post_exec_file}"')
|
||||
if self.rootItem().result.success:
|
||||
# tests backup is done here
|
||||
succ, res = func_exec(post_exec_file, "post_exec", [])
|
||||
if not succ == TestValue.SUCCESS:
|
||||
tm.print_debug(
|
||||
f"Test success but the \"post_exec\" function failed: {res}"
|
||||
)
|
||||
else:
|
||||
succ, res = func_exec(post_exec_file, "post_exec_fail", [])
|
||||
if not succ == TestValue.SUCCESS:
|
||||
tm.print_debug(
|
||||
f"Test failed but the \"post_exec_fail\" function failed: {res}"
|
||||
)
|
||||
|
||||
|
||||
def rootItem(self):
|
||||
return self._rootItem
|
||||
|
||||
def load_test_recursively(self, tree_parent, parent_seq, file_name):
|
||||
ret = {}
|
||||
try:
|
||||
parent_seq_name = parent_seq["name"]
|
||||
except KeyError:
|
||||
parent_seq["name"] = "sequence"
|
||||
except TypeError:
|
||||
raise ETUMSyntaxError(
|
||||
f"No 'name' attribute in '{tree_parent.type()}' (a child of '{tree_parent.parent().name()}')",
|
||||
file_name
|
||||
)
|
||||
try:
|
||||
parent_seq_actions = parent_seq["steps"]
|
||||
except KeyError:
|
||||
raise ETUMSyntaxError(
|
||||
f"No step list found for '{parent_seq_name}' sequence. \n" +
|
||||
f"Check the syntax of the 'steps' parameter of the '{tree_parent.cmd()}' test item definition.",
|
||||
file_name
|
||||
)
|
||||
# if action is a dictionary , we assume it is a single action
|
||||
# that has not been nested in a list, so do it
|
||||
if isinstance(parent_seq_actions, (dict)):
|
||||
parent_seq_actions = [parent_seq_actions]
|
||||
if not isinstance(parent_seq_actions, (list, tuple)):
|
||||
raise ETUMSyntaxError(
|
||||
f"No valid list of actions in sequence {parent_seq_name}",
|
||||
file_name
|
||||
)
|
||||
# first we merged to the same level 'sequence dict entries and list within the list
|
||||
counter = 0
|
||||
test_dir = tm.gd("test_directory")
|
||||
la = len(parent_seq_actions)
|
||||
while counter < la:
|
||||
action = parent_seq_actions[counter]
|
||||
# if action is a list raise up to the the same level,
|
||||
# ie insert action element into the parent_seq_actions
|
||||
if isinstance(action, (list, tuple)):
|
||||
parent_seq_actions[counter : counter + 1] = action
|
||||
parent_seq_actions = (
|
||||
parent_seq_actions[:counter]
|
||||
+ action
|
||||
+ parent_seq_actions[counter + 1 :]
|
||||
)
|
||||
la = len(parent_seq_actions)
|
||||
continue
|
||||
# if action is a NoneType skip and continue
|
||||
# (when pointing to an unused alias for instance)
|
||||
if action is None:
|
||||
counter += 1
|
||||
continue
|
||||
# if action is a sequence we insert its entry into the action list
|
||||
if "sequence" in action:
|
||||
sequence = action["sequence"]["data"]
|
||||
f = action["sequence"]["filename"]
|
||||
if isinstance(sequence, dict):
|
||||
sequence = [{k: v} for k, v in sequence.items()]
|
||||
# Case of an empty sequence
|
||||
elif sequence is None:
|
||||
tm.print_info(
|
||||
f"An empty sequence is loaded in '{parent_seq_name}'."
|
||||
)
|
||||
sequence = []
|
||||
elif not isinstance(sequence, list):
|
||||
raise ETUMSyntaxError(
|
||||
f"Syntax error in '{parent_seq_name}' step number {counter+1}. Sequence definition: '{str(action)}'",
|
||||
f
|
||||
)
|
||||
for s in sequence:
|
||||
s[list(s.keys())[0]]["seq_filename"] = f
|
||||
parent_seq_actions = (
|
||||
parent_seq_actions[:counter]
|
||||
+ sequence
|
||||
+ parent_seq_actions[counter + 1 :]
|
||||
)
|
||||
la = len(parent_seq_actions)
|
||||
continue
|
||||
|
||||
# Action is now for sure a list of dict of length 1
|
||||
k = list(action.keys())[0]
|
||||
if action[k].get("seq_filename", None) is None:
|
||||
action[k]["seq_filename"] = file_name
|
||||
|
||||
executed = False
|
||||
for it in TEST_TYPE_LIST:
|
||||
# Test items not executable
|
||||
if (
|
||||
(it == cst_type.TYPE_ROOT)
|
||||
or
|
||||
# Items which don't have to be loaded by test_set module
|
||||
(it.item_class is None)
|
||||
):
|
||||
continue
|
||||
if (it.item_cmd in action) or (
|
||||
(cst.FOLDED_CHAR + it.item_cmd) in action
|
||||
):
|
||||
executed = True
|
||||
is_folded = False
|
||||
action_name = it.item_cmd
|
||||
|
||||
# Check if a "." is before the cmd_name (meaning folded)
|
||||
if (cst.FOLDED_CHAR + it.item_cmd) in action:
|
||||
is_folded = True
|
||||
action_name = cst.FOLDED_CHAR + it.item_cmd
|
||||
|
||||
seq_filename = action[action_name]["seq_filename"]
|
||||
item = (it.item_class)(
|
||||
action[action_name],
|
||||
tree_parent,
|
||||
self.status_queue,
|
||||
filename=seq_filename
|
||||
)
|
||||
item.is_folded = is_folded
|
||||
child = {}
|
||||
# case where the test item loads itself its descendants
|
||||
if it == cst_type.TYPE_UNITTEST_FILE:
|
||||
item.setTestDir(test_dir)
|
||||
child = item.load()
|
||||
elif issubclass(it.item_class, TestItemActions):
|
||||
child = item.load()
|
||||
# case where the test item is an items container
|
||||
elif item.is_container:
|
||||
child = self.load_test_recursively(
|
||||
item, action[action_name], seq_filename
|
||||
)
|
||||
|
||||
ret.update(test_data(item, child))
|
||||
|
||||
if not executed:
|
||||
raise ETUMSyntaxError(
|
||||
f"test item '{k}' is not known.",
|
||||
action[k]["seq_filename"]
|
||||
)
|
||||
|
||||
counter += 1
|
||||
|
||||
return ret
|
||||
|
||||
def tree(self):
|
||||
return self._tree
|
||||
|
||||
def skipped_state(self):
|
||||
ret = {}
|
||||
13
src/testium/interpreter/utils/__init__.py
Normal file
13
src/testium/interpreter/utils/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
def clear_recursively(obj):
|
||||
if not isinstance(obj, (dict, list)):
|
||||
del obj
|
||||
return
|
||||
if isinstance(obj, list):
|
||||
for o in obj:
|
||||
clear_recursively(o)
|
||||
else:
|
||||
for key in list(obj.keys()):
|
||||
clear_recursively(obj[key])
|
||||
o = obj.pop(key, None)
|
||||
9
src/testium/interpreter/utils/api.py
Normal file
9
src/testium/interpreter/utils/api.py
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
SUPPORTED_API = [
|
||||
"gd",
|
||||
"setgd",
|
||||
"delgd",
|
||||
"add_plot_values",
|
||||
"last_plot_value"
|
||||
]
|
||||
|
||||
23
src/testium/interpreter/utils/api_srv.py
Normal file
23
src/testium/interpreter/utils/api_srv.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from interpreter.utils.api import SUPPORTED_API
|
||||
|
||||
import libs.testium as tm
|
||||
|
||||
# Fill the api_dict with the function of tm
|
||||
api_dict = {k: getattr(tm, k) for k in SUPPORTED_API if hasattr(tm, k)}
|
||||
|
||||
|
||||
def api_request(method, params):
|
||||
if method in api_dict.keys():
|
||||
if params is None:
|
||||
params = []
|
||||
if not isinstance(params, list):
|
||||
params = [params]
|
||||
try:
|
||||
return {"result": api_dict[method](*params)}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
elif method == "print":
|
||||
print(*params, end="")
|
||||
return {"result": 0}
|
||||
else:
|
||||
return {"error": "unsupported function"}
|
||||
133
src/testium/interpreter/utils/constants.py
Normal file
133
src/testium/interpreter/utils/constants.py
Normal file
@@ -0,0 +1,133 @@
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class TestItemEnum():
|
||||
def __init__(self, cmd, name, item_class=None) -> None:
|
||||
self.name = name
|
||||
self.cmd = cmd
|
||||
self.item_class = item_class
|
||||
|
||||
class TestItemType(Enum):
|
||||
TYPE_UNITTEST_FILE = TestItemEnum("unittest_file", "unittest file")
|
||||
TYPE_UNITTEST_STEP = TestItemEnum("unittest_step", "unittest step")
|
||||
TYPE_CONSOLE = TestItemEnum("console", "Console")
|
||||
TYPE_CONSOLE_ACTION = TestItemEnum("console_action", "Console action")
|
||||
TYPE_CYCLE = TestItemEnum("loop", "Cycle")
|
||||
TYPE_FUNCTION = TestItemEnum("py_func", "python Function")
|
||||
TYPE_REPORT = TestItemEnum("report", "Report")
|
||||
TYPE_GIT = TestItemEnum("git", "git repository")
|
||||
TYPE_GRAPH = TestItemEnum("plot", "Runtime plot")
|
||||
TYPE_GRAPH_ACTION = TestItemEnum("plot_action", "Runtime plot action")
|
||||
TYPE_GROUP = TestItemEnum("group", "Group")
|
||||
TYPE_IMAGE_DLG = TestItemEnum("dialog_image", "Image Dialog")
|
||||
TYPE_MESSAGE_DLG = TestItemEnum("dialog_message", "Message Dialog")
|
||||
TYPE_LET = TestItemEnum("let", "Let")
|
||||
TYPE_CHECK = TestItemEnum("check", "Check value")
|
||||
TYPE_NOTE_DLG = TestItemEnum("dialog_note", "Note Dialog")
|
||||
TYPE_QUESTION_DLG = TestItemEnum("dialog_question", "Question Dialog")
|
||||
TYPE_SLEEP = TestItemEnum("sleep", "Sleep")
|
||||
TYPE_REFERENCE_DLG = TestItemEnum("dialog_references", "References Dialog")
|
||||
TYPE_VALUE_DLG = TestItemEnum("dialog_value", "Value Dialog")
|
||||
TYPE_CHOICES_DLG = TestItemEnum("dialog_choices", "Choices Dialog")
|
||||
TYPE_RUN = TestItemEnum("run", "Run tum")
|
||||
TYPE_JSON_RPC = TestItemEnum("json_rpc", "JSON-RPC")
|
||||
TYPE_JSON_RPC_ACTION = TestItemEnum("json_rpc_action", "JSON-RPC action")
|
||||
TYPE_ROOT = TestItemEnum("default", "default")
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
return list(map(lambda c: c.value, TestItemType))
|
||||
|
||||
@property
|
||||
def item_name(self):
|
||||
return self.value.name
|
||||
|
||||
@property
|
||||
def item_cmd(self):
|
||||
return self.value.cmd
|
||||
|
||||
@property
|
||||
def item_class(self):
|
||||
return self.value.item_class
|
||||
|
||||
@item_class.setter
|
||||
def item_class(self, c):
|
||||
self.value.item_class = c
|
||||
|
||||
def __str__(self):
|
||||
return self.value.name
|
||||
|
||||
TEST_TYPE_LIST = [e for e in TestItemType]
|
||||
|
||||
REP_TYPE_SQLITE = "sqlite"
|
||||
REP_TYPE_JUNIT = "junit"
|
||||
REP_TYPE_JSON = "json"
|
||||
REP_TYPE_HTML = "html"
|
||||
REP_TYPE_TEXT = "text"
|
||||
|
||||
REP_TYPES = [
|
||||
REP_TYPE_SQLITE,
|
||||
REP_TYPE_JUNIT,
|
||||
REP_TYPE_JSON,
|
||||
REP_TYPE_HTML,
|
||||
REP_TYPE_TEXT,
|
||||
]
|
||||
|
||||
# Report related constants
|
||||
|
||||
DB_REPORT_VERSION = "report_version"
|
||||
DB_TEST_FILE = "test_file"
|
||||
DB_TEST_SET_NAME = "test_name"
|
||||
DB_TEST_SET_RESULT = "test_result"
|
||||
DB_TEST_REVISION = "test_revision"
|
||||
DB_SEQUENCER_VERSION = "testium_version"
|
||||
DB_TESTRUN_DATE = "testrun_date"
|
||||
DB_TESTRUN_TIME = "testrun_time"
|
||||
DB_TEST_SET_DURATION = "test_duration"
|
||||
|
||||
DB_HEADER_FIELDS = [
|
||||
DB_REPORT_VERSION,
|
||||
DB_TEST_FILE,
|
||||
DB_TEST_SET_NAME,
|
||||
DB_TEST_SET_RESULT,
|
||||
DB_TEST_REVISION,
|
||||
DB_SEQUENCER_VERSION,
|
||||
DB_TESTRUN_DATE,
|
||||
DB_TESTRUN_TIME,
|
||||
DB_TEST_SET_DURATION,
|
||||
]
|
||||
|
||||
DB_TEST_TIMESTAMP_START = "timestamp_start"
|
||||
DB_TEST_ID = "test_id"
|
||||
DB_TEST_PARENT_ID = "parent_id"
|
||||
DB_TEST_NAME = "test_name"
|
||||
DB_TEST_TYPE = "test_type"
|
||||
DB_TEST_KEY = "report_key"
|
||||
DB_TEST_RESULT = "result"
|
||||
DB_TEST_MESSAGE = "message"
|
||||
DB_TEST_DURATION = "duration"
|
||||
DB_TEST_DATA = "data"
|
||||
DB_TEST_LEVEL = "level"
|
||||
DB_TEST_LOG = "log"
|
||||
|
||||
DB_TEST_FIELDS = [
|
||||
DB_TEST_TIMESTAMP_START,
|
||||
DB_TEST_ID,
|
||||
DB_TEST_PARENT_ID,
|
||||
DB_TEST_NAME,
|
||||
DB_TEST_TYPE,
|
||||
DB_TEST_KEY,
|
||||
DB_TEST_RESULT,
|
||||
DB_TEST_MESSAGE,
|
||||
DB_TEST_DURATION,
|
||||
DB_TEST_DATA,
|
||||
DB_TEST_LEVEL,
|
||||
DB_TEST_LOG,
|
||||
]
|
||||
|
||||
ICON_THEMES_PREFIX = [
|
||||
":/color",
|
||||
":/black"
|
||||
]
|
||||
|
||||
FOLDED_CHAR = "."
|
||||
65
src/testium/interpreter/utils/eval.py
Normal file
65
src/testium/interpreter/utils/eval.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import random
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
import math
|
||||
import json
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import (ETUMSyntaxError, ETUMRuntimeError)
|
||||
|
||||
def evaluate(val, **replacement_dict):
|
||||
v2 = val
|
||||
evaluated = False
|
||||
if isinstance(val, str):
|
||||
|
||||
for key, replacement in replacement_dict.items():
|
||||
val = val.replace(f"$({key})", str(replacement))
|
||||
try:
|
||||
v2 = eval(val)
|
||||
except Exception as e:
|
||||
# eval can crash
|
||||
if tm.debug_enabled():
|
||||
s=f"Evaluation of '{val}' failed with message:\n "+str(e)
|
||||
tm.print_debug(s)
|
||||
v2 = val
|
||||
evaluated = (val != v2)
|
||||
return evaluated, v2
|
||||
|
||||
def eval_to_boolean(c):
|
||||
if isinstance(c, bool):
|
||||
condition = c
|
||||
elif isinstance(c, (str, bytes)):
|
||||
if c.lower() in ['true', 't', 'y', 'yes', 'ok', ]:
|
||||
condition = True
|
||||
elif c.lower() in ['f', 'n', 'nok', 'ko', 'false', 'no',]:
|
||||
condition = False
|
||||
else:
|
||||
try:
|
||||
cond = eval(c)
|
||||
condition = eval_to_boolean(cond)
|
||||
except Exception as e:
|
||||
print("eval with c: {}".format(c))
|
||||
raise e
|
||||
elif type(c) is int:
|
||||
condition = (c > 0)
|
||||
else:
|
||||
raise ETUMSyntaxError('c : {} not string, int or bool'.format(c))
|
||||
return condition
|
||||
|
||||
def post_evaluate(post_eval, res):
|
||||
"""This function is evaluating the result of a test,
|
||||
therefore it may include a $(result) parameter.
|
||||
"""
|
||||
if (not post_eval is None) and (post_eval != ""):
|
||||
if (not isinstance(post_eval, str)) or (not ("$(result)" in post_eval)):
|
||||
raise ETUMRuntimeError(
|
||||
f"'eval' ({post_eval}) must be a string and have the '$(result)' substitution keyword."
|
||||
)
|
||||
|
||||
is_evaluated, res = evaluate(post_eval, result=res)
|
||||
if not is_evaluated:
|
||||
raise ETUMRuntimeError(
|
||||
f"Function result evaluation fails: '{post_eval}' syntax error."
|
||||
)
|
||||
return res
|
||||
156
src/testium/interpreter/utils/func_exec.py
Normal file
156
src/testium/interpreter/utils/func_exec.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import socket
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.tum_except import ETUMRuntimeError
|
||||
from interpreter.utils.jrpc import JsonRpcClient
|
||||
from interpreter.test_items.test_result import TestValue
|
||||
|
||||
function_call_process = None
|
||||
|
||||
|
||||
def func_call_init(python_path, request_handler):
|
||||
global function_call_process
|
||||
function_call_process = FuncExecEngine(python_path, request_handler)
|
||||
return function_call_process
|
||||
|
||||
|
||||
def python_version(path: str):
|
||||
result = subprocess.run(
|
||||
[path, "-c", "import sys; print(sys.version_info[:3])"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
return eval(result.stdout.strip())
|
||||
|
||||
|
||||
def is_python_interpreter(path: str, timeout=2) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[path, "-c", "import sys; print(sys.executable)"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
|
||||
class FuncExecEngine:
|
||||
|
||||
def __init__(self, python_path="", request_handler=None):
|
||||
if python_path != "":
|
||||
|
||||
if shutil.which(python_path) is None:
|
||||
raise ETUMRuntimeError(
|
||||
f"The passed python path is not pointing to an executable: '{python_path}'"
|
||||
)
|
||||
|
||||
if not is_python_interpreter(python_path):
|
||||
raise ETUMRuntimeError(
|
||||
f"The passed executable is not a python interpreter: '{python_path}'"
|
||||
)
|
||||
|
||||
else:
|
||||
python_path = sys.executable
|
||||
|
||||
self._ppath = python_path
|
||||
self._req_handler = request_handler
|
||||
self._process = None
|
||||
self._port = 0
|
||||
self._rpc = None
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
run the subprocess to execute the python functions of the test.
|
||||
"""
|
||||
# This thread is not closed until new test is loaded
|
||||
if self._process is not None:
|
||||
raise ETUMRuntimeError("The function subprocess has already been started.")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(("localhost", 0))
|
||||
self._port = sock.getsockname()[1]
|
||||
|
||||
func_proc_path = tm.gd("testium_path")
|
||||
|
||||
self._process = subprocess.Popen(
|
||||
[self._ppath, "-m", "py_func", "-p", f"{self._port}"], cwd=func_proc_path
|
||||
)
|
||||
|
||||
# Port was reserved until the sub-process is started. Now released.
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
self._rpc = JsonRpcClient(self._port, req_handler=self._req_handler)
|
||||
self._rpc.start()
|
||||
|
||||
def join(self):
|
||||
if self._rpc is not None:
|
||||
self._rpc.join()
|
||||
self._rpc = None
|
||||
self._process = None
|
||||
|
||||
def wait_ready(self, timeout=None):
|
||||
if self._rpc is not None and self._rpc.is_alive():
|
||||
return self._rpc.wait_ready(timeout)
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
if self._rpc is not None:
|
||||
self._rpc.stop()
|
||||
|
||||
def func_call(self, file: str, func_name: str, params: list, verbose: bool = True):
|
||||
if (self._rpc is not None) and self._rpc.is_alive():
|
||||
answer = self._rpc.call(
|
||||
"func_call",
|
||||
{
|
||||
"file": file,
|
||||
"fname": func_name,
|
||||
"params": params,
|
||||
"verbose": verbose,
|
||||
},
|
||||
)
|
||||
if "result" in answer:
|
||||
reported_values = answer["result"].get("reported_values", {})
|
||||
if "returned_value" in answer["result"]:
|
||||
res = answer["result"]["returned_value"]
|
||||
return TestValue.SUCCESS, (res, reported_values)
|
||||
else:
|
||||
raise ETUMRuntimeError(
|
||||
"Unexepected py_func jrpc result. To be reported to testium support team."
|
||||
)
|
||||
|
||||
# In case an error was encountered in the called function
|
||||
elif "error" in answer:
|
||||
msg = f"{answer["error"]}"
|
||||
return TestValue.FAILURE, msg
|
||||
|
||||
else:
|
||||
raise ETUMRuntimeError(
|
||||
"Unexepected py_func call failure to be reported to testium support team."
|
||||
)
|
||||
else:
|
||||
raise ETUMRuntimeError(
|
||||
"No function execution process active. To be reported to testium support team."
|
||||
)
|
||||
|
||||
|
||||
def func_exec(file: str, func_name: str, params: list, verbose: bool = True):
|
||||
"""Executes a python function and returns its result and reported values"""
|
||||
global function_call_process
|
||||
|
||||
if function_call_process is not None:
|
||||
success, result = function_call_process.func_call(
|
||||
file, func_name, params, verbose
|
||||
)
|
||||
else:
|
||||
raise ETUMRuntimeError(
|
||||
"No function execution process active. To be reported to testium support team."
|
||||
)
|
||||
|
||||
return success, result
|
||||
52
src/testium/interpreter/utils/globdict.py
Normal file
52
src/testium/interpreter/utils/globdict.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from threading import Lock
|
||||
|
||||
|
||||
global_dict = {}
|
||||
|
||||
global_dict_lock = Lock()
|
||||
|
||||
# Global dictionnary helper functions
|
||||
def gd(name, default=None):
|
||||
''' Function which returns a variable from the global dictionary of testium
|
||||
|
||||
:param name: The name of the element to return.
|
||||
:type name: str
|
||||
:param default: The default value returned by the function if the item
|
||||
has not been found in the global dictionary (``None`` by default).
|
||||
:type default: object
|
||||
:return: The value of the item of the global dictionary or the default value.
|
||||
:rtype: object
|
||||
'''
|
||||
with global_dict_lock:
|
||||
return global_dict.get(name, default)
|
||||
|
||||
def setgd(name, value):
|
||||
''' Function which updates a variable from the global dictionary of testium
|
||||
|
||||
:param name: The name of the element to set.
|
||||
:type name: str
|
||||
:param value: The object to include in the global dictionary.
|
||||
:type name: str
|
||||
:return: No returned value
|
||||
'''
|
||||
with global_dict_lock:
|
||||
global_dict.update({name: value})
|
||||
|
||||
def delgd(name):
|
||||
''' Function which removes a variable from the global dictionary of testium
|
||||
|
||||
:param name: The name of the element to be removed.
|
||||
:type name: str
|
||||
:return: No returned value
|
||||
'''
|
||||
with global_dict_lock:
|
||||
try:
|
||||
del global_dict[name]
|
||||
except:
|
||||
pass
|
||||
|
||||
def cleargd():
|
||||
with global_dict_lock:
|
||||
if global_dict is not None:
|
||||
global_dict.clear()
|
||||
|
||||
8
src/testium/interpreter/utils/icons.py
Normal file
8
src/testium/interpreter/utils/icons.py
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
import interpreter.utils.constants as cst
|
||||
import interpreter.utils.settings as prefs
|
||||
|
||||
def icon_prefix():
|
||||
if not hasattr(prefs, "settings"):
|
||||
prefs.init()
|
||||
return cst.ICON_THEMES_PREFIX[1] if prefs.settings.icons_theme != 0 else cst.ICON_THEMES_PREFIX[0]
|
||||
95
src/testium/interpreter/utils/include.py
Normal file
95
src/testium/interpreter/utils/include.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import yaml
|
||||
import os.path
|
||||
import libs.testium as tm
|
||||
from interpreter.utils.params import expanse
|
||||
from interpreter.utils.tum_except import ETUMFileError
|
||||
from interpreter.utils.template import template_to_test
|
||||
from copy import copy
|
||||
from interpreter.utils.globdict import global_dict
|
||||
from interpreter.utils.yaml_load import yaml_load
|
||||
|
||||
|
||||
class TUMLoaderNoIncludes(yaml.Loader):
|
||||
|
||||
def __init__(self, stream):
|
||||
|
||||
if hasattr(stream, "root"):
|
||||
self._root = stream.root
|
||||
else:
|
||||
self._root = os.path.split(stream.name)[0]
|
||||
|
||||
super().__init__(stream)
|
||||
|
||||
def include_none(self, node):
|
||||
return None
|
||||
|
||||
|
||||
class TUMLoaderRawIncludes(TUMLoaderNoIncludes):
|
||||
"""Class used to preload the test files.
|
||||
When this class is used, the files are not included
|
||||
recursively."""
|
||||
|
||||
def _include(self, node, is_raw: bool = False):
|
||||
data = None
|
||||
try:
|
||||
# Check if templating used on the include file
|
||||
# {file: <filename>, ...} dictionnary required.
|
||||
p = self.construct_mapping(node, deep=True)
|
||||
filename = expanse(p["file"])
|
||||
p.pop("file")
|
||||
except:
|
||||
# Only file parameter
|
||||
p = self.construct_scalar(node)
|
||||
filename = expanse(p)
|
||||
|
||||
if not os.path.isabs(filename):
|
||||
filename = os.path.join(self._root, filename)
|
||||
|
||||
if not os.path.isfile(filename):
|
||||
raise ETUMFileError('File "{}" not found'.format(filename))
|
||||
|
||||
# Copy of the global dict content to be passed as parameter
|
||||
gd_copy = copy(global_dict)
|
||||
|
||||
if not isinstance(p, str):
|
||||
# Case where there are template explicit params
|
||||
for k, v in p.items():
|
||||
gd_copy.update({k: expanse(v)})
|
||||
|
||||
# Processes eventual jinja2 template
|
||||
tmpf = template_to_test(filename, gd_copy)
|
||||
|
||||
# load the yaml test file (with potential includes)
|
||||
data = yaml_load(tmpf, filename, TUMLoader)
|
||||
|
||||
if not is_raw:
|
||||
# This part allows to define include with no "sequence: " before
|
||||
if (
|
||||
isinstance(data, dict)
|
||||
and (len(data) == 1)
|
||||
and "sequence" in data.keys()
|
||||
):
|
||||
data = {"sequence": {"filename": filename, "data": data["sequence"]}}
|
||||
else:
|
||||
data = {"sequence": {"filename": filename, "data": data}}
|
||||
|
||||
return data
|
||||
|
||||
def raw_include(self, node):
|
||||
return self._include(node, True)
|
||||
|
||||
|
||||
class TUMLoader(TUMLoaderRawIncludes):
|
||||
"""Class used to include sub-sequences recursively.
|
||||
A jinja2 based templating of included files is supported."""
|
||||
|
||||
def include(self, node):
|
||||
return self._include(node, False)
|
||||
|
||||
|
||||
TUMLoaderNoIncludes.add_constructor("!include", TUMLoaderRawIncludes.include_none)
|
||||
TUMLoaderNoIncludes.add_constructor("!raw_include", TUMLoaderRawIncludes.include_none)
|
||||
TUMLoaderRawIncludes.add_constructor("!include", TUMLoaderRawIncludes.include_none)
|
||||
TUMLoaderRawIncludes.add_constructor("!raw_include", TUMLoaderRawIncludes.raw_include)
|
||||
TUMLoader.add_constructor("!include", TUMLoader.include)
|
||||
TUMLoader.add_constructor("!raw_include", TUMLoader.raw_include)
|
||||
408
src/testium/interpreter/utils/jrpc.py
Normal file
408
src/testium/interpreter/utils/jrpc.py
Normal file
@@ -0,0 +1,408 @@
|
||||
import sys
|
||||
import socket
|
||||
import json
|
||||
import threading
|
||||
import itertools
|
||||
from time import sleep
|
||||
from typing import Callable, Any
|
||||
|
||||
from interpreter.utils.tum_except import ETUMRuntimeError
|
||||
|
||||
"""Lightweight JSON-RPC 2.0 helpers over TCP sockets.
|
||||
|
||||
This module implements a minimal JSON-RPC 2.0 messaging layer using
|
||||
newline-delimited JSON over TCP sockets. It is intended for simple
|
||||
request/response exchanges useful during development and testing. The
|
||||
implementation favors clarity and small surface area rather than full
|
||||
JSON-RPC compliance.
|
||||
|
||||
Main public classes
|
||||
- `JsonRpcConnection` -- Wraps a connected TCP socket and manages
|
||||
sending requests and dispatching incoming requests and responses. It
|
||||
runs a background receiver thread and pairs outgoing requests with
|
||||
responses using per-request identifiers and `threading.Event` objects.
|
||||
|
||||
- `JsonRpcBase` -- Threaded base class for simple server/client helpers.
|
||||
Provides a `call()` method to send requests to the connected peer and
|
||||
a `handle_request()` hook that can be overridden or supplied at
|
||||
construction.
|
||||
|
||||
- `JsonRpcSrv` / `JsonRpcClient` -- Convenience single-connection server
|
||||
and client classes that accept/connect to a peer and create a
|
||||
`JsonRpcConnection` to handle message I/O.
|
||||
|
||||
Usage example (server):
|
||||
|
||||
srv = JsonRpcSrv(port, req_handler=my_handler)
|
||||
srv.start() # runs in background thread
|
||||
|
||||
Usage example (client):
|
||||
|
||||
clt = JsonRpcClient(port)
|
||||
clt.start()
|
||||
result = clt.call('method_name', {'foo': 'bar'})
|
||||
|
||||
Notes:
|
||||
- Messages must be valid JSON objects and are expected to be
|
||||
single-line (newline-delimited).
|
||||
- This helper is intended for local/testing use; it does not provide
|
||||
authentication, encryption, or advanced JSON-RPC features (notifications,
|
||||
batch requests, error objects beyond simple dicts).
|
||||
"""
|
||||
|
||||
|
||||
class JsonRpcConnection:
|
||||
|
||||
def __init__(self, name, conn: socket.socket, req_handler: Callable[..., Any], timeout=0.2, dbg_out=None):
|
||||
|
||||
self.name = name
|
||||
self.conn = conn
|
||||
if not callable(req_handler):
|
||||
raise TypeError("req_handler must be a callable (function)")
|
||||
|
||||
# User-provided function called to handle incoming requests.
|
||||
# It may accept either the full request dict or (method, params).
|
||||
self.req_handler = req_handler
|
||||
self.send_lock = threading.Lock()
|
||||
self.pending = {} # id -> Event + response
|
||||
self.id_gen = itertools.count(1)
|
||||
self.running = True
|
||||
self._dbg_out = dbg_out
|
||||
|
||||
self.conn.settimeout(timeout)
|
||||
|
||||
self.recv_thread = threading.Thread(target=self._recv_loop, daemon=True)
|
||||
self.recv_thread.start()
|
||||
|
||||
@property
|
||||
def dbg_out(self):
|
||||
return self._dbg_out
|
||||
|
||||
@dbg_out.setter
|
||||
def dbg_out(self, dbg_out):
|
||||
self._dbg_out = dbg_out
|
||||
|
||||
# ---------- Reception ----------
|
||||
def _recv_loop(self):
|
||||
buffer = b""
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
try:
|
||||
data = self.conn.recv(4096)
|
||||
if not data:
|
||||
self.print_info("Connection closed")
|
||||
break
|
||||
except socket.timeout:
|
||||
continue
|
||||
else:
|
||||
buffer += data
|
||||
|
||||
while b"\n" in buffer:
|
||||
line, buffer = buffer.split(b"\n", 1)
|
||||
try:
|
||||
msg = json.loads(line.decode())
|
||||
except Exception as e:
|
||||
self.print_info(str(e))
|
||||
else:
|
||||
if isinstance(msg, dict):
|
||||
self._dispatch(msg)
|
||||
else:
|
||||
self.print_info(f"msg not dict ! = '{msg}'")
|
||||
|
||||
except (ConnectionResetError, OSError):
|
||||
self.print_info("Connection lost")
|
||||
|
||||
finally:
|
||||
self.running = False
|
||||
|
||||
# ---------- Dispatch ----------
|
||||
def _dispatch(self, msg):
|
||||
if "method" in msg:
|
||||
# request to be sent
|
||||
meth=msg["method"]
|
||||
params=msg.get("params", None)
|
||||
rid=msg.get("id", None)
|
||||
|
||||
threading.Thread(
|
||||
target=self._handle_request, args=(meth, params, rid), daemon=True
|
||||
).start()
|
||||
|
||||
elif "id" in msg:
|
||||
# we just received an answer to a previously sent request
|
||||
if msg["id"] in self.pending:
|
||||
self.pending[msg["id"]]["response"] = msg
|
||||
self.pending[msg["id"]]["event"].set()
|
||||
else:
|
||||
self.print_info(f"msg id '{msg["id"]}' inconsistency")
|
||||
|
||||
# ---------- Handler ----------
|
||||
def _handle_request(self, meth, params, rid=None):
|
||||
"""Basic request handler.
|
||||
|
||||
In this implementation
|
||||
a `req_handler` callable provided at construction is invoked to handle
|
||||
the request and produce a response value.
|
||||
"""
|
||||
# print(f"Request received: m:'{meth}', p:'{params}'")
|
||||
|
||||
# Delegate handling to the user-provided function. Accept both
|
||||
# `handler(req_dict)` and `handler(method, params)` signatures; if
|
||||
# the handler raises, capture the exception message as the result.
|
||||
try:
|
||||
result = self.req_handler(meth, params)
|
||||
except Exception as exc:
|
||||
result = {"error": str(exc)}
|
||||
|
||||
self.print_info(f"result: {result}")
|
||||
|
||||
# If the request contains an `id`, send a JSON-RPC response.
|
||||
if rid is not None:
|
||||
msg = {"jsonrpc": "2.0", **result, "id": rid}
|
||||
self._send(msg)
|
||||
|
||||
# ---------- Send ----------
|
||||
def _send(self, obj):
|
||||
"""Send a JSON-serializable object terminated by newline.
|
||||
|
||||
The send operation is protected by a lock to avoid interleaving when
|
||||
multiple threads attempt to write to the underlying socket.
|
||||
"""
|
||||
|
||||
msg = json.dumps(obj) + "\n"
|
||||
data = (msg).encode()
|
||||
self.print_info("sending : " + msg)
|
||||
with self.send_lock:
|
||||
self.conn.sendall(data)
|
||||
|
||||
# ---------- Outgoing request ----------
|
||||
def call(self, method, params=None, timeout=5.0):
|
||||
"""Send a request and wait for its response.
|
||||
|
||||
Args:
|
||||
method: The RPC method name.
|
||||
params: Parameters for the method (any JSON-serializable object).
|
||||
timeout: Seconds to wait for a response before raising
|
||||
`TimeoutError`.
|
||||
|
||||
Returns:
|
||||
The response message (dict) received from the peer.
|
||||
|
||||
Raises:
|
||||
TimeoutError: If no response is received within `timeout`.
|
||||
"""
|
||||
|
||||
req_id = next(self.id_gen)
|
||||
event = threading.Event()
|
||||
|
||||
self.pending[req_id] = {"event": event, "response": None}
|
||||
|
||||
self._send({"jsonrpc": "2.0", "method": method, "params": params, "id": req_id})
|
||||
|
||||
if not event.wait(timeout):
|
||||
# Timeout: remove pending entry and raise
|
||||
self.pending.pop(req_id, None)
|
||||
raise TimeoutError("Timeout JSON-RPC")
|
||||
|
||||
return self.pending.pop(req_id)["response"]
|
||||
|
||||
def print_info(self, msg):
|
||||
if self.dbg_out is not None:
|
||||
print(f"{self.name}: " + str(msg), file=self.dbg_out)
|
||||
|
||||
def stop(self):
|
||||
if self.running:
|
||||
self.running = False
|
||||
|
||||
def join(self):
|
||||
self.recv_thread.join()
|
||||
|
||||
class JsonRpcBase(threading.Thread):
|
||||
"""Threaded base class for simple JSON-RPC server/client helpers.
|
||||
|
||||
Subclasses implement `run()` to accept or establish a single TCP
|
||||
connection and create a `JsonRpcConnection` instance assigned to
|
||||
`self._rpc`. The base class provides a `call()` helper that forwards
|
||||
to the active connection, and a `handle_request(method, params)` hook
|
||||
which may be overridden or supplied via the `req_handler` constructor
|
||||
argument.
|
||||
|
||||
Constructor:
|
||||
- `port` (int): TCP port to bind/connect to.
|
||||
- `req_handler` (callable|None): optional request handler.
|
||||
- `timeout` (int|float): operation timeout in seconds.
|
||||
|
||||
Behavior:
|
||||
- `call()` raises `ETUMRuntimeError` if no active connection exists.
|
||||
"""
|
||||
|
||||
def __init__(self, port, req_handler: Callable[[dict], Any]=None, timeout=10, dbg_out=None):
|
||||
super().__init__()
|
||||
self._port = port
|
||||
self._timeout = timeout
|
||||
self._rpc = None
|
||||
self._req_handler = req_handler
|
||||
self._dbg_out = dbg_out
|
||||
self._event_ready = threading.Event()
|
||||
|
||||
def handle_request(self, method, params):
|
||||
"""Override to implement server-side request handling.
|
||||
|
||||
The default implementation delegates to the `req_handler` provided
|
||||
at construction (if any). Override this method to customize
|
||||
behaviour.
|
||||
"""
|
||||
if self._req_handler is not None:
|
||||
return self._req_handler(method, params)
|
||||
|
||||
self.print_info("No handler defined for the calls")
|
||||
|
||||
def call(self, method, params):
|
||||
if (self._rpc is not None) and self._rpc.running:
|
||||
return self._rpc.call(method, params)
|
||||
else:
|
||||
raise ETUMRuntimeError(f"'{self.name}' JRPC Server not started.")
|
||||
|
||||
def print_info(self, msg):
|
||||
if self.dbg_out is not None:
|
||||
print(f"{self.name}: " + str(msg), file=self.dbg_out)
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
if self._rpc is not None:
|
||||
self._rpc.stop()
|
||||
|
||||
def connect(self, sock):
|
||||
self._rpc = JsonRpcConnection(self.name, sock, self.handle_request, dbg_out=self.dbg_out)
|
||||
self._event_ready.set()
|
||||
|
||||
def wait_ready(self, timeout=None):
|
||||
return self._event_ready.wait(timeout)
|
||||
|
||||
@property
|
||||
def dbg_out(self):
|
||||
return self._dbg_out
|
||||
|
||||
@dbg_out.setter
|
||||
def dbg_out(self, dbg_out):
|
||||
self._dbg_out = dbg_out
|
||||
if self._rpc is not None:
|
||||
self._rpc.dbg_out = dbg_out
|
||||
|
||||
class JsonRpcSrv(JsonRpcBase):
|
||||
"""Single-connection JSON-RPC server.
|
||||
|
||||
`JsonRpcSrv` binds to `localhost` on the provided port and waits for a
|
||||
single client connection. When a client connects it creates a
|
||||
`JsonRpcConnection` and runs until the connection closes or is stopped.
|
||||
|
||||
Typical usage::
|
||||
|
||||
srv = JsonRpcSrv(port, req_handler=my_handler)
|
||||
srv.start() # runs in background thread
|
||||
|
||||
The server will raise `ETUMRuntimeError` on accept/connect timeout.
|
||||
"""
|
||||
|
||||
def __init__(self, port, req_handler = None, timeout=10):
|
||||
super().__init__(port, req_handler, timeout)
|
||||
self.name = f"JsonRpcSvr_{port}"
|
||||
|
||||
def run(self):
|
||||
# TCP/IP socket creation
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
# Link of the socket at the configured port
|
||||
sock.bind(("localhost", self._port))
|
||||
|
||||
sock.settimeout(self._timeout)
|
||||
|
||||
# Listens incoming connections
|
||||
sock.listen(1)
|
||||
|
||||
self.print_info("awaiting connection")
|
||||
tslice = 0.2
|
||||
t = self._timeout
|
||||
while True:
|
||||
try:
|
||||
conn, addr = sock.accept()
|
||||
except socket.timeout:
|
||||
if t >= 0:
|
||||
sleep(tslice)
|
||||
continue
|
||||
else:
|
||||
raise ETUMRuntimeError(f"{self.name}: Timeout")
|
||||
break
|
||||
|
||||
self.print_info("Client connected")
|
||||
with conn:
|
||||
self.connect(conn)
|
||||
|
||||
while self._rpc.running:
|
||||
# Sleep a short time to avoid a busy loop and allow
|
||||
# the receiver thread to process messages.
|
||||
sleep(0.1)
|
||||
|
||||
finally:
|
||||
if self._rpc is not None:
|
||||
self._rpc.stop()
|
||||
self._rpc.join()
|
||||
self.print_info("stopped")
|
||||
|
||||
|
||||
class JsonRpcClient(JsonRpcBase):
|
||||
"""Simple JSON-RPC client that connects to a server on localhost.
|
||||
|
||||
`JsonRpcClient` will attempt to connect to the given port until the
|
||||
configured timeout elapses. On successful connection it creates a
|
||||
`JsonRpcConnection` and serves requests/responses until closed.
|
||||
|
||||
Typical usage::
|
||||
|
||||
clt = JsonRpcClient(port)
|
||||
clt.start()
|
||||
resp = clt.call('method', {'a': 1})
|
||||
"""
|
||||
|
||||
def __init__(self, port, req_handler = None, timeout=10):
|
||||
super().__init__(port, req_handler, timeout)
|
||||
self.name = f"JsonRpcClt_{port}"
|
||||
|
||||
def run(self):
|
||||
# TCP/IP socket creation
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
|
||||
# Link of the socket at the configured port
|
||||
tslice = 0.2
|
||||
t = self._timeout
|
||||
while True:
|
||||
try:
|
||||
sock.connect(("localhost", self._port))
|
||||
except OSError:
|
||||
t -= tslice
|
||||
if t >= 0:
|
||||
sleep(tslice)
|
||||
continue
|
||||
else:
|
||||
raise ETUMRuntimeError(f"{self.name}: failed to connect")
|
||||
break
|
||||
self.print_info("Connected to server")
|
||||
|
||||
self.connect(sock)
|
||||
|
||||
while self._rpc.running:
|
||||
# Sleep a short time to avoid a busy loop and allow
|
||||
# the receiver thread to process messages.
|
||||
sleep(0.1)
|
||||
|
||||
finally:
|
||||
if self._rpc is not None:
|
||||
self._rpc.stop()
|
||||
self._rpc.join()
|
||||
self.print_info("closed")
|
||||
12
src/testium/interpreter/utils/modules.py
Normal file
12
src/testium/interpreter/utils/modules.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import importlib.util
|
||||
import importlib.machinery
|
||||
|
||||
def load_source(modname, filename):
|
||||
loader = importlib.machinery.SourceFileLoader(modname, filename)
|
||||
spec = importlib.util.spec_from_file_location(modname, filename, loader=loader)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
# The module is always executed and not cached in sys.modules.
|
||||
# Uncomment the following line to cache the module.
|
||||
# sys.modules[module.__name__] = module
|
||||
loader.exec_module(module)
|
||||
return module
|
||||
379
src/testium/interpreter/utils/params.py
Normal file
379
src/testium/interpreter/utils/params.py
Normal file
@@ -0,0 +1,379 @@
|
||||
import interpreter.utils.globdict as globdict
|
||||
from interpreter.utils.eval import evaluate
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError, ETUMRuntimeError
|
||||
|
||||
|
||||
class TestItemParams:
|
||||
|
||||
def __init__(self, dict_item={}, parent=None):
|
||||
self._dicoparam = dict_item
|
||||
self._parent = parent
|
||||
|
||||
def expanse(self, param_value):
|
||||
return expanse(param_value, self._parent)
|
||||
|
||||
def getParam(self, parameter, default=None, required=False, processed=False):
|
||||
"""Returns a parameter value from the test item dictionnary.
|
||||
|
||||
:param parameter: list or string which are the parameter name(s).
|
||||
:type parameter: list or string
|
||||
:param default: default value if no param and not required.
|
||||
:type default: string
|
||||
:param required: if True, the function raises an Exception in case of missing param.
|
||||
:type required: bool
|
||||
:param processed: if True, variable substitution is applied.
|
||||
:type processed: bool
|
||||
:return: a parameter value or default
|
||||
"""
|
||||
result = default
|
||||
|
||||
if not isinstance(parameter, (tuple, list)):
|
||||
if not isinstance(parameter, str):
|
||||
raise ETUMSyntaxError('"%s" parameter syntax error' % (parameter))
|
||||
parameter = [parameter]
|
||||
|
||||
has_parameter = False
|
||||
for para in parameter:
|
||||
if (
|
||||
(not (self._dicoparam is None))
|
||||
and (isinstance(self._dicoparam, dict))
|
||||
and (para in self._dicoparam)
|
||||
):
|
||||
result = self._dicoparam[para]
|
||||
if processed:
|
||||
result = self.expanse(result)
|
||||
has_parameter = True
|
||||
break
|
||||
|
||||
if (not has_parameter) and required:
|
||||
raise ETUMSyntaxError('"%s" parameter must exist' % (parameter[0]))
|
||||
return result
|
||||
|
||||
def getParamAll(self, parameter, default=[], required=False, processed=False):
|
||||
"""Returns a parameter list (if any) from the test item dictionnary.
|
||||
|
||||
:param parameter: list or string giving the parameter name.
|
||||
:type parameter: list or string
|
||||
:param default: default value if no param and not required.
|
||||
:type default: list
|
||||
:param required: if True, the function raises an Exception in case of missing param.
|
||||
:type required: bool
|
||||
:param processed: if True, variable substitution is applied.
|
||||
:type processed: bool
|
||||
:return: a parameter list or default
|
||||
"""
|
||||
results = default
|
||||
|
||||
if not isinstance(parameter, (tuple, list)):
|
||||
if not isinstance(parameter, str):
|
||||
raise ETUMSyntaxError('"%s" parameter syntax error' % (parameter))
|
||||
parameter = [parameter]
|
||||
|
||||
has_parameter = False
|
||||
for para in parameter:
|
||||
if para in self._dicoparam:
|
||||
has_parameter = True
|
||||
results = []
|
||||
if isinstance(self._dicoparam[para], (tuple, list)):
|
||||
list_params = self._dicoparam[para]
|
||||
else:
|
||||
list_params = [self._dicoparam[para]]
|
||||
|
||||
for p in list_params:
|
||||
if processed:
|
||||
p = self.expanse(p)
|
||||
results.append(p)
|
||||
|
||||
if (not has_parameter) and required:
|
||||
raise ETUMSyntaxError('"%s" parameter must exist' % (parameter[0]))
|
||||
|
||||
return results
|
||||
|
||||
def getParamFromList(self, params):
|
||||
results = []
|
||||
|
||||
for param in params:
|
||||
if "$(loop_param)" == param:
|
||||
result = getLoopParam(self._parent)
|
||||
if result is None:
|
||||
raise ETUMSyntaxError("parent sequence is not a loop")
|
||||
elif "$(loop_index)" == param:
|
||||
result = getLoopIndex(self._parent)
|
||||
if result is None:
|
||||
raise ETUMSyntaxError("parent sequence is not a loop")
|
||||
else:
|
||||
# If not in global, try in local
|
||||
result = param
|
||||
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
def getData(self):
|
||||
return self._dicoparam
|
||||
|
||||
|
||||
def getLoopParam(parent):
|
||||
"""This function is returning the first found loop_param value.
|
||||
The loop_param is searched recursively into the upper layers of tests
|
||||
items.
|
||||
It returns the loop_param or 'None'.
|
||||
"""
|
||||
res = None
|
||||
if hasattr(parent, "_currentLoop"):
|
||||
res = parent._currentLoop
|
||||
else:
|
||||
# Parent is None in case of a root item
|
||||
if parent._parent is not None:
|
||||
res = getLoopParam(parent._parent)
|
||||
return res
|
||||
|
||||
|
||||
def getLoopIndex(parent):
|
||||
"""This function is returning the first found loop_index value.
|
||||
The loop_index is searched recursively into the upper layers of tests
|
||||
items.
|
||||
It returns the loop_index or 'None'.
|
||||
"""
|
||||
res = None
|
||||
try:
|
||||
res = parent._currentIter
|
||||
except AttributeError:
|
||||
# Parent is None in case of a root item
|
||||
if parent._parent is not None:
|
||||
res = getLoopIndex(parent._parent)
|
||||
return res
|
||||
|
||||
def getLoopCount(parent):
|
||||
"""This function is returning the first found loop_count value.
|
||||
The loop_count is searched recursively into the upper layers of tests
|
||||
items.
|
||||
It returns the loop_index or 'None'.
|
||||
"""
|
||||
res = None
|
||||
try:
|
||||
res = parent._niter
|
||||
except AttributeError:
|
||||
# Parent is None in case of a root item
|
||||
if parent._parent is not None:
|
||||
res = getLoopCount(parent._parent)
|
||||
return res
|
||||
|
||||
def getInverseLoopIndex(parent):
|
||||
"""This function is returning the first found loop_index_inverse value.
|
||||
The loop_index_inverse is searched recursively into the upper layers of tests
|
||||
items.
|
||||
It returns the loop_index_inverse or 'None'.
|
||||
"""
|
||||
res = None
|
||||
try:
|
||||
res = parent._currentInverseIter
|
||||
except AttributeError:
|
||||
# Parent is None in case of a root item
|
||||
if parent._parent is not None:
|
||||
res = getInverseLoopIndex(parent._parent)
|
||||
return res
|
||||
|
||||
|
||||
def find_matches(string, left_patt, right_patt):
|
||||
""" The object of this function is to identify the expandable
|
||||
parts of a string.
|
||||
The returned values are tables of doublets corresponding to
|
||||
the index of extractable sub-strings.
|
||||
"""
|
||||
result = []
|
||||
|
||||
# find all left pattern
|
||||
l = len(string)
|
||||
i = 0
|
||||
while i < l:
|
||||
# first we are looking for the first left pattern
|
||||
leftind = string.find(left_patt, i)
|
||||
if leftind >= 0:
|
||||
leftind += len(left_patt)
|
||||
# Second we are looking for the first right pattern
|
||||
# (on the right of the first left pattern)
|
||||
rightind = string.find(right_patt, leftind)
|
||||
if rightind >= 0:
|
||||
# Right pattern found
|
||||
next_left = leftind
|
||||
while next_left < rightind:
|
||||
# third we are looking for the last left pattern
|
||||
# before the right pattern
|
||||
j = string.find(left_patt, next_left)
|
||||
if j > 0 and j < rightind:
|
||||
next_left = j + len(left_patt)
|
||||
else:
|
||||
break
|
||||
if (next_left >= 0) and next_left < rightind:
|
||||
result.append([next_left, rightind])
|
||||
i = rightind + len(right_patt)
|
||||
else:
|
||||
i = next_left
|
||||
else:
|
||||
# right pattern not found on the right of the first left pattern
|
||||
# No match then
|
||||
break
|
||||
else:
|
||||
# left pattern not found
|
||||
# No match then
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _parse_and_process(left_patt, right_patt, value, func, *fparam):
|
||||
"""This function parses a string value to check if patterns corresponding
|
||||
to expr exist.
|
||||
syntax_weight is the size of the syntax around the extracted variable name.
|
||||
for ex: $(toto) syntax weight is len("$()")
|
||||
When this kind of pattern is found, operation on the extracted value is
|
||||
performed. this is the object of func and fparam (fparam: func
|
||||
params as table).
|
||||
"""
|
||||
result = value
|
||||
cont = True
|
||||
while cont and (isinstance(result, str)):
|
||||
cont = False
|
||||
o = 0
|
||||
tmp_res = ""
|
||||
matches = find_matches(result, left_patt, right_patt)
|
||||
for s in matches:
|
||||
len_left = len(left_patt)
|
||||
len_right = len(right_patt)
|
||||
# Get the positions of the match
|
||||
r = s[0] - len_left
|
||||
tmp_res = tmp_res + result[o : r]
|
||||
o = s[1] + len_right
|
||||
# Get the global value to search
|
||||
extract = result[s[0] : s[1]]
|
||||
# Try to access to the global value
|
||||
treated, g = func(extract, *fparam)
|
||||
if not treated:
|
||||
# No result found in globals
|
||||
tmp_res = tmp_res + result[r : o]
|
||||
else:
|
||||
# Results found, we continue to loop
|
||||
cont = True
|
||||
if isinstance(g, str):
|
||||
tmp_res = tmp_res + g
|
||||
else:
|
||||
if len(result.strip()) == (
|
||||
len(extract) + len_left + len_right
|
||||
):
|
||||
tmp_res = g
|
||||
else:
|
||||
tmp_res = tmp_res + str(g)
|
||||
|
||||
# if something has been replaced
|
||||
if isinstance(tmp_res, str) and cont:
|
||||
tmp_res = tmp_res + result[o:]
|
||||
result = tmp_res
|
||||
elif cont:
|
||||
result = tmp_res
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _operate_param(glob, parent):
|
||||
"""This function checks if glog exists in the global dict or
|
||||
if it is a loop variable.
|
||||
"""
|
||||
treated = True
|
||||
if (glob == "loop_param") and (parent is not None):
|
||||
g = getLoopParam(parent)
|
||||
elif (glob == "loop_index") and (parent is not None):
|
||||
g = getLoopIndex(parent)
|
||||
elif (glob == "loop_index_inverse") and (parent is not None):
|
||||
g = getInverseLoopIndex(parent)
|
||||
elif (glob == "loop_count") and (parent is not None):
|
||||
g = getLoopCount(parent)
|
||||
else:
|
||||
g = globdict.gd(glob)
|
||||
if g is None:
|
||||
treated = False
|
||||
g = glob
|
||||
return treated, g
|
||||
|
||||
|
||||
# def _dummy_eval(val):
|
||||
# bla = evaluate(val)
|
||||
# print("******** evaluate(" + str(val) + ") = " + str(bla[1]))
|
||||
# return bla
|
||||
|
||||
|
||||
def _preprocess_string(value, parent=None):
|
||||
"""This function parses a string value to check if patterns corresponding
|
||||
to $(xxx) exists.
|
||||
When this kind of pattern is found, an attempt to replace the variable
|
||||
by its value in the global dict is performed.
|
||||
If it can't be found in the global dict, not replaced.
|
||||
"""
|
||||
return _parse_and_process("$(", ")", value, _operate_param, parent)
|
||||
|
||||
|
||||
def _eval_param(value):
|
||||
"""This function parses a string value to check if patterns corresponding
|
||||
to <@xxx@ exists.
|
||||
When this kind of pattern is found, an attempt to evaluate its
|
||||
content is done.
|
||||
If it is not evaluable, not replaced.
|
||||
"""
|
||||
return _parse_and_process("<@", "@>", value, evaluate)
|
||||
|
||||
|
||||
def _process_recursively(func, param_value, *fparams):
|
||||
"""This function is scaning recursively param_value to expand it with
|
||||
global variables or loop variables.
|
||||
"""
|
||||
result = None
|
||||
if isinstance(param_value, str):
|
||||
# If a string --> direct expansion
|
||||
result = func(param_value, *fparams)
|
||||
elif isinstance(param_value, dict):
|
||||
# If a dictionary --> check all elements
|
||||
result = {}
|
||||
for key, val in param_value.items():
|
||||
k = key
|
||||
if isinstance(key, str):
|
||||
k = func(key, *fparams)
|
||||
v = _process_recursively(func, val, *fparams)
|
||||
result.update({k: v})
|
||||
elif isinstance(param_value, list):
|
||||
# If a list --> check all elements
|
||||
result = []
|
||||
for val in param_value:
|
||||
result.append(_process_recursively(func, val, *fparams))
|
||||
else:
|
||||
result = param_value
|
||||
|
||||
return result
|
||||
|
||||
def ProcessParam(param_value, parent=None):
|
||||
"""This function is scaning recursively param_value to expand it with
|
||||
global variables or loop variables.
|
||||
"""
|
||||
return _process_recursively(_preprocess_string, param_value, parent)
|
||||
|
||||
|
||||
def ProcessEval(param_value):
|
||||
"""This function is scaning recursively param_value to expand it with
|
||||
global variables or loop variables.
|
||||
"""
|
||||
return _process_recursively(_eval_param, param_value)
|
||||
|
||||
|
||||
def expanse(param_value, parent=None):
|
||||
"""This function is scaning recursively param_value to expand it:
|
||||
- with global variables or loop variables when $() pattern is found.
|
||||
- with evaluation of the content of %() pattern when found.
|
||||
"""
|
||||
n = 0
|
||||
result = param_value
|
||||
while n < 10:
|
||||
tmp_res = ProcessParam(result, parent)
|
||||
tmp_res = ProcessEval(tmp_res)
|
||||
if tmp_res == result:
|
||||
break
|
||||
result = tmp_res
|
||||
n += 1
|
||||
return result
|
||||
36
src/testium/interpreter/utils/paths.py
Normal file
36
src/testium/interpreter/utils/paths.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import testium
|
||||
from interpreter.utils.params import expanse
|
||||
|
||||
import libs.testium as tm
|
||||
|
||||
|
||||
def testium_path():
|
||||
tp = inspect.getfile(inspect.getmodule(testium))
|
||||
return str(Path(tp).parent.resolve())
|
||||
|
||||
|
||||
def prepare_file_to_save(file_name, file_ext=""):
|
||||
iname = file_name
|
||||
if file_ext != "":
|
||||
iname = os.path.splitext(file_name)[0] + file_ext
|
||||
|
||||
if os.path.isfile(iname):
|
||||
i = 0
|
||||
fname = iname
|
||||
while os.path.isfile(fname):
|
||||
i += 1
|
||||
fname = iname + "-" + str(i) + ".saved"
|
||||
os.rename(iname, fname)
|
||||
return iname
|
||||
|
||||
|
||||
def abs_path_from_file(file):
|
||||
abs_file_path = Path(expanse(file))
|
||||
if not abs_file_path.is_absolute():
|
||||
abs_file_path = Path(tm.gd("test_directory")) / abs_file_path
|
||||
abs_file_path = abs_file_path.resolve()
|
||||
return abs_file_path
|
||||
|
||||
32
src/testium/interpreter/utils/periodic_timer.py
Normal file
32
src/testium/interpreter/utils/periodic_timer.py
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
from threading import Timer
|
||||
from time import monotonic
|
||||
|
||||
class PeriodicTimer:
|
||||
def __init__(self, interval, function):
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.execution = None
|
||||
self.active = False
|
||||
self.t0 = 0
|
||||
|
||||
def exec_periodically(self):
|
||||
if self.active:
|
||||
self.function()
|
||||
time_elapsed = monotonic() - self.t0
|
||||
time_waiting = max(0.01, self.interval-time_elapsed)
|
||||
self.execution = Timer(time_waiting, self.exec_periodically)
|
||||
self.t0 = self.t0 + self.interval
|
||||
self.execution.start()
|
||||
|
||||
def start(self):
|
||||
if not self.active:
|
||||
self.active = True
|
||||
self.t0 = monotonic()
|
||||
self.execution = Timer(self.interval, self.exec_periodically)
|
||||
self.execution.start()
|
||||
|
||||
def stop(self):
|
||||
if self.active:
|
||||
self.execution.cancel()
|
||||
self.active = False
|
||||
258
src/testium/interpreter/utils/settings.py
Normal file
258
src/testium/interpreter/utils/settings.py
Normal file
@@ -0,0 +1,258 @@
|
||||
import os
|
||||
import configparser
|
||||
import json
|
||||
import platform
|
||||
from interpreter.utils.tum_except import ETUMRuntimeError
|
||||
|
||||
SettingsCompany = 'Testium'
|
||||
SettingsApplication = 'testium'
|
||||
|
||||
|
||||
def init():
|
||||
global settings
|
||||
settings = TestiumSettings()
|
||||
|
||||
|
||||
class SettingsItem():
|
||||
def __init__(self, name: str, item_type: type) -> None:
|
||||
self.name = name
|
||||
self.t = item_type
|
||||
|
||||
|
||||
class TestiumSettings():
|
||||
SettingsRecentFiles = SettingsItem('recentFileList', list)
|
||||
SettingsLastLogFile = SettingsItem('lastLogFile', str)
|
||||
SettingsLogFileSaved = SettingsItem('logFileSaved', bool)
|
||||
SettingsHideDocPane = SettingsItem('docPaneHidden', bool)
|
||||
SettingsHideLogPane = SettingsItem('logPaneHidden', bool)
|
||||
SettingsShowCheckboxes = SettingsItem('checkBoxesShow', bool)
|
||||
SettingsLogPath = SettingsItem('defaultLogPath', str)
|
||||
SettingsReportPath = SettingsItem('defaultReportPath', str)
|
||||
SettingsShowTimeColumn = SettingsItem('showTimeColumn', bool)
|
||||
SettingsColumnsSize = SettingsItem('columnsSize', dict)
|
||||
SettingsDblClickEnabled = SettingsItem('dblClickEnabled', bool)
|
||||
SettingsIconsTheme = SettingsItem('iconsTheme', int)
|
||||
SettingsLogFont = SettingsItem('logFont', str)
|
||||
SettingsLogFontSize = SettingsItem('logFontSize', int)
|
||||
SettingsGitSupported = SettingsItem('logGitSupported', bool)
|
||||
|
||||
def __init__(self):
|
||||
if 'windows' in platform.system().lower():
|
||||
user_path = os.getenv('APPDATA')
|
||||
else:
|
||||
user_path = os.path.join(os.getenv('HOME'), '.config')
|
||||
|
||||
self.settings_fname = os.path.join(user_path, SettingsCompany,
|
||||
SettingsApplication,
|
||||
SettingsApplication + '.conf')
|
||||
|
||||
if not os.path.isfile(self.settings_fname):
|
||||
try:
|
||||
if not os.path.isdir(os.path.dirname(os.path.dirname(self.settings_fname))):
|
||||
os.mkdir(os.path.dirname(os.path.dirname(self.settings_fname)))
|
||||
if not os.path.isdir(os.path.dirname(self.settings_fname)):
|
||||
os.mkdir(os.path.dirname(self.settings_fname))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if os.path.exists(os.path.dirname(self.settings_fname)):
|
||||
with open(self.settings_fname, "x") as fd:
|
||||
pass
|
||||
|
||||
self.conf = configparser.ConfigParser()
|
||||
if os.path.isfile(self.settings_fname):
|
||||
self.conf.read(self.settings_fname)
|
||||
if not 'Default' in self.conf:
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
self.conf['Default'] = {}
|
||||
self.sync()
|
||||
|
||||
def value(self, key: SettingsItem, default=''):
|
||||
if not isinstance(key, SettingsItem):
|
||||
raise ETUMRuntimeError('Not a proper Settings item.')
|
||||
if type(default) != key.t:
|
||||
raise ETUMRuntimeError(
|
||||
'Types mismatch in config file. You could try to erase "{}" to solve the issue'.format(self.settings_fname))
|
||||
ret = default
|
||||
try:
|
||||
if key.t == int:
|
||||
ret = int(self.conf.getint('Default', key.name, fallback=default))
|
||||
elif key.t == bool:
|
||||
ret = bool(self.conf.getboolean(
|
||||
'Default', key.name, fallback=default))
|
||||
elif key.t == str:
|
||||
ret = self.conf.get('Default', key.name, fallback=default)
|
||||
elif key.t == bytearray:
|
||||
ba = json.loads(self.conf.get(
|
||||
'Default', key.name, fallback=default))
|
||||
ret = bytearray(ba)
|
||||
else:
|
||||
ret = self.conf.get('Default', key.name, fallback=default)
|
||||
if isinstance(ret, str):
|
||||
ret = json.loads(ret)
|
||||
except:
|
||||
self.clear()
|
||||
return ret
|
||||
|
||||
def set_value(self, key: SettingsItem, value: any):
|
||||
if type(value) != key.t:
|
||||
raise ETUMRuntimeError(
|
||||
'Types mismatch in config file. You could try to erase "{}" to solve the issue'.format(self.settings_fname))
|
||||
if key.t == int:
|
||||
self.conf.set('Default', key.name, str(int(value)))
|
||||
elif key.t == bool:
|
||||
self.conf.set('Default', key.name, str(bool(value)))
|
||||
elif key.t == str:
|
||||
self.conf.set('Default', key.name, str(value))
|
||||
elif key.t == bytearray:
|
||||
ba = [int(v) for v in value]
|
||||
self.conf.set('Default', key.name, json.dumps(ba))
|
||||
else:
|
||||
self.conf.set('Default', key.name, json.dumps(value))
|
||||
|
||||
def sync(self):
|
||||
if os.path.isfile(self.settings_fname):
|
||||
with open(self.settings_fname, 'w') as configfile:
|
||||
if configfile.writable():
|
||||
self.conf.write(configfile)
|
||||
|
||||
# SettingsRecentFiles = 'recentFileList'
|
||||
@property
|
||||
def recent_files(self):
|
||||
return self.value(self.SettingsRecentFiles, [])
|
||||
|
||||
@recent_files.setter
|
||||
def recent_files(self, value):
|
||||
self.set_value(self.SettingsRecentFiles, value)
|
||||
|
||||
# SettingsLastLogFile = 'lastLogFile'
|
||||
@property
|
||||
def log_file(self):
|
||||
return self.value(self.SettingsLastLogFile)
|
||||
|
||||
@log_file.setter
|
||||
def log_file(self, value):
|
||||
self.set_value(self.SettingsLastLogFile, value)
|
||||
|
||||
# SettingsLogFileSaved = 'logFileSaved'
|
||||
@property
|
||||
def log_file_saved(self):
|
||||
return self.value(self.SettingsLogFileSaved, False)
|
||||
|
||||
@log_file_saved.setter
|
||||
def log_file_saved(self, value):
|
||||
self.set_value(self.SettingsLogFileSaved, value)
|
||||
|
||||
# SettingsHideDocPane = 'docPaneHidden'
|
||||
@property
|
||||
def hide_doc_pane(self):
|
||||
return self.value(self.SettingsHideDocPane, False)
|
||||
|
||||
@hide_doc_pane.setter
|
||||
def hide_doc_pane(self, value):
|
||||
self.set_value(self.SettingsHideDocPane, value)
|
||||
|
||||
# SettingsHideLogPane = 'logPaneHidden'
|
||||
@property
|
||||
def hide_log_pane(self):
|
||||
return self.value(self.SettingsHideLogPane, False)
|
||||
|
||||
@hide_log_pane.setter
|
||||
def hide_log_pane(self, value):
|
||||
self.set_value(self.SettingsHideLogPane, value)
|
||||
|
||||
# SettingsShowCheckboxes = 'checkBoxesShow'
|
||||
@property
|
||||
def show_checkboxes(self):
|
||||
return self.value(self.SettingsShowCheckboxes, False)
|
||||
|
||||
@show_checkboxes.setter
|
||||
def show_checkboxes(self, value):
|
||||
self.set_value(self.SettingsShowCheckboxes, value)
|
||||
|
||||
# SettingsLogPath = 'defaultLogPath'
|
||||
@property
|
||||
def log_path(self):
|
||||
return self.value(self.SettingsLogPath, '$(test_directory)')
|
||||
|
||||
@log_path.setter
|
||||
def log_path(self, value):
|
||||
self.set_value(self.SettingsLogPath, value)
|
||||
|
||||
# SettingsReportPath = 'defaultReportPath'
|
||||
@property
|
||||
def report_path(self):
|
||||
return self.value(self.SettingsReportPath, '$(home)')
|
||||
|
||||
@report_path.setter
|
||||
def report_path(self, value):
|
||||
self.set_value(self.SettingsReportPath, value)
|
||||
|
||||
# SettingsShowTimeColumn = 'showTimeColumn'
|
||||
@property
|
||||
def show_time_column(self):
|
||||
return self.value(self.SettingsShowTimeColumn, False)
|
||||
|
||||
@show_time_column.setter
|
||||
def show_time_column(self, value):
|
||||
self.set_value(self.SettingsShowTimeColumn, value)
|
||||
|
||||
# SettingsColumnsSize = 'columnsSize'
|
||||
@property
|
||||
def columns_size(self):
|
||||
return self.value(self.SettingsColumnsSize, {})
|
||||
|
||||
@columns_size.setter
|
||||
def columns_size(self, value):
|
||||
self.set_value(self.SettingsColumnsSize, value)
|
||||
|
||||
# SettingsDblClickEnabled = 'dblClickEnabled'
|
||||
@property
|
||||
def dbl_click_enabled(self):
|
||||
return self.value(self.SettingsDblClickEnabled, False)
|
||||
|
||||
@dbl_click_enabled.setter
|
||||
def dbl_click_enabled(self, value):
|
||||
self.set_value(self.SettingsDblClickEnabled, value)
|
||||
|
||||
# SettingsIconsTheme = 'iconsTheme'
|
||||
@property
|
||||
def icons_theme(self):
|
||||
return self.value(self.SettingsIconsTheme, 0)
|
||||
|
||||
@icons_theme.setter
|
||||
def icons_theme(self, value):
|
||||
self.set_value(self.SettingsIconsTheme, value)
|
||||
|
||||
# SettingsLogFont = 'logFont'
|
||||
@property
|
||||
def log_font(self):
|
||||
return self.value(self.SettingsLogFont, 'Monospace')
|
||||
|
||||
@log_font.setter
|
||||
def log_font(self, value):
|
||||
self.set_value(self.SettingsLogFont, value)
|
||||
|
||||
# SettingsLogFontSize = 'logFontSize'
|
||||
@property
|
||||
def log_font_size(self):
|
||||
v = self.value(self.SettingsLogFontSize, 8)
|
||||
if v <= 0:
|
||||
v = 8
|
||||
return v
|
||||
|
||||
@log_font_size.setter
|
||||
def log_font_size(self, value):
|
||||
self.set_value(self.SettingsLogFontSize, value)
|
||||
|
||||
# SettingsGitSupported = 'gitSupported'
|
||||
@property
|
||||
def git_supported(self):
|
||||
r = self.value(self.SettingsGitSupported, True)
|
||||
return r
|
||||
|
||||
@git_supported.setter
|
||||
def git_supported(self, value):
|
||||
self.set_value(self.SettingsGitSupported, value)
|
||||
75
src/testium/interpreter/utils/stdout_redirect.py
Normal file
75
src/testium/interpreter/utils/stdout_redirect.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import sys
|
||||
from threading import (Thread, Event)
|
||||
from interpreter.utils.string_queue import StringQueue
|
||||
from time import (sleep)
|
||||
|
||||
class StdioRedirect:
|
||||
|
||||
def __init__(self):
|
||||
self.redirect_enabled = False
|
||||
self.spy_enabled = False
|
||||
self.ini_stdout = sys.stdout
|
||||
self.ini_stderr = sys.stderr
|
||||
self.stream = self.ini_stdout
|
||||
|
||||
def redirect(self, stream):
|
||||
if not self.spy_enabled:
|
||||
self.out_stream = stream
|
||||
self.stream = self.out_stream
|
||||
sys.stdout = self.out_stream
|
||||
sys.stderr = self.out_stream
|
||||
self.redirect_enabled = True
|
||||
|
||||
def restore(self):
|
||||
if not self.spy_enabled and self.redirect_enabled:
|
||||
sys.stdout = self.ini_stdout
|
||||
sys.stderr = self.ini_stderr
|
||||
self.redirect_enabled = False
|
||||
|
||||
def intercept(self):
|
||||
if not self.spy_enabled:
|
||||
self.thr_started = Event()
|
||||
self.log_buf = StringQueue()
|
||||
self.in_stream = StringQueue()
|
||||
self.stop_output = Event()
|
||||
self.thrd_out = Thread(target=self.interceptStdOut)
|
||||
self.thrd_out.daemon = True
|
||||
sys.stdout = self.in_stream
|
||||
sys.stderr = self.in_stream
|
||||
self.stream = self.in_stream
|
||||
self.thrd_out.start()
|
||||
self.thr_started.wait()
|
||||
self.spy_enabled = True
|
||||
|
||||
|
||||
def stop(self):
|
||||
if self.spy_enabled:
|
||||
sys.stdout = self.out_stream
|
||||
sys.stderr = self.out_stream
|
||||
self.stream = self.out_stream
|
||||
self.stop_output.set()
|
||||
self.thrd_out.join()
|
||||
del self.log_buf
|
||||
del self.in_stream
|
||||
del self.stop_output
|
||||
del self.thrd_out
|
||||
del self.thr_started
|
||||
|
||||
self.spy_enabled = False
|
||||
|
||||
def interceptStdOut(self):
|
||||
self.thr_started.set()
|
||||
while not self.stop_output.is_set():
|
||||
data = self.in_stream.read()
|
||||
self.log_buf.write(data)
|
||||
self.out_stream.write(data)
|
||||
if data == '':
|
||||
sleep(0.1)
|
||||
|
||||
def read(self):
|
||||
ret = ''
|
||||
if self.spy_enabled:
|
||||
ret = self.log_buf.read()
|
||||
return ret
|
||||
|
||||
stdio_redir = StdioRedirect()
|
||||
60
src/testium/interpreter/utils/string_queue.py
Normal file
60
src/testium/interpreter/utils/string_queue.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# from io import (StringIO, SEEK_SET, SEEK_CUR, SEEK_END)
|
||||
from multiprocessing import Queue
|
||||
from queue import (Empty)
|
||||
from threading import (Thread, Event, Condition)
|
||||
from threading import Lock
|
||||
|
||||
class StringQueue(object):
|
||||
""" Class used to store the buffered consoles data:
|
||||
- SerialConsole
|
||||
- TermConsole
|
||||
"""
|
||||
def __init__(self):
|
||||
self.cond = Condition()
|
||||
self.string = ''
|
||||
|
||||
def write(self, data):
|
||||
with self.cond:
|
||||
self.string += data
|
||||
self.cond.notify() # Wake 1 thread waiting on cond (if any)
|
||||
|
||||
def writeln(self, data=''):
|
||||
self.write(data + '\n')
|
||||
|
||||
def read(self, block=False, timeout=None):
|
||||
ret = ''
|
||||
with self.cond:
|
||||
# If blocking is true, always return at least 1 item
|
||||
if block and len(self.string) == 0:
|
||||
self.cond.wait(timeout)
|
||||
if len(self.string) != 0:
|
||||
ret = self.string
|
||||
self.string = ''
|
||||
return ret
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
class BufferedStringQueue(StringQueue):
|
||||
def __init__(self, stream_out):
|
||||
super().__init__()
|
||||
self.stream_out = stream_out
|
||||
self.thr_started = Event()
|
||||
self.stop_evt = Event()
|
||||
self.thrd = Thread(target=self.loop)
|
||||
self.thrd.daemon = True
|
||||
self.thrd.start()
|
||||
self.thr_started.wait()
|
||||
|
||||
def stop(self):
|
||||
self.stop_evt.set()
|
||||
self.thrd.join()
|
||||
del self.stop_evt
|
||||
del self.thrd
|
||||
del self.thr_started
|
||||
|
||||
def loop(self):
|
||||
self.thr_started.set()
|
||||
while not self.stop_evt.is_set():
|
||||
data = self.read(True, 0.1)
|
||||
self.stream_out.write(data)
|
||||
38
src/testium/interpreter/utils/template.py
Normal file
38
src/testium/interpreter/utils/template.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
from sys import exc_info
|
||||
from jinja2 import Template
|
||||
from jinja2.exceptions import TemplateError, UndefinedError
|
||||
from tempfile import TemporaryFile
|
||||
from interpreter.utils.yaml_load import print_yaml
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
|
||||
|
||||
def template_to_test(filename: str, params: list):
|
||||
""" Function which processes an eventual jinja2 template to a test file
|
||||
"""
|
||||
# Temporary file created to receive the processed include
|
||||
# file
|
||||
tmpf = TemporaryFile('w+t')
|
||||
with open(filename, 'r') as f:
|
||||
try:
|
||||
j2_template = Template(f.read())
|
||||
except TemplateError as e:
|
||||
print_yaml(f, filename)
|
||||
type, value, tb = exc_info()
|
||||
msg = "Template error"
|
||||
if hasattr(value, 'lineno'):
|
||||
msg = msg + f" on line {value.lineno}: "
|
||||
else:
|
||||
msg += ": "
|
||||
raise ETUMSyntaxError(msg + str(e), filename)
|
||||
try:
|
||||
params["include_directory"] = os.path.dirname(os.path.abspath(filename))
|
||||
tmpf.write(j2_template.render(params))
|
||||
except (UndefinedError, TypeError):
|
||||
raise ETUMSyntaxError(f"Template loading of file '{filename}' with following parameters '{str(params)}'")
|
||||
|
||||
# return to begining of the temp file
|
||||
tmpf.seek(0, os.SEEK_SET)
|
||||
tmpf.root = os.path.dirname(filename)
|
||||
|
||||
return tmpf
|
||||
104
src/testium/interpreter/utils/termlog.py
Normal file
104
src/testium/interpreter/utils/termlog.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import colorama
|
||||
import re
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
COLOR_DEFAULT = Fore.WHITE
|
||||
COLOR_RESET = Fore.RESET + Style.RESET_ALL + COLOR_DEFAULT
|
||||
|
||||
|
||||
def colored_string(string: str, inputs: list) -> None:
|
||||
"""Function which calculate the coloring of strings with many layers.
|
||||
Overlap of layers and inner layers are managed.
|
||||
"""
|
||||
cols = [COLOR_DEFAULT for i in range(len(string))]
|
||||
for input in inputs:
|
||||
for i in range(input[0][0], input[0][1]):
|
||||
cols[i] = input[1]
|
||||
|
||||
# construction of the string
|
||||
s = ""
|
||||
ilast = 0
|
||||
last_col = COLOR_DEFAULT
|
||||
for i in range(len(string)):
|
||||
if last_col != cols[i]:
|
||||
s = s + string[ilast:i] + COLOR_RESET + cols[i]
|
||||
ilast = i
|
||||
last_col = cols[i]
|
||||
|
||||
return s + string[ilast:] + COLOR_RESET
|
||||
|
||||
|
||||
class TermLog:
|
||||
PASS = ["PASS", "Success", "SUCCESS"]
|
||||
FAIL = ["FAIL", "Fail", "fail", "Error", "ERROR", "error"]
|
||||
WARN = ["Warning", "warning", "WARNING", "Warn", "WARN"]
|
||||
INFO = ["INFO"]
|
||||
DEBUG = ["DEBUG"]
|
||||
BOOL = ["False", "True", "false", "true", "FALSE", "TRUE"]
|
||||
|
||||
def __init__(self, out) -> None:
|
||||
"""Class used to color the stdout in batch and terminal mode."""
|
||||
colorama.init()
|
||||
self.out = out
|
||||
self.pats = []
|
||||
self.pats = self.pats + [
|
||||
[re.compile('(\\"[^\\"]+\\")'), Fore.LIGHTBLUE_EX + Style.BRIGHT],
|
||||
[re.compile("(\\'[^\\']+\\')"), Fore.LIGHTBLUE_EX + Style.BRIGHT],
|
||||
[re.compile("(<-----|----->) step"), Fore.BLUE],
|
||||
[
|
||||
re.compile(
|
||||
r"([\d\.]+)",
|
||||
),
|
||||
Fore.MAGENTA,
|
||||
],
|
||||
[re.compile(r"(@@\d+@@)"), Fore.BLACK],
|
||||
]
|
||||
for word in self.BOOL:
|
||||
self.pats.append([re.compile("({})".format(word)), Fore.MAGENTA])
|
||||
for word in self.WARN:
|
||||
self.pats.append([re.compile("({})".format(word)), Fore.YELLOW])
|
||||
for word in self.INFO:
|
||||
self.pats.append([re.compile("({})".format(word)), Style.BRIGHT])
|
||||
for word in self.DEBUG:
|
||||
self.pats.append([re.compile("({})".format(word)), Fore.BLUE + Style.BRIGHT])
|
||||
for word in self.PASS:
|
||||
self.pats.append(
|
||||
[re.compile("({})".format(word)), Fore.GREEN + Style.BRIGHT]
|
||||
)
|
||||
for word in self.FAIL:
|
||||
self.pats.append([re.compile("({})".format(word)), Fore.RED + Style.BRIGHT])
|
||||
self.residue = ""
|
||||
|
||||
def find_pats(self, line):
|
||||
spans = []
|
||||
for p in self.pats:
|
||||
it = p[0].finditer(line)
|
||||
for m in it:
|
||||
if m:
|
||||
spans.append([m.span(), p[1]])
|
||||
return spans
|
||||
|
||||
def write(self, s: str) -> None:
|
||||
if s == "":
|
||||
return
|
||||
s = self.residue + s
|
||||
self.residue = ""
|
||||
if s[-1:] != "\n":
|
||||
pos = s.rfind("\n")
|
||||
if pos >= 0:
|
||||
self.residue = s[pos:]
|
||||
s = s[:pos]
|
||||
else:
|
||||
# only one line
|
||||
self.out.write(colored_string(s, self.find_pats(s)))
|
||||
return
|
||||
# multiline case
|
||||
for l in s.splitlines():
|
||||
self.out.write(colored_string(l, self.find_pats(l)) + "\n")
|
||||
|
||||
def flush(self):
|
||||
if self.residue != "":
|
||||
self.out.write(self.residue)
|
||||
self.residue = ""
|
||||
self.out.flush()
|
||||
51
src/testium/interpreter/utils/test_ctrl.py
Normal file
51
src/testium/interpreter/utils/test_ctrl.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from multiprocessing import Queue
|
||||
from queue import Empty
|
||||
from interpreter.utils.tum_except import ETUMRuntimeError
|
||||
|
||||
|
||||
class TestSetController:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._test_ctrl = Queue()
|
||||
self._test_resp = Queue()
|
||||
|
||||
@property
|
||||
def ctrl(self) -> Queue:
|
||||
return self._test_ctrl
|
||||
|
||||
@property
|
||||
def resp(self) -> Queue:
|
||||
return self._test_resp
|
||||
|
||||
def control(self, cmd: str, **args):
|
||||
block = True
|
||||
timeout = None
|
||||
if "block" in args:
|
||||
block = args.pop("block")
|
||||
if "timeout" in args:
|
||||
timeout = args.pop("timeout")
|
||||
self._test_ctrl.put({cmd: args})
|
||||
res = self._test_resp.get(block, timeout)
|
||||
if isinstance(res, tuple):
|
||||
raise ETUMRuntimeError(f"Test set command '{cmd}' failed: '{res[1]}'")
|
||||
if isinstance(res, dict) and not cmd in res.keys():
|
||||
raise ETUMRuntimeError(f"Unexpected return error in test set controller")
|
||||
return res[cmd]
|
||||
|
||||
def clear(self):
|
||||
while True:
|
||||
try:
|
||||
self._test_ctrl.get_nowait()
|
||||
except Empty:
|
||||
# we return without error in that case
|
||||
break
|
||||
while True:
|
||||
try:
|
||||
self._test_resp.get_nowait()
|
||||
except Empty:
|
||||
# we return without error in that case
|
||||
break
|
||||
|
||||
def close(self):
|
||||
self.ctrl.close()
|
||||
self.resp.close()
|
||||
473
src/testium/interpreter/utils/test_init.py
Normal file
473
src/testium/interpreter/utils/test_init.py
Normal file
@@ -0,0 +1,473 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import datetime
|
||||
from socket import gethostname
|
||||
import ast
|
||||
import json
|
||||
import yaml
|
||||
import xml.etree.ElementTree as ET
|
||||
import copy
|
||||
|
||||
import yaml
|
||||
|
||||
from interpreter.utils.constants import TestItemType as cst
|
||||
import libs.testium as tm
|
||||
import interpreter.utils.globdict as globdict
|
||||
import interpreter.utils.settings as prefs
|
||||
from interpreter.utils.paths import testium_path
|
||||
from interpreter.utils.yaml_load import yaml_load
|
||||
from interpreter.utils import clear_recursively
|
||||
from interpreter.utils.include import TUMLoader, TUMLoaderNoIncludes, TUMLoaderRawIncludes
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
from interpreter.utils.params import (expanse)
|
||||
from interpreter.utils.version import (
|
||||
get_version, get_testium_version, get_modifications)
|
||||
from interpreter.utils.eval import evaluate
|
||||
from interpreter.utils.template import template_to_test
|
||||
|
||||
from interpreter.test_items.test_item import TestItem
|
||||
from interpreter.test_items.test_item_sleep import TestItemSleep
|
||||
from interpreter.test_items.test_item_unittest import TestItemUnittestFile
|
||||
from interpreter.test_items.test_item_cycle import TestItemCycle
|
||||
from interpreter.test_items.test_item_runtime_plot import TestItemPlot
|
||||
from interpreter.test_items.test_item_group import TestItemGroup
|
||||
from interpreter.test_items.test_item_git import TestItemGit
|
||||
from interpreter.test_items.test_item_func import TestItemFunc
|
||||
from interpreter.test_items.test_item_let import TestItemLet
|
||||
from interpreter.test_items.test_item_check import TestItemCheckValue
|
||||
from interpreter.test_items.test_item_json_rpc import TestItemJSON_RPC
|
||||
from interpreter.test_items.test_item_value_dialog import TestItemValueDialog
|
||||
from interpreter.test_items.test_item_note_dialog import TestItemNoteDialog
|
||||
from interpreter.test_items.test_item_image_dialog import TestItemImageDialog
|
||||
from interpreter.test_items.test_item_msg_dialog import TestItemMsgDialog
|
||||
from interpreter.test_items.test_item_question_dialog import TestItemQuestionDialog
|
||||
from interpreter.test_items.test_item_tested_references import TestItemTestedRefsDialog
|
||||
from interpreter.test_items.test_item_choices_dialog import TestItemChoicesDialog
|
||||
from interpreter.test_items.test_item_console import TestItemConsole
|
||||
from interpreter.test_items.test_item_run import TestItemRun
|
||||
from interpreter.test_items.test_item_report import TestItemReport
|
||||
|
||||
|
||||
def _constants_init():
|
||||
cst.TYPE_CONSOLE.item_class = TestItemConsole
|
||||
cst.TYPE_CYCLE.item_class = TestItemCycle
|
||||
cst.TYPE_FUNCTION.item_class = TestItemFunc
|
||||
cst.TYPE_GIT.item_class = TestItemGit
|
||||
cst.TYPE_GRAPH.item_class = TestItemPlot
|
||||
cst.TYPE_GROUP.item_class = TestItemGroup
|
||||
cst.TYPE_IMAGE_DLG.item_class = TestItemImageDialog
|
||||
cst.TYPE_JSON_RPC.item_class = TestItemJSON_RPC
|
||||
cst.TYPE_LET.item_class = TestItemLet
|
||||
cst.TYPE_CHECK.item_class = TestItemCheckValue
|
||||
cst.TYPE_MESSAGE_DLG.item_class = TestItemMsgDialog
|
||||
cst.TYPE_NOTE_DLG.item_class = TestItemNoteDialog
|
||||
cst.TYPE_QUESTION_DLG.item_class = TestItemQuestionDialog
|
||||
cst.TYPE_REFERENCE_DLG.item_class = TestItemTestedRefsDialog
|
||||
cst.TYPE_CHOICES_DLG.item_class = TestItemChoicesDialog
|
||||
cst.TYPE_REPORT.item_class = TestItemReport
|
||||
cst.TYPE_ROOT.item_class = TestItem
|
||||
cst.TYPE_RUN.item_class = TestItemRun
|
||||
cst.TYPE_SLEEP.item_class = TestItemSleep
|
||||
cst.TYPE_UNITTEST_FILE.item_class = TestItemUnittestFile
|
||||
cst.TYPE_VALUE_DLG.item_class = TestItemValueDialog
|
||||
|
||||
|
||||
def _locate_config_files(test_dir, config_files, silent=False):
|
||||
ret = []
|
||||
pf = []
|
||||
if len(config_files) == 0:
|
||||
for p in ['param.xml', 'param.yaml', 'param.json']:
|
||||
param_filename = os.path.join(test_dir, p)
|
||||
if os.path.exists(param_filename):
|
||||
pf.append(param_filename)
|
||||
if not silent:
|
||||
tm.print_info(f"Configuration file loaded: {p}.")
|
||||
else:
|
||||
if not silent:
|
||||
tm.print_info(f"Default param file \"{p}\" does not exist.")
|
||||
else:
|
||||
pf = config_files
|
||||
|
||||
for p in pf:
|
||||
ret.append(p)
|
||||
return ret
|
||||
|
||||
|
||||
def locate_report_file(rep_file):
|
||||
# report file name treatment
|
||||
if rep_file != '':
|
||||
if not os.path.isabs(rep_file):
|
||||
rep_file = os.path.join(
|
||||
os.getcwd(), rep_file)
|
||||
rep_file = os.path.normpath(rep_file)
|
||||
if not os.path.exists(os.path.dirname(rep_file)):
|
||||
os.makedirs(os.path.dirname(rep_file))
|
||||
|
||||
return rep_file
|
||||
|
||||
|
||||
def _config_files_from_test(test_dict, config_files=None):
|
||||
test_dir = tm.gd('test_directory')
|
||||
pf = []
|
||||
if isinstance(config_files, list) and len(config_files) == 0:
|
||||
param_filename = test_dict.get('config_file', None)
|
||||
if param_filename is None:
|
||||
param_node = test_dict.get('param_file', None)
|
||||
if param_node is not None:
|
||||
if isinstance(param_node, dict):
|
||||
p = param_node.get('file_name', None)
|
||||
if p is not None:
|
||||
param_filename = p
|
||||
else:
|
||||
param_filename = param_node
|
||||
else:
|
||||
param_filename = param_node
|
||||
if param_filename is None:
|
||||
pf = _locate_config_files(test_dir, [])
|
||||
elif isinstance(param_filename, str):
|
||||
pf.append(param_filename)
|
||||
elif isinstance(param_filename, (list)):
|
||||
pf = []
|
||||
for p in param_filename:
|
||||
if isinstance(p, list):
|
||||
for pp in p:
|
||||
pf.append(pp)
|
||||
elif p is not None:
|
||||
pf.append(p)
|
||||
else:
|
||||
raise ETUMSyntaxError(
|
||||
'Unrecognized tum "param_file" : {}'.format(param_filename))
|
||||
elif isinstance(config_files, list):
|
||||
pf = config_files
|
||||
elif isinstance(config_files, str):
|
||||
pf = [config_files]
|
||||
else:
|
||||
raise ETUMSyntaxError(
|
||||
'Unrecognized config_files parameter : {}'.format(config_files))
|
||||
return pf
|
||||
|
||||
|
||||
def _load_test_dict(test_file, variables: dict, no_include: bool = False, raw_include: bool = False):
|
||||
loader = TUMLoader
|
||||
loader = TUMLoaderRawIncludes if raw_include else loader
|
||||
loader = TUMLoaderNoIncludes if no_include else loader
|
||||
|
||||
# Jinja template processing
|
||||
tmpf = template_to_test(test_file, variables)
|
||||
try:
|
||||
d = yaml_load(tmpf, test_file, loader)
|
||||
finally:
|
||||
tmpf.close()
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def load_test(test_file, test_dir, cmdline_pfs, cmdline_defs):
|
||||
# First step: populate config files without includes considered
|
||||
test_dict = _load_test_dict(test_file, {}, no_include=True)
|
||||
_check_test_dict(test_dict)
|
||||
prepare_global()
|
||||
|
||||
# Define the global builtin variables
|
||||
set_standard_gd_keys(test_dict["main"].get(
|
||||
"name", "Unnamed"), test_dir, test_file, cmdline_pfs)
|
||||
|
||||
# Include the content of the first config files into glob dict
|
||||
old_pfs = _config_files_from_test(test_dict, cmdline_pfs)
|
||||
|
||||
# Variables updated
|
||||
gd = update_global(old_pfs, cmdline_defs, silent=True)
|
||||
|
||||
while True:
|
||||
# Loop to check param files until all param files are identified
|
||||
test_dict = _load_test_dict(test_file, gd, raw_include=True)
|
||||
new_pfs = _config_files_from_test(test_dict, cmdline_pfs)
|
||||
|
||||
# Check if things have changed since previous evaluation of
|
||||
# config files
|
||||
new_stuff = False
|
||||
if len(old_pfs) != len(new_pfs):
|
||||
new_stuff = True
|
||||
|
||||
if not new_stuff:
|
||||
for i in range(len(old_pfs)):
|
||||
if old_pfs[i] != new_pfs[i]:
|
||||
new_stuff = True
|
||||
break
|
||||
|
||||
# If the param files are identical, we continue in loading process
|
||||
if not new_stuff:
|
||||
break
|
||||
|
||||
# Variables updated
|
||||
gd = update_global(new_pfs, cmdline_defs, silent=False)
|
||||
old_pfs = copy.copy(new_pfs)
|
||||
|
||||
# Processing (with includes) for complete file loading
|
||||
test_dict = _load_test_dict(test_file, gd)
|
||||
return test_dict, new_pfs
|
||||
|
||||
|
||||
def xmltodict(xml_param_file, silent=True):
|
||||
""" return a dictionnarie of parameter from xml file.
|
||||
"""
|
||||
tag = 'parameter'
|
||||
returned_dict = {}
|
||||
returned_str_dict = {}
|
||||
xml_tree = ET.parse(xml_param_file)
|
||||
xml_root = xml_tree.getroot()
|
||||
xml_params = xml_root.findall(tag)
|
||||
|
||||
for param in xml_params:
|
||||
name = param.get('name', '')
|
||||
if name != '':
|
||||
v = param.get('value', None)
|
||||
if v is None:
|
||||
v = param.get('str', '')
|
||||
v = v.replace("\\n", "\n")
|
||||
v = v.replace("\\r", "\r")
|
||||
v = v.replace("\\t", "\t")
|
||||
returned_str_dict[name] = v
|
||||
else:
|
||||
v = v.replace("\\n", "\n")
|
||||
v = v.replace("\\r", "\r")
|
||||
v = v.replace("\\t", "\t")
|
||||
returned_dict[name] = v
|
||||
|
||||
# reinitializes the global dict values with the xml file content
|
||||
globdict.global_dict.update(returned_str_dict)
|
||||
globdict.global_dict.update(returned_dict)
|
||||
|
||||
for i in range(10):
|
||||
for key, val in returned_dict.items():
|
||||
val = expanse(val)
|
||||
returned_dict.update({key: val})
|
||||
|
||||
globdict.global_dict.update(returned_dict)
|
||||
|
||||
if not silent:
|
||||
if not tm.debug_enabled():
|
||||
tm.print_info(f"\"{xml_param_file}\" loaded.")
|
||||
else:
|
||||
tm.print_debug(f"\"{xml_param_file}\" loading:")
|
||||
for k, v in returned_str_dict.items():
|
||||
tm.print_debug(f" {k}: {v}")
|
||||
for k, v in returned_dict.items():
|
||||
tm.print_debug(f" {k}: {v}")
|
||||
tm.print_debug(f"done.")
|
||||
|
||||
|
||||
def yamltodict(param_file, silent=True):
|
||||
# load of the file
|
||||
with open(param_file, 'r') as fd:
|
||||
dp = yaml_load(fd, param_file, yaml.Loader)
|
||||
|
||||
if dp is None:
|
||||
tm.print_info(f"The YAML file '{param_file}' is empty.")
|
||||
return
|
||||
|
||||
# update the global dict with raw data
|
||||
globdict.global_dict.update(dp)
|
||||
|
||||
# Apply variables expansion
|
||||
for i in range(10):
|
||||
for key, val in dp.items():
|
||||
val = expanse(val)
|
||||
dp.update({key: val})
|
||||
|
||||
if not silent:
|
||||
if not tm.debug_enabled():
|
||||
tm.print_info(f"\"{param_file}\" loaded.")
|
||||
else:
|
||||
tm.print_debug(f"\"{param_file}\" loading:")
|
||||
for k, v in dp.items():
|
||||
tm.print_debug(f" {k}: {v}")
|
||||
tm.print_debug(f"done.")
|
||||
|
||||
# Finalize the global dict update
|
||||
globdict.global_dict.update(dp)
|
||||
|
||||
|
||||
def jsontodict(param_file, silent=True):
|
||||
with open(param_file, 'r') as fd:
|
||||
s = fd.read()
|
||||
dp = json.loads(s)
|
||||
|
||||
# update the global dict with raw data
|
||||
globdict.global_dict.update(dp)
|
||||
|
||||
# Apply variables expansion
|
||||
for i in range(10):
|
||||
for key, val in dp.items():
|
||||
val = expanse(val)
|
||||
dp.update({key: val})
|
||||
|
||||
if not silent:
|
||||
if not tm.debug_enabled():
|
||||
tm.print_info(f"\"{param_file}\" loaded.")
|
||||
else:
|
||||
tm.print_debug(f"\"{param_file}\" loading:")
|
||||
for k, v in dp.items():
|
||||
tm.print_debug(f" {k}: {v}")
|
||||
tm.print_debug(f"done.")
|
||||
|
||||
# Finalize the global dict update
|
||||
globdict.global_dict.update(dp)
|
||||
|
||||
|
||||
def _feed_gd_with_params(param_file, silent=True):
|
||||
test_dir = tm.gd('test_directory')
|
||||
# param files pre-processing
|
||||
files = []
|
||||
for p in param_file:
|
||||
if isinstance(p, str):
|
||||
files.append(p)
|
||||
elif isinstance(p, list):
|
||||
for pp in p:
|
||||
files.append(pp)
|
||||
for p in files:
|
||||
if p is None:
|
||||
continue
|
||||
if not isinstance(p, str):
|
||||
raise ETUMSyntaxError(f'Parameter file "{p}" not a file path.')
|
||||
p = expanse(p)
|
||||
pf = p
|
||||
if not os.path.isabs(pf):
|
||||
pf = os.path.normpath(os.path.join(test_dir, pf))
|
||||
if not os.path.isfile(pf):
|
||||
raise ETUMSyntaxError(f'Parameter file "{pf}" not found')
|
||||
|
||||
ext = os.path.splitext(pf)[1]
|
||||
if ext == '.xml':
|
||||
xmltodict(pf, silent)
|
||||
elif ext == '.json':
|
||||
jsontodict(pf, silent)
|
||||
elif ext == '.yaml':
|
||||
yamltodict(pf, silent)
|
||||
else:
|
||||
raise ETUMSyntaxError(
|
||||
'config files must be "*.xml", "*.yaml" or "*.json"')
|
||||
|
||||
|
||||
def set_standard_gd_keys(test_name, test_dir, test_file, config_files):
|
||||
tm.setgd('testium_version', get_testium_version())
|
||||
tm.setgd('testium_path', testium_path())
|
||||
tm.setgd('test_name', test_name)
|
||||
tm.setgd('test_directory', test_dir)
|
||||
tm.setgd('test_main_file', test_file)
|
||||
tm.setgd('config_files', config_files)
|
||||
tm.setgd('host_name', gethostname())
|
||||
tm.setgd('home', str(Path.home()))
|
||||
tm.setgd('os', tm.OS())
|
||||
|
||||
|
||||
def env_init():
|
||||
if not hasattr(prefs, "settings"):
|
||||
prefs.init()
|
||||
_constants_init()
|
||||
|
||||
|
||||
def _check_test_dict(test_dict):
|
||||
if not isinstance(test_dict, dict):
|
||||
raise ETUMSyntaxError(
|
||||
"The tum file has a major problem. Please check the documentation for syntax.")
|
||||
if not 'main' in test_dict.keys():
|
||||
raise ETUMSyntaxError(
|
||||
"The tum file has a major problem. The 'main' section could not be found.")
|
||||
|
||||
|
||||
def update_global(config_files, defines, silent=False):
|
||||
'''Global dict updated with the content of the config file and a dict provided.
|
||||
this function returns the resulting dict.
|
||||
'''
|
||||
# command line defines are applied first
|
||||
for k, v in defines.items():
|
||||
try:
|
||||
val = ast.literal_eval(v)
|
||||
except:
|
||||
val = v
|
||||
tm.setgd(k, val)
|
||||
|
||||
# Then the configuration files
|
||||
# load global dic before test item
|
||||
_feed_gd_with_params(config_files, silent)
|
||||
|
||||
# Re-apply command line defines to ensure it has not been
|
||||
# overloaded by the configuration files
|
||||
for k, v in defines.items():
|
||||
try:
|
||||
val = ast.literal_eval(v)
|
||||
except:
|
||||
val = v
|
||||
|
||||
conf_val = tm.gd(k)
|
||||
if val != conf_val:
|
||||
if not silent:
|
||||
tm.print_info(f"Variable $({k}) overloaded by command line arg --> \"{val}\".")
|
||||
tm.setgd(k, val)
|
||||
|
||||
return globdict.global_dict
|
||||
|
||||
|
||||
def prepare_global():
|
||||
# Global dict setup
|
||||
globdict.cleargd()
|
||||
|
||||
|
||||
def backup_gd():
|
||||
return copy.deepcopy(globdict.global_dict)
|
||||
|
||||
|
||||
def restore_gd(dict):
|
||||
clear_recursively(globdict.global_dict)
|
||||
globdict.global_dict.update(dict)
|
||||
|
||||
|
||||
def test_run_init():
|
||||
tm.init_timestamp()
|
||||
|
||||
test_dir = tm.gd('test_directory')
|
||||
tm.setgd('test_version', get_version(test_dir))
|
||||
tm.setgd('test_modifs', get_modifications(test_dir))
|
||||
|
||||
start_test_date = datetime.datetime.now()
|
||||
tm.setgd('start_test_date', start_test_date)
|
||||
tm.setgd('testrun_date', start_test_date.strftime("%Y-%m-%d"))
|
||||
tm.setgd('testrun_time', start_test_date.strftime("%H:%M:%S"))
|
||||
|
||||
|
||||
def test_run_header():
|
||||
tool_version = tm.gd('testium_version')
|
||||
test_file = tm.gd('test_main_file', '')
|
||||
has_test_file = (tm.gd('test_main_file') != '')
|
||||
|
||||
s = ''
|
||||
s += (80*'=') + '\n'
|
||||
s += '====== Test overview' + '\n'
|
||||
s += (80*'=') + '\n'
|
||||
if has_test_file:
|
||||
s += ('Executed test file : ' + test_file) + '\n'
|
||||
for cf in tm.gd('config_files'):
|
||||
s += ('With param file : {}'.format(cf)) + '\n'
|
||||
s += ('Test started : ' + tm.gd('testrun_date') + ' ' +
|
||||
tm.gd('testrun_time')) + '\n'
|
||||
|
||||
s += (80*'=') + '\n'
|
||||
s += ('====== Test configuration') + '\n'
|
||||
s += (80*'=') + '\n'
|
||||
s += ('Test executed with testium : ' +
|
||||
tool_version.splitlines()[0]) + '\n'
|
||||
for l in tool_version.splitlines()[1:]:
|
||||
s += (32*' ' + ': ' + l) + '\n'
|
||||
s += (' \n')
|
||||
if has_test_file:
|
||||
test_version = tm.gd('test_version')
|
||||
test_modifs = tm.gd('test_modifs')
|
||||
s += ('Test scripts revision : ' +
|
||||
test_version.splitlines()[0]) + '\n'
|
||||
|
||||
for l in test_version.splitlines()[1:]:
|
||||
s += (32*' ' + ': ' + l) + '\n'
|
||||
for l in test_modifs.splitlines():
|
||||
s += (' '+l) + '\n'
|
||||
return s
|
||||
76
src/testium/interpreter/utils/tum_except.py
Normal file
76
src/testium/interpreter/utils/tum_except.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import traceback
|
||||
import textwrap
|
||||
|
||||
|
||||
class ETUMError(Exception):
|
||||
def __init__(self, message: str, file: str):
|
||||
self._message = message
|
||||
self._file = file
|
||||
|
||||
def str_lines(self):
|
||||
return [self._message, self._file]
|
||||
|
||||
def __str__(self):
|
||||
return "\n".join(self.str_lines())
|
||||
|
||||
|
||||
class ETUMRuntimeError(ETUMError):
|
||||
def __init__(self, message: str, file: str = ""):
|
||||
super().__init__(message, file)
|
||||
|
||||
def str_lines(self):
|
||||
lines = ["TUM runtime error:"]
|
||||
if self._file != "":
|
||||
lines += [f"In \"{self._file}\""]
|
||||
lines += [f"{self._message}"]
|
||||
return lines
|
||||
|
||||
|
||||
class ETUMFileError(ETUMError):
|
||||
def __init__(self, message, file: str = ""):
|
||||
super().__init__(message, file)
|
||||
|
||||
def str_lines(self):
|
||||
lines = ["TUM I/O error:"]
|
||||
if self._file != "":
|
||||
lines += [f"In \"{self._file}\""]
|
||||
lines += [f"{self._message}"]
|
||||
return lines
|
||||
|
||||
|
||||
class ETUMSyntaxError(ETUMError):
|
||||
def __init__(self, message: str, file: str = ""):
|
||||
super().__init__(message, file)
|
||||
|
||||
def str_lines(self):
|
||||
lines = ["TUM file syntax error:"]
|
||||
if self._file != "":
|
||||
lines += [f" In File \"{self._file}\""]
|
||||
lines += textwrap.indent(f"{self._message}", " |").splitlines()
|
||||
return lines
|
||||
|
||||
|
||||
class ETUMParamError(ETUMError):
|
||||
def __init__(self, message: str, param: str = "", item: str = "", item_name: str = "", file: str = ""):
|
||||
super().__init__(message, file)
|
||||
self._item_name=item_name
|
||||
self._item = item
|
||||
self._param = param
|
||||
|
||||
def str_lines(self):
|
||||
lines = ["TUM Item parameter missing:"]
|
||||
if self._file != "":
|
||||
lines += [f"In \"{self._file}\""]
|
||||
lines += [f"Item of type {self._item} with name \"{self._item_name}\""]
|
||||
lines += [f"Concerning parameter \"{self._param}\""]
|
||||
lines += [f"{self._message}"]
|
||||
return lines
|
||||
|
||||
|
||||
def print_exception(exc: ETUMError):
|
||||
if not isinstance(exc, ETUMError):
|
||||
print(traceback.format_exc(4))
|
||||
|
||||
print("\n" + "*"*80)
|
||||
print(exc)
|
||||
print("*"*80)
|
||||
127
src/testium/interpreter/utils/version.py
Normal file
127
src/testium/interpreter/utils/version.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
|
||||
import interpreter.utils.settings as prefs
|
||||
import libs.testium as tm
|
||||
|
||||
_cached_versions = {}
|
||||
|
||||
def repo_rev(path):
|
||||
ret = _cached_versions.get(path, None)
|
||||
if ret:
|
||||
return ret
|
||||
git = import_module("git")
|
||||
repo = git.Repo(path, search_parent_directories=True)
|
||||
if repo.bare:
|
||||
ret ="Warning Bare repo: {}, modifications cannot be tracked !".format(path)
|
||||
else:
|
||||
ret = getSubmoduleVersion(git, repo)
|
||||
_cached_versions.update({path: ret})
|
||||
repo.close()
|
||||
return ret
|
||||
|
||||
def get_version(path :str)-> str:
|
||||
if prefs.settings.git_supported:
|
||||
try:
|
||||
return repo_rev(path)
|
||||
except:
|
||||
return "Warning : {} not versioned".format(path)
|
||||
else:
|
||||
return "Warning git not supported in your settings, version of {} unknown".format(path)
|
||||
|
||||
def get_testium_version():
|
||||
# case where we're executing from an Appimage
|
||||
if 'APPIMAGE' in os.environ:
|
||||
ver = 'unknown'
|
||||
if 'SEQUENCER_REV' in os.environ:
|
||||
ver = os.getenv('SEQUENCER_REV')
|
||||
return (ver + " (binary release)")
|
||||
|
||||
# case where we're executing from pyinstaller exe
|
||||
if getattr(sys, 'frozen', False):
|
||||
file_path = os.path.join(sys._MEIPASS, "VERSION")
|
||||
with open(file_path, 'r') as file:
|
||||
ver = file.read()
|
||||
return (ver + " (binary release)")
|
||||
|
||||
# Executed from sources
|
||||
if prefs.settings.git_supported:
|
||||
git = import_module("git")
|
||||
path = tm.get_main_dir()
|
||||
try:
|
||||
return repo_rev(path)
|
||||
except git.InvalidGitRepositoryError:
|
||||
pkg_rec = import_module("pkg_resources")
|
||||
try:
|
||||
ret = pkg_rec.get_distribution("testium").version
|
||||
_cached_versions.update({path: ret})
|
||||
return str(ret) + " (wheel release)"
|
||||
except:
|
||||
return "Warning : testium not versioned"
|
||||
else:
|
||||
return "Warning git not supported in your settings, version of testium is unknown."
|
||||
|
||||
def get_modifications(path : str)-> str:
|
||||
|
||||
if prefs.settings.git_supported:
|
||||
git = import_module("git")
|
||||
modifs = ""
|
||||
try:
|
||||
repo = git.Repo(path, search_parent_directories=True)
|
||||
for item in repo.index.diff(None):
|
||||
modifs = modifs + '"' + item.a_path + '"' + ' (modified)\n'
|
||||
for item in repo.untracked_files:
|
||||
modifs = modifs + '"' + item + '"' + ' (untracked)\n'
|
||||
repo.close()
|
||||
return modifs
|
||||
except git.InvalidGitRepositoryError:
|
||||
return "Warning : {} not versioned".format(path)
|
||||
else:
|
||||
return "Warning git not supported in your settings, version of {} unknown".format(path)
|
||||
|
||||
def getSubmoduleVersion(git, repo) -> str:
|
||||
v = ""
|
||||
for subM in repo.iter_submodules(ignore_self=False):
|
||||
try:
|
||||
v = v + getCommitVsTag(subM.module()) + "\n"
|
||||
except git.InvalidGitRepositoryError:
|
||||
v = v +"{} not versioned".format(subM.module().git_dir) + "\n"
|
||||
return v
|
||||
|
||||
def getCommitVsTag(repo) -> str:
|
||||
sha = repo.head.object.hexsha
|
||||
short_sha = repo.git.rev_parse(sha, short=12)
|
||||
url = change = ''
|
||||
|
||||
# check if a tag or no
|
||||
t = None
|
||||
for tag in repo.tags:
|
||||
# Try excepted added after crash encountered because of strange tag
|
||||
try:
|
||||
if tag.commit == repo.head.commit:
|
||||
t = tag
|
||||
except:
|
||||
pass
|
||||
|
||||
if repo.is_dirty():
|
||||
change = '(M)'
|
||||
try:
|
||||
url = "".join(repo.remote().urls)
|
||||
except:
|
||||
pass
|
||||
if t:
|
||||
ret = "tag {}".format(t.name)
|
||||
else:
|
||||
branch = ""
|
||||
if not repo.head.is_detached:
|
||||
branch = repo.active_branch.name
|
||||
else:
|
||||
for h in repo.heads:
|
||||
if h.commit == repo.head.commit:
|
||||
branch = "detached from " + h.name
|
||||
ret = "{}{}, commit {}".format(branch, change, short_sha)
|
||||
if url:
|
||||
ret = ret + " from : " + url
|
||||
repo.close()
|
||||
return ret
|
||||
30
src/testium/interpreter/utils/yaml_load.py
Normal file
30
src/testium/interpreter/utils/yaml_load.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from yaml.parser import ParserError
|
||||
from yaml import load, Loader
|
||||
from yaml.scanner import ScannerError
|
||||
from libs.testium import print_debug
|
||||
from interpreter.utils.tum_except import ETUMSyntaxError
|
||||
import io
|
||||
|
||||
|
||||
def print_yaml(file: io.TextIOWrapper, file_name):
|
||||
""" Prints YAML file if debug mode is activated.
|
||||
"""
|
||||
file.seek(0)
|
||||
print_debug(f"Dump of \"{file_name}\":")
|
||||
lines = file.read().splitlines()
|
||||
lines = [f"{i+1:>3d}: " + lines[i] for i in range(len(lines))]
|
||||
print_debug("\n".join(lines))
|
||||
|
||||
|
||||
def yaml_load(file, real_file_name: str, loader: Loader):
|
||||
try:
|
||||
return load(file, loader)
|
||||
|
||||
except ParserError as e:
|
||||
if isinstance(file, io.TextIOWrapper):
|
||||
print_yaml(file, real_file_name)
|
||||
raise ETUMSyntaxError(f"yaml file parsing error: " + str(e), real_file_name)
|
||||
except ScannerError as e:
|
||||
if isinstance(file, io.TextIOWrapper):
|
||||
print_yaml(file, real_file_name)
|
||||
raise ETUMSyntaxError("yaml file scanning error: " + str(e), real_file_name)
|
||||
0
src/testium/libs/__init__.py
Normal file
0
src/testium/libs/__init__.py
Normal file
629
src/testium/libs/console.py
Executable file
629
src/testium/libs/console.py
Executable file
@@ -0,0 +1,629 @@
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from queue import Queue, Empty
|
||||
from time import sleep
|
||||
import collections
|
||||
import serial
|
||||
import threading
|
||||
|
||||
from telnetlib3 import Telnet, DO, WILL, WONT, TTYPE, IAC, SB, SE, theNULL
|
||||
|
||||
TIMEOUT_NULL = 0.000001
|
||||
|
||||
|
||||
class BytesStore(object):
|
||||
""" Class used to store the buffered consoles data:
|
||||
- SerialConsole
|
||||
- TermConsole
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.cond = threading.Condition()
|
||||
self.items = b''
|
||||
|
||||
def put(self, item):
|
||||
with self.cond:
|
||||
self.items += item
|
||||
self.cond.notify() # Wake 1 thread waiting on cond (if any)
|
||||
|
||||
def get(self, block=False, timeout=None):
|
||||
with self.cond:
|
||||
# If blocking is true, always return at least 1 item
|
||||
if block and len(self.items) == 0:
|
||||
self.cond.wait(timeout)
|
||||
if len(self.items) != 0:
|
||||
c = bytes([self.items[0]])
|
||||
self.items = self.items[1:]
|
||||
return c
|
||||
else:
|
||||
return None
|
||||
|
||||
def getAll(self):
|
||||
with self.cond:
|
||||
items = self.items
|
||||
self.items = b''
|
||||
return items
|
||||
|
||||
def pushBack(self, data):
|
||||
with self.cond:
|
||||
self.items = data + self.items
|
||||
|
||||
|
||||
class Console(object):
|
||||
|
||||
def __init__(self, name, echoOn=False, write_delay=0):
|
||||
self.stream = sys.stdout
|
||||
self.name = name
|
||||
self.echo_on = echoOn
|
||||
self.write_delay = write_delay
|
||||
self.string_buffer = '['+str(datetime.now()).split('.')[0].split(' ')[1]+' '+self.name+']'
|
||||
self.port = None
|
||||
self.isOpened = False
|
||||
|
||||
def __del__(self):
|
||||
""" This is a safeguard that tries to close the telnet connection, in case it was not done,
|
||||
before the Console object is terminated by the garbage collector (GC).
|
||||
"""
|
||||
if self.isOpened:
|
||||
print('Warning: {classname} is about to be deleted but the connection was not closed. \
|
||||
A {classname}.close() is missing somewhere in your code !'.format(classname=type(self).__name__))
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
""" Make Console a context manager and allow the use of the 'with ... as' statement
|
||||
"""
|
||||
self.open()
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
""" Make Console a context manager and allow the use of the 'with ... as' statement
|
||||
"""
|
||||
self.close()
|
||||
|
||||
def set_read_timeout(self, timeout):
|
||||
pass
|
||||
|
||||
def readchar(self, timeout):
|
||||
pass
|
||||
|
||||
def read_nowait(self, mute=False):
|
||||
pass
|
||||
|
||||
def flush(self):
|
||||
self.read_nowait(mute=True)
|
||||
|
||||
def is_opened(self):
|
||||
return self.isOpened
|
||||
|
||||
def _is_valid_character(self, data):
|
||||
""" return True if data is a valid ascii char [0x20-0x7E] or '\n' or '\r'
|
||||
"""
|
||||
if data == '':
|
||||
return False
|
||||
|
||||
# new line and carriage return are fine
|
||||
if data == '\n' or data == '\r':
|
||||
return True
|
||||
|
||||
# reject all other non-ascii charaters
|
||||
code = ord(data)
|
||||
if code == 0x09: # TAB
|
||||
return True
|
||||
if code <= 0x1f or code >= 0x7f:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _compute_char(self, data):
|
||||
c = data.decode('utf-8', errors='replace')
|
||||
if not self._is_valid_character(c):
|
||||
c = ''
|
||||
return c
|
||||
|
||||
def read_until(self, match, timeout=None, return_data=False, mute=False):
|
||||
"""
|
||||
read until the string 'match is found
|
||||
If timeout is not set (None), this function runs indefinitely
|
||||
If timeout is set to zero, this function returns immediately
|
||||
If mute is set to True the characters read from the console will not be displayed
|
||||
|
||||
If function fails (because of a timeout) it will return a 'status' integer set to -1
|
||||
otherwise it will return 0.
|
||||
The returned data may be a list in the form of [status, data] with the "data" string
|
||||
being the data read on the device when return_data has been set to true.
|
||||
"""
|
||||
read_data = ''
|
||||
status = -1
|
||||
if not match:
|
||||
raise ValueError('match parameter can not be empty')
|
||||
# replace all '\r' by '\n' as any '\r' read will undergo the same replacement
|
||||
# match = match.replace('\r\n', '\n')
|
||||
# match = match.replace('\r', '')
|
||||
|
||||
# update the console timeout in conformity with what is required.
|
||||
|
||||
self.set_read_timeout(timeout)
|
||||
|
||||
if timeout is None:
|
||||
timeout = 1000000
|
||||
|
||||
# Fixed-length queue that will contain the readout characters
|
||||
search_deque = collections.deque(maxlen=len(match))
|
||||
# convert match string into a deque for faster comparisons
|
||||
match_deque = collections.deque(match)
|
||||
|
||||
# In case of a timeout equal to zero, it must be looped until the
|
||||
# buffer is empty
|
||||
# Otherwise we are waiting for the timeout to rise
|
||||
if timeout < TIMEOUT_NULL:
|
||||
data = self.readchar(0)
|
||||
|
||||
while (status < 0) and ((data is not None) and (data != b'')):
|
||||
|
||||
data = self._compute_char(data)
|
||||
|
||||
if data != '':
|
||||
if not mute:
|
||||
self.string_buffer += data
|
||||
read_data += data
|
||||
|
||||
search_deque.append(data)
|
||||
if search_deque == match_deque:
|
||||
status = 0
|
||||
if (not mute) and (data != '\n'):
|
||||
self.string_buffer += '\n'
|
||||
|
||||
if data == '\n' or (status >= 0):
|
||||
# the datas are written line by line for display optimisation in GUI mode
|
||||
if not mute:
|
||||
self.string_buffer = self.string_buffer.replace('\r\n', '\n')
|
||||
self.string_buffer = self.string_buffer.replace('\r', '')
|
||||
self.stream.write(self.string_buffer)
|
||||
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.string_buffer = '[{} {}]'.format(date_str, self.name)
|
||||
|
||||
if status < 0:
|
||||
data = self.readchar(0)
|
||||
|
||||
# Timeout different than zero
|
||||
else:
|
||||
|
||||
time_is_out = threading.Event()
|
||||
timer = threading.Timer(timeout, lambda: time_is_out.set())
|
||||
timer.start()
|
||||
|
||||
# We are waiting for the timeout to rise
|
||||
|
||||
while (status < 0) and (not time_is_out.isSet()):
|
||||
|
||||
data = self.readchar(timeout)
|
||||
if data is not None:
|
||||
data = self._compute_char(data)
|
||||
if data != '':
|
||||
if not mute:
|
||||
self.string_buffer += data
|
||||
read_data += data
|
||||
|
||||
search_deque.append(data)
|
||||
if search_deque == match_deque:
|
||||
timer.cancel()
|
||||
status = 0
|
||||
if (not mute) and (data != '\n'):
|
||||
self.string_buffer += '\n'
|
||||
|
||||
if data == '\n' or (status >= 0):
|
||||
# the datas are written line by line for display optimisation in GUI mode
|
||||
if not mute:
|
||||
self.string_buffer = self.string_buffer.replace('\r\n', '\n')
|
||||
self.string_buffer = self.string_buffer.replace('\r', '')
|
||||
self.stream.write(self.string_buffer)
|
||||
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.string_buffer = '[{} {}]'.format(date_str, self.name)
|
||||
|
||||
if return_data:
|
||||
return status, read_data
|
||||
return status
|
||||
|
||||
def write(self, characters, mute=False):
|
||||
if self.echo_on and not mute:
|
||||
ech = '' if characters.strip(" ").endswith('\n') else '\n'
|
||||
print(('[>' + self.name + '] : ' + characters), end=ech)
|
||||
if self.write_delay != 0:
|
||||
for char in characters:
|
||||
self.port.write(char.encode('utf-8'))
|
||||
sleep(self.write_delay)
|
||||
return len(characters)
|
||||
else:
|
||||
return self.port.write(characters.encode('utf-8'))
|
||||
|
||||
|
||||
if not sys.platform.startswith('win'):
|
||||
# import SshConsole if pexpect is installed
|
||||
try:
|
||||
from libs.console_ssh import SshConsole
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class TelnetConsole(Console):
|
||||
TYPE = 'telnet'
|
||||
|
||||
def __init__(self, name, host, port=23, echoOn=False, write_delay=0, tries=1, try_delay=2):
|
||||
|
||||
super().__init__(name, echoOn, write_delay)
|
||||
self.port = None
|
||||
self.host = host
|
||||
self.port_id = port
|
||||
self.tries = tries
|
||||
self.try_delay = try_delay
|
||||
|
||||
def open(self, user=None, pwd=None):
|
||||
|
||||
mtries, mdelay = self.tries, self.try_delay
|
||||
while mtries > 1:
|
||||
try:
|
||||
self.port = Telnet(self.host, self.port_id)
|
||||
break
|
||||
except (TimeoutError, ConnectionRefusedError) as exc:
|
||||
msg = '{}, Retrying in {} seconds...'.format(str(exc), mdelay)
|
||||
print(msg)
|
||||
sleep(mdelay)
|
||||
mtries -= 1
|
||||
mdelay *= 2
|
||||
else:
|
||||
self.port = Telnet(self.host, self.port_id)
|
||||
|
||||
self.isOpened = True
|
||||
|
||||
if not user:
|
||||
return
|
||||
self.stream.write(self.port.read_until("login: "))
|
||||
self.port.write(user + "\n")
|
||||
|
||||
self.stream.write(self.port.read_until("assword"))
|
||||
self.stream.write(self.port.read_until(":"))
|
||||
self.port.write(pwd + "\n")
|
||||
|
||||
def readchar(self, timeout):
|
||||
return self.port.expect([re.compile(b'.{1}', re.DOTALL), ], timeout)[2]
|
||||
|
||||
def readline(self):
|
||||
return self.read_until('\n', return_data=True)[1]
|
||||
|
||||
def read_nowait(self, mute=False):
|
||||
st = self.port.read_very_eager().decode('utf-8', errors='replace')
|
||||
if not mute:
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.stream.write('[{} {}]'.format(date_str, self.name)+st)
|
||||
return st
|
||||
|
||||
def close(self):
|
||||
if self.isOpened:
|
||||
self.port.close()
|
||||
self.isOpened = False
|
||||
|
||||
def neg(self, sock, command, option):
|
||||
negotiation_list = [
|
||||
['BINARY', WONT, 'WONT'],
|
||||
['ECHO', WONT, 'WONT'],
|
||||
['RCP', WONT, 'WONT'],
|
||||
['SGA', WONT, 'WONT'],
|
||||
['NAMS', WONT, 'WONT'],
|
||||
['STATUS', WONT, 'WONT'],
|
||||
['TM', WONT, 'WONT'],
|
||||
['RCTE', WONT, 'WONT'],
|
||||
['NAOL', WONT, 'WONT'],
|
||||
['NAOP', WONT, 'WONT'],
|
||||
['NAOCRD', WONT, 'WONT'],
|
||||
['NAOHTS', WONT, 'WONT'],
|
||||
['NAOHTD', WONT, 'WONT'],
|
||||
['NAOFFD', WONT, 'WONT'],
|
||||
['NAOVTS', WONT, 'WONT'],
|
||||
['NAOVTD', WONT, 'WONT'],
|
||||
['NAOLFD', WONT, 'WONT'],
|
||||
['XASCII', WONT, 'WONT'],
|
||||
['LOGOUT', WONT, 'WONT'],
|
||||
['BM', WONT, 'WONT'],
|
||||
['DET', WONT, 'WONT'],
|
||||
['SUPDUP', WONT, 'WONT'],
|
||||
['SUPDUPOUTPUT', WONT, 'WONT'],
|
||||
['SNDLOC', WONT, 'WONT'],
|
||||
['TTYPE', WILL, 'WILL'],
|
||||
['EOR', WONT, 'WONT'],
|
||||
['TUID', WONT, 'WONT'],
|
||||
['OUTMRK', WONT, 'WONT'],
|
||||
['TTYLOC', WONT, 'WONT'],
|
||||
['VT3270REGIME', WONT, 'WONT'],
|
||||
['X3PAD', WONT, 'WONT'],
|
||||
['NAWS', WONT, 'WONT'],
|
||||
['TSPEED', WONT, 'WONT'],
|
||||
['LFLOW', WONT, 'WONT'],
|
||||
['LINEMODE', WONT, 'WONT'],
|
||||
['XDISPLOC', WONT, 'WONT'],
|
||||
['OLD_ENVIRON', WONT, 'WONT'],
|
||||
['AUTHENTICATION', WONT, 'WONT'],
|
||||
['ENCRYPT', WONT, 'WONT'],
|
||||
['NEW_ENVIRON', WONT, 'WONT']
|
||||
]
|
||||
if ord(option) < 40:
|
||||
response = negotiation_list[ord(option)][1]
|
||||
else:
|
||||
response = WONT
|
||||
if command == DO:
|
||||
s = b''.join((IAC, response, option))
|
||||
sock.sendall(s)
|
||||
elif command == SE:
|
||||
s = ("%s%s%s%sDEC-VT100%s%s" % (IAC, SB, TTYPE, chr(0), IAC, SE))
|
||||
s = b''.join((IAC, SB, TTYPE, theNULL, b'DEC-VT100', IAC, SE))
|
||||
sock.sendall(s)
|
||||
return
|
||||
|
||||
|
||||
class ETSConsole(TelnetConsole):
|
||||
TYPE = 'ETS'
|
||||
|
||||
def open(self, port):
|
||||
TelnetConsole.open(self)
|
||||
self.port.set_option_negotiation_callback(self.neg)
|
||||
self.read_until("Username>", 5)
|
||||
self.write("rach_script\n")
|
||||
self.read_until(">", 2)
|
||||
self.write('c local port_'+str(port)+'\n')
|
||||
|
||||
self.write("\r\n")
|
||||
self.read_until(">", 5)
|
||||
|
||||
|
||||
class SerialConsole(Console):
|
||||
TYPE = 'serial'
|
||||
|
||||
def __init__(self, name, port=None, baudrate=9600, parity="none", stopbits=1, xonxoff=False,
|
||||
bufferize=False, echoOn=False, write_delay=0):
|
||||
super().__init__(name, echoOn, write_delay)
|
||||
self.baudrate = baudrate
|
||||
self.bufferize = bufferize
|
||||
self.xonxoff = False
|
||||
if xonxoff:
|
||||
self.xonxoff = True
|
||||
self.parity = serial.PARITY_NONE
|
||||
if parity.lower() == "even":
|
||||
self.parity = serial.PARITY_EVEN
|
||||
if parity.lower() == "odd":
|
||||
self.parity = serial.PARITY_ODD
|
||||
self.stopbits = serial.STOPBITS_ONE
|
||||
if stopbits == 2:
|
||||
self.stopbits = serial.STOPBITS_TWO
|
||||
if bufferize:
|
||||
self.rx_queue = BytesStore()
|
||||
self.stop = threading.Event()
|
||||
self.port = None
|
||||
self.port_id = port
|
||||
|
||||
def open(self):
|
||||
self.port = serial.Serial(port=self.port_id,
|
||||
baudrate=self.baudrate,
|
||||
stopbits=self.stopbits,
|
||||
parity=self.parity,
|
||||
xonxoff=self.xonxoff,
|
||||
timeout=None)
|
||||
self.isOpened = True
|
||||
if self.bufferize:
|
||||
self.port.timeout = 2
|
||||
self._thd = threading.Thread(target=self.read_thread)
|
||||
self._thd.start()
|
||||
|
||||
def read_thread(self):
|
||||
while not self.stop.is_set():
|
||||
c = self.port.read(1)
|
||||
if c:
|
||||
self.rx_queue.put(c)
|
||||
|
||||
def close(self):
|
||||
if self.bufferize:
|
||||
self.stop.set()
|
||||
self._thd.join()
|
||||
if self.port is not None:
|
||||
self.port.close()
|
||||
self.isOpened = False
|
||||
|
||||
def set_read_timeout(self, timeout):
|
||||
if not self.bufferize:
|
||||
self.port.timeout = timeout
|
||||
|
||||
def readchar(self, timeout):
|
||||
if self.bufferize:
|
||||
if not self._thd.is_alive() and not self.stop.isSet():
|
||||
raise RuntimeError(
|
||||
"Impossible to read the serial console, it may be already openned")
|
||||
if timeout < TIMEOUT_NULL:
|
||||
return self.rx_queue.get(block=False)
|
||||
else:
|
||||
return self.rx_queue.get(block=True, timeout=timeout)
|
||||
|
||||
return self.port.read(1)
|
||||
|
||||
def flush(self):
|
||||
self.port.flush()
|
||||
|
||||
def read_nowait(self, mute=False):
|
||||
if self.bufferize:
|
||||
if not self._thd.is_alive() and not self.stop.isSet():
|
||||
raise RuntimeError(
|
||||
"Impossible to read the serial console, it may be already openned")
|
||||
st = self.rx_queue.getAll().decode('utf-8', errors='replace')
|
||||
if not mute:
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.stream.write('[{} {}]'.format(date_str, self.name)+st)
|
||||
return st
|
||||
|
||||
st = self.port.read(self.port.inWaiting()).decode('utf-8', errors='replace')
|
||||
if not mute:
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.stream.write('[{} {}]'.format(date_str, self.name)+st)
|
||||
return st
|
||||
|
||||
|
||||
class TelnetSerialConsole(TelnetConsole):
|
||||
TYPE = 'telnet&serial'
|
||||
|
||||
def __init__(self, name, host, port=23, serial_port=None, baudrate=9600, echoOn=False, write_delay=0):
|
||||
Console.__init__(self, name, echoOn, write_delay)
|
||||
self.port = None
|
||||
self.host = host
|
||||
self.port_id = port
|
||||
self.serial_port = serial_port
|
||||
self.baudrate = baudrate
|
||||
|
||||
def open(self, user=None, pwd=None):
|
||||
self.port = Telnet(self.host, self.port_id)
|
||||
self.isOpened = True
|
||||
if not user:
|
||||
return
|
||||
self.stream.write(self.port.read_until("login: "))
|
||||
self.port.write(user + "\n")
|
||||
self.stream.write(self.port.read_until("assword"))
|
||||
self.stream.write(self.port.read_until(":"))
|
||||
self.port.write(pwd + "\n")
|
||||
# then connect to the serial port using miniterm console
|
||||
self.stream.write(self.port.read_until("~]$"))
|
||||
self.stream.write("miniterm.py -p " + str(self.serial_port) +
|
||||
" -b " + str(self.baudrate) + " --parity=N --lf\n")
|
||||
if (self.read_until("--- Miniterm on", 5) == -1):
|
||||
return
|
||||
|
||||
|
||||
class LoggedConsole(Console):
|
||||
def __init__(self, name, overwriteFile=True, echoOn=False, logPath='', write_delay=0):
|
||||
super().__init__(name, echoOn, write_delay)
|
||||
self.rx_queue = Queue()
|
||||
self.stop = threading.Event()
|
||||
if logPath.endswith('.log'):
|
||||
if os.path.exists(os.path.dirname(logPath)):
|
||||
self.logfile_name = logPath
|
||||
else:
|
||||
os.makedirs(os.path.join(os.getcwd(), os.path.dirname(logPath)), exist_ok=True)
|
||||
self.logfile_name = os.path.join(os.getcwd(), logPath)
|
||||
else:
|
||||
if not os.path.isabs(logPath):
|
||||
logPath = os.path.join(os.getcwd(), logPath)
|
||||
os.makedirs(logPath, exist_ok=True)
|
||||
self.logfile_name = '{}/{}.log'.format(logPath, self.name)
|
||||
self.overwriteFile = overwriteFile
|
||||
if self.overwriteFile:
|
||||
open_mode = "w"
|
||||
else:
|
||||
open_mode = "a"
|
||||
# open with flush every new line
|
||||
self.log_fd = open(self.logfile_name, open_mode, buffering=1)
|
||||
|
||||
def open(self):
|
||||
self.isOpened = True
|
||||
if self.log_fd is None:
|
||||
self.log_fd = open(self.logfile_name, "a", buffering=1)
|
||||
self._thd = threading.Thread(target=self.read_thread)
|
||||
self._thd.start()
|
||||
|
||||
def _readPort(self):
|
||||
pass
|
||||
|
||||
def read_thread(self):
|
||||
line_buffer = None
|
||||
while not self.stop.is_set():
|
||||
data = self._readPort()
|
||||
|
||||
if data:
|
||||
self.rx_queue.put(data)
|
||||
else:
|
||||
continue
|
||||
data = data.decode('utf-8', errors='replace')
|
||||
# if valid char, write into the file
|
||||
if self._is_valid_character(data):
|
||||
# replace '\r' by '\n' and '\r\n' by '\n'
|
||||
if data == '\r':
|
||||
data = ''
|
||||
continue
|
||||
# date at reception of first new char of the line
|
||||
if line_buffer is None:
|
||||
line_buffer = '['+str(datetime.now()).split('.')[0].split(' ')[1]+']'
|
||||
line_buffer += data
|
||||
if data == '\n':
|
||||
# the datas are written line by line
|
||||
self.log_fd.write(line_buffer)
|
||||
line_buffer = None
|
||||
# if exit, flush data first
|
||||
if line_buffer is not None:
|
||||
self.log_fd.write(line_buffer)
|
||||
print('closing console "%s" log file' % (self.name))
|
||||
self.log_fd.close()
|
||||
self.log_fd = None
|
||||
|
||||
def close(self):
|
||||
self.stop.set()
|
||||
self._thd.join()
|
||||
if self.port is not None:
|
||||
print('closing console "%s"' % (self.name))
|
||||
self.port.close()
|
||||
self.isOpened = False
|
||||
|
||||
def readchar(self, timeout=None):
|
||||
if self.log_fd is None:
|
||||
raise ConnectionAbortedError
|
||||
try:
|
||||
return self.rx_queue.get(timeout=timeout)
|
||||
except Empty:
|
||||
return None
|
||||
|
||||
def read_nowait(self, mute=False):
|
||||
if self.log_fd is None:
|
||||
raise ConnectionAbortedError
|
||||
chars = ''
|
||||
for _ in range(self.rx_queue.qsize()):
|
||||
chars = chars + self.rx_queue.get().decode('utf-8', errors='replace')
|
||||
|
||||
if not mute:
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.stream.write('[{} {}]'.format(date_str, self.name)+chars)
|
||||
return chars
|
||||
|
||||
|
||||
class SerialLoggedConsole(LoggedConsole):
|
||||
TYPE = 'serial'
|
||||
|
||||
def __init__(self, name, port=None, baudrate=9600, overwriteFile=True, echoOn=False, logPath='', write_delay=0):
|
||||
super().__init__(name, overwriteFile, echoOn, logPath, write_delay)
|
||||
self.baudrate = baudrate
|
||||
self.port = None
|
||||
self.port_id = port
|
||||
|
||||
def _readPort(self):
|
||||
return self.port.read(1)
|
||||
|
||||
def open(self):
|
||||
self.port = serial.Serial(port=self.port_id, baudrate=self.baudrate, timeout=None)
|
||||
super().open()
|
||||
|
||||
|
||||
class TelnetLoggedConsole(LoggedConsole):
|
||||
TYPE = 'telnet'
|
||||
|
||||
def __init__(self, name, host, port=23, overwriteFile=True, echoOn=False, logPath='', write_delay=0):
|
||||
super().__init__(name, overwriteFile, echoOn, logPath, write_delay)
|
||||
self.port = None
|
||||
self.host = host
|
||||
self.port_id = port
|
||||
|
||||
def open(self):
|
||||
self.port = Telnet(self.host, self.port_id)
|
||||
super().open()
|
||||
|
||||
def _readPort(self, timeout=0.2):
|
||||
try:
|
||||
c = self.port.expect([re.compile(b'.{1}', re.DOTALL), ], timeout)[2]
|
||||
except (ConnectionAbortedError, ConnectionResetError):
|
||||
return None
|
||||
return c
|
||||
569
src/testium/libs/console_ssh.py
Executable file
569
src/testium/libs/console_ssh.py
Executable file
@@ -0,0 +1,569 @@
|
||||
"""A concrete implementation of Console based on SSH access.
|
||||
This requires the pexpect library to be installed.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
import os
|
||||
import pexpect
|
||||
from pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn
|
||||
|
||||
from libs.console import Console
|
||||
|
||||
# Exception classes used by this module.
|
||||
|
||||
|
||||
class ExceptionPxssh(ExceptionPexpect):
|
||||
"""Raised for pxssh exceptions."""
|
||||
|
||||
|
||||
# pxssh is a modified version of the pxssh class from the pexpect library. That custom version
|
||||
# returns an exception when a timeout occurs during the login phase
|
||||
class pxssh(spawn):
|
||||
"""This class extends pexpect.spawn to specialize setting up SSH
|
||||
connections. This adds methods for login, logout, and expecting the shell
|
||||
prompt. It does various tricky things to handle many situations in the SSH
|
||||
login process. For example, if the session is your first login, then pxssh
|
||||
automatically accepts the remote certificate; or if you have public key
|
||||
authentication setup then pxssh won't wait for the password prompt.
|
||||
|
||||
pxssh uses the shell prompt to synchronize output from the remote host. In
|
||||
order to make this more robust it sets the shell prompt to something more
|
||||
unique than just $ or #. This should work on most Borne/Bash or Csh style
|
||||
shells.
|
||||
|
||||
Example that runs a few commands on a remote server and prints the result::
|
||||
|
||||
from pexpect import pxssh
|
||||
import getpass
|
||||
try:
|
||||
s = pxssh.pxssh()
|
||||
hostname = raw_input('hostname: ')
|
||||
username = raw_input('username: ')
|
||||
password = getpass.getpass('password: ')
|
||||
s.login(hostname, username, password)
|
||||
s.sendline('uptime') # run a command
|
||||
s.prompt() # match the prompt
|
||||
print(s.before) # print everything before the prompt.
|
||||
s.sendline('ls -l')
|
||||
s.prompt()
|
||||
print(s.before)
|
||||
s.sendline('df')
|
||||
s.prompt()
|
||||
print(s.before)
|
||||
s.logout()
|
||||
except pxssh.ExceptionPxssh as e:
|
||||
print("pxssh failed on login.")
|
||||
print(e)
|
||||
|
||||
Example showing how to specify SSH options::
|
||||
|
||||
from pexpect import pxssh
|
||||
s = pxssh.pxssh(options={
|
||||
"StrictHostKeyChecking": "no",
|
||||
"UserKnownHostsFile": "/dev/null"})
|
||||
...
|
||||
|
||||
Note that if you have ssh-agent running while doing development with pxssh
|
||||
then this can lead to a lot of confusion. Many X display managers (xdm,
|
||||
gdm, kdm, etc.) will automatically start a GUI agent. You may see a GUI
|
||||
dialog box popup asking for a password during development. You should turn
|
||||
off any key agents during testing. The 'force_password' attribute will turn
|
||||
off public key authentication. This will only work if the remote SSH server
|
||||
is configured to allow password logins. Example of using 'force_password'
|
||||
attribute::
|
||||
|
||||
s = pxssh.pxssh()
|
||||
s.force_password = True
|
||||
hostname = raw_input('hostname: ')
|
||||
username = raw_input('username: ')
|
||||
password = getpass.getpass('password: ')
|
||||
s.login (hostname, username, password)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout=30,
|
||||
maxread=2000,
|
||||
searchwindowsize=None,
|
||||
logfile=None,
|
||||
cwd=None,
|
||||
env=None,
|
||||
ignore_sighup=True,
|
||||
echo=True,
|
||||
options={},
|
||||
encoding=None,
|
||||
codec_errors="strict",
|
||||
dimensions=(24, 1000),
|
||||
):
|
||||
|
||||
spawn.__init__(
|
||||
self,
|
||||
None,
|
||||
timeout=timeout,
|
||||
maxread=maxread,
|
||||
searchwindowsize=searchwindowsize,
|
||||
logfile=logfile,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
ignore_sighup=ignore_sighup,
|
||||
echo=echo,
|
||||
encoding=encoding,
|
||||
codec_errors=codec_errors,
|
||||
dimensions=dimensions,
|
||||
)
|
||||
|
||||
self.name = "<pxssh>"
|
||||
|
||||
# SUBTLE HACK ALERT! Note that the command that SETS the prompt uses a
|
||||
# slightly different string than the regular expression to match it. This
|
||||
# is because when you set the prompt the command will echo back, but we
|
||||
# don't want to match the echoed command. So if we make the set command
|
||||
# slightly different than the regex we eliminate the problem. To make the
|
||||
# set command different we add a backslash in front of $. The $ doesn't
|
||||
# need to be escaped, but it doesn't hurt and serves to make the set
|
||||
# prompt command different than the regex.
|
||||
|
||||
# used to match the command-line prompt
|
||||
self.UNIQUE_PROMPT = "\[PEXPECT\][\$\#] "
|
||||
self.PROMPT = self.UNIQUE_PROMPT
|
||||
|
||||
# used to set shell command-line prompt to UNIQUE_PROMPT.
|
||||
self.PROMPT_SET_SH = "PS1='[PEXPECT]\$ '"
|
||||
self.PROMPT_SET_CSH = "set prompt='[PEXPECT]\$ '"
|
||||
self.SSH_OPTS = "-o'RSAAuthentication=no'" + " -o 'PubkeyAuthentication=no'"
|
||||
# Disabling host key checking, makes you vulnerable to MITM attacks.
|
||||
# + " -o 'StrictHostKeyChecking=no'"
|
||||
# + " -o 'UserKnownHostsFile /dev/null' ")
|
||||
# Disabling X11 forwarding gets rid of the annoying SSH_ASKPASS from
|
||||
# displaying a GUI password dialog. I have not figured out how to
|
||||
# disable only SSH_ASKPASS without also disabling X11 forwarding.
|
||||
# Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying!
|
||||
# self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'"
|
||||
self.force_password = False
|
||||
|
||||
# User defined SSH options, eg,
|
||||
# ssh.otions = dict(StrictHostKeyChecking="no",UserKnownHostsFile="/dev/null")
|
||||
self.options = options
|
||||
|
||||
self.dimensions = dimensions
|
||||
|
||||
def levenshtein_distance(self, a, b):
|
||||
"""This calculates the Levenshtein distance between a and b."""
|
||||
|
||||
n, m = len(a), len(b)
|
||||
if n > m:
|
||||
a, b = b, a
|
||||
n, m = m, n
|
||||
current = range(n + 1)
|
||||
for i in range(1, m + 1):
|
||||
previous, current = current, [i] + [0] * n
|
||||
for j in range(1, n + 1):
|
||||
add, delete = previous[j] + 1, current[j - 1] + 1
|
||||
change = previous[j - 1]
|
||||
if a[j - 1] != b[i - 1]:
|
||||
change = change + 1
|
||||
current[j] = min(add, delete, change)
|
||||
return current[n]
|
||||
|
||||
def try_read_prompt(self, timeout_multiplier):
|
||||
"""This facilitates using communication timeouts to perform
|
||||
synchronization as quickly as possible, while supporting high latency
|
||||
connections with a tunable worst case performance. Fast connections
|
||||
should be read almost immediately. Worst case performance for this
|
||||
method is timeout_multiplier * 3 seconds.
|
||||
"""
|
||||
|
||||
# maximum time allowed to read the first response
|
||||
first_char_timeout = timeout_multiplier * 0.5
|
||||
|
||||
# maximum time allowed between subsequent characters
|
||||
inter_char_timeout = timeout_multiplier * 0.1
|
||||
|
||||
# maximum time for reading the entire prompt
|
||||
total_timeout = timeout_multiplier * 3.0
|
||||
|
||||
prompt = self.string_type()
|
||||
begin = time.time()
|
||||
expired = 0.0
|
||||
timeout = first_char_timeout
|
||||
|
||||
while expired < total_timeout:
|
||||
try:
|
||||
prompt += self.read_nonblocking(size=1, timeout=timeout)
|
||||
expired = time.time() - begin # updated total time expired
|
||||
timeout = inter_char_timeout
|
||||
except TIMEOUT:
|
||||
break
|
||||
|
||||
return prompt
|
||||
|
||||
def sync_original_prompt(self, sync_multiplier=1.0):
|
||||
"""This attempts to find the prompt. Basically, press enter and record
|
||||
the response; press enter again and record the response; if the two
|
||||
responses are similar then assume we are at the original prompt.
|
||||
This can be a slow function. Worst case with the default sync_multiplier
|
||||
can take 12 seconds. Low latency connections are more likely to fail
|
||||
with a low sync_multiplier. Best case sync time gets worse with a
|
||||
high sync multiplier (500 ms with default)."""
|
||||
|
||||
# All of these timing pace values are magic.
|
||||
# I came up with these based on what seemed reliable for
|
||||
# connecting to a heavily loaded machine I have.
|
||||
self.sendline()
|
||||
time.sleep(0.1)
|
||||
|
||||
try:
|
||||
# Clear the buffer before getting the prompt.
|
||||
self.try_read_prompt(sync_multiplier)
|
||||
except TIMEOUT:
|
||||
pass
|
||||
|
||||
self.sendline()
|
||||
x = self.try_read_prompt(sync_multiplier)
|
||||
|
||||
self.sendline()
|
||||
a = self.try_read_prompt(sync_multiplier)
|
||||
|
||||
self.sendline()
|
||||
b = self.try_read_prompt(sync_multiplier)
|
||||
|
||||
ld = self.levenshtein_distance(a, b)
|
||||
len_a = len(a)
|
||||
if len_a == 0:
|
||||
return False
|
||||
if float(ld) / len_a < 0.4:
|
||||
return True
|
||||
return False
|
||||
|
||||
# TODO: This is getting messy and I'm pretty sure this isn't perfect.
|
||||
# TODO: I need to draw a flow chart for this.
|
||||
def login(
|
||||
self,
|
||||
server,
|
||||
username,
|
||||
password="",
|
||||
terminal_type="ansi",
|
||||
original_prompt=r"[#$]",
|
||||
login_timeout=10,
|
||||
port=None,
|
||||
auto_prompt_reset=True,
|
||||
ssh_key=None,
|
||||
quiet=True,
|
||||
sync_multiplier=1,
|
||||
check_local_ip=True,
|
||||
):
|
||||
"""This logs the user into the given server.
|
||||
|
||||
It uses
|
||||
'original_prompt' to try to find the prompt right after login. When it
|
||||
finds the prompt it immediately tries to reset the prompt to something
|
||||
more easily matched. The default 'original_prompt' is very optimistic
|
||||
and is easily fooled. It's more reliable to try to match the original
|
||||
prompt as exactly as possible to prevent false matches by server
|
||||
strings such as the "Message Of The Day". On many systems you can
|
||||
disable the MOTD on the remote server by creating a zero-length file
|
||||
called :file:`~/.hushlogin` on the remote server. If a prompt cannot be found
|
||||
then this will not necessarily cause the login to fail. In the case of
|
||||
a timeout when looking for the prompt we assume that the original
|
||||
prompt was so weird that we could not match it, so we use a few tricks
|
||||
to guess when we have reached the prompt. Then we hope for the best and
|
||||
blindly try to reset the prompt to something more unique. If that fails
|
||||
then login() raises an :class:`ExceptionPxssh` exception.
|
||||
|
||||
In some situations it is not possible or desirable to reset the
|
||||
original prompt. In this case, pass ``auto_prompt_reset=False`` to
|
||||
inhibit setting the prompt to the UNIQUE_PROMPT. Remember that pxssh
|
||||
uses a unique prompt in the :meth:`prompt` method. If the original prompt is
|
||||
not reset then this will disable the :meth:`prompt` method unless you
|
||||
manually set the :attr:`PROMPT` attribute.
|
||||
"""
|
||||
|
||||
ssh_options = "".join(
|
||||
[" -o '%s=%s'" % (o, v) for (o, v) in self.options.items()]
|
||||
)
|
||||
if quiet:
|
||||
ssh_options = ssh_options + " -q"
|
||||
if not check_local_ip:
|
||||
ssh_options = ssh_options + " -o'NoHostAuthenticationForLocalhost=yes'"
|
||||
if self.force_password:
|
||||
ssh_options = ssh_options + " " + self.SSH_OPTS
|
||||
if port is not None:
|
||||
ssh_options = ssh_options + " -p %s" % (str(port))
|
||||
if ssh_key is not None:
|
||||
try:
|
||||
os.path.isfile(ssh_key)
|
||||
except:
|
||||
raise ExceptionPxssh("private ssh key does not exist")
|
||||
ssh_options = ssh_options + " -i %s" % (ssh_key)
|
||||
cmd = "ssh %s -l %s %s" % (ssh_options, username, server)
|
||||
|
||||
# This does not distinguish between a remote server 'password' prompt
|
||||
# and a local ssh 'passphrase' prompt (for unlocking a private key).
|
||||
spawn._spawn(self, cmd, dimensions=self.dimensions)
|
||||
i = self.expect(
|
||||
[
|
||||
"(?i)are you sure you want to continue connecting",
|
||||
original_prompt,
|
||||
"(?i)(?:password)|(?:passphrase for key)",
|
||||
"(?i)permission denied",
|
||||
"(?i)terminal type",
|
||||
TIMEOUT,
|
||||
"(?i)connection closed by remote host",
|
||||
EOF,
|
||||
],
|
||||
timeout=login_timeout,
|
||||
)
|
||||
|
||||
# First phase
|
||||
if i == 0:
|
||||
# New certificate -- always accept it.
|
||||
# This is what you get if SSH does not have the remote host's
|
||||
# public key stored in the 'known_hosts' cache.
|
||||
self.sendline("yes")
|
||||
i = self.expect(
|
||||
[
|
||||
"(?i)are you sure you want to continue connecting",
|
||||
original_prompt,
|
||||
"(?i)(?:password)|(?:passphrase for key)",
|
||||
"(?i)permission denied",
|
||||
"(?i)terminal type",
|
||||
TIMEOUT,
|
||||
]
|
||||
)
|
||||
if i == 2: # password or passphrase
|
||||
self.sendline(password)
|
||||
i = self.expect(
|
||||
[
|
||||
"(?i)are you sure you want to continue connecting",
|
||||
original_prompt,
|
||||
"(?i)(?:password)|(?:passphrase for key)",
|
||||
"(?i)permission denied",
|
||||
"(?i)terminal type",
|
||||
TIMEOUT,
|
||||
]
|
||||
)
|
||||
if i == 4:
|
||||
self.sendline(terminal_type)
|
||||
i = self.expect(
|
||||
[
|
||||
"(?i)are you sure you want to continue connecting",
|
||||
original_prompt,
|
||||
"(?i)(?:password)|(?:passphrase for key)",
|
||||
"(?i)permission denied",
|
||||
"(?i)terminal type",
|
||||
TIMEOUT,
|
||||
]
|
||||
)
|
||||
if i == 7:
|
||||
self.close()
|
||||
raise ExceptionPxssh("Could not establish connection to host")
|
||||
|
||||
# Second phase
|
||||
if i == 0:
|
||||
# This is weird. This should not happen twice in a row.
|
||||
self.close()
|
||||
raise ExceptionPxssh('Weird error. Got "are you sure" prompt twice.')
|
||||
elif i == 1: # can occur if you have a public key pair set to authenticate.
|
||||
# TODO: May NOT be OK if expect() got tricked and matched a false prompt.
|
||||
pass
|
||||
elif i == 2: # password prompt again
|
||||
# For incorrect passwords, some ssh servers will
|
||||
# ask for the password again, others return 'denied' right away.
|
||||
# If we get the password prompt again then this means
|
||||
# we didn't get the password right the first time.
|
||||
self.close()
|
||||
raise ExceptionPxssh("password refused")
|
||||
elif i == 3: # permission denied -- password was bad.
|
||||
self.close()
|
||||
raise ExceptionPxssh("permission denied")
|
||||
elif i == 4: # terminal type again? WTF?
|
||||
self.close()
|
||||
raise ExceptionPxssh('Weird error. Got "terminal type" prompt twice.')
|
||||
elif i == 5: # Timeout
|
||||
# This is tricky... I presume that we are at the command-line prompt.
|
||||
# It may be that the shell prompt was so weird that we couldn't match
|
||||
# it. Or it may be that we couldn't log in for some other reason. I
|
||||
# can't be sure, but it's safe to guess that we did login because if
|
||||
# I presume wrong and we are not logged in then this should be caught
|
||||
# later when I try to set the shell prompt.
|
||||
raise ExceptionPxssh("connection timeout")
|
||||
elif i == 6: # Connection closed by remote host
|
||||
self.close()
|
||||
raise ExceptionPxssh("connection closed")
|
||||
else: # Unexpected
|
||||
self.close()
|
||||
raise ExceptionPxssh("unexpected login response")
|
||||
if not self.sync_original_prompt(sync_multiplier):
|
||||
self.close()
|
||||
raise ExceptionPxssh("could not synchronize with original prompt")
|
||||
# We appear to be in.
|
||||
# set shell prompt to something unique.
|
||||
if auto_prompt_reset:
|
||||
if not self.set_unique_prompt():
|
||||
self.close()
|
||||
raise ExceptionPxssh(
|
||||
"could not set shell prompt "
|
||||
"(received: %r, expected: %r)."
|
||||
% (
|
||||
self.before,
|
||||
self.PROMPT,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
def logout(self):
|
||||
"""Sends exit to the remote shell.
|
||||
|
||||
If there are stopped jobs then this automatically sends exit twice.
|
||||
"""
|
||||
self.sendline("exit")
|
||||
index = self.expect([EOF, "(?i)there are stopped jobs"])
|
||||
if index == 1:
|
||||
self.sendline("exit")
|
||||
self.expect(EOF)
|
||||
self.close()
|
||||
|
||||
def prompt(self, timeout=-1):
|
||||
"""Match the next shell prompt.
|
||||
|
||||
This is little more than a short-cut to the :meth:`~pexpect.spawn.expect`
|
||||
method. Note that if you called :meth:`login` with
|
||||
``auto_prompt_reset=False``, then before calling :meth:`prompt` you must
|
||||
set the :attr:`PROMPT` attribute to a regex that it will use for
|
||||
matching the prompt.
|
||||
|
||||
Calling :meth:`prompt` will erase the contents of the :attr:`before`
|
||||
attribute even if no prompt is ever matched. If timeout is not given or
|
||||
it is set to -1 then self.timeout is used.
|
||||
|
||||
:return: True if the shell prompt was matched, False if the timeout was
|
||||
reached.
|
||||
"""
|
||||
|
||||
if timeout == -1:
|
||||
timeout = self.timeout
|
||||
i = self.expect([self.PROMPT, TIMEOUT], timeout=timeout)
|
||||
if i == 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_unique_prompt(self):
|
||||
"""This sets the remote prompt to something more unique than ``#`` or ``$``.
|
||||
This makes it easier for the :meth:`prompt` method to match the shell prompt
|
||||
unambiguously. This method is called automatically by the :meth:`login`
|
||||
method, but you may want to call it manually if you somehow reset the
|
||||
shell prompt. For example, if you 'su' to a different user then you
|
||||
will need to manually reset the prompt. This sends shell commands to
|
||||
the remote host to set the prompt, so this assumes the remote host is
|
||||
ready to receive commands.
|
||||
|
||||
Alternatively, you may use your own prompt pattern. In this case you
|
||||
should call :meth:`login` with ``auto_prompt_reset=False``; then set the
|
||||
:attr:`PROMPT` attribute to a regular expression. After that, the
|
||||
:meth:`prompt` method will try to match your prompt pattern.
|
||||
"""
|
||||
|
||||
self.sendline("unset PROMPT_COMMAND")
|
||||
self.sendline(self.PROMPT_SET_SH) # sh-style
|
||||
i = self.expect([TIMEOUT, self.PROMPT], timeout=10)
|
||||
if i == 0: # csh-style
|
||||
self.sendline(self.PROMPT_SET_CSH)
|
||||
i = self.expect([TIMEOUT, self.PROMPT], timeout=10)
|
||||
if i == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class SshConsole(Console):
|
||||
"""Concrete implementation of Console based on SSH."""
|
||||
|
||||
TYPE = "ssh"
|
||||
|
||||
class LoginException(Exception):
|
||||
"""Raised when failed to login"""
|
||||
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
host,
|
||||
user="root",
|
||||
password="",
|
||||
options={"StrictHostKeyChecking": "no"},
|
||||
port=None,
|
||||
sync_multiplier=2.0,
|
||||
echoOn=False,
|
||||
mirror=False,
|
||||
):
|
||||
super().__init__(name, echoOn=echoOn)
|
||||
self.name = name
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.echo_on = echoOn
|
||||
self.mirror = mirror
|
||||
self.password = password
|
||||
self.options = options
|
||||
self.port = port
|
||||
self.sync_multiplier = sync_multiplier
|
||||
self.session = None
|
||||
|
||||
def open(self, logfile=None, login_timeout=5):
|
||||
"""Start the SSH session"""
|
||||
|
||||
# have to create a new pxssh for each login temptative
|
||||
session = pxssh(logfile=logfile, options=self.options, echo=self.mirror)
|
||||
try:
|
||||
session.login(
|
||||
server=self.host,
|
||||
username=self.user,
|
||||
password=self.password,
|
||||
auto_prompt_reset=False,
|
||||
login_timeout=login_timeout,
|
||||
port=self.port,
|
||||
sync_multiplier=self.sync_multiplier,
|
||||
)
|
||||
except ExceptionPxssh as exc:
|
||||
raise SshConsole.LoginException(exc)
|
||||
|
||||
self.session = session
|
||||
self.isOpened = True
|
||||
|
||||
def close(self):
|
||||
"""Close the SSH session"""
|
||||
if self.isOpened:
|
||||
self.session.close()
|
||||
self.session = None
|
||||
self.isOpened = False
|
||||
|
||||
def write(self, characters, mute=False):
|
||||
"""Write a set of characters into the SSH session"""
|
||||
if self.echo_on and not mute:
|
||||
ech = "" if characters.strip(' ').endswith("\n") else "\n"
|
||||
print(("[>" + self.name + "] : " + characters), end=ech)
|
||||
self.session.write(characters)
|
||||
|
||||
def readchar(self, timeout):
|
||||
"""Read a single character from the SSH session"""
|
||||
try:
|
||||
return self.session.read_nonblocking(size=1, timeout=timeout)
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
return None
|
||||
|
||||
def readline(self):
|
||||
"""Read until a \r\n is found."""
|
||||
return self.session.readline()
|
||||
|
||||
def read_nowait(self, mute=False):
|
||||
"""Read a single character from the SSH session"""
|
||||
try:
|
||||
st = self.session.read_nonblocking(size=self.session.maxread, timeout=0)
|
||||
except pexpect.TIMEOUT:
|
||||
return ""
|
||||
|
||||
s = st.decode("utf-8", errors="replace")
|
||||
if not mute:
|
||||
date_str = str(datetime.now()).split(".")[0].split(" ")[1]
|
||||
self.stream.write("[{} {}]".format(date_str, self.name) + s)
|
||||
return s
|
||||
73
src/testium/libs/raw_tcp_console.py
Normal file
73
src/testium/libs/raw_tcp_console.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import socket
|
||||
import traceback
|
||||
|
||||
from libs.console import *
|
||||
|
||||
class RawTCPConsole(Console):
|
||||
TYPE = 'rawtcp'
|
||||
|
||||
def __init__(self, name, address, port, echoOn=False, write_delay=0):
|
||||
super().__init__(name, echoOn, write_delay)
|
||||
self.sock = None
|
||||
self.address = address
|
||||
self.port = int(port)
|
||||
self.stimeout = 0
|
||||
|
||||
def open(self):
|
||||
#if trying to connect when already connected.
|
||||
_socket = None
|
||||
if self.sock is not None:
|
||||
raise Exception('Already connected to the target')
|
||||
else:
|
||||
try:
|
||||
_socket = socket.create_connection((self.address, self.port))
|
||||
self.sock = _socket
|
||||
self.isOpened = True
|
||||
self.sock.settimeout(self.stimeout)
|
||||
except:
|
||||
if _socket is not None:
|
||||
_socket.close()
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
self.sock = None
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self.sock != None:
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
self.isOpened = False
|
||||
except:
|
||||
pass
|
||||
|
||||
def set_read_timeout(self, timeout):
|
||||
if self.stimeout != timeout:
|
||||
self.sock.settimeout(timeout)
|
||||
self.stimeout = timeout
|
||||
|
||||
def readchar(self, timeout):
|
||||
c = ''.encode()
|
||||
try:
|
||||
c = self.sock.recv(1)
|
||||
except:
|
||||
pass
|
||||
return c
|
||||
|
||||
def read_nowait(self, mute=False):
|
||||
s = ''.encode()
|
||||
self.sock.settimeout(0)
|
||||
self.stimeout = 0
|
||||
s = self.sock.recv(4096)
|
||||
st = s.decode('utf-8', errors='replace')
|
||||
if not mute:
|
||||
date_str = str(datetime.now()).split('.')[0].split(' ')[1]
|
||||
self.stream.write('[{} {}]'.format(date_str, self.name)+st)
|
||||
return st
|
||||
|
||||
def write(self, s, mute=False):
|
||||
if self.echo_on and not mute:
|
||||
ech = '' if s.strip(' ').endswith('\n') else '\n'
|
||||
print(('[>' + self.name + '] : ' + s), end=ech)
|
||||
res = self.sock.sendall(s.encode('utf-8'))
|
||||
return res
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user