OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2011 Google Inc. All rights reserved. |
| 3 # |
| 4 # Redistribution and use in source and binary forms, with or without |
| 5 # modification, are permitted provided that the following conditions are |
| 6 # met: |
| 7 # |
| 8 # * Redistributions of source code must retain the above copyright |
| 9 # notice, this list of conditions and the following disclaimer. |
| 10 # * Redistributions in binary form must reproduce the above |
| 11 # copyright notice, this list of conditions and the following disclaimer |
| 12 # in the documentation and/or other materials provided with the |
| 13 # distribution. |
| 14 # * Neither the name of Google Inc. nor the names of its |
| 15 # contributors may be used to endorse or promote products derived from |
| 16 # this software without specific prior written permission. |
| 17 # |
| 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 # |
| 30 # Inspector protocol validator. |
| 31 # |
| 32 # Tests that subsequent protocol changes are not breaking backwards compatibilit
y. |
| 33 # Following violations are reported: |
| 34 # |
| 35 # - Domain has been removed |
| 36 # - Command has been removed |
| 37 # - Required command parameter was added or changed from optional |
| 38 # - Required response parameter was removed or changed to optional |
| 39 # - Event has been removed |
| 40 # - Required event parameter was removed or changed to optional |
| 41 # - Parameter type has changed. |
| 42 # |
| 43 # For the parameters with composite types the above checks are also applied |
| 44 # recursively to every property of the type. |
| 45 # |
| 46 # Adding --show_changes to the command line prints out a list of valid public AP
I changes. |
| 47 |
| 48 import os.path |
| 49 import re |
| 50 import sys |
| 51 |
| 52 def list_to_map(items, key): |
| 53 result = {} |
| 54 for item in items: |
| 55 if not "hidden" in item: |
| 56 result[item[key]] = item |
| 57 return result |
| 58 |
| 59 def named_list_to_map(container, name, key): |
| 60 if name in container: |
| 61 return list_to_map(container[name], key) |
| 62 return {} |
| 63 |
| 64 def removed(reverse): |
| 65 if reverse: |
| 66 return "added" |
| 67 return "removed" |
| 68 |
| 69 def required(reverse): |
| 70 if reverse: |
| 71 return "optional" |
| 72 return "required" |
| 73 |
| 74 def compare_schemas(schema_1, schema_2, reverse): |
| 75 errors = [] |
| 76 types_1 = normalize_types_in_schema(schema_1) |
| 77 types_2 = normalize_types_in_schema(schema_2) |
| 78 |
| 79 domains_by_name_1 = list_to_map(schema_1, "domain") |
| 80 domains_by_name_2 = list_to_map(schema_2, "domain") |
| 81 |
| 82 for name in domains_by_name_1: |
| 83 domain_1 = domains_by_name_1[name] |
| 84 if not name in domains_by_name_2: |
| 85 errors.append("%s: domain has been %s" % (name, removed(reverse))) |
| 86 continue |
| 87 compare_domains(domain_1, domains_by_name_2[name], types_1, types_2, err
ors, reverse) |
| 88 return errors |
| 89 |
| 90 def compare_domains(domain_1, domain_2, types_map_1, types_map_2, errors, revers
e): |
| 91 domain_name = domain_1["domain"] |
| 92 commands_1 = named_list_to_map(domain_1, "commands", "name") |
| 93 commands_2 = named_list_to_map(domain_2, "commands", "name") |
| 94 for name in commands_1: |
| 95 command_1 = commands_1[name] |
| 96 if not name in commands_2: |
| 97 errors.append("%s.%s: command has been %s" % (domain_1["domain"], na
me, removed(reverse))) |
| 98 continue |
| 99 compare_commands(domain_name, command_1, commands_2[name], types_map_1,
types_map_2, errors, reverse) |
| 100 |
| 101 events_1 = named_list_to_map(domain_1, "events", "name") |
| 102 events_2 = named_list_to_map(domain_2, "events", "name") |
| 103 for name in events_1: |
| 104 event_1 = events_1[name] |
| 105 if not name in events_2: |
| 106 errors.append("%s.%s: event has been %s" % (domain_1["domain"], name
, removed(reverse))) |
| 107 continue |
| 108 compare_events(domain_name, event_1, events_2[name], types_map_1, types_
map_2, errors, reverse) |
| 109 |
| 110 def compare_commands(domain_name, command_1, command_2, types_map_1, types_map_2
, errors, reverse): |
| 111 context = domain_name + "." + command_1["name"] |
| 112 |
| 113 params_1 = named_list_to_map(command_1, "parameters", "name") |
| 114 params_2 = named_list_to_map(command_2, "parameters", "name") |
| 115 # 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) |
| 117 |
| 118 returns_1 = named_list_to_map(command_1, "returns", "name") |
| 119 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) |
| 121 |
| 122 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"] |
| 124 params_1 = named_list_to_map(event_1, "parameters", "name") |
| 125 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) |
| 127 |
| 128 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: |
| 130 param_1 = params_1[name] |
| 131 if not name in params_2: |
| 132 if not "optional" in param_1: |
| 133 errors.append("%s.%s: required %s has been %s" % (context, name,
kind, removed(reverse))) |
| 134 continue |
| 135 |
| 136 param_2 = params_2[name] |
| 137 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))) |
| 139 continue |
| 140 type_1 = extract_type(param_1, types_map_1, errors) |
| 141 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) |
| 143 |
| 144 def compare_types(context, kind, type_1, type_2, types_map_1, types_map_2, depth
, errors, reverse): |
| 145 if depth > 10: |
| 146 return |
| 147 |
| 148 base_type_1 = type_1["type"] |
| 149 base_type_2 = type_2["type"] |
| 150 |
| 151 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)) |
| 153 elif base_type_1 == "object": |
| 154 params_1 = named_list_to_map(type_1, "properties", "name") |
| 155 params_2 = named_list_to_map(type_2, "properties", "name") |
| 156 # 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"]: |
| 158 type_name = type_1["id"] |
| 159 else: |
| 160 type_name = "<object>" |
| 161 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) |
| 163 elif base_type_1 == "array": |
| 164 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) |
| 166 compare_types(context, kind, item_type_1, item_type_2, types_map_1, type
s_map_2, depth + 1, errors, reverse) |
| 167 |
| 168 def extract_type(typed_object, types_map, errors): |
| 169 if "type" in typed_object: |
| 170 result = { "id": "<transient>", "type": typed_object["type"] } |
| 171 if typed_object["type"] == "object": |
| 172 result["properties"] = [] |
| 173 elif typed_object["type"] == "array": |
| 174 result["items"] = typed_object["items"] |
| 175 return result |
| 176 elif "$ref" in typed_object: |
| 177 ref = typed_object["$ref"] |
| 178 if not ref in types_map: |
| 179 errors.append("Can not resolve type: %s" % ref) |
| 180 types_map[ref] = { "id": "<transient>", "type": "object" } |
| 181 return types_map[ref] |
| 182 |
| 183 def normalize_types_in_schema(schema): |
| 184 types = {} |
| 185 for domain in schema: |
| 186 domain_name = domain["domain"] |
| 187 normalize_types(domain, domain_name, types) |
| 188 return types |
| 189 |
| 190 def normalize_types(obj, domain_name, types): |
| 191 if isinstance(obj, list): |
| 192 for item in obj: |
| 193 normalize_types(item, domain_name, types) |
| 194 elif isinstance(obj, dict): |
| 195 for key, value in obj.items(): |
| 196 if key == "$ref" and value.find(".") == -1: |
| 197 obj[key] = "%s.%s" % (domain_name, value) |
| 198 elif key == "id": |
| 199 obj[key] = "%s.%s" % (domain_name, value) |
| 200 types[obj[key]] = obj |
| 201 else: |
| 202 normalize_types(value, domain_name, types) |
| 203 |
| 204 def load_json(filename): |
| 205 input_file = open(filename, "r") |
| 206 json_string = input_file.read() |
| 207 json_string = re.sub(":\s*true", ": True", json_string) |
| 208 json_string = re.sub(":\s*false", ": False", json_string) |
| 209 return eval(json_string) |
| 210 |
| 211 def self_test(): |
| 212 def create_test_schema_1(): |
| 213 return [ |
| 214 { |
| 215 "domain": "Network", |
| 216 "types": [ |
| 217 { |
| 218 "id": "LoaderId", |
| 219 "type": "string" |
| 220 }, |
| 221 { |
| 222 "id": "Headers", |
| 223 "type": "object" |
| 224 }, |
| 225 { |
| 226 "id": "Request", |
| 227 "type": "object", |
| 228 "properties": [ |
| 229 { "name": "url", "type": "string" }, |
| 230 { "name": "method", "type": "string" }, |
| 231 { "name": "headers", "$ref": "Headers" }, |
| 232 { "name": "becameOptionalField", "type": "string" }, |
| 233 { "name": "removedField", "type": "string" }, |
| 234 ] |
| 235 } |
| 236 ], |
| 237 "commands": [ |
| 238 { |
| 239 "name": "removedCommand", |
| 240 }, |
| 241 { |
| 242 "name": "setExtraHTTPHeaders", |
| 243 "parameters": [ |
| 244 { "name": "headers", "$ref": "Headers" }, |
| 245 { "name": "mismatched", "type": "string" }, |
| 246 { "name": "becameOptional", "$ref": "Headers" }, |
| 247 { "name": "removedRequired", "$ref": "Headers" }, |
| 248 { "name": "becameRequired", "$ref": "Headers", "optional
": True }, |
| 249 { "name": "removedOptional", "$ref": "Headers", "optiona
l": True }, |
| 250 ], |
| 251 "returns": [ |
| 252 { "name": "mimeType", "type": "string" }, |
| 253 { "name": "becameOptional", "type": "string" }, |
| 254 { "name": "removedRequired", "type": "string" }, |
| 255 { "name": "becameRequired", "type": "string", "optional"
: True }, |
| 256 { "name": "removedOptional", "type": "string", "optional
": True }, |
| 257 ] |
| 258 } |
| 259 ], |
| 260 "events": [ |
| 261 { |
| 262 "name": "requestWillBeSent", |
| 263 "parameters": [ |
| 264 { "name": "frameId", "type": "string", "hidden": True }, |
| 265 { "name": "request", "$ref": "Request" }, |
| 266 { "name": "becameOptional", "type": "string" }, |
| 267 { "name": "removedRequired", "type": "string" }, |
| 268 { "name": "becameRequired", "type": "string", "optional"
: True }, |
| 269 { "name": "removedOptional", "type": "string", "optional
": True }, |
| 270 ] |
| 271 }, |
| 272 { |
| 273 "name": "removedEvent", |
| 274 "parameters": [ |
| 275 { "name": "errorText", "type": "string" }, |
| 276 { "name": "canceled", "type": "boolean", "optional": Tru
e } |
| 277 ] |
| 278 } |
| 279 ] |
| 280 }, |
| 281 { |
| 282 "domain": "removedDomain" |
| 283 } |
| 284 ] |
| 285 |
| 286 def create_test_schema_2(): |
| 287 return [ |
| 288 { |
| 289 "domain": "Network", |
| 290 "types": [ |
| 291 { |
| 292 "id": "LoaderId", |
| 293 "type": "string" |
| 294 }, |
| 295 { |
| 296 "id": "Request", |
| 297 "type": "object", |
| 298 "properties": [ |
| 299 { "name": "url", "type": "string" }, |
| 300 { "name": "method", "type": "string" }, |
| 301 { "name": "headers", "type": "object" }, |
| 302 { "name": "becameOptionalField", "type": "string", "opti
onal": True }, |
| 303 ] |
| 304 } |
| 305 ], |
| 306 "commands": [ |
| 307 { |
| 308 "name": "addedCommand", |
| 309 }, |
| 310 { |
| 311 "name": "setExtraHTTPHeaders", |
| 312 "parameters": [ |
| 313 { "name": "headers", "type": "object" }, |
| 314 { "name": "mismatched", "type": "object" }, |
| 315 { "name": "becameOptional", "type": "object" , "optional
": True }, |
| 316 { "name": "addedRequired", "type": "object" }, |
| 317 { "name": "becameRequired", "type": "object" }, |
| 318 { "name": "addedOptional", "type": "object", "optional":
True }, |
| 319 ], |
| 320 "returns": [ |
| 321 { "name": "mimeType", "type": "string" }, |
| 322 { "name": "becameOptional", "type": "string", "optional"
: True }, |
| 323 { "name": "addedRequired", "type": "string"}, |
| 324 { "name": "becameRequired", "type": "string" }, |
| 325 { "name": "addedOptional", "type": "string", "optional":
True }, |
| 326 ] |
| 327 } |
| 328 ], |
| 329 "events": [ |
| 330 { |
| 331 "name": "requestWillBeSent", |
| 332 "parameters": [ |
| 333 { "name": "request", "$ref": "Request" }, |
| 334 { "name": "becameOptional", "type": "string", "optional"
: True }, |
| 335 { "name": "addedRequired", "type": "string"}, |
| 336 { "name": "becameRequired", "type": "string" }, |
| 337 { "name": "addedOptional", "type": "string", "optional":
True }, |
| 338 ] |
| 339 }, |
| 340 { |
| 341 "name": "addedEvent" |
| 342 } |
| 343 ] |
| 344 }, |
| 345 { |
| 346 "domain": "addedDomain" |
| 347 } |
| 348 ] |
| 349 |
| 350 expected_errors = [ |
| 351 "removedDomain: domain has been removed", |
| 352 "Network.removedCommand: command has been removed", |
| 353 "Network.removedEvent: event has been removed", |
| 354 "Network.setExtraHTTPHeaders.mismatched: parameter base type mismatch, '
object' vs 'string'", |
| 355 "Network.setExtraHTTPHeaders.addedRequired: required parameter has been
added", |
| 356 "Network.setExtraHTTPHeaders.becameRequired: optional parameter is now r
equired", |
| 357 "Network.setExtraHTTPHeaders.removedRequired: required response paramete
r has been removed", |
| 358 "Network.setExtraHTTPHeaders.becameOptional: required response parameter
is now optional", |
| 359 "Network.requestWillBeSent.removedRequired: required parameter has been
removed", |
| 360 "Network.requestWillBeSent.becameOptional: required parameter is now opt
ional", |
| 361 "Network.requestWillBeSent.request parameter->Network.Request.removedFie
ld: required property has been removed", |
| 362 "Network.requestWillBeSent.request parameter->Network.Request.becameOpti
onalField: required property is now optional", |
| 363 ] |
| 364 |
| 365 expected_errors_reverse = [ |
| 366 "addedDomain: domain has been added", |
| 367 "Network.addedEvent: event has been added", |
| 368 "Network.addedCommand: command has been added", |
| 369 "Network.setExtraHTTPHeaders.mismatched: parameter base type mismatch, 's
tring' vs 'object'", |
| 370 "Network.setExtraHTTPHeaders.removedRequired: required parameter has been
removed", |
| 371 "Network.setExtraHTTPHeaders.becameOptional: required parameter is now op
tional", |
| 372 "Network.setExtraHTTPHeaders.addedRequired: required response parameter h
as been added", |
| 373 "Network.setExtraHTTPHeaders.becameRequired: optional response parameter
is now required", |
| 374 "Network.requestWillBeSent.becameRequired: optional parameter is now requ
ired", |
| 375 "Network.requestWillBeSent.addedRequired: required parameter has been add
ed", |
| 376 ] |
| 377 |
| 378 def is_subset(subset, superset, message): |
| 379 for i in range(len(subset)): |
| 380 if subset[i] not in superset: |
| 381 sys.stderr.write("%s error: %s\n" % (message, subset[i])) |
| 382 return False |
| 383 return True |
| 384 |
| 385 def errors_match(expected, actual): |
| 386 return (is_subset(actual, expected, "Unexpected") and |
| 387 is_subset(expected, actual, "Missing")) |
| 388 |
| 389 return (errors_match(expected_errors, |
| 390 compare_schemas(create_test_schema_1(), create_test_sch
ema_2(), False)) and |
| 391 errors_match(expected_errors_reverse, |
| 392 compare_schemas(create_test_schema_2(), create_test_sch
ema_1(), True))) |
| 393 |
| 394 |
| 395 def main(): |
| 396 if not self_test(): |
| 397 sys.stderr.write("Self-test failed") |
| 398 return 1 |
| 399 |
| 400 if len(sys.argv) < 4 or sys.argv[1] != "-o": |
| 401 sys.stderr.write("Usage: %s -o OUTPUT_FILE INPUT_FILE [--show-changes]\n
" % sys.argv[0]) |
| 402 return 1 |
| 403 |
| 404 output_path = sys.argv[2] |
| 405 output_file = open(output_path, "w") |
| 406 |
| 407 input_path = sys.argv[3] |
| 408 dir_name = os.path.dirname(input_path) |
| 409 schema = load_json(input_path) |
| 410 |
| 411 major = schema["version"]["major"] |
| 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 |
| 419 errors = compare_schemas(baseline_schema["domains"], schema["domains"], Fals
e) |
| 420 if len(errors) > 0: |
| 421 sys.stderr.write(" Compatibility with %s: FAILED\n" % version) |
| 422 for error in errors: |
| 423 sys.stderr.write( " %s\n" % error) |
| 424 return 1 |
| 425 |
| 426 if len(sys.argv) > 4 and sys.argv[4] == "--show-changes": |
| 427 changes = compare_schemas( |
| 428 load_json(input_path)["domains"], load_json(baseline_path)["domains"
], True) |
| 429 if len(changes) > 0: |
| 430 print " Public changes since %s:" % version |
| 431 for change in changes: |
| 432 print " %s" % change |
| 433 |
| 434 output_file.write(""" |
| 435 #ifndef InspectorProtocolVersion_h |
| 436 #define InspectorProtocolVersion_h |
| 437 |
| 438 #include "wtf/Vector.h" |
| 439 #include "wtf/text/WTFString.h" |
| 440 |
| 441 namespace blink { |
| 442 |
| 443 String inspectorProtocolVersion() { return "%s"; } |
| 444 |
| 445 int inspectorProtocolVersionMajor() { return %s; } |
| 446 |
| 447 int inspectorProtocolVersionMinor() { return %s; } |
| 448 |
| 449 bool supportsInspectorProtocolVersion(const String& version) |
| 450 { |
| 451 Vector<String> tokens; |
| 452 version.split(".", tokens); |
| 453 if (tokens.size() != 2) |
| 454 return false; |
| 455 |
| 456 bool ok = true; |
| 457 int major = tokens[0].toInt(&ok); |
| 458 if (!ok || major != %s) |
| 459 return false; |
| 460 |
| 461 int minor = tokens[1].toInt(&ok); |
| 462 if (!ok || minor > %s) |
| 463 return false; |
| 464 |
| 465 return true; |
| 466 } |
| 467 |
| 468 } |
| 469 |
| 470 #endif // !defined(InspectorProtocolVersion_h) |
| 471 """ % (version, major, minor, major, minor)) |
| 472 |
| 473 output_file.close() |
| 474 |
| 475 if __name__ == '__main__': |
| 476 sys.exit(main()) |
OLD | NEW |