generate-inspector-protocol-version 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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 compatibility.
  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 API changes.
  47. import os.path
  48. import re
  49. import sys
  50. def list_to_map(items, key):
  51. result = {}
  52. for item in items:
  53. if not "hidden" in item:
  54. result[item[key]] = item
  55. return result
  56. def named_list_to_map(container, name, key):
  57. if name in container:
  58. return list_to_map(container[name], key)
  59. return {}
  60. def removed(reverse):
  61. if reverse:
  62. return "added"
  63. return "removed"
  64. def required(reverse):
  65. if reverse:
  66. return "optional"
  67. return "required"
  68. def compare_schemas(schema_1, schema_2, reverse):
  69. errors = []
  70. types_1 = normalize_types_in_schema(schema_1)
  71. types_2 = normalize_types_in_schema(schema_2)
  72. domains_by_name_1 = list_to_map(schema_1, "domain")
  73. domains_by_name_2 = list_to_map(schema_2, "domain")
  74. for name in domains_by_name_1:
  75. domain_1 = domains_by_name_1[name]
  76. if not name in domains_by_name_2:
  77. errors.append("%s: domain has been %s" % (name, removed(reverse)))
  78. continue
  79. compare_domains(domain_1, domains_by_name_2[name], types_1, types_2, errors, reverse)
  80. return errors
  81. def compare_domains(domain_1, domain_2, types_map_1, types_map_2, errors, reverse):
  82. domain_name = domain_1["domain"]
  83. commands_1 = named_list_to_map(domain_1, "commands", "name")
  84. commands_2 = named_list_to_map(domain_2, "commands", "name")
  85. for name in commands_1:
  86. command_1 = commands_1[name]
  87. if not name in commands_2:
  88. errors.append("%s.%s: command has been %s" % (domain_1["domain"], name, removed(reverse)))
  89. continue
  90. compare_commands(domain_name, command_1, commands_2[name], types_map_1, types_map_2, errors, reverse)
  91. events_1 = named_list_to_map(domain_1, "events", "name")
  92. events_2 = named_list_to_map(domain_2, "events", "name")
  93. for name in events_1:
  94. event_1 = events_1[name]
  95. if not name in events_2:
  96. errors.append("%s.%s: event has been %s" % (domain_1["domain"], name, removed(reverse)))
  97. continue
  98. compare_events(domain_name, event_1, events_2[name], types_map_1, types_map_2, errors, reverse)
  99. def compare_commands(domain_name, command_1, command_2, types_map_1, types_map_2, errors, reverse):
  100. context = domain_name + "." + command_1["name"]
  101. params_1 = named_list_to_map(command_1, "parameters", "name")
  102. params_2 = named_list_to_map(command_2, "parameters", "name")
  103. # Note the reversed order: we allow removing but forbid adding parameters.
  104. compare_params_list(context, "parameter", params_2, params_1, types_map_2, types_map_1, 0, errors, not reverse)
  105. returns_1 = named_list_to_map(command_1, "returns", "name")
  106. returns_2 = named_list_to_map(command_2, "returns", "name")
  107. compare_params_list(context, "response parameter", returns_1, returns_2, types_map_1, types_map_2, 0, errors, reverse)
  108. def compare_events(domain_name, event_1, event_2, types_map_1, types_map_2, errors, reverse):
  109. context = domain_name + "." + event_1["name"]
  110. params_1 = named_list_to_map(event_1, "parameters", "name")
  111. params_2 = named_list_to_map(event_2, "parameters", "name")
  112. compare_params_list(context, "parameter", params_1, params_2, types_map_1, types_map_2, 0, errors, reverse)
  113. def compare_params_list(context, kind, params_1, params_2, types_map_1, types_map_2, depth, errors, reverse):
  114. for name in params_1:
  115. param_1 = params_1[name]
  116. if not name in params_2:
  117. if not "optional" in param_1:
  118. errors.append("%s.%s: required %s has been %s" % (context, name, kind, removed(reverse)))
  119. continue
  120. param_2 = params_2[name]
  121. if param_2 and "optional" in param_2 and not "optional" in param_1:
  122. errors.append("%s.%s: %s %s is now %s" % (context, name, required(reverse), kind, required(not reverse)))
  123. continue
  124. type_1 = extract_type(param_1, types_map_1, errors)
  125. type_2 = extract_type(param_2, types_map_2, errors)
  126. compare_types(context + "." + name, kind, type_1, type_2, types_map_1, types_map_2, depth, errors, reverse)
  127. def compare_types(context, kind, type_1, type_2, types_map_1, types_map_2, depth, errors, reverse):
  128. if depth > 10:
  129. return
  130. base_type_1 = type_1["type"]
  131. base_type_2 = type_2["type"]
  132. if base_type_1 != base_type_2:
  133. errors.append("%s: %s base type mismatch, '%s' vs '%s'" % (context, kind, base_type_1, base_type_2))
  134. elif base_type_1 == "object":
  135. params_1 = named_list_to_map(type_1, "properties", "name")
  136. params_2 = named_list_to_map(type_2, "properties", "name")
  137. # If both parameters have the same named type use it in the context.
  138. if "id" in type_1 and "id" in type_2 and type_1["id"] == type_2["id"]:
  139. type_name = type_1["id"]
  140. else:
  141. type_name = "<object>"
  142. context += " %s->%s" % (kind, type_name)
  143. compare_params_list(context, "property", params_1, params_2, types_map_1, types_map_2, depth + 1, errors, reverse)
  144. elif base_type_1 == "array":
  145. item_type_1 = extract_type(type_1["items"], types_map_1, errors)
  146. item_type_2 = extract_type(type_2["items"], types_map_2, errors)
  147. compare_types(context, kind, item_type_1, item_type_2, types_map_1, types_map_2, depth + 1, errors, reverse)
  148. def extract_type(typed_object, types_map, errors):
  149. if "type" in typed_object:
  150. result = { "id": "<transient>", "type": typed_object["type"] }
  151. if typed_object["type"] == "object":
  152. result["properties"] = []
  153. elif typed_object["type"] == "array":
  154. result["items"] = typed_object["items"]
  155. return result
  156. elif "$ref" in typed_object:
  157. ref = typed_object["$ref"]
  158. if not ref in types_map:
  159. errors.append("Can not resolve type: %s" % ref)
  160. types_map[ref] = { "id": "<transient>", "type": "object" }
  161. return types_map[ref]
  162. def normalize_types_in_schema(schema):
  163. types = {}
  164. for domain in schema:
  165. domain_name = domain["domain"]
  166. normalize_types(domain, domain_name, types)
  167. return types
  168. def normalize_types(obj, domain_name, types):
  169. if isinstance(obj, list):
  170. for item in obj:
  171. normalize_types(item, domain_name, types)
  172. elif isinstance(obj, dict):
  173. for key, value in obj.items():
  174. if key == "$ref" and value.find(".") == -1:
  175. obj[key] = "%s.%s" % (domain_name, value)
  176. elif key == "id":
  177. obj[key] = "%s.%s" % (domain_name, value)
  178. types[obj[key]] = obj
  179. else:
  180. normalize_types(value, domain_name, types)
  181. def load_json(filename):
  182. input_file = open(filename, "r")
  183. json_string = input_file.read()
  184. json_string = re.sub(":\s*true", ": True", json_string)
  185. json_string = re.sub(":\s*false", ": False", json_string)
  186. return eval(json_string)
  187. def self_test():
  188. def create_test_schema_1():
  189. return [
  190. {
  191. "domain": "Network",
  192. "types": [
  193. {
  194. "id": "LoaderId",
  195. "type": "string"
  196. },
  197. {
  198. "id": "Headers",
  199. "type": "object"
  200. },
  201. {
  202. "id": "Request",
  203. "type": "object",
  204. "properties": [
  205. { "name": "url", "type": "string" },
  206. { "name": "method", "type": "string" },
  207. { "name": "headers", "$ref": "Headers" },
  208. { "name": "becameOptionalField", "type": "string" },
  209. { "name": "removedField", "type": "string" },
  210. ]
  211. }
  212. ],
  213. "commands": [
  214. {
  215. "name": "removedCommand",
  216. },
  217. {
  218. "name": "setExtraHTTPHeaders",
  219. "parameters": [
  220. { "name": "headers", "$ref": "Headers" },
  221. { "name": "mismatched", "type": "string" },
  222. { "name": "becameOptional", "$ref": "Headers" },
  223. { "name": "removedRequired", "$ref": "Headers" },
  224. { "name": "becameRequired", "$ref": "Headers", "optional": True },
  225. { "name": "removedOptional", "$ref": "Headers", "optional": True },
  226. ],
  227. "returns": [
  228. { "name": "mimeType", "type": "string" },
  229. { "name": "becameOptional", "type": "string" },
  230. { "name": "removedRequired", "type": "string" },
  231. { "name": "becameRequired", "type": "string", "optional": True },
  232. { "name": "removedOptional", "type": "string", "optional": True },
  233. ]
  234. }
  235. ],
  236. "events": [
  237. {
  238. "name": "requestWillBeSent",
  239. "parameters": [
  240. { "name": "frameId", "type": "string", "hidden": True },
  241. { "name": "request", "$ref": "Request" },
  242. { "name": "becameOptional", "type": "string" },
  243. { "name": "removedRequired", "type": "string" },
  244. { "name": "becameRequired", "type": "string", "optional": True },
  245. { "name": "removedOptional", "type": "string", "optional": True },
  246. ]
  247. },
  248. {
  249. "name": "removedEvent",
  250. "parameters": [
  251. { "name": "errorText", "type": "string" },
  252. { "name": "canceled", "type": "boolean", "optional": True }
  253. ]
  254. }
  255. ]
  256. },
  257. {
  258. "domain": "removedDomain"
  259. }
  260. ]
  261. def create_test_schema_2():
  262. return [
  263. {
  264. "domain": "Network",
  265. "types": [
  266. {
  267. "id": "LoaderId",
  268. "type": "string"
  269. },
  270. {
  271. "id": "Request",
  272. "type": "object",
  273. "properties": [
  274. { "name": "url", "type": "string" },
  275. { "name": "method", "type": "string" },
  276. { "name": "headers", "type": "object" },
  277. { "name": "becameOptionalField", "type": "string", "optional": True },
  278. ]
  279. }
  280. ],
  281. "commands": [
  282. {
  283. "name": "addedCommand",
  284. },
  285. {
  286. "name": "setExtraHTTPHeaders",
  287. "parameters": [
  288. { "name": "headers", "type": "object" },
  289. { "name": "mismatched", "type": "object" },
  290. { "name": "becameOptional", "type": "object" , "optional": True },
  291. { "name": "addedRequired", "type": "object" },
  292. { "name": "becameRequired", "type": "object" },
  293. { "name": "addedOptional", "type": "object", "optional": True },
  294. ],
  295. "returns": [
  296. { "name": "mimeType", "type": "string" },
  297. { "name": "becameOptional", "type": "string", "optional": True },
  298. { "name": "addedRequired", "type": "string"},
  299. { "name": "becameRequired", "type": "string" },
  300. { "name": "addedOptional", "type": "string", "optional": True },
  301. ]
  302. }
  303. ],
  304. "events": [
  305. {
  306. "name": "requestWillBeSent",
  307. "parameters": [
  308. { "name": "request", "$ref": "Request" },
  309. { "name": "becameOptional", "type": "string", "optional": True },
  310. { "name": "addedRequired", "type": "string"},
  311. { "name": "becameRequired", "type": "string" },
  312. { "name": "addedOptional", "type": "string", "optional": True },
  313. ]
  314. },
  315. {
  316. "name": "addedEvent"
  317. }
  318. ]
  319. },
  320. {
  321. "domain": "addedDomain"
  322. }
  323. ]
  324. expected_errors = [
  325. "removedDomain: domain has been removed",
  326. "Network.removedCommand: command has been removed",
  327. "Network.removedEvent: event has been removed",
  328. "Network.setExtraHTTPHeaders.mismatched: parameter base type mismatch, 'object' vs 'string'",
  329. "Network.setExtraHTTPHeaders.addedRequired: required parameter has been added",
  330. "Network.setExtraHTTPHeaders.becameRequired: optional parameter is now required",
  331. "Network.setExtraHTTPHeaders.removedRequired: required response parameter has been removed",
  332. "Network.setExtraHTTPHeaders.becameOptional: required response parameter is now optional",
  333. "Network.requestWillBeSent.removedRequired: required parameter has been removed",
  334. "Network.requestWillBeSent.becameOptional: required parameter is now optional",
  335. "Network.requestWillBeSent.request parameter->Network.Request.removedField: required property has been removed",
  336. "Network.requestWillBeSent.request parameter->Network.Request.becameOptionalField: required property is now optional",
  337. ]
  338. expected_errors_reverse = [
  339. "addedDomain: domain has been added",
  340. "Network.addedEvent: event has been added",
  341. "Network.addedCommand: command has been added",
  342. "Network.setExtraHTTPHeaders.mismatched: parameter base type mismatch, 'string' vs 'object'",
  343. "Network.setExtraHTTPHeaders.removedRequired: required parameter has been removed",
  344. "Network.setExtraHTTPHeaders.becameOptional: required parameter is now optional",
  345. "Network.setExtraHTTPHeaders.addedRequired: required response parameter has been added",
  346. "Network.setExtraHTTPHeaders.becameRequired: optional response parameter is now required",
  347. "Network.requestWillBeSent.becameRequired: optional parameter is now required",
  348. "Network.requestWillBeSent.addedRequired: required parameter has been added",
  349. ]
  350. def is_subset(subset, superset, message):
  351. for i in range(len(subset)):
  352. if subset[i] not in superset:
  353. sys.stderr.write("%s error: %s\n" % (message, subset[i]))
  354. return False
  355. return True
  356. def errors_match(expected, actual):
  357. return (is_subset(actual, expected, "Unexpected") and
  358. is_subset(expected, actual, "Missing"))
  359. return (errors_match(expected_errors,
  360. compare_schemas(create_test_schema_1(), create_test_schema_2(), False)) and
  361. errors_match(expected_errors_reverse,
  362. compare_schemas(create_test_schema_2(), create_test_schema_1(), True)))
  363. def main():
  364. if not self_test():
  365. sys.stderr.write("Self-test failed")
  366. return 1
  367. if len(sys.argv) < 4 or sys.argv[1] != "-o":
  368. sys.stderr.write("Usage: %s -o OUTPUT_FILE INPUT_FILE [--show-changes]\n" % sys.argv[0])
  369. return 1
  370. output_path = sys.argv[2]
  371. output_file = open(output_path, "w")
  372. input_path = sys.argv[3]
  373. dir_name = os.path.dirname(input_path)
  374. schema = load_json(input_path)
  375. major = schema["version"]["major"]
  376. minor = schema["version"]["minor"]
  377. version = "%s.%s" % (major, minor)
  378. if len(dir_name) == 0:
  379. dir_name = "."
  380. baseline_path = os.path.normpath(dir_name + "/Inspector-" + version + ".json")
  381. baseline_schema = load_json(baseline_path)
  382. errors = compare_schemas(baseline_schema["domains"], schema["domains"], False)
  383. if len(errors) > 0:
  384. sys.stderr.write(" Compatibility with %s: FAILED\n" % version)
  385. for error in errors:
  386. sys.stderr.write( " %s\n" % error)
  387. return 1
  388. if len(sys.argv) > 4 and sys.argv[4] == "--show-changes":
  389. changes = compare_schemas(
  390. load_json(input_path)["domains"], load_json(baseline_path)["domains"], True)
  391. if len(changes) > 0:
  392. sys.stdout.write(" Public changes since %s:\n" % version)
  393. for change in changes:
  394. sys.stdout.write(" %s\n" % change)
  395. output_file.write("""
  396. #ifndef InspectorProtocolVersion_h
  397. #define InspectorProtocolVersion_h
  398. #include <wtf/Vector.h>
  399. #include <wtf/text/WTFString.h>
  400. namespace WebCore {
  401. String inspectorProtocolVersion() { return "%s"; }
  402. int inspectorProtocolVersionMajor() { return %s; }
  403. int inspectorProtocolVersionMinor() { return %s; }
  404. bool supportsInspectorProtocolVersion(const String& version)
  405. {
  406. Vector<String> tokens;
  407. version.split(".", tokens);
  408. if (tokens.size() != 2)
  409. return false;
  410. bool ok = true;
  411. int major = tokens[0].toInt(&ok);
  412. if (!ok || major != %s)
  413. return false;
  414. int minor = tokens[1].toInt(&ok);
  415. if (!ok || minor > %s)
  416. return false;
  417. return true;
  418. }
  419. }
  420. #endif // !defined(InspectorProtocolVersion_h)
  421. """ % (version, major, minor, major, minor))
  422. output_file.close()
  423. if __name__ == '__main__':
  424. sys.exit(main())