"""
FontBakery reporters/serialize can report the events of the FontBakery
CheckRunner Protocol to a serializeable document e.g. for usage with `json.dumps`.
Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.
"""
from fontbakery.status import Status, DEBUG, END, ENDCHECK, SECTIONSUMMARY, START
from fontbakery.reporters import FontbakeryReporter
[docs]class SerializeReporter(FontbakeryReporter):
"""
usage:
>> sr = SerializeReporter(runner=runner, collect_results_by='font')
>> sr.run()
>> import json
>> print(json.dumps(sr.getdoc(), sort_keys=True, indent=4))
"""
def __init__(self, loglevels, succinct=None, collect_results_by=None, **kwd):
super().__init__(**kwd)
self.succinct = succinct
self.loglevels = loglevels
self._results_by = collect_results_by
self._items = {}
self._doc = None
# used when self._results_by is set
# this way we minimize our knowledge of the profile
self._max_cluster_by_index = None
self._observed_checks = {}
@staticmethod
def _set_metadata(identity, item):
section, check, iterargs = identity
# If section is None this is the main doc.
# If check is None this is `section`
# otherwise this `check`
pass
[docs] def omit_loglevel(self, msg) -> bool:
"""Determine if message is below log level."""
return self.loglevels and (self.loglevels[0] > Status(msg))
def _register(self, event):
super()._register(event)
status, message, identity = event
section, check, iterargs = identity
key = self._get_key(identity)
# not item == True when item is empty
item = self._items.get(key, {})
if not item:
self._items[key] = item
# init
if status in (START, END) and not item:
item.update({"result": None, "sections": []})
if self._results_by:
# give the consumer a clue that/how the sections
# are structured differently.
item["clusteredBy"] = self._results_by
if status == SECTIONSUMMARY:
item.update({"key": key, "result": None, "checks": []})
if check:
item.update({"key": key, "result": None, "logs": []})
if self._results_by:
if self._results_by == "*check":
if check.id not in self._observed_checks:
self._observed_checks[check.id] = len(self._observed_checks)
index = self._observed_checks[check.id]
value = check.id
else:
index = dict(iterargs).get(self._results_by, None)
value = None
if self.runner:
value = self.runner.get_iterarg(self._results_by, index)
if index is not None:
if self._max_cluster_by_index is not None:
self._max_cluster_by_index = max(
index, self._max_cluster_by_index
)
else:
self._max_cluster_by_index = index
item["clustered"] = {
"name": self._results_by,
# 'index' is None if this check did not require self.results_by
"index": index,
}
if (
value
): # Not set if self.runner was not defined on initialization
item["clustered"]["value"] = value
self._set_metadata(identity, item)
if check:
item["description"] = check.description
if check.documentation:
item["documentation"] = check.documentation
if check.rationale:
item["rationale"] = check.rationale
if check.experimental:
item["experimental"] = check.experimental
if check.severity:
item["severity"] = check.severity
if item["key"][2] != ():
item["filename"] = self.runner.get_iterarg(*item["key"][2][0])
if status == END:
item["result"] = message # is a Counter
if status == SECTIONSUMMARY:
_, item["result"] = message # message[1] is a Counter
if status == ENDCHECK:
item["result"] = message.name # is a Status
if status >= DEBUG:
item["logs"].append(
{
"status": status.name,
"message": f"{message}",
"traceback": getattr(message, "traceback", None),
}
)
[docs] def getdoc(self):
if not self._ended:
raise Exception("Can't create doc before END status was recevived.")
if self._doc is not None:
return self._doc
doc = self._items[self._get_key((None, None, None))]
seen = set()
# this puts all in the original order
for identity in self._order:
key = self._get_key(identity)
section, _, _ = identity
sectionKey = self._get_key((section, None, None))
sectionDoc = self._items[sectionKey]
check = self._items[key]
if self._results_by:
if not sectionDoc["checks"]:
clusterlen = self._max_cluster_by_index + 1
if self._results_by != "*check":
# + 1 for rests bucket
clusterlen += 1
sectionDoc["checks"] = [[] for _ in range(clusterlen)]
index = check["clustered"]["index"]
if index is None:
# last element collects unclustered
index = -1
sectionDoc["checks"][index].append(
check
) # pytype: disable=attribute-error
else:
sectionDoc["checks"].append(check)
if sectionKey not in seen:
seen.add(sectionKey)
doc["sections"].append(sectionDoc)
self._doc = doc
return doc
[docs] def write(self):
import json
with open(self.output_file, "w", encoding="utf-8") as fh:
json.dump(self.getdoc(), fh, sort_keys=True, indent=4)
print(f'A report in JSON format has been saved to "{self.output_file}"')