Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # SPDX-License-Identifier: Apache-2.0 |
| 3 | """ |
| 4 | A tool for validating entity manager configurations. |
| 5 | """ |
| 6 | import argparse |
| 7 | import json |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 8 | import os |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 9 | import re |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 10 | import sys |
| 11 | |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 12 | import jsonschema.validators |
| 13 | |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 14 | DEFAULT_SCHEMA_FILENAME = "global.json" |
| 15 | |
| 16 | |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 17 | def remove_c_comments(string): |
| 18 | # first group captures quoted strings (double or single) |
| 19 | # second group captures comments (//single-line or /* multi-line */) |
| 20 | pattern = r"(\".*?(?<!\\)\"|\'.*?(?<!\\)\')|(/\*.*?\*/|//[^\r\n]*$)" |
| 21 | regex = re.compile(pattern, re.MULTILINE | re.DOTALL) |
| 22 | |
| 23 | def _replacer(match): |
| 24 | if match.group(2) is not None: |
| 25 | return "" |
| 26 | else: |
| 27 | return match.group(1) |
| 28 | |
| 29 | return regex.sub(_replacer, string) |
| 30 | |
| 31 | |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 32 | def main(): |
| 33 | parser = argparse.ArgumentParser( |
| 34 | description="Entity manager configuration validator", |
| 35 | ) |
| 36 | parser.add_argument( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 37 | "-s", |
| 38 | "--schema", |
| 39 | help=( |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 40 | "Use the specified schema file instead of the default " |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 41 | "(__file__/../../schemas/global.json)" |
| 42 | ), |
| 43 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 44 | parser.add_argument( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 45 | "-c", |
| 46 | "--config", |
| 47 | action="append", |
| 48 | help=( |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 49 | "Validate the specified configuration files (can be " |
| 50 | "specified more than once) instead of the default " |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 51 | "(__file__/../../configurations/**.json)" |
| 52 | ), |
| 53 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 54 | parser.add_argument( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 55 | "-e", |
| 56 | "--expected-fails", |
| 57 | help=( |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 58 | "A file with a list of configurations to ignore should " |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 59 | "they fail to validate" |
| 60 | ), |
| 61 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 62 | parser.add_argument( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 63 | "-k", |
| 64 | "--continue", |
| 65 | action="store_true", |
| 66 | help="keep validating after a failure", |
| 67 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 68 | parser.add_argument( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 69 | "-v", "--verbose", action="store_true", help="be noisy" |
| 70 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 71 | args = parser.parse_args() |
| 72 | |
| 73 | schema_file = args.schema |
| 74 | if schema_file is None: |
| 75 | try: |
| 76 | source_dir = os.path.realpath(__file__).split(os.sep)[:-2] |
| 77 | schema_file = os.sep + os.path.join( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 78 | *source_dir, "schemas", DEFAULT_SCHEMA_FILENAME |
| 79 | ) |
Patrick Williams | cad2d1f | 2022-12-04 14:38:16 -0600 | [diff] [blame] | 80 | except Exception: |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 81 | sys.stderr.write( |
| 82 | "Could not guess location of {}\n".format( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 83 | DEFAULT_SCHEMA_FILENAME |
| 84 | ) |
| 85 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 86 | sys.exit(2) |
| 87 | |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 88 | config_files = args.config or [] |
| 89 | if len(config_files) == 0: |
| 90 | try: |
| 91 | source_dir = os.path.realpath(__file__).split(os.sep)[:-2] |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 92 | configs_dir = os.sep + os.path.join(*source_dir, "configurations") |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 93 | data = os.walk(configs_dir) |
| 94 | for root, _, files in data: |
| 95 | for f in files: |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 96 | if f.endswith(".json"): |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 97 | config_files.append(os.path.join(root, f)) |
Patrick Williams | cad2d1f | 2022-12-04 14:38:16 -0600 | [diff] [blame] | 98 | except Exception: |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 99 | sys.stderr.write("Could not guess location of configurations\n") |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 100 | sys.exit(2) |
| 101 | |
| 102 | configs = [] |
| 103 | for config_file in config_files: |
| 104 | try: |
| 105 | with open(config_file) as fd: |
Potin Lai | 0f3a4d9 | 2023-12-05 00:13:55 +0800 | [diff] [blame] | 106 | configs.append(json.loads(remove_c_comments(fd.read()))) |
Patrick Williams | cad2d1f | 2022-12-04 14:38:16 -0600 | [diff] [blame] | 107 | except FileNotFoundError: |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 108 | sys.stderr.write( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 109 | "Could not parse config file '{}'\n".format(config_file) |
| 110 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 111 | sys.exit(2) |
| 112 | |
| 113 | expected_fails = [] |
| 114 | if args.expected_fails: |
| 115 | try: |
| 116 | with open(args.expected_fails) as fd: |
| 117 | for line in fd: |
| 118 | expected_fails.append(line.strip()) |
Patrick Williams | cad2d1f | 2022-12-04 14:38:16 -0600 | [diff] [blame] | 119 | except Exception: |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 120 | sys.stderr.write( |
Patrick Williams | fa8ee87 | 2022-12-07 07:00:42 -0600 | [diff] [blame] | 121 | "Could not read expected fails file '{}'\n".format( |
| 122 | args.expected_fails |
| 123 | ) |
| 124 | ) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 125 | sys.exit(2) |
| 126 | |
Alexander Hansen | 46072c4 | 2025-04-11 16:16:07 +0200 | [diff] [blame] | 127 | validator = validator_from_file(schema_file) |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 128 | |
| 129 | results = { |
| 130 | "invalid": [], |
| 131 | "unexpected_pass": [], |
| 132 | } |
| 133 | for config_file, config in zip(config_files, configs): |
Alexander Hansen | a47bdad | 2025-04-11 16:05:28 +0200 | [diff] [blame] | 134 | if not validate_single_config( |
| 135 | args, config_file, config, expected_fails, validator, results |
| 136 | ): |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 137 | break |
| 138 | |
| 139 | exit_status = 0 |
| 140 | if len(results["invalid"]) + len(results["unexpected_pass"]): |
| 141 | exit_status = 1 |
| 142 | unexpected_pass_suffix = " **" |
| 143 | show_suffix_explanation = False |
| 144 | print("results:") |
| 145 | for f in config_files: |
| 146 | if any([x in f for x in results["unexpected_pass"]]): |
| 147 | show_suffix_explanation = True |
| 148 | print(" '{}' passed!{}".format(f, unexpected_pass_suffix)) |
| 149 | if any([x in f for x in results["invalid"]]): |
| 150 | print(" '{}' failed!".format(f)) |
| 151 | |
| 152 | if show_suffix_explanation: |
| 153 | print("\n** configuration expected to fail") |
| 154 | |
| 155 | sys.exit(exit_status) |
| 156 | |
| 157 | |
Alexander Hansen | 46072c4 | 2025-04-11 16:16:07 +0200 | [diff] [blame] | 158 | def validator_from_file(schema_file): |
| 159 | |
| 160 | schema = {} |
| 161 | try: |
| 162 | with open(schema_file) as fd: |
| 163 | schema = json.load(fd) |
| 164 | except FileNotFoundError: |
| 165 | sys.stderr.write( |
| 166 | "Could not read schema file '{}'\n".format(schema_file) |
| 167 | ) |
| 168 | sys.exit(2) |
| 169 | |
| 170 | spec = jsonschema.Draft202012Validator |
| 171 | spec.check_schema(schema) |
| 172 | base_uri = "file://{}/".format( |
| 173 | os.path.split(os.path.realpath(schema_file))[0] |
| 174 | ) |
| 175 | resolver = jsonschema.RefResolver(base_uri, schema) |
| 176 | validator = spec(schema, resolver=resolver) |
| 177 | |
| 178 | return validator |
| 179 | |
| 180 | |
Alexander Hansen | a47bdad | 2025-04-11 16:05:28 +0200 | [diff] [blame] | 181 | def validate_single_config( |
| 182 | args, config_file, config, expected_fails, validator, results |
| 183 | ): |
| 184 | name = os.path.split(config_file)[1] |
| 185 | expect_fail = name in expected_fails |
| 186 | try: |
| 187 | validator.validate(config) |
| 188 | if expect_fail: |
| 189 | results["unexpected_pass"].append(name) |
| 190 | if not getattr(args, "continue"): |
| 191 | return False |
| 192 | except jsonschema.exceptions.ValidationError as e: |
| 193 | if not expect_fail: |
| 194 | results["invalid"].append(name) |
| 195 | if args.verbose: |
| 196 | print(e) |
| 197 | if expect_fail or getattr(args, "continue"): |
| 198 | return True |
| 199 | return False |
| 200 | return True |
| 201 | |
| 202 | |
Brad Bishop | c04b3f4 | 2020-05-01 08:17:59 -0400 | [diff] [blame] | 203 | if __name__ == "__main__": |
| 204 | main() |