blob: f09c80a8cca6a106f0d503064f55f890fd30d372 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Print one fully-calculated fw-testing-config to stdout.
This script takes one platform name (and optionally a model name) as inputs,
calculates the full config for that platform, and prints the results as
plaintext JSON.
The full config is calculated based on the fw-testing-configs inheritance
model. Each platform config has an optional "parent" attribute. For each
config field, if the platform does not explicitly set a value for that field,
then the value is instead inherited from the parent config. This inheritance
can be recursive: the parent may have a parent, and so on. At the top of the
inheritance tree (when parent is None), any remaining fields inherit their
values from DEFAULTS. Also, if the platform has a "models" attribute matching
the passed-in model name, then its field-value pairs override everything else.
"""
import argparse
import collections
import json
import os
import re
import sys
# Regexes matching fields which describe config metadata: that is, they do not
# contain actual config information. When calculating the final config, fields
# matching any of these regexes will be ignored.
META_FIELD_REGEXES = (re.compile(r"^models$"), re.compile(r"\.DOC$"))
class PlatformNotFoundError(AttributeError):
"""Error class for when the requested platform name is not found."""
class FieldNotFoundError(AttributeError):
"""Error class for when the requested field name is not found."""
def parse_args(argv):
"""Determine input dir and output file from command-line args.
Args:
argv: List of command-line args, excluding the invoked script.
Typically, this should be set to sys.argv[1:].
Returns:
An argparse.Namespace with the following attributes:
condense_output: A bool determining whether to remove pretty
whitespace from the script's final output.
consolidated: The filepath to CONSOLIDATED.json
field: If specified, then only this field's value will be printed.
platform: The name of the board whose config should be calculated
model: The name of the model for the board
Raises:
ValueError: If the passed-in platform name ends in '.json'
"""
parser = argparse.ArgumentParser()
parser.add_argument(
"platform",
help="The platform name to calculate a config for. "
'Should not include ".json" suffix.',
)
parser.add_argument(
"-m",
"--model",
default=None,
help="The model name of the board. If not specified, "
"then no model overrides will be used.",
)
parser.add_argument(
"-f",
"--field",
default=None,
help="If specified, only print this field's value.",
)
parser.add_argument(
"-c",
"--consolidated",
default="CONSOLIDATED.json",
help="The filepath to CONSOLIDATED.json",
)
parser.add_argument(
"--condense-output",
action="store_true",
help="Print the output without pretty whitespace.",
)
args = parser.parse_args(argv)
return args
def calculate_field_value(consolidated_json, field, platform, model=None):
"""Calculate a platform's ultimate value for a single field.
Args:
consolidated_json: The key-value contents of CONSOLIDATED.json.
field: The name of the JSON field to calculate.
platform: The name of the platform to calculate for. If the
platform does not define the field, then recursively check its
parent's config (or DEFAULTS).
model: The name of the model to check overrides for.
Raises:
PlatformNotFoundError: If platform is not in consolidated_json.
"""
if platform not in consolidated_json:
raise PlatformNotFoundError(platform)
# Model overrides are most important.
# Not all models have config overrides, so it's OK for the model name not
# to be present in models_json.
models_json = consolidated_json[platform].get("models", {})
if field in models_json.get(model, {}):
return models_json[model][field]
# Then check if the platform explicitly defines the value.
if field in consolidated_json[platform]:
return consolidated_json[platform][field]
# Finally, inherit from the parent (or DEFAULTS).
# The DEFAULTS config contains every field name, so this will terminate the
# recursion.
parent = consolidated_json[platform].get("parent", "DEFAULTS")
return calculate_field_value(consolidated_json, field, parent, model)
def load_consolidated_json(consolidated_fp):
"""Return the contents of consolidated_fp as a Python object."""
if not os.path.isfile(consolidated_fp):
raise FileNotFoundError(consolidated_fp)
with open(consolidated_fp, encoding="utf-8") as consolidated_file:
return json.load(consolidated_file)
def calculate_config(platform, model, consolidated_json):
"""Calculate a platform's ultimate config values for all fields.
Args:
platform: The name of the platform to calculate values for.
model: The name of the model to check for overrides.
consolidated_json: The full json dict for all platforms.
"""
final_json = collections.OrderedDict()
for field in consolidated_json["DEFAULTS"]:
if any(regex.search(field) for regex in META_FIELD_REGEXES):
continue
value = calculate_field_value(consolidated_json, field, platform, model)
final_json[field] = value
return final_json
def main(argv):
"""Parse command-line args and print a JSON config to stdout.
Args:
argv: List of command-line args, excluding the invoked script.
Typically, this should be set to sys.argv[1:].
"""
args = parse_args(argv)
consolidated_json = load_consolidated_json(args.consolidated)
config_json = calculate_config(args.platform, args.model, consolidated_json)
if args.field is None:
# indent=None means no newlines/indents in stringified JSON.
# indent=4 means 4-space indents.
indent = None if args.condense_output else 4
output = json.dumps(config_json, indent=indent)
elif args.field not in config_json:
raise FieldNotFoundError(args.field)
else:
output = config_json[args.field]
print(output, end="" if args.condense_output else "\n")
if __name__ == "__main__":
main(sys.argv[1:])