Skip to content

Commit aa99458

Browse files
committed
WIP: Added repeat for duration
1 parent d3fd1fc commit aa99458

File tree

2 files changed

+96
-13
lines changed

2 files changed

+96
-13
lines changed

vunit/ui/__init__.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
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
@@ -35,7 +36,7 @@
3536
from ..builtins import Builtins
3637
from ..vhdl_standard import VHDL, VHDLStandard
3738
from ..test.bench_list import TestBenchList
38-
from ..test.report import TestReport
39+
from ..test.report import TestReport, get_parsed_time
3940
from ..test.runner import TestRunner
4041

4142
from .common import LOGGER, TEST_OUTPUT_PATH, select_vhdl_standard, check_not_empty
@@ -749,7 +750,7 @@ def _main(self, post_run):
749750
if self._args.compile:
750751
return self._main_compile_only()
751752

752-
all_ok = self._main_run(post_run, n_iterations=self._args.repeat + 1)
753+
all_ok = self._main_run(post_run, self._args.repeat)
753754

754755
return all_ok
755756

@@ -771,7 +772,36 @@ def _create_simulator_if(self):
771772

772773
return self._simulator_class.from_args(args=self._args, output_path=self._simulator_output_path)
773774

774-
def _main_run(self, post_run, n_iterations):
775+
def _print_repetition_report(self, iteration_status, duration):
776+
n_iterations = len(iteration_status)
777+
n_passed = sum(iteration_status)
778+
n_failed = n_iterations - n_passed
779+
780+
781+
self._printer.write("\n==== Repetitions Summary ====\n")
782+
for iteration, status in enumerate(iteration_status):
783+
iteration_name = f"Repetition {iteration}" if iteration > 0 else "Initial test suite"
784+
if not status:
785+
self._printer.write("fail", fg="ri")
786+
else:
787+
self._printer.write("pass", fg="gi")
788+
self._printer.write(f" {iteration_name}\n")
789+
790+
self._printer.write("=============================\n")
791+
self._printer.write("pass", fg="gi")
792+
self._printer.write(f" {n_passed} of {n_iterations}\n")
793+
if n_failed > 0:
794+
self._printer.write("fail", fg="ri")
795+
self._printer.write(f" {n_failed} of {n_iterations}\n")
796+
self._printer.write("=============================\n")
797+
self._printer.write(f"Total time was {get_parsed_time(duration)}\n")
798+
self._printer.write("=============================\n")
799+
if n_failed > 0:
800+
self._printer.write("Some failed!\n", fg="ri")
801+
else:
802+
self._printer.write("All passed!\n", fg="gi")
803+
804+
def _main_run(self, post_run, iteration_limit):
775805
"""
776806
Main with running tests
777807
"""
@@ -780,35 +810,58 @@ def _main_run(self, post_run, n_iterations):
780810
self._compile(simulator_if)
781811
print()
782812

783-
all_ok = True
784-
for iteration in range(1, n_iterations + 1):
785-
start_time = ostools.get_time()
813+
iteration = 1
814+
ctrl_c = False
815+
start_time = ostools.get_time()
816+
iteration_status = []
817+
818+
def keep_running():
819+
if ctrl_c:
820+
return False
821+
822+
if isinstance(iteration_limit, int):
823+
return iteration <= iteration_limit
824+
825+
return ostools.get_time() < start_time + iteration_limit.total_seconds()
826+
827+
while keep_running():
828+
iteration_start_time = ostools.get_time()
829+
if iteration > 1:
830+
now = datetime.now().strftime("%H:%M:%S")
831+
print(f"\n({now}) Starting repetition {iteration - 1}\n")
832+
786833
report = TestReport(printer=self._printer)
787834

788835
try:
789836
self._run_test(test_list, report, iteration)
790837
except KeyboardInterrupt:
838+
ctrl_c = True
791839
print()
792840
LOGGER.debug("_main: Caught Ctrl-C shutting down")
793841
finally:
794-
if (sys.exc_info()[0] is not None) or (iteration == n_iterations):
842+
if sys.exc_info()[0] is not None:
795843
del test_list
796844

797-
report.set_real_total_time(ostools.get_time() - start_time)
845+
report.set_real_total_time(ostools.get_time() - iteration_start_time)
798846
report.print_str()
799847

800848
if post_run is not None:
801849
post_run(results=Results(self._output_path, simulator_if, report))
802850

803-
all_ok &= report.all_ok()
851+
iteration_status.append(report.all_ok())
852+
iteration += 1
853+
854+
if iteration > 1:
855+
self._print_repetition_report(iteration_status, ostools.get_time() - start_time)
804856

857+
del test_list
805858
del simulator_if
806859

807860
if self._args.xunit_xml is not None:
808861
xml = report.to_junit_xml_str(self._args.xunit_xml_format)
809862
ostools.write_file(self._args.xunit_xml, xml)
810863

811-
return all_ok
864+
return sum(iteration_status) == len(iteration_status)
812865

813866
def _main_list_only(self):
814867
"""

vunit/vunit_cli.py

Lines changed: 33 additions & 3 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

@@ -255,9 +257,14 @@ def _create_argument_parser(description=None, for_documentation=False):
255257
parser.add_argument(
256258
"-r",
257259
"--repeat",
258-
type=nonnegative_int,
259-
default=0,
260-
help="Number of times the tests are repeated with new seed values (unless --seed is set). Default: 0. ",
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."""
261268
)
262269
SIMULATOR_FACTORY.add_arguments(parser)
263270

@@ -276,6 +283,29 @@ def nonnegative_int(val):
276283
raise argparse.ArgumentTypeError(f"'{val!s}' is not a valid non-negative int") from exv
277284

278285

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+
279309
def _parser_for_documentation():
280310
"""
281311
Returns an argparse object used by sphinx for documentation in user_guide.rst

0 commit comments

Comments
 (0)