Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 548b4b20e2004d30a1a77fa6fde2d7bd593ec6ee @jfd committed Jan 21, 2013
Showing with 8,140 additions and 0 deletions.
  1. +19 −0 LICENSE
  2. +67 −0 README
  3. +223 −0 bin/stage
  4. +90 −0 bin/stage-abort
  5. +87 −0 bin/stage-clients
  6. +93 −0 bin/stage-collect
  7. +301 −0 bin/stage-init-test
  8. +162 −0 bin/stage-install
  9. +96 −0 bin/stage-jobs
  10. +1,498 −0 bin/stage-master
  11. +121 −0 bin/stage-run
  12. +531 −0 bin/stage-setup-machine
  13. +329 −0 bin/stage-setup-smartdc
  14. +867 −0 bin/stage-slave
  15. +89 −0 bin/stage-tests
  16. +77 −0 bin/stage-uninstall
  17. +62 −0 doc/commands/abort.md
  18. +62 −0 doc/commands/clients.md
  19. +62 −0 doc/commands/collect.md
  20. +57 −0 doc/commands/init-test.md
  21. +63 −0 doc/commands/install.md
  22. +63 −0 doc/commands/jobs.md
  23. +67 −0 doc/commands/master.md
  24. +72 −0 doc/commands/run.md
  25. +69 −0 doc/commands/setup-machine.md
  26. +97 −0 doc/commands/setup-smartdc.md
  27. +69 −0 doc/commands/slave.md
  28. +117 −0 doc/commands/stage.md
  29. +62 −0 doc/commands/tests.md
  30. +63 −0 doc/commands/uninstall.md
  31. +14 −0 examples/long-running-test/package.json
  32. +38 −0 examples/long-running-test/test.js
  33. +14 −0 examples/simple-test/package.json
  34. +36 −0 examples/simple-test/test.js
  35. +291 −0 lib/api.js
  36. +140 −0 lib/cliutil.js
  37. +47 −0 lib/consts.js
  38. +9 −0 lib/dashboard/bootstrap.min.css
  39. +33 −0 lib/dashboard/dashboard.js
  40. +53 −0 lib/dashboard/index.html
  41. +108 −0 man/abort.1
  42. +108 −0 man/clients.1
  43. +108 −0 man/collect.1
  44. +92 −0 man/init-test.1
  45. +108 −0 man/install.1
  46. +111 −0 man/jobs.1
  47. +123 −0 man/master.1
  48. +129 −0 man/run.1
  49. +129 −0 man/setup-machine.1
  50. +183 −0 man/setup-smartdc.1
  51. +123 −0 man/slave.1
  52. +218 −0 man/stage.1
  53. +108 −0 man/tests.1
  54. +114 −0 man/uninstall.1
  55. +33 −0 package.json
  56. +35 −0 scripts/buildman.sh
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013, Johan Dahlberg <https://github.com/jfd>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
67 README
@@ -0,0 +1,67 @@
+Stage - Distributed Testing Suite
+=================================
+
+Stage is a distributed testing suite for Node.js. The main goal is to provide a test suite that can help with network related test cases such as load balancing and performance testing.
+
+Stage includes tools for setting up test networks (via SSH or Joyent SmartDC), distribute and running test and collecting results.
+
+
+## Installation
+
+Stage installs via NPM. Run the following command in your terminal:
+
+ $ npm install stage -g
+
+
+## Example
+
+This example is creating a testing enviroment using your current Joyent Smartdc settings (expects that SDC is currently installed and configured with environmental variables).
+
+First of, we need to setup a Stage Master server:
+
+ $ stage setup-smartdc master
+
+The Master Server it self cannot run tests. This is done by slave servers. We can simply setup a Stage Slave server with the `setup-smartdc` command as well:
+
+ $ stage setup-smartdc slave --remote-url ws://<ip-and-port-to-master-server>
+
+We are now ready to create our first test. Stage comes with a tool that initialize a basic test for you:
+
+ $ stage init-test mytest
+
+Our test is now created in the folder 'mytest'. The test can be run out-of-the-box but will not do much. In order for it do to something, you can edit the 'test.js' file.
+
+All commands from this point is need to now the address to the master server. There is two ways of telling the command who to talk to. Via the comamnd line or via an environmental variable. We will go with the environmental variable in this case.
+
+ $ export STAGE_URL=http://<ip-and-port-to-master-server>
+
+You could pass the argument `--url http://<ip-and-port-to-master-server>` if you prefer to leave the environment untouched.
+
+It is now time to install the test on the Master Server:
+
+ $ stage install mytest
+
+The test is now installed. Next step is to tell the master to run the test on the connected Slave. Note that this phase is async, the command will exit immidently.
+
+ $ stage run mytest@1.0.0
+
+You can monitor the test via the `stage list` command. Once it is ready, you can collect the test results. This is done with the `stage collect` command:
+
+ $ stage collect 1
+
+This is just a basic example in how to use Stage. See manpages for more information:
+
+ $ stage help
+
+
+## Issues
+
+
+## License
+
+Stage is licensed under the MIT license. See LICENSE in this repo for more information.
+
+
+## Copyright
+
+Copyright (c) 2013 Johan Dahlberg <http://jfd.github.com>
223 bin/stage
@@ -0,0 +1,223 @@
+#!/usr/bin/env node
+
+/**
+Usage: stage <command>
+
+Most common used commands for stage:
+
+ abort Aborts a currently running test
+ clients List available clients (slaves and monitors) on master process
+ collect Get's report for specifed test
+ extras Shows available extras commands
+ help Shows available commands for stage (this section)
+ install Installs a test package on master process
+ jobs List all running and finished jobs on master process
+ master Spawns a master process
+ run Runs a test package on master process
+ slave Spawns a slave process
+ tests List all available test packages on master process
+ uninstall Uninstalls a test package on master process
+
+Built-in maintenance commands:
+
+ init-test Initializes a test template.
+ setup-machine Installs dependencies on a remote machine via SSH.
+ setup-smartdc Installs dependencies on a SmartDC machine.
+
+See 'stage <command> --help' for more information on a specific command.
+*/
+
+"use strict";
+
+var resolve = require.resolve;
+
+var spawn = require("child_process").spawn;
+
+var basename = require("path").basename;
+var extname = require("path").extname;
+var presolve = require("path").resolve;
+var join = require("path").join;
+
+var readFileSync = require("fs").readFileSync;
+var readdirSync = require("fs").readdirSync;
+var existsSync = require("fs").existsSync;
+
+
+var BUILTIN_EXTRAS = [ resolve("./stage-setup-machine"),
+ resolve("./stage-setup-smartdc"),
+ resolve("./stage-init-test")
+ ];
+
+
+function main () {
+ var cmd = process.argv[2];
+
+ process.argv.splice(1, 1);
+
+ switch (cmd) {
+
+ case "--help":
+ case "help":
+ return help();
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage();
+
+ case "master":
+ process.argv[1] = resolve("./stage-master");
+ break;
+
+ case "slave":
+ process.argv[1] = resolve("./stage-slave");
+ break;
+
+ case "i":
+ case "install":
+ process.argv[1] = resolve("./stage-install");
+ break;
+
+ case "u":
+ case "uninstall":
+ process.argv[1] = resolve("./stage-uninstall");
+ break;
+
+ case "a":
+ case "abort":
+ process.argv[1] = resolve("./stage-abort");
+ break;
+
+ case "c":
+ case "clients":
+ process.argv[1] = resolve("./stage-clients");
+ break;
+
+ case "j":
+ case "jobs":
+ process.argv[1] = resolve("./stage-jobs");
+ break;
+
+ case "t":
+ case "tests":
+ process.argv[1] = resolve("./stage-tests");
+ break;
+
+ case "r":
+ case "run":
+ process.argv[1] = resolve("./stage-run");
+ break;
+
+ case "co":
+ case "collect":
+ process.argv[1] = resolve("./stage-collect");
+ break;
+
+ case "e":
+ case "extras":
+ return extras();
+
+ default:
+
+ if (!cmd) {
+ return usage();
+ }
+
+ process.argv[1] = getExtraByAlias(cmd);
+
+ if (existsSync(process.argv[1]) == false) {
+ console.error("stage: bad command -- %s", basename(cmd));
+ process.exit(1);
+ }
+ break;
+
+ }
+
+ require(process.argv[1]);
+}
+
+
+function usage () {
+ var match = /\/\*\*\n([^*]+)\*\//gi;
+ process.stdout.write(match.exec(readFileSync(__filename))[1]);
+ process.exit(1);
+}
+
+
+function version () {
+ console.log(require("../package").version);
+ process.exit(1);
+}
+
+
+function help () {
+ spawn("man", ["stage"], { customFds : [0, 1, 2] });
+}
+
+
+function extras () {
+ var extras;
+
+ console.log("Usage: stage <command>");
+ console.log("Available extras commands:\n");
+
+ extras = getAvailableExtras();
+ extras.forEach(function (cmd) {
+ var alias = /^stage\-(.+)/.exec(basename(cmd))[1];
+ console.log(" " + alias);
+ });
+}
+
+
+function getExtraByAlias (alias) {
+ var res;
+
+ alias = "stage-" + alias;
+
+ res = getAvailableExtras().filter(function (t) {
+ return basename(t) == alias;
+ });
+
+ return res[0];
+}
+
+
+function getAvailableExtras () {
+ var dirs;
+ var extras;
+
+ extras = BUILTIN_EXTRAS.slice(0);
+
+ if ("STAGE_EXTRAS_PATH" in process.env) {
+ dirs = process.env["STAGE_EXTRAS_PATH"].split(":");
+ } else {
+ return extras;
+ }
+
+ dirs.forEach(function (dir) {
+ var files;
+
+ try {
+ files = readdirSync(dir).filter(function (file) {
+ return (/^stage\-/.test(file));
+ });
+ } catch (err) {
+ return;
+ }
+
+ files = files.map(function (file) {
+ return join(dir, file);
+ });
+
+ extras = extras.concat(files);
+ });
+
+ return extras;
+}
+
+
+if (process.mainModule == module) {
+ main();
+}
90 bin/stage-abort
@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var abortJob = require("../lib/api").abortJob;
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+var toHttpUrl = require("../lib/cliutil").toHttpUrl;
+
+var consts = require("../lib/consts");
+
+
+var DEFAULT_OPTIONS = { "url" : consts.DEFAULT_URL,
+ "json" : false,
+ "id" : null
+ };
+
+
+var options = Object.create(DEFAULT_OPTIONS);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var arg;
+
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage("<jobid>");
+
+ case "--help":
+ return help();
+
+ case "--url":
+ case "--hostname":
+ case "--port":
+ case "--token":
+ case "--secure":
+ options.url = url(options.url, arg, args.shift());
+ break;
+
+ case "--json":
+ options.json = true;
+ break;
+
+ default:
+
+ if (arg[0] == "-") {
+ halt("unknown option " + arg);
+ }
+
+ options.id = parseInt(arg, 10);
+ break;
+ }
+ }
+
+ options.url = toHttpUrl(options.url);
+
+ if (!options.id || isNaN(options.id)) {
+ halt("expected <jobid>");
+ }
+
+ abortJob(options.url, options.id, function (err, result) {
+ if (err) {
+ halt(err);
+ }
+
+ if (options.json) {
+ process.stdout.write(JSON.stringify(result));
+ return;
+ }
+
+ console.log("Job #%s was aborted succesfully", result.id);
+ });
+}
+
+
+if (process.argv[1] == __filename) {
+ main();
+}
87 bin/stage-clients
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+var toHttpUrl = require("../lib/cliutil").toHttpUrl;
+
+var listClients = require("../lib/api").listClients;
+
+var consts = require("../lib/consts");
+
+
+var DEFAULT_OPTIONS = { "url" : consts.DEFAULT_URL,
+ "json" : false
+ };
+
+
+var options = Object.create(DEFAULT_OPTIONS);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var arg;
+
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "--usage":
+ return usage();
+
+ case "--help":
+ return help();
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--url":
+ case "--hostname":
+ case "--port":
+ case "--token":
+ case "--secure":
+ options.url = url(options.url, arg, args.shift());
+ break;
+
+ case "--json":
+ options.json = true;
+ break;
+
+ default:
+ return halt("bad argument - " + arg);
+ }
+ }
+
+ options.url = toHttpUrl(options.url);
+
+ listClients(options.url, function (err, result) {
+ if (err) {
+ halt(err.message);
+ }
+
+ if (options.json) {
+ return console.log(JSON.stringify(result));
+ }
+
+ console.log("total %s", result.length);
+
+ result.forEach(function (c) {
+ console.log("#%s - %s - %s:%s",
+ c.id,
+ c.role.toUpperCase(),
+ c.remoteAddress,
+ c.remotePort
+ );
+ });
+ });
+}
+
+
+if (process.argv[1] == __filename) {
+ main();
+}
93 bin/stage-collect
@@ -0,0 +1,93 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var getReport = require("../lib/api").getReport;
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+var toHttpUrl = require("../lib/cliutil").toHttpUrl;
+
+var consts = require("../lib/consts");
+
+
+var DEFAULT_OPTIONS = { "url" : consts.DEFAULT_URL,
+ "json" : false,
+ "jobid" : null
+ };
+
+
+var options = Object.create(DEFAULT_OPTIONS);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var arg;
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage("<jobid>");
+
+ case "--help":
+ return help();
+
+ case "--url":
+ case "--hostname":
+ case "--port":
+ case "--token":
+ case "--secure":
+ options.url = url(options.url, arg, args.shift());
+ break;
+
+ case "--json":
+ options.json = true;
+ break;
+
+ default:
+
+ if (arg[0] == "-") {
+ halt("unknown option " + arg);
+ }
+
+ options.jobid = parseInt(arg, 10);
+ break;
+ }
+ }
+
+ options.url = toHttpUrl(options.url);
+
+ if (!options.jobid || isNaN(options.jobid)) {
+ halt("expected <jobid>");
+ }
+
+ getReport(options.url, options.jobid, function (err, report) {
+ if (err) {
+ halt(err.message);
+ }
+
+ if (options.json) {
+ return console.log(JSON.stringify(result));
+ }
+
+ console.log(report);
+ // console.log("total %s", list.length);
+ //
+ // list.forEach(function (job) {
+ // console.log("#%s %s %s", job.id, job.name, job.state);
+ // });
+ });
+}
+
+
+if (process.argv[1] == __filename) {
+ main();
+}
301 bin/stage-init-test
@@ -0,0 +1,301 @@
+#!/usr/bin/env node
+
+/***package
+{
+ "author": "{{=author}}",
+ "name": "{{=name}}",
+ "version": "{{=version}}",
+ "dependencies": {
+ },
+ "scripts": {
+ "test": "node test.js",
+ "stagetest": "node test.js"
+ },
+ "engines": {
+ "node": ">=0.7.7"
+ }
+}
+*/
+
+
+/***test
+
+// {{=name}}
+// This test can be used as a template for your own test. The script is divided
+// into two phases, compatible phase and setup phase. The compatible phase
+// checks if Stage signals are supported.
+//
+// If so, we need to signal the parent process once that all setup
+// routines is done.
+//
+// Generated by: {{=program}}, version {{=version}}
+
+"use strict";
+
+
+function runTest () {
+ // Your test code goes here...
+ console.log("Test finished successfully!!");
+ process.exit(0);
+}
+
+
+function initStage () {
+ var parent = parseInt(process.env["STAGE_PARENT_PID"]);
+ var signal = process.env["STAGE_PARENT_SIGNAL"];
+
+ // The PID of parent is set if we are executed as a Stage test
+ if (!parent || isNaN(parent)) {
+ return false;
+ }
+
+ // Wait for parent to signal us. When it does, start the test
+ process.stdin.resume();
+ process.on(signal, runTest);
+
+ // Stage was initialized for this test. Returning true indicates
+ // that we should send a signal to parent when we are ready to
+ // go.
+ return true;
+}
+
+
+function ready () {
+ var parent = parseInt(process.env["STAGE_PARENT_PID"]);
+ var signal = process.env["STAGE_PARENT_SIGNAL"];
+ process.kill(parent, signal);
+}
+
+// Check if this script was called by a Stage process. If not, it was
+// probably called by NPM or antoher testing suite.
+if (initStage() == true) {
+
+ // Do all your asynchronized calls here.....
+
+ // Signal the parent process when you are ready to start the test.
+ ready();
+} else {
+
+ // You can choose to exit the process with an exit code at this
+ // time. This template is running the test even if not called by
+ // Stage.
+ runTest();
+}
+*/
+
+"use strict";
+
+var exec = require("child_process").exec;
+
+var readFileSync = require("fs").readFileSync;
+var writeFileSync = require("fs").writeFileSync;
+var existsSync = require("fs").existsSync;
+var mkdirSync = require("fs").mkdirSync;
+var statSync = require("fs").statSync;
+
+var resolve = require("path").resolve;
+var basename = require("path").basename;
+var dirname = require("path").dirname;
+
+var format = require("util").format;
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+
+
+var DEFAULT_OPTIONS = { "basepath" : process.cwd(),
+ "author" : process.env["STAGE_AUTHOR"] ||
+ process.env["USER"],
+ "version" : "1.0.0",
+ "name" : null,
+ "force" : false
+ };
+
+
+var options = Object.create(DEFAULT_OPTIONS);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var packagePath;
+ var testPath;
+ var arg;
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage("<path>");
+
+ case "--help":
+ return help();
+
+ case "--force":
+ options.force = true;
+ break;
+
+ case "--package-author":
+ options.author = args.shift();
+ break;
+
+ case "--package-name":
+ options.name = args.shift();
+ break;
+
+ case "--package-version":
+ options.version = args.shift();
+ break;
+
+ default:
+
+ if (arg[0] == "-") {
+ halt("unknown option " + arg);
+ }
+
+ options.basepath = resolve(arg);
+ break;
+ }
+ }
+
+
+ if (!options.author) {
+ halt("Option --package-author must be set");
+ }
+
+ packagePath = resolve(options.basepath, "package.json");
+ testPath = resolve(options.basepath, "test.js");
+
+ if (existsSync(packagePath) && !options.force) {
+ halt("File '" + packagePath + "' already exists");
+ }
+
+ if (existsSync(testPath) && !options.force) {
+ halt("File '" + packagePath + "' already exists");
+ }
+
+ if (!options.name) {
+ options.name = basename(options.basepath);
+ }
+
+ mkdirp(options.basepath);
+
+ writePackageJson(packagePath);
+ writeTestJs(testPath);
+
+ console.log("Test '%s@%s' was successfully initialized",
+ options.name,
+ options.version);
+}
+
+
+function writePackageJson (path) {
+ var content;
+ var context;
+
+ content = getNamedSection("package");
+
+ context = {
+ name : options.name,
+ version : options.version,
+ author : options.author
+ };
+
+ writeFileSync(path, template(content, context));
+}
+
+
+function writeTestJs (path) {
+ var content;
+ var context;
+
+ content = getNamedSection("test");
+
+ context = {
+ name : options.name,
+ program : basename(process.argv[1]),
+ version : require("../package").version
+ };
+
+ writeFileSync(path, template(content, context));
+}
+
+
+// Based on https://github.com/substack/node-mkdirp/blob/master/index.js
+function mkdirp (path, mode, made) {
+ var stat;
+
+ if (mode === undefined) {
+ mode = 0x1ff & (~process.umask());
+ }
+
+ if (!made) {
+ made = null;
+ }
+
+ path = resolve(path);
+
+ try {
+ mkdirSync(path, mode);
+ made = made || path;
+ } catch (er) {
+ if (er.code == "ENOENT") {
+ made = mkdirp(dirname(path), mode, made);
+ mkdirp(path, mode, made);
+ } else {
+ try {
+ stat = statSync(path);
+ } catch (err) {
+ throw er;
+ }
+ if (stat.isDirectory() == false) {
+ throw er;
+ }
+ }
+ }
+
+ return made;
+}
+
+
+function template (content, context) {
+ var re;
+ context = context || {};
+ for (var k in context) {
+ re = new RegExp("{{=" + k + "}}");
+ content = content.replace(re, context[k]);
+ }
+ return content;
+}
+
+
+function getNamedSection (name) {
+ var splitre = /(\/\*\*\*[^*]+\*\/)/;
+ var sectionre = /\/\*\*\*([a-z]+)\n([^*]+)\*\//gi;
+ var content;
+ var sections;
+ var section;
+ var match;
+
+ content = readFileSync(__filename).toString();
+ sections = content.split(splitre);
+
+ for (var i = 0; i < sections.length; i++) {
+ section = sections[i];
+ match = sectionre.exec(section);
+ if (match && match[1] == name) {
+ return match[2];
+ }
+ }
+}
+
+
+if (process.argv[1] == __filename) {
+ main();
+}
162 bin/stage-install
@@ -0,0 +1,162 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var statSync = require("fs").statSync;
+var readFileSync = require("fs").readFileSync;
+var existsSync = require("fs").existsSync;
+
+var parseUrl = require("url").parse;
+
+var resolve = require("path").resolve;
+var join = require("path").join;
+
+var spawn = require("child_process").spawn;
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+var toHttpUrl = require("../lib/cliutil").toHttpUrl;
+
+var prepareRequest = require("../lib/api").prepareRequest;
+
+
+var consts = require("../lib/consts");
+
+
+var DEFAULT_OPTIONS = { "url" : consts.DEFAULT_URL,
+ "force" : false,
+ "target" : null
+ };
+
+var options = Object.create(DEFAULT_OPTIONS);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var arg;
+ var tmpurl;
+ var pkg;
+
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage("<path>");
+
+ case "--help":
+ return help();
+
+ case "--url":
+ case "--hostname":
+ case "--port":
+ case "--token":
+ case "--secure":
+ options.url = url(options.url, arg, args.shift());
+ break;
+
+ case "--force":
+ options.url = url(options.url, arg, "1");
+ break;
+
+ default:
+
+ if (arg[0] == "-") {
+ halt("unknown option " + arg);
+ }
+
+ options.target = resolve(arg);
+ break;
+ }
+ }
+
+ options.url = toHttpUrl(options.url);
+
+ if (existsSync(options.target) == false ||
+ statSync(options.target).isDirectory() == false) {
+ halt("<path> must be a directory");
+ }
+
+ pkg = readFileSync(resolve(options.target, "package.json"));
+ pkg = JSON.parse(pkg);
+
+ if ("name" in pkg == false) {
+ halt("File '<path>/package.json' must contain the 'name' field");
+ }
+
+ if ("version" in pkg == false) {
+ halt("File '<path>/package.json' must contain the 'version' field");
+ }
+
+ if ("scripts" in pkg == false ||
+ ("stagetest" in pkg.scripts == false && "test" in pkg.scripts == false)) {
+ halt("Package <path> must contain either a 'stagetest' or a 'test' script");
+ }
+
+ install(options.target, options.url, function (err, result) {
+ if (err) {
+ halt(err.message);
+ }
+
+ console.log("Package '%s' was installed successfully", result.id);
+ });
+}
+
+
+function install (path, url, C) {
+ var child;
+ var opts;
+ var stderr;
+ var req;
+
+ opts = {
+ method : "POST",
+ url : url,
+ path : "tests",
+ json : true
+ };
+
+ req = prepareRequest(opts, function (err, result) {
+ child.kill("SIGKILL");
+ return C(err, result);
+ });
+
+ req.setHeader("Content-Type", "application/octet-stream");
+ req.setHeader("Transfer-Encoding", "chunked");
+
+ child = spawn("tar", ["-c", "."], { cwd: path });
+ child.stdout.pipe(req);
+
+ stderr = [];
+
+ child.stderr.on("data", function (chunk) {
+ stderr.push(chunk);
+ });
+
+ child.on("error", function (err) {
+ req.abort();
+ return C(err);
+ });
+
+ child.on("exit", function (code) {
+ var arg;
+
+ if (code) {
+ arg = Buffer.concat(stderr).toString();
+ req.abort();
+ return C(new Error(arg));
+ }
+ });
+}
+
+
+if (process.argv[1] == __filename) {
+ main();
+}
96 bin/stage-jobs
@@ -0,0 +1,96 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var listJobs = require("../lib/api").listJobs;
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+var toHttpUrl = require("../lib/cliutil").toHttpUrl;
+
+var consts = require("../lib/consts");
+
+
+var DEFAULT_OPTIONS = { "url" : consts.DEFAULT_URL,
+ "json" : false,
+ "monitor" : false
+ };
+
+
+var options = Object.create(DEFAULT_OPTIONS);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var arg;
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage();
+
+ case "--help":
+ return help();
+
+ case "--url":
+ case "--hostname":
+ case "--port":
+ case "--token":
+ case "--secure":
+ options.url = url(options.url, arg, args.shift());
+ break;
+
+ case "--monitor":
+ options.monitor = true;
+ break;
+
+ case "--json":
+ options.json = true;
+ break;
+
+ default:
+ return halt("bad argument - " + arg);
+ }
+ }
+
+ options.url = toHttpUrl(options.url);
+
+ listJobs(options.url, function (err, list) {
+ if (err) {
+ halt(err.message);
+ }
+
+ if (options.json) {
+ return console.log(JSON.stringify(list));
+ }
+
+ if (list.length == 0) {
+ console.log("No jobs are currently running");
+ return;
+ }
+
+ console.log("total %s", list.length);
+
+ list.forEach(function (job) {
+ console.log("#%s %s %s", job.id, job.name, job.state);
+ });
+ });
+}
+
+
+// function monitorjobs (url, C) {
+//
+// }
+//
+
+if (process.argv[1] == __filename) {
+ main();
+}
1,498 bin/stage-master
@@ -0,0 +1,1498 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var ok = require("assert").ok;
+var equal = require("assert").equal;
+var notEqual = require("assert").notEqual;
+
+var exec = require("child_process").exec;
+var spawn = require("child_process").spawn;
+
+var createDomain = require("domain").create;
+
+var EventEmitter = require("events").EventEmitter;
+
+var statSync = require("fs").statSync;
+var stat = require("fs").stat;
+var readdir = require("fs").readdir;
+var readFile = require("fs").readFile;
+var readFileSync = require("fs").readFileSync;
+var writeFile = require("fs").writeFile;
+var exists = require("fs").exists;
+var existsSync = require("fs").existsSync;
+var unlink = require("fs").unlink;
+
+var createServer = require("http").createServer;
+var createSecureServer = require("https").createServer;
+
+var basename = require("path").basename;
+var extname = require("path").extname;
+var join = require("path").join;
+var resolve = require("path").resolve;
+
+var parseUrl = require("url").parse;
+
+var inherits = require("util").inherits;
+var format = require("util").format;
+var log = require("util").log;
+
+var WebSocket = require("ws");
+var WebSocketServer = require("ws").Server;
+
+var version = require("../lib/cliutil").version;
+var usage = require("../lib/cliutil").usage;
+var help = require("../lib/cliutil").help;
+var halt = require("../lib/cliutil").halt;
+var url = require("../lib/cliutil").url;
+
+var consts = require("../lib/consts");
+
+var JOBSTATE_NA = consts.JOBSTATE_NA;
+var JOBSTATE_QUEUED = consts.JOBSTATE_QUEUED;
+var JOBSTATE_INITIALIZED = consts.JOBSTATE_INITIALIZED;
+var JOBSTATE_INSTALLING = consts.JOBSTATE_INSTALLING;
+var JOBSTATE_SETUP = consts.JOBSTATE_SETUP;
+var JOBSTATE_RUNNING = consts.JOBSTATE_RUNNING;
+var JOBSTATE_STOPPING = consts.JOBSTATE_STOPPING;
+var JOBSTATE_FINISHED = consts.JOBSTATE_FINISHED;
+var JOBSTATE_KILLED = consts.JOBSTATE_KILLED;
+
+var SOP_INSTALL = consts.SOP_INSTALL;
+var COP_INSTALL = consts.COP_INSTALL;
+var SOP_SETUP = consts.SOP_SETUP;
+var COP_SETUP = consts.COP_SETUP;
+var SOP_START = consts.SOP_START;
+var COP_START = consts.COP_START;
+var COP_RESULT = consts.COP_RESULT;
+var SOP_ABORT = consts.SOP_ABORT;
+var COP_TESTERROR = consts.COP_TESTERROR;
+var SOP_JOBSTATE = consts.SOP_JOBSTATE;
+
+var LOG_FATAL = consts.LOG_FATAL;
+var LOG_WARN = consts.LOG_WARN;
+var LOG_INFO = consts.LOG_INFO;
+var LOG_VERBOSE = consts.LOG_VERBOSE;
+var LOG_DEBUG = consts.LOG_DEBUG;
+
+var LOG_LEVELS = consts.LOG_LEVELS;
+
+
+var HTTPERROR = function(n,m){var e=Error(m);e.code=n;throw e;};
+var CLIENTIDGEN = (function $(){return $.c?(++$.c):($.c=1)});
+var JOBIDGEN = (function $(){return $.c?(++$.c):($.c=1)});
+var ROUTE = (function $(r,m,f){$[r+m]=[RegExp(r),m,f];});
+var MSGH = (function $(o,t,h,r){$[o]={t:t,h:h,r:r};});
+var FILELOCK = (function $(k){$[k]?delete $[k]:$[k]=1;});
+var LOGHANDLER = (function $(l,h){h.l=l;});
+
+
+
+var MANANGER_INTERVAL = 5000;
+var MANANGER_ERRORINTERVAL = 1000;
+
+var SERVER_ERRORINTERVAL = 1000;
+
+var DASHBOARD_BASE = resolve(__dirname, "../lib/dashboard");
+
+var CONTENT_TYPES = { ".html" : "text/html",
+ ".js" : "application/javascript",
+ ".css" : "text/css"
+ };
+
+var DEFAULT_OPTIONS = { "host" : null,
+ "token" : null,
+ "port" : 8080,
+ "maxjobs" : 5,
+ "loglevel" : consts.DEFAULT_LOGLEVEL,
+ "cachepath" : resolve("cache"),
+ "secure" : false,
+ "key" : "",
+ "cert" : "",
+ "lifetime" : 1440
+ };
+
+
+var options = Object.create(DEFAULT_OPTIONS);
+var serverDomain = null;
+var managerDomain = null;
+
+
+MSGH(COP_INSTALL, Job, "handleInstall", "slave");
+MSGH(COP_SETUP, Job, "handleSetup", "slave");
+MSGH(COP_RESULT, Job, "handleResult", "slave");
+MSGH(COP_TESTERROR, Job, "handleTestError", "slave");
+
+
+ROUTE("/info/$", "GET", infoHandler);
+ROUTE("/tests/$", "POST", installTestHandler);
+ROUTE("/tests/$", "GET", listTestsHandler);
+ROUTE("/tests/(.*)/$", "DELETE", uninstallTestHandler);
+ROUTE("/tests/(.*)/$", "GET", listTestsHandler);
+ROUTE("/jobs/(.*)/$", "POST", runTestHandler);
+ROUTE("/jobs/$", "GET", listJobsHandler);
+ROUTE("/jobs/(.*)$", "GET", getReportHandler);
+ROUTE("/jobs/(.*)/$", "DELETE", abortJobHandler);
+ROUTE("/clients/$", "GET", listClientsHandler);
+ROUTE("/clients/(slaves)/$", "GET", listClientsHandler);
+ROUTE("/$", "GET", staticHandler);
+ROUTE("/(favicon.ico)$", "GET", staticHandler);
+ROUTE("/static/(.*)$", "GET", staticHandler);
+
+
+LOGHANDLER(LOG_FATAL, logServerError);
+LOGHANDLER(LOG_FATAL, logJobManagerError);
+LOGHANDLER(LOG_INFO, logServerListen);
+LOGHANDLER(LOG_VERBOSE, logClientConnect);
+LOGHANDLER(LOG_VERBOSE, logClientDisconnect);
+LOGHANDLER(LOG_VERBOSE, logClientReject);
+LOGHANDLER(LOG_VERBOSE, logJobStateChange);
+LOGHANDLER(LOG_VERBOSE, logJobInitError);
+LOGHANDLER(LOG_DEBUG, logDebugInfo);
+LOGHANDLER(LOG_DEBUG, logRequestError);
+LOGHANDLER(LOG_DEBUG, logUnhandledMessage);
+LOGHANDLER(LOG_DEBUG, logRequest);
+
+
+function main () {
+ var args = process.argv.slice(2);
+ var arg;
+
+ while ((arg = args.shift())) {
+ switch (arg) {
+
+ case "-v":
+ case "--version":
+ return version();
+
+ case "--usage":
+ return usage("<host>");
+
+ case "--help":
+ return help();
+
+ case "-p":
+ case "--port":
+ options.port = parseInt(args.shift());
+ break;
+
+ case "--host":
+ options.host = args.shift();
+ break;
+
+ case "--token":
+ options.token = args.shift();
+ break;
+
+ case "--cachepath":
+ options.cachepath = resolve(args.shift());
+ break;
+
+ case "--loglevel":
+ options.loglevel = LOG_LEVELS.indexOf(args.shift());
+ break;
+
+ case "--maxjobs":
+ options.maxjobs = parseInt(args.shift());
+ break;
+
+ case "--secure":
+ options.secure = true;
+ break;
+
+ case "--key":
+ options.key = resolve(args.shift());
+ break;
+
+ case "--cert":
+ options.cert = resolve(args.shift());
+ break;
+
+ case "--joblifetime":
+ options.lifetime = parseInt(args.shift());
+ break;
+
+ default:
+ return halt("bad argument - " + arg);
+ }
+ }
+
+ if (options.secure && existsSync(options.key) == false) {
+ halt("specified key does not exists");
+ }
+
+ if (options.secure && existsSync(options.cert) == false) {
+ halt("specified cert does not exists");
+ }
+
+ if (isNaN(options.loglevel) ||
+ options.loglevel < 0 ||
+ options.loglevel > 5) {
+ halt("invalid log level");
+ }
+
+ if (isNaN(options.lifetime) || options.lifetime < 0) {
+ halt("invalid joblifetime");
+ }
+
+ if (existsSync(options.cachepath) == false ||
+ statSync(options.cachepath).isDirectory() == false) {
+ halt("option cache-path must be a directory");
+ }
+
+ if (isNaN(options.maxjobs) || options.maxjobs < 0) {
+ halt("max-jobs must be a positive number");
+ }
+
+ notif(logDebugInfo);
+
+ runServer();
+ runJobManager();
+}
+
+
+function notif (handler, a, b, c) {
+ if (handler.l < options.loglevel + 1) {
+ handler.call(this, a, b, c);
+ }
+}
+
+
+function runServer () {
+ var http;
+ var ws;
+
+ if (serverDomain) {
+ serverDomain.dispose();
+ }
+
+ serverDomain = createDomain();
+
+ serverDomain.on("error", function (err) {
+
+ this.members.forEach(function (member) {
+ if ("close" in member == false) return;
+ try { member.close(); } catch (err) {};
+ });
+
+ notif(logServerError, err, SERVER_ERRORINTERVAL);
+
+ setTimeout(runServer, SERVER_ERRORINTERVAL);
+ });
+
+ if (options.secure) {
+ http = createSecureServer({
+ key: readFileSync(options.key),
+ cert: readFileSync(options.cert)
+ });
+ } else {
+ http = createServer();
+ }
+ http.on("request", onrequest);
+
+ ws = new WebSocketServer({ server: http });
+ ws.on("connection", onclientconnect);
+
+ serverDomain.add(http);
+ serverDomain.add(ws);
+
+ http.listen(options.port, options.host);
+
+ http.on("error", function (err) {
+ console.log(err.message);
+ });
+
+ http.on("listening", function () {
+ notif(logServerListen, options.port, options.host);
+ });
+}
+
+
+function runJobManager () {
+ var interval;
+
+ if (managerDomain) {
+ managerDomain.dispose();
+ }
+
+ managerDomain = createDomain();
+
+ managerDomain.on("error", function (err) {
+ notif(logJobManagerError, err, MANANGER_ERRORINTERVAL);
+ setTimeout(runJobManager, MANANGER_ERRORINTERVAL);
+ });
+
+ function loop () {
+ var now = Date.now();
+ var jobsToDestroy = [];
+
+ Job.all.forEach(function (job) {
+ switch (job.state) {
+
+ case JOBSTATE_QUEUED:
+ job.init();
+ break;
+
+ case JOBSTATE_INITIALIZED:
+ job.install();
+ break;
+
+ case JOBSTATE_FINISHED:
+ if (now >= job.finishtime + (options.lifetime * 60 * 1000)) {
+ jobsToDestroy.push(job);
+ }
+ break;
+ }
+ });
+
+ jobsToDestroy.forEach(function (job) {
+ if (job.domain) {
+ job.domain.dispose()
+ } else {
+ job.destroy();
+ }
+ });
+ }
+
+ managerDomain.add(setInterval(loop, MANANGER_INTERVAL));
+}
+
+
+function onrequest (req, res) {
+ var reqd;
+ var url;
+ var cmd;
+ var m;
+
+ url = parseUrl(req.url, true);
+
+ notif(logRequest, req.method, url);
+
+ reqd = createDomain();
+
+ reqd.add(req);
+ reqd.add(res);
+
+ reqd.on("error", function (err) {
+ notif(logRequestError, err, url);
+ try {
+ if (err.name == "AssertionError") {
+ res.writeHead(400);
+ } else {
+ res.writeHead(err.code || 500);
+ }
+ res.end(err.message || "Unknown error");
+ res.on("close", function() {
+ reqd.dispose();
+ });
+ } catch (er) {
+ console.error("Error sending 500", err, req.url);
+ reqd.dispose();
+ }
+ });
+
+ reqd.run(function () {
+ var keys = Object.keys(ROUTE);
+ var route;
+ var expr;
+ var method;
+ var badmethod;
+ var auth;
+
+ for (var i = 0, l = keys.length; i < l; i++) {
+ route = ROUTE[keys[i]];
+ if ((expr = route[0].exec(url.pathname))) {
+ method = route[1];
+ cmd = route[2];
+
+ if (method !== req.method) {
+ badmethod = true;
+ continue;
+ }
+
+ if (options.token) {
+ auth = "Basic " + Buffer(":" + options.token).toString("base64");
+ if (options.token !== url.query.token &&
+ req.headers.authorization !== auth) {
+ res.setHeader("WWW-Authenticate", "Basic realm=\"stage\"");
+ res.writeHead(401, "Not Authorized");
+ res.end();
+ return;
+ }
+ }
+
+ return cmd(req, res, expr, url);
+ }
+ }
+
+ if (badmethod) {
+ HTTPERROR(405, "method not allowed");
+ } else {
+ HTTPERROR(404, "not found");
+ }
+ });
+}
+
+
+function onclientconnect (sock) {
+ var domain;
+
+ domain = createDomain();
+ domain.add(sock);
+
+ domain.on("error", onclientdomainerror);
+
+ sock.on("close", onclientclose);
+ sock.destroy = sockDestroyImpl;
+
+ domain.run(function () {
+ var req;
+ var url;
+
+ req = sock.upgradeReq;
+
+ url = parseUrl(req.url, true);
+
+ if (options.token) {
+ equal(options.token, url.query.token, "bad handshake token");
+ }
+
+ equal(/slave/.test(url.query.role), true, "Invalid role for connection");
+
+ sock.on("message", onclientmessage);
+ sock._validMasterClient = true;
+
+ sock.instance = new TestClient(sock, url.query);
+ domain.add(sock.instance);
+
+ notif(logClientConnect, sock.instance);
+ });
+}
+
+
+function onclientdomainerror (err) {
+ console.error(err.stack);
+ this.members.forEach(function (member) {
+ member.domainError = err;
+ });
+ this.dispose();
+}
+
+
+function onclientclose (sock) {
+ var idx;
+ var err;
+ var reason;
+
+ sock = this instanceof WebSocket ? this : sock;
+
+ err = sock.domainError || Error("Closed by remote part");
+
+ if (sock.instance) {
+ notif(logClientDisconnect, sock.instance, err.message);
+ sock.instance.destroy(err);
+ sock.instance = null;
+ }
+
+ if (sock.readyState == WebSocket.OPEN) {
+ sock.close(1002, err.message);
+ }
+}
+
+
+function onclientmessage (msg) {
+ var handlers;
+ var handler;
+ var instance;
+ var target;
+ var handled;
+ var idx;
+ var op;
+ var fn;
+
+ if (!(op = msg[0]) || !(handler = MSGH[op])) {
+ throw new Error("Bad command 0x" + op.toString(16));
+ }
+
+ if (!(instance = this.instance)) {
+ throw new Error("Invalid connection");
+ }
+
+ idx = (handlers = instance.handlers) && handlers.length || 0;
+
+ while (idx--) {
+ if ((target = handlers[idx]) instanceof handler.t &&
+ (typeof (fn = target[handler.h])) == "function" &&
+ (this.instance.role) == handler.r &&
+ (fn.call(target, instance, msg))) {
+ return;
+ }
+ }
+
+ notif(logUnhandledMessage, this.instance, msg);
+}
+
+
+function sockDestroyImpl () {
+ var err = this.domainError;
+ var reason = err ? err.message : "";
+
+ if (this.readyState == WebSocket.OPEN) {
+ try {
+ this.close(1002, reason);
+ } catch (err) {
+ }
+ }
+
+ if (this.instance) {
+ this.instance.destroy(err);
+ this.instance = null;
+ }
+
+ if ("_validMasterClient" in this == false) {
+ notif(logClientReject, this, reason);
+ }
+}
+
+
+function infoHandler (req, res) {
+ var context;
+
+ context = {
+ version : require("../package").version
+ };
+
+ writeJsonResponse(res, context);
+}
+
+
+function listTestsHandler (req, res, expr) {
+ var root = options.cachepath;
+ var result = [];
+ var regex;
+
+ regex = getPackageMatchRegExp(expr[1]);
+
+ readdir(root, function (err, files) {
+ if (err) throw err;
+
+ (function next () {
+ var current;
+
+ if (!(current = files.pop())) {
+ return writeJsonResponse(res, result);
+ }
+
+ current = join(root, current);
+
+ if (regex.test(current) == false) {
+ return next();
+ }
+
+ readPackageInfo(current, function (err, info) {
+ if (err) throw err;
+ result.push(info);
+ next();
+ });
+ })();
+
+ });
+}
+
+
+function installTestHandler (req, res, expr, url) {
+ var buffers = [];
+
+ equal(req.headers["content-type"], "application/octet-stream");
+
+ req.on("data", function (chunk) {
+ buffers.push(chunk);
+ });
+
+ req.on("end", function () {
+ var data = Buffer.concat(buffers);
+
+ if (data.length == 0) {
+ HTTPERROR(400, "expected data");
+ }
+
+ readPackageInfo(data, function (err, pkg) {
+ var filename;
+ var pkgname;
+
+ if (err) {
+ throw err;
+ }
+
+ pkgname = pkg.name + "@" + pkg.version;
+ filename = resolve(options.cachepath, pkgname + ".tar");
+
+ equal(filename in FILELOCK, false, "package is about to install");
+
+ FILELOCK(filename);
+
+ exists(filename, function (doexists) {
+ FILELOCK(filename);
+ if (url.query.force != "1") {
+ equal(doexists, false, "package already exists");
+ }
+ FILELOCK(filename);
+
+ writeFile(filename, data, function (err) {
+ FILELOCK(filename);
+
+ if (err) {
+ throw err;
+ }
+
+ writeJsonResponse(res, { id: pkgname });
+ });
+ });
+ });
+ });
+}
+
+
+function uninstallTestHandler (req, res, expr) {
+ var root = options.cachepath;
+ var regex;
+ var count = 0;
+
+ regex = getPackageMatchRegExp(expr[1]);
+
+ readdir(root, function (err, files) {
+ if (err) throw err;
+
+ (function next () {
+ var current;
+
+ if (!(current = files.pop())) {
+ notEqual(count, 0, "no such test(s)");
+ res.writeHead(200);
+ return res.end();
+ }
+
+ current = join(root, current);
+
+ if (regex.test(current) == false) {
+ return next();
+ }
+
+ // Ignore package if locked
+ if (current in FILELOCK) {
+ return next();
+ }
+
+ // TODO: Check that test is not currently running?
+
+ unlink(current, function (err) {
+ if (err) throw err;
+ count++;
+ next();
+ });
+ })();
+
+ });
+}
+
+
+function runTestHandler (req, res, expr, url) {
+ var root = options.cachepath;
+ var filename;
+ var data = "";
+
+ filename = resolve(root, expr[1] + ".tar");
+
+ req.setEncoding("utf8");
+
+ req.on("data", function (chunk) {
+ data += chunk;
+ });
+
+ req.on("end", function () {
+ exists(filename, function (doexists) {
+ var job;
+ var config;
+
+ equal(doexists, true, "package was not found: " + expr[1]);
+ equal(filename in FILELOCK, false, "package is about to install");
+
+ if (data.length) {
+ equal(req.headers["content-type"], "application/json", "expected json");
+ config = JSON.parse(data);
+ }
+
+ job = createJob(filename);
+
+ job.setConfig(config || {});
+
+ for (var k in url.query) {
+ switch (k) {
+ case "clients":
+ job.setRequestedCandidates(parseInt(url.query.clients));
+ break;
+ }
+ }
+
+ writeJsonResponse(res, job);
+ });
+ });
+}
+
+
+function listJobsHandler (req, res) {
+ writeJsonResponse(res, Job.all);
+}
+
+
+function abortJobHandler (req, res, expr) {
+ var data;
+ var job;
+
+ job = Job.byId(parseInt(expr[1], 10));
+
+ if (!job) {
+ HTTPERROR(404, "No such job");
+ }
+
+ if (job.isRunning() == false) {
+ HTTPERROR(400, "Job #" + job.id + "is not running");
+ }
+
+ job.abort();
+
+ writeJsonResponse(res, job);
+}
+
+
+function listClientsHandler (req, res, expr) {
+ var result;
+
+ switch (expr && expr[1]) {
+
+ case "slaves":
+ result = TestClient.slaves;
+ break;
+
+ default:
+ result = TestClient.all;
+ break;
+ }
+
+ writeJsonResponse(res, result);
+}
+
+
+function getReportHandler (req, res, expr) {
+ var data;
+ var job;
+ var report;
+
+ job = Job.byId(parseInt(expr[1], 10));
+
+ if (!job) {
+ HTTPERROR(404, "No such job");
+ }
+
+ report = job.getReport();
+
+ writeJsonResponse(res, report);
+}
+
+
+function staticHandler (req, res, expr) {
+ var path;
+
+ path = join(DASHBOARD_BASE, expr[1] ? expr[1] : "index.html");
+
+ exists(path, function (doexists) {
+
+ if (doexists == false) {
+ HTTPERROR(404, "not found");
+ }
+
+ readFile(path, function (err, data) {
+
+ if (err) {
+ HTTPERROR(500, err.message);
+ }
+
+ res.setHeader("Content-Type", CONTENT_TYPES[extname(path)]);
+ res.setHeader("Content-Length", data.length);
+ res.writeHead(200);
+ res.end(data);
+
+ });
+ });
+}
+
+
+function writeJsonResponse (res, obj) {
+ var data = JSON.stringify(obj);
+ res.setHeader("Content-Type", "application/json");
+ res.setHeader("Content-Length", data.length);
+ res.writeHead(200);
+ res.end(data);
+}
+
+
+function getPackageMatchRegExp (q) {
+ var regexp;
+ q = q && q.length ? q : "*";
+ regexp = new RegExp(q.replace(/\*/, "(.*)") + "(.*).tar");
+ return regexp;
+}
+
+
+function readPackageInfo (bufferOrPath, C) {
+
+ function onexit (err, stdout, stderr) {
+ if (err) return C(new Error(stderr));
+ return C(null, JSON.parse(stdout));
+ }
+
+ if (Buffer.isBuffer(bufferOrPath)) {
+ exec("tar -xO ./package.json", onexit).stdin.end(bufferOrPath);
+ } else {
+ exec("tar -xOf " + bufferOrPath + " ./package.json", onexit);
+ }
+}
+
+
+function createJob (path) {
+ var domain;
+ var job;
+
+ job = new Job(path);
+
+ domain = createDomain();
+ domain.add(job);
+
+ domain.on("error", function (err) {
+ job.finish(err);
+ });
+
+ return job;
+}
+
+
+function Job (path) {
+ EventEmitter.call(this);
+
+ this.id = JOBIDGEN();
+ this.name = basename(path, ".tar");
+ this.testpath = path;
+ this.createtime = Date.now();
+ this.starttime = null;
+ this.finishtime = null;
+ this.state = JOBSTATE_QUEUED;
+ this.lastError = null;
+
+ this.config = {};
+
+ this.report = { state: "initializing" };
+
+ this.requestedCandidates = 0;
+
+ this.installCount = 0;
+ this.setupCount = 0;
+ this.finishCount = 0;
+ this.candidateCount = 0;
+
+ this.candidates = null;
+ this.subscribers = [];
+
+ this.constructor.all.push(this);
+}
+
+inherits(Job, EventEmitter);
+
+Job.all = [];
+
+
+Object.defineProperty(Job, "runningJobs", {
+ get: function () {
+ Job.all.reduce(function (prev, curr) {
+ prev = typeof prev == "number" ? prev : 0;
+ return curr.isRunning() ? prev + 1 : prev;
+ });
+ }
+});
+
+
+Job.byId = function (id) {
+ var job;
+ for (var i = 0, l = Job.all.length; i < l; i++) {
+ job = Job.all[i];
+ if (job.id == id) {
+ return job;
+ }
+ }
+ return null;
+};
+
+
+Job.prototype.setRequestedCandidates = function (val) {
+ if (isNaN(val) || val < 0) {
+ throw new Error("Bad value for requestedCandidates");
+ }
+ this.requestedCandidates = val;
+};
+
+
+Job.prototype.isRunning = function () {
+ return this.state !== JOBSTATE_QUEUED &&
+ this.state !== JOBSTATE_FINISHED;
+};
+
+
+Job.prototype.setState = function (newstate) {
+ this.state = newstate;
+ this.broadcastState();
+ notif(logJobStateChange, this);
+};
+
+
+Job.prototype.setConfig = function (config) {
+ for (var k in config) {
+ this.config[k] = config[k];
+ }
+};
+
+
+Job.prototype.init = function () {
+ var self = this;
+ var candidates;
+
+ if (options.maxjobs && Job.runningJobs + 1 > options.maxjobs) {
+ notif(logJobInitError, this, "Too many running jobs");
+ return;
+ }
+
+ candidates = [];
+
+ TestClient.slaves.forEach(function (c) {
+ // TODO: More filters when choosing candidates
+ if (candidates.length < self.requestedCandidates ||
+ self.requestedCandidates === 0) {
+ candidates.push(c);
+ }
+ });
+
+ if (candidates.length < 1 ||
+ (self.requestedCandidates !== 0 &&
+ candidates.length != self.requestedCandidates)) {
+ notif(logJobInitError, this, "Not enough candidates");
+ return;
+ }
+
+ candidates.forEach(function (candidate) {
+ candidate.addHandler(self);
+ });
+
+ this.candidates = candidates;
+ this.candidateCount = candidates.length;
+
+ this.report = { state: "running" };
+ this.setState(JOBSTATE_INITIALIZED);
+};
+
+
+Job.prototype.install = function () {
+ var id = this.id;
+ var candidates = this.candidates;
+
+ // Check if we need to wait until the test is
+ // installed on local
+ if (this.testPath in FILELOCK) {
+ return;
+ }
+
+ this.setState(JOBSTATE_INSTALLING);
+
+ readFile(this.testpath, function (err, data) {
+ var msg;
+
+ if (err) throw err;
+
+ msg = new Buffer(data.length + 5);
+ msg.writeUInt8(SOP_INSTALL, 0);
+ msg.writeUInt32BE(id, 1);
+ data.copy(msg, 5);
+
+ candidates.forEach(function (candidate) {
+ candidate.send(msg);
+ });
+ });
+
+};
+
+
+Job.prototype.setup = function () {
+ var candidates = this.candidates;
+ var id = this.id;
+ var name = this.name;
+ var config = this.config;
+
+ this.setState(JOBSTATE_SETUP);
+
+ candidates.forEach(function (candidate) {
+ var cfg;
+ var ctx;
+ var msg;
+
+ cfg = {};
+
+ for (var k in config) {
+ cfg[k] = config[k];
+ }
+
+ config["name"] = name;
+ config["clientid"] = candidates.indexOf(candidate) + 1;
+
+ ctx = JSON.stringify(config);
+
+ msg = new Buffer(ctx.length + 5);
+ msg.writeUInt8(SOP_SETUP, 0);
+ msg.writeUInt32BE(id, 1);
+ msg.write(ctx, 5);
+
+ candidate.send(msg);
+ });
+};
+
+
+Job.prototype.start = function () {
+ var candidates = this.candidates;
+ var msg;
+
+ this.starttime = Date.now();
+ this.setState(JOBSTATE_RUNNING);
+
+ msg = new Buffer(5);
+ msg.writeUInt8(SOP_START, 0);
+ msg.writeUInt32BE(this.id, 1);
+
+ candidates.forEach(function (candidate) {
+ candidate.send(msg);
+ });
+};
+
+
+Job.prototype.handleTestError = function (candidate, msg) {
+ var reason;
+ var idx;
+
+ if (msg.readUInt32BE(1) !== this.id) {
+ return false;
+ }
+
+ candidate.removeHandler(this);
+ idx = this.candidates.indexOf(candidate);
+ this.candidates.splice(idx, 1);
+
+ reason = msg.toString("utf8", 5);
+
+ this.abort(Error("Slave #" +
+ candidate.id +
+ " had a critical error: " +
+ reason));
+
+ return true;
+};
+
+
+Job.prototype.handleInstall = function (client, msg) {
+
+ if (msg.readUInt32BE(1) !== this.id) {
+ return false;
+ }
+
+ if (this.state !== JOBSTATE_INSTALLING) {
+ return true;
+ }
+
+ if (++this.installCount == this.candidateCount) {
+ this.setup();
+ }
+
+ return true;
+};
+
+
+Job.prototype.handleSetup = function (client, msg) {
+
+ if (msg.readUInt32BE(1) !== this.id) {
+ return false;
+ }
+
+ if (this.state !== JOBSTATE_SETUP) {
+ return true;
+ }
+
+ if (++this.setupCount == this.candidateCount) {
+ this.start();
+ }
+
+ return true;
+};
+
+
+Job.prototype.handleResult = function (client, msg) {
+
+ if (msg.readUInt32BE(1) !== this.id) {
+ return false;
+ }
+
+ if (this.state !== JOBSTATE_RUNNING) {
+ return true;
+ }
+
+ this.report.state = "collecting";
+ this.report.collection = this.report.collection || [];
+ this.report.collection.push(msg.toString("utf8", 5));
+
+ console.log("----- Test result ----");
+ console.log(msg.toString("utf8", 5));
+
+ if (++this.finishCount == this.candidateCount) {
+ this.report.state = "ok";
+ this.finish();
+ }
+
+ return true;
+};
+
+
+Job.prototype.handleDisconnect = function (candidate, reason) {
+ var idx;
+