Project restart

This commit is contained in:
2025-12-29 10:46:05 +01:00
commit 59d19cb48c
388 changed files with 48020 additions and 0 deletions

1
src/VERSION Normal file
View File

@@ -0,0 +1 @@
0.1

38
src/pyproject.toml Normal file
View 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
View 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
View 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
View 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()

View File

View 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")

View 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

View 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')

View 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:])

View File

@@ -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

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

@@ -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:])

View File

@@ -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

View File

@@ -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>

View File

@@ -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:])

View File

@@ -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()

View File

@@ -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:])

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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:])

View 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, "")

View 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

View 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

View 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')

View File

@@ -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))

View 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
)

View 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))

View 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),
)

View 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)

View 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, '')

View 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()

View File

@@ -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

View File

@@ -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)]

View 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')

View 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()

View 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()

View File

@@ -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()

View 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)

View 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')

View 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")

View 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)))

View File

@@ -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()

View 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

View 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()

View 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'"))

View File

@@ -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:])

View File

@@ -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

View File

@@ -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>

View 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

View 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'

View 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)

View 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)

View 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

View 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())

View 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

View 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 = {}

View 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)

View File

@@ -0,0 +1,9 @@
SUPPORTED_API = [
"gd",
"setgd",
"delgd",
"add_plot_values",
"last_plot_value"
]

View 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"}

View 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 = "."

View 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

View 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

View 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()

View 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]

View 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)

View 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")

View 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

View 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

View 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

View 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

View 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)

View 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()

View 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)

View 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

View 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()

View 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()

View 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

View 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)

View 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

View 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)

View File

629
src/testium/libs/console.py Executable file
View 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
View 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

View 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