diff --git a/testflo/duration.py b/testflo/duration.py new file mode 100644 index 0000000..28ef46a --- /dev/null +++ b/testflo/duration.py @@ -0,0 +1,45 @@ +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 " + + eqs = "=" * 16 + + 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): + 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("\n" + "=" * (len(title) + 2 * len(eqs)) + "\n") diff --git a/testflo/main.py b/testflo/main.py index 850a0b0..fd86860 100644 --- a/testflo/main.py +++ b/testflo/main.py @@ -37,10 +37,11 @@ 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 -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 @@ -115,6 +116,7 @@ def main(args=None): skip_dirs=site-packages, dist-packages, build, + _build, contrib """) read_config_file(rcfile, options) @@ -139,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 @@ -206,6 +209,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 +223,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..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 @@ -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,14 @@ 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('--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 " + "(MPI or isolated).") return parser @@ -393,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 'testflo' in config: + parser_map = _get_parser_action_map() - 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()] + 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', 'num_procs'): - options.num_procs = int(config.get('testflo', 'num_procs')) + action = parser_map[name] + typ = action.type + if typ is None: + typ = lambda x: x - if config.has_option('testflo', 'noreport'): - options.noreport = bool(config.get('testflo', 'noreport')) + 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():