def test_commonprefix(self): self.assertEqual( commonprefix( [os.path.join('foo', 'bar', 'baz'), 'foo/qux', 'foo/baz/qux']), 'foo/') self.assertEqual( commonprefix( [os.path.join('foo', 'bar', 'baz'), 'foo/qux', 'baz/qux']), '')
def test_commonprefix(self): self.assertEqual( commonprefix([ self.SEP.join(("foo", "bar", "baz")), "foo/qux", "foo/baz/qux" ]), "foo/", ) self.assertEqual( commonprefix( [self.SEP.join(("foo", "bar", "baz")), "foo/qux", "baz/qux"]), "", )
def __call__(self, result): paths = set(result.issues.keys() + result.suppressed_warnings.keys()) commonprefix = mozpath.commonprefix( [mozpath.abspath(p) for p in paths]) commonprefix = commonprefix.rsplit('/', 1)[0] + '/' summary = defaultdict(lambda: [0, 0]) for path in paths: abspath = mozpath.abspath(path) assert abspath.startswith(commonprefix) if abspath != commonprefix: parts = mozpath.split(mozpath.relpath( abspath, commonprefix))[:self.depth] abspath = mozpath.join(commonprefix, *parts) summary[abspath][0] += len( [r for r in result.issues[path] if r.level == 'error']) summary[abspath][1] += len( [r for r in result.issues[path] if r.level == 'warning']) summary[abspath][1] += result.suppressed_warnings[path] msg = [] for path, (errors, warnings) in sorted(summary.items()): warning_str = ", {}".format(pluralize( 'warning', warnings)) if warnings else '' msg.append('{}: {}{}'.format(path, pluralize('error', errors), warning_str)) return '\n'.join(msg)
def find_shell_scripts(config, paths): found = dict() root = config["root"] exclude = [mozpath.join(root, e) for e in config.get("exclude", [])] if config.get("extensions"): pattern = "**/*.{}".format(config.get("extensions")[0]) else: pattern = "**/*.sh" files = [] for path in paths: path = mozpath.normsep(path) ignore = [ e[len(path):].lstrip("/") for e in exclude if mozpath.commonprefix((path, e)) == path ] finder = FileFinder(path, ignore=ignore) files.extend([os.path.join(path, p) for p, f in finder.find(pattern)]) for filename in files: shell = determine_shell_from_script(filename) if shell: found[filename] = shell return found
def __call__(self, result): paths = set( list(result.issues.keys()) + list(result.suppressed_warnings.keys())) commonprefix = mozpath.commonprefix( [mozpath.abspath(p) for p in paths]) commonprefix = commonprefix.rsplit("/", 1)[0] + "/" summary = defaultdict(lambda: [0, 0]) for path in paths: abspath = mozpath.abspath(path) assert abspath.startswith(commonprefix) if abspath != commonprefix: parts = mozpath.split(mozpath.relpath( abspath, commonprefix))[:self.depth] abspath = mozpath.join(commonprefix, *parts) summary[abspath][0] += len( [r for r in result.issues[path] if r.level == "error"]) summary[abspath][1] += len( [r for r in result.issues[path] if r.level == "warning"]) summary[abspath][1] += result.suppressed_warnings[path] msg = [] for path, (errors, warnings) in sorted(summary.items()): warning_str = (", {}".format(pluralize("warning", warnings)) if warnings else "") msg.append("{}: {}{}".format(path, pluralize("error", errors), warning_str)) return "\n".join(msg)
def collapse(paths, base=None, dotfiles=False): """Given an iterable of paths, collapse them into the smallest possible set of paths that contain the original set (without containing any extra paths). For example, if directory 'a' contains two files b.txt and c.txt, calling: collapse(['a/b.txt', 'a/c.txt']) returns ['a']. But if a third file d.txt also exists, then it will return ['a/b.txt', 'a/c.txt'] since ['a'] would also include that extra file. :param paths: An iterable of paths (files and directories) to collapse. :returns: The smallest set of paths (files and directories) that contain the original set of paths and only the original set. """ if not paths: if not base: return [] # Need to test whether directory chain is empty. If it is then bubble # the base back up so that it counts as 'covered'. for _, _, names in os.walk(base): if names: return [] return [base] if not base: paths = list(map(mozpath.abspath, paths)) base = mozpath.commonprefix(paths) if not os.path.isdir(base): base = os.path.dirname(base) if base in paths: return [base] covered = set() full = set() for name in os.listdir(base): if not dotfiles and name[0] == '.': continue path = mozpath.join(base, name) full.add(path) if path in paths: # This path was explicitly specified, so just bubble it back up # without recursing down into it (if it was a directory). covered.add(path) elif os.path.isdir(path): new_paths = [p for p in paths if p.startswith(path)] covered.update(collapse(new_paths, base=path, dotfiles=dotfiles)) if full == covered: # Every file under this base was covered, so we can collapse them all # up into the base path. return [base] return list(covered)
def close(self, auto_root_manifest=True): ''' Add possibly missing bits and push all instructions to the formatter. ''' if auto_root_manifest: # Simple package manifests don't contain the root manifests, so # find and add them. paths = [mozpath.dirname(m) for m in self._manifests] path = mozpath.dirname(mozpath.commonprefix(paths)) for p, f in self._finder.find(mozpath.join(path, 'chrome.manifest')): if p not in self._manifests: self.packager.add(SimpleManifestSink.normalize_path(p), f) self.packager.close()
def close(self, auto_root_manifest=True): ''' Add possibly missing bits and push all instructions to the formatter. ''' if auto_root_manifest: # Simple package manifests don't contain the root manifests, so # find and add them. paths = [mozpath.dirname(m) for m in self._manifests] path = mozpath.dirname(mozpath.commonprefix(paths)) for p, f in self._finder.find(mozpath.join(path, 'chrome.manifest')): if not p in self._manifests: self.packager.add(SimpleManifestSink.normalize_path(p), f) self.packager.close()
def expand_exclusions(paths, config, root): """Returns all files that match patterns and aren't excluded. This is used by some external linters who receive 'batch' files (e.g dirs) but aren't capable of applying their own exclusions. There is an argument to be made that this step should just apply to all linters no matter what. Args: paths (list): List of candidate paths to lint. config (dict): Linter's config object. root (str): Root of the repository. Returns: Generator which generates list of paths that weren't excluded. """ extensions = [e.lstrip(".") for e in config.get("extensions", [])] find_dotfiles = config.get("find-dotfiles", False) def normalize(path): path = mozpath.normpath(path) if os.path.isabs(path): return path return mozpath.join(root, path) exclude = list(map(normalize, config.get("exclude", []))) for path in paths: path = mozpath.normsep(path) if os.path.isfile(path): if any(path.startswith(e) for e in exclude if "*" not in e): continue if any(mozpath.match(path, e) for e in exclude if "*" in e): continue yield path continue ignore = [ e[len(path):].lstrip("/") for e in exclude if mozpath.commonprefix((path, e)) == path ] finder = FileFinder(path, ignore=ignore, find_dotfiles=find_dotfiles) _, ext = os.path.splitext(path) ext.lstrip(".") for ext in extensions: for p, f in finder.find("**/*.{}".format(ext)): yield os.path.join(path, p)
def run_linter(python, paths, config, **lintargs): binary = find_executable(python) if not binary: # TODO bootstrap python3 if not available print( 'error: {} not detected, aborting py-compat check'.format(python)) if 'MOZ_AUTOMATION' in os.environ: return 1 return [] root = lintargs['root'] pattern = "**/*.py" exclude = [mozpath.join(root, e) for e in lintargs.get('exclude', [])] files = [] for path in paths: path = mozpath.normsep(path) if os.path.isfile(path): files.append(path) continue ignore = [ e[len(path):].lstrip('/') for e in exclude if mozpath.commonprefix((path, e)) == path ] finder = FileFinder(path, ignore=ignore) files.extend([os.path.join(path, p) for p, f in finder.find(pattern)]) with tempfile.NamedTemporaryFile(mode='w') as fh: fh.write('\n'.join(files)) fh.flush() cmd = [binary, os.path.join(here, 'check_compat.py'), fh.name] proc = PyCompatProcess(config, cmd) proc.run() try: proc.wait() except KeyboardInterrupt: proc.kill() return results
def __call__(self, result, **kwargs): commonprefix = mozpath.commonprefix( [mozpath.abspath(p) for p in result]) commonprefix = commonprefix.rsplit('/', 1)[0] + '/' summary = defaultdict(int) for path, errors in result.iteritems(): path = mozpath.abspath(path) assert path.startswith(commonprefix) if path == commonprefix: summary[path] += len(errors) continue parts = mozpath.split(mozpath.relpath(path, commonprefix))[:self.depth] path = mozpath.join(commonprefix, *parts) summary[path] += len(errors) return '\n'.join( ['{}: {}'.format(k, summary[k]) for k in sorted(summary)])
def run_linter(python, paths, config, **lintargs): binary = find_executable(python) if not binary: # If we're in automation, this is fatal. Otherwise, the warning in the # setup method was already printed. if 'MOZ_AUTOMATION' in os.environ: return 1 return [] root = lintargs['root'] pattern = "**/*.py" exclude = [mozpath.join(root, e) for e in lintargs.get('exclude', [])] files = [] for path in paths: path = mozpath.normsep(path) if os.path.isfile(path): files.append(path) continue ignore = [ e[len(path):].lstrip('/') for e in exclude if mozpath.commonprefix((path, e)) == path ] finder = FileFinder(path, ignore=ignore) files.extend([os.path.join(path, p) for p, f in finder.find(pattern)]) with mozfile.NamedTemporaryFile(mode='w') as fh: fh.write('\n'.join(files)) fh.flush() cmd = [binary, os.path.join(here, 'check_compat.py'), fh.name] proc = PyCompatProcess(config, cmd) proc.run() try: proc.wait() except KeyboardInterrupt: proc.kill() return results
def run_linter(python, paths, config, **lintargs): binary = find_executable(python) if not binary: # TODO bootstrap python3 if not available print('error: {} not detected, aborting py-compat check'.format(python)) if 'MOZ_AUTOMATION' in os.environ: return 1 return [] root = lintargs['root'] pattern = "**/*.py" exclude = [mozpath.join(root, e) for e in lintargs.get('exclude', [])] files = [] for path in paths: path = mozpath.normsep(path) if os.path.isfile(path): files.append(path) continue ignore = [e[len(path):].lstrip('/') for e in exclude if mozpath.commonprefix((path, e)) == path] finder = FileFinder(path, ignore=ignore) files.extend([os.path.join(path, p) for p, f in finder.find(pattern)]) with tempfile.NamedTemporaryFile(mode='w') as fh: fh.write('\n'.join(files)) fh.flush() cmd = [binary, os.path.join(here, 'check_compat.py'), fh.name] proc = PyCompatProcess(config, cmd) proc.run() try: proc.wait() except KeyboardInterrupt: proc.kill() return results
def test_commonprefix(self): self.assertEqual(commonprefix([os.path.join('foo', 'bar', 'baz'), 'foo/qux', 'foo/baz/qux']), 'foo/') self.assertEqual(commonprefix([os.path.join('foo', 'bar', 'baz'), 'foo/qux', 'baz/qux']), '')
def test_commonprefix(self): self.assertEqual(commonprefix([os.path.join("foo", "bar", "baz"), "foo/qux", "foo/baz/qux"]), "foo/") self.assertEqual(commonprefix([os.path.join("foo", "bar", "baz"), "foo/qux", "baz/qux"]), "")
def test_commonprefix(self): self.assertEqual(commonprefix([self.SEP.join(('foo', 'bar', 'baz')), 'foo/qux', 'foo/baz/qux']), 'foo/') self.assertEqual(commonprefix([self.SEP.join(('foo', 'bar', 'baz')), 'foo/qux', 'baz/qux']), '')
def report(self, components, flavor, subsuite, paths, show_manifests, show_tests, show_summary, show_annotations, show_activedata, filter_values, filter_keys, show_components, output_file, branches, days): def matches_filters(test): ''' Return True if all of the requested filter_values are found in this test; if filter_keys are specified, restrict search to those test keys. ''' for value in filter_values: value_found = False for key in test: if not filter_keys or key in filter_keys: if re.search(value, test[key]): value_found = True break if not value_found: return False return True start_time = datetime.datetime.now() # Ensure useful report by default if not show_manifests and not show_tests and not show_summary and not show_annotations: show_manifests = True show_summary = True by_component = {} if components: components = components.split(',') if filter_keys: filter_keys = filter_keys.split(',') if filter_values: filter_values = filter_values.split(',') else: filter_values = [] display_keys = (filter_keys or []) + ['skip-if', 'fail-if', 'fails-if'] display_keys = set(display_keys) print("Finding tests...") here = os.path.abspath(os.path.dirname(__file__)) resolver = TestResolver.from_environment(cwd=here, loader_cls=TestManifestLoader) tests = list(resolver.resolve_tests(paths=paths, flavor=flavor, subsuite=subsuite)) manifest_paths = set() for t in tests: if 'manifest' in t and t['manifest'] is not None: manifest_paths.add(t['manifest']) manifest_count = len(manifest_paths) print("Resolver found {} tests, {} manifests".format(len(tests), manifest_count)) if show_manifests: topsrcdir = self.build_obj.topsrcdir by_component['manifests'] = {} manifest_paths = list(manifest_paths) manifest_paths.sort() relpaths = [] for manifest_path in manifest_paths: relpath = mozpath.relpath(manifest_path, topsrcdir) if mozpath.commonprefix((manifest_path, topsrcdir)) != topsrcdir: continue relpaths.append(relpath) reader = self.build_obj.mozbuild_reader(config_mode='empty') files_info = reader.files_info(relpaths) for manifest_path in manifest_paths: relpath = mozpath.relpath(manifest_path, topsrcdir) if mozpath.commonprefix((manifest_path, topsrcdir)) != topsrcdir: continue manifest_info = None if relpath in files_info: bug_component = files_info[relpath].get('BUG_COMPONENT') if bug_component: key = "{}::{}".format(bug_component.product, bug_component.component) else: key = "<unknown bug component>" if (not components) or (key in components): manifest_info = { 'manifest': relpath, 'tests': 0, 'skipped': 0 } rkey = key if show_components else 'all' if rkey in by_component['manifests']: by_component['manifests'][rkey].append(manifest_info) else: by_component['manifests'][rkey] = [manifest_info] if manifest_info: for t in tests: if t['manifest'] == manifest_path: manifest_info['tests'] += 1 if t.get('skip-if'): manifest_info['skipped'] += 1 for key in by_component['manifests']: by_component['manifests'][key].sort() if show_tests: by_component['tests'] = {} if show_tests or show_summary or show_annotations: test_count = 0 failed_count = 0 skipped_count = 0 annotation_count = 0 condition_count = 0 component_set = set() relpaths = [] conditions = {} known_unconditional_annotations = ['skip', 'fail', 'asserts', 'random'] known_conditional_annotations = ['skip-if', 'fail-if', 'run-if', 'fails-if', 'fuzzy-if', 'random-if', 'asserts-if'] for t in tests: relpath = t.get('srcdir_relpath') relpaths.append(relpath) reader = self.build_obj.mozbuild_reader(config_mode='empty') files_info = reader.files_info(relpaths) for t in tests: if not matches_filters(t): continue if 'referenced-test' in t: # Avoid double-counting reftests: disregard reference file entries continue if show_annotations: for key in t: if key in known_unconditional_annotations: annotation_count += 1 if key in known_conditional_annotations: annotation_count += 1 # Here 'key' is a manifest annotation type like 'skip-if' and t[key] # is the associated condition. For example, the manifestparser # manifest annotation, "skip-if = os == 'win'", is expected to be # encoded as t['skip-if'] = "os == 'win'". # To allow for reftest manifests, t[key] may have multiple entries # separated by ';', each corresponding to a condition for that test # and annotation type. For example, # "skip-if(Android&&webrender) skip-if(OSX)", would be # encoded as t['skip-if'] = "Android&&webrender;OSX". annotation_conditions = t[key].split(';') for condition in annotation_conditions: condition_count += 1 # Trim reftest fuzzy-if ranges: everything after the first comma # eg. "Android,0-2,1-3" -> "Android" condition = condition.split(',')[0] if condition not in conditions: conditions[condition] = 0 conditions[condition] += 1 test_count += 1 relpath = t.get('srcdir_relpath') if relpath in files_info: bug_component = files_info[relpath].get('BUG_COMPONENT') if bug_component: key = "{}::{}".format(bug_component.product, bug_component.component) else: key = "<unknown bug component>" if (not components) or (key in components): component_set.add(key) test_info = {'test': relpath} for test_key in display_keys: value = t.get(test_key) if value: test_info[test_key] = value if t.get('fail-if'): failed_count += 1 if t.get('fails-if'): failed_count += 1 if t.get('skip-if'): skipped_count += 1 if show_tests: rkey = key if show_components else 'all' if rkey in by_component['tests']: # Avoid duplicates: Some test paths have multiple TestResolver # entries, as when a test is included by multiple manifests. found = False for ctest in by_component['tests'][rkey]: if ctest['test'] == test_info['test']: found = True break if not found: by_component['tests'][rkey].append(test_info) else: by_component['tests'][rkey] = [test_info] if show_tests: for key in by_component['tests']: by_component['tests'][key].sort(key=lambda k: k['test']) if show_activedata: try: self.add_activedata(branches, days, by_component) except Exception: print("Failed to retrieve some ActiveData data.") traceback.print_exc() self.log_verbose("%d tests updated with matching ActiveData data" % self.total_activedata_matches) self.log_verbose("%d seconds waiting for ActiveData" % self.total_activedata_seconds) by_component['description'] = self.description( components, flavor, subsuite, paths, show_manifests, show_tests, show_summary, show_annotations, show_activedata, filter_values, filter_keys, branches, days) if show_summary: by_component['summary'] = {} by_component['summary']['components'] = len(component_set) by_component['summary']['manifests'] = manifest_count by_component['summary']['tests'] = test_count by_component['summary']['failed tests'] = failed_count by_component['summary']['skipped tests'] = skipped_count if show_annotations: by_component['annotations'] = {} by_component['annotations']['total annotations'] = annotation_count by_component['annotations']['total conditions'] = condition_count by_component['annotations']['unique conditions'] = len(conditions) by_component['annotations']['conditions'] = conditions json_report = json.dumps(by_component, indent=2, sort_keys=True) if output_file: output_file = os.path.abspath(output_file) output_dir = os.path.dirname(output_file) if not os.path.isdir(output_dir): os.makedirs(output_dir) with open(output_file, 'w') as f: f.write(json_report) else: print(json_report) end_time = datetime.datetime.now() self.log_verbose("%d seconds total to generate report" % (end_time - start_time).total_seconds())
def report( self, components, flavor, subsuite, paths, show_manifests, show_tests, show_summary, show_annotations, filter_values, filter_keys, show_components, output_file, ): def matches_filters(test): """ Return True if all of the requested filter_values are found in this test; if filter_keys are specified, restrict search to those test keys. """ for value in filter_values: value_found = False for key in test: if not filter_keys or key in filter_keys: if re.search(value, test[key]): value_found = True break if not value_found: return False return True start_time = datetime.datetime.now() # Ensure useful report by default if (not show_manifests and not show_tests and not show_summary and not show_annotations): show_manifests = True show_summary = True by_component = {} if components: components = components.split(",") if filter_keys: filter_keys = filter_keys.split(",") if filter_values: filter_values = filter_values.split(",") else: filter_values = [] display_keys = (filter_keys or []) + ["skip-if", "fail-if", "fails-if"] display_keys = set(display_keys) print("Finding tests...") here = os.path.abspath(os.path.dirname(__file__)) resolver = TestResolver.from_environment(cwd=here, loader_cls=TestManifestLoader) tests = list( resolver.resolve_tests(paths=paths, flavor=flavor, subsuite=subsuite)) manifest_paths = set() for t in tests: if "manifest" in t and t["manifest"] is not None: manifest_paths.add(t["manifest"]) manifest_count = len(manifest_paths) print("Resolver found {} tests, {} manifests".format( len(tests), manifest_count)) if show_manifests: topsrcdir = self.build_obj.topsrcdir by_component["manifests"] = {} manifest_paths = list(manifest_paths) manifest_paths.sort() relpaths = [] for manifest_path in manifest_paths: relpath = mozpath.relpath(manifest_path, topsrcdir) if mozpath.commonprefix( (manifest_path, topsrcdir)) != topsrcdir: continue relpaths.append(relpath) reader = self.build_obj.mozbuild_reader(config_mode="empty") files_info = reader.files_info(relpaths) for manifest_path in manifest_paths: relpath = mozpath.relpath(manifest_path, topsrcdir) if mozpath.commonprefix( (manifest_path, topsrcdir)) != topsrcdir: continue manifest_info = None if relpath in files_info: bug_component = files_info[relpath].get("BUG_COMPONENT") if bug_component: key = "{}::{}".format(bug_component.product, bug_component.component) else: key = "<unknown bug component>" if (not components) or (key in components): manifest_info = { "manifest": relpath, "tests": 0, "skipped": 0 } rkey = key if show_components else "all" if rkey in by_component["manifests"]: by_component["manifests"][rkey].append( manifest_info) else: by_component["manifests"][rkey] = [manifest_info] if manifest_info: for t in tests: if t["manifest"] == manifest_path: manifest_info["tests"] += 1 if t.get("skip-if"): manifest_info["skipped"] += 1 for key in by_component["manifests"]: by_component["manifests"][key].sort( key=lambda k: k["manifest"]) if show_tests: by_component["tests"] = {} if show_tests or show_summary or show_annotations: test_count = 0 failed_count = 0 skipped_count = 0 annotation_count = 0 condition_count = 0 component_set = set() relpaths = [] conditions = {} known_unconditional_annotations = [ "skip", "fail", "asserts", "random" ] known_conditional_annotations = [ "skip-if", "fail-if", "run-if", "fails-if", "fuzzy-if", "random-if", "asserts-if", ] for t in tests: relpath = t.get("srcdir_relpath") relpaths.append(relpath) reader = self.build_obj.mozbuild_reader(config_mode="empty") files_info = reader.files_info(relpaths) for t in tests: if not matches_filters(t): continue if "referenced-test" in t: # Avoid double-counting reftests: disregard reference file entries continue if show_annotations: for key in t: if key in known_unconditional_annotations: annotation_count += 1 if key in known_conditional_annotations: annotation_count += 1 # Here 'key' is a manifest annotation type like 'skip-if' and t[key] # is the associated condition. For example, the manifestparser # manifest annotation, "skip-if = os == 'win'", is expected to be # encoded as t['skip-if'] = "os == 'win'". # To allow for reftest manifests, t[key] may have multiple entries # separated by ';', each corresponding to a condition for that test # and annotation type. For example, # "skip-if(Android&&webrender) skip-if(OSX)", would be # encoded as t['skip-if'] = "Android&&webrender;OSX". annotation_conditions = t[key].split(";") for condition in annotation_conditions: condition_count += 1 # Trim reftest fuzzy-if ranges: everything after the first comma # eg. "Android,0-2,1-3" -> "Android" condition = condition.split(",")[0] if condition not in conditions: conditions[condition] = 0 conditions[condition] += 1 test_count += 1 relpath = t.get("srcdir_relpath") if relpath in files_info: bug_component = files_info[relpath].get("BUG_COMPONENT") if bug_component: key = "{}::{}".format(bug_component.product, bug_component.component) else: key = "<unknown bug component>" if (not components) or (key in components): component_set.add(key) test_info = {"test": relpath} for test_key in display_keys: value = t.get(test_key) if value: test_info[test_key] = value if t.get("fail-if"): failed_count += 1 if t.get("fails-if"): failed_count += 1 if t.get("skip-if"): skipped_count += 1 if show_tests: rkey = key if show_components else "all" if rkey in by_component["tests"]: # Avoid duplicates: Some test paths have multiple TestResolver # entries, as when a test is included by multiple manifests. found = False for ctest in by_component["tests"][rkey]: if ctest["test"] == test_info["test"]: found = True break if not found: by_component["tests"][rkey].append( test_info) else: by_component["tests"][rkey] = [test_info] if show_tests: for key in by_component["tests"]: by_component["tests"][key].sort(key=lambda k: k["test"]) by_component["description"] = self.description( components, flavor, subsuite, paths, show_manifests, show_tests, show_summary, show_annotations, filter_values, filter_keys, ) if show_summary: by_component["summary"] = {} by_component["summary"]["components"] = len(component_set) by_component["summary"]["manifests"] = manifest_count by_component["summary"]["tests"] = test_count by_component["summary"]["failed tests"] = failed_count by_component["summary"]["skipped tests"] = skipped_count if show_annotations: by_component["annotations"] = {} by_component["annotations"]["total annotations"] = annotation_count by_component["annotations"]["total conditions"] = condition_count by_component["annotations"]["unique conditions"] = len(conditions) by_component["annotations"]["conditions"] = conditions self.write_report(by_component, output_file) end_time = datetime.datetime.now() self.log_verbose("%d seconds total to generate report" % (end_time - start_time).total_seconds())