From 468516c9594bc8490de40be7ffac7069fa24a5a3 Mon Sep 17 00:00:00 2001 From: "andresp@webrtc.org" Date: Tue, 2 Sep 2014 10:52:54 +0000 Subject: [PATCH] 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 --- webrtc/tools/rtcbot/OWNERS | 2 + webrtc/tools/rtcbot/README | 27 ++++ webrtc/tools/rtcbot/bot/browser/api.js | 37 ++++++ webrtc/tools/rtcbot/bot/browser/bot.js | 27 ++++ webrtc/tools/rtcbot/bot/browser/index.html | 11 ++ webrtc/tools/rtcbot/botmanager.js | 118 ++++++++++++++++++ webrtc/tools/rtcbot/test.js | 84 +++++++++++++ .../tools/rtcbot/test/simple_offer_answer.js | 54 ++++++++ 8 files changed, 360 insertions(+) create mode 100644 webrtc/tools/rtcbot/OWNERS create mode 100644 webrtc/tools/rtcbot/README create mode 100644 webrtc/tools/rtcbot/bot/browser/api.js create mode 100644 webrtc/tools/rtcbot/bot/browser/bot.js create mode 100644 webrtc/tools/rtcbot/bot/browser/index.html create mode 100644 webrtc/tools/rtcbot/botmanager.js create mode 100644 webrtc/tools/rtcbot/test.js create mode 100644 webrtc/tools/rtcbot/test/simple_offer_answer.js diff --git a/webrtc/tools/rtcbot/OWNERS b/webrtc/tools/rtcbot/OWNERS new file mode 100644 index 000000000..efdce51ca --- /dev/null +++ b/webrtc/tools/rtcbot/OWNERS @@ -0,0 +1,2 @@ +andresp@webrtc.org +houssainy@google.com diff --git a/webrtc/tools/rtcbot/README b/webrtc/tools/rtcbot/README new file mode 100644 index 000000000..06fa33273 --- /dev/null +++ b/webrtc/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/webrtc/tools/rtcbot/bot/browser/api.js b/webrtc/tools/rtcbot/bot/browser/api.js new file mode 100644 index 000000000..b51d49df2 --- /dev/null +++ b/webrtc/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/webrtc/tools/rtcbot/bot/browser/bot.js b/webrtc/tools/rtcbot/bot/browser/bot.js new file mode 100644 index 000000000..130d006e2 --- /dev/null +++ b/webrtc/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/webrtc/tools/rtcbot/bot/browser/index.html b/webrtc/tools/rtcbot/bot/browser/index.html new file mode 100644 index 000000000..9d4758c88 --- /dev/null +++ b/webrtc/tools/rtcbot/bot/browser/index.html @@ -0,0 +1,11 @@ + + + diff --git a/webrtc/tools/rtcbot/botmanager.js b/webrtc/tools/rtcbot/botmanager.js new file mode 100644 index 000000000..420123733 --- /dev/null +++ b/webrtc/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/webrtc/tools/rtcbot/test.js b/webrtc/tools/rtcbot/test.js new file mode 100644 index 000000000..83bb39f4c --- /dev/null +++ b/webrtc/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/webrtc/tools/rtcbot/test/simple_offer_answer.js b/webrtc/tools/rtcbot/test/simple_offer_answer.js new file mode 100644 index 000000000..61eb0bafa --- /dev/null +++ b/webrtc/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);