blob: ae5fb19cc8a5f22651f8f911903502091506b886 [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
88 schema = {}
89 try:
90 with open(schema_file) as fd:
91 schema = json.load(fd)
Patrick Williamscad2d1f2022-12-04 14:38:16 -060092 except FileNotFoundError:
Brad Bishopc04b3f42020-05-01 08:17:59 -040093 sys.stderr.write(
Patrick Williamsfa8ee872022-12-07 07:00:42 -060094 "Could not read schema file '{}'\n".format(schema_file)
95 )
Brad Bishopc04b3f42020-05-01 08:17:59 -040096 sys.exit(2)
97
98 config_files = args.config or []
99 if len(config_files) == 0:
100 try:
101 source_dir = os.path.realpath(__file__).split(os.sep)[:-2]
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600102 configs_dir = os.sep + os.path.join(*source_dir, "configurations")
Brad Bishopc04b3f42020-05-01 08:17:59 -0400103 data = os.walk(configs_dir)
104 for root, _, files in data:
105 for f in files:
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600106 if f.endswith(".json"):
Brad Bishopc04b3f42020-05-01 08:17:59 -0400107 config_files.append(os.path.join(root, f))
Patrick Williamscad2d1f2022-12-04 14:38:16 -0600108 except Exception:
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600109 sys.stderr.write("Could not guess location of configurations\n")
Brad Bishopc04b3f42020-05-01 08:17:59 -0400110 sys.exit(2)
111
112 configs = []
113 for config_file in config_files:
114 try:
115 with open(config_file) as fd:
Potin Lai0f3a4d92023-12-05 00:13:55 +0800116 configs.append(json.loads(remove_c_comments(fd.read())))
Patrick Williamscad2d1f2022-12-04 14:38:16 -0600117 except FileNotFoundError:
Brad Bishopc04b3f42020-05-01 08:17:59 -0400118 sys.stderr.write(
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600119 "Could not parse config file '{}'\n".format(config_file)
120 )
Brad Bishopc04b3f42020-05-01 08:17:59 -0400121 sys.exit(2)
122
123 expected_fails = []
124 if args.expected_fails:
125 try:
126 with open(args.expected_fails) as fd:
127 for line in fd:
128 expected_fails.append(line.strip())
Patrick Williamscad2d1f2022-12-04 14:38:16 -0600129 except Exception:
Brad Bishopc04b3f42020-05-01 08:17:59 -0400130 sys.stderr.write(
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600131 "Could not read expected fails file '{}'\n".format(
132 args.expected_fails
133 )
134 )
Brad Bishopc04b3f42020-05-01 08:17:59 -0400135 sys.exit(2)
136
Patrick Williams4f93bb82024-09-04 12:40:25 -0400137 spec = jsonschema.Draft202012Validator
Andrew Jeffery183fab92024-03-20 11:52:34 +1030138 spec.check_schema(schema)
Brad Bishopc04b3f42020-05-01 08:17:59 -0400139 base_uri = "file://{}/".format(
Patrick Williamsfa8ee872022-12-07 07:00:42 -0600140 os.path.split(os.path.realpath(schema_file))[0]
141 )
Brad Bishopc04b3f42020-05-01 08:17:59 -0400142 resolver = jsonschema.RefResolver(base_uri, schema)
Andrew Jeffery183fab92024-03-20 11:52:34 +1030143 validator = spec(schema, resolver=resolver)
Brad Bishopc04b3f42020-05-01 08:17:59 -0400144
145 results = {
146 "invalid": [],
147 "unexpected_pass": [],
148 }
149 for config_file, config in zip(config_files, configs):
150 name = os.path.split(config_file)[1]
151 expect_fail = name in expected_fails
152 try:
153 validator.validate(config)
154 if expect_fail:
155 results["unexpected_pass"].append(name)
156 if not getattr(args, "continue"):
157 break
158 except jsonschema.exceptions.ValidationError as e:
159 if not expect_fail:
160 results["invalid"].append(name)
161 if args.verbose:
162 print(e)
163 if expect_fail or getattr(args, "continue"):
164 continue
165 break
166
167 exit_status = 0
168 if len(results["invalid"]) + len(results["unexpected_pass"]):
169 exit_status = 1
170 unexpected_pass_suffix = " **"
171 show_suffix_explanation = False
172 print("results:")
173 for f in config_files:
174 if any([x in f for x in results["unexpected_pass"]]):
175 show_suffix_explanation = True
176 print(" '{}' passed!{}".format(f, unexpected_pass_suffix))
177 if any([x in f for x in results["invalid"]]):
178 print(" '{}' failed!".format(f))
179
180 if show_suffix_explanation:
181 print("\n** configuration expected to fail")
182
183 sys.exit(exit_status)
184
185
186if __name__ == "__main__":
187 main()