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);