Skip to content

Commit

Permalink
Merge pull request benschmaus#7 from gamechanger/tl-refactor
Browse files Browse the repository at this point in the history
Refactored Program API
  • Loading branch information
tleach committed Jan 23, 2013
2 parents a99aacb + 77a8525 commit 225d3b6
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 105 deletions.
4 changes: 2 additions & 2 deletions lib/loadtesting.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ var START = NODELOAD_CONFIG.START;
var REPORT_MANAGER = reporting.REPORT_MANAGER;
var HTTP_SERVER = require('./http').HTTP_SERVER;

var registerHttp = require('./user/http_program').registerHttp;

var Program = exports.Program = require('./user/program').Program;
require('./user/http_program').install(Program);
}

registerHttp();
/** TEST_OPTIONS defines all of the parameters that can be set in a test specifiction passed to
run(). By default (calling require('nodeload').run({});), will GET localhost:8080/ as fast as possible
with 10 users for 2 minutes. */
Expand Down
12 changes: 7 additions & 5 deletions lib/loop/userloop.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var USER_LOOP_OPTIONS = exports.USER_LOOP_OPTIONS = {
duration: Infinity,
numberOfTimes: Infinity,
concurrency: 1,
concurrencyProfile: undefined,
concurrencyProfile: undefined
};

var UserLoop = exports.UserLoop = function UserLoop(programOrSpec, args, conditions) {
Expand Down Expand Up @@ -67,7 +67,7 @@ UserLoop.prototype.stop = function() {

UserLoop.prototype.restartProgram = function() {
this.program = new Program(this.programFn, this.programArgs);
}
};

/** Checks conditions and schedules the next loop iteration. 'startiteration' is emitted before each
iteration and 'enditeration' is emitted after. */
Expand All @@ -79,12 +79,14 @@ UserLoop.prototype.loop_ = function() {
if (self.program.finished()) {
self.restartProgram();
}
var waiting = self.program.willWait();
if (!waiting) {

var isRequest = self.program.pendingIsRequest();
if (isRequest) {
self.emit('startiteration');
}
self.program.next(function(res) {
if (!waiting) {
if (isRequest) {
console.log('e');
self.emit('enditeration', res);
}
self.loop_();
Expand Down
181 changes: 131 additions & 50 deletions lib/user/http_program.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,142 @@
var http = require('http');

var BUILD_AS_SINGLE_FILE;
if (BUILD_AS_SINGLE_FILE === undefined) {
var Program = require('./program').Program;
}

var registerHttp = exports.registerHttp = function() {
Program.registerAttrHelper('host', function(hostname, port) {
var settings = {hostname: hostname};
if (port) {
settings.port = port;
var _ = require('underscore'),
request = require('superagent'),
url = require('url'),
fs = require('fs'),
async = require('async'),
temp = require('temp'),
zlib = require('zlib'),
querystring = require('querystring'),
traverse = require('traverse');

_.str = require('underscore.string');
_.mixin(_.str);

exports.request = function(options, next) {
var self = this;
options.headers = options.headers || {};
if (this.attrs.headers) {
_(options.headers).extend(this.attrs.headers);
}

// Build a static JSON object from the options hash by invoking
// any dynamic value functions.
options = traverse(options).clone();
traverse(options).forEach(function(value) {
if (_(value).isFunction()) {
this.update(value.call(self, options));
}
return settings;
}, {hostname: 'localhost', port: 80});
});

if (!this.runData.requests) {
this.runData.requests = [];
}
this.runData.requests.push(options);

Program.registerInterpreter('request', function(options, optionsFn, cb) {
self = this;
if (optionsFn) {
optionsFn.call(self, options);
options.protocol = 'http';
options.pathname = options.path;
options.hostname = options.hostname || this.attrs.hostname;
options.port = options.port || this.attrs.port;
var req = request(options.method, url.format(options)).set(options.headers);

if (options.body) {
var content;
if (options.body.json) {
// This is a total hack to deal with the fact that the API accepts this
// bizarre urlencoded json string within a json object format. I'm sorry.
var isV2 = options.path.indexOf('/v2') > 1,
payload = JSON.stringify(options.body.json);
content = querystring.stringify(isV2? {data: payload} : {json: payload});

} else {
content = options.body; // wouldn't it be nice if it worked this way...
}
if (options.data) {
options.headers['Content-Length'] = options.data.length;
req.type('json').send(content);
}
req.end(function(res) {
// Deal with the fact that sometimes, we get text/html as the
// content type... sigh...
if (res.type === 'text/html') {
res.body = JSON.parse(res.text);
}
var req = http.request(options, function(res) {
var finished = function() {
cb({req: req, res: res});
};
if (options.cb) {
return options.cb(req, res, finished);

// Deal with the possibility of receiving errors via 200
// response and fudge the status code to give us meaningful
// data in our reports.
if (res.status == 200 && res.body.error) {
res.status = 500;
res.res.statusCode = 500;
}

if (res.status !== 200) { // TODO: Probably get ride of this
console.log("Error: " + res.status);
console.log(res.body);
}

options.response = res.body;
next(res);
});
};

// Sets the hostname and port (port optional)
exports.host = function(hostname, port, next) {
if (arguments.length === 2) {
next = port;
port = 80;
}

this.attrs.hostname = hostname;
this.attrs.port = port;
next();
};

exports.headers = function(headers, next) {
this.attrs.headers = this.attrs.headers || {};
_(this.attrs.headers).extend(headers);
next();
};


_(['get', 'post', 'put', 'delete']).each(function(verb) {
exports[verb] = function(path, next) {
// Sort out args - we also accept path inserts and an options hash
// The last arg is always the "next" callback
var options = {};
var pathInserts = [];
_.chain(arguments).rest().initial().each(function(arg) {
if (_(arg).isFunction()) {
pathInserts.push(arg);
} else {
options = arg;
}
finished();
});
if (options.data) {
if (typeof options.data === 'object') {
options.data = JSON.stringify(options.data);
}
req.write(options.data);
next = _(arguments).last();
options.method = verb.toUpperCase();

// If there were path inserts, we need to make path a function.
if (pathInserts.length > 0) {
options.path = function() {
var args = [path];
// Allow path inserts to be functions
args = args.concat(_(pathInserts).map(function(insert) {
if (_(insert).isFunction()) {
return insert();
}
return insert;
}));
return _.sprintf.apply(this, args);
};
} else {
options.path = path;
}
req.end();
}, function(options, optionsFn) {
options.hostname = options.hostname || this.attrs.hostname;
options.port = options.port || this.attrs.port;
return [options, optionsFn];
});
exports.request.call(this, options, next);
};
});

Program.registerHelper('get', 'request', function(path, options, optionsFn) {
options = options || {};
options.method = 'GET';
options.path = path;
return [options, optionsFn];
exports.install = function(Program) {
_(['get', 'post', 'put', 'delete']).each(function(method) {
Program.registerInterpreter(method, exports[method], true);
});

Program.registerHelper('post', 'request', function(path, data, options, optionsFn) {
options = options || {};
options.method = 'POST';
options.path = path;
options.data = data;
return [options, optionsFn];
_(['headers', 'host']).each(function(method) {
Program.registerInterpreter(method, exports[method], false);
});
};
48 changes: 15 additions & 33 deletions lib/user/program.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,21 @@ var Program = exports.Program = function(fun, args) {

// Registration

Program.interpreters = {}
Program.default_attrs = {}
Program.interpreters = {};
Program.default_attrs = {};

Program.registerInterpreter = function(type, interpreter, argsFn) {
if (!argsFn) {
argsFn = function() {
return Array.prototype.slice.call(arguments);
}
}
Program.registerInterpreter = function(type, fn, isRequest) {
Program.prototype[type] = function() {
return this.addStep(type, argsFn.apply(this, arguments));
return this.addStep(type, Array.prototype.slice.call(arguments));
};
Program.interpreters[type] = interpreter;
};

Program.registerHelper = function(name, type, argsFn) {
Program.prototype[name] = function() {
return this[type].apply(this, argsFn.apply(this, arguments));
};
}

Program.registerAttrHelper = function(name, fn, defaults) {
Program.prototype[name] = function() {
util.extend(this.attrs, fn.apply(this, arguments));
return this;
Program.interpreters[type] = {
fn: fn,
isRequest: isRequest
};
if (defaults) {
util.extend(Program.default_attrs, defaults);
}
console.log(Program.interpreters[type]);
};


// Planning

Program.prototype.addStep = function(type, args) {
Expand All @@ -51,24 +35,22 @@ Program.prototype.addStep = function(type, args) {
};

// Execution

// This needs to be special-cased since executors need to recognize waits
Program.prototype.willWait = function() {
return !this.finished() && this._plan[0].type == 'wait';
Program.prototype.pendingIsRequest = function() {
return Program.interpreters[this._plan[0].type].isRequest;
};

Program.prototype.next = function(cb) {
var nextStep = this._plan.shift();
nextStep.args.push(cb);
var interpreter = Program.interpreters[nextStep.type];
var interpreter = Program.interpreters[nextStep.type].fn;
return interpreter.apply(this, nextStep.args);
};

Program.prototype.finished = function() {
return this._plan.length === 0;
}
};

// We always want "wait"
Program.registerInterpreter('wait', function(duration, cb) {
setTimeout(cb, duration)
Program.registerInterpreter('wait', function(duration, next) {
setTimeout(next, duration);
});
29 changes: 14 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@
"engines": {
"node": ">=0.4"
},
"contributors": [
"Benjamin Schmaus <[email protected]>",
"Jonathan Lee <[email protected]>",
"Robert Newson <[email protected]>",
"Michael Mattozzi <[email protected]>"
],
"contributors": ["Benjamin Schmaus <[email protected]>", "Jonathan Lee <[email protected]>", "Robert Newson <[email protected]>", "Michael Mattozzi <[email protected]>"],
"bugs": {
"web" : "https://github.com/benschmaus/nodeload/issues"
"web": "https://github.com/benschmaus/nodeload/issues"
},
"repository": {
"type": "git",
Expand All @@ -38,12 +33,16 @@
"expresso": ">=0.7.7"
},
"dependencies": {
"optparse": "1.0.3"
},
"licenses": [
{
"type": "MIT",
"url": "https://github.com/benschmaus/nodeload/raw/master/LICENSE"
}
]
"optparse": "1.0.3",
"underscore": "~1.4.3",
"underscore.string": "~2.3.1",
"traverse": "~0.6.3",
"superagent": "~0.12.1",
"temp": "~0.5.0",
"async": "~0.1.22"
},
"licenses": [{
"type": "MIT",
"url": "https://github.com/benschmaus/nodeload/raw/master/LICENSE"
}]
}

0 comments on commit 225d3b6

Please sign in to comment.