Index: sky/engine/core/inspector/generate-inspector-protocol-version |
diff --git a/sky/engine/core/inspector/generate-inspector-protocol-version b/sky/engine/core/inspector/generate-inspector-protocol-version |
new file mode 100755 |
index 0000000000000000000000000000000000000000..d6ee227267c3e3730ff34c0c12e6b524a0d441f2 |
--- /dev/null |
+++ b/sky/engine/core/inspector/generate-inspector-protocol-version |
@@ -0,0 +1,476 @@ |
+#!/usr/bin/env python |
+# Copyright (c) 2011 Google Inc. All rights reserved. |
+# |
+# Redistribution and use in source and binary forms, with or without |
+# modification, are permitted provided that the following conditions are |
+# met: |
+# |
+# * Redistributions of source code must retain the above copyright |
+# notice, this list of conditions and the following disclaimer. |
+# * Redistributions in binary form must reproduce the above |
+# copyright notice, this list of conditions and the following disclaimer |
+# in the documentation and/or other materials provided with the |
+# distribution. |
+# * Neither the name of Google Inc. nor the names of its |
+# contributors may be used to endorse or promote products derived from |
+# this software without specific prior written permission. |
+# |
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+# |
+# Inspector protocol validator. |
+# |
+# Tests that subsequent protocol changes are not breaking backwards compatibility. |
+# Following violations are reported: |
+# |
+# - Domain has been removed |
+# - Command has been removed |
+# - Required command parameter was added or changed from optional |
+# - Required response parameter was removed or changed to optional |
+# - Event has been removed |
+# - Required event parameter was removed or changed to optional |
+# - Parameter type has changed. |
+# |
+# For the parameters with composite types the above checks are also applied |
+# recursively to every property of the type. |
+# |
+# Adding --show_changes to the command line prints out a list of valid public API changes. |
+ |
+import os.path |
+import re |
+import sys |
+ |
+def list_to_map(items, key): |
+ result = {} |
+ for item in items: |
+ if not "hidden" in item: |
+ result[item[key]] = item |
+ return result |
+ |
+def named_list_to_map(container, name, key): |
+ if name in container: |
+ return list_to_map(container[name], key) |
+ return {} |
+ |
+def removed(reverse): |
+ if reverse: |
+ return "added" |
+ return "removed" |
+ |
+def required(reverse): |
+ if reverse: |
+ return "optional" |
+ return "required" |
+ |
+def compare_schemas(schema_1, schema_2, reverse): |
+ errors = [] |
+ types_1 = normalize_types_in_schema(schema_1) |
+ types_2 = normalize_types_in_schema(schema_2) |
+ |
+ domains_by_name_1 = list_to_map(schema_1, "domain") |
+ domains_by_name_2 = list_to_map(schema_2, "domain") |
+ |
+ for name in domains_by_name_1: |
+ domain_1 = domains_by_name_1[name] |
+ if not name in domains_by_name_2: |
+ errors.append("%s: domain has been %s" % (name, removed(reverse))) |
+ continue |
+ compare_domains(domain_1, domains_by_name_2[name], types_1, types_2, errors, reverse) |
+ return errors |
+ |
+def compare_domains(domain_1, domain_2, types_map_1, types_map_2, errors, reverse): |
+ domain_name = domain_1["domain"] |
+ commands_1 = named_list_to_map(domain_1, "commands", "name") |
+ commands_2 = named_list_to_map(domain_2, "commands", "name") |
+ for name in commands_1: |
+ command_1 = commands_1[name] |
+ if not name in commands_2: |
+ errors.append("%s.%s: command has been %s" % (domain_1["domain"], name, removed(reverse))) |
+ continue |
+ compare_commands(domain_name, command_1, commands_2[name], types_map_1, types_map_2, errors, reverse) |
+ |
+ events_1 = named_list_to_map(domain_1, "events", "name") |
+ events_2 = named_list_to_map(domain_2, "events", "name") |
+ for name in events_1: |
+ event_1 = events_1[name] |
+ if not name in events_2: |
+ errors.append("%s.%s: event has been %s" % (domain_1["domain"], name, removed(reverse))) |
+ continue |
+ compare_events(domain_name, event_1, events_2[name], types_map_1, types_map_2, errors, reverse) |
+ |
+def compare_commands(domain_name, command_1, command_2, types_map_1, types_map_2, errors, reverse): |
+ context = domain_name + "." + command_1["name"] |
+ |
+ params_1 = named_list_to_map(command_1, "parameters", "name") |
+ params_2 = named_list_to_map(command_2, "parameters", "name") |
+ # Note the reversed order: we allow removing but forbid adding parameters. |
+ compare_params_list(context, "parameter", params_2, params_1, types_map_2, types_map_1, 0, errors, not reverse) |
+ |
+ returns_1 = named_list_to_map(command_1, "returns", "name") |
+ returns_2 = named_list_to_map(command_2, "returns", "name") |
+ compare_params_list(context, "response parameter", returns_1, returns_2, types_map_1, types_map_2, 0, errors, reverse) |
+ |
+def compare_events(domain_name, event_1, event_2, types_map_1, types_map_2, errors, reverse): |
+ context = domain_name + "." + event_1["name"] |
+ params_1 = named_list_to_map(event_1, "parameters", "name") |
+ params_2 = named_list_to_map(event_2, "parameters", "name") |
+ compare_params_list(context, "parameter", params_1, params_2, types_map_1, types_map_2, 0, errors, reverse) |
+ |
+def compare_params_list(context, kind, params_1, params_2, types_map_1, types_map_2, depth, errors, reverse): |
+ for name in params_1: |
+ param_1 = params_1[name] |
+ if not name in params_2: |
+ if not "optional" in param_1: |
+ errors.append("%s.%s: required %s has been %s" % (context, name, kind, removed(reverse))) |
+ continue |
+ |
+ param_2 = params_2[name] |
+ if param_2 and "optional" in param_2 and not "optional" in param_1: |
+ errors.append("%s.%s: %s %s is now %s" % (context, name, required(reverse), kind, required(not reverse))) |
+ continue |
+ type_1 = extract_type(param_1, types_map_1, errors) |
+ type_2 = extract_type(param_2, types_map_2, errors) |
+ compare_types(context + "." + name, kind, type_1, type_2, types_map_1, types_map_2, depth, errors, reverse) |
+ |
+def compare_types(context, kind, type_1, type_2, types_map_1, types_map_2, depth, errors, reverse): |
+ if depth > 10: |
+ return |
+ |
+ base_type_1 = type_1["type"] |
+ base_type_2 = type_2["type"] |
+ |
+ if base_type_1 != base_type_2: |
+ errors.append("%s: %s base type mismatch, '%s' vs '%s'" % (context, kind, base_type_1, base_type_2)) |
+ elif base_type_1 == "object": |
+ params_1 = named_list_to_map(type_1, "properties", "name") |
+ params_2 = named_list_to_map(type_2, "properties", "name") |
+ # If both parameters have the same named type use it in the context. |
+ if "id" in type_1 and "id" in type_2 and type_1["id"] == type_2["id"]: |
+ type_name = type_1["id"] |
+ else: |
+ type_name = "<object>" |
+ context += " %s->%s" % (kind, type_name) |
+ compare_params_list(context, "property", params_1, params_2, types_map_1, types_map_2, depth + 1, errors, reverse) |
+ elif base_type_1 == "array": |
+ item_type_1 = extract_type(type_1["items"], types_map_1, errors) |
+ item_type_2 = extract_type(type_2["items"], types_map_2, errors) |
+ compare_types(context, kind, item_type_1, item_type_2, types_map_1, types_map_2, depth + 1, errors, reverse) |
+ |
+def extract_type(typed_object, types_map, errors): |
+ if "type" in typed_object: |
+ result = { "id": "<transient>", "type": typed_object["type"] } |
+ if typed_object["type"] == "object": |
+ result["properties"] = [] |
+ elif typed_object["type"] == "array": |
+ result["items"] = typed_object["items"] |
+ return result |
+ elif "$ref" in typed_object: |
+ ref = typed_object["$ref"] |
+ if not ref in types_map: |
+ errors.append("Can not resolve type: %s" % ref) |
+ types_map[ref] = { "id": "<transient>", "type": "object" } |
+ return types_map[ref] |
+ |
+def normalize_types_in_schema(schema): |
+ types = {} |
+ for domain in schema: |
+ domain_name = domain["domain"] |
+ normalize_types(domain, domain_name, types) |
+ return types |
+ |
+def normalize_types(obj, domain_name, types): |
+ if isinstance(obj, list): |
+ for item in obj: |
+ normalize_types(item, domain_name, types) |
+ elif isinstance(obj, dict): |
+ for key, value in obj.items(): |
+ if key == "$ref" and value.find(".") == -1: |
+ obj[key] = "%s.%s" % (domain_name, value) |
+ elif key == "id": |
+ obj[key] = "%s.%s" % (domain_name, value) |
+ types[obj[key]] = obj |
+ else: |
+ normalize_types(value, domain_name, types) |
+ |
+def load_json(filename): |
+ input_file = open(filename, "r") |
+ json_string = input_file.read() |
+ json_string = re.sub(":\s*true", ": True", json_string) |
+ json_string = re.sub(":\s*false", ": False", json_string) |
+ return eval(json_string) |
+ |
+def self_test(): |
+ def create_test_schema_1(): |
+ return [ |
+ { |
+ "domain": "Network", |
+ "types": [ |
+ { |
+ "id": "LoaderId", |
+ "type": "string" |
+ }, |
+ { |
+ "id": "Headers", |
+ "type": "object" |
+ }, |
+ { |
+ "id": "Request", |
+ "type": "object", |
+ "properties": [ |
+ { "name": "url", "type": "string" }, |
+ { "name": "method", "type": "string" }, |
+ { "name": "headers", "$ref": "Headers" }, |
+ { "name": "becameOptionalField", "type": "string" }, |
+ { "name": "removedField", "type": "string" }, |
+ ] |
+ } |
+ ], |
+ "commands": [ |
+ { |
+ "name": "removedCommand", |
+ }, |
+ { |
+ "name": "setExtraHTTPHeaders", |
+ "parameters": [ |
+ { "name": "headers", "$ref": "Headers" }, |
+ { "name": "mismatched", "type": "string" }, |
+ { "name": "becameOptional", "$ref": "Headers" }, |
+ { "name": "removedRequired", "$ref": "Headers" }, |
+ { "name": "becameRequired", "$ref": "Headers", "optional": True }, |
+ { "name": "removedOptional", "$ref": "Headers", "optional": True }, |
+ ], |
+ "returns": [ |
+ { "name": "mimeType", "type": "string" }, |
+ { "name": "becameOptional", "type": "string" }, |
+ { "name": "removedRequired", "type": "string" }, |
+ { "name": "becameRequired", "type": "string", "optional": True }, |
+ { "name": "removedOptional", "type": "string", "optional": True }, |
+ ] |
+ } |
+ ], |
+ "events": [ |
+ { |
+ "name": "requestWillBeSent", |
+ "parameters": [ |
+ { "name": "frameId", "type": "string", "hidden": True }, |
+ { "name": "request", "$ref": "Request" }, |
+ { "name": "becameOptional", "type": "string" }, |
+ { "name": "removedRequired", "type": "string" }, |
+ { "name": "becameRequired", "type": "string", "optional": True }, |
+ { "name": "removedOptional", "type": "string", "optional": True }, |
+ ] |
+ }, |
+ { |
+ "name": "removedEvent", |
+ "parameters": [ |
+ { "name": "errorText", "type": "string" }, |
+ { "name": "canceled", "type": "boolean", "optional": True } |
+ ] |
+ } |
+ ] |
+ }, |
+ { |
+ "domain": "removedDomain" |
+ } |
+ ] |
+ |
+ def create_test_schema_2(): |
+ return [ |
+ { |
+ "domain": "Network", |
+ "types": [ |
+ { |
+ "id": "LoaderId", |
+ "type": "string" |
+ }, |
+ { |
+ "id": "Request", |
+ "type": "object", |
+ "properties": [ |
+ { "name": "url", "type": "string" }, |
+ { "name": "method", "type": "string" }, |
+ { "name": "headers", "type": "object" }, |
+ { "name": "becameOptionalField", "type": "string", "optional": True }, |
+ ] |
+ } |
+ ], |
+ "commands": [ |
+ { |
+ "name": "addedCommand", |
+ }, |
+ { |
+ "name": "setExtraHTTPHeaders", |
+ "parameters": [ |
+ { "name": "headers", "type": "object" }, |
+ { "name": "mismatched", "type": "object" }, |
+ { "name": "becameOptional", "type": "object" , "optional": True }, |
+ { "name": "addedRequired", "type": "object" }, |
+ { "name": "becameRequired", "type": "object" }, |
+ { "name": "addedOptional", "type": "object", "optional": True }, |
+ ], |
+ "returns": [ |
+ { "name": "mimeType", "type": "string" }, |
+ { "name": "becameOptional", "type": "string", "optional": True }, |
+ { "name": "addedRequired", "type": "string"}, |
+ { "name": "becameRequired", "type": "string" }, |
+ { "name": "addedOptional", "type": "string", "optional": True }, |
+ ] |
+ } |
+ ], |
+ "events": [ |
+ { |
+ "name": "requestWillBeSent", |
+ "parameters": [ |
+ { "name": "request", "$ref": "Request" }, |
+ { "name": "becameOptional", "type": "string", "optional": True }, |
+ { "name": "addedRequired", "type": "string"}, |
+ { "name": "becameRequired", "type": "string" }, |
+ { "name": "addedOptional", "type": "string", "optional": True }, |
+ ] |
+ }, |
+ { |
+ "name": "addedEvent" |
+ } |
+ ] |
+ }, |
+ { |
+ "domain": "addedDomain" |
+ } |
+ ] |
+ |
+ expected_errors = [ |
+ "removedDomain: domain has been removed", |
+ "Network.removedCommand: command has been removed", |
+ "Network.removedEvent: event has been removed", |
+ "Network.setExtraHTTPHeaders.mismatched: parameter base type mismatch, 'object' vs 'string'", |
+ "Network.setExtraHTTPHeaders.addedRequired: required parameter has been added", |
+ "Network.setExtraHTTPHeaders.becameRequired: optional parameter is now required", |
+ "Network.setExtraHTTPHeaders.removedRequired: required response parameter has been removed", |
+ "Network.setExtraHTTPHeaders.becameOptional: required response parameter is now optional", |
+ "Network.requestWillBeSent.removedRequired: required parameter has been removed", |
+ "Network.requestWillBeSent.becameOptional: required parameter is now optional", |
+ "Network.requestWillBeSent.request parameter->Network.Request.removedField: required property has been removed", |
+ "Network.requestWillBeSent.request parameter->Network.Request.becameOptionalField: required property is now optional", |
+ ] |
+ |
+ expected_errors_reverse = [ |
+ "addedDomain: domain has been added", |
+ "Network.addedEvent: event has been added", |
+ "Network.addedCommand: command has been added", |
+ "Network.setExtraHTTPHeaders.mismatched: parameter base type mismatch, 'string' vs 'object'", |
+ "Network.setExtraHTTPHeaders.removedRequired: required parameter has been removed", |
+ "Network.setExtraHTTPHeaders.becameOptional: required parameter is now optional", |
+ "Network.setExtraHTTPHeaders.addedRequired: required response parameter has been added", |
+ "Network.setExtraHTTPHeaders.becameRequired: optional response parameter is now required", |
+ "Network.requestWillBeSent.becameRequired: optional parameter is now required", |
+ "Network.requestWillBeSent.addedRequired: required parameter has been added", |
+ ] |
+ |
+ def is_subset(subset, superset, message): |
+ for i in range(len(subset)): |
+ if subset[i] not in superset: |
+ sys.stderr.write("%s error: %s\n" % (message, subset[i])) |
+ return False |
+ return True |
+ |
+ def errors_match(expected, actual): |
+ return (is_subset(actual, expected, "Unexpected") and |
+ is_subset(expected, actual, "Missing")) |
+ |
+ return (errors_match(expected_errors, |
+ compare_schemas(create_test_schema_1(), create_test_schema_2(), False)) and |
+ errors_match(expected_errors_reverse, |
+ compare_schemas(create_test_schema_2(), create_test_schema_1(), True))) |
+ |
+ |
+def main(): |
+ if not self_test(): |
+ sys.stderr.write("Self-test failed") |
+ return 1 |
+ |
+ if len(sys.argv) < 4 or sys.argv[1] != "-o": |
+ sys.stderr.write("Usage: %s -o OUTPUT_FILE INPUT_FILE [--show-changes]\n" % sys.argv[0]) |
+ return 1 |
+ |
+ output_path = sys.argv[2] |
+ output_file = open(output_path, "w") |
+ |
+ input_path = sys.argv[3] |
+ dir_name = os.path.dirname(input_path) |
+ schema = load_json(input_path) |
+ |
+ major = schema["version"]["major"] |
+ minor = schema["version"]["minor"] |
+ version = "%s.%s" % (major, minor) |
+ if len(dir_name) == 0: |
+ dir_name = "." |
+ baseline_path = os.path.normpath(dir_name + "/Inspector-" + version + ".json") |
+ baseline_schema = load_json(baseline_path) |
+ |
+ errors = compare_schemas(baseline_schema["domains"], schema["domains"], False) |
+ if len(errors) > 0: |
+ sys.stderr.write(" Compatibility with %s: FAILED\n" % version) |
+ for error in errors: |
+ sys.stderr.write( " %s\n" % error) |
+ return 1 |
+ |
+ if len(sys.argv) > 4 and sys.argv[4] == "--show-changes": |
+ changes = compare_schemas( |
+ load_json(input_path)["domains"], load_json(baseline_path)["domains"], True) |
+ if len(changes) > 0: |
+ print " Public changes since %s:" % version |
+ for change in changes: |
+ print " %s" % change |
+ |
+ output_file.write(""" |
+#ifndef InspectorProtocolVersion_h |
+#define InspectorProtocolVersion_h |
+ |
+#include "wtf/Vector.h" |
+#include "wtf/text/WTFString.h" |
+ |
+namespace blink { |
+ |
+String inspectorProtocolVersion() { return "%s"; } |
+ |
+int inspectorProtocolVersionMajor() { return %s; } |
+ |
+int inspectorProtocolVersionMinor() { return %s; } |
+ |
+bool supportsInspectorProtocolVersion(const String& version) |
+{ |
+ Vector<String> tokens; |
+ version.split(".", tokens); |
+ if (tokens.size() != 2) |
+ return false; |
+ |
+ bool ok = true; |
+ int major = tokens[0].toInt(&ok); |
+ if (!ok || major != %s) |
+ return false; |
+ |
+ int minor = tokens[1].toInt(&ok); |
+ if (!ok || minor > %s) |
+ return false; |
+ |
+ return true; |
+} |
+ |
+} |
+ |
+#endif // !defined(InspectorProtocolVersion_h) |
+""" % (version, major, minor, major, minor)) |
+ |
+ output_file.close() |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |