Skip to content

Commit 191ff4e

Browse files
committed
WIP: Add support for repeating tests with different seeds.
1 parent ce51561 commit 191ff4e

File tree

2 files changed

+133
-18
lines changed

2 files changed

+133
-18
lines changed

vunit/ui/__init__.py

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
import logging
1717
import json
1818
import os
19+
from datetime import datetime
1920
from typing import Optional, Set, Union
2021
from pathlib import Path
2122
from fnmatch import fnmatch
23+
from math import log10, ceil
2224

2325
from ..database import PickledDataBase, DataBase
2426
from .. import ostools
@@ -35,7 +37,7 @@
3537
from ..builtins import Builtins
3638
from ..vhdl_standard import VHDL, VHDLStandard
3739
from ..test.bench_list import TestBenchList
38-
from ..test.report import TestReport
40+
from ..test.report import TestReport, get_parsed_time
3941
from ..test.runner import TestRunner
4042

4143
from .common import LOGGER, TEST_OUTPUT_PATH, select_vhdl_standard, check_not_empty
@@ -749,7 +751,8 @@ def _main(self, post_run):
749751
if self._args.compile:
750752
return self._main_compile_only()
751753

752-
all_ok = self._main_run(post_run)
754+
all_ok = self._main_run(post_run, self._args.repeat)
755+
753756
return all_ok
754757

755758
def _create_simulator_if(self):
@@ -770,7 +773,47 @@ def _create_simulator_if(self):
770773

771774
return self._simulator_class.from_args(args=self._args, output_path=self._simulator_output_path)
772775

773-
def _main_run(self, post_run):
776+
def _print_repetition_report(self, iteration_status, iteration_duration, total_duration):
777+
n_iterations = len(iteration_status)
778+
n_passed = sum(iteration_status)
779+
n_failed = n_iterations - n_passed
780+
781+
parsed_duration = [get_parsed_time(duration) for duration in iteration_duration]
782+
max_parsed_duration = max(len(duration) for duration in parsed_duration)
783+
784+
first_iteration_name = "Initial test suite"
785+
max_name_length = max(len(first_iteration_name), len("Repetition ") + 1 + int(ceil(log10(n_iterations - 1))))
786+
max_length = 8 + max_name_length + max_parsed_duration
787+
header_prefix = "==== Repetitions Summary "
788+
789+
self._printer.write(f"\n{header_prefix}")
790+
self._printer.write("=" * (max_length - len(header_prefix)) + "\n")
791+
792+
for iteration, (status, duration) in enumerate(zip(iteration_status, iteration_duration)):
793+
iteration_name = f"Repetition {iteration}" if iteration > 0 else first_iteration_name
794+
if not status:
795+
self._printer.write("fail", fg="ri")
796+
else:
797+
self._printer.write("pass", fg="gi")
798+
self._printer.write(f" {iteration_name} ")
799+
self._printer.write(" " * (max_name_length - len(iteration_name)))
800+
self._printer.write(f"({get_parsed_time(duration)})\n")
801+
802+
self._printer.write("=" * max_length + "\n")
803+
self._printer.write("pass", fg="gi")
804+
self._printer.write(f" {n_passed} of {n_iterations}\n")
805+
if n_failed > 0:
806+
self._printer.write("fail", fg="ri")
807+
self._printer.write(f" {n_failed} of {n_iterations}\n")
808+
self._printer.write("=" * max_length + "\n")
809+
self._printer.write(f"Total time was {get_parsed_time(total_duration)}\n")
810+
self._printer.write("=" * max_length + "\n")
811+
if n_failed > 0:
812+
self._printer.write("Some failed!\n", fg="ri")
813+
else:
814+
self._printer.write("All passed!\n", fg="gi")
815+
816+
def _main_run(self, post_run, iteration_limit):
774817
"""
775818
Main with running tests
776819
"""
@@ -779,30 +822,60 @@ def _main_run(self, post_run):
779822
self._compile(simulator_if)
780823
print()
781824

825+
iteration = 1
826+
ctrl_c = False
782827
start_time = ostools.get_time()
783-
report = TestReport(printer=self._printer)
828+
iteration_status = []
829+
iteration_duration = []
784830

785-
try:
786-
self._run_test(test_list, report)
787-
except KeyboardInterrupt:
788-
print()
789-
LOGGER.debug("_main: Caught Ctrl-C shutting down")
790-
finally:
791-
del test_list
831+
def keep_running():
832+
if ctrl_c:
833+
return False
834+
835+
if isinstance(iteration_limit, int):
836+
return iteration <= iteration_limit
837+
838+
return ostools.get_time() < start_time + iteration_limit.total_seconds()
792839

793-
report.set_real_total_time(ostools.get_time() - start_time)
794-
report.print_str()
840+
while keep_running():
841+
iteration_start_time = ostools.get_time()
842+
if iteration > 1:
843+
now = datetime.now().strftime("%H:%M:%S")
844+
print(f"\n({now}) Starting repetition {iteration - 1}\n")
795845

796-
if post_run is not None:
797-
post_run(results=Results(self._output_path, simulator_if, report))
846+
report = TestReport(printer=self._printer)
798847

848+
try:
849+
self._run_test(test_list, report, iteration)
850+
except KeyboardInterrupt:
851+
ctrl_c = True
852+
print()
853+
LOGGER.debug("_main: Caught Ctrl-C shutting down")
854+
finally:
855+
if sys.exc_info()[0] is not None:
856+
del test_list
857+
858+
iteration_duration.append(ostools.get_time() - iteration_start_time)
859+
report.set_real_total_time(iteration_duration[-1])
860+
report.print_str()
861+
862+
if post_run is not None:
863+
post_run(results=Results(self._output_path, simulator_if, report))
864+
865+
iteration_status.append(report.all_ok())
866+
iteration += 1
867+
868+
if iteration > 1:
869+
self._print_repetition_report(iteration_status, iteration_duration, ostools.get_time() - start_time)
870+
871+
del test_list
799872
del simulator_if
800873

801874
if self._args.xunit_xml is not None:
802875
xml = report.to_junit_xml_str(self._args.xunit_xml_format)
803876
ostools.write_file(self._args.xunit_xml, xml)
804877

805-
return report.all_ok()
878+
return sum(iteration_status) == len(iteration_status)
806879

807880
def _main_list_only(self):
808881
"""
@@ -938,7 +1011,7 @@ def _get_testbench_files(self, simulator_if: Union[None, SimulatorInterface]):
9381011
for file_name in tb_file_names
9391012
]
9401013

941-
def _run_test(self, test_cases, report):
1014+
def _run_test(self, test_cases, report, iteration):
9421015
"""
9431016
Run the test suites and return the report
9441017
"""
@@ -950,9 +1023,14 @@ def _run_test(self, test_cases, report):
9501023
else:
9511024
verbosity = TestRunner.VERBOSITY_NORMAL
9521025

1026+
full_test_output_path = Path(self._output_path) / TEST_OUTPUT_PATH
1027+
if iteration > 1:
1028+
full_test_output_path /= f"rep_{iteration - 1}"
1029+
full_test_output_path = str(full_test_output_path)
1030+
9531031
runner = TestRunner(
9541032
report,
955-
str(Path(self._output_path) / TEST_OUTPUT_PATH),
1033+
full_test_output_path,
9561034
verbosity=verbosity,
9571035
num_threads=self._args.num_threads,
9581036
fail_fast=self._args.fail_fast,

vunit/vunit_cli.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import argparse
3838
from pathlib import Path
3939
import os
40+
import re
41+
from datetime import timedelta
4042
from vunit.sim_if.factory import SIMULATOR_FACTORY
4143
from vunit.about import version
4244

@@ -252,6 +254,18 @@ def _create_argument_parser(description=None, for_documentation=False):
252254
help="Base seed provided to the simulation. Must be 16 hex digits. Default seed is generated from system time.",
253255
)
254256

257+
parser.add_argument(
258+
"-r",
259+
"--repeat",
260+
type=integer_or_duration,
261+
default="0",
262+
help="""Repeat test suite REPEAT times if REPEAT is a non-negative integer.
263+
If REPEAT is on the duration format [wd][xh][ym][zs], the tests suite is repeated for the specified duration.
264+
w, x, y, and z corresponds to the number of days, hours, minutes, and seconds.
265+
Each field is optional, but at least one field must be provided.
266+
For example, 1h30m will repeat the test suite until the end of a test suite repetion exceeds 1 hour and 30 minutes.
267+
The default is no repetitions."""
268+
)
255269
SIMULATOR_FACTORY.add_arguments(parser)
256270

257271
return parser
@@ -269,6 +283,29 @@ def nonnegative_int(val):
269283
raise argparse.ArgumentTypeError(f"'{val!s}' is not a valid non-negative int") from exv
270284

271285

286+
def integer_or_duration(value):
287+
"""Parse value for repeat option and return number of iterations or a time delta."""
288+
289+
if value is None:
290+
return 1
291+
292+
if value.isdigit():
293+
return int(value) + 1
294+
295+
duration_re = re.compile(r"(?P<days>\d+d)?(?P<hours>\d+h)?(?P<minutes>\d+m)?(?P<seconds>\d+s)?$", re.IGNORECASE)
296+
duration_match = duration_re.match(value)
297+
298+
if not duration_match or not duration_match.group():
299+
raise argparse.ArgumentTypeError(f"'{value}' is not a valid number of repetitions, nor a duration.")
300+
301+
return timedelta(
302+
days=int(duration_match.group("days")[:-1]) if duration_match.group("days") else 0,
303+
hours=int(duration_match.group("hours")[:-1]) if duration_match.group("hours") else 0,
304+
minutes=int(duration_match.group("minutes")[:-1]) if duration_match.group("minutes") else 0,
305+
seconds=int(duration_match.group("seconds")[:-1]) if duration_match.group("seconds") else 0,
306+
)
307+
308+
272309
def _parser_for_documentation():
273310
"""
274311
Returns an argparse object used by sphinx for documentation in user_guide.rst

0 commit comments

Comments
 (0)