diff options
author | zelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-26 22:21:19 +0000 |
---|---|---|
committer | zelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-26 22:21:19 +0000 |
commit | db68a26de1babc3d55ec739bdaf618cce220b79c (patch) | |
tree | b2cb9ddf30fbc68d174e7b39e7bb940052f52c6b /chrome/test/ext_auto | |
parent | 7aa4a654323295d972bd2b228618c7826f8638a1 (diff) | |
download | chromium_src-db68a26de1babc3d55ec739bdaf618cce220b79c.zip chromium_src-db68a26de1babc3d55ec739bdaf618cce220b79c.tar.gz chromium_src-db68a26de1babc3d55ec739bdaf618cce220b79c.tar.bz2 |
Prototype test automation provider based on extension/webapp API.
This component extension opens a TCP socket through which it can receive simple automation commands for:
1. Adding extension API event listeners:
LISTEN <method-1>[,<method-2>[, ...]]
LISTEN <method>[?<url-encoded-json-params>]
for example,
LISTEN chrome.tabs.onUpdated.addListener,chrome.tabs.onRemoved.addListener
2. Directly running sync or async API methods:
RUN <method>[?<url-encoded-json-params>]
On the server output side, it will send a stream of response chunks in following format
<url-encoded-json-chunk>\r\n
there are 5 types of response identified in JSON as
{
'type': 'eventRegistration|eventCallback|methodResult|methodCallback|error',
... // response-type specific payload
}
To play with it, run following in the shell:
out/Debug/chrome --load-component-extension=<path_to_this_extension>
in another window, open telnet with
telnet localhost 8666
then type following commands:
LISTEN chrome.tabs.onUpdated.addListener,chrome.tabs.onHighlighted.addListener
RUN chrome.tabs.create?%5B%7B%22url%22%3A%20%22http%3A%2F%2Fwww.google.com%22%7D%5D
BUG=161404
TEST=see telnet instructions above, automated tests are coming...
Review URL: https://codereview.chromium.org/11280045
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@169499 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/test/ext_auto')
-rw-r--r-- | chrome/test/ext_auto/OWNERS | 2 | ||||
-rw-r--r-- | chrome/test/ext_auto/auto_provider/background.js | 6 | ||||
-rw-r--r-- | chrome/test/ext_auto/auto_provider/connection_handler.js | 243 | ||||
-rw-r--r-- | chrome/test/ext_auto/auto_provider/manifest.json | 86 | ||||
-rw-r--r-- | chrome/test/ext_auto/auto_provider/server.js | 96 |
5 files changed, 433 insertions, 0 deletions
diff --git a/chrome/test/ext_auto/OWNERS b/chrome/test/ext_auto/OWNERS new file mode 100644 index 0000000..acdc1b8 --- /dev/null +++ b/chrome/test/ext_auto/OWNERS @@ -0,0 +1,2 @@ +achuith@chromium.org +zelidrag@chromium.org diff --git a/chrome/test/ext_auto/auto_provider/background.js b/chrome/test/ext_auto/auto_provider/background.js new file mode 100644 index 0000000..7e080e9 --- /dev/null +++ b/chrome/test/ext_auto/auto_provider/background.js @@ -0,0 +1,6 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Kick off web server. +AutomationServer.getInstance().start();
\ No newline at end of file diff --git a/chrome/test/ext_auto/auto_provider/connection_handler.js b/chrome/test/ext_auto/auto_provider/connection_handler.js new file mode 100644 index 0000000..4e744b4 --- /dev/null +++ b/chrome/test/ext_auto/auto_provider/connection_handler.js @@ -0,0 +1,243 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Automation connection handler is responsible for reading requests from the +// stream, finding and executing appropriate extension API method. +function ConnectionHandler() { + // Event listener registration map socket->event->callback + this.eventListener_ = {}; +} + +ConnectionHandler.prototype = { + // Stream delegate callback. + onStreamError: function(stream) { + this.unregisterListeners_(stream); + }, + + // Stream delegate callback. + onStreamTerminated: function(stream) { + this.unregisterListeners_(stream); + }, + + // Pairs event |listenerMethod| with a given |stream|. + registerListener_: function(stream, eventName, eventObject, + listenerMethod) { + if (!this.eventListener_[stream.socketId_]) + this.eventListener_[stream.socketId_] = {}; + + if (!this.eventListener_[stream.socketId_][eventName]) { + this.eventListener_[stream.socketId_][eventName] = { + 'event': eventObject, + 'method': listenerMethod }; + } + }, + + // Removes event listeners. + unregisterListeners_: function(stream) { + if (!this.eventListener_[stream.socketId_]) + return; + + for (var eventName in this.eventListener_[stream.socketId_]) { + var listenerDefinition = this.eventListener_[stream.socketId_][eventName]; + var removeFunction = listenerDefinition.event['removeListener']; + if (removeFunction) { + removeFunction.call(listenerDefinition.event, + listenerDefinition.method); + } + } + delete this.eventListener_[stream.socketId_]; + }, + + // Finds appropriate method/event to invoke/register. + findExecutionTarget_: function(functionName) { + var funcSegments = functionName.split('.'); + if (funcSegments.size < 2) + return null; + + if (funcSegments[0] != 'chrome') + return null; + + var eventName = ""; + var prevSegName = null; + var prevSegment = null; + var segmentObject = null; + var segName = null; + for (var i = 0; i < funcSegments.length; i++) { + if (prevSegName) { + if (eventName.length) + eventName += '.'; + + eventName += prevSegName; + } + + segName = funcSegments[i]; + prevSegName = segName; + if (!segmentObject) { + // TODO(zelidrag): Get rid of this eval. + segmentObject = eval(segName); + continue; + } + + prevSegment = segmentObject; + if (segmentObject[segName]) + segmentObject = segmentObject[segName]; + else + segmentObject = null; + } + if (segmentObject == window) + return null; + + var isEventMethod = segName == 'addListener'; + return {'method': segmentObject, + 'eventName': (isEventMethod ? eventName : null), + 'event': (isEventMethod ? prevSegment : null)}; + }, + + // TODO(zelidrag): Figure out how to automatically detect or generate list of + // sync API methods. + isSyncFunction_: function(funcName) { + if (funcName == 'chrome.omnibox.setDefaultSuggestion') + return true; + + return false; + }, + + // Parses |command|, finds appropriate JS method runs it with |argsJson|. + // If the method is an event registration, it will register an event listener + // method and start sending data from its callback. + processCommand_: function(stream, command, argsJson) { + var target = this.findExecutionTarget_(command); + if (!target || !target.method) { + return {'result': false, + 'objectName': command}; + } + + var args = JSON.parse(decodeURIComponent(argsJson)); + if (!args) + args = []; + + console.log(command + '(' + decodeURIComponent(argsJson) + ')', + stream.socketId_); + // Check if we need to register an event listener. + if (target.event) { + // Register listener method. + var listener = function() { + stream.write(JSON.stringify({ 'type': 'eventCallback', + 'eventName': target.eventName, + 'arguments' : arguments})); + }.bind(this); + // Add event handler method to arguments. + args.push(listener); + args.push(null); // for |filters|. + target.method.apply(target.event, args); + this.registerListener_(stream, target.eventName, + target.event, listener); + stream.write(JSON.stringify({'type': 'eventRegistration', + 'eventName': command})); + return {'result': true, + 'wasEvent': true}; + } + + // Run extension method directly. + if (this.isSyncFunction_(command)) { + // Run sync method. + console.log(command + '(' + unescape(argsJson) + ')'); + var result = target.method.apply(undefined, args); + stream.write(JSON.stringify({'type': 'methodResult', + 'methodName': command, + 'isCallback': false, + 'result' : result})); + } else { // Async method. + // Add callback method to arguments. + args.push(function() { + stream.write(JSON.stringify({'type': 'methodCallback', + 'methodName': command, + 'isCallback': true, + 'arguments' : arguments})); + }.bind(this)); + target.method.apply(undefined, args); + } + return {'result': true, + 'wasEvent': false}; + }, + + arrayBufferToString_: function(buffer) { + var str = ''; + var uArrayVal = new Uint8Array(buffer); + for(var s = 0; s < uArrayVal.length; s++) { + str += String.fromCharCode(uArrayVal[s]); + } + return str; + }, + + // Callback for stream read requests. + onStreamRead_: function(stream, readInfo) { + console.log("READ", readInfo); + // Parse the request. + var data = this.arrayBufferToString_(readInfo.data); + var spacePos = data.indexOf(" "); + try { + if (spacePos == -1) { + spacePos = data.indexOf("\r\n"); + if (spacePos == -1) + throw {'code': 400, 'description': 'Bad Request'}; + } + + var verb = data.substring(0, spacePos); + var isEvent = false; + switch (verb) { + case 'TERMINATE': + throw {'code': 200, 'description': 'OK'}; + break; + case 'RUN': + break; + case 'LISTEN': + this.isEvent = true; + break; + default: + throw {'code': 400, 'description': 'Bad Request: ' + verb}; + return; + } + + var command = data.substring(verb.length + 1); + var endLine = command.indexOf('\r\n'); + if (endLine) + command = command.substring(0, endLine); + + var objectNames = command; + var argsJson = null; + var funcNameEnd = command.indexOf("?"); + if (funcNameEnd >= 0) { + objectNames = command.substring(0, funcNameEnd); + argsJson = command.substring(funcNameEnd + 1); + } + var functions = objectNames.split(','); + for (var i = 0; i < functions.length; i++) { + var objectName = functions[i]; + var commandStatus = + this.processCommand_(stream, objectName, argsJson); + if (!commandStatus.result) { + throw {'code': 404, + 'description': 'Not Found: ' + commandStatus.objectName}; + } + // If we have run all requested commands, read the socket again. + if (i == (functions.length - 1)) { + setTimeout(function() { + this.readRequest_(stream); + }.bind(this), 0); + } + } + } catch(err) { + console.warn('Error', err); + stream.writeError(err.code, err.description); + } + }, + + // Reads next request from the |stream|. + readRequest_: function(stream) { + console.log("Reading socket " + stream.socketId_); + // Read in the data + stream.read(this.onStreamRead_.bind(this)); + } +}; diff --git a/chrome/test/ext_auto/auto_provider/manifest.json b/chrome/test/ext_auto/auto_provider/manifest.json new file mode 100644 index 0000000..0cece37 --- /dev/null +++ b/chrome/test/ext_auto/auto_provider/manifest.json @@ -0,0 +1,86 @@ +{ + "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDr+Q7QFcTr4Wmn9sSICKWbxnYLhIM0ERbcapZCDmpAkiBUhOPt+KkYnTdUFl4Kx2xv02MwIowh36Fho9Dhqh7cPWGIPsLHUaJosO6t6oaHxQsMQS/K4MlnP5pNJykExo82DcajSXGV+mIQH3RslxL+XhtmIh2BQLwbizVG0bA+mwIDAQAB", + "name": "Test Automation Provider Extension", + "version": "1", + "manifest_version": 2, + "description": "Test Automation Provider Extension", + "default_locale": "en", + "background": { + "scripts": ["connection_handler.js", "server.js", "background.js"] + }, + "content_security_policy": "default-src 'self' 'unsafe-eval'; script-src 'self' 'unsafe-eval';", + "permissions": [ + "activeTab", + "alarms", + "app.runtime", + "app.window", + "appNotifications", + "audioCapture", + "background", + "bluetooth", + "bookmarkManagerPrivate", + "bookmarks", + "browsingData", + "chromePrivate", + "chromeosInfoPrivate", + "clipboardRead", + "clipboardWrite", + "cloudPrintPrivate", + "contentSettings", + "contextMenus", + "cookies", + "debugger", + "devtools", + "declarativeWebRequest", + "downloads", + "experimental", + "fileBrowserHandler", + "fileBrowserPrivate", + "fileSystem", + "fileSystem.write", + "fontSettings", + "geolocation", + "history", + "idle", + "input", + "inputMethodPrivate", + "managedModePrivate", + "management", + "mediaGalleries", + "mediaGalleries.allAutoDetected", + "mediaGalleries.read", + "mediaGalleriesPrivate", + "mediaPlayerPrivate", + "metricsPrivate", + "notifications", + "echoPrivate", + "pageCapture", + "plugin", + "privacy", + "proxy", + "pushMessaging", + "rtcPrivate", + "runtime", + "serial", + "syncFileSystem", + {"socket": ["tcp-connect", "tcp-listen"]}, + "storage", + "systemPrivate", + "tabs", + "tabCapture", + "terminalPrivate", + "topSites", + "tts", + "ttsEngine", + "unlimitedStorage", + "usb", + "videoCapture", + "wallpaperPrivate", + "webNavigation", + "webSocketProxyPrivate", + "webstorePrivate", + "webRequest", + "webRequestBlocking", + "webview" + ] +} diff --git a/chrome/test/ext_auto/auto_provider/server.js b/chrome/test/ext_auto/auto_provider/server.js new file mode 100644 index 0000000..a4f70ad --- /dev/null +++ b/chrome/test/ext_auto/auto_provider/server.js @@ -0,0 +1,96 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var socket = chrome.experimental.socket || chrome.socket; + +// Stream encapsulates read/write operations over socket. +function Stream(delegate, socketId) { + this.socketId_ = socketId; + this.delegate_ = delegate; +} + +Stream.prototype = { + stringToUint8Array_: function(string) { + var utf8string = unescape(encodeURIComponent(string)); + var buffer = new ArrayBuffer(utf8string.length); + var view = new Uint8Array(buffer); + for(var i = 0; i < utf8string.length; i++) { + view[i] = utf8string.charCodeAt(i); + } + return view; + }, + + read: function(callback) { + socket.read(this.socketId_, function(readInfo) { + callback(this, readInfo); + }.bind(this)); + }, + + write: function(output) { + var header = this.stringToUint8Array_(output + '\n\n'); + var outputBuffer = new ArrayBuffer(header.byteLength); + var view = new Uint8Array(outputBuffer); + view.set(header, 0); + socket.write(this.socketId_, outputBuffer, function(writeInfo) { + if (writeInfo.bytesWritten < 0) + this.delegate_.onStreamError(this); + }.bind(this)); + }, + + writeError: function(errorCode, description) { + var content = JSON.stringify({'type': 'error', + 'code': errorCode, + 'description': description}); + var buffer = this.stringToUint8Array_(content + "\n\n"); + var outputBuffer = new ArrayBuffer(buffer.byteLength); + var view = new Uint8Array(outputBuffer); + view.set(buffer, 0); + socket.write(this.socketId_, outputBuffer, function(writeInfo) { + this.terminateConnection_(); + }.bind(this)); + }, + + terminateConnection_: function() { + this.delegate_.onStreamTerminated(this); + socket.destroy(this.socketId_); + } +}; + +// Automation server listens socket and passed its processing to +// |connectionHandler|. +function AutomationServer(connectionHandler) { + this.socketInfo = null; + this.handler_ = connectionHandler; +} + +AutomationServer.instance_ = null; + +AutomationServer.getInstance = function() { + if (!AutomationServer.instance_) + AutomationServer.instance_ = new AutomationServer(new ConnectionHandler()); + + return AutomationServer.instance_; +} + +AutomationServer.prototype = { + onAccept_: function(acceptInfo) { + console.log("Accepting socket " + acceptInfo.socketId); + socket.setNoDelay(acceptInfo.socketId, true, function(result) { + this.handler_.readRequest_(new Stream(this.handler_, + acceptInfo.socketId)); + socket.accept(this.socketInfo.socketId, this.onAccept_.bind(this)); + }.bind(this)); + }, + + start: function() { + socket.create("tcp", {}, function(_socketInfo) { + this.socketInfo = _socketInfo; + socket.listen(this.socketInfo.socketId, "127.0.0.1", 8666, 20, + function(result) { + console.log("LISTENING:", result); + socket.accept(this.socketInfo.socketId, this.onAccept_.bind(this)); + }.bind(this)); + }.bind(this)); + } +}; |