#!/usr/bin/env python '''Run Font Bakery tests using a standard profile with option to specify an alternative profile It defaults to ttfchecks.py - ufo checks are not supported yet''' __url__ = 'http://github.com/silnrsi/pysilfont' __copyright__ = 'Copyright (c) 2020 SIL International (http://www.sil.org)' __license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)' __author__ = 'David Raymond' import glob, os, csv from textwrap import TextWrapper from fontbakery.reporters.serialize import SerializeReporter from fontbakery.reporters.html import HTMLReporter from fontbakery.checkrunner import distribute_generator, CheckRunner, get_module_profile, SKIP, INFO from fontbakery.configuration import Configuration from fontbakery.commands.check_profile import get_module from fontbakery import __version__ as version from silfont.core import execute argspec = [ ('fonts',{'help': 'font(s) to run checks against; wildcards allowed', 'nargs': "+"}, {'type': 'filename'}), ('--profile', {'help': 'profile to use instead of Pysilfont default'}, {}), ('--html', {'help': 'Write html report to htmlfile', 'metavar': "HTMLFILE"}, {}), ('--csv',{'help': 'Write results to csv file'}, {'type': 'filename', 'def': None}), ('-F', '--full-lists',{'help': "Don't truncate lists of items" ,'action': 'store_true', 'default': False}, {}), ('--ttfaudit', {'help': 'Compare the list of ttf checks in pysilfont with those in Font Bakery and output a csv to "fonts". No checks are actually run', 'action': 'store_true', 'default': False}, {}), ('-l', '--log', {'help': 'Log file'}, {'type': 'outfile', 'def': '_runfbchecks.log'})] def doit(args): global version v = version.split(".") version = f'{v[0]}.{v[1]}.{v[2]}' # Set version to just the number part - ie without .dev... 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 if args.csv: try: csvfile = open(args.csv, 'w') csvwriter = csv.writer(csvfile) csvlines = [] except Exception as e: logger.log("Failed to open " + args.csv + ": " + str(e), "S") else: csvfile = None # 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") # Find the main folder name for ttf files - strips "results" if present (path, ttfdir) = os.path.split(os.path.dirname(fonts[0])) if ttfdir == ("results"): ttfdir = os.path.basename(path) # 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) profile.configuration_defaults = { "com.google.fonts/check/file_size": { "WARN_SIZE": 1 * 1024 * 1024, "FAIL_SIZE": 9 * 1024 * 1024 } } psfcheck_list = module.psfcheck_list # Create the runner and reporter objects, then run the tests configuration = Configuration(full_lists = args.full_lists) runner = CheckRunner(profile, values={ "fonts": fonts, 'ufos': [], 'designspaces': [], 'glyphs_files': [], 'readme_md': [], 'metadata_pb': []} , config=configuration) if version == "0.8.6": sr = SerializeReporter(runner=runner) # This produces results from all the tests in sr.getdoc for later analysis else: sr = SerializeReporter(runner=runner, loglevels = [INFO]) # loglevels was added with 0.8.7 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["result"] 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] ## Should validate new status is one of FAIL, WARN or PASS 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")) # Alphabetic list of fonts if "Family-wide" in checks: fontlist.append("Family-wide") # Add 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: checkid = check["key"][1][17:-1] csvline = [ttfdir, fontname, check["key"][1][17:-1], status, check["description"]] messparts.append(" > {}".format(checkid)) for record in check["logs"]: message = record["message"] if record["status"] != status: message = record["status"] + " " + message messparts += wrapper.wrap(message) csvline.append(message) if csvfile: csvlines.append(csvline) logger.log("\n".join(messparts) , psflevel) if csvfile: # Output to csv file, worted by font then checkID for line in sorted(csvlines, key = lambda x: (x[1],x[2])): csvwriter.writerow(line) 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, "E") def audit(fonts, logger): if len(fonts) != 1: logger.log("For audit, specify output csv file instead of list of fonts", "S") csvname = fonts[0] from silfont.fbtests.ttfchecks import all_checks_dict missingfromprofile=[] missingfromchecklist=[] checks = all_checks_dict() logger.log("Opening " + csvname + " for audit output csv", "P") with open(csvname, 'w', newline='') as csvfile: csvwriter = csv.writer(csvfile, dialect='excel') fields = ["id", "psfaction", "section", "description", "rationale", "conditions"] csvwriter.writerow(fields) for checkid in checks: check = checks[checkid] row = [checkid] for field in fields: if field != "id": row.append(check[field]) if check["section"] == "Missing": missingfromprofile.append(checkid) if check["psfaction"] == "Not in psfcheck_list": missingfromchecklist.append(checkid) csvwriter.writerow(row) if missingfromprofile != []: mess = "The following checks are in psfcheck_list but not in the ttfchecks.py profile:" for checkid in missingfromprofile: mess += "\n " + checkid logger.log(mess, "E") if missingfromchecklist != []: mess = "The following checks are in the ttfchecks.py profile but not in psfcheck_list:" for checkid in missingfromchecklist: mess += "\n " + checkid logger.log(mess, "E") return def cmd(): execute(None, doit, argspec) if __name__ == "__main__": cmd()