2022-06-09 13:56:26 -04:00
|
|
|
# vim: set syntax=python ts=4 :
|
|
|
|
#
|
|
|
|
# Copyright (c) 2018-2022 Intel Corporation
|
2022-07-13 12:58:48 +07:00
|
|
|
# Copyright 2022 NXP
|
2022-06-09 13:56:26 -04:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
2023-06-12 18:15:31 +02:00
|
|
|
from __future__ import annotations
|
2024-05-28 12:31:53 +00:00
|
|
|
from enum import Enum
|
2022-06-09 13:56:26 -04:00
|
|
|
import os
|
|
|
|
import hashlib
|
|
|
|
import random
|
2022-06-09 14:48:11 -04:00
|
|
|
import logging
|
2022-06-23 13:16:28 -04:00
|
|
|
import shutil
|
|
|
|
import glob
|
2024-05-10 17:27:59 +02:00
|
|
|
import csv
|
2022-06-09 13:56:26 -04:00
|
|
|
|
2023-06-12 18:15:31 +02:00
|
|
|
from twisterlib.testsuite import TestCase, TestSuite
|
|
|
|
from twisterlib.platform import Platform
|
2024-08-14 13:01:51 +00:00
|
|
|
from twisterlib.error import BuildError, StatusAttributeError
|
2022-06-23 17:40:57 -04:00
|
|
|
from twisterlib.size_calc import SizeCalculator
|
2024-05-28 12:31:53 +00:00
|
|
|
from twisterlib.statuses import TwisterStatus
|
2023-06-12 18:15:31 +02:00
|
|
|
from twisterlib.handlers import (
|
|
|
|
Handler,
|
|
|
|
SimulationHandler,
|
|
|
|
BinaryHandler,
|
|
|
|
QEMUHandler,
|
2024-01-04 09:53:51 +01:00
|
|
|
QEMUWinHandler,
|
2023-06-12 18:15:31 +02:00
|
|
|
DeviceHandler,
|
|
|
|
SUPPORTED_SIMS,
|
|
|
|
SUPPORTED_SIMS_IN_PYTEST,
|
|
|
|
)
|
2022-06-23 17:40:57 -04:00
|
|
|
|
2022-06-09 14:48:11 -04:00
|
|
|
logger = logging.getLogger('twister')
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
2022-06-09 13:56:26 -04:00
|
|
|
class TestInstance:
|
|
|
|
"""Class representing the execution of a particular TestSuite on a platform
|
|
|
|
|
|
|
|
@param test The TestSuite object we want to build/execute
|
|
|
|
@param platform Platform object that we want to build and run against
|
|
|
|
@param base_outdir Base directory for all test results. The actual
|
|
|
|
out directory used is <outdir>/<platform>/<test case name>
|
|
|
|
"""
|
|
|
|
|
2023-05-18 23:36:17 -06:00
|
|
|
__test__ = False
|
|
|
|
|
2022-06-09 13:56:26 -04:00
|
|
|
def __init__(self, testsuite, platform, outdir):
|
|
|
|
|
2023-06-12 18:15:31 +02:00
|
|
|
self.testsuite: TestSuite = testsuite
|
|
|
|
self.platform: Platform = platform
|
2022-06-09 13:56:26 -04:00
|
|
|
|
2024-05-28 12:31:53 +00:00
|
|
|
self._status = TwisterStatus.NONE
|
2022-06-09 13:56:26 -04:00
|
|
|
self.reason = "Unknown"
|
|
|
|
self.metrics = dict()
|
|
|
|
self.handler = None
|
2023-12-01 21:19:09 +01:00
|
|
|
self.recording = None
|
2022-06-09 13:56:26 -04:00
|
|
|
self.outdir = outdir
|
|
|
|
self.execution_time = 0
|
2023-11-22 16:30:10 -05:00
|
|
|
self.build_time = 0
|
2022-10-24 13:34:48 -04:00
|
|
|
self.retries = 0
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
self.name = os.path.join(platform.name, testsuite.name)
|
2023-08-17 16:42:42 -04:00
|
|
|
self.dut = None
|
2022-09-14 22:23:15 +02:00
|
|
|
|
2023-07-21 16:09:39 +02:00
|
|
|
if testsuite.detailed_test_id:
|
2022-09-14 22:23:15 +02:00
|
|
|
self.build_dir = os.path.join(outdir, platform.normalized_name, testsuite.name)
|
2023-07-21 16:09:39 +02:00
|
|
|
else:
|
|
|
|
# if suite is not in zephyr, keep only the part after ".." in reconstructed dir structure
|
|
|
|
source_dir_rel = testsuite.source_dir_rel.rsplit(os.pardir+os.path.sep, 1)[-1]
|
2022-09-14 22:23:15 +02:00
|
|
|
self.build_dir = os.path.join(outdir, platform.normalized_name, source_dir_rel, testsuite.name)
|
2023-12-06 13:55:18 +01:00
|
|
|
self.run_id = self._get_run_id()
|
2023-04-11 14:41:01 -05:00
|
|
|
self.domains = None
|
2024-06-05 12:52:40 +02:00
|
|
|
# Instance need to use sysbuild if a given suite or a platform requires it
|
|
|
|
self.sysbuild = testsuite.sysbuild or platform.sysbuild
|
2023-04-11 14:41:01 -05:00
|
|
|
|
2022-06-09 13:56:26 -04:00
|
|
|
self.run = False
|
2023-06-12 18:15:31 +02:00
|
|
|
self.testcases: list[TestCase] = []
|
2022-06-09 13:56:26 -04:00
|
|
|
self.init_cases()
|
|
|
|
self.filters = []
|
|
|
|
self.filter_type = None
|
|
|
|
|
2024-05-10 17:27:59 +02:00
|
|
|
def record(self, recording, fname_csv="recording.csv"):
|
|
|
|
if recording:
|
|
|
|
if self.recording is None:
|
|
|
|
self.recording = recording.copy()
|
|
|
|
else:
|
|
|
|
self.recording.extend(recording)
|
|
|
|
|
|
|
|
filename = os.path.join(self.build_dir, fname_csv)
|
|
|
|
with open(filename, "wt") as csvfile:
|
|
|
|
cw = csv.DictWriter(csvfile,
|
|
|
|
fieldnames = self.recording[0].keys(),
|
|
|
|
lineterminator = os.linesep,
|
|
|
|
quoting = csv.QUOTE_NONNUMERIC)
|
|
|
|
cw.writeheader()
|
|
|
|
cw.writerows(self.recording)
|
|
|
|
|
2024-05-28 12:31:53 +00:00
|
|
|
@property
|
|
|
|
def status(self) -> TwisterStatus:
|
|
|
|
return self._status
|
|
|
|
|
|
|
|
@status.setter
|
|
|
|
def status(self, value : TwisterStatus) -> None:
|
|
|
|
# Check for illegal assignments by value
|
|
|
|
try:
|
|
|
|
key = value.name if isinstance(value, Enum) else value
|
|
|
|
self._status = TwisterStatus[key]
|
|
|
|
except KeyError:
|
2024-08-14 13:01:51 +00:00
|
|
|
raise StatusAttributeError(self.__class__, value)
|
2024-05-28 12:31:53 +00:00
|
|
|
|
2022-06-09 13:56:26 -04:00
|
|
|
def add_filter(self, reason, filter_type):
|
|
|
|
self.filters.append({'type': filter_type, 'reason': reason })
|
2024-05-28 12:31:53 +00:00
|
|
|
self.status = TwisterStatus.FILTER
|
2022-06-09 13:56:26 -04:00
|
|
|
self.reason = reason
|
|
|
|
self.filter_type = filter_type
|
|
|
|
|
|
|
|
# Fix an issue with copying objects from testsuite, need better solution.
|
|
|
|
def init_cases(self):
|
|
|
|
for c in self.testsuite.testcases:
|
|
|
|
self.add_testcase(c.name, freeform=c.freeform)
|
|
|
|
|
|
|
|
def _get_run_id(self):
|
|
|
|
""" generate run id from instance unique identifier and a random
|
2023-12-06 13:55:18 +01:00
|
|
|
number
|
|
|
|
If exist, get cached run id from previous run."""
|
|
|
|
run_id = ""
|
|
|
|
run_id_file = os.path.join(self.build_dir, "run_id.txt")
|
|
|
|
if os.path.exists(run_id_file):
|
|
|
|
with open(run_id_file, "r") as fp:
|
|
|
|
run_id = fp.read()
|
|
|
|
else:
|
|
|
|
hash_object = hashlib.md5(self.name.encode())
|
|
|
|
random_str = f"{random.getrandbits(64)}".encode()
|
|
|
|
hash_object.update(random_str)
|
|
|
|
run_id = hash_object.hexdigest()
|
|
|
|
os.makedirs(self.build_dir, exist_ok=True)
|
|
|
|
with open(run_id_file, 'w+') as fp:
|
|
|
|
fp.write(run_id)
|
|
|
|
return run_id
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
def add_missing_case_status(self, status, reason=None):
|
|
|
|
for case in self.testcases:
|
2024-05-28 12:31:53 +00:00
|
|
|
if case.status == TwisterStatus.STARTED:
|
|
|
|
case.status = TwisterStatus.FAIL
|
|
|
|
elif case.status == TwisterStatus.NONE:
|
2022-06-09 13:56:26 -04:00
|
|
|
case.status = status
|
|
|
|
if reason:
|
|
|
|
case.reason = reason
|
|
|
|
else:
|
|
|
|
case.reason = self.reason
|
|
|
|
|
|
|
|
def __getstate__(self):
|
|
|
|
d = self.__dict__.copy()
|
|
|
|
return d
|
|
|
|
|
|
|
|
def __setstate__(self, d):
|
|
|
|
self.__dict__.update(d)
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
return self.name < other.name
|
|
|
|
|
|
|
|
def set_case_status_by_name(self, name, status, reason=None):
|
|
|
|
tc = self.get_case_or_create(name)
|
|
|
|
tc.status = status
|
|
|
|
if reason:
|
|
|
|
tc.reason = reason
|
|
|
|
return tc
|
|
|
|
|
|
|
|
def add_testcase(self, name, freeform=False):
|
|
|
|
tc = TestCase(name=name)
|
|
|
|
tc.freeform = freeform
|
|
|
|
self.testcases.append(tc)
|
|
|
|
return tc
|
|
|
|
|
|
|
|
def get_case_by_name(self, name):
|
|
|
|
for c in self.testcases:
|
|
|
|
if c.name == name:
|
|
|
|
return c
|
|
|
|
return None
|
|
|
|
|
|
|
|
def get_case_or_create(self, name):
|
|
|
|
for c in self.testcases:
|
|
|
|
if c.name == name:
|
|
|
|
return c
|
|
|
|
|
|
|
|
logger.debug(f"Could not find a matching testcase for {name}")
|
|
|
|
tc = TestCase(name=name)
|
|
|
|
self.testcases.append(tc)
|
|
|
|
return tc
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def testsuite_runnable(testsuite, fixtures):
|
|
|
|
can_run = False
|
|
|
|
# console harness allows us to run the test and capture data.
|
2023-04-05 15:21:44 +02:00
|
|
|
if testsuite.harness in [ 'console', 'ztest', 'pytest', 'test', 'gtest', 'robot']:
|
2022-06-09 13:56:26 -04:00
|
|
|
can_run = True
|
|
|
|
# if we have a fixture that is also being supplied on the
|
|
|
|
# command-line, then we need to run the test, not just build it.
|
|
|
|
fixture = testsuite.harness_config.get('fixture')
|
|
|
|
if fixture:
|
2024-05-29 18:10:32 +00:00
|
|
|
can_run = fixture in map(lambda f: f.split(sep=':')[0], fixtures)
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
return can_run
|
|
|
|
|
2022-06-10 10:51:01 -04:00
|
|
|
def setup_handler(self, env):
|
2022-06-09 14:48:11 -04:00
|
|
|
if self.handler:
|
|
|
|
return
|
|
|
|
|
2022-06-10 10:51:01 -04:00
|
|
|
options = env.options
|
2022-11-21 14:04:11 -05:00
|
|
|
handler = Handler(self, "")
|
2023-01-17 15:27:42 +00:00
|
|
|
if options.device_testing:
|
|
|
|
handler = DeviceHandler(self, "device")
|
|
|
|
handler.call_make_run = False
|
|
|
|
handler.ready = True
|
|
|
|
elif self.platform.simulation != "na":
|
2022-11-21 14:04:11 -05:00
|
|
|
if self.platform.simulation == "qemu":
|
2024-01-04 09:53:51 +01:00
|
|
|
if os.name != "nt":
|
|
|
|
handler = QEMUHandler(self, "qemu")
|
|
|
|
else:
|
|
|
|
handler = QEMUWinHandler(self, "qemu")
|
2024-03-11 12:13:12 +01:00
|
|
|
handler.args.append(f"QEMU_PIPE={handler.get_fifo()}")
|
|
|
|
handler.ready = True
|
2022-11-21 14:04:11 -05:00
|
|
|
else:
|
|
|
|
handler = SimulationHandler(self, self.platform.simulation)
|
|
|
|
|
|
|
|
if self.platform.simulation_exec and shutil.which(self.platform.simulation_exec):
|
|
|
|
handler.ready = True
|
2022-06-09 14:48:11 -04:00
|
|
|
elif self.testsuite.type == "unit":
|
|
|
|
handler = BinaryHandler(self, "unit")
|
|
|
|
handler.binary = os.path.join(self.build_dir, "testbinary")
|
|
|
|
if options.enable_coverage:
|
2022-11-21 14:04:11 -05:00
|
|
|
handler.args.append("COVERAGE=1")
|
2022-06-09 14:48:11 -04:00
|
|
|
handler.call_make_run = False
|
2022-11-21 14:04:11 -05:00
|
|
|
handler.ready = True
|
2022-06-09 14:48:11 -04:00
|
|
|
|
|
|
|
if handler:
|
2022-06-10 10:51:01 -04:00
|
|
|
handler.options = options
|
2022-06-13 09:16:32 -04:00
|
|
|
handler.generator_cmd = env.generator_cmd
|
2022-06-09 14:48:11 -04:00
|
|
|
handler.suite_name_check = not options.disable_suite_name_check
|
|
|
|
self.handler = handler
|
|
|
|
|
2022-06-09 13:56:26 -04:00
|
|
|
# Global testsuite parameters
|
2023-09-21 11:39:30 +02:00
|
|
|
def check_runnable(self, enable_slow=False, filter='buildable', fixtures=[], hardware_map=None):
|
2022-06-09 13:56:26 -04:00
|
|
|
|
2024-01-04 09:53:51 +01:00
|
|
|
if os.name == 'nt':
|
|
|
|
# running on simulators is currently supported only for QEMU on Windows
|
|
|
|
if self.platform.simulation not in ('na', 'qemu'):
|
|
|
|
return False
|
|
|
|
|
|
|
|
# check presence of QEMU on Windows
|
2024-03-14 22:12:33 -07:00
|
|
|
if self.platform.simulation == 'qemu' and 'QEMU_BIN_PATH' not in os.environ:
|
2024-01-04 09:53:51 +01:00
|
|
|
return False
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
# we asked for build-only on the command line
|
|
|
|
if self.testsuite.build_only:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Do not run slow tests:
|
|
|
|
skip_slow = self.testsuite.slow and not enable_slow
|
|
|
|
if skip_slow:
|
|
|
|
return False
|
|
|
|
|
|
|
|
target_ready = bool(self.testsuite.type == "unit" or \
|
|
|
|
self.platform.type == "native" or \
|
2023-12-12 15:40:49 +08:00
|
|
|
(self.platform.simulation in SUPPORTED_SIMS and \
|
|
|
|
self.platform.simulation not in self.testsuite.simulation_exclude) or \
|
2022-06-09 13:56:26 -04:00
|
|
|
filter == 'runnable')
|
|
|
|
|
2023-05-26 11:43:36 +02:00
|
|
|
# check if test is runnable in pytest
|
|
|
|
if self.testsuite.harness == 'pytest':
|
|
|
|
target_ready = bool(filter == 'runnable' or self.platform.simulation in SUPPORTED_SIMS_IN_PYTEST)
|
2022-06-09 13:56:26 -04:00
|
|
|
|
2023-05-26 11:43:36 +02:00
|
|
|
SUPPORTED_SIMS_WITH_EXEC = ['nsim', 'mdb-nsim', 'renode', 'tsim', 'native']
|
|
|
|
if filter != 'runnable' and \
|
|
|
|
self.platform.simulation in SUPPORTED_SIMS_WITH_EXEC and \
|
|
|
|
self.platform.simulation_exec:
|
|
|
|
if not shutil.which(self.platform.simulation_exec):
|
|
|
|
target_ready = False
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
testsuite_runnable = self.testsuite_runnable(self.testsuite, fixtures)
|
|
|
|
|
2023-09-21 11:39:30 +02:00
|
|
|
if hardware_map:
|
|
|
|
for h in hardware_map.duts:
|
|
|
|
if (h.platform == self.platform.name and
|
|
|
|
self.testsuite_runnable(self.testsuite, h.fixtures)):
|
|
|
|
testsuite_runnable = True
|
|
|
|
break
|
|
|
|
|
2022-06-09 13:56:26 -04:00
|
|
|
return testsuite_runnable and target_ready
|
|
|
|
|
|
|
|
def create_overlay(self, platform, enable_asan=False, enable_ubsan=False, enable_coverage=False, coverage_platform=[]):
|
|
|
|
# Create this in a "twister/" subdirectory otherwise this
|
|
|
|
# will pass this overlay to kconfig.py *twice* and kconfig.cmake
|
|
|
|
# will silently give that second time precedence over any
|
|
|
|
# --extra-args=CONFIG_*
|
|
|
|
subdir = os.path.join(self.build_dir, "twister")
|
|
|
|
|
|
|
|
content = ""
|
|
|
|
|
|
|
|
if self.testsuite.extra_configs:
|
2023-01-11 22:25:08 +00:00
|
|
|
new_config_list = []
|
|
|
|
# some configs might be conditional on arch or platform, see if we
|
|
|
|
# have a namespace defined and apply only if the namespace matches.
|
|
|
|
# we currently support both arch: and platform:
|
|
|
|
for config in self.testsuite.extra_configs:
|
|
|
|
cond_config = config.split(":")
|
|
|
|
if cond_config[0] == "arch" and len(cond_config) == 3:
|
|
|
|
if self.platform.arch == cond_config[1]:
|
|
|
|
new_config_list.append(cond_config[2])
|
2023-05-29 11:33:03 +02:00
|
|
|
elif cond_config[0] == "platform" and len(cond_config) == 3:
|
2023-01-11 22:25:08 +00:00
|
|
|
if self.platform.name == cond_config[1]:
|
|
|
|
new_config_list.append(cond_config[2])
|
|
|
|
else:
|
|
|
|
new_config_list.append(config)
|
|
|
|
|
|
|
|
content = "\n".join(new_config_list)
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
if enable_coverage:
|
|
|
|
if platform.name in coverage_platform:
|
|
|
|
content = content + "\nCONFIG_COVERAGE=y"
|
|
|
|
content = content + "\nCONFIG_COVERAGE_DUMP=y"
|
|
|
|
|
|
|
|
if enable_asan:
|
|
|
|
if platform.type == "native":
|
|
|
|
content = content + "\nCONFIG_ASAN=y"
|
|
|
|
|
|
|
|
if enable_ubsan:
|
|
|
|
if platform.type == "native":
|
|
|
|
content = content + "\nCONFIG_UBSAN=y"
|
|
|
|
|
|
|
|
if content:
|
|
|
|
os.makedirs(subdir, exist_ok=True)
|
|
|
|
file = os.path.join(subdir, "testsuite_extra.conf")
|
tests: testinstance: enforce utf-8 write
as in windows platfrom the format by default is not utf-8,
and we will see below error
Traceback (most recent call last):
File "...zephyr\scripts\twister", line 211, in <module>
ret = main(options)
^^^^^^^^^^^^^
File "...scripts/pylib/twister\twisterlib\twister_main.py",
tplan.load()
File "...scripts/pylib/twister\twisterlib\testplan.py",
self.load_from_file(last_run, filter_platform=connected_list)
File "...scripts/pylib/twister\twisterlib\testplan.py",
instance.create_overlay(platform, self.options.enable_asan,
self.options.enable_ubsan, self.options.enable_coverage,
self.options.coverage_platform)
File "...scripts/pylib/twister\twisterlib\testinstance.py"
f.write(content)
UnicodeEncodeError: 'gbk' codec can't encode character '\xf8'
in position 64: illegal multibyte sequence
Signed-off-by: Hake Huang <hake.huang@oss.nxp.com>
2023-12-06 17:20:03 +08:00
|
|
|
with open(file, "w", encoding='utf-8') as f:
|
2022-06-09 13:56:26 -04:00
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
2022-10-18 15:45:21 +02:00
|
|
|
def calculate_sizes(self, from_buildlog: bool = False, generate_warning: bool = True) -> SizeCalculator:
|
2022-06-09 13:56:26 -04:00
|
|
|
"""Get the RAM/ROM sizes of a test case.
|
|
|
|
|
|
|
|
This can only be run after the instance has been executed by
|
|
|
|
MakeGenerator, otherwise there won't be any binaries to measure.
|
|
|
|
|
|
|
|
@return A SizeCalculator object
|
|
|
|
"""
|
2022-10-18 15:45:21 +02:00
|
|
|
elf_filepath = self.get_elf_file()
|
|
|
|
buildlog_filepath = self.get_buildlog_file() if from_buildlog else ''
|
|
|
|
return SizeCalculator(elf_filename=elf_filepath,
|
2022-12-01 02:00:26 +00:00
|
|
|
extra_sections=self.testsuite.extra_sections,
|
|
|
|
buildlog_filepath=buildlog_filepath,
|
|
|
|
generate_warning=generate_warning)
|
2022-10-18 15:45:21 +02:00
|
|
|
|
|
|
|
def get_elf_file(self) -> str:
|
2023-04-11 14:41:01 -05:00
|
|
|
|
2024-06-05 12:52:40 +02:00
|
|
|
if self.sysbuild:
|
2023-04-11 14:41:01 -05:00
|
|
|
build_dir = self.domains.get_default_domain().build_dir
|
|
|
|
else:
|
|
|
|
build_dir = self.build_dir
|
|
|
|
|
|
|
|
fns = glob.glob(os.path.join(build_dir, "zephyr", "*.elf"))
|
2023-04-13 15:57:34 -06:00
|
|
|
fns.extend(glob.glob(os.path.join(build_dir, "testbinary")))
|
2023-04-11 01:27:56 +00:00
|
|
|
blocklist = [
|
|
|
|
'remapped', # used for xtensa plaforms
|
|
|
|
'zefi', # EFI for Zephyr
|
2023-07-27 12:41:07 +00:00
|
|
|
'qemu', # elf files generated after running in qemu
|
|
|
|
'_pre']
|
2023-04-13 15:57:34 -06:00
|
|
|
fns = [x for x in fns if not any(bad in os.path.basename(x) for bad in blocklist)]
|
2023-07-27 12:41:07 +00:00
|
|
|
if not fns:
|
|
|
|
raise BuildError("Missing output binary")
|
|
|
|
elif len(fns) > 1:
|
|
|
|
logger.warning(f"multiple ELF files detected: {', '.join(fns)}")
|
2022-10-18 15:45:21 +02:00
|
|
|
return fns[0]
|
|
|
|
|
|
|
|
def get_buildlog_file(self) -> str:
|
|
|
|
"""Get path to build.log file.
|
2022-06-09 13:56:26 -04:00
|
|
|
|
2022-10-18 15:45:21 +02:00
|
|
|
@raises BuildError: Incorrect amount (!=1) of build logs.
|
|
|
|
@return: Path to build.log (str).
|
|
|
|
"""
|
|
|
|
buildlog_paths = glob.glob(os.path.join(self.build_dir, "build.log"))
|
|
|
|
if len(buildlog_paths) != 1:
|
|
|
|
raise BuildError("Missing/multiple build.log file.")
|
|
|
|
return buildlog_paths[0]
|
2022-06-09 13:56:26 -04:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "<TestSuite %s on %s>" % (self.testsuite.name, self.platform.name)
|