blob: 0e5fe3512bcfd25b7c685ce162cfcd6a0ea3cb08 [file] [log] [blame]
#!/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())