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:
parent
561a9eccc5
commit
468516c959
2
webrtc/tools/rtcbot/OWNERS
Normal file
2
webrtc/tools/rtcbot/OWNERS
Normal file
@ -0,0 +1,2 @@
|
||||
andresp@webrtc.org
|
||||
houssainy@google.com
|
27
webrtc/tools/rtcbot/README
Normal file
27
webrtc/tools/rtcbot/README
Normal 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
|
37
webrtc/tools/rtcbot/bot/browser/api.js
Normal file
37
webrtc/tools/rtcbot/bot/browser/api.js
Normal 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;
|
27
webrtc/tools/rtcbot/bot/browser/bot.js
Normal file
27
webrtc/tools/rtcbot/bot/browser/bot.js
Normal 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);
|
11
webrtc/tools/rtcbot/bot/browser/index.html
Normal file
11
webrtc/tools/rtcbot/bot/browser/index.html
Normal 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>
|
118
webrtc/tools/rtcbot/botmanager.js
Normal file
118
webrtc/tools/rtcbot/botmanager.js
Normal 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;
|
84
webrtc/tools/rtcbot/test.js
Normal file
84
webrtc/tools/rtcbot/test.js
Normal 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");
|
54
webrtc/tools/rtcbot/test/simple_offer_answer.js
Normal file
54
webrtc/tools/rtcbot/test/simple_offer_answer.js
Normal 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);
|
Loading…
Reference in New Issue
Block a user