From fff983d089ef081ba9585c27cf2baaa2fe1fc988 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 20 Jul 2022 10:50:41 -0400 Subject: [PATCH 1/2] added --durations option --- testflo/duration.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ testflo/main.py | 7 +++++++ testflo/util.py | 15 ++++++++++++--- 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 testflo/duration.py diff --git a/testflo/duration.py b/testflo/duration.py new file mode 100644 index 0000000..2574107 --- /dev/null +++ b/testflo/duration.py @@ -0,0 +1,46 @@ +import sys +import os + +class DurationSummary(object): + """Writes a summary of the tests taking the longest time.""" + + def __init__(self, options, stream=sys.stdout): + self.stream = stream + self.options = options + self.startdir = os.getcwd() + + def get_iter(self, input_iter): + durations = [] + + for test in input_iter: + durations.append((test.spec, test.end_time - test.start_time)) + yield test + + write = self.stream.write + mintime = self.options.durations_min + + if mintime > 0.: + title = " Max duration tests with duration >= {} sec ".format(mintime) + else: + title = " Max duration tests " + + prefix = "\n\n" + "=" * 30 + title + "=" * 30 + "\n\n" + suffix = "\n" + "=" * len(prefix) + "\n" + + write(prefix) + count = self.options.durations + + for spec, duration in sorted(durations, key=lambda t: t[1], reverse=True): + if duration < mintime: + break + + if spec.startswith(self.startdir): + spec = spec[len(self.startdir):] + + write("{:8.3f} sec - {}\n".format(duration, spec)) + + count -= 1 + if count <= 0: + break + + write(suffix) diff --git a/testflo/main.py b/testflo/main.py index 850a0b0..01d559e 100644 --- a/testflo/main.py +++ b/testflo/main.py @@ -37,6 +37,7 @@ def get_iter(self, input_iter) from testflo.printer import ResultPrinter from testflo.benchmark import BenchmarkWriter from testflo.summary import ResultSummary +from testflo.duration import DurationSummary from testflo.discover import TestDiscoverer from testflo.filters import TimeFilter, FailFilter @@ -206,6 +207,9 @@ def func_matcher(funcname): if options.benchmark: pipeline.append(BenchmarkWriter(stream=bdata).get_iter) + if options.durations: + pipeline.append(DurationSummary(options).get_iter) + if options.compact: verbose = -1 else: @@ -217,6 +221,9 @@ def func_matcher(funcname): ]) if not options.noreport: # print verbose results and summary to a report file + if options.durations: + pipeline.append(DurationSummary(options, stream=report).get_iter) + pipeline.extend([ ResultPrinter(options, report, verbose=1).get_iter, ResultSummary(options, stream=report).get_iter, diff --git a/testflo/util.py b/testflo/util.py index d2cb255..6d2d5e3 100644 --- a/testflo/util.py +++ b/testflo/util.py @@ -95,6 +95,14 @@ def _get_parser(): metavar='FILE', default='benchmark_data.csv', help='Name of benchmark data file. Default is benchmark_data.csv.') + parser.add_argument('--durations', action='store', type=int, dest='durations', default=0, + metavar='NUM', + help="Display 'NUM' tests with longest durations.") + + parser.add_argument('--durations-min', action='store', type=float, dest='durations_min', + default=0.005, metavar='MIN_TIME', + help='Specify the minimum duration test to include in the durations list.') + parser.add_argument('--noreport', action='store_true', dest='noreport', help="Don't create a test results file.") @@ -116,9 +124,10 @@ def _get_parser(): parser.add_argument('--exclude', action='append', dest='excludes', metavar='GLOB', default=[], help="Pattern to exclude test functions. Multiple patterns are allowed.") - parser.add_argument('--timeout', action='store', dest='timeout', type=float, - help='Timeout in seconds. Test will be terminated if it takes longer than timeout. Only' - ' works for tests running in a subprocess (MPI and isolated).') + parser.add_argument('--timeout', action='store', dest='timeout', type=float, metavar='TIME_LIMIT', + help="Timeout in seconds. A test will be terminated if it takes longer than " + "'TIME_LIMIT'. Only works for tests running in a subprocess " + "(MPI or isolated).") return parser From 62ccb099d9c07dfcfe62a9ef9d38959af8f55ca5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 20 Jul 2022 14:30:45 -0400 Subject: [PATCH 2/2] fixed config file issues --- testflo/duration.py | 7 +++---- testflo/main.py | 6 ++++-- testflo/util.py | 48 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/testflo/duration.py b/testflo/duration.py index 2574107..28ef46a 100644 --- a/testflo/duration.py +++ b/testflo/duration.py @@ -24,10 +24,9 @@ def get_iter(self, input_iter): else: title = " Max duration tests " - prefix = "\n\n" + "=" * 30 + title + "=" * 30 + "\n\n" - suffix = "\n" + "=" * len(prefix) + "\n" + eqs = "=" * 16 - write(prefix) + write("\n\n{}{}{}\n\n".format(eqs, title, eqs)) count = self.options.durations for spec, duration in sorted(durations, key=lambda t: t[1], reverse=True): @@ -43,4 +42,4 @@ def get_iter(self, input_iter): if count <= 0: break - write(suffix) + write("\n" + "=" * (len(title) + 2 * len(eqs)) + "\n") diff --git a/testflo/main.py b/testflo/main.py index 01d559e..fd86860 100644 --- a/testflo/main.py +++ b/testflo/main.py @@ -41,7 +41,7 @@ def get_iter(self, input_iter) from testflo.discover import TestDiscoverer from testflo.filters import TimeFilter, FailFilter -from testflo.util import read_config_file, read_test_file +from testflo.util import read_config_file, read_test_file, _get_parser from testflo.cover import setup_coverage, finalize_coverage from testflo.options import get_options from testflo.qman import get_server_queue @@ -116,6 +116,7 @@ def main(args=None): skip_dirs=site-packages, dist-packages, build, + _build, contrib """) read_config_file(rcfile, options) @@ -140,8 +141,9 @@ def main(args=None): tests = [os.getcwd()] def dir_exclude(d): + base = os.path.basename(d) for skip in options.skip_dirs: - if fnmatch(os.path.basename(d), skip): + if fnmatch(base, skip): return True return False diff --git a/testflo/util.py b/testflo/util.py index 6d2d5e3..9eabd2e 100644 --- a/testflo/util.py +++ b/testflo/util.py @@ -6,8 +6,8 @@ import sys import itertools import inspect -import warnings import importlib +import warnings from importlib import import_module from configparser import ConfigParser @@ -15,7 +15,7 @@ from fnmatch import fnmatch from os.path import join, dirname, basename, isfile, abspath, split, splitext -from argparse import ArgumentParser +from argparse import ArgumentParser, _AppendAction from testflo.cover import start_coverage, stop_coverage @@ -124,6 +124,10 @@ def _get_parser(): parser.add_argument('--exclude', action='append', dest='excludes', metavar='GLOB', default=[], help="Pattern to exclude test functions. Multiple patterns are allowed.") + parser.add_argument('--skip_dir', action='append', dest='skip_dirs', metavar='GLOB', default=[], + help="Pattern to skip directories. Multiple patterns are allowed. Patterns " + "are applied only to local dir names, not full paths.") + parser.add_argument('--timeout', action='store', dest='timeout', type=float, metavar='TIME_LIMIT', help="Timeout in seconds. A test will be terminated if it takes longer than " "'TIME_LIMIT'. Only works for tests running in a subprocess " @@ -402,19 +406,43 @@ def read_test_file(testfile): yield line +_parser_types = None + + +def _get_parser_action_map(): + global _parser_types + + if _parser_types is None: + _parser_types = {} + p = _get_parser() + for action in p._actions: + _parser_types[action.dest] = action + + return _parser_types + + def read_config_file(cfgfile, options): config = ConfigParser() - config.readfp(open(cfgfile)) + config.read_file(open(cfgfile), source=cfgfile) - if config.has_option('testflo', 'skip_dirs'): - skips = config.get('testflo', 'skip_dirs') - options.skip_dirs = [s.strip() for s in skips.split(',') if s.strip()] + if 'testflo' in config: + parser_map = _get_parser_action_map() - if config.has_option('testflo', 'num_procs'): - options.num_procs = int(config.get('testflo', 'num_procs')) + for name, optstr in config['testflo'].items(): + if name not in parser_map: + warnings.warn("Unknown option '{}' in testflo config file '{}'.".format(name, + cfgfile)) + continue - if config.has_option('testflo', 'noreport'): - options.noreport = bool(config.get('testflo', 'noreport')) + action = parser_map[name] + typ = action.type + if typ is None: + typ = lambda x: x + + if isinstance(action, _AppendAction): + setattr(options, name, [typ(s.strip()) for s in optstr.split(',') if s.strip()]) + else: + setattr(options, name, typ(optstr)) def get_memory_usage():