Client management tooling, cl interface prototype, tests refactored/passing

The test objects and the server now use `ClientList` to contain, track, and interact with lists of socket clients.

The test constructor now has a `toJSON` method that prevents conversion of circular structures and unecessary data transfer to the clients. It also ensures that only connected clients are stored at any time, and deleted when they disconnect, emitting the `remove` event.

Hacked-out terminal interface now allows for running test files outside the panic project folder, and testing the full integration of the suite, as well as ensuring that I'm taking this in the right direction.

Most of the tests were from an age where I didn't understand how to unit test. They assumed too much about the implementation. But now I'm reformed! They've been refactored to be less obtrusive.
This commit is contained in:
Jesse Gibson
2016-03-25 15:16:30 -06:00
parent 32f5c00d31
commit b5ae660842
10 changed files with 235 additions and 304 deletions

View File

@@ -3,6 +3,9 @@
"version": "0.1.0",
"description": "Test gun against a storm of requests",
"main": "src/index.js",
"bin": {
"panic": "src/cli/index.js"
},
"scripts": {
"test": "jasmine",
"start": "node server"
@@ -21,10 +24,11 @@
"author": "Jesse Gibson <jesse@gundb.io> (http://techllama.com)",
"license": "(Zlib OR MIT OR Apache-2.0)",
"bugs": {
"url": "https://github.com/PsychoLlama/panic/issues"
"url": "https://github.com/PsychoLlama/panic-server/issues"
},
"homepage": "https://github.com/PsychoLlama/panic#readme",
"dependencies": {
"glob": "^7.0.3",
"object-assign-deep": "0.0.4",
"socket.io": "^1.4.5"
},

View File

@@ -2,21 +2,18 @@
'use strict';
var Emitter = require('events');
var assign = require('object-assign-deep');
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('disconnect', function () {
delete server.clients[client.PANIC_ID];
server.emit('disconnect', client);
});
client.on('ID', function (ID) {
client.PANIC_ID = ID;
server.clients[ID] = client;
clients.add(client);
server.emit('connection', client);
});
@@ -24,6 +21,8 @@ function subscribe(socket) {
server.emit('ready', testID, client);
});
client.on('event', server.emit.bind(server));
client.on('done', function (meta) {
server.emit('done', meta);
});
@@ -57,19 +56,18 @@ function close(port) {
return socket;
}
module.exports = new Emitter();
assign(server = module.exports, {
sockets: {},
clients: {},
port: null,
open: open,
close: close
});
// Merge server with event emitter
server = module.exports = new Emitter();
server.sockets = {};
server.port = null;
server.open = open;
server.close = close;
server.clients = clients;
server.on('run', function (ID) {
console.log('Running:', ID);
Object.keys(server.clients).forEach(function (key) {
var client = server.clients[key];
clients.each(function (client) {
client.emit('run', ID);
});
});

View File

@@ -0,0 +1,9 @@
/*globals jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
var ClientList = require('../../src/framework/ClientList');
describe('ClientLists', function () {
});

View File

@@ -1,139 +1,34 @@
/*global jasmine, describe, it, expect, beforeEach*/
/*global jasmine, describe, it, expect, beforeEach, spyOn*/
/*jslint node: true*/
'use strict';
// should import test
var test = require('../../src');
var Emitter = require('events');
var test = require('../../src/framework/Test');
var stack = require('../../src/framework/stack');
describe('The test function', function () {
it('should be a function', function () {
expect(global.test).toEqual(jasmine.any(Function));
expect(test).toEqual(jasmine.any(Function));
describe('The Test constructor', function () {
it('should give a UID to each test', function () {
var first, second;
first = test(function () {});
second = test(function () {});
expect(first.ID).not.toBe(second.ID);
});
it('should take a function and invoke it', function (done) {
test(done);
it('should give a description to each test', function () {
var instance = test('success', function () {});
expect(instance.description).toMatch(/success/);
});
it('should assign an ID to each test', function () {
var result = test(function () {});
expect(result.ID).toEqual(jasmine.any(String));
it('should push tests to the stack', function () {
spyOn(stack, 'push');
var TDO = test(function () {});
expect(stack.push).toHaveBeenCalledWith(TDO);
});
it('should provide unique IDs', function () {
var ID2, ID1;
ID1 = test(function () {}).ID;
ID2 = test(function () {}).ID;
expect(ID1).not.toBe(ID2);
});
it('should expose the connected runners', function () {
var runners = test(function () {}).runners;
expect(runners).toEqual(jasmine.any(Object));
});
it('should inherit from EventEmitter', function () {
var instance = test(function () {});
expect(instance).toEqual(jasmine.any(Emitter));
});
it('should invoke with a new test context', function () {
test(function () {
expect(this).toEqual(jasmine.any(test));
});
});
it('should allow you to name tests', function (done) {
test('Named test', done);
});
it('should pass the context as arg0', function () {
test(function (ctx) {
expect(ctx).toEqual(jasmine.any(test));
});
});
it('should name tests without a name "Anonymous"', function () {
var result = test(function () {});
expect(result.description).toBe('Anonymous');
});
it('should remember the test name', function () {
var result = test('fabulous success', function () {});
expect(result.description).toBe('fabulous success');
});
it('should push the test onto the stack', function () {
stack.next = [];
stack.current = {};
test(function () {});
expect(stack.next.length).toBe(1);
});
});
describe('The Context constructor', function () {
var ctx, proto = test.prototype;
beforeEach(function () {
ctx = test(function () {});
});
it('should have a "server" method', function () {
expect(proto.server).toEqual(jasmine.any(Function));
});
it('should have a "config" property', function () {
expect(ctx.config).toEqual(jasmine.any(Object));
});
it('should have a "cbs" array in the config', function () {
expect(ctx.config.cbs).toEqual(jasmine.any(Array));
});
describe('"client" method', function () {
var cbs;
beforeEach(function () {
ctx.client(function () {});
cbs = ctx.config.cbs;
});
it('should be a function', function () {
expect(proto.client).toEqual(jasmine.any(Function));
});
it('should push to the "cbs" array', function () {
expect(cbs.length).toBe(1);
});
it('should create a test descriptor', function () {
expect(cbs[0]).toEqual(jasmine.any(Object));
});
it('should create a test conditional', function () {
expect(cbs[0].conditional).toBeTruthy();
});
});
describe('"server" method', function () {
var cbs;
beforeEach(function () {
ctx.server(function () {});
cbs = ctx.config.cbs;
});
it('should be a function', function () {
expect(ctx.server).toEqual(jasmine.any(Function));
});
it('should push to the "cbs" array', function () {
expect(cbs[0]).toEqual(jasmine.any(Object));
});
it('should create a test conditional', function () {
expect(cbs[0].conditional).toEqual(jasmine.any(String));
});
it('should allow JSONification', function () {
var circular, TDO = test(function () {});
circular = {};
circular.ref = circular;
TDO.circular = circular;
JSON.stringify(TDO);
});
});

View File

@@ -1,64 +1,43 @@
/*globals jasmine, describe, it, expect*/
/*globals jasmine, describe, it, expect, beforeEach*/
/*jslint node: true*/
'use strict';
var stack = require('../../src/framework/stack');
var Emitter = require('events');
describe('The test stack', function () {
it('should expose the current test', function () {
expect(stack.current).not.toBe(undefined);
});
describe('The stack', function () {
var obj = {
on: function noop() {}
};
it('should inherit from EventEmitter', function () {
expect(stack).toEqual(jasmine.any(Emitter));
});
it('should have a list of upcoming tests', function () {
expect(stack.next).toEqual(jasmine.any(Array));
});
it('should have a list of completed tests', function () {
expect(stack.completed).toEqual(jasmine.any(Array));
});
it('should have a "push" function', function () {
expect(stack.push).toEqual(jasmine.any(Function));
});
describe('push function', function () {
it('should serve the first test', function () {
// clear the stack
describe('push method', function () {
it('should add to the stack', function () {
stack.next = [];
stack.current = null;
var obj = {};
stack.push(obj);
expect(stack.current).toBe(obj);
});
});
it('should respect queued tests', function () {
var obj = {};
// first
stack.push({});
// second in line
stack.push(obj);
expect(stack.current).not.toBe(obj);
describe('shift method', function () {
it('should change the current test', function () {
var last = stack.current;
stack.shift();
expect(last).not.toBe(stack.current);
});
});
it('should select the next test when done', function () {
var obj = {};
stack.next = [obj];
stack.emit('done');
expect(stack.current).toBe(obj);
expect(stack.next.length).toBe(0);
});
describe('events', function () {
it('should fire on change', function (done) {
stack.on('change', done);
stack.push(obj);
stack.shift();
});
it('should mark the last test as done', function () {
var obj;
stack.current = obj = {};
stack.completed = [];
stack.emit('done');
expect(stack.completed[0]).toBe(obj);
it('should fire on finish', function (done) {
stack.next = [];
stack.push(obj);
stack.on('finished', done);
stack.shift();
});
});
});

View File

@@ -1,72 +0,0 @@
/*globals jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
// override port default
process.argv[2] = 3000;
var server = require('../../server');
var Emitter = require('events');
var Socket = require('socket.io');
var io = require('socket.io-client');
describe('The socket server', function () {
it('should have an event emitter', function () {
expect(server.events).toEqual(jasmine.any(Emitter));
});
describe('open method', function () {
it('should be a function', function () {
expect(server.open).toEqual(jasmine.any(Function));
});
it('should open a new socket', function () {
server.open(8080);
expect(server.socket).toEqual(jasmine.any(Socket));
});
it('should close the old socket', function () {
// same port, shouldn't throw EADDRINUSE
server.open(8080);
server.open(8080);
});
it('should return the new socket', function () {
var result = server.open(8080);
expect(server.socket).toBe(result);
});
it('should update the port number', function () {
server.open(1234);
expect(server.port).toBe(1234);
});
it('should default the port to 8080', function () {
server.open();
expect(server.port).toBe(8080);
});
});
it('should expose the socket server', function () {
expect(server.hasOwnProperty('socket')).toBe(true);
});
it('should expose the port number', function () {
expect(server.hasOwnProperty('port')).toBe(true);
});
it('should notify when clients join', function (done) {
server.open(8080);
server.events.on('join', done);
io('http://localhost:8080');
});
it('should pass the client obj on join', function (done) {
server.open(8080);
server.events.on('join', function (client) {
expect(client).toEqual(jasmine.any(Object));
done();
});
io('http://localhost:8080');
});
});

31
src/cli/index.js Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env node
/*jslint node: true*/
'use strict';
var panic = require('../index');
var stack = require('../framework/stack');
var server = require('../../server');
var glob = require('glob');
var fs = require('fs');
var dir = process.cwd();
var path = require('path');
var port = process.argv[2] || 8080;
var file = path.join(dir, 'test', 'panic.config.js');
var config = require(file);
config.tests = config.tests || path.join('tests', '*.panic.js');
glob(path.join(dir, config.tests), function (err, list) {
list.forEach(function (test) {
require(test);
});
if (stack.current) {
server.open(process.argv[2]);
console.log('Beginning on port:', port);
stack.on('finished', function () {
process.exit(0);
});
}
});

100
src/framework/ClientList.js Normal file
View File

@@ -0,0 +1,100 @@
/*jslint node: true, es5: true, continue: true, forin: true*/
'use strict';
var assign = require('object-assign-deep');
var Emitter = require('events');
function ClientList(socket) {
assign(this, new Emitter());
}
ClientList.prototype = {
constructor: ClientList,
/*
* Add a client and keep the
* list updated as they disconnect
**/
add: function (client) {
var ID, list = this;
if (!client.connected) {
return list;
}
ID = client.PANIC_ID;
if (!ID) {
throw new Error('PanicError: No panic ID provided.');
}
list[ID] = client;
list.emit('add', client, ID);
client.on('disconnect', function () {
list.remove(ID);
});
return list;
},
/*
* Removes a peer from the
* list, and emits 'remove'
* if it existed.
**/
remove: function (ID) {
var client = this[ID];
if (!client) {
return this;
}
delete this[ID];
this.emit('remove', client, ID);
return this;
},
/*
* Iterate over each connected
* test client.
**/
each: function (cb) {
var key, list = this;
// filter out emitter properties
for (key in list) {
// filter out unwanted properties
if (!list.hasOwnProperty(key)) {
continue;
}
if (!list[key] || !list[key].connected) {
continue;
}
cb(list[key], key, list);
}
return list;
},
/*
* Return the number of
* connected clients.
**/
get length() {
var count = 0;
this.each(function () {
count += 1;
});
return count;
},
set length(value) {
throw new Error('Cannot set ClientList length.');
}
};
assign(ClientList.prototype, Emitter.prototype);
module.exports = ClientList;

View File

@@ -6,18 +6,11 @@ var assign = require('object-assign-deep');
var Emitter = require('events');
var stack = require('./stack');
var server = require('../../server');
var List = require('./ClientList');
// String.random
require('../configuration/extensions');
function clients(test) {
var keys, length;
keys = Object.keys(test.runners);
return keys.filter(function (ID) {
return server.clients[ID];
});
}
function Test(name, cb) {
if (!(this instanceof Test)) {
return new Test(name, cb);
@@ -28,8 +21,8 @@ function Test(name, cb) {
cb = name;
}
this.ID = String.random();
this.runners = {};
this.ID = String.random(10);
this.runners = new List();
Test.list[this.ID] = this;
if (typeof name === 'string') {
@@ -38,22 +31,13 @@ function Test(name, cb) {
this.description = 'Anonymous';
}
this.on('client', function (ID, client) {
this.runners[ID] = false;
});
this.config = new Response();
cb.call(this, this);
this.on('peer-done', function (meta) {
var length, keys, test = this;
test.runners[meta.clientID] = 'done';
keys = clients(test);
length = keys.filter(function (ID) {
return test.runners[ID];
}).length;
if (length === keys.length) {
test.emit('done');
this.runners.remove(meta.clientID);
if (!this.runners.length) {
this.end();
}
});
@@ -115,13 +99,15 @@ assign(Test.prototype, Emitter.prototype, {
**/
peers: function (num) {
var test = this;
this.on('client', function () {
var length = clients(test).length;
if (length >= num) {
if (test.runners.length >= num) {
return test.run();
}
this.runners.on('add', function () {
if (test.runners.length >= num) {
test.run();
}
});
return this;
return test;
},
/*
@@ -146,8 +132,20 @@ assign(Test.prototype, Emitter.prototype, {
return;
}
this.hasEnded = true;
server.emit('done');
this.emit('done');
return this;
},
/*
* Only send a subset of the
* test to `JSON.stringify`.
**/
toJSON: function () {
return {
ID: this.ID,
description: this.description,
config: this.config
};
}
});
@@ -157,7 +155,7 @@ assign(Test.prototype, Emitter.prototype, {
* let the corresponding test know.
**/
server.on('ready', function (testID, client) {
Test.list[testID].emit('client', client.PANIC_ID, client);
Test.list[testID].runners.add(client);
});

View File

@@ -13,19 +13,9 @@ test('Panic client', function () {
working: true
});
this.use(function () {
var thing = {};
this.env.thing = thing;
thing.thing = thing;
});
this.client(function (ctx) {
console.log(this.env.thing);
if (location.hash === '#wait') {
this.done();
} else {
setTimeout(this.done.bind(this), 5000);
}
console.log('Working:', this.env.working);
setTimeout(this.done, 5000);
});
this.peers(1);
@@ -34,8 +24,7 @@ test('Panic client', function () {
test(function () {
this.client(function () {
console.log('WOOOOOOT!');
this.done();
setTimeout(this.done, 3000);
});
this.peers(1);