blob: ce3cb72dd38cc47c190626a08909ca5eeedf3116 [file] [log] [blame]
Brad Bishopc04b3f42020-05-01 08:17:59 -04001#!/usr/bin/env python3
2# SPDX-License-Identifier: Apache-2.0
3"""
4A tool for validating entity manager configurations.
5"""
6import argparse
7import json
Brad Bishopc04b3f42020-05-01 08:17:59 -04008import os
Potin Lai0f3a4d92023-12-05 00:13:55 +08009import re
Brad Bishopc04b3f42020-05-01 08:17:59 -040010import sys
11
Patrick Williamsfa8ee872022-12-07 07:00:42 -060012import jsonschema.validators
13
Brad Bishopc04b3f42020-05-01 08:17:59 -040014DEFAULT_SCHEMA_FILENAME = "global.json"
15
16
Potin Lai0f3a4d92023-12-05 00:13:55 +080017def 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 Bishopc04b3f42020-05-01 08:17:59 -040032def main():
33 parser = argparse.ArgumentParser(
34 description="Entity manager configuration validator",
35 )
36 parser.add_argument(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060037 "-s",
38 "--schema",
39 help=(
Brad Bishopc04b3f42020-05-01 08:17:59 -040040 "Use the specified schema file instead of the default "
Patrick Williamsfa8ee872022-12-07 07:00:42 -060041 "(__file__/../../schemas/global.json)"
42 ),
43 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040044 parser.add_argument(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060045 "-c",
46 "--config",
47 action="append",
48 help=(
Brad Bishopc04b3f42020-05-01 08:17:59 -040049 "Validate the specified configuration files (can be "
50 "specified more than once) instead of the default "
Patrick Williamsfa8ee872022-12-07 07:00:42 -060051 "(__file__/../../configurations/**.json)"
52 ),
53 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040054 parser.add_argument(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060055 "-e",
56 "--expected-fails",
57 help=(
Brad Bishopc04b3f42020-05-01 08:17:59 -040058 "A file with a list of configurations to ignore should "
Patrick Williamsfa8ee872022-12-07 07:00:42 -060059 "they fail to validate"
60 ),
61 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040062 parser.add_argument(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060063 "-k",
64 "--continue",
65 action="store_true",
66 help="keep validating after a failure",
67 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040068 parser.add_argument(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060069 "-v", "--verbose", action="store_true", help="be noisy"
70 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040071 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 Williamsfa8ee872022-12-07 07:00:42 -060078 *source_dir, "schemas", DEFAULT_SCHEMA_FILENAME
79 )
Patrick Williamscad2d1f2022-12-04 14:38:16 -060080 except Exception:
Brad Bishopc04b3f42020-05-01 08:17:59 -040081 sys.stderr.write(
82 "Could not guess location of {}\n".format(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060083 DEFAULT_SCHEMA_FILENAME
84 )
85 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040086 sys.exit(2)
87
Brad Bishopc04b3f42020-05-01 08:17:59 -040088 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 Williamsfa8ee872022-12-07 07:00:42 -060092 configs_dir = os.sep + os.path.join(*source_dir, "configurations")
Brad Bishopc04b3f42020-05-01 08:17:59 -040093 data = os.walk(configs_dir)
94 for root, _, files in data:
95 for f in files:
Patrick Williamsfa8ee872022-12-07 07:00:42 -060096 if f.endswith(".json"):
Brad Bishopc04b3f42020-05-01 08:17:59 -040097 config_files.append(os.path.join(root, f))
Patrick Williamscad2d1f2022-12-04 14:38:16 -060098 except Exception:
Patrick Williamsfa8ee872022-12-07 07:00:42 -060099 sys.stderr.write("Could not guess location of configurations\n")
Brad Bishopc04b3f42020-05-01 08:17:59 -0400100 sys.exit(2)
101
102 configs = []
103 for config_file in config_files:
104 try:
105 with open(config_file) as fd:
Potin Lai0f3a4d92023-12-05 00:13:55 +0800106 configs.append(json.loads(remove_c_comments(fd.read())))
Patrick Williamscad2d1f2022-12-04 14:38:16 -0600107 except FileNotFoundError:
Brad Bishopc04b3f42020-05-01 08:17:59 -0400108 sys.stderr.write(
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600109 "Could not parse config file '{}'\n".format(config_file)
110 )
Brad Bishopc04b3f42020-05-01 08:17:59 -0400111 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 Williamscad2d1f2022-12-04 14:38:16 -0600119 except Exception:
Brad Bishopc04b3f42020-05-01 08:17:59 -0400120 sys.stderr.write(
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600121 "Could not read expected fails file '{}'\n".format(
122 args.expected_fails
123 )
124 )
Brad Bishopc04b3f42020-05-01 08:17:59 -0400125 sys.exit(2)
126
Alexander Hansen46072c42025-04-11 16:16:07 +0200127 validator = validator_from_file(schema_file)
Brad Bishopc04b3f42020-05-01 08:17:59 -0400128
129 results = {
130 "invalid": [],
131 "unexpected_pass": [],
132 }
133 for config_file, config in zip(config_files, configs):
Alexander Hansena47bdad2025-04-11 16:05:28 +0200134 if not validate_single_config(
135 args, config_file, config, expected_fails, validator, results
136 ):
Brad Bishopc04b3f42020-05-01 08:17:59 -0400137 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 Hansen46072c42025-04-11 16:16:07 +0200158def 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 Hansena47bdad2025-04-11 16:05:28 +0200181def 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 Bishopc04b3f42020-05-01 08:17:59 -0400203if __name__ == "__main__":
204 main()