summaryrefslogtreecommitdiffstats
path: root/chrome/test/ext_auto
diff options
context:
space:
mode:
authorzelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-11-26 22:21:19 +0000
committerzelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-11-26 22:21:19 +0000
commitdb68a26de1babc3d55ec739bdaf618cce220b79c (patch)
treeb2cb9ddf30fbc68d174e7b39e7bb940052f52c6b /chrome/test/ext_auto
parent7aa4a654323295d972bd2b228618c7826f8638a1 (diff)
downloadchromium_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/OWNERS2
-rw-r--r--chrome/test/ext_auto/auto_provider/background.js6
-rw-r--r--chrome/test/ext_auto/auto_provider/connection_handler.js243
-rw-r--r--chrome/test/ext_auto/auto_provider/manifest.json86
-rw-r--r--chrome/test/ext_auto/auto_provider/server.js96
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));
+ }
+};