diff options
Diffstat (limited to 'src/silfont/scripts/psfrunfbchecks.py')
-rw-r--r-- | src/silfont/scripts/psfrunfbchecks.py | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/src/silfont/scripts/psfrunfbchecks.py b/src/silfont/scripts/psfrunfbchecks.py new file mode 100644 index 0000000..69ff192 --- /dev/null +++ b/src/silfont/scripts/psfrunfbchecks.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +'''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__ = 'https://github.com/silnrsi/pysilfont' +__copyright__ = 'Copyright (c) 2020 SIL International (https://www.sil.org)' +__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)' +__author__ = 'David Raymond' + +import glob, os, csv + +from textwrap import TextWrapper + +# Error message for users installing pysilfont manually +try: + import fontbakery +except ImportError: + print("\nError: Fontbakery is not installed by default, type pip3 install fontbakery[all]\n") +else: + from fontbakery.reporters.serialize import SerializeReporter + from fontbakery.reporters.html import HTMLReporter + from fontbakery.checkrunner import distribute_generator, CheckRunner, get_module_profile + from fontbakery.status import PASS, FAIL, WARN, ERROR, INFO, SKIP + 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() |