OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 Google Inc. All rights reserved. | 2 # Copyright (c) 2011 Google Inc. All rights reserved. |
3 # | 3 # |
4 # Redistribution and use in source and binary forms, with or without | 4 # Redistribution and use in source and binary forms, with or without |
5 # modification, are permitted provided that the following conditions are | 5 # modification, are permitted provided that the following conditions are |
6 # met: | 6 # met: |
7 # | 7 # |
8 # * Redistributions of source code must retain the above copyright | 8 # * Redistributions of source code must retain the above copyright |
9 # notice, this list of conditions and the following disclaimer. | 9 # notice, this list of conditions and the following disclaimer. |
10 # * Redistributions in binary form must reproduce the above | 10 # * Redistributions in binary form must reproduce the above |
(...skipping 27 matching lines...) Expand all Loading... |
38 # - Required response parameter was removed or changed to optional | 38 # - Required response parameter was removed or changed to optional |
39 # - Event has been removed | 39 # - Event has been removed |
40 # - Required event parameter was removed or changed to optional | 40 # - Required event parameter was removed or changed to optional |
41 # - Parameter type has changed. | 41 # - Parameter type has changed. |
42 # | 42 # |
43 # For the parameters with composite types the above checks are also applied | 43 # For the parameters with composite types the above checks are also applied |
44 # recursively to every property of the type. | 44 # recursively to every property of the type. |
45 # | 45 # |
46 # Adding --show_changes to the command line prints out a list of valid public AP
I changes. | 46 # Adding --show_changes to the command line prints out a list of valid public AP
I changes. |
47 | 47 |
| 48 import collections |
| 49 import copy |
48 import os.path | 50 import os.path |
| 51 import optparse |
49 import re | 52 import re |
50 import sys | 53 import sys |
51 | 54 |
| 55 try: |
| 56 import json |
| 57 except ImportError: |
| 58 import simplejson as json |
| 59 |
52 def list_to_map(items, key): | 60 def list_to_map(items, key): |
53 result = {} | 61 result = {} |
54 for item in items: | 62 for item in items: |
55 if not "hidden" in item: | 63 if not "hidden" in item: |
56 result[item[key]] = item | 64 result[item[key]] = item |
57 return result | 65 return result |
58 | 66 |
| 67 |
59 def named_list_to_map(container, name, key): | 68 def named_list_to_map(container, name, key): |
60 if name in container: | 69 if name in container: |
61 return list_to_map(container[name], key) | 70 return list_to_map(container[name], key) |
62 return {} | 71 return {} |
63 | 72 |
| 73 |
64 def removed(reverse): | 74 def removed(reverse): |
65 if reverse: | 75 if reverse: |
66 return "added" | 76 return "added" |
67 return "removed" | 77 return "removed" |
68 | 78 |
| 79 |
69 def required(reverse): | 80 def required(reverse): |
70 if reverse: | 81 if reverse: |
71 return "optional" | 82 return "optional" |
72 return "required" | 83 return "required" |
73 | 84 |
74 def compare_schemas(schema_1, schema_2, reverse): | 85 |
| 86 def compare_schemas(d_1, d_2, reverse): |
75 errors = [] | 87 errors = [] |
76 types_1 = normalize_types_in_schema(schema_1) | 88 domains_1 = copy.deepcopy(d_1) |
77 types_2 = normalize_types_in_schema(schema_2) | 89 domains_2 = copy.deepcopy(d_2) |
| 90 types_1 = normalize_types_in_schema(domains_1) |
| 91 types_2 = normalize_types_in_schema(domains_2) |
78 | 92 |
79 domains_by_name_1 = list_to_map(schema_1, "domain") | 93 domains_by_name_1 = list_to_map(domains_1, "domain") |
80 domains_by_name_2 = list_to_map(schema_2, "domain") | 94 domains_by_name_2 = list_to_map(domains_2, "domain") |
81 | 95 |
82 for name in domains_by_name_1: | 96 for name in domains_by_name_1: |
83 domain_1 = domains_by_name_1[name] | 97 domain_1 = domains_by_name_1[name] |
84 if not name in domains_by_name_2: | 98 if not name in domains_by_name_2: |
85 errors.append("%s: domain has been %s" % (name, removed(reverse))) | 99 errors.append("%s: domain has been %s" % (name, removed(reverse))) |
86 continue | 100 continue |
87 compare_domains(domain_1, domains_by_name_2[name], types_1, types_2, err
ors, reverse) | 101 compare_domains(domain_1, domains_by_name_2[name], types_1, types_2, err
ors, reverse) |
88 return errors | 102 return errors |
89 | 103 |
| 104 |
90 def compare_domains(domain_1, domain_2, types_map_1, types_map_2, errors, revers
e): | 105 def compare_domains(domain_1, domain_2, types_map_1, types_map_2, errors, revers
e): |
91 domain_name = domain_1["domain"] | 106 domain_name = domain_1["domain"] |
92 commands_1 = named_list_to_map(domain_1, "commands", "name") | 107 commands_1 = named_list_to_map(domain_1, "commands", "name") |
93 commands_2 = named_list_to_map(domain_2, "commands", "name") | 108 commands_2 = named_list_to_map(domain_2, "commands", "name") |
94 for name in commands_1: | 109 for name in commands_1: |
95 command_1 = commands_1[name] | 110 command_1 = commands_1[name] |
96 if not name in commands_2: | 111 if not name in commands_2: |
97 errors.append("%s.%s: command has been %s" % (domain_1["domain"], na
me, removed(reverse))) | 112 errors.append("%s.%s: command has been %s" % (domain_1["domain"], na
me, removed(reverse))) |
98 continue | 113 continue |
99 compare_commands(domain_name, command_1, commands_2[name], types_map_1,
types_map_2, errors, reverse) | 114 compare_commands(domain_name, command_1, commands_2[name], types_map_1,
types_map_2, errors, reverse) |
100 | 115 |
101 events_1 = named_list_to_map(domain_1, "events", "name") | 116 events_1 = named_list_to_map(domain_1, "events", "name") |
102 events_2 = named_list_to_map(domain_2, "events", "name") | 117 events_2 = named_list_to_map(domain_2, "events", "name") |
103 for name in events_1: | 118 for name in events_1: |
104 event_1 = events_1[name] | 119 event_1 = events_1[name] |
105 if not name in events_2: | 120 if not name in events_2: |
106 errors.append("%s.%s: event has been %s" % (domain_1["domain"], name
, removed(reverse))) | 121 errors.append("%s.%s: event has been %s" % (domain_1["domain"], name
, removed(reverse))) |
107 continue | 122 continue |
108 compare_events(domain_name, event_1, events_2[name], types_map_1, types_
map_2, errors, reverse) | 123 compare_events(domain_name, event_1, events_2[name], types_map_1, types_
map_2, errors, reverse) |
109 | 124 |
| 125 |
110 def compare_commands(domain_name, command_1, command_2, types_map_1, types_map_2
, errors, reverse): | 126 def compare_commands(domain_name, command_1, command_2, types_map_1, types_map_2
, errors, reverse): |
111 context = domain_name + "." + command_1["name"] | 127 context = domain_name + "." + command_1["name"] |
112 | 128 |
113 params_1 = named_list_to_map(command_1, "parameters", "name") | 129 params_1 = named_list_to_map(command_1, "parameters", "name") |
114 params_2 = named_list_to_map(command_2, "parameters", "name") | 130 params_2 = named_list_to_map(command_2, "parameters", "name") |
115 # Note the reversed order: we allow removing but forbid adding parameters. | 131 # Note the reversed order: we allow removing but forbid adding parameters. |
116 compare_params_list(context, "parameter", params_2, params_1, types_map_2, t
ypes_map_1, 0, errors, not reverse) | 132 compare_params_list(context, "parameter", params_2, params_1, types_map_2, t
ypes_map_1, 0, errors, not reverse) |
117 | 133 |
118 returns_1 = named_list_to_map(command_1, "returns", "name") | 134 returns_1 = named_list_to_map(command_1, "returns", "name") |
119 returns_2 = named_list_to_map(command_2, "returns", "name") | 135 returns_2 = named_list_to_map(command_2, "returns", "name") |
120 compare_params_list(context, "response parameter", returns_1, returns_2, typ
es_map_1, types_map_2, 0, errors, reverse) | 136 compare_params_list(context, "response parameter", returns_1, returns_2, typ
es_map_1, types_map_2, 0, errors, reverse) |
121 | 137 |
| 138 |
122 def compare_events(domain_name, event_1, event_2, types_map_1, types_map_2, erro
rs, reverse): | 139 def compare_events(domain_name, event_1, event_2, types_map_1, types_map_2, erro
rs, reverse): |
123 context = domain_name + "." + event_1["name"] | 140 context = domain_name + "." + event_1["name"] |
124 params_1 = named_list_to_map(event_1, "parameters", "name") | 141 params_1 = named_list_to_map(event_1, "parameters", "name") |
125 params_2 = named_list_to_map(event_2, "parameters", "name") | 142 params_2 = named_list_to_map(event_2, "parameters", "name") |
126 compare_params_list(context, "parameter", params_1, params_2, types_map_1, t
ypes_map_2, 0, errors, reverse) | 143 compare_params_list(context, "parameter", params_1, params_2, types_map_1, t
ypes_map_2, 0, errors, reverse) |
127 | 144 |
| 145 |
128 def compare_params_list(context, kind, params_1, params_2, types_map_1, types_ma
p_2, depth, errors, reverse): | 146 def compare_params_list(context, kind, params_1, params_2, types_map_1, types_ma
p_2, depth, errors, reverse): |
129 for name in params_1: | 147 for name in params_1: |
130 param_1 = params_1[name] | 148 param_1 = params_1[name] |
131 if not name in params_2: | 149 if not name in params_2: |
132 if not "optional" in param_1: | 150 if not "optional" in param_1: |
133 errors.append("%s.%s: required %s has been %s" % (context, name,
kind, removed(reverse))) | 151 errors.append("%s.%s: required %s has been %s" % (context, name,
kind, removed(reverse))) |
134 continue | 152 continue |
135 | 153 |
136 param_2 = params_2[name] | 154 param_2 = params_2[name] |
137 if param_2 and "optional" in param_2 and not "optional" in param_1: | 155 if param_2 and "optional" in param_2 and not "optional" in param_1: |
138 errors.append("%s.%s: %s %s is now %s" % (context, name, required(re
verse), kind, required(not reverse))) | 156 errors.append("%s.%s: %s %s is now %s" % (context, name, required(re
verse), kind, required(not reverse))) |
139 continue | 157 continue |
140 type_1 = extract_type(param_1, types_map_1, errors) | 158 type_1 = extract_type(param_1, types_map_1, errors) |
141 type_2 = extract_type(param_2, types_map_2, errors) | 159 type_2 = extract_type(param_2, types_map_2, errors) |
142 compare_types(context + "." + name, kind, type_1, type_2, types_map_1, t
ypes_map_2, depth, errors, reverse) | 160 compare_types(context + "." + name, kind, type_1, type_2, types_map_1, t
ypes_map_2, depth, errors, reverse) |
143 | 161 |
| 162 |
144 def compare_types(context, kind, type_1, type_2, types_map_1, types_map_2, depth
, errors, reverse): | 163 def compare_types(context, kind, type_1, type_2, types_map_1, types_map_2, depth
, errors, reverse): |
145 if depth > 10: | 164 if depth > 10: |
146 return | 165 return |
147 | 166 |
148 base_type_1 = type_1["type"] | 167 base_type_1 = type_1["type"] |
149 base_type_2 = type_2["type"] | 168 base_type_2 = type_2["type"] |
150 | 169 |
151 if base_type_1 != base_type_2: | 170 if base_type_1 != base_type_2: |
152 errors.append("%s: %s base type mismatch, '%s' vs '%s'" % (context, kind
, base_type_1, base_type_2)) | 171 errors.append("%s: %s base type mismatch, '%s' vs '%s'" % (context, kind
, base_type_1, base_type_2)) |
153 elif base_type_1 == "object": | 172 elif base_type_1 == "object": |
154 params_1 = named_list_to_map(type_1, "properties", "name") | 173 params_1 = named_list_to_map(type_1, "properties", "name") |
155 params_2 = named_list_to_map(type_2, "properties", "name") | 174 params_2 = named_list_to_map(type_2, "properties", "name") |
156 # If both parameters have the same named type use it in the context. | 175 # If both parameters have the same named type use it in the context. |
157 if "id" in type_1 and "id" in type_2 and type_1["id"] == type_2["id"]: | 176 if "id" in type_1 and "id" in type_2 and type_1["id"] == type_2["id"]: |
158 type_name = type_1["id"] | 177 type_name = type_1["id"] |
159 else: | 178 else: |
160 type_name = "<object>" | 179 type_name = "<object>" |
161 context += " %s->%s" % (kind, type_name) | 180 context += " %s->%s" % (kind, type_name) |
162 compare_params_list(context, "property", params_1, params_2, types_map_1
, types_map_2, depth + 1, errors, reverse) | 181 compare_params_list(context, "property", params_1, params_2, types_map_1
, types_map_2, depth + 1, errors, reverse) |
163 elif base_type_1 == "array": | 182 elif base_type_1 == "array": |
164 item_type_1 = extract_type(type_1["items"], types_map_1, errors) | 183 item_type_1 = extract_type(type_1["items"], types_map_1, errors) |
165 item_type_2 = extract_type(type_2["items"], types_map_2, errors) | 184 item_type_2 = extract_type(type_2["items"], types_map_2, errors) |
166 compare_types(context, kind, item_type_1, item_type_2, types_map_1, type
s_map_2, depth + 1, errors, reverse) | 185 compare_types(context, kind, item_type_1, item_type_2, types_map_1, type
s_map_2, depth + 1, errors, reverse) |
167 | 186 |
| 187 |
168 def extract_type(typed_object, types_map, errors): | 188 def extract_type(typed_object, types_map, errors): |
169 if "type" in typed_object: | 189 if "type" in typed_object: |
170 result = { "id": "<transient>", "type": typed_object["type"] } | 190 result = { "id": "<transient>", "type": typed_object["type"] } |
171 if typed_object["type"] == "object": | 191 if typed_object["type"] == "object": |
172 result["properties"] = [] | 192 result["properties"] = [] |
173 elif typed_object["type"] == "array": | 193 elif typed_object["type"] == "array": |
174 result["items"] = typed_object["items"] | 194 result["items"] = typed_object["items"] |
175 return result | 195 return result |
176 elif "$ref" in typed_object: | 196 elif "$ref" in typed_object: |
177 ref = typed_object["$ref"] | 197 ref = typed_object["$ref"] |
178 if not ref in types_map: | 198 if not ref in types_map: |
179 errors.append("Can not resolve type: %s" % ref) | 199 errors.append("Can not resolve type: %s" % ref) |
180 types_map[ref] = { "id": "<transient>", "type": "object" } | 200 types_map[ref] = { "id": "<transient>", "type": "object" } |
181 return types_map[ref] | 201 return types_map[ref] |
182 | 202 |
183 def normalize_types_in_schema(schema): | 203 |
| 204 def normalize_types_in_schema(domains): |
184 types = {} | 205 types = {} |
185 for domain in schema: | 206 for domain in domains: |
186 domain_name = domain["domain"] | 207 domain_name = domain["domain"] |
187 normalize_types(domain, domain_name, types) | 208 normalize_types(domain, domain_name, types) |
188 return types | 209 return types |
189 | 210 |
| 211 |
190 def normalize_types(obj, domain_name, types): | 212 def normalize_types(obj, domain_name, types): |
191 if isinstance(obj, list): | 213 if isinstance(obj, list): |
192 for item in obj: | 214 for item in obj: |
193 normalize_types(item, domain_name, types) | 215 normalize_types(item, domain_name, types) |
194 elif isinstance(obj, dict): | 216 elif isinstance(obj, dict): |
195 for key, value in obj.items(): | 217 for key, value in obj.items(): |
196 if key == "$ref" and value.find(".") == -1: | 218 if key == "$ref" and value.find(".") == -1: |
197 obj[key] = "%s.%s" % (domain_name, value) | 219 obj[key] = "%s.%s" % (domain_name, value) |
198 elif key == "id": | 220 elif key == "id": |
199 obj[key] = "%s.%s" % (domain_name, value) | 221 obj[key] = "%s.%s" % (domain_name, value) |
200 types[obj[key]] = obj | 222 types[obj[key]] = obj |
201 else: | 223 else: |
202 normalize_types(value, domain_name, types) | 224 normalize_types(value, domain_name, types) |
203 | 225 |
204 def load_json(filename): | 226 |
205 input_file = open(filename, "r") | 227 def load_schema(file, domains): |
| 228 if not os.path.isfile(file): |
| 229 return |
| 230 input_file = open(file, "r") |
206 json_string = input_file.read() | 231 json_string = input_file.read() |
207 json_string = re.sub(":\s*true", ": True", json_string) | 232 parsed_json = json.loads(json_string, object_pairs_hook=collections.OrderedD
ict) |
208 json_string = re.sub(":\s*false", ": False", json_string) | 233 domains += parsed_json["domains"] |
209 return eval(json_string) | 234 return parsed_json["version"] |
| 235 |
210 | 236 |
211 def self_test(): | 237 def self_test(): |
212 def create_test_schema_1(): | 238 def create_test_schema_1(): |
213 return [ | 239 return [ |
214 { | 240 { |
215 "domain": "Network", | 241 "domain": "Network", |
216 "types": [ | 242 "types": [ |
217 { | 243 { |
218 "id": "LoaderId", | 244 "id": "LoaderId", |
219 "type": "string" | 245 "type": "string" |
(...skipping 165 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
385 def errors_match(expected, actual): | 411 def errors_match(expected, actual): |
386 return (is_subset(actual, expected, "Unexpected") and | 412 return (is_subset(actual, expected, "Unexpected") and |
387 is_subset(expected, actual, "Missing")) | 413 is_subset(expected, actual, "Missing")) |
388 | 414 |
389 return (errors_match(expected_errors, | 415 return (errors_match(expected_errors, |
390 compare_schemas(create_test_schema_1(), create_test_sch
ema_2(), False)) and | 416 compare_schemas(create_test_schema_1(), create_test_sch
ema_2(), False)) and |
391 errors_match(expected_errors_reverse, | 417 errors_match(expected_errors_reverse, |
392 compare_schemas(create_test_schema_2(), create_test_sch
ema_1(), True))) | 418 compare_schemas(create_test_schema_2(), create_test_sch
ema_1(), True))) |
393 | 419 |
394 | 420 |
| 421 |
| 422 def load_domains_and_baselines(file, domains, baseline_domains): |
| 423 version = load_schema(os.path.normpath(file), domains) |
| 424 suffix = "-%s.%s.json" % (version["major"], version["minor"]) |
| 425 baseline_file = file.replace(".json", suffix) |
| 426 load_schema(os.path.normpath(baseline_file), baseline_domains) |
| 427 return version |
| 428 |
| 429 |
395 def main(): | 430 def main(): |
396 if not self_test(): | 431 if not self_test(): |
397 sys.stderr.write("Self-test failed") | 432 sys.stderr.write("Self-test failed") |
398 return 1 | 433 return 1 |
399 | 434 |
400 if len(sys.argv) < 4 or sys.argv[1] != "-o": | 435 cmdline_parser = optparse.OptionParser() |
401 sys.stderr.write("Usage: %s -o OUTPUT_FILE INPUT_FILE [--show-changes]\n
" % sys.argv[0]) | 436 cmdline_parser.add_option("--show_changes") |
| 437 cmdline_parser.add_option("--o") |
| 438 arg_options, arg_values = cmdline_parser.parse_args() |
| 439 |
| 440 if len(arg_values) < 1 or not arg_options.o: |
| 441 sys.stderr.write("Usage: %s --o OUTPUT_FILE [--show_changes] PROTOCOL_FO
LDER1 ?PROTOCOL_FOLDER2 \n" % sys.argv[0]) |
402 return 1 | 442 return 1 |
403 | 443 |
404 output_path = sys.argv[2] | 444 output_path = arg_options.o |
405 output_file = open(output_path, "w") | 445 output_file = open(output_path, "w") |
406 | 446 |
407 input_path = sys.argv[3] | 447 domains = [] |
408 dir_name = os.path.dirname(input_path) | 448 baseline_domains = [] |
409 schema = load_json(input_path) | 449 version = load_domains_and_baselines(arg_values[0], domains, baseline_domain
s) |
410 | 450 if len(arg_values) > 1: |
411 major = schema["version"]["major"] | 451 load_domains_and_baselines(arg_values[1], domains, baseline_domains) |
412 minor = schema["version"]["minor"] | |
413 version = "%s.%s" % (major, minor) | |
414 if len(dir_name) == 0: | |
415 dir_name = "." | |
416 baseline_path = os.path.normpath(dir_name + "/Inspector-" + version + ".json
") | |
417 baseline_schema = load_json(baseline_path) | |
418 | 452 |
419 expected_errors = [ | 453 expected_errors = [ |
420 "Debugger.globalObjectCleared: event has been removed" | 454 "Debugger.globalObjectCleared: event has been removed" |
421 ] | 455 ] |
422 | 456 |
423 errors = compare_schemas(baseline_schema["domains"], schema["domains"], Fals
e) | 457 errors = compare_schemas(baseline_domains, domains, False) |
424 unexpected_errors = [] | 458 unexpected_errors = [] |
425 for i in range(len(errors)): | 459 for i in range(len(errors)): |
426 if errors[i] not in expected_errors: | 460 if errors[i] not in expected_errors: |
427 unexpected_errors.append(errors[i]) | 461 unexpected_errors.append(errors[i]) |
428 if len(unexpected_errors) > 0: | 462 if len(unexpected_errors) > 0: |
429 sys.stderr.write(" Compatibility with %s: FAILED\n" % version) | 463 sys.stderr.write(" Compatibility checks FAILED\n") |
430 for error in unexpected_errors: | 464 for error in unexpected_errors: |
431 sys.stderr.write( " %s\n" % error) | 465 sys.stderr.write( " %s\n" % error) |
432 return 1 | 466 return 1 |
433 | 467 |
434 if len(sys.argv) > 4 and sys.argv[4] == "--show-changes": | 468 if arg_options.show_changes: |
435 changes = compare_schemas( | 469 changes = compare_schemas(domains, baseline_domains, True) |
436 load_json(input_path)["domains"], load_json(baseline_path)["domains"
], True) | |
437 if len(changes) > 0: | 470 if len(changes) > 0: |
438 print " Public changes since %s:" % version | 471 print " Public changes since %s:" % version |
439 for change in changes: | 472 for change in changes: |
440 print " %s" % change | 473 print " %s" % change |
441 | 474 |
442 output_file.write(""" | 475 json.dump({"version": version, "domains": domains}, output_file, indent=4, s
ort_keys=False, separators=(',', ': ')) |
443 #ifndef InspectorProtocolVersion_h | |
444 #define InspectorProtocolVersion_h | |
445 | |
446 #include "platform/inspector_protocol/String16.h" | |
447 | |
448 namespace blink { | |
449 | |
450 String inspectorProtocolVersion() { return "%s"; } | |
451 | |
452 int inspectorProtocolVersionMajor() { return %s; } | |
453 | |
454 int inspectorProtocolVersionMinor() { return %s; } | |
455 | |
456 } | |
457 | |
458 #endif // !defined(InspectorProtocolVersion_h) | |
459 """ % (version, major, minor)) | |
460 | |
461 output_file.close() | 476 output_file.close() |
462 | 477 |
463 if __name__ == '__main__': | 478 if __name__ == '__main__': |
464 sys.exit(main()) | 479 sys.exit(main()) |
OLD | NEW |