def __call__(self, values, condition_overrides={}): from fontTools.ttLib import TTFont if isinstance(values, str): values = {'font': values, 'fonts': [values]} elif isinstance(values, TTFont): values = { 'font': values.reader.file.name, 'fonts': [values.reader.file.name], 'ttFont': values, 'ttFonts': [values] } elif isinstance(values, list): if isinstance(values[0], str): values = {'fonts': values} elif isinstance(values[0], TTFont): values = { 'fonts': [v.reader.file.name for v in values], 'ttFonts': values } self.runner = CheckRunner( self.profile, values, Configuration(explicit_checks=[self.check_id])) for check_identity in self.runner.order: _, check, _ = check_identity if check.id != self.check_id: continue self.check_identity = check_identity self.check_section, self.check, self.check_iterargs = check_identity break if self.check_identity is None: raise KeyError(f'Check with id "{self.check_id}" not found.') self._args = self._get_args(condition_overrides) return list(self.runner._exec_check(self.check, self._args))
def multiprocessing_worker(jobs_queue, results_queue, profile_module_locator, runner_kwds): profile = get_profile_from_module_locator(profile_module_locator) runner = CheckRunner(profile, **runner_kwds) reporter = WorkerToQueueReporter(results_queue, profile=profile, runner=runner, ticks_to_flush=5) next_check_gen = _worker_jobs_generator(jobs_queue, profile, reporter) runner.run_externally_controlled(reporter.receive, next_check_gen)
def runner_factory( specification , explicit_checks=None , custom_order=None , values=None): """ Convenience CheckRunner factory. """ return CheckRunner( specification, values , explicit_checks=explicit_checks , custom_order=custom_order )
def start(self): profile = get_module_profile( get_module("fontbakery.profiles." + self.profilename)) print(self.checks) runner = CheckRunner(profile, values={"fonts": self.paths}, config={ "custom_order": None, "explicit_checks": self.checks, "exclude_checks": None }) print("Log levels: ", self.loglevels) hr = HTMLReporter(runner=runner, loglevels=self.loglevels) ghmd = GHMarkdownReporter(runner=runner, loglevels=self.loglevels) prog = ProgressReporter(self.progressStatus, runner=runner) reporters = [hr.receive, prog.receive, ghmd.receive] status_generator = runner.run() print("Starting distribute_generator") distribute_generator(status_generator, reporters) print("Done with distribute_generator") self.signalStatus.emit(hr.get_html(), ghmd.get_markdown())
def doit(args): logger = args.logger htmlfile = args.html if args.ttfaudit: # Special action to compare checks in profile against check_list values audit(args.fonts, logger) # args.fonts used as output file name for audit return # Process list of fonts supplied, expanding wildcards using glob if needed fonts = [] fontstype = None for pattern in args.fonts: for fullpath in glob.glob(pattern): ftype = fullpath.lower().rsplit(".", 1)[-1] if ftype == "otf": ftype = "ttf" if ftype not in ("ttf", "ufo"): logger.log( "Fonts must be OpenType or UFO - " + fullpath + " invalid", "S") if fontstype is None: fontstype = ftype else: if ftype != fontstype: logger.log( "All fonts must be of the same type - both UFO and ttf/otf fonts supplied", "S") fonts.append(fullpath) if fonts == []: logger.log( "No files match the filespec provided for fonts: " + str(args.fonts), "S") # Create the profile object if args.profile: proname = args.profile else: if fontstype == "ttf": proname = "silfont.fbtests.ttfchecks" else: logger.log("UFO fonts not yet supported", "S") try: module = get_module(proname) except Exception as e: logger.log("Failed to import profile: " + proname + "\n" + str(e), "S") profile = get_module_profile(module) psfcheck_list = module.psfcheck_list # Create the runner and reporter objetcs, then run the tests runner = CheckRunner(profile, values={"fonts": fonts}) sr = SerializeReporter( runner=runner ) # This produces results from all the tests in sr.getdoc for later analysis reporters = [sr.receive] if htmlfile: hr = HTMLReporter(runner=runner, loglevels=[SKIP]) reporters.append(hr.receive) distribute_generator(runner.run(), reporters) # Process the results results = sr.getdoc() sections = results["sections"] checks = {} maxname = 11 somedebug = False overrides = {} tempoverrides = False for section in sections: secchecks = section["checks"] for check in secchecks: checkid = check["key"][1][17:-1] fontfile = check[ "filename"] if "filename" in check else "Family-wide" path, fontname = os.path.split(fontfile) if fontname not in checks: checks[fontname] = { "ERROR": [], "FAIL": [], "WARN": [], "INFO": [], "SKIP": [], "PASS": [], "DEBUG": [] } if len(fontname) > maxname: maxname = len(fontname) status = check["logs"][0]["status"] if checkid in psfcheck_list: # Look for status overrides (changetype, temp) = ("temp_change_status", True) if "temp_change_status" in psfcheck_list[checkid]\ else ("change_status", False) if changetype in psfcheck_list[checkid]: change_status = psfcheck_list[checkid][changetype] if status in change_status: reason = change_status[ "reason"] if "reason" in change_status else None overrides[fontname + ", " + checkid] = (status + " to " + change_status[status], temp, reason) if temp: tempoverrides = True status = change_status[status] checks[fontname][status].append(check) if status == "DEBUG": somedebug = True if htmlfile: logger.log("Writing results to " + htmlfile, "P") with open(htmlfile, 'w') as hfile: hfile.write(hr.get_html()) fbstats = ["ERROR", "FAIL", "WARN", "INFO", "SKIP", "PASS"] psflevels = ["E", "E", "W", "I", "I", "V"] if somedebug: # Only have debug column if some debug statuses are present fbstats.append("DEBUG") psflevels.append("W") wrapper = TextWrapper(width=120, initial_indent=" ", subsequent_indent=" ") errorcnt = 0 failcnt = 0 summarymess = "Check status summary:\n" summarymess += "{:{pad}}ERROR FAIL WARN INFO SKIP PASS".format( "", pad=maxname + 4) if somedebug: summarymess += " DEBUG" fontlist = list(sorted(x for x in checks if x != "Family-wide")) + [ "Family-wide" ] # Sort with Family-wide last for fontname in fontlist: summarymess += "\n {:{pad}}".format(fontname, pad=maxname) for i, status in enumerate(fbstats): psflevel = psflevels[i] checklist = checks[fontname][status] cnt = len(checklist) if cnt > 0 or status != "DEBUG": summarymess += "{:6d}".format(cnt) # Suppress 0 for DEBUG if cnt: if status == "ERROR": errorcnt += cnt if status == "FAIL": failcnt += cnt messparts = [ "Checks with status {} for {}".format(status, fontname) ] for check in checklist: messparts.append(" > {}".format(check["key"][1][17:-1])) messparts += wrapper.wrap(check["logs"][0]["message"]) logger.log("\n".join(messparts), psflevel) if overrides != {}: summarymess += "\n Note: " + str( len(overrides) ) + " Fontbakery statuses were overridden - see log file for details" if tempoverrides: summarymess += "\n ******** Some of the overrides were temporary overrides ********" logger.log(summarymess, "P") if overrides != {}: for oname in overrides: override = overrides[oname] mess = "Status override for " + oname + ": " + override[0] if override[1]: mess += " (Temporary override)" logger.log(mess, "W") if override[2] is not None: logger.log("Override reason: " + override[2], "I") if errorcnt + failcnt > 0: mess = str( failcnt) + " test(s) gave a status of FAIL" if failcnt > 0 else "" if errorcnt > 0: if failcnt > 0: mess += "\n " mess += str(errorcnt) + " test(s) gave a status of ERROR which means they failed to execute properly." \ "\n " \ " ERROR probably indicates a software issue rather than font issue" logger.log(mess, "S")
def main(profile=None, values=None): # profile can be injected by e.g. check-googlefonts injects it's own profile add_profile_arg = False if profile is None: profile = get_profile() add_profile_arg = True argument_parser, values_keys = ArgumentParser(profile, profile_arg=add_profile_arg) args = argument_parser.parse_args() if args.list_checks: if args.loglevels == [PASS]: # if verbose: from fontbakery.constants import WHITE_STR, CYAN_STR, BLUE_STR for section in profile._sections.values(): print(WHITE_STR.format("\nSection:") + " " + section.name) for check in section._checks: print( CYAN_STR.format(check.id) + "\n" + BLUE_STR.format(f'"{check.description}"') + "\n") else: for section_name, section in profile._sections.items(): for check in section._checks: print(check.id) sys.exit() values_ = {} if values is not None: values_.update(values) # values_keys are returned by profile.setup_argparse # these are keys for custom arguments required by the profile. if values_keys: for key in values_keys: if hasattr(args, key): values_[key] = getattr(args, key) try: runner = CheckRunner(profile, values=values_, custom_order=args.order, explicit_checks=args.checkid, exclude_checks=args.exclude_checkid) except ValueValidationError as e: print(e) argument_parser.print_usage() sys.exit(1) # The default Windows Terminal just displays the escape codes. The argument # parser above therefore has these options disabled. if sys.platform == "win32": args.no_progress = True args.no_colors = True # the most verbose loglevel wins loglevel = min(args.loglevels) if args.loglevels else DEFAULT_LOG_LEVEL tr = TerminalReporter(runner=runner, is_async=False , print_progress=not args.no_progress , check_threshold=loglevel , log_threshold=args.loglevel_messages or loglevel , usecolor=not args.no_colors , collect_results_by=args.gather_by , skip_status_report=None if args.show_sections\ else (STARTSECTION, ENDSECTION) ) reporters = [tr.receive] if args.json: sr = SerializeReporter(runner=runner, collect_results_by=args.gather_by) reporters.append(sr.receive) if args.ghmarkdown: mdr = GHMarkdownReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(mdr.receive) if args.html: hr = HTMLReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(hr.receive) distribute_generator(runner.run(), reporters) if args.json: import json json.dump(sr.getdoc(), args.json, sort_keys=True, indent=4) print("A report in JSON format has been" " saved to '{}'".format(args.json.name)) if args.ghmarkdown: args.ghmarkdown.write(mdr.get_markdown()) print("A report in GitHub Markdown format which can be useful\n" " for posting issues on a GitHub issue tracker has been\n" " saved to '{}'".format(args.ghmarkdown.name)) if args.html: args.html.write(hr.get_html()) print(f"A report in HTML format has been saved to '{args.html.name}'") # Fail and error let the command fail return 1 if tr.worst_check_status in (ERROR, FAIL) else 0
def main(profile=None, values=None): # profile can be injected by e.g. check-googlefonts injects it's own profile add_profile_arg = False if profile is None: profile = get_profile() add_profile_arg = True argument_parser, values_keys = ArgumentParser(profile, profile_arg=add_profile_arg) args = argument_parser.parse_args() if args.list_checks: if args.loglevels == [PASS]: # if verbose: from fontbakery.constants import WHITE_STR, CYAN_STR, BLUE_STR for section in profile._sections.values(): print(WHITE_STR.format("\nSection:") + " " + section.name) for check in section._checks: print(CYAN_STR.format(check.id) + "\n" + BLUE_STR.format(f'"{check.description}"') + "\n") else: for section_name, section in profile._sections.items(): for check in section._checks: print(check.id) sys.exit() values_ = {} if values is not None: values_.update(values) # values_keys are returned by profile.setup_argparse # these are keys for custom arguments required by the profile. if values_keys: for key in values_keys: if hasattr(args, key): values_[key] = getattr(args, key) try: runner = CheckRunner(profile , values=values_ , custom_order=args.order , explicit_checks=args.checkid , exclude_checks=args.exclude_checkid ) except ValueValidationError as e: print(e) argument_parser.print_usage() sys.exit(1) # The default Windows Terminal just displays the escape codes. The argument # parser above therefore has these options disabled. if sys.platform == "win32": args.no_progress = True args.no_colors = True # the most verbose loglevel wins loglevel = min(args.loglevels) if args.loglevels else DEFAULT_LOG_LEVEL tr = TerminalReporter(runner=runner, is_async=False , print_progress=not args.no_progress , check_threshold=loglevel , log_threshold=args.loglevel_messages or loglevel , usecolor=not args.no_colors , collect_results_by=args.gather_by , skip_status_report=None if args.show_sections\ else (STARTSECTION, ENDSECTION) ) reporters = [tr.receive] if args.json: sr = SerializeReporter(runner=runner, collect_results_by=args.gather_by) reporters.append(sr.receive) if args.ghmarkdown: mdr = GHMarkdownReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(mdr.receive) if args.html: hr = HTMLReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(hr.receive) distribute_generator(runner.run(), reporters) if args.json: import json json.dump(sr.getdoc(), args.json, sort_keys=True, indent=4) print("A report in JSON format has been" " saved to '{}'".format(args.json.name)) if args.ghmarkdown: args.ghmarkdown.write(mdr.get_markdown()) print("A report in GitHub Markdown format which can be useful\n" " for posting issues on a GitHub issue tracker has been\n" " saved to '{}'".format(args.ghmarkdown.name)) if args.html: args.html.write(hr.get_html()) print(f"A report in HTML format has been saved to '{args.html.name}'") # Fail and error let the command fail return 1 if tr.worst_check_status in (ERROR, FAIL) else 0
def main(profile=None, values=None): # profile can be injected by e.g. check-googlefonts injects it's own profile add_profile_arg = False if profile is None: profile = get_profile() add_profile_arg = True argument_parser, values_keys = ArgumentParser(profile, profile_arg=add_profile_arg) args = argument_parser.parse_args() # The default Windows Terminal just displays the escape codes. The argument # parser above therefore has these options disabled. if sys.platform == "win32": args.no_progress = True args.no_colors = True theme = get_theme(args) # the most verbose loglevel wins loglevel = min(args.loglevels) if args.loglevels else DEFAULT_LOG_LEVEL if args.list_checks: list_checks(profile, theme, verbose=loglevel > DEFAULT_LOG_LEVEL) values_ = {} if values is not None: values_.update(values) # values_keys are returned by profile.setup_argparse # these are keys for custom arguments required by the profile. if values_keys: for key in values_keys: if hasattr(args, key): values_[key] = getattr(args, key) if args.configfile: configuration = Configuration.from_config_file(args.configfile) else: configuration = Configuration() # Command line args overrides config, but only if given configuration.maybe_override( Configuration(custom_order=args.order, explicit_checks=args.checkid, exclude_checks=args.exclude_checkid)) runner_kwds = dict(values=values_, config=configuration) try: runner = CheckRunner(profile, **runner_kwds) except ValueValidationError as e: print(e) argument_parser.print_usage() sys.exit(1) is_async = args.multiprocessing != 0 tr = TerminalReporter(runner=runner, is_async=is_async , print_progress=not args.no_progress , succinct=args.succinct , check_threshold=loglevel , log_threshold=args.loglevel_messages or loglevel , theme=theme , collect_results_by=args.gather_by , skip_status_report=None if args.show_sections \ else (SECTIONSUMMARY, ) ) reporters = [tr] if "reporters" not in args: args.reporters = [] for reporter_class, output_file in args.reporters: reporters.append( reporter_class(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by, output_file=output_file)) if args.multiprocessing == 0: status_generator = runner.run() else: status_generator = multiprocessing_runner(args.multiprocessing, runner, runner_kwds) distribute_generator(status_generator, [reporter.receive for reporter in reporters]) for reporter in reporters: reporter.write() # Fail and error let the command fail return 1 if tr.worst_check_status in (ERROR, FAIL) else 0
def main(profile=None, values=None): # profile can be injected by e.g. check-googlefonts injects it's own profile add_profile_arg = False if profile is None: profile = get_profile() add_profile_arg = True argument_parser, values_keys = ArgumentParser(profile, profile_arg=add_profile_arg) args = argument_parser.parse_args() # The default Windows Terminal just displays the escape codes. The argument # parser above therefore has these options disabled. if sys.platform == "win32": args.no_progress = True args.no_colors = True from fontbakery.constants import NO_COLORS_THEME, DARK_THEME, LIGHT_THEME if args.no_colors: theme = NO_COLORS_THEME else: if args.light_theme: theme = LIGHT_THEME elif args.dark_theme: theme = DARK_THEME elif sys.platform == "darwin": # The vast majority of MacOS users seem to use a light-background on the text terminal theme = LIGHT_THEME else: # For orther systems like GNU+Linux and Windows, a dark terminal seems to be more common. theme = DARK_THEME if args.list_checks: if args.loglevels == [PASS]: # if verbose: for section in profile._sections.values(): print(theme["list-checks: section"]("\nSection:") + " " + section.name) for check in section._checks: print(theme["list-checks: check-id"](check.id) + "\n" + theme["list-checks: description"] (f'"{check.description}"') + "\n") else: for section_name, section in profile._sections.items(): for check in section._checks: print(check.id) sys.exit() values_ = {} if values is not None: values_.update(values) # values_keys are returned by profile.setup_argparse # these are keys for custom arguments required by the profile. if values_keys: for key in values_keys: if hasattr(args, key): values_[key] = getattr(args, key) runner_kwds = dict(values=values_, custom_order=args.order, explicit_checks=args.checkid, exclude_checks=args.exclude_checkid) try: runner = CheckRunner(profile, **runner_kwds) except ValueValidationError as e: print(e) argument_parser.print_usage() sys.exit(1) is_async = args.multiprocessing != 0 # the most verbose loglevel wins loglevel = min(args.loglevels) if args.loglevels else DEFAULT_LOG_LEVEL tr = TerminalReporter(runner=runner, is_async=is_async , print_progress=not args.no_progress , succinct=args.succinct , check_threshold=loglevel , log_threshold=args.loglevel_messages or loglevel , theme=theme , collect_results_by=args.gather_by , skip_status_report=None if args.show_sections \ else (SECTIONSUMMARY, ) ) reporters = [tr.receive] if args.json: sr = SerializeReporter(runner=runner, collect_results_by=args.gather_by) reporters.append(sr.receive) if args.ghmarkdown: mdr = GHMarkdownReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(mdr.receive) if args.html: hr = HTMLReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(hr.receive) if args.multiprocessing == 0: status_generator = runner.run() else: status_generator = multiprocessing_runner(args.multiprocessing, runner, runner_kwds) distribute_generator(status_generator, reporters) if args.json: import json json.dump(sr.getdoc(), args.json, sort_keys=True, indent=4) print(f'A report in JSON format has been' f' saved to "{args.json.name}"') if args.ghmarkdown: args.ghmarkdown.write(mdr.get_markdown()) print(f'A report in GitHub Markdown format which can be useful\n' f' for posting issues on a GitHub issue tracker has been\n' f' saved to "{args.ghmarkdown.name}"') if args.html: args.html.write(hr.get_html()) print(f'A report in HTML format has been saved to "{args.html.name}"') # Fail and error let the command fail return 1 if tr.worst_check_status in (ERROR, FAIL) else 0
class CheckTester: """ This class offers a bit of automation to aid in the implementation of code-tests to validade the proper behaviour of FontBakery checks. !!!CAUTION: this uses a lot of "private" methods and properties of CheckRunner, in order to make unit testing different cases simpler. This is not intended to run in production. However, if that is desired we may or may not find inspiration here on how to implement a proper public CheckRunner API. Not built for performance either! The idea is that we can let this class take care of computing the dependencies of a check for us. And we can also optionaly "fake" some of them in order to create useful testing scenarios for the checks. An initial run can be with unaltered arguments, as CheckRunner would produce them by itself. And subsequent calls can reuse some of them. """ def __init__(self, module_or_profile, check_id): self.profile = module_or_profile \ if isinstance(module_or_profile, Profile) \ else get_module_profile(module_or_profile) self.check_id = check_id self.check_identity = None self.check_section = None self.check = None self.check_iterargs = None self._args = None def _get_args(self, condition_overrides=None): if condition_overrides is not None: for name_key, value in condition_overrides.items(): if isinstance(name_key, str): # this is a simplified form of a cache key: # write the conditions directly to the iterargs of the check identity used_iterargs = self.runner._filter_condition_used_iterargs( name_key, self.check_iterargs) key = (name_key, used_iterargs) else: # Full control for the caller, who has to inspect how # the desired key needs to be set up. key = name_key # error, value self.runner._cache['conditions'][key] = None, value args = self.runner._get_args(self.check, self.check_iterargs) # args that are derived iterables are generators that must be # converted to lists, otherwise we end up with exhausted # generators after their first consumption. for k in args: if self.profile.get_type(k, None) == 'derived_iterables': args[k] = list(args[k]) return args def __getitem__(self, key): if key in self._args: return self._args[key] used_iterargs = self.runner._filter_condition_used_iterargs( key, self.check_iterargs) key = (key, used_iterargs) if key in self.runner._cache['conditions']: return self.runner._cache['conditions'][key][1] def __call__(self, values, condition_overrides={}): from fontTools.ttLib import TTFont if isinstance(values, str): values = {'font': values, 'fonts': [values]} elif isinstance(values, TTFont): values = { 'font': values.reader.file.name, 'fonts': [values.reader.file.name], 'ttFont': values, 'ttFonts': [values] } elif isinstance(values, list): if isinstance(values[0], str): values = {'fonts': values} elif isinstance(values[0], TTFont): values = { 'fonts': [v.reader.file.name for v in values], 'ttFonts': values } self.runner = CheckRunner( self.profile, values, Configuration(explicit_checks=[self.check_id])) for check_identity in self.runner.order: _, check, _ = check_identity if check.id != self.check_id: continue self.check_identity = check_identity self.check_section, self.check, self.check_iterargs = check_identity break if self.check_identity is None: raise KeyError(f'Check with id "{self.check_id}" not found.') self._args = self._get_args(condition_overrides) return list(self.runner._exec_check(self.check, self._args))
def main(specification=None, values=None): # specification can be injected by e.g. check-googlefonts injects it's own spec add_spec_arg = False if specification is None: specification = get_spec() add_spec_arg = True argument_parser, values_keys = ArgumentParser(specification, spec_arg=add_spec_arg) args = argument_parser.parse_args() if args.list_checks: print('Available checks') for section_name, section in specification._sections.items(): checks = section.list_checks() message = "# {}:\n {}".format(section_name,"\n ".join(checks)) print(message) sys.exit() values_ = {} if values is not None: values_.update(values) # values_keys are returned by specification.setup_argparse # these are keys for custom arguments required by the spec. if values_keys: for key in values_keys: if hasattr(args, key): values_[key] = getattr(args, key) try: runner = CheckRunner(specification , values=values_ , custom_order=args.order , explicit_checks=args.checkid , exclude_checks=args.exclude_checkid ) except ValueValidationError as e: print(e) argument_parser.print_usage() sys.exit(1) # The default Windows Terminal just displays the escape codes. The argument # parser above therefore has these options disabled. if sys.platform == "win32": args.no_progress = True args.no_colors = True # the most verbose loglevel wins loglevel = min(args.loglevels) if args.loglevels else DEFAULT_LOG_LEVEL tr = TerminalReporter(runner=runner, is_async=False , print_progress=not args.no_progress , check_threshold=loglevel , log_threshold=args.loglevel_messages or loglevel , usecolor=not args.no_colors , collect_results_by=args.gather_by , skip_status_report=None if args.show_sections\ else (STARTSECTION, ENDSECTION) ) reporters = [tr.receive] if args.json: sr = SerializeReporter(runner=runner, collect_results_by=args.gather_by) reporters.append(sr.receive) if args.ghmarkdown: mdr = GHMarkdownReporter(loglevels=args.loglevels, runner=runner, collect_results_by=args.gather_by) reporters.append(mdr.receive) distribute_generator(runner.run(), reporters) if args.json: import json json.dump(sr.getdoc(), args.json, sort_keys=True, indent=4) print("A report in JSON format has been" " saved to '{}'".format(args.json.name)) if args.ghmarkdown: args.ghmarkdown.write(mdr.get_markdown()) print("A report in GitHub Markdown format which can be useful\n" " for posting issues on a GitHub issue tracker has been\n" " saved to '{}'".format(args.ghmarkdown.name)) # Fail and error let the command fail return 1 if tr.worst_check_status in (ERROR, FAIL) else 0