13 from uuid
import uuid4
14 from os
import urandom
15 from functools
import wraps
16 from multiprocessing
import pool, cpu_count
17 from http.server
import SimpleHTTPRequestHandler, HTTPServer
19 from fake_http_server.fake_test_server
import FakeTestServerBackground
20 from sota_tools.treehub_server
import create_repo
23 logger = logging.getLogger(__name__)
28 def __init__(self, aktualizr_primary_exe, aktualizr_info_exe, id,
29 uptane_server, wait_port=9040, wait_timeout=60, log_level=1,
30 primary_port=None, secondaries=None, secondary_wait_sec=600, output_logs=True,
31 run_mode='once', director=None, image_repo=None,
32 sysroot=None, treehub=None, ostree_mock_path=None, **kwargs):
43 with open(path.join(self.
_storage_dir.name,
'secondary_config.json'),
'w+')
as secondary_config_file:
44 secondary_cfg = json.loads(Aktualizr.SECONDARY_CONFIG_TEMPLATE.
45 format(port=primary_port
if primary_port
else wait_port,
46 timeout=wait_timeout))
47 json.dump(secondary_cfg, secondary_config_file)
51 with open(path.join(self.
_storage_dir.name,
'config.toml'),
'w+')
as config_file:
52 config_file.write(Aktualizr.CONFIG_TEMPLATE.format(server_url=uptane_server.base_url,
54 serial=id[1], hw_ID=id[0],
60 director=director.base_url
if director
else '',
61 image_repo=image_repo.base_url
if image_repo
else '',
62 pacman_type=
'ostree' if treehub
and sysroot
else 'none',
63 ostree_sysroot=sysroot.path
if sysroot
else '',
64 treehub_server=treehub.base_url
if treehub
else '',
69 if secondaries
is not None:
75 if sysroot
and ostree_mock_path:
76 self.
_run_env[
'LD_PRELOAD'] = os.path.abspath(ostree_mock_path)
77 self.
_run_env[
'OSTREE_DEPLOYMENT_VERSION_FILE'] = sysroot.version_file
81 server = "{server_url}"
84 base_path = "{import_path}"
85 tls_cacert_path = "ca.pem"
86 tls_pkey_path = "pkey.pem"
87 tls_clientcert_path = "client.pem"
90 primary_ecu_serial = "{serial}"
91 primary_ecu_hardware_id = "{hw_ID}"
94 path = "{storage_dir}"
96 sqldb_path = "{db_path}"
99 type = "{pacman_type}"
100 sysroot = "{ostree_sysroot}"
101 ostree_server = "{treehub_server}"
106 secondary_config_file = "{secondary_cfg_file}"
107 secondary_preinstall_wait_sec = {secondary_wait_sec}
108 director_server = "{director}"
109 repo_server = "{image_repo}"
112 reboot_sentinel_dir = "{sentinel_dir}"
113 reboot_sentinel_name = "{sentinel_name}"
117 loglevel = {log_level}
121 SECONDARY_CONFIG_TEMPLATE =
'''
124 "secondaries_wait_port": {port},
125 "secondaries_wait_timeout": {timeout},
131 def add_secondary(self, secondary):
133 sec_cfg = json.load(config_file)
134 sec_cfg[
"IP"][
"secondaries"].append({
"addr":
"127.0.0.1:{}".format(secondary.port)})
136 json.dump(sec_cfg, config_file)
138 def update_wait_timeout(self, timeout):
140 sec_cfg = json.load(config_file)
141 sec_cfg[
"IP"][
"secondaries_wait_timeout"] = timeout
143 json.dump(sec_cfg, config_file)
145 def run(self, run_mode):
149 def get_info(self, retry=15):
151 for ii
in range(0, retry):
153 timeout=60, stdout=subprocess.PIPE, env=self.
_run_env)
154 if info_exe_res.returncode == 0
and \
155 str(info_exe_res.stdout).find(
'no details about installed nor pending images') != -1:
158 if info_exe_res
and info_exe_res.returncode == 0:
159 return str(info_exe_res.stdout)
161 logger.error(str(info_exe_res.stderr))
166 def is_ecu_registered(self, ecu_id):
168 if not ((device_status.find(ecu_id[0]) != -1)
and (device_status.find(ecu_id[1]) != -1)):
170 not_registered_field =
"Removed or not registered ecus:"
171 not_reg_start = device_status.find(not_registered_field)
172 return not_reg_start == -1
or (device_status.find(ecu_id[1], not_reg_start) == -1)
174 def get_current_image_info(self, ecu_id):
175 if self.
id == ecu_id:
180 def get_current_pending_image_info(self, ecu_id):
187 def _get_current_image_info(self, ecu_id, secondary_image_hash_field='installed image hash:
'):
190 ecu_serial = ecu_id[1]
191 ecu_info_position = aktualizr_status.find(ecu_serial)
192 if ecu_info_position == -1:
195 start = aktualizr_status.find(secondary_image_hash_field, ecu_info_position)
196 end = aktualizr_status.find(
'\\n', start)
197 hash_val = aktualizr_status[start + len(secondary_image_hash_field):end]
207 def get_current_primary_image_info(self):
208 primary_hash_field =
'Current primary ecu running version: '
211 start = aktualizr_status.find(primary_hash_field)
212 end = aktualizr_status.find(
'\\n', start)
213 return aktualizr_status[start + len(primary_hash_field):end]
215 logger.error(
"Failed to get aktualizr info/status")
220 def get_primary_pending_version(self):
221 primary_hash_field =
'Pending primary ecu version: '
223 start = aktualizr_status.find(primary_hash_field)
224 end = aktualizr_status.find(
'\\n', start)
225 return aktualizr_status[start + len(primary_hash_field):end]
230 stderr=
None if self.
_output_logs else subprocess.STDOUT,
233 logger.debug(
"Aktualizr has been started")
236 def __exit__(self, exc_type, exc_val, exc_tb):
239 logger.debug(
"Aktualizr has been stopped")
241 def terminate(self, sig=signal.SIGTERM):
245 return self.
_process.stdout.read().decode(errors=
'replace')
247 def wait_for_completion(self, timeout=120):
250 def wait_for_provision(self, timeout=60):
251 deadline = time.time() + timeout
252 while timeout == 0
or time.time() < deadline:
254 if info
is not None and 'Provisioned on server: yes' in info:
259 def emulate_reboot(self):
267 def copy_keys(dest_path):
269 shutil.copy(KeyStore.ca(), dest_path)
270 shutil.copy(KeyStore.pkey(), dest_path)
271 shutil.copy(KeyStore.cert(), dest_path)
275 return path.join(KeyStore.base_dir,
'tests/test_data/prov_testupdate/ca.pem')
279 return path.join(KeyStore.base_dir,
'tests/test_data/prov_testupdate/pkey.pem')
283 return path.join(KeyStore.base_dir,
'tests/test_data/prov_testupdate/client.pem')
288 def __init__(self, aktualizr_secondary_exe, id, port=None, primary_port=None,
289 sysroot=None, treehub=None, output_logs=True, ostree_mock_path=None, **kwargs):
300 with open(path.join(self.
storage_dir.name,
'config.toml'),
'w+')
as config_file:
301 config_file.write(IPSecondary.CONFIG_TEMPLATE.format(serial=id[1], hw_ID=id[0],
304 db_path=path.join(self.
storage_dir.name,
'db.sql'),
305 pacman_type=
'ostree' if treehub
and sysroot
else 'none',
306 ostree_sysroot=sysroot.path
if sysroot
else '',
307 treehub_server=treehub.base_url
if treehub
else '',
314 if sysroot
and ostree_mock_path:
315 self.
_run_env[
'LD_PRELOAD'] = os.path.abspath(ostree_mock_path)
316 self.
_run_env[
'OSTREE_DEPLOYMENT_VERSION_FILE'] = sysroot.version_file
319 CONFIG_TEMPLATE =
'''
321 ecu_serial = "{serial}"
322 ecu_hardware_id = "{hw_ID}"
326 primary_ip = "127.0.0.1"
327 primary_port = {primary_port}
331 path = "{storage_dir}"
332 sqldb_path = "{db_path}"
335 type = "{pacman_type}"
336 sysroot = "{ostree_sysroot}"
337 ostree_server = "{treehub_server}"
341 reboot_sentinel_dir = "{sentinel_dir}"
342 reboot_sentinel_name = "{sentinel_name}"
346 def is_running(self):
347 return True if self.
_process.poll()
is None else False
351 tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
353 port = tcp.getsockname()[1]
360 stderr=
None if self.
_output_logs else subprocess.STDOUT,
363 logger.debug(
"IP Secondary {} has been started: {}".format(self.
id, self.
port))
366 def __exit__(self, exc_type, exc_val, exc_tb):
369 logger.debug(
"IP Secondary {} has been stopped".format(self.
id))
371 def emulate_reboot(self):
376 def __init__(self, doc_root, ifc, port, client_handler_map={}):
377 super(UptaneRepo, self).__init__(server_address=(ifc, port), RequestHandlerClass=self.
Handler)
379 self.
base_url =
'http://{}:{}'.format(self.server_address[0], self.server_address[1])
384 lambda request: (self.
Handler.handler_map.get(
'POST', {})).get(request.path,
385 self.
Handler.default_handler)(request)
388 lambda request: (self.
Handler.handler_map.get(
'PUT', {})).get(request.path,
389 self.
Handler.default_handler)(request)
392 lambda request: (self.
Handler.handler_map.get(
'GET', {})).get(request.path,
393 self.
Handler.default_get)(request)
395 for method, method_handlers
in client_handler_map.items():
396 for url, handler
in method_handlers.items():
397 if self.
Handler.handler_map.get(method,
None)
is None:
398 self.
Handler.handler_map[method] = {}
399 self.
Handler.handler_map[method][url] = handler
402 def __init__(self, request, client_address, server):
407 def default_handler(self):
408 self.send_response(200)
411 def default_get(self):
413 self.send_response(404)
416 self.send_response(200)
418 with open(self.
file_path,
'rb')
as source:
419 self.copyfile(source, self.wfile)
425 return os.path.join(self.
doc_root, self.path[1:])
428 self._server_thread = threading.Thread(target=self.serve_forever)
429 self._server_thread.start()
435 if self._server_thread:
436 self._server_thread.join(timeout=60)
437 self._server_thread =
None
442 def __exit__(self, exc_type, exc_val, exc_tb):
449 - serves signed metadata about images
450 - receives device manifest which includes installation report if any installation has happened
453 director_subdir =
"repo/director"
455 def __init__(self, uptane_repo_root, ifc, port, client_handler_map={}):
456 super(DirectorRepo, self).__init__(os.path.join(uptane_repo_root, self.
director_subdir), ifc=ifc, port=port,
457 client_handler_map=client_handler_map)
465 def handle_manifest(self):
466 self.send_response(200)
470 data_size = int(self.headers[
'Content-Length'])
471 data_string = self.rfile.read(data_size)
472 json_data = json.loads(data_string)
473 except Exception
as exc:
477 install_report = json_data[
'signed'].get(
'installation_report',
"")
479 self.server.set_install_event(json_data)
481 handler_map = {
'PUT': {
'/manifest': handle_manifest}}
483 def set_install_event(self, manifest):
484 with self._installed_condition:
485 self._manifest = manifest
486 self._last_install_res = manifest[
'signed'][
'installation_report'][
'report'][
'result'][
'success']
487 self._installed_condition.notifyAll()
489 def wait_for_install(self, timeout=180):
490 with self._installed_condition:
491 self._installed_condition.wait(timeout=timeout)
492 return self._last_install_res
494 def get_install_result(self):
495 with self._installed_condition:
496 return self._last_install_res
498 def get_manifest(self):
499 with self._installed_condition:
500 return self._manifest
502 def get_ecu_manifest(self, ecu_serial):
503 return self.get_manifest()[
'signed'][
'ecu_version_manifests'][ecu_serial]
505 def get_ecu_manifest_filepath(self, ecu_serial):
506 return self.get_ecu_manifest(ecu_serial)[
'signed'][
'installed_image'][
'filepath']
511 This server serves signed metadata about images
512 as well as images by default (it's possible to serve images from another server by using the 'custom URI' feature)
515 image_subdir =
"repo/repo"
517 def __init__(self, uptane_repo_root, ifc, port, client_handler_map={}):
518 super(ImageRepo, self).__init__(os.path.join(uptane_repo_root, self.
image_subdir), ifc=ifc, port=port,
519 client_handler_map=client_handler_map)
524 This server serves images
526 image_subdir =
"repo/repo"
528 def __init__(self, root, ifc, port, client_handler_map={}):
529 super(CustomRepo, self).__init__(os.path.join(root, self.
image_subdir),
530 ifc=ifc, port=port, client_handler_map=client_handler_map)
535 This server serves requests from an ostree client, i.e. emulates/mocks the treehub server
537 def __init__(self, ifc, port, client_handler_map={}):
538 self.
root = tempfile.mkdtemp()
539 super(Treehub, self).__init__(self.
root, ifc=ifc, port=port, client_handler_map=client_handler_map)
543 return super(Treehub, self).__enter__()
545 def __exit__(self, exc_type, exc_val, exc_tb):
546 super(Treehub, self).__exit__(exc_type, exc_val, exc_tb)
547 shutil.rmtree(self.
root, ignore_errors=
True)
550 def default_get(self):
552 self.send_response(404)
560 def __init__(self, number_of_failures=1, bytes_to_send_before_interruption=10, url=''):
566 def __call__(self, request_handler):
568 request_handler.send_response(200)
569 file_size = os.path.getsize(request_handler.file_path)
570 request_handler.send_header(
'Content-Length', file_size)
571 request_handler.end_headers()
573 with open(request_handler.file_path,
'rb')
as source:
575 request_handler.wfile.write(data)
579 request_handler.default_get()
581 def map(self, url=''):
587 dummy_filez = (b
'\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06' +
588 b
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x81\xa4\x00\x00\x00\x00' +
589 b
'\x00\x19\x33\x34\x32\x36\x31\xe5\x02\x00')
591 def __init__(self, number_of_failures=1, url='', fake_filez=False):
597 def __call__(self, request_handler):
599 request_handler.send_response(200)
600 request_handler.end_headers()
604 request_handler.wfile.write(b
'malformed image')
608 request_handler.default_get()
616 def __init__(self, number_of_failures=1, url=''):
621 def __call__(self, request_handler):
623 request_handler.send_response(200)
624 file_size = os.path.getsize(request_handler.file_path)
625 request_handler.end_headers()
627 with open(request_handler.file_path,
'rb')
as source:
629 data = source.read(1)
632 request_handler.wfile.write(data)
633 request_handler.wfile.flush()
639 request_handler.default_get()
646 def __init__(self, number_of_redirects=1, url=''):
651 def __call__(self, request_handler):
653 request_handler.send_response(301)
654 request_handler.send_header(
'Location', request_handler.server.base_url + request_handler.path)
655 request_handler.end_headers()
658 request_handler.default_get()
665 def __init__(self, number_of_failures=1):
669 def __call__(self, request_handler):
671 request_handler.send_response(200)
672 request_handler.end_headers()
673 request_handler.wfile.write(b
'{"non-uptane-json": "some-value"}')
677 request_handler.default_get()
684 def __init__(self, repo_manager_exe, server_port=0, director_port=0, image_repo_port=0, custom_repo_port=0):
696 def create_generic_server(self, **kwargs):
699 def create_director_repo(self, handler_map={}):
702 def create_image_repo(self, handler_map={}):
705 def create_custom_repo(self, handler_map={}):
713 def target_dir(self):
717 def target_file(self):
718 return path.join(self.
image_dir,
'targets.json')
720 def add_image(self, id, image_filename, target_name=None, image_size=1024, custom_url=''):
722 targetname = target_name
if target_name
else image_filename
724 with open(path.join(self.
image_dir, image_filename),
'wb')
as image_file:
725 image_file.write(urandom(image_size))
728 '--command',
'image',
'--filename', image_filename,
'--targetname', targetname,
'--hwid', id[0]]
731 image_creation_cmdline.append(
'--url')
732 image_creation_cmdline.append(custom_url)
734 subprocess.run(image_creation_cmdline, cwd=self.
image_dir, check=
True)
738 '--command',
'addtarget',
'--targetname', targetname,
739 '--hwid', id[0],
'--serial', id[1]], check=
True)
745 targets = json.load(target_file)
746 target_hash = targets[
"signed"][
"targets"][targetname][
"hashes"][
"sha256"]
750 def add_ostree_target(self, id, rev_hash, target_name=None, expires_within_sec=(60 * 5)):
752 target_name = rev_hash
if target_name
is None else "{}-{}".format(target_name, rev_hash)
754 '--command',
'image',
756 '--targetname', target_name,
757 '--targetsha256', rev_hash,
758 '--targetlength',
'0',
759 '--targetformat',
'OSTREE',
761 subprocess.run(image_creation_cmdline, check=
True)
763 expiration_time = time.time() + expires_within_sec
764 expiration_time_str = time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(expiration_time))
767 '--command',
'addtarget',
769 '--targetname', target_name,
772 '--expires', expiration_time_str],
783 def __exit__(self, exc_type, exc_val, exc_tb):
784 shutil.rmtree(self.
root_dir, ignore_errors=
True)
786 def _generate_repo(self):
788 '--command',
'generate',
'--keytype',
'ED25519'], check=
True)
791 def with_aktualizr(start=True, output_logs=False, id=(
'primary-hw-ID-001', str(uuid4())), wait_timeout=60,
792 secondary_wait_sec=600, log_level=1, aktualizr_primary_exe=
'src/aktualizr_primary/aktualizr',
793 aktualizr_info_exe=
'src/aktualizr_info/aktualizr-info',
797 def wrapper(*args, ostree_mock_path=None, **kwargs):
798 aktualizr =
Aktualizr(aktualizr_primary_exe=aktualizr_primary_exe,
799 aktualizr_info_exe=aktualizr_info_exe, id=id,
800 wait_timeout=wait_timeout,
801 secondary_wait_sec=secondary_wait_sec,
802 log_level=log_level, output_logs=output_logs,
803 run_mode=run_mode, ostree_mock_path=ostree_mock_path, **kwargs)
806 result = test(*args, **kwargs, aktualizr=aktualizr)
808 result = test(*args, **kwargs, aktualizr=aktualizr)
816 def with_uptane_backend(start_generic_server=True, repo_manager_exe='src/uptane_generator/uptane-generator'):
819 def wrapper(*args, **kwargs):
820 repo_manager_exe_abs_path = path.abspath(repo_manager_exe)
822 if start_generic_server:
823 with repo.create_generic_server()
as uptane_server:
824 result = test(*args, **kwargs, uptane_repo=repo, uptane_server=uptane_server)
826 result = test(*args, **kwargs, uptane_repo=repo)
832 def with_director(start=True, handlers=[]):
835 def wrapper(*args, uptane_repo, **kwargs):
836 def func(handler_map={}):
837 director = uptane_repo.create_director_repo(handler_map=handler_map)
840 result = test(*args, **kwargs, uptane_repo=uptane_repo, director=director)
842 result = test(*args, **kwargs, uptane_repo=uptane_repo, director=director)
845 if handlers
and len(handlers) > 0:
846 for handler
in handlers:
847 result = func(handler.map(kwargs.get(
'test_path',
'')))
857 def with_imagerepo(start=True, handlers=[]):
860 def wrapper(*args, uptane_repo, **kwargs):
861 def func(handler_map={}):
862 image_repo = uptane_repo.create_image_repo(handler_map=handler_map)
865 result = test(*args, **kwargs, uptane_repo=uptane_repo, image_repo=image_repo)
867 result = test(*args, **kwargs, uptane_repo=uptane_repo, image_repo=image_repo)
870 if handlers
and len(handlers) > 0:
871 for handler
in handlers:
872 result = func(handler.map(kwargs.get(
'test_path',
'')))
882 def with_secondary(start=True, output_logs=False, id=(
'secondary-hw-ID-001',
None), arg_name=
'secondary',
883 aktualizr_secondary_exe=
'src/aktualizr_secondary/aktualizr-secondary'):
886 def wrapper(*args, **kwargs):
889 id1 = (id1[0], str(uuid4()))
890 secondary =
IPSecondary(aktualizr_secondary_exe=aktualizr_secondary_exe, output_logs=output_logs, id=id1, **kwargs)
891 sl = kwargs.get(
"secondaries", []) + [secondary]
892 kwargs.update({arg_name: secondary,
"secondaries": sl})
893 if "primary_port" not in kwargs:
894 kwargs[
"primary_port"] = secondary.primary_port
897 result = test(*args, **kwargs)
899 result = test(*args, **kwargs)
905 def with_path(paths):
908 def wrapper(*args, **kwargs):
909 for test_path
in paths:
910 result = test(*args, **kwargs, test_path=test_path)
919 def __init__(self, aktualizr, uptane_repo, images_to_install=[]):
922 for image
in images_to_install:
925 'filename': image[1],
926 'hash': uptane_repo.add_image(image[0], image[1], custom_url=image[2]
if len(image) > 2
else '')
929 def are_images_installed(self):
932 if not (image[
'hash'] == self.
aktualizr.get_current_image_info(image[
'ecu_id'])):
939 def with_install_manager(default_images=True):
942 def wrapper(*args, aktualizr, uptane_repo, secondary=None, images_to_install=[], **kwargs):
943 if default_images
and (
not images_to_install
or len(images_to_install) == 0):
944 images_to_install = [(aktualizr.id,
'primary-image.img')]
946 images_to_install.append((secondary.id,
'secondary-image.img'))
947 install_mngr =
InstallManager(aktualizr, uptane_repo, images_to_install)
948 result = test(*args, **kwargs, aktualizr=aktualizr, secondary=secondary,
949 uptane_repo=uptane_repo, install_mngr=install_mngr)
955 def with_images(images_to_install):
958 def wrapper(*args, **kwargs):
959 return test(*args, **kwargs, images_to_install=images_to_install)
964 def with_customrepo(start=True, handlers=[]):
967 def wrapper(*args, uptane_repo, **kwargs):
968 def func(handler_map={}):
969 custom_repo = uptane_repo.create_custom_repo(handler_map=handler_map)
972 result = test(*args, **kwargs, uptane_repo=uptane_repo, custom_repo=custom_repo)
974 result = test(*args, **kwargs, uptane_repo=uptane_repo, custom_repo=custom_repo)
977 if handlers
and len(handlers) > 0:
978 for handler
in handlers:
979 result = func(handler.map(kwargs.get(
'test_path',
'')))
990 repo_path =
'ostree_repo'
993 self.
_root = tempfile.mkdtemp()
997 subprocess.run([
'cp',
'-r', self.
repo_path, self.
_root], check=
True)
1001 version_file.writelines([
'{}\n'.format(initial_revision),
1003 '{}\n'.format(initial_revision),
1004 '{}\n'.format(
'0')])
1008 def __exit__(self, exc_type, exc_val, exc_tb):
1009 shutil.rmtree(self.
_root, ignore_errors=
True)
1011 def get_revision(self):
1012 rev_cmd_res = subprocess.run([
'ostree',
'rev-parse',
'--repo', self.
path +
'/ostree/repo',
'generate-remote:generated'],
1013 timeout=60, check=
True, stdout=subprocess.PIPE)
1015 return rev_cmd_res.stdout.decode(
'ascii').rstrip(
'\n')
1017 def update_revision(self, rev):
1019 version_file.writelines([
'{}\n'.format(rev),
1022 '{}\n'.format(
'1')])
1025 def with_sysroot(ostree_mock_path='tests/libostree_mock.so'):
1026 def decorator(test):
1028 def wrapper(*args, **kwargs):
1030 return test(*args, **kwargs, sysroot=sysroot, ostree_mock_path=ostree_mock_path)
1035 def with_treehub(handlers=[], port=0):
1036 def decorator(test):
1038 def wrapper(*args, **kwargs):
1039 def func(handler_map={}):
1040 with Treehub(
'localhost', port=port, client_handler_map=handler_map)
as treehub:
1041 return test(*args, **kwargs, treehub=treehub)
1043 if handlers
and len(handlers) > 0:
1044 for handler
in handlers:
1045 result = func(handler.map(kwargs.get(
'test_path',
'')))
1056 def Process(self, *args, **kwds):
1057 proc = super(NonDaemonPool, self).
Process(*args, **kwds)
1059 class NonDaemonProcess(proc.__class__):
1060 """Monkey-patch process to ensure it is never daemonized"""
1067 def daemon(self, val):
1070 proc.__class__ = NonDaemonProcess
1076 def __init__(self, tests):
1081 def test_runner(test):
1082 logger.info(
'>>> Running {}...'.format(test.__name__))
1083 test_run_result = test()
1084 logger.info(
'>>> {}: {}\n'.format(
'OK' if test_run_result
else 'FAILED', test.__name__))
1085 return test_run_result
1090 for result
in results:
1091 total_result = total_result
and result