Customized eslint, use mocha instead, implement client

filters/exclusions, all tests passing.

Eslint came with these nasty notions about alphabetizing your variable
definitions. Strict enforcement has been enabled, and empty functions
are now allowed (noop, and it was causing noise while developing).

The tests are now using mocha instead of jasmine, as I'm trying to
become more familiar with it.

The clientList is now dynamically controlled. Only connected peers are
shown, and add/remove events are fired as things change. You can now
filter a client set against criteria (platform, generic callback) and
return a new list of live updating clients, subscribed to changes on the
parent list. There's some syntax sugar also. You can create a list that
excludes another set, such as a list of browsers might be everything
that isn't a Node.js platform. You'd write that as an exclusion.

Added some basic tests for the clientList and mock logic.
This commit is contained in:
Jesse Gibson
2016-04-27 09:59:02 -06:00
parent c046b7e209
commit 4edd691277
10 changed files with 311 additions and 69 deletions

View File

@@ -99,7 +99,7 @@ module.exports = {
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "error",
"no-empty-function": "error",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "off",
@@ -203,7 +203,7 @@ module.exports = {
"semi": "error",
"semi-spacing": "error",
"sort-imports": "error",
"sort-vars": "error",
"sort-vars": "off",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-in-parens": [
@@ -213,7 +213,7 @@ module.exports = {
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "off",
"strict": "off",
"strict": "error",
"template-curly-spacing": "error",
"valid-jsdoc": "error",
"vars-on-top": "off",
@@ -225,4 +225,4 @@ module.exports = {
"never"
]
}
};
};

View File

@@ -4,7 +4,7 @@
"description": "E2E distributed test framework",
"main": "src/index.js",
"scripts": {
"test": "jasmine"
"test": "mocha"
},
"repository": {
"type": "git",
@@ -24,9 +24,11 @@
},
"homepage": "https://github.com/gundb/panic-server#readme",
"dependencies": {
"bluebird": "^3.3.5",
"socket.io": "^1.4.5"
},
"devDependencies": {
"chai": "^3.5.0",
"eslint": "^2.8.0",
"mocha": "^2.4.5"
}

View File

@@ -1,64 +0,0 @@
/*jslint node: true*/
'use strict';
var Emitter = require('events');
var io = require('socket.io');
var List = require('../src/framework/ClientList');
var server;
var clients = new List();
function subscribe(socket) {
socket.on('connection', function (client) {
client.on('details', function (ID, platform) {
client.platform = platform;
client.PANIC_ID = ID;
client.platform.ID = ID;
client.setMaxListeners(Infinity);
clients.add(client);
});
client.on('ready', function (testID) {
server.emit('ready', testID, client);
});
client.on('event', server.emit.bind(server));
});
}
function open(port) {
// set default port
port = port || 8080;
// don't try to re-open
if (server.socket) {
return server.socket;
}
// update state
var socket = io(port);
server.socket = socket;
subscribe(socket);
return socket;
}
function close(port) {
var socket = server.socket;
if (server.socket) {
socket.close();
server.socket = null;
}
return socket;
}
// Merge server with event emitter
server = module.exports = new Emitter();
server.setMaxListeners(Infinity);
server.socket = null;
server.port = null;
server.open = open;
server.close = close;
server.clients = clients;

117
src/ClientList.js Normal file
View File

@@ -0,0 +1,117 @@
'use strict';
var Emitter = require('events');
var match = require('./matcher');
var Promise = require('bluebird');
function ClientList() {
Emitter.call(this);
this.clients = {};
}
var API = ClientList.prototype = new Emitter();
API.each = function (cb) {
var key;
for (key in this.clients) {
if (this.clients.hasOwnProperty(key)) {
cb(this.clients[key], key, this);
}
}
return this;
};
API.add = function (client) {
var list = this;
if (!client.socket.connected) {
return this;
}
this.clients[client.socket.id] = client;
client.socket.on('disconnect', function () {
list.remove(client);
});
this.emit('add', client, client.socket.id);
return this;
};
API.remove = function (client) {
if (client.socket.id in this.clients) {
delete this.clients[client.socket.id];
this.emit('remove', client, client.socket.id);
}
return this;
};
API.get = function (ID) {
return this.clients[ID] || null;
};
API.filter = function (query) {
var list = new ClientList();
function filter(client, ID) {
if (query instanceof Function && query(client, ID)) {
list.add(client);
return;
} else if (typeof query === 'string') {
query = {
name: query
};
}
if (typeof query === 'object' && query) {
if (match(query, client.platform)) {
list.add(client);
}
}
}
this.each(filter).on('add', filter);
return list;
};
API.excluding = function (exclude) {
if (!(exclude instanceof ClientList)) {
throw new Error('Exclusion set is not a ClientList');
}
return this.filter(function (client) {
return !exclude.get(client.socket.id);
});
};
API.len = function () {
if (Object.keys instanceof Function) {
return Object.keys(this.clients).length;
}
var num = 0;
this.each(function () {
num += 1;
});
return num;
};
API.run = function (cb) {
var key, done = 0, list = this, length = this.len();
key = Math.random()
.toString(36)
.slice(2);
return new Promise(function (resolve, reject) {
function count(err) {
if (err) {
reject(err);
} else if ((done += 1) >= length) {
resolve(list);
}
}
function add() {
count(null);
}
list.each(function (client) {
client.socket
.on(key, function (err) {
count(err);
client.socket.removeListener('disconnect', add);
})
.once('disconnect', add)
.emit('run', cb, key);
});
});
};
module.exports = ClientList;

2
src/clients.js Normal file
View File

@@ -0,0 +1,2 @@
var List = require('./ClientList');
module.exports = new List();

7
src/index.js Normal file
View File

@@ -0,0 +1,7 @@
var server = require('./server');
var clients = require('./clients');
module.exports = {
serve: server,
clients: clients
};

19
src/matcher.js Normal file
View File

@@ -0,0 +1,19 @@
function match(query, platform) {
var key, value, matches = true;
for (key in query) {
if (!(query.hasOwnProperty(key))) {
continue;
}
value = query[key];
if (value instanceof RegExp) {
matches = matches && !!platform[key].match(value);
} else if (typeof value === 'string') {
matches = matches && platform[key] === value;
} else if (value instanceof Object) {
return match(value, platform[key] || {});
}
}
return matches;
}
module.exports = match;

31
src/server.js Normal file
View File

@@ -0,0 +1,31 @@
var io = require('socket.io');
var fs = require('fs');
var clients = require('./clients');
var server = require('http').createServer(function (req, res) {
if (req.url !== '/panic.js' && req.url !== '/') {
return;
}
var path = require.resolve('../../panic-client/panic.js');
fs.createReadStream(path).pipe(res);
});
function open(config) {
config = config || {};
config.port = config.port || 8080;
config.hostname = config.hostname || 'localhost';
server.listen(config.port, config.hostname);
io(server).on('connection', function (client) {
client.on('handshake', function (platform) {
clients.add({
socket: client,
platform: platform
});
});
});
return config;
}
module.exports = open;

113
test/index.js Normal file
View File

@@ -0,0 +1,113 @@
/*globals beforeEach, describe, it*/
'use strict';
var mock = require('./mock');
var Client = mock.Client;
var ClientList = require('../src/ClientList');
var expect = require('chai').expect;
describe('A clientList', function () {
var list, client;
beforeEach(function () {
list = new ClientList();
client = new Client({
name: 'Node.js'
});
});
it('should emit when a client is added', function () {
var fired = false;
list.on('add', function () {
fired = true;
}).add(client);
expect(fired).to.eq(true);
});
it('should emit when a client is removed', function () {
var fired = false;
list.on('remove', function () {
fired = true;
})
.add(client)
.remove(client);
expect(fired).to.eq(true);
});
it('should return length when "len()" is called', function () {
list.add(client);
expect(list.len()).to.eq(1);
list.remove(client);
expect(list.len()).to.eq(0);
});
it('should not add disconnected clients', function () {
client.socket.connected = false;
list.add(client);
expect(list.len()).to.eq(0);
});
it('should remove a client on disconnect', function () {
list.add(client);
expect(list.len()).to.eq(1);
client.socket.emit('disconnect');
expect(list.len()).to.eq(0);
});
describe('filter', function () {
it('should not mutate the original list', function () {
list.add(client);
expect(list.len()).to.eq(1);
list.filter(function () {
return false;
});
expect(list.len()).to.eq(1);
});
it('should return a new, filtered list', function () {
list.add(client);
var servers = list.filter('Node.js');
var browsers = list.filter(function (client) {
return client.platform.name !== 'Node.js';
});
expect(servers.len()).to.eq(1);
expect(browsers.len()).to.eq(0);
});
it('should be reactive to changes to the parent list', function () {
var servers = list.filter('Node.js');
expect(servers.len()).to.eq(0);
list.add(client);
expect(servers.len()).to.eq(1);
});
});
describe('exclusion', function () {
it('should not contain excluded clients', function () {
list.add(client);
var filtered = list.excluding(list);
expect(filtered.len()).to.eq(0);
});
});
it('should resolve a promise when all clients finish', function (done) {
client.socket.on('run', function (cb, jobID) {
client.socket.emit(jobID);
});
list.add(client).run(function () {})
.then(function () {
done();
})
.catch(done);
});
it('should reject a promise if an error is sent', function (done) {
client.socket.on('run', function (cb, job) {
client.socket.emit(job, 'fake error');
});
list.add(client).run(function () {})
.catch(function (err) {
expect(err).to.eq('fake error');
done();
});
});
});

15
test/mock.js Normal file
View File

@@ -0,0 +1,15 @@
'use strict';
var Emitter = require('events');
function Client(platform) {
this.socket = new Emitter();
this.socket.connected = true;
this.socket.id = Math.random()
.toString(36)
.slice(2);
this.platform = platform || {};
}
module.exports = {
Client: Client
};