| #!/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: |
| # |
| # - Command has been removed |
| # - Required command parameter was added (could be a part of referenced type) |
| # - Required response parameter was removed |
| # - Event has been removed |
| # - Required event parameter was removed (could be a part of referenced type) |
| # |
| |
| 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 print_errors(domain_name, method_name, local_errors, errors): |
| for error in local_errors: |
| errors.append("%s.%s: %s" % (domain_name, method_name, error)) |
| |
| def compare_schemas(schema_1, schema_2): |
| 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("Domain %s is missing\n") |
| continue |
| compare_domains(domain_1, domains_by_name_2[name], types_1, types_2, errors) |
| return errors |
| |
| def compare_domains(domain_1, domain_2, types_map_1, types_map_2, errors): |
| 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 is missing" % (domain_1["domain"], name)) |
| continue |
| compare_commands(domain_name, command_1, commands_2[name], types_map_1, types_map_2, errors) |
| |
| 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 is missing" % (domain_1["domain"], name)) |
| continue |
| compare_events(domain_name, event_1, events_2[name], types_map_1, types_map_2, errors) |
| |
| def compare_commands(domain_name, command_1, command_2, types_map_1, types_map_2, errors): |
| command_errors = [] |
| 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 parameters. |
| compare_params_list(params_2, params_1, types_map_2, types_map_1, 0, command_errors) |
| |
| returns_1 = named_list_to_map(command_1, "returns", "name") |
| returns_2 = named_list_to_map(command_2, "returns", "name") |
| compare_params_list(returns_1, returns_2, types_map_1, types_map_2, 0, command_errors) |
| print_errors(domain_name, command_1["name"], command_errors, errors) |
| |
| def compare_events(domain_name, event_1, event_2, types_map_1, types_map_2, errors): |
| event_errors = [] |
| params_1 = named_list_to_map(event_1, "parameters", "name") |
| params_2 = named_list_to_map(event_2, "parameters", "name") |
| compare_params_list(params_1, params_2, types_map_1, types_map_2, 0, event_errors) |
| print_errors(domain_name, event_1["name"], event_errors, errors) |
| |
| def compare_params_list(params_1, params_2, types_map_1, types_map_2, depth, errors): |
| for name in params_1: |
| param_1 = params_1[name] |
| if not name in params_2: |
| if not "optional" in param_1: |
| errors.append("required parameter is missing: %s" % name) |
| continue |
| |
| param_2 = params_2[name] |
| if param_2 and "optional" in param_2 and not "optional" in param_1: |
| errors.append("required parameter is now optional: %s" % name) |
| continue; |
| type_1 = extract_type(param_1, types_map_1, errors) |
| type_2 = extract_type(param_2, types_map_2, errors) |
| compare_types("param " + name, type_1, type_2, types_map_1, types_map_2, depth, errors) |
| |
| def compare_types(context, type_1, type_2, types_map_1, types_map_2, depth, errors): |
| 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 base type mismatch, '%s' vs '%s'" % (context, 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") |
| compare_params_list(params_1, params_2, types_map_1, types_map_2, depth + 1, errors) |
| 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, item_type_1, item_type_2, types_map_1, types_map_2, depth + 1, errors) |
| |
| 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(): |
| schema_1 = [ |
| { |
| "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": "postData", "type": "string" } |
| ] |
| } |
| ], |
| "commands": [ |
| { |
| "name": "setUserAgentOverride", |
| "parameters": [ |
| { "name": "userAgent", "type": "string" } |
| ] |
| }, |
| { |
| "name": "setExtraHTTPHeaders", |
| "parameters": [ |
| { "name": "headers", "$ref": "Headers" } |
| ], |
| "returns": [ |
| { "name": "mimeType", "type": "string" }, |
| { "name": "nonOptionalMimeType", "type": "string" }, |
| { "name": "optionalMimeType", "type": "string", "optional": True } |
| ] |
| } |
| ], |
| "events": [ |
| { |
| "name": "requestWillBeSent", |
| "parameters": [ |
| { "name": "frameId", "type": "string", "hidden": True }, |
| { "name": "documentURL", "type": "string" }, |
| { "name": "request", "$ref": "Request" }, |
| ] |
| }, |
| { |
| "name": "loadingFailed", |
| "parameters": [ |
| { "name": "errorText", "type": "string" }, |
| { "name": "canceled", "type": "boolean", "optional": True } |
| ] |
| } |
| ] |
| } |
| ] |
| |
| schema_2 = [ |
| { |
| "domain": "Network", |
| "types": [ |
| { |
| "id": "LoaderId", |
| "type": "string" |
| }, |
| { |
| "id": "Request", |
| "type": "object", |
| "properties": [ |
| { "name": "url", "type": "string" }, |
| { "name": "method", "type": "string" }, |
| { "name": "headers", "type": "object" }, |
| ] |
| } |
| ], |
| "commands": [ |
| { |
| "name": "setExtraHTTPHeaders", |
| "parameters": [ |
| { "name": "headers", "type": "object" }, |
| ], |
| "returns": [ |
| { "name": "nonOptionalMimeType", "type": "string", "optional": True } |
| ] |
| } |
| ], |
| "events": [ |
| { |
| "name": "requestWillBeSent", |
| "parameters": [ |
| { "name": "request", "$ref": "Request" } |
| ] |
| } |
| ] |
| } |
| ] |
| |
| errors = compare_schemas(schema_1, schema_2) |
| golden_errors = [ |
| "Network.setUserAgentOverride: command is missing", |
| "Network.setExtraHTTPHeaders: required parameter is missing: mimeType", |
| "Network.setExtraHTTPHeaders: required parameter is now optional: nonOptionalMimeType", |
| "Network.loadingFailed: event is missing", |
| "Network.requestWillBeSent: required parameter is missing: postData", |
| "Network.requestWillBeSent: required parameter is missing: documentURL" ] |
| for i in range(len(errors)): |
| if errors[i] != golden_errors[i]: |
| return False |
| return 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\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) |
| version_file_name = os.path.normpath(dir_name + "/Inspector-" + version + ".json") |
| version_schema = load_json(version_file_name) |
| |
| errors = compare_schemas(version_schema["domains"], schema["domains"]) |
| 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 |
| |
| output_file.write(""" |
| #ifndef InspectorProtocolVersion_h |
| #define InspectorProtocolVersion_h |
| #include "PlatformString.h" |
| #include <wtf/Vector.h> |
| |
| namespace WebCore { |
| |
| 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()) |