Files
meteor/tools/selftest.js
David Glasser 9401572041 Remove broken do-not-update-to-rcs test
It was broken because fake-warehouse and test-package-server were
incompatible.  In order to actually use the published packages, we had
to sync test-package-server into the local warehouse.  But as soon as we
did that, the latest release became some other random release.

In any case, the semantics tested here have changed twice since 0.9.0!
In 0.9.0.1 we changed it from "ask for any RC means get any RC" to
"don't get any RCs that aren't explicitly anticipated".  And in 1.1
we're changing it to "minimize unanticipated RCs".

Manually running through the equivalent of this test, most of it works
except for the last line; `meteor update` will not take you from one RC
to the next, due to minimizing unanticipated RCs.  That's probably fine
though.

While we're at it, make it much more clear that warehouse and
test-package-server don't work together.
2015-03-31 13:08:53 -04:00

1918 lines
60 KiB
JavaScript

var _ = require('underscore');
var files = require('./files.js');
var utils = require('./utils.js');
var parseStack = require('./parse-stack.js');
var release = require('./release.js');
var catalog = require('./catalog.js');
var archinfo = require('./archinfo.js');
var Future = require('fibers/future');
var isopackets = require("./isopackets.js");
var config = require('./config.js');
var buildmessage = require('./buildmessage.js');
var util = require('util');
var child_process = require('child_process');
var webdriver = require('browserstack-webdriver');
var phantomjs = require('phantomjs');
var catalogRemote = require('./catalog-remote.js');
var Console = require('./console.js').Console;
var tropohouseModule = require('./tropohouse.js');
var packageMapModule = require('./package-map.js');
var isopackCacheModule = require('./isopack-cache.js');
// Exception representing a test failure
var TestFailure = function (reason, details) {
var self = this;
self.reason = reason;
self.details = details || {};
self.stack = (new Error).stack;
};
// Use this to decorate functions that throw TestFailure. Decorate the
// first function that should not be included in the call stack shown
// to the user.
var markStack = function (f) {
return parseStack.markTop(f);
};
// Call from a test to throw a TestFailure exception and bail out of the test
var fail = markStack(function (reason) {
throw new TestFailure(reason);
});
// Call from a test to assert that 'actual' is equal to 'expected',
// with 'actual' being the value that the test got and 'expected'
// being the expected value
var expectEqual = markStack(function (actual, expected) {
var Package = isopackets.load('ejson');
if (! Package.ejson.EJSON.equals(actual, expected)) {
throw new TestFailure("not-equal", {
expected: expected,
actual: actual
});
}
});
// Call from a test to assert that 'actual' is truthy.
var expectTrue = markStack(function (actual) {
if (! actual) {
throw new TestFailure('not-true');
}
});
// Call from a test to assert that 'actual' is falsey.
var expectFalse = markStack(function (actual) {
if (actual) {
throw new TestFailure('not-false');
}
});
var expectThrows = markStack(function (f) {
var threw = false;
try {
f();
} catch (e) {
threw = true;
}
if (! threw)
throw new TestFailure("expected-exception");
});
// Execute a command synchronously, discarding stderr.
var execFileSync = function (binary, args, opts) {
return Future.wrap(function(cb) {
var cb2 = function(err, stdout, stderr) { cb(err, stdout); };
child_process.execFile(binary, args, opts, cb2);
})().wait();
};
var doOrThrow = function (f) {
var ret;
var messages = buildmessage.capture(function () {
ret = f();
});
if (messages.hasMessages()) {
throw Error(messages.formatMessages());
}
return ret;
};
// Our current strategy for running tests that need warehouses is to build all
// packages from the checkout into this temporary tropohouse directory, and for
// each test that need a fake warehouse, copy the built packages into the
// test-specific warehouse directory. This isn't particularly fast, but it'll
// do for now. We build the packages during the first test that needs them.
var builtPackageTropohouseDir = null;
var tropohouseLocalCatalog = null;
var tropohouseIsopackCache = null;
// Let's build a minimal set of packages that's enough to get self-test
// working. (And that doesn't need us to download any Atmosphere packages.)
var ROOT_PACKAGES_TO_BUILD_IN_SANDBOX = [
// We need the tool in order to run from the fake warehouse at all.
"meteor-tool",
// We need the packages in the skeleton app in order to test 'meteor create'.
"meteor-platform", "autopublish", "insecure"
];
var setUpBuiltPackageTropohouse = function () {
if (builtPackageTropohouseDir)
return;
builtPackageTropohouseDir = files.mkdtemp('built-package-tropohouse');
if (config.getPackagesDirectoryName() !== 'packages')
throw Error("running self-test with METEOR_PACKAGE_SERVER_URL set?");
var tropohouse = new tropohouseModule.Tropohouse(builtPackageTropohouseDir);
tropohouseLocalCatalog = newSelfTestCatalog();
var versions = {};
_.each(
tropohouseLocalCatalog.getAllNonTestPackageNames(),
function (packageName) {
versions[packageName] =
tropohouseLocalCatalog.getLatestVersion(packageName).version;
});
var packageMap = new packageMapModule.PackageMap(versions, {
localCatalog: tropohouseLocalCatalog
});
// Make an isopack cache that doesn't automatically save isopacks to disk and
// has no access to versioned packages.
tropohouseIsopackCache = new isopackCacheModule.IsopackCache({
packageMap: packageMap,
includeCordovaUnibuild: true
});
doOrThrow(function () {
buildmessage.enterJob("building self-test packages", function () {
// Build the packages into the in-memory IsopackCache.
tropohouseIsopackCache.buildLocalPackages(
ROOT_PACKAGES_TO_BUILD_IN_SANDBOX);
});
});
// Save all the isopacks into builtPackageTropohouseDir/packages. (Note that
// we are always putting them into the default 'packages' (assuming
// $METEOR_PACKAGE_SERVER_URL is not set in the self-test process itself) even
// though some tests will want them to be under
// 'packages-for-server/test-packages'; we'll fix this in _makeWarehouse.
tropohouseIsopackCache.eachBuiltIsopack(function (name, isopack) {
tropohouse._saveIsopack(isopack, name);
});
};
var newSelfTestCatalog = function () {
if (! files.inCheckout())
throw Error("Only can build packages from a checkout");
var catalogLocal = require('./catalog-local.js');
var selfTestCatalog = new catalogLocal.LocalCatalog;
var messages = buildmessage.capture(
{ title: "scanning local core packages" },
function () {
// When building a fake warehouse from a checkout, we use local packages,
// but *ONLY THOSE FROM THE CHECKOUT*: not app packages or $PACKAGE_DIRS
// packages. One side effect of this: we really really expect them to all
// build, and we're fine with dying if they don't (there's no worries
// about needing to springboard).
selfTestCatalog.initialize({
localPackageSearchDirs: [files.pathJoin(
files.getCurrentToolsDir(), 'packages')]
});
});
if (messages.hasMessages()) {
Console.arrowError("Errors while scanning core packages:");
Console.printMessages(messages);
throw new Error("scan failed?");
}
return selfTestCatalog;
};
///////////////////////////////////////////////////////////////////////////////
// Matcher
///////////////////////////////////////////////////////////////////////////////
// Handles the job of waiting until text is seen that matches a
// regular expression.
var Matcher = function (run) {
var self = this;
self.buf = "";
self.ended = false;
self.matchPattern = null;
self.matchFuture = null;
self.matchStrict = null;
self.run = run; // used only to set a field on exceptions
};
_.extend(Matcher.prototype, {
write: function (data) {
var self = this;
self.buf += data;
self._tryMatch();
},
match: function (pattern, timeout, strict) {
var self = this;
if (self.matchFuture)
throw new Error("already have a match pending?");
self.matchPattern = pattern;
self.matchStrict = strict;
var f = self.matchFuture = new Future;
self._tryMatch(); // could clear self.matchFuture
var timer = null;
if (timeout) {
timer = setTimeout(function () {
self.matchPattern = null;
self.matchStrict = null;
self.matchFuture = null;
f['throw'](new TestFailure('match-timeout', { run: self.run }));
}, timeout * 1000);
}
try {
return f.wait();
} finally {
if (timer)
clearTimeout(timer);
}
},
end: function () {
var self = this;
self.ended = true;
self._tryMatch();
},
matchEmpty: function () {
var self = this;
if (self.buf.length > 0) {
Console.info("Extra junk is :", self.buf);
throw new TestFailure('junk-at-end', { run: self.run });
}
},
_tryMatch: function () {
var self = this;
var f = self.matchFuture;
if (! f)
return;
var ret = null;
if (self.matchPattern instanceof RegExp) {
var m = self.buf.match(self.matchPattern);
if (m) {
if (self.matchStrict && m.index !== 0) {
self.matchFuture = null;
self.matchStrict = null;
self.matchPattern = null;
Console.info("Extra junk is: ", self.buf.substr(0, m.index));
f['throw'](new TestFailure(
'junk-before', { run: self.run, pattern: self.matchPattern }));
return;
}
ret = m;
self.buf = self.buf.slice(m.index + m[0].length);
}
} else {
var i = self.buf.indexOf(self.matchPattern);
if (i !== -1) {
if (self.matchStrict && i !== 0) {
self.matchFuture = null;
self.matchStrict = null;
self.matchPattern = null;
Console.info("Extra junk is: ", self.buf.substr(0, i));
f['throw'](new TestFailure('junk-before',
{ run: self.run, pattern: self.matchPattern }));
return;
}
ret = self.matchPattern;
self.buf = self.buf.slice(i + self.matchPattern.length);
}
}
if (ret !== null) {
self.matchFuture = null;
self.matchStrict = null;
self.matchPattern = null;
f['return'](ret);
return;
}
if (self.ended) {
var failure = new TestFailure('no-match', { run: self.run,
pattern: self.matchPattern });
self.matchFuture = null;
self.matchStrict = null;
self.matchPattern = null;
f['throw'](failure);
return;
}
}
});
///////////////////////////////////////////////////////////////////////////////
// OutputLog
///////////////////////////////////////////////////////////////////////////////
// Maintains a line-by-line merged log of multiple output channels
// (eg, stdout and stderr).
var OutputLog = function (run) {
var self = this;
// each entry is an object with keys 'channel', 'text', and if it is
// the last entry and there was no newline terminator, 'bare'
self.lines = [];
// map from a channel name to an object representing a partially
// read line of text on that channel. That object has keys 'text'
// (text read), 'offset' (cursor position, equal to text.length
// unless a '\r' has been read).
self.buffers = {};
// a Run, exclusively for inclusion in exceptions
self.run = run;
};
_.extend(OutputLog.prototype, {
write: function (channel, text) {
var self = this;
if (! _.has(self.buffers, 'channel'))
self.buffers[channel] = { text: '', offset: 0};
var b = self.buffers[channel];
while (text.length) {
var m = text.match(/^[^\n\r]+/);
if (m) {
// A run of non-control characters.
b.text = b.text.substr(0, b.offset) +
m[0] + b.text.substr(b.offset + m[0].length);
b.offset += m[0].length;
text = text.substr(m[0].length);
continue;
}
if (text[0] === '\r') {
b.offset = 0;
text = text.substr(1);
continue;
}
if (text[0] === '\n') {
self.lines.push({ channel: channel, text: b.text });
b.text = '';
b.offset = 0;
text = text.substr(1);
continue;
}
throw new Error("conditions should have been exhaustive?");
}
},
end: function () {
var self = this;
_.each(_.keys(self.buffers), function (channel) {
if (self.buffers[channel].text.length) {
self.lines.push({ channel: channel,
text: self.buffers[channel].text,
bare: true });
self.buffers[channel] = { text: '', offset: 0};
}
});
},
forbid: function (pattern, channel) {
var self = this;
_.each(self.lines, function (line) {
if (channel && channel !== line.channel)
return;
var match = (pattern instanceof RegExp) ?
(line.text.match(pattern)) : (line.text.indexOf(pattern) !== -1);
if (match)
throw new TestFailure('forbidden-string-present', { run: self.run });
});
},
get: function () {
var self = this;
return self.lines;
}
});
///////////////////////////////////////////////////////////////////////////////
// Sandbox
///////////////////////////////////////////////////////////////////////////////
// Represents an install of the tool. Creating this creates a private
// sandbox with its own state, separate from the state of the current
// meteor install or checkout, from the user's homedir, and from the
// state of any other sandbox. It also creates an empty directory
// which will be, by default, the cwd for runs created inside the
// sandbox (you can change this with the cd() method).
//
// This will throw TestFailure if it has to build packages to set up
// the sandbox and the build fails. So, only call it from inside
// tests.
//
// options:
// - warehouse: set to sandbox the warehouse too. If you don't do
// this, the tests are run in the same context (checkout or
// warehouse) as the actual copy of meteor you're running (the
// meteor in 'meteor self-test'. This may only be set when you're
// running 'meteor self-test' from a checkout. If it is set, it
// should look something like this:
// {
// version1: { tools: 'tools1', notices: (...) },
// version2: { tools: 'tools2', upgraders: ["a"],
// notices: (...), latest: true }
// }
// This would set up a simulated warehouse with two releases in it,
// one called 'version1' and having a tools version of 'tools1', and
// similarly with 'version2'/'tools2', with the latter being marked
// as the latest release, and the latter also having a single
// upgrader named "a". The releases are made by building the
// checkout into a release, and are identical except for their
// version names. If you pass 'notices' (which is optional), set it
// to the verbatim contents of the notices.json file for the
// release, as an object.
// - fakeMongo: if set, set an environment variable that causes our
// 'fake-mongod' stub process to be started instead of 'mongod'. The
// tellMongo method then becomes available on Runs for controlling
// the stub.
// - clients
// - browserstack: true if browserstack clients should be used
// - port: the port that the clients should run on
var Sandbox = function (options) {
var self = this;
// default options
options = _.extend({ clients: {} }, options);
self.root = files.mkdtemp();
self.warehouse = null;
self.home = files.pathJoin(self.root, 'home');
files.mkdir(self.home, 0755);
self.cwd = self.home;
self.env = {};
self.fakeMongo = options.fakeMongo;
// By default, tests use the package server that this meteor binary is built
// with. If a test is tagged 'test-package-server', it uses the test
// server. Tests that publish packages should have this flag; tests that
// assume that the release's packages can be found on the server should not.
// Note that this only affects subprocess meteor runs, not direct invocation
// of packageClient!
if (_.contains(runningTest.tags, 'test-package-server')) {
if (_.has(options, 'warehouse')) {
// test-package-server and warehouse are basically two different ways of
// sort of faking out the package system for tests. test-package-server
// means "use a specific production test server"; warehouse means "use
// some fake files we put on disk and never sync" (see _makeEnv where the
// offline flag is set). Combining them doesn't make sense: either you
// don't sync, in which case you don't see the stuff you published, or you
// do sync, and suddenly the mock catalog we built is overridden by
// test-package-server.
// XXX we should just run servers locally instead of either of these
// strategies
throw Error("test-package-server and warehouse cannot be combined");
}
self.set('METEOR_PACKAGE_SERVER_URL', exports.testPackageServerUrl);
}
if (_.has(options, 'warehouse')) {
if (!files.inCheckout())
throw Error("make only use a fake warehouse in a checkout");
self.warehouse = files.pathJoin(self.root, 'tropohouse');
self._makeWarehouse(options.warehouse);
}
self.clients = [new PhantomClient({
host: 'localhost',
port: options.clients.port || 3000
})];
if (options.clients && options.clients.browserstack) {
var browsers = [
{ browserName: 'firefox' },
{ browserName: 'chrome' },
{ browserName: 'internet explorer',
browserVersion: '11' },
{ browserName: 'internet explorer',
browserVersion: '8',
timeout: 60 },
{ browserName: 'safari' },
{ browserName: 'android' }
];
_.each(browsers, function (browser) {
self.clients.push(new BrowserStackClient({
host: 'localhost',
port: 3000,
browserName: browser.browserName,
browserVersion: browser.browserVersion,
timeout: browser.timeout
}));
});
}
var meteorScript = process.platform === "win32" ? "meteor.bat" : "meteor";
// Figure out the 'meteor' to run
if (self.warehouse)
self.execPath = files.pathJoin(self.warehouse, meteorScript);
else
self.execPath = files.pathJoin(files.getCurrentToolsDir(), meteorScript);
};
_.extend(Sandbox.prototype, {
// Create a new test run of the tool in this sandbox.
run: function (/* arguments */) {
var self = this;
return new Run(self.execPath, {
sandbox: self,
args: _.toArray(arguments),
cwd: self.cwd,
env: self._makeEnv(),
fakeMongo: self.fakeMongo
});
},
// Tests a set of clients with the argument function. Each call to f(run)
// instantiates a Run with a different client.
// Use:
// sandbox.testWithAllClients(function (run) {
// // pre-connection checks
// run.connectClient();
// // post-connection checks
// });
testWithAllClients: function (f) {
var self = this;
var argsArray = _.compact(_.toArray(arguments).slice(1));
console.log("running test with " + self.clients.length + " client(s).");
_.each(self.clients, function (client) {
console.log("testing with " + client.name + "...");
var run = new Run(self.execPath, {
sandbox: self,
args: argsArray,
cwd: self.cwd,
env: self._makeEnv(),
fakeMongo: self.fakeMongo,
client: client
});
run.baseTimeout = client.timeout;
f(run);
});
},
// Copy an app from a template into the current directory in the
// sandbox. 'to' is the subdirectory to put the app in, and
// 'template' is a subdirectory of tools/tests/apps to copy.
//
// Note that the arguments are the opposite order from 'cp'. That
// seems more intuitive to me -- if you disagree, my apologies.
//
// For example:
// s.createApp('myapp', 'empty');
// s.cd('myapp');
createApp: function (to, template, options) {
var self = this;
options = options || {};
files.cp_r(files.pathJoin(files.convertToStandardPath(__dirname), 'tests',
'apps', template),
files.pathJoin(self.cwd, to),
{ ignore: [/^local$/] });
// If the test isn't explicitly managing a mock warehouse, ensure that apps
// run with our release by default.
if (options.release) {
self.write(files.pathJoin(to, '.meteor/release'), options.release);
} else if (!self.warehouse && release.current.isProperRelease()) {
self.write(files.pathJoin(to, '.meteor/release'), release.current.name);
}
if (options.dontPrepareApp)
return;
// Prepare the app (ie, build or download packages). We give this a nice
// long timeout, which allows the next command to not need a bloated
// timeout. (meteor create does this anyway.)
self.cd(to, function () {
var run = self.run("--prepare-app");
// XXX Can we cache the output of running this once somewhere, so that
// multiple calls to createApp with the same template get the same cache?
// This is a little tricky because isopack-buildinfo.json uses absolute
// paths.
run.waitSecs(20);
run.expectExit(0);
});
},
// Same as createApp, but with a package.
//
// @param packageDir {String} The directory in which to create the package
// @param packageName {String} The package name to create. This string will
// replace all appearances of ~package-name~
// in any package*.js files in the template
// @param template {String} The package template to use. Found as a
// subdirectory in tests/packages/
//
// For example:
// s.createPackage('me_mypack', me:mypack', 'empty');
// s.cd('me_mypack');
createPackage: function (packageDir, packageName, template) {
var self = this;
var packagePath = files.pathJoin(self.cwd, packageDir);
var templatePackagePath = files.pathJoin(
files.convertToStandardPath(__dirname), 'tests', 'packages', template);
files.cp_r(templatePackagePath, packagePath);
_.each(files.readdir(packagePath), function (file) {
if (file.match(/^package.*\.js$/)) {
var packageJsFile = files.pathJoin(packagePath, file);
files.writeFile(
packageJsFile,
files.readFile(packageJsFile, "utf8")
.replace("~package-name~", packageName));
}
});
},
// Change the cwd to be used for subsequent runs. For example:
// s.run('create', 'myapp').expectExit(0);
// s.cd('myapp');
// s.run('add', 'somepackage') ...
// If you provide a callback, it will invoke the callback and then
// change the cwd back to the previous value. eg:
// s.cd('app1', function () {
// s.run('add', 'somepackage');
// });
// s.cd('app2', function () {
// s.run('add', 'somepackage');
// });
cd: function (relativePath, callback) {
var self = this;
var previous = self.cwd;
self.cwd = files.pathResolve(self.cwd, relativePath);
if (callback) {
callback();
self.cwd = previous;
}
},
// Set an environment variable for subsequent runs.
set: function (name, value) {
var self = this;
self.env[name] = value;
},
// Undo set().
unset: function (name) {
var self = this;
delete self.env[name];
},
// Write to a file in the sandbox, overwriting its current contents
// if any. 'filename' is a path intepreted relative to the Sandbox's
// cwd. 'contents' is a string (utf8 is assumed).
write: function (filename, contents) {
var self = this;
files.writeFile(files.pathJoin(self.cwd, filename), contents, 'utf8');
},
// Reads a file in the sandbox as a utf8 string. 'filename' is a
// path intepreted relative to the Sandbox's cwd. Returns null if
// file does not exist.
read: function (filename) {
var self = this;
var file = files.pathJoin(self.cwd, filename);
if (!files.exists(file))
return null;
else
return files.readFile(files.pathJoin(self.cwd, filename), 'utf8');
},
// Copy the contents of one file to another. In these series of tests, we often
// want to switch contents of package.js files. It is more legible to copy in
// the backup file rather than trying to write into it manually.
cp: function(from, to) {
var self = this;
var contents = self.read(from);
if (!contents) {
throw new Error("File " + from + " does not exist.");
};
self.write(to, contents);
},
// Delete a file in the sandbox. 'filename' is as in write().
unlink: function (filename) {
var self = this;
files.unlink(files.pathJoin(self.cwd, filename));
},
// Make a directory in the sandbox. 'filename' is as in write().
mkdir: function (dirname) {
var self = this;
var dirPath = files.pathJoin(self.cwd, dirname);
if (! files.exists(dirPath)) {
files.mkdir(dirPath);
}
},
// Rename something in the sandbox. 'oldName' and 'newName' are as in write().
rename: function (oldName, newName) {
var self = this;
files.rename(files.pathJoin(self.cwd, oldName),
files.pathJoin(self.cwd, newName));
},
// Return the current contents of .meteorsession in the sandbox.
readSessionFile: function () {
var self = this;
return files.readFile(files.pathJoin(self.root, '.meteorsession'), 'utf8');
},
// Overwrite .meteorsession in the sandbox with 'contents'. You
// could use this in conjunction with readSessionFile to save and
// restore authentication states.
writeSessionFile: function (contents) {
var self = this;
return files.writeFile(files.pathJoin(self.root, '.meteorsession'),
contents, 'utf8');
},
_makeEnv: function () {
var self = this;
var env = _.clone(self.env);
env.METEOR_SESSION_FILE = files.convertToOSPath(
files.pathJoin(self.root, '.meteorsession'));
if (self.warehouse) {
// Tell it where the warehouse lives.
env.METEOR_WAREHOUSE_DIR = files.convertToOSPath(self.warehouse);
// Don't ever try to refresh the stub catalog we made.
env.METEOR_OFFLINE_CATALOG = "t";
}
// By default (ie, with no mock warehouse and no --release arg) we should be
// testing the actual release this is built in, so we pretend that it is the
// latest release.
if (!self.warehouse && release.current.isProperRelease())
env.METEOR_TEST_LATEST_RELEASE = release.current.name;
return env;
},
// Writes a stub warehouse (really a tropohouse) to the directory
// self.warehouse. This warehouse only contains a meteor-tool package and some
// releases containing that tool only (and no packages).
//
// packageServerUrl indicates which package server we think we are using. Use
// the default, if we do not pass this in; you should pass it in any case that
// you will be specifying $METEOR_PACKAGE_SERVER_URL in the environment of a
// command you are running in this sandbox.
_makeWarehouse: function (releases) {
var self = this;
// Ensure we have a tropohouse to copy stuff out of.
setUpBuiltPackageTropohouse();
var serverUrl = self.env.METEOR_PACKAGE_SERVER_URL;
var packagesDirectoryName = config.getPackagesDirectoryName(serverUrl);
files.cp_r(files.pathJoin(builtPackageTropohouseDir, 'packages'),
files.pathJoin(self.warehouse, packagesDirectoryName),
{ preserveSymlinks: true });
var stubCatalog = {
syncToken: {},
formatVersion: "1.0",
collections: {
packages: [],
versions: [],
builds: [],
releaseTracks: [],
releaseVersions: []
}
};
var packageVersions = {};
var toolPackageVersion = null;
tropohouseIsopackCache.eachBuiltIsopack(function (packageName, isopack) {
var packageRec = tropohouseLocalCatalog.getPackage(packageName);
if (! packageRec)
throw Error("no package record for " + packageName);
stubCatalog.collections.packages.push(packageRec);
var versionRec = tropohouseLocalCatalog.getLatestVersion(packageName);
if (! versionRec)
throw Error("no version record for " + packageName);
stubCatalog.collections.versions.push(versionRec);
stubCatalog.collections.builds.push({
buildArchitectures: isopack.buildArchitectures(),
versionId: versionRec._id,
_id: utils.randomToken()
});
if (packageName === "meteor-tool") {
toolPackageVersion = versionRec.version;
} else {
packageVersions[packageName] = versionRec.version;
}
});
if (! toolPackageVersion)
throw Error("no meteor-tool?");
stubCatalog.collections.releaseTracks.push({
name: catalog.DEFAULT_TRACK,
_id: utils.randomToken()
});
// Now create each requested release.
_.each(releases, function (configuration, releaseName) {
// Release info
stubCatalog.collections.releaseVersions.push({
track: catalog.DEFAULT_TRACK,
_id: Math.random().toString(),
version: releaseName,
orderKey: releaseName,
description: "test release " + releaseName,
recommended: !!configuration.recommended,
tool: configuration.tool || "meteor-tool@" + toolPackageVersion,
packages: packageVersions
});
});
var dataFile = config.getPackageStorage({
root: self.warehouse,
serverUrl: serverUrl
});
self.warehouseOfficialCatalog = new catalogRemote.RemoteCatalog();
self.warehouseOfficialCatalog.initialize({
packageStorage: dataFile
});
self.warehouseOfficialCatalog.insertData(stubCatalog);
// And a cherry on top
// XXX this is hacky
files.linkToMeteorScript(
files.pathJoin(self.warehouse, packagesDirectoryName, "meteor-tool", toolPackageVersion,
'mt-' + archinfo.host(), 'meteor'),
files.pathJoin(self.warehouse, 'meteor'));
}
});
///////////////////////////////////////////////////////////////////////////////
// Client
///////////////////////////////////////////////////////////////////////////////
var Client = function (options) {
var self = this;
self.host = options.host;
self.port = options.port;
self.url = "http://" + self.host + ":" + self.port + '/' +
(Math.random() * 0x100000000 + 1).toString(36);
self.timeout = options.timeout || 40;
if (! self.connect || ! self.stop) {
console.log("Missing methods in subclass of Client.");
}
};
// PhantomClient
var PhantomClient = function (options) {
var self = this;
Client.apply(this, arguments);
self.name = "phantomjs";
self.process = null;
self._logError = true;
};
util.inherits(PhantomClient, Client);
_.extend(PhantomClient.prototype, {
connect: function () {
var self = this;
var phantomPath = phantomjs.path;
var scriptPath = files.pathJoin(files.getCurrentToolsDir(), "tools",
"phantom", "open-url.js");
self.process = child_process.execFile(phantomPath, ["--load-images=no",
files.convertToOSPath(scriptPath), self.url],
{}, function (error, stdout, stderr) {
if (self._logError && error) {
console.log("PhantomJS exited with error ", error, "\nstdout:\n", stdout, "\nstderr:\n", stderr);
}
});
},
stop: function() {
var self = this;
// Suppress the expected SIGTERM exit 'failure'
self._logError = false;
self.process && self.process.kill();
self.process = null;
}
});
// BrowserStackClient
var browserStackKey = null;
var BrowserStackClient = function (options) {
var self = this;
Client.apply(this, arguments);
self.tunnelProcess = null;
self.driver = null;
self.browserName = options.browserName;
self.browserVersion = options.browserVersion;
self.name = "BrowserStack - " + self.browserName;
if (self.browserVersion) {
self.name += " " + self.browserVersion;
}
};
util.inherits(BrowserStackClient, Client);
_.extend(BrowserStackClient.prototype, {
connect: function () {
var self = this;
// memoize the key
if (browserStackKey === null)
browserStackKey = self._getBrowserStackKey();
if (! browserStackKey)
throw new Error("BrowserStack key not found. Ensure that you " +
"have installed your S3 credentials.");
var capabilities = {
'browserName' : self.browserName,
'browserstack.user' : 'meteor',
'browserstack.local' : 'true',
'browserstack.key' : browserStackKey
};
if (self.browserVersion) {
capabilities.browserVersion = self.browserVersion;
}
self._launchBrowserStackTunnel(function (error) {
if (error)
throw error;
self.driver = new webdriver.Builder().
usingServer('http://hub.browserstack.com/wd/hub').
withCapabilities(capabilities).
build();
self.driver.get(self.url);
});
},
stop: function() {
var self = this;
self.tunnelProcess && self.tunnelProcess.kill();
self.tunnelProcess = null;
self.driver && self.driver.quit();
self.driver = null;
},
_getBrowserStackKey: function () {
var outputDir = files.pathJoin(files.mkdtemp(), "key");
try {
execFileSync("s3cmd", ["get",
"s3://meteor-browserstack-keys/browserstack-key",
outputDir
]);
return files.readFile(outputDir, "utf8").trim();
} catch (e) {
return null;
}
},
_launchBrowserStackTunnel: function (callback) {
var self = this;
var browserStackPath =
files.pathJoin(files.getDevBundle(), 'bin', 'BrowserStackLocal');
files.chmod(browserStackPath, 0755);
var args = [
browserStackPath,
browserStackKey,
[self.host, self.port, 0].join(','),
// Disable Live Testing and Screenshots, just test with Automate.
'-onlyAutomate',
// Do not wait for the server to be ready to spawn the process.
'-skipCheck'
];
self.tunnelProcess = child_process.execFile(
'/bin/bash',
['-c', args.join(' ')]
);
// Called when the SSH tunnel is established.
self.tunnelProcess.stdout.on('data', function(data) {
if (data.toString().match(/You can now access your local server/))
callback();
});
}
});
///////////////////////////////////////////////////////////////////////////////
// Run
///////////////////////////////////////////////////////////////////////////////
// Represents a test run of the tool (except we also use it in
// tests/old.js to run Node scripts). Typically created through the
// run() method on Sandbox, but can also be created directly, say if
// you want to do something other than invoke the 'meteor' command in
// a nice sandbox.
//
// Options: args, cwd, env
//
// The 'execPath' argument and the 'cwd' option are assumed to be standard
// paths.
//
// Arguments in the 'args' option are not assumed to be standard paths, so
// calling any of the 'files.*' methods on them is not safe.
var Run = function (execPath, options) {
var self = this;
self.execPath = execPath;
self.cwd = options.cwd || files.convertToStandardPath(process.cwd());
// default env variables
self.env = _.extend({ SELFTEST: "t", METEOR_NO_WORDWRAP: "t" }, options.env);
self._args = [];
self.proc = null;
self.baseTimeout = 20;
self.extraTime = 0;
self.client = options.client;
self.stdoutMatcher = new Matcher(self);
self.stderrMatcher = new Matcher(self);
self.outputLog = new OutputLog(self);
self.exitStatus = undefined; // 'null' means failed rather than exited
self.exitFutures = [];
var opts = options.args || [];
self.args.apply(self, opts || []);
self.fakeMongoPort = null;
self.fakeMongoConnection = null;
if (options.fakeMongo) {
self.fakeMongoPort = require('./utils.js').randomPort();
self.env.METEOR_TEST_FAKE_MONGOD_CONTROL_PORT = self.fakeMongoPort;
}
runningTest.onCleanup(function () {
self._stopWithoutWaiting();
});
};
_.extend(Run.prototype, {
// Set command-line arguments. This may be called multiple times as
// long as the run has not yet started (the run starts after the
// first call to a function that requires it, like match()).
//
// Pass as many arguments as you want. Non-object values will be
// cast to string, and object values will be treated as maps from
// option names to values.
args: function (/* arguments */) {
var self = this;
if (self.proc)
throw new Error("already started?");
_.each(_.toArray(arguments), function (a) {
if (typeof a !== "object") {
self._args.push('' + a);
} else {
_.each(a, function (value, key) {
self._args.push("--" + key);
self._args.push('' + value);
});
}
});
},
connectClient: function () {
var self = this;
if (! self.client)
throw new Error("Must create Run with a client to use connectClient().");
self._ensureStarted();
self.client.connect();
},
_exited: function (status) {
var self = this;
if (self.exitStatus !== undefined)
throw new Error("already exited?");
self.client && self.client.stop();
self.exitStatus = status;
var exitFutures = self.exitFutures;
self.exitFutures = null;
_.each(exitFutures, function (f) {
f['return']();
});
self.stdoutMatcher.end();
self.stderrMatcher.end();
},
_ensureStarted: function () {
var self = this;
if (self.proc)
return;
var env = _.clone(process.env);
_.extend(env, self.env);
self.proc = child_process.spawn(files.convertToOSPath(self.execPath),
self._args, {
cwd: files.convertToOSPath(self.cwd),
env: env
});
self.proc.on('close', function (code, signal) {
if (self.exitStatus === undefined)
self._exited({ code: code, signal: signal });
});
self.proc.on('exit', function (code, signal) {
if (self.exitStatus === undefined)
self._exited({ code: code, signal: signal });
});
self.proc.on('error', function (err) {
if (self.exitStatus === undefined)
self._exited(null);
});
self.proc.stdout.setEncoding('utf8');
self.proc.stdout.on('data', function (data) {
self.outputLog.write('stdout', data);
self.stdoutMatcher.write(data);
});
self.proc.stderr.setEncoding('utf8');
self.proc.stderr.on('data', function (data) {
self.outputLog.write('stderr', data);
self.stderrMatcher.write(data);
});
},
// Wait until we get text on stdout that matches 'pattern', which
// may be a regular expression or a string. Consume stdout up to
// that point. If this pattern does not appear after a timeout (or
// the program exits before emitting the pattern), fail.
match: markStack(function (pattern, _strict) {
var self = this;
self._ensureStarted();
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
return self.stdoutMatcher.match(pattern, timeout, _strict);
}),
// As expect(), but for stderr instead of stdout.
matchErr: markStack(function (pattern, _strict) {
var self = this;
self._ensureStarted();
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
return self.stderrMatcher.match(pattern, timeout, _strict);
}),
// Like match(), but won't skip ahead looking for a match. It must
// follow immediately after the last thing we matched or read.
read: markStack(function (pattern) {
return this.match(pattern, true);
}),
// As read(), but for stderr instead of stdout.
readErr: markStack(function (pattern) {
return this.matchErr(pattern, true);
}),
// Assert that 'pattern' (again, a regexp or string) has not
// occurred on stdout at any point so far in this run. Currently
// this works on complete lines, so unlike match() and read(),
// 'pattern' cannot span multiple lines, and furthermore if it is
// called before the end of the program, it may not see text on a
// partially read line. We could lift these restrictions easily, but
// there may not be any benefit since the usual way to use this is
// to call it after expectExit or expectEnd.
//
// Example:
// run = s.run("--help");
// run.expectExit(1); // <<-- improtant to actually run the command
// run.forbidErr("unwanted string"); // <<-- important to run **after** the
// // command ran the process.
forbid: markStack(function (pattern) {
this._ensureStarted();
this.outputLog.forbid(pattern, 'stdout');
}),
// As forbid(), but for stderr instead of stdout.
forbidErr: markStack(function (pattern) {
this._ensureStarted();
this.outputLog.forbid(pattern, 'stderr');
}),
// Combination of forbid() and forbidErr(). Forbids the pattern on
// both stdout and stderr.
forbidAll: markStack(function (pattern) {
this._ensureStarted();
this.outputLog.forbid(pattern);
}),
// Expect the program to exit without anything further being
// printed on either stdout or stderr.
expectEnd: markStack(function () {
var self = this;
self._ensureStarted();
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
self.expectExit();
self.stdoutMatcher.matchEmpty();
self.stderrMatcher.matchEmpty();
}),
// Expect the program to exit with the given (numeric) exit
// status. Fail if the process exits with a different code, or if
// the process does not exit after a timeout. You can also omit the
// argument to simply wait for the program to exit.
expectExit: markStack(function (code) {
var self = this;
self._ensureStarted();
if (self.exitStatus === undefined) {
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
var fut = new Future;
self.exitFutures.push(fut);
var timer = setTimeout(function () {
self.exitFutures = _.without(self.exitFutures, fut);
fut['throw'](new TestFailure('exit-timeout', { run: self }));
}, timeout * 1000);
try {
fut.wait();
} finally {
clearTimeout(timer);
}
}
if (! self.exitStatus)
throw new TestFailure('spawn-failure', { run: self });
if (code !== undefined && self.exitStatus.code !== code) {
throw new TestFailure('wrong-exit-code', {
expected: { code: code },
actual: self.exitStatus,
run: self
});
}
}),
// Extend the timeout for the next operation by 'secs' seconds.
waitSecs: function (secs) {
var self = this;
self.extraTime += secs;
},
// Send 'string' to the program on its stdin.
write: function (string) {
var self = this;
self._ensureStarted();
self.proc.stdin.write(string);
},
// Kill the program and then wait for it to actually exit.
stop: markStack(function () {
var self = this;
if (self.exitStatus === undefined) {
self._ensureStarted();
self.client && self.client.stop();
self._killProcess();
self.expectExit();
}
}),
// Like stop, but doesn't wait for it to exit.
_stopWithoutWaiting: function () {
var self = this;
if (self.exitStatus === undefined && self.proc) {
self.client && self.client.stop();
self._killProcess();
}
},
// Kills the running process and it's child processes
_killProcess: function () {
if (!this.proc)
throw new Error("Unexpected: `this.proc` undefined when calling _killProcess");
if (process.platform === "win32") {
// looks like in Windows `self.proc.kill()` doesn't kill child
// processes.
utils.execFileSync("taskkill", ["/pid", this.proc.pid, '/f', '/t']);
} else {
this.proc.kill();
}
},
// If the fakeMongo option was set, sent a command to the stub
// mongod. Available commands currently are:
//
// - { stdout: "xyz" } to make fake-mongod write "xyz" to stdout
// - { stderr: "xyz" } likewise for stderr
// - { exit: 123 } to make fake-mongod exit with code 123
//
// Blocks until a connection to fake-mongod can be
// established. Throws a TestFailure if it cannot be established.
tellMongo: markStack(function (command) {
var self = this;
if (! self.fakeMongoPort)
throw new Error("fakeMongo option on sandbox must be set");
self._ensureStarted();
// If it's the first time we've called tellMongo on this sandbox,
// open a connection to fake-mongod. Wait up to 10 seconds for it
// to accept the connection, retrying every 100ms.
//
// XXX we never clean up this connection. Hopefully once
// fake-mongod has dropped its end of the connection, and we hold
// no reference to our end, it will get gc'd. If not, that's not
// great, but it probably doesn't actually create any practical
// problems since this is only for testing.
if (! self.fakeMongoConnection) {
var net = require('net');
var lastStartTime = 0;
for (var attempts = 0; ! self.fakeMongoConnection && attempts < 100;
attempts ++) {
// Throttle attempts to one every 100ms
utils.sleepMs((lastStartTime + 100) - (+ new Date));
lastStartTime = +(new Date);
// Use an anonymous function so that each iteration of the
// loop gets its own values of 'fut' and 'conn'.
(function () {
var fut = new Future;
var conn = net.connect(self.fakeMongoPort, function () {
if (fut)
fut['return'](true);
});
conn.setNoDelay();
conn.on('error', function () {
if (fut)
fut['return'](false);
});
setTimeout(function () {
if (fut)
fut['return'](false); // 100ms connection timeout
}, 100);
// This is all arranged so that if a previous attempt
// belatedly succeeds, somehow, we ignore it.
if (fut.wait())
self.fakeMongoConnection = conn;
fut = null;
})();
}
if (! self.fakeMongoConnection)
throw new TestFailure("mongo-not-running", { run: self });
}
self.fakeMongoConnection.write(JSON.stringify(command) + "\n");
// If we told it to exit, then we should close our end and connect again if
// asked to send more.
if (command.exit) {
self.fakeMongoConnection.end();
self.fakeMongoConnection = null;
}
})
});
///////////////////////////////////////////////////////////////////////////////
// Defining tests
///////////////////////////////////////////////////////////////////////////////
var Test = function (options) {
var self = this;
self.name = options.name;
self.file = options.file;
self.fileHash = options.fileHash;
self.tags = options.tags || [];
self.f = options.func;
self.cleanupHandlers = [];
};
_.extend(Test.prototype, {
onCleanup: function (cleanupHandler) {
this.cleanupHandlers.push(cleanupHandler);
},
cleanup: function () {
var self = this;
_.each(self.cleanupHandlers, function (cleanupHandler) {
cleanupHandler();
});
self.cleanupHandlers = [];
}
});
var allTests = null;
var fileBeingLoaded = null;
var fileBeingLoadedHash = null;
var runningTest = null;
var getAllTests = function () {
if (allTests)
return allTests;
allTests = [];
// Load all files in the 'tests' directory that end in .js. They
// are supposed to then call define() to register their tests.
var testdir = files.pathJoin(__dirname, 'tests');
var filenames = files.readdir(testdir);
_.each(filenames, function (n) {
if (! n.match(/^[^.].*\.js$/)) // ends in '.js', doesn't start with '.'
return;
try {
if (fileBeingLoaded)
throw new Error("called recursively?");
fileBeingLoaded = files.pathBasename(n, '.js');
var fullPath = files.pathJoin(testdir, n);
var contents = files.readFile(fullPath, 'utf8');
fileBeingLoadedHash =
require('crypto').createHash('sha1').update(contents).digest('hex');
require(files.pathJoin(testdir, n));
} finally {
fileBeingLoaded = null;
fileBeingLoadedHash = null;
}
});
return allTests;
};
var define = function (name, tagsList, f) {
if (typeof tagsList === "function") {
// tagsList is optional
f = tagsList;
tagsList = [];
}
var tags = tagsList.slice();
tags.sort();
allTests.push(new Test({
name: name,
tags: tags,
file: fileBeingLoaded,
fileHash: fileBeingLoadedHash,
func: f
}));
};
///////////////////////////////////////////////////////////////////////////////
// Choosing tests
///////////////////////////////////////////////////////////////////////////////
var tagDescriptions = {
checkout: 'can only run from checkouts',
net: 'require an internet connection',
slow: 'take quite a long time; use --slow to include',
cordova: 'requires Cordova support in tool (eg not on Windows)',
windows: 'runs only on Windows',
// these are pseudo-tags, assigned to tests when you specify
// --changed, --file, or a pattern argument
unchanged: 'unchanged since last pass',
'non-matching': "don't match specified pattern",
'in other files': ""
};
// Returns a TestList object representing a filtered list of tests,
// according to the options given (which are based closely on the
// command-line arguments). Used as the first step of both listTests
// and runTests.
//
// Options: testRegexp, fileRegexp, onlyChanged, offline, includeSlowTests
var getFilteredTests = function (options) {
options = options || {};
var allTests = getAllTests();
if (allTests.length) {
var testState = readTestState();
// Add pseudo-tags 'non-matching', 'unchanged', and 'in other files'
// (but only so that we can then skip tests with those tags)
allTests = allTests.map(function (test) {
var newTags = [];
if (options.fileRegexp && ! options.fileRegexp.test(test.file)) {
newTags.push('in other files');
} else if (options.testRegexp && ! options.testRegexp.test(test.name)) {
newTags.push('non-matching');
} else if (options.onlyChanged &&
test.fileHash === testState.lastPassedHashes[test.file]) {
newTags.push('unchanged');
}
if (! newTags.length) {
return test;
}
return _.extend({}, test, { tags: test.tags.concat(newTags) });
});
}
// (order of tags is significant to the "skip counts" that are displayed)
var tagsToSkip = [];
if (options.fileRegexp) {
tagsToSkip.push('in other files');
}
if (options.testRegexp) {
tagsToSkip.push('non-matching');
}
if (options.onlyChanged) {
tagsToSkip.push('unchanged');
}
if (! files.inCheckout()) {
tagsToSkip.push('checkout');
}
if (options.offline) {
tagsToSkip.push('net');
}
if (! options.includeSlowTests) {
tagsToSkip.push('slow');
}
if (process.platform === "win32") {
tagsToSkip.push("cordova");
tagsToSkip.push("yet-unsolved-windows-failure");
} else {
tagsToSkip.push("windows");
}
return new TestList(allTests, tagsToSkip, testState);
};
// A TestList is the result of getFilteredTests. It holds the original
// list of all tests, the filtered list, and stats on how many tests
// were skipped (see generateSkipReport).
//
// TestList also has code to save the hashes of files where all tests
// ran and passed (for the `--changed` option). If a testState is
// provided, the notifyFailed and saveTestState can be used to modify
// the testState appropriately and write it out.
var TestList = function (allTests, tagsToSkip, testState) {
tagsToSkip = (tagsToSkip || []);
testState = (testState || null); // optional
var self = this;
self.allTests = allTests;
self.skippedTags = tagsToSkip;
self.skipCounts = {};
self.testState = testState;
_.each(tagsToSkip, function (tag) {
self.skipCounts[tag] = 0;
});
self.fileInfo = {}; // path -> {hash, hasSkips, hasFailures}
self.filteredTests = _.filter(allTests, function (test) {
if (! self.fileInfo[test.file]) {
self.fileInfo[test.file] = {
hash: test.fileHash,
hasSkips: false,
hasFailures: false
};
}
var fileInfo = self.fileInfo[test.file];
// We look for tagsToSkip *in order*, and when we decide to
// skip a test, we don't keep looking at more tags, and we don't
// add the test to any further "skip counts".
return !_.any(tagsToSkip, function (tag) {
if (_.contains(test.tags, tag)) {
self.skipCounts[tag]++;
fileInfo.hasSkips = true;
return true;
} else {
return false;
}
});
});
};
// Mark a test's file as having failures. This prevents
// saveTestState from saving its hash as a potentially
// "unchanged" file to be skipped in a future run.
TestList.prototype.notifyFailed = function (test) {
this.fileInfo[test.file].hasFailures = true;
};
// If this TestList was constructed with a testState,
// modify it and write it out based on which tests
// were skipped and which tests had failures.
TestList.prototype.saveTestState = function () {
var self = this;
var testState = self.testState;
if (! (testState && self.filteredTests.length)) {
return;
}
_.each(self.fileInfo, function (info, f) {
if (info.hasFailures) {
delete testState.lastPassedHashes[f];
} else if (! info.hasSkips) {
testState.lastPassedHashes[f] = info.hash;
}
});
writeTestState(testState);
};
// Return a string like "Skipped 1 foo test\nSkipped 5 bar tests\n"
TestList.prototype.generateSkipReport = function () {
var self = this;
var result = '';
_.each(self.skippedTags, function (tag) {
var count = self.skipCounts[tag];
if (count) {
var noun = "test" + (count > 1 ? "s" : ""); // "test" or "tests"
// "non-matching tests" or "tests in other files"
var nounPhrase = (/ /.test(tag) ?
(noun + " " + tag) : (tag + " " + noun));
// " (foo)" or ""
var parenthetical = (tagDescriptions[tag] ? " (" +
tagDescriptions[tag] + ")" : '');
result += ("Skipped " + count + " " + nounPhrase + parenthetical + '\n');
}
});
return result;
};
var getTestStateFilePath = function () {
return files.pathJoin(files.getHomeDir(), '.meteortest');
};
var readTestState = function () {
var testStateFile = getTestStateFilePath();
var testState;
if (files.exists(testStateFile))
testState = JSON.parse(files.readFile(testStateFile, 'utf8'));
if (! testState || testState.version !== 1)
testState = { version: 1, lastPassedHashes: {} };
return testState;
};
var writeTestState = function (testState) {
var testStateFile = getTestStateFilePath();
files.writeFile(testStateFile, JSON.stringify(testState), 'utf8');
};
// Same options as getFilteredTests. Writes to stdout and stderr.
var listTests = function (options) {
var testList = getFilteredTests(options);
if (! testList.allTests.length) {
Console.error("No tests defined.\n");
return;
}
_.each(_.groupBy(testList.filteredTests, 'file'), function (tests, file) {
Console.rawInfo(file + ':\n');
_.each(tests, function (test) {
Console.rawInfo(' - ' + test.name +
(test.tags.length ? ' [' + test.tags.join(' ') + ']'
: '') + '\n');
});
});
Console.error();
Console.error(testList.filteredTests.length + " tests listed.");
Console.error(testList.generateSkipReport());
};
///////////////////////////////////////////////////////////////////////////////
// Running tests
///////////////////////////////////////////////////////////////////////////////
// options: onlyChanged, offline, includeSlowTests, historyLines, testRegexp,
// fileRegexp,
// clients:
// - browserstack (need s3cmd credentials)
var runTests = function (options) {
var testList = getFilteredTests(options);
if (! testList.allTests.length) {
Console.error("No tests defined.");
return 0;
}
var totalRun = 0;
var failedTests = [];
_.each(testList.filteredTests, function (test) {
totalRun++;
Console.error(test.file + ": " + test.name + " ... ");
var failure = null;
try {
runningTest = test;
var startTime = +(new Date);
test.f(options);
} catch (e) {
failure = e;
} finally {
runningTest = null;
test.cleanup();
}
if (failure) {
Console.error("... fail!", Console.options({ indent: 2 }));
failedTests.push(test);
testList.notifyFailed(test);
if (failure instanceof TestFailure) {
var frames = parseStack.parse(failure);
var relpath = files.pathRelative(files.getCurrentToolsDir(),
frames[0].file);
Console.rawError(" => " + failure.reason + " at " +
relpath + ":" + frames[0].line + "\n");
if (failure.reason === 'no-match' || failure.reason === 'junk-before') {
Console.arrowError("Pattern: " + failure.details.pattern, 2);
}
if (failure.reason === "wrong-exit-code") {
var s = function (status) {
return status.signal || ('' + status.code) || "???";
};
Console.rawError(
" => " + "Expected: " + s(failure.details.expected) +
"; actual: " + s(failure.details.actual) + "\n");
}
if (failure.reason === 'expected-exception') {
}
if (failure.reason === 'not-equal') {
Console.rawError(
" => " + "Expected: " + JSON.stringify(failure.details.expected) +
"; actual: " + JSON.stringify(failure.details.actual) + "\n");
}
if (failure.details.run) {
failure.details.run.outputLog.end();
var lines = failure.details.run.outputLog.get();
if (! lines.length) {
Console.arrowError("No output", 2);
} else {
var historyLines = options.historyLines || 100;
Console.arrowError("Last " + historyLines + " lines:", 2);
_.each(lines.slice(-historyLines), function (line) {
Console.rawError(" " +
(line.channel === "stderr" ? "2| " : "1| ") +
line.text +
(line.bare ? "%" : "") + "\n");
});
}
}
if (failure.details.messages) {
Console.arrowError("Errors while building:", 2);
Console.rawError(failure.details.messages.formatMessages() + "\n");
}
} else {
Console.rawError(" => Test threw exception: " + failure.stack + "\n");
}
} else {
var durationMs = +(new Date) - startTime;
Console.error(
"... ok (" + durationMs + " ms)",
Console.options({ indent: 2 }));
}
});
testList.saveTestState();
if (totalRun > 0)
Console.error();
Console.error(testList.generateSkipReport());
if (testList.filteredTests.length === 0) {
Console.error("No tests run.");
return 0;
} else if (failedTests.length === 0) {
var disclaimers = '';
if (testList.filteredTests.length < testList.allTests.length)
disclaimers += " other";
Console.error("All" + disclaimers + " tests passed.");
return 0;
} else {
var failureCount = failedTests.length;
Console.error(failureCount + " failure" +
(failureCount > 1 ? "s" : "") + ":");
_.each(failedTests, function (test) {
Console.rawError(" - " + test.file + ": " + test.name + "\n");
});
return 1;
}
};
// To create self-tests:
//
// Create a new .js file in the tests directory. It will be picked
// up automatically.
//
// Start your file with something like:
// var selftest = require('../selftest.js');
// var Sandbox = selftest.Sandbox;
//
// Define tests with:
// selftest.define("test-name", ['tag1', 'tag2'], function () {
// ...
// });
//
// The tags are used to group tests. Currently used tags:
// - 'checkout': should only be run when we're running from a
// checkout as opposed to a released copy.
// - 'net': test requires an internet connection. Not going to work
// if you're on a plane; will be skipped if we appear to be
// offline unless run with 'self-test --force-online'.
// - 'slow': test is slow enough that you don't want to run it
// except on purpose. Won't run unless you say 'self-test --slow'.
//
// If you don't want to set any tags, you can omit that parameter
// entirely.
//
// Inside your test function, first create a Sandbox object, then call
// the run() method on the sandbox to set up a new run of meteor with
// arguments of your choice, and then use functions like match(),
// write(), and expectExit() to script that run.
_.extend(exports, {
runTests: runTests,
listTests: listTests,
markStack: markStack,
define: define,
Sandbox: Sandbox,
Run: Run,
fail: fail,
expectEqual: expectEqual,
expectThrows: expectThrows,
expectTrue: expectTrue,
expectFalse: expectFalse,
execFileSync: execFileSync,
doOrThrow: doOrThrow,
testPackageServerUrl: config.getTestPackageServerUrl()
});