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@7021 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
andresp@webrtc.org 2014-09-02 10:52:54 +00:00
parent 561a9eccc5
commit 468516c959
8 changed files with 360 additions and 0 deletions

View File

@ -0,0 +1,2 @@
andresp@webrtc.org
houssainy@google.com

View File

@ -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

View File

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

View File

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

View File

@ -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>

View File

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

View File

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

View File

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