RTCBot is a framework that allows to write tests where logic runs on a single
host that controls multiple endpoints ("bots"). Thus allowing to create more
complex scenarios that would otherwise require non-trival signalling between
multiple parties.
R=houssainy@google.com, phoglund@webrtc.org
Review URL: https://webrtc-codereview.appspot.com/22239004
git-svn-id: http://webrtc.googlecode.com/svn/trunk/webrtc@7021 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/tools/rtcbot/OWNERS b/tools/rtcbot/OWNERS
new file mode 100644
index 0000000..efdce51
--- /dev/null
+++ b/tools/rtcbot/OWNERS
@@ -0,0 +1,2 @@
+andresp@webrtc.org
+houssainy@google.com
diff --git a/tools/rtcbot/README b/tools/rtcbot/README
new file mode 100644
index 0000000..06fa332
--- /dev/null
+++ b/tools/rtcbot/README
@@ -0,0 +1,27 @@
+=== RTCBot ===
+RTCBot is a framework to write tests that need to spawn multiple webrtc
+endpoints.
+
+== Description ==
+RTCBot is a framework that allows to write tests where logic runs on a single
+host that controls multiple endpoints ("bots"). It allows creating complex
+scenarios that would otherwise require non-trival signalling between multiple
+parties.
+
+The host runs in node.js, but the test code is run in an isolated context with
+no access to node.js specifics other than the exposed api via a test variable.
+
+Part of the exposed api (test.spawnBot) allows a test to spawn a bot and
+access its exposed API. Details are in BotManager.js.
+
+== How to run the test ==
+ $ cd trunk/webrtc/tool/rtcbot
+ $ npm install express browserify ws websocket-stream dnode
+ $ node test.js
+
+== Example on how to install nodejs ==
+ $ cd /work/tools/
+ $ git clone https://github.com/creationix/nvm.git
+ $ export NVM_DIR=/work/tools/nvm; source $NVM_DIR/nvm.sh
+ $ nvm install 0.10
+ $ nvm use 0.10
diff --git a/tools/rtcbot/bot/browser/api.js b/tools/rtcbot/bot/browser/api.js
new file mode 100644
index 0000000..b51d49d
--- /dev/null
+++ b/tools/rtcbot/bot/browser/api.js
@@ -0,0 +1,37 @@
+// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS. All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+//
+// This file exposes the api for the bot to connect to the host script
+// waiting a websocket connection and using dnode for javascript rpc.
+//
+// This file is served to the browser via browserify to resolve the
+// dnode requires.
+var WebSocketStream = require('websocket-stream');
+var Dnode = require('dnode');
+
+function connectToServer(api) {
+ var stream = new WebSocketStream("ws://127.0.0.1:8080/");
+ var dnode = new Dnode(api);
+ dnode.on('error', function (error) { console.log(error); });
+ dnode.pipe(stream).pipe(dnode);
+}
+
+// Dnode loses certain method calls when exposing native browser objects such as
+// peer connections. This methods helps work around that by allowing one to
+// redefine a non-native method in a target "obj" from "src" that applies a list
+// of casts to the arguments (types are lost in dnode).
+function expose(obj, src, method, casts) {
+ obj[method] = function () {
+ for (index in casts)
+ arguments[index] = new (casts[index])(arguments[index]);
+ src[method].apply(src, arguments);
+ }
+}
+
+window.expose = expose;
+window.connectToServer = connectToServer;
diff --git a/tools/rtcbot/bot/browser/bot.js b/tools/rtcbot/bot/browser/bot.js
new file mode 100644
index 0000000..130d006
--- /dev/null
+++ b/tools/rtcbot/bot/browser/bot.js
@@ -0,0 +1,27 @@
+// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS. All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+
+var botExposedApi = {
+ ping: function (callback) {
+ callback("pong");
+ },
+
+ createPeerConnection: function (doneCallback) {
+ console.log("Creating peer connection");
+ var pc = new webkitRTCPeerConnection(null);
+ var obj = {};
+ expose(obj, pc, "close");
+ expose(obj, pc, "createOffer");
+ expose(obj, pc, "createAnswer");
+ expose(obj, pc, "setRemoteDescription", { 0: RTCSessionDescription });
+ expose(obj, pc, "setLocalDescription", { 0: RTCSessionDescription });
+ doneCallback(obj);
+ },
+};
+
+connectToServer(botExposedApi);
diff --git a/tools/rtcbot/bot/browser/index.html b/tools/rtcbot/bot/browser/index.html
new file mode 100644
index 0000000..9d4758c
--- /dev/null
+++ b/tools/rtcbot/bot/browser/index.html
@@ -0,0 +1,11 @@
+<!--
+// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS. All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+-->
+<script src="api.js"></script>
+<script src="bot.js"></script>
diff --git a/tools/rtcbot/botmanager.js b/tools/rtcbot/botmanager.js
new file mode 100644
index 0000000..4201237
--- /dev/null
+++ b/tools/rtcbot/botmanager.js
@@ -0,0 +1,118 @@
+// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS. All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+//
+// botmanager.js module allows a test to spawn bots that expose an RPC API
+// to be controlled by tests.
+var http = require('http');
+var child = require('child_process');
+var Browserify = require('browserify');
+var Dnode = require('dnode');
+var Express = require('express');
+var WebSocketServer = require('ws').Server;
+var WebSocketStream = require('websocket-stream');
+
+// BotManager runs a HttpServer that serves bots assets and and WebSocketServer
+// that listens to incoming connections. Once a connection is available it
+// connects it to bots pending endpoints.
+//
+// TODO(andresp): There should be a way to control which bot was spawned
+// and what bot instance it gets connected to.
+BotManager = function () {
+ this.webSocketServer_ = null;
+ this.bots_ = [];
+ this.pendingConnections_ = [];
+}
+
+BotManager.prototype = {
+ spawnNewBot: function (name, callback) {
+ this.startWebSocketServer_();
+ var bot = new BrowserBot(name, callback);
+ this.bots_.push(bot);
+ this.pendingConnections_.push(bot.onBotConnected.bind(bot));
+ },
+
+ startWebSocketServer_: function () {
+ if (this.webSocketServer_) return;
+
+ this.app_ = new Express();
+
+ this.app_.use('/bot/browser/api.js',
+ this.serveBrowserifyFile_.bind(this,
+ __dirname + '/bot/browser/api.js'));
+
+ this.app_.use('/bot/browser/', Express.static(__dirname + '/bot/browser'));
+
+ this.server_ = http.createServer(this.app_);
+
+ this.webSocketServer_ = new WebSocketServer({ server: this.server_ });
+ this.webSocketServer_.on('connection', this.onConnection_.bind(this));
+
+ this.server_.listen(8080);
+ },
+
+ onConnection_: function (ws) {
+ var callback = this.pendingConnections_.shift();
+ callback(new WebSocketStream(ws));
+ },
+
+ serveBrowserifyFile_: function (file, request, result) {
+ // TODO(andresp): Cache browserify result for future serves.
+ var browserify = new Browserify();
+ browserify.add(file);
+ browserify.bundle().pipe(result);
+ }
+}
+
+// A basic bot waits for onBotConnected to be called with a stream to the actual
+// endpoint with the bot. Once that stream is available it establishes a dnode
+// connection and calls the callback with the other endpoint interface so the
+// test can interact with it.
+Bot = function (name, callback) {
+ this.name_ = name;
+ this.onbotready_ = callback;
+}
+
+Bot.prototype = {
+ log: function (msg) {
+ console.log("bot:" + this.name_ + " > " + msg);
+ },
+
+ name: function () { return this.name_; },
+
+ onBotConnected: function (stream) {
+ this.log('Connected');
+ this.stream_ = stream;
+ this.dnode_ = new Dnode();
+ this.dnode_.on('remote', this.onRemoteFromDnode_.bind(this));
+ this.dnode_.pipe(this.stream_).pipe(this.dnode_);
+ },
+
+ onRemoteFromDnode_: function (remote) {
+ this.onbotready_(remote);
+ }
+}
+
+// BrowserBot spawns a process to open "http://localhost:8080/bot/browser/".
+//
+// That page once loaded, connects to the websocket server run by BotManager
+// and exposes the bot api.
+BrowserBot = function (name, callback) {
+ Bot.call(this, name, callback);
+ this.spawnBotProcess_();
+}
+
+BrowserBot.prototype = {
+ spawnBotProcess_: function () {
+ this.log('Spawning browser');
+ child.exec('google-chrome "http://localhost:8080/bot/browser/"');
+ },
+
+ __proto__: Bot.prototype
+}
+
+module.exports = BotManager;
diff --git a/tools/rtcbot/test.js b/tools/rtcbot/test.js
new file mode 100644
index 0000000..83bb39f
--- /dev/null
+++ b/tools/rtcbot/test.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS. All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+//
+// This script loads the test file in the virtual machine and runs it in a
+// context that only exposes a test variable with methods for testing and to
+// spawn bots.
+//
+// Note: an important part of this script is to keep nodejs-isms away from test
+// code and isolate it from implementation details.
+var fs = require('fs');
+var vm = require('vm');
+var BotManager = require('./botmanager.js');
+
+function Test() {
+ // Make the test fail if not completed in 3 seconds.
+ this.timeout_ = setTimeout(
+ this.fail.bind(this, "Test timeout!"),
+ 3000);
+}
+
+Test.prototype = {
+ log: function () {
+ console.log.apply(console.log, arguments);
+ },
+
+ abort: function (error) {
+ var error = error || new Error("Test aborted");
+ console.log(error.stack);
+ process.exit(1);
+ },
+
+ assert: function (value, message) {
+ if (value !== true) {
+ this.abort(message || "Assert failed.");
+ }
+ },
+
+ fail: function () {
+ this.assert(false, "Test failed.");
+ },
+
+ done: function () {
+ clearTimeout(this.timeout_);
+ console.log("Test succeeded");
+ process.exit(0);
+ },
+
+ // Utility method to wait for multiple callbacks to be executed.
+ // functions - array of functions to call with a callback.
+ // doneCallback - called when all callbacks on the array have completed.
+ wait: function (functions, doneCallback) {
+ var result = new Array(functions.length);
+ var missingResult = functions.length;
+ for (var i = 0; i != functions.length; ++i)
+ functions[i](complete.bind(this, i));
+
+ function complete(index, value) {
+ missingResult--;
+ result[index] = value;
+ if (missingResult == 0)
+ doneCallback.apply(null, result);
+ }
+ },
+
+ spawnBot: function (name, doneCallback) {
+ // Lazy initialization of botmanager.
+ if (!this.botManager_)
+ this.botManager_ = new BotManager();
+ this.botManager_.spawnNewBot(name, doneCallback);
+ },
+}
+
+function runTest(testfile) {
+ console.log("Running test: " + testfile);
+ var script = vm.createScript(fs.readFileSync(testfile), testfile);
+ script.runInNewContext({ test: new Test() });
+}
+
+runTest("./test/simple_offer_answer.js");
diff --git a/tools/rtcbot/test/simple_offer_answer.js b/tools/rtcbot/test/simple_offer_answer.js
new file mode 100644
index 0000000..61eb0ba
--- /dev/null
+++ b/tools/rtcbot/test/simple_offer_answer.js
@@ -0,0 +1,54 @@
+// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS. All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+//
+// Test that offer/answer between 2 peers completes successfully.
+//
+// Note: This test does not performs ice candidate exchange and
+// does not verifies that media can flow between the peers.
+function testOfferAnswer(peer1, peer2) {
+ test.wait([
+ createPeerConnection.bind(peer1),
+ createPeerConnection.bind(peer2) ],
+ establishCall);
+
+ function createPeerConnection(done) {
+ this.createPeerConnection(done, test.fail);
+ }
+
+ function establishCall(pc1, pc2) {
+ test.log("Establishing call.");
+ pc1.createOffer(gotOffer);
+
+ function gotOffer(offer) {
+ test.log("Got offer");
+ expectedCall();
+ pc1.setLocalDescription(offer, expectedCall, test.fail);
+ pc2.setRemoteDescription(offer, expectedCall, test.fail);
+ pc2.createAnswer(gotAnswer, test.fail);
+ }
+
+ function gotAnswer(answer) {
+ test.log("Got answer");
+ expectedCall();
+ pc2.setLocalDescription(answer, expectedCall, test.fail);
+ pc1.setRemoteDescription(answer, expectedCall, test.fail);
+ }
+ }
+}
+
+// TODO(andresp): Implement utilities in test to write expectations that certain
+// methods must be called.
+var expectedCalls = 0;
+function expectedCall() {
+ if (++expectedCalls == 6)
+ test.done();
+}
+
+test.wait( [ test.spawnBot.bind(test, "alice"),
+ test.spawnBot.bind(test, "bob") ],
+ testOfferAnswer);