diff --git a/examples/entityScripts/tribble.x.40.js b/examples/entityScripts/tribble.js similarity index 94% rename from examples/entityScripts/tribble.x.40.js rename to examples/entityScripts/tribble.js index 7dd2fe3aa5..a5aaf4feb6 100644 --- a/examples/entityScripts/tribble.x.40.js +++ b/examples/entityScripts/tribble.js @@ -66,11 +66,6 @@ dimensions = Vec3.multiply(scale, properties.dimensions); baton = virtualBaton({ batonName: 'io.highfidelity.tribble', - connectionTest: function (id) { - var connected = baton.validId(id); - print(MyAvatar.sessionUUID, id, connected); - return connected; - }, debugFlow: debug.flow, debugSend: debug.send, debugReceive: debug.receive diff --git a/examples/libraries/virtualBaton.42.js b/examples/libraries/virtualBaton.js similarity index 91% rename from examples/libraries/virtualBaton.42.js rename to examples/libraries/virtualBaton.js index fd81671c44..b1f813f764 100644 --- a/examples/libraries/virtualBaton.42.js +++ b/examples/libraries/virtualBaton.js @@ -31,7 +31,8 @@ function virtualBatonf(options) { var allowedDeviation = number * variability; return number - allowedDeviation + (Math.random() * 2 * allowedDeviation); } - // Allow testing outside in a harness of High Fidelity. + // Allow testing outside in a harness outside of High Fidelity. + // See sourceCodeSandbox/tests/mocha/test/testVirtualBaton.js var globals = options.globals || {}, messages = globals.Messages || Messages, myAvatar = globals.MyAvatar || MyAvatar, @@ -43,20 +44,20 @@ function virtualBatonf(options) { var batonName = options.batonName, // The identify of the baton. // instanceId is the identify of this particular copy of the script among all copies using the same batonName // in the domain. For example, if you wanted only one entity among multiple entity scripts to hold the baton, - // you could specify virtualBaton({batonName: 'someBatonName', id: MyAvatar.sessionUUID + entityID}). + // you could specify virtualBaton({batonName: 'someBatonName', instanceId: MyAvatar.sessionUUID + entityID}). instanceId = options.instanceId || myAvatar.sessionUUID, // virtualBaton() returns the exports object with properties. You can pass in an object to be side-effected. exports = options.exports || {}, // Handy to set false if we believe the optimizations are wrong, or to use both values in a test harness. useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, - // electionTimeout = options.electionTimeout || randomize(500, 0.2), // ms. If no winner in this time, hold a new election. recheckInterval = options.recheckInterval || randomize(500, 0.2), // ms. Check that winners remain connected. - // If you supply your own instanceId, you can also supply a connectionTest to check that answers - // truthy iff the given id is still valid and connected, and is run at recheckInterval. You can use - // exports.validId (see below), and the default answers truthy if id is valid or a concatenation of two valid - // ids. (This handles the most common cases of instanceId being either (the default) MyAvatar.sessionUUID, an - // entityID, or the concatenation (in either order) of both.) + // If you supply your own instanceId, you might also supply a connectionTest that answers + // truthy iff the given id is still valid and connected, and is run at recheckInterval. You + // can use exports.validId (see below), and the default answers truthy if id is valid or a + // concatenation of two valid ids. (This handles the most common cases of instanceId being + // either (the default) MyAvatar.sessionUUID, an entityID, or the concatenation (in either + // order) of both.) connectionTest = options.connectionTest || function connectionTest(id) { var idLength = 38; if (id.length === idLength) { return exports.validId(id); } @@ -126,7 +127,7 @@ function virtualBatonf(options) { // It would be great if we had a way to know how many subscribers our channel has. Failing that... var nNack = 0, previousNSubscribers = 0, lastGathering = 0, thisTimeout = electionTimeout; - function nSubscribers() { // Answer the number of subscribers - or zero to indicate that we have to wait. + function nSubscribers() { // Answer the number of subscribers. // To find nQuorum, we need to know how many scripts are being run using this batonName, which isn't // the same as the number of clients! // @@ -136,7 +137,7 @@ function virtualBatonf(options) { // // If we understimate by too much, there can be different pockets on the Internet that each // believe they have agreement on different holders of the baton, which is precisely what - // the virtualBaton is supposed to avoid. Therefore we need to allow 'nack' gather stragglers. + // the virtualBaton is supposed to avoid. Therefore we need to allow 'nack' to gather stragglers. var now = Date.now(), elapsed = now - lastGathering; if (elapsed >= thisTimeout) { @@ -144,8 +145,8 @@ function virtualBatonf(options) { lastGathering = now; } // ...otherwise we use the previous value unchanged. - // On startup, we do one proposal that we cannot possibly win, so that we'll - // lock things up for timeout to gather the number of responses. + // On startup, we do one proposal that we cannot possibly close, so that we'll + // lock things up for the full electionTimeout to gather responses. if (!previousNSubscribers) { var LARGE_INTEGER = Number.MAX_SAFE_INTEGER || (-1 >>> 1); // QT doesn't define the ECMA constant. Max int will do for our purposes. previousNSubscribers = LARGE_INTEGER; @@ -170,7 +171,7 @@ function virtualBatonf(options) { claimCallback, releaseCallback; function propose() { // Make a new proposal, so that we learn/update the proposalNumber and winner. - // Even though we send back a 'nack' if the proposal is obsolete, with net work errors + // Even though we send back a 'nack' if the proposal is obsolete, with network errors // there's no way to know for certain that we've failed. The electionWatchdog will try a new // proposal if we have not been accepted by a quorum after election Timeout. if (electionWatchdog) { @@ -178,7 +179,7 @@ function virtualBatonf(options) { // timers.clearTimeout(electionWatchdog) and not return. return; } - thisTimeout = randomize(electionTimeout, 0.5); // for accurate nSubcribers counting. + thisTimeout = randomize(electionTimeout, 0.5); // Note use in nSubcribers. electionWatchdog = timers.setTimeout(function () { electionWatchdog = null; propose(); @@ -208,7 +209,7 @@ function virtualBatonf(options) { receive('nack' + instanceId, function (data) { // An acceptor reports more recent data... if (data.proposalNumber === proposalNumber) { nNack++; // For updating nQuorum. - // IWBNI if we started our next proposal now (here, in a setTimeout, but we need a decent nNack count. + // IWBNI if we started our next proposal right now/here, but we need a decent nNack count. // Lets save that optimization for another day... } }); @@ -232,7 +233,7 @@ function virtualBatonf(options) { if (!betterNumber(bestProposal, data)) { bestProposal = accepted = data; // Update both with current data. Might have missed the proposal earlier. if (useOptimizations) { - // The Paxos literature describe every acceptor sending 'accepted' to + // The Paxos literature describes every acceptor sending 'accepted' to // every proposer and learner. In our case, these are the same nodes that received // the 'accept!' message, so we can send to just the originating proposer and invoke // our own accepted handler directly. @@ -288,14 +289,16 @@ function virtualBatonf(options) { } }); + // Public Interface + // // Registers an intent to hold the baton: // Calls onElection(batonName) once, if you are elected by the scripts // to be the unique holder of the baton, which may be never. - // Calls onRelease(batonName) once, if you release the baton held by you, - // whether this is by you calling release(), or by loosing + // Calls onRelease(batonName) once, if the baton held by you is released, + // whether this is by you calling release(), or by losing // an election when you become disconnected. // You may claim again at any time after the start of onRelease - // being called. Otherwise, you will not participate in further elections. + // being called. exports.claim = function claim(onElection, onRelease) { debugFlow('baton:', batonName, instanceId, 'claim'); if (claimCallback) { @@ -314,7 +317,7 @@ function virtualBatonf(options) { // Release the baton you hold, or just log that you are not holding it. exports.release = function release(optionalReplacementOnRelease) { debugFlow('baton:', batonName, instanceId, 'release'); - if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. + if (optionalReplacementOnRelease) { // If you want to change. releaseCallback = optionalReplacementOnRelease; } if (acceptedId() !== instanceId) { @@ -323,7 +326,7 @@ function virtualBatonf(options) { } localRelease(); if (!claimCallback) { // No claim set in release callback. - propose(); // We are the distinguished proposer, but we'll pick anyone else interested, else set it to null. + propose(); } return exports; }; @@ -348,7 +351,7 @@ function virtualBatonf(options) { propose(); return exports; } -if (typeof module !== 'undefined') { +if (typeof module !== 'undefined') { // Allow testing in nodejs. module.exports = virtualBatonf; } else { virtualBaton = virtualBatonf; diff --git a/examples/tests/performance/tribbles.js b/examples/tests/performance/tribbles.js index da533f490a..a48ded730d 100644 --- a/examples/tests/performance/tribbles.js +++ b/examples/tests/performance/tribbles.js @@ -14,8 +14,8 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // The _TIMEOUT parameters can be 0 for no activity, and -1 to be active indefinitely. // -var NUMBER_TO_CREATE = 200; -var LIFETIME = 60; // seconds +var NUMBER_TO_CREATE = 1; // FIXME 200; +var LIFETIME = 30; // FIXME 60; // seconds var EDIT_RATE = 60; // hz var EDIT_TIMEOUT = -1; var MOVE_RATE = 1; // hz @@ -68,7 +68,8 @@ Script.setInterval(function () { moveTimeout: MOVE_TIMEOUT, moveRate: MOVE_RATE, editTimeout: EDIT_TIMEOUT, - editRate: EDIT_RATE + editRate: EDIT_RATE, + debug: {flow: true, send: true} }); for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) { Entities.addEntity({ diff --git a/tests/mocha/README.md b/tests/mocha/README.md new file mode 100644 index 0000000000..0bda9a20bc --- /dev/null +++ b/tests/mocha/README.md @@ -0,0 +1,5 @@ +mocha tests of javascript code (e.g., from ../../examples/libraries/). +``` +npm install +npm test +``` \ No newline at end of file diff --git a/tests/mocha/package.json b/tests/mocha/package.json new file mode 100644 index 0000000000..0532677f26 --- /dev/null +++ b/tests/mocha/package.json @@ -0,0 +1,11 @@ +{ + "name": "HighFidelityTests", + "version": "1.0.0", + "scripts": { + "test": "mocha" + }, + "license": "Apache 2.0", + "devDependencies": { + "mocha": "^2.2.1" + } +} diff --git a/tests/mocha/test/testVirtualBaton.js b/tests/mocha/test/testVirtualBaton.js new file mode 100644 index 0000000000..c661bfeffc --- /dev/null +++ b/tests/mocha/test/testVirtualBaton.js @@ -0,0 +1,220 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true */ +var assert = require('assert'); +var mocha = require('mocha'), describe = mocha.describe, it = mocha.it, after = mocha.after; +var virtualBaton = require('../../../examples/libraries/virtualBaton.js'); + +describe('temp', function () { + var messageCount = 0, testStart = Date.now(); + function makeMessager(nodes, me, mode) { // shim for High Fidelity Message system + function noopSend(channel, string, source) { } + function hasChannel(node, channel) { return -1 !== node.subscribed.indexOf(channel); } + function sendSync(channel, message, nodes, skip) { + nodes.forEach(function (node) { + if (!hasChannel(node, channel) || (node === skip)) { return; } + node.sender(channel, message, me.name); + }); + } + nodes.forEach(function (node) { + node.sender = node.sender || noopSend; + node.subscribed = node.subscribed || []; + }); + return { + subscriberCount: function () { + var c = 0; + nodes.forEach(function (n) { if (n.subscribed.length) { c++; } }); + return c; + }, + subscribe: function (channel) { + me.subscribed.push(channel); + }, + unsubscribe: function (channel) { + me.subscribed.splice(me.subscribed.indexOf(channel), 1); + }, + sendMessage: function (channel, message) { + if ((mode === 'immediate2Me') && hasChannel(me, channel)) { me.sender(channel, message, me.name); } + if (mode === 'immediate') { + sendSync(channel, message, nodes, null); + } else { + process.nextTick(function () { + sendSync(channel, message, nodes, (mode === 'immediate2Me') ? me : null); + }); + } + }, + messageReceived: { + connect: function (f) { + me.sender = function (c, m, i) { messageCount++; f(c, m, i); }; + }, + disconnect: function () { + me.sender = noopSend; + } + } + }; + } + var debug = {}; //{flow: true, send: false, receive: false}; + function makeBaton(testKey, nodes, node, debug, mode, optimize) { + debug = debug || {}; + var baton = virtualBaton({ + batonName: testKey, + debugSend: debug.send, + debugReceive: debug.receive, + debugFlow: debug.flow, + useOptimizations: optimize, + connectionTest: function (id) { return baton.validId(id); }, + globals: { + Messages: makeMessager(nodes, node, mode), + MyAvatar: {sessionUUID: node.name}, + Script: { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval + }, + AvatarList: { + getAvatar: function (id) { return {sessionUUID: id}; } + }, + Entities: {getEntityProperties: function () { }}, + print: console.log + } + }); + return baton; + } + function noRelease(batonName) { assert.ok(!batonName, "should not release"); } + function defineABunch(mode, optimize) { + function makeKey(prefix) { return prefix + mode + (optimize ? '-opt' : ''); } + var testKeys = makeKey('single-'); + it(testKeys, function (done) { + var nodes = [{name: 'a'}]; + var a = makeBaton(testKeys, nodes, nodes[0], debug, mode).claim(function (key) { + console.log('claimed a'); + assert.equal(testKeys, key); + a.unload(); + done(); + }, noRelease); + }); + var testKeydp = makeKey('dual-parallel-'); + it(testKeydp, function (done) { + this.timeout(10000); + var nodes = [{name: 'ap'}, {name: 'bp'}]; + var a = makeBaton(testKeydp, nodes, nodes[0], debug, mode, optimize), + b = makeBaton(testKeydp, nodes, nodes[1], debug, mode, optimize); + function accepted(key) { // Under some circumstances of network timing, either a or b can win. + console.log('claimed ap'); + assert.equal(testKeydp, key); + done(); + } + a.claim(accepted, noRelease); + b.claim(accepted, noRelease); + }); + var testKeyds = makeKey('dual-serial-'); + it(testKeyds, function (done) { + var nodes = [{name: 'as'}, {name: 'bs'}], + gotA = false, + gotB = false; + makeBaton(testKeyds, nodes, nodes[0], debug, mode, optimize).claim(function (key) { + console.log('claimed as', key); + assert.ok(!gotA, "should not get A after B"); + gotA = true; + done(); + }, noRelease); + setTimeout(function () { + makeBaton(testKeyds, nodes, nodes[1], debug, mode, optimize).claim(function (key) { + console.log('claimed bs', key); + assert.ok(!gotB, "should not get B after A"); + gotB = true; + done(); + }, noRelease); + }, 500); + }); + var testKeydsl = makeKey('dual-serial-long-'); + it(testKeydsl, function (done) { + this.timeout(5000); + var nodes = [{name: 'al'}, {name: 'bl'}], + gotA = false, + gotB = false, + releaseA = false; + makeBaton(testKeydsl, nodes, nodes[0], debug, mode, optimize).claim(function (key) { + console.log('claimed al', key); + assert.ok(!gotB, "should not get A after B"); + gotA = true; + if (!gotB) { done(); } + }, function () { + assert.ok(gotA, "Should claim it first"); + releaseA = true; + if (gotB) { done(); } + }); + setTimeout(function () { + makeBaton(testKeydsl, nodes, nodes[1], debug, mode, optimize).claim(function (key) { + console.log('claimed bl', key); + gotB = true; + if (releaseA) { done(); } + }, noRelease); + }, 3000); + }); + var testKeydsr = makeKey('dual-serial-with-release-'); + it(testKeydsr, function (done) { + this.timeout(5000); + var nodes = [{name: 'asr'}, {name: 'bsr'}], + gotClaimA = false, + gotReleaseA = false, + a = makeBaton(testKeydsr, nodes, nodes[0], debug, mode, optimize), + b = makeBaton(testKeydsr, nodes, nodes[1], debug, mode, optimize); + a.claim(function (key) { + console.log('claimed asr'); + assert.equal(testKeydsr, key); + gotClaimA = true; + b.claim(function (key) { + console.log('claimed bsr'); + assert.equal(testKeydsr, key); + assert.ok(gotReleaseA); + done(); + }, noRelease); + a.release(); + }, function (key) { + console.log('released asr'); + assert.equal(testKeydsr, key); + assert.ok(gotClaimA); + gotReleaseA = true; + }); + }); + var testKeydpr = makeKey('dual-parallel-with-release-'); + it(testKeydpr, function (done) { + this.timeout(5000); + var nodes = [{name: 'ar'}, {name: 'br'}]; + var a = makeBaton(testKeydpr, nodes, nodes[0], debug, mode, optimize), + b = makeBaton(testKeydpr, nodes, nodes[1], debug, mode, optimize), + gotClaimA = false, + gotReleaseA = false, + gotClaimB = false; + a.claim(function (key) { + console.log('claimed ar'); + assert.equal(testKeydpr, key); + gotClaimA = true; + assert.ok(!gotClaimB, "if b claimed, should not get a"); + a.release(); + }, function (key) { + console.log('released ar'); + assert.equal(testKeydpr, key); + assert.ok(gotClaimA); + gotReleaseA = true; + }); + b.claim(function (key) { + console.log('claimed br', gotClaimA ? 'with' : 'without', 'ar first'); + assert.equal(testKeydpr, key); + gotClaimB = true; + assert.ok(!gotClaimA || gotReleaseA); + done(); + }, noRelease); + }); + } + function defineAllModeTests(optimize) { + defineABunch('delayed', optimize); + defineABunch('immediate2Me', optimize); + defineABunch('immediate', optimize); + } + defineAllModeTests(true); + defineAllModeTests(false); + after(function () { + console.log(messageCount, 'messages sent over', (Date.now() - testStart), 'ms.'); + }); +});