Greg Edelston | 9cd16f5 | 2020-11-12 10:50:28 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """Print one platform's full config data to stdout. |
| 7 | |
| 8 | This script takes one platform name (and optionally a model name) as inputs, |
| 9 | calculates the full config for that platform, and prints the results as |
| 10 | plaintext JSON. |
| 11 | |
| 12 | The full config is calculated based on the fw-testing-configs inheritance |
| 13 | model. Each platform config has an optional "parent" attribute. For each |
| 14 | config field, if the platform does not explicitly set a value for that field, |
| 15 | then the value is instead inherited from the parent config. This inheritance |
| 16 | can be recursive: the parent may have a parent, and so on. At the top of the |
| 17 | inheritance tree (when parent is None), any remaining fields inherit their |
| 18 | values from DEFAULTS. Also, if the platform has a "models" attribute matching |
| 19 | the passed-in model name, then its field-value pairs override everything else. |
| 20 | """ |
| 21 | |
| 22 | import argparse |
| 23 | import collections |
| 24 | import json |
| 25 | import os |
| 26 | import re |
| 27 | import sys |
| 28 | |
| 29 | |
| 30 | # Regexes matching fields which describe config metadata: that is, they do not |
| 31 | # contain actual config information. When calculating the final config, fields |
| 32 | # matching any of these regexes will be ignored. |
| 33 | META_FIELD_REGEXES = ( |
| 34 | re.compile(r'^models$'), |
| 35 | re.compile(r'\.DOC$') |
| 36 | ) |
| 37 | |
| 38 | |
| 39 | class PlatformNotFoundError(AttributeError): |
| 40 | """Error class for when the requested platform name is not found.""" |
| 41 | pass |
| 42 | |
| 43 | |
Greg Edelston | f70963a | 2020-11-17 14:36:06 -0700 | [diff] [blame^] | 44 | class FieldNotFoundError(AttributeError): |
| 45 | """Error class for when the requested field name is not found.""" |
| 46 | |
| 47 | |
Greg Edelston | 9cd16f5 | 2020-11-12 10:50:28 -0700 | [diff] [blame] | 48 | def parse_args(argv): |
| 49 | """Determine input dir and output file from command-line args. |
| 50 | |
| 51 | Args: |
| 52 | argv: List of command-line args, excluding the invoked script. |
| 53 | Typically, this should be set to sys.argv[1:]. |
| 54 | |
| 55 | Returns: |
| 56 | An argparse.Namespace with the following attributes: |
| 57 | condense_output: A bool determining whether to remove pretty |
| 58 | whitespace from the script's final output. |
| 59 | consolidated: The filepath to CONSOLIDATED.json |
Greg Edelston | f70963a | 2020-11-17 14:36:06 -0700 | [diff] [blame^] | 60 | field: If specified, then only this field's value will be printed. |
Greg Edelston | 9cd16f5 | 2020-11-12 10:50:28 -0700 | [diff] [blame] | 61 | platform: The name of the board whose config should be calculated |
| 62 | model: The name of the model for the board |
| 63 | |
| 64 | Raises: |
| 65 | ValueError: If the passed-in platform name ends in '.json' |
| 66 | """ |
| 67 | parser = argparse.ArgumentParser() |
| 68 | parser.add_argument('platform', |
| 69 | help='The platform name to calculate a config for. ' |
| 70 | 'Should not include ".json" suffix.') |
| 71 | parser.add_argument('-m', '--model', default=None, |
| 72 | help='The model name of the board. If not specified, ' |
| 73 | 'then no model overrides will be used.') |
Greg Edelston | f70963a | 2020-11-17 14:36:06 -0700 | [diff] [blame^] | 74 | parser.add_argument('-f', '--field', default=None, |
| 75 | help="If specified, only print this field's value.") |
Greg Edelston | 9cd16f5 | 2020-11-12 10:50:28 -0700 | [diff] [blame] | 76 | parser.add_argument('-c', '--consolidated', default='CONSOLIDATED.json', |
| 77 | help='The filepath to CONSOLIDATED.json') |
| 78 | parser.add_argument('--condense-output', action='store_true', |
| 79 | help='Print the output without pretty whitespace.') |
| 80 | args = parser.parse_args(argv) |
| 81 | return args |
| 82 | |
| 83 | |
| 84 | def calculate_field_value(consolidated_json, field, platform, model=None): |
| 85 | """Calculate a platform's ultimate value for a single field. |
| 86 | |
| 87 | Args: |
| 88 | consolidated_json: The key-value contents of CONSOLIDATED.json. |
| 89 | field: The name of the JSON field to calculate. |
| 90 | platform: The name of the platform to calculate for. If the |
| 91 | platform does not define the field, then recursively check its |
| 92 | parent's config (or DEFAULTS). |
| 93 | model: The name of the model to check overrides for. |
| 94 | |
| 95 | Raises: |
| 96 | PlatformNotFoundError: If platform is not in consolidated_json. |
| 97 | """ |
| 98 | if platform not in consolidated_json: |
| 99 | raise PlatformNotFoundError(platform) |
| 100 | |
| 101 | # Model overrides are most important. |
| 102 | # Not all models have config overrides, so it's OK for the model name not |
| 103 | # to be present in models_json. |
| 104 | models_json = consolidated_json[platform].get('models', {}) |
| 105 | if field in models_json.get(model, {}): |
| 106 | return models_json[model][field] |
| 107 | |
| 108 | # Then check if the platform explicitly defines the value. |
| 109 | if field in consolidated_json[platform]: |
| 110 | return consolidated_json[platform][field] |
| 111 | |
| 112 | # Finally, inherit from the parent (or DEFAULTS). |
| 113 | # The DEFAULTS config contains every field name, so this will terminate the |
| 114 | # recursion. |
| 115 | parent = consolidated_json[platform].get('parent', 'DEFAULTS') |
| 116 | return calculate_field_value(consolidated_json, field, parent, model) |
| 117 | |
| 118 | |
| 119 | def calculate_platform_json(platform, model, consolidated_fp): |
| 120 | """Calculate a platform's ultimate config values for all fields. |
| 121 | |
| 122 | Args: |
| 123 | platform: The name of the platform to calculate values for. |
| 124 | model: The name of the model to check for overrides. |
| 125 | """ |
| 126 | if not os.path.isfile(consolidated_fp): |
| 127 | raise FileNotFoundError(consolidated_fp) |
| 128 | with open(consolidated_fp) as consolidated_file: |
| 129 | consolidated_json = json.load(consolidated_file) |
| 130 | final_json = collections.OrderedDict() |
| 131 | for field in consolidated_json['DEFAULTS']: |
| 132 | if any(regex.search(field) for regex in META_FIELD_REGEXES): |
| 133 | continue |
| 134 | value = calculate_field_value(consolidated_json, field, platform, model) |
| 135 | final_json[field] = value |
| 136 | return final_json |
| 137 | |
| 138 | |
| 139 | def main(argv): |
| 140 | """ |
| 141 | Parse command-line args and print a JSON config to stdout. |
| 142 | |
| 143 | Args: |
| 144 | argv: List of command-line args, excluding the invoked script. |
| 145 | Typically, this should be set to sys.argv[1:]. |
| 146 | """ |
| 147 | args = parse_args(argv) |
Greg Edelston | f70963a | 2020-11-17 14:36:06 -0700 | [diff] [blame^] | 148 | config_json = calculate_platform_json(args.platform, args.model, |
| 149 | args.consolidated) |
| 150 | if args.field is None: |
| 151 | # indent=None means no newlines/indents in stringified JSON. |
| 152 | # indent=4 means 4-space indents. |
| 153 | indent = None if args.condense_output else 4 |
| 154 | output = json.dumps(config_json, indent=indent) |
| 155 | elif args.field not in config_json: |
| 156 | raise FieldNotFoundError(args.field) |
Greg Edelston | 9cd16f5 | 2020-11-12 10:50:28 -0700 | [diff] [blame] | 157 | else: |
Greg Edelston | f70963a | 2020-11-17 14:36:06 -0700 | [diff] [blame^] | 158 | output = config_json[args.field] |
| 159 | print(output, end='' if args.condense_output else '\n') |
Greg Edelston | 9cd16f5 | 2020-11-12 10:50:28 -0700 | [diff] [blame] | 160 | |
| 161 | |
| 162 | if __name__ == '__main__': |
| 163 | main(sys.argv[1:]) |