Comparing version 4.0.1 to 4.1.0
815
ospec.js
"use strict" | ||
/* | ||
Ospec is made of four parts: | ||
1. a test definition API That creates a spec/tests tree | ||
2. a test runner that walks said spec tree | ||
3. an assertion API that populates a results array | ||
4. a reporter which presents the results | ||
The tepmoral sequence at run time is 1 then (2 and 3), then 4 | ||
The various sections (and sub-sections thereof) share information through stack-managed globals | ||
which are enumerated in the "Setup" section below. | ||
there are three kind of data structures, that reflect the above segregation: | ||
1. Specs, that group other specs and tasks | ||
2. Tasks, that represent hooks and tests, and internal logic | ||
3. Assertions which end up in the results array. | ||
At run-time, the specs are converted to lists of task (one per entry in the spec) | ||
In each of these tasks: | ||
- sub-specs receive the same treament as their parent, when their turn comes. | ||
- tests are also turned into lists of tasks [...beforeEach, test, ...afterEach] | ||
*/ | ||
;(function(m) { | ||
@@ -6,70 +32,165 @@ if (typeof module !== "undefined") module["exports"] = m() | ||
})(function init(name) { | ||
var spec = {}, subjects = [], results, only = [], ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty | ||
var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/), timeoutStackName | ||
// # Setup | ||
// const | ||
var hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty | ||
var hasSuiteName = arguments.length !== 0 | ||
var only = [] | ||
var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/) | ||
var rootSpec = new Spec() | ||
var subjects = [] | ||
// stack-managed globals | ||
var globalBail | ||
var globalContext = rootSpec | ||
var globalDepth = 1 | ||
var globalFile | ||
var globalTestOrHook = null | ||
var globalTimeout = noTimeoutRightNow | ||
var currentTestError = null | ||
if (name != null) spec[name] = ctx = {} | ||
var globalTimedOutAndPendingResolution = 0 | ||
try {throw new Error} catch (e) { | ||
var ospecFileName = e.stack && (/[\/\\](.*?):\d+:\d+/).test(e.stack) ? e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] : null | ||
// Shared state, set only once, but initialization is delayed | ||
var results, stats, timeoutStackName | ||
// # General utils | ||
function isRunning() {return results != null} | ||
function ensureStackTrace(error) { | ||
// mandatory to get a stack in IE 10 and 11 (and maybe other envs?) | ||
if (error.stack === undefined) try { throw error } catch(e) {return e} | ||
else return error | ||
} | ||
function getStackName(e, exp) { | ||
return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null | ||
} | ||
function noTimeoutRightNow() { | ||
throw new Error("`o.timeout()` must be called synchronously from within a test definition or a hook") | ||
} | ||
function timeoutParamDeprecationNotice(n) { | ||
console.error(new Error("`timeout()` as a test argument has been deprecated, use `o.timeout()`")) | ||
o.timeout(n) | ||
} | ||
// TODO: handle async functions? | ||
function validateDone(fn, error) { | ||
if (error == null || fn.length === 0) return | ||
var body = fn.toString() | ||
// Don't change the RegExp by hand, it is generated by | ||
// `scripts/build-done-parser.js`. | ||
// If needed, update the script and paste its output here. | ||
var arg = (body.match(/^(?:(?:function(?:\s|\/\*[^]*?\*\/|\/\/[^\n]*\n)*(?:\b[^\s(\/]+(?:\s|\/\*[^]*?\*\/|\/\/[^\n]*\n)*)?)?\((?:\s|\/\*[^]*?\*\/|\/\/[^\n]*\n)*)?([^\s{[),=\/]+)/) || []).pop() | ||
if (arg) { | ||
if(body.indexOf(arg) === body.lastIndexOf(arg)) { | ||
var doneError = new Error | ||
doneError.stack = "'" + arg + "()' should be called at least once\n" + o.cleanStackTrace(error) | ||
throw doneError | ||
} | ||
} else { | ||
console.warn("we couldn't determine the `done` callback name, please file a bug report at https://github.com/mithriljs/ospec/issues") | ||
arg = "done" | ||
} | ||
return "`" + arg + "()` should only be called once" | ||
} | ||
// # Spec definition | ||
function Spec() { | ||
this.before = [] | ||
this.beforeEach = [] | ||
this.after = [] | ||
this.afterEach = [] | ||
this.specTimeout = null | ||
this.customAssert = null | ||
this.children = Object.create(null) | ||
} | ||
// Used for both user-defined tests and internal book keeping | ||
// Internal tasks don't have an `err`. `hookName` is only defined | ||
// for hooks | ||
function Task(fn, err, hookName) { | ||
// This test needs to be here rather than in `o("name", test(){})` | ||
// in order to also cover nested hooks. | ||
if (isRunning() && err != null) throw new Error("Test definitions and hooks shouldn't be nested. To group tests, use 'o.spec()'") | ||
this.context = null | ||
this.file = globalFile | ||
// give tests an extra level of depth (simplifies bail out logic) | ||
this.depth = globalDepth + (hookName == null ? 1 : 0) | ||
this.doneTwiceError = validateDone(fn, err) || "A thenable should only be resolved once" | ||
this.error = err | ||
this.fn = fn | ||
this.hookName = hookName | ||
} | ||
function hook(name) { | ||
return function(predicate) { | ||
if (globalContext[name].length > 0) throw new Error("Attempt to register o." + name + "() more than once. A spec can only have one hook of each kind") | ||
globalContext[name][0] = new Task(predicate, ensureStackTrace(new Error), name) | ||
} | ||
} | ||
function unique(subject) { | ||
if (hasOwn.call(globalContext.children, subject)) { | ||
console.warn("A test or a spec named '" + subject + "' was already defined in this spec") | ||
console.warn(o.cleanStackTrace(ensureStackTrace(new Error)).split("\n")[0]) | ||
while (hasOwn.call(globalContext.children, subject)) subject += "*" | ||
} | ||
return subject | ||
} | ||
// # API | ||
function o(subject, predicate) { | ||
if (predicate === undefined) { | ||
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions") | ||
return new Assert(subject) | ||
return new Assertion(subject) | ||
} else { | ||
if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`") | ||
subject = String(subject) | ||
if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use") | ||
ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error)) | ||
globalContext.children[unique(subject)] = new Task(predicate, ensureStackTrace(new Error), null) | ||
} | ||
} | ||
o.before = hook("\x01before") | ||
o.after = hook("\x01after") | ||
o.beforeEach = hook("\x01beforeEach") | ||
o.afterEach = hook("\x01afterEach") | ||
o.before = hook("before") | ||
o.after = hook("after") | ||
o.beforeEach = hook("beforeEach") | ||
o.afterEach = hook("afterEach") | ||
o.specTimeout = function (t) { | ||
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()") | ||
if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context") | ||
if (globalContext.specTimeout != null) throw new Error("A default timeout has already been defined in this context") | ||
if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument") | ||
ctx["\x01specTimeout"] = t | ||
globalContext.specTimeout = t | ||
} | ||
o.new = init | ||
o.spec = function(subject, predicate) { | ||
var parent = ctx | ||
ctx = ctx[unique(subject)] = {} | ||
predicate() | ||
ctx = parent | ||
if (isRunning()) throw new Error("`o.spec()` can't only be called at test definition time, not run time") | ||
// stack managed globals | ||
var parent = globalContext | ||
var name = unique(subject) | ||
globalContext = globalContext.children[name] = new Spec() | ||
globalDepth++ | ||
try { | ||
predicate() | ||
} catch(e) { | ||
console.error(e) | ||
globalContext.children[name].children = {"> > BAILED OUT < < <": new Task(function(){ | ||
throw e | ||
}, ensureStackTrace(new Error), null)} | ||
} | ||
globalDepth-- | ||
globalContext = parent | ||
} | ||
o.only = function(subject, predicate, silent) { | ||
if (!silent) console.log( | ||
highlight("/!\\ WARNING /!\\ o.only() mode") + "\n" + o.cleanStackTrace(ensureStackTrace(new Error)) + "\n", | ||
cStyle("red"), "" | ||
) | ||
var onlyCalledAt = [] | ||
o.only = function(subject, predicate) { | ||
onlyCalledAt.push(o.cleanStackTrace(ensureStackTrace(new Error)).split("\n")[0]) | ||
only.push(predicate) | ||
o(subject, predicate) | ||
} | ||
o.spy = function(fn) { | ||
var spy = function() { | ||
spy.this = this | ||
spy.args = [].slice.call(arguments) | ||
spy.calls.push({this: this, args: spy.args}) | ||
spy.callCount++ | ||
if (fn) return fn.apply(this, arguments) | ||
} | ||
if (fn) | ||
Object.defineProperties(spy, { | ||
length: {value: fn.length}, | ||
name: {value: fn.name} | ||
}) | ||
spy.args = [] | ||
spy.calls = [] | ||
spy.callCount = 0 | ||
return spy | ||
} | ||
o.cleanStackTrace = function(error) { | ||
// For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack | ||
if (error.stack == null) return "" | ||
var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack | ||
var header = error.message ? error.name + ": " + error.message : error.name, stack | ||
// some environments add the name and message to the stack trace | ||
@@ -84,41 +205,137 @@ if (error.stack.indexOf(header) === 0) { | ||
// skip ospec-related entries on the stack | ||
while (stack[i] != null && stack[i].indexOf(ospecFileName) !== -1) i++ | ||
// now we're in user code (or past the stack end) | ||
return stack[i] | ||
return stack.filter(function(line) { return line.indexOf(ospecFileName) === -1 }).join("\n") | ||
} | ||
o.timeout = function(n) { | ||
globalTimeout(n) | ||
} | ||
// # Test runner | ||
var stack = [] | ||
var scheduled = false | ||
function cycleStack() { | ||
try { | ||
while (stack.length) stack.shift()() | ||
} finally { | ||
// Don't stop on error, but still let it propagate to the host as usual. | ||
if (stack.length) setTimeout(cycleStack, 0) | ||
else scheduled = false | ||
} | ||
} | ||
var nextTickish = hasProcess | ||
? process.nextTick | ||
: typeof Promise === "function" | ||
? Promise.prototype.then.bind(Promise.resolve()) | ||
: function fakeFastNextTick(next) { | ||
if (!scheduled) { | ||
scheduled = true | ||
setTimeout(cycleStack, 0) | ||
} | ||
stack.push(next) | ||
} | ||
o.metadata = function(opts) { | ||
if (arguments.length === 0) { | ||
if (!isRunning()) throw new Error("getting `o.metadata()` is only allowed at test run time") | ||
return { | ||
file: globalTestOrHook.file, | ||
name: globalTestOrHook.context | ||
} | ||
} else { | ||
if (isRunning() || globalContext !== rootSpec) throw new Error("setting `o.metadata()` is only allowed at the root, at test definition time") | ||
globalFile = opts.file | ||
} | ||
} | ||
o.run = function(reporter) { | ||
if (rootSpec !== globalContext) throw new Error("`o.run()` can't be called from within a spec") | ||
if (isRunning()) throw new Error("`o.run()` has already been called") | ||
results = [] | ||
start = new Date | ||
test(spec, [], [], new Task(function() { | ||
setTimeout(function () { | ||
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/) | ||
if (typeof reporter === "function") reporter(results) | ||
else { | ||
var errCount = o.report(results) | ||
if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit | ||
} | ||
}) | ||
}, null), 200 /*default timeout delay*/) | ||
stats = { | ||
asyncSuccesses: 0, | ||
bailCount: 0, | ||
onlyCalledAt: onlyCalledAt | ||
} | ||
function test(spec, pre, post, finalize, defaultDelay) { | ||
if (hasOwn.call(spec, "\x01specTimeout")) defaultDelay = spec["\x01specTimeout"] | ||
pre = [].concat(pre, spec["\x01beforeEach"] || []) | ||
post = [].concat(spec["\x01afterEach"] || [], post) | ||
series([].concat(spec["\x01before"] || [], Object.keys(spec).reduce(function(tasks, key) { | ||
if (key.charCodeAt(0) !== 1 && (only.length === 0 || only.indexOf(spec[key].fn) !== -1 || !(spec[key] instanceof Task))) { | ||
tasks.push(new Task(function(done) { | ||
o.timeout(Infinity) | ||
subjects.push(key) | ||
var pop = new Task(function pop() {subjects.pop(), done()}, null) | ||
if (spec[key] instanceof Task) series([].concat(pre, spec[key], post, pop), defaultDelay) | ||
else test(spec[key], pre, post, pop, defaultDelay) | ||
}, null)) | ||
} | ||
return tasks | ||
}, []), spec["\x01after"] || [], finalize), defaultDelay) | ||
if (hasSuiteName) { | ||
var parent = new Spec() | ||
parent.children[name] = rootSpec | ||
} | ||
var finalize = new Task(function() { | ||
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/) | ||
if (typeof reporter === "function") reporter(results, stats) | ||
else { | ||
var errCount = o.report(results, stats) | ||
if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit | ||
} | ||
}, null, null) | ||
// always async for consistent external behavior | ||
// otherwise, an async test would release Zalgo | ||
// https://blog.izs.me/2013/08/designing-apis-for-asynchrony | ||
nextTickish(function () { | ||
runSpec(hasSuiteName ? parent : rootSpec, [], [], finalize, 200 /*default timeout delay*/) | ||
}) | ||
function runSpec(spec, beforeEach, afterEach, finalize, defaultDelay) { | ||
var bailed = false | ||
if (spec.specTimeout) defaultDelay = spec.specTimeout | ||
// stack-managed globals | ||
var previousBail = globalBail | ||
globalBail = function() {bailed = true; stats.bailCount++} | ||
var restoreStack = new Task(function() { | ||
globalBail = previousBail | ||
}, null, null) | ||
beforeEach = [].concat( | ||
beforeEach, | ||
spec.beforeEach | ||
) | ||
afterEach = [].concat( | ||
spec.afterEach, | ||
afterEach | ||
) | ||
series( | ||
[].concat( | ||
spec.before, | ||
Object.keys(spec.children).reduce(function(tasks, key) { | ||
if ( | ||
// If in `only` mode, skip the tasks that are not flagged to run. | ||
only.length === 0 | ||
|| only.indexOf(spec.children[key].fn) !== -1 | ||
// Always run specs though, in case there are `only` tests nested in there. | ||
|| !(spec.children[key] instanceof Task) | ||
) { | ||
tasks.push(new Task(function(done) { | ||
if (bailed) return done() | ||
o.timeout(Infinity) | ||
subjects.push(key) | ||
var popSubjects = new Task(function pop() {subjects.pop(), done()}, null, null) | ||
if (spec.children[key] instanceof Task) { | ||
// this is a test | ||
series( | ||
[].concat(beforeEach, spec.children[key], afterEach, popSubjects), | ||
defaultDelay | ||
) | ||
} else { | ||
// a spec... | ||
runSpec(spec.children[key], beforeEach, afterEach, popSubjects, defaultDelay) | ||
} | ||
}, null, null)) | ||
} | ||
return tasks | ||
}, []), | ||
spec.after, | ||
restoreStack, | ||
finalize | ||
), | ||
defaultDelay | ||
) | ||
} | ||
// Executes a list of tasks in series. | ||
// This is quite convoluted because we handle both sync and async tasks. | ||
// Async tasks can either use a legacy `done(error?)` API, or return a | ||
// thenable, which may or may not behave like a Promise | ||
function series(tasks, defaultDelay) { | ||
@@ -131,98 +348,174 @@ var cursor = 0 | ||
// const | ||
var task = tasks[cursor++] | ||
var fn = task.fn | ||
currentTestError = task.err | ||
var timeout = 0, delay = defaultDelay, s = new Date | ||
var current = cursor | ||
var arg | ||
var isHook = task.hookName != null | ||
var isInternal = task.error == null | ||
var taskStartTime = new Date | ||
globalTimeout = setDelay | ||
// let | ||
var delay = defaultDelay | ||
var isAsync = false | ||
var isDone = false | ||
var isFinalized = false | ||
var timeout | ||
var isDone = false | ||
// public API, may only be called once from use code (or after returned Promise resolution) | ||
function done(err) { | ||
if (!isDone) isDone = true | ||
else throw new Error("`" + arg + "()` should only be called once") | ||
if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err)) | ||
finalizeAsync(err) | ||
if (!isInternal) { | ||
globalTestOrHook = task | ||
task.context = subjects.join(" > ") | ||
if (isHook) { | ||
task.context = "o." + task.hookName + Array.apply(null, {length: task.depth}).join("*") + "( " + task.context + " )" | ||
} | ||
} | ||
// for internal use only | ||
function finalizeAsync(err) { | ||
if (err == null) { | ||
if (task.err != null) succeed(new Assert) | ||
globalTimeout = function timeout (t) { | ||
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument") | ||
delay = t | ||
} | ||
try { | ||
if (fn.length > 0) { | ||
fn(done, timeoutParamDeprecationNotice) | ||
} else { | ||
if (err instanceof Error) fail(new Assert, err.message, err) | ||
else fail(new Assert, String(err), null) | ||
var p = fn() | ||
if (p && p.then) { | ||
// Use `_done`, not `finalize` here to defend against badly behaved thenables. | ||
// Let it crash if `then()` doesn't work as expected. | ||
p.then(function() { _done(null, false) }, function(e) {_done(e, true)}) | ||
} else { | ||
finalize(null, false, false) | ||
} | ||
} | ||
if (timeout !== undefined) timeout = clearTimeout(timeout) | ||
if (current === cursor) next() | ||
if (!isFinalized) { | ||
// done()/_done() haven't been called synchronously | ||
isAsync = true | ||
startTimer() | ||
} | ||
} | ||
catch (e) { | ||
if (isInternal) throw e | ||
else finalize(e, true, false) | ||
} | ||
globalTimeout = noTimeoutRightNow | ||
// public API, may only be called once from user code (or after the resolution | ||
// of a thenable that's been returned at the end of the test) | ||
function done(err) { | ||
// `!!err` would be more correct as far as node callback go, but we've been | ||
// using a `err != null` test for a while and no one complained... | ||
_done(err, err != null) | ||
} | ||
// common abstraction for node-style callbacks and thenables | ||
function _done(err, threw) { | ||
if (isDone) throw new Error(task.doneTwiceError) | ||
isDone = true | ||
if (isAsync && timeout === undefined) { | ||
globalTimedOutAndPendingResolution-- | ||
console.warn( | ||
task.context | ||
+ "\n# elapsed: " + Math.round(new Date - taskStartTime) | ||
+ "ms, expected under " + delay + "ms\n" | ||
+ o.cleanStackTrace(task.error)) | ||
} | ||
// temporary, for the "old style count" report | ||
if (!threw && task.error != null) {stats.asyncSuccesses++} | ||
if (!isFinalized) finalize(err, threw, false) | ||
} | ||
// called only for async tests | ||
function startTimer() { | ||
timeout = setTimeout(function() { | ||
timeout = undefined | ||
finalizeAsync("async test timed out after " + delay + "ms") | ||
}, Math.min(delay, 2147483647)) | ||
globalTimedOutAndPendingResolution++ | ||
finalize("async test timed out after " + delay + "ms\nWarning: assertions starting with `???` may not be properly labelled", true, true) | ||
}, Math.min(delay, 0x7fffffff)) | ||
} | ||
function setDelay (t) { | ||
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument") | ||
delay = t | ||
} | ||
if (fn.length > 0) { | ||
var body = fn.toString() | ||
arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop() | ||
if (body.indexOf(arg) === body.lastIndexOf(arg)) { | ||
var e = new Error | ||
e.stack = "`" + arg + "()` should be called at least once\n" + o.cleanStackTrace(task.err) | ||
throw e | ||
// common test finalization code path, for internal use only | ||
function finalize(err, threw, isTimeout) { | ||
if (isFinalized) { | ||
// failsafe for hacking, should never happen in released code | ||
throw new Error("Multiple finalization") | ||
} | ||
try { | ||
fn(done, setDelay) | ||
} | ||
catch (e) { | ||
if (task.err != null) finalizeAsync(e) | ||
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown. | ||
else throw e | ||
} | ||
if (timeout === 0) { | ||
startTimer() | ||
} | ||
} else { | ||
try{ | ||
var p = fn() | ||
if (p && p.then) { | ||
startTimer() | ||
p.then(function() { done() }, done) | ||
} else { | ||
nextTickish(next) | ||
isFinalized = true | ||
if (threw) { | ||
if (err instanceof Error) fail(new Assertion().i, err.message, err) | ||
else fail(new Assertion().i, String(err), null) | ||
if (!isTimeout) { | ||
globalBail() | ||
if (task.hookName === "beforeEach") { | ||
while (tasks[cursor].error != null && tasks[cursor].depth > task.depth) cursor++ | ||
} | ||
} | ||
} catch (e) { | ||
if (task.err != null) finalizeAsync(e) | ||
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown. | ||
else throw e | ||
} | ||
if (timeout !== undefined) timeout = clearTimeout(timeout) | ||
if (isAsync) next() | ||
else nextTickish(next) | ||
} | ||
globalTimeout = noTimeoutRightNow | ||
} | ||
} | ||
} | ||
function unique(subject) { | ||
if (hasOwn.call(ctx, subject)) { | ||
console.warn("A test or a spec named `" + subject + "` was already defined") | ||
while (hasOwn.call(ctx, subject)) subject += "*" | ||
// #Assertions | ||
function Assertion(value) { | ||
this.value = value | ||
this.i = results.length | ||
results.push({ | ||
pass: null, | ||
message: "Incomplete assertion in the test definition starting at...", | ||
error: globalTestOrHook.error, | ||
task: globalTestOrHook, | ||
timeoutLimbo: globalTimedOutAndPendingResolution === 0, | ||
// Deprecated | ||
context: (globalTimedOutAndPendingResolution === 0 ? "" : "??? ") + globalTestOrHook.context, | ||
testError: globalTestOrHook.error | ||
}) | ||
} | ||
function plainAssertion(verb, compare) { | ||
return function(self, value) { | ||
var success = compare(self.value, value) | ||
var message = serialize(self.value) + "\n " + verb + "\n" + serialize(value) | ||
if (success) succeed(self.i, message, null) | ||
else fail(self.i, message, null) | ||
} | ||
return subject | ||
} | ||
function hook(name) { | ||
return function(predicate) { | ||
if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate) | ||
ctx[name] = new Task(predicate, ensureStackTrace(new Error)) | ||
function define(name, assertion) { | ||
Assertion.prototype[name] = function assert(value) { | ||
var self = this | ||
assertion(self, value) | ||
return function(message) { | ||
results[self.i].message = message + "\n\n" + results[self.i].message | ||
} | ||
} | ||
} | ||
define("equals", "should equal", function(a, b) {return a === b}) | ||
define("notEquals", "should not equal", function(a, b) {return a !== b}) | ||
define("deepEquals", "should deep equal", deepEqual) | ||
define("notDeepEquals", "should not deep equal", function(a, b) {return !deepEqual(a, b)}) | ||
define("throws", "should throw a", throws) | ||
define("notThrows", "should not throw a", function(a, b) {return !throws(a, b)}) | ||
define("equals", plainAssertion("should equal", function(a, b) {return a === b})) | ||
define("notEquals", plainAssertion("should not equal", function(a, b) {return a !== b})) | ||
define("deepEquals", plainAssertion("should deep equal", deepEqual)) | ||
define("notDeepEquals", plainAssertion("should not deep equal", function(a, b) {return !deepEqual(a, b)})) | ||
define("throws", plainAssertion("should throw a", throws)) | ||
define("notThrows", plainAssertion("should not throw a", function(a, b) {return !throws(a, b)})) | ||
define("satisfies", function satisfies(self, check) { | ||
try { | ||
var res = check(self.value) | ||
if (res.pass) succeed(self.i, String(res.message), null) | ||
else fail(self.i, String(res.message), null) | ||
} catch (e) { | ||
results.pop() | ||
throw e | ||
} | ||
}) | ||
define("notSatisfies", function notSatisfies(self, check) { | ||
try { | ||
var res = check(self.value) | ||
if (!res.pass) succeed(self.i, String(res.message), null) | ||
else fail(self.i, String(res.message), null) | ||
} catch (e) { | ||
results.pop() | ||
throw e | ||
} | ||
}) | ||
@@ -235,2 +528,3 @@ function isArguments(a) { | ||
} | ||
function deepEqual(a, b) { | ||
@@ -250,3 +544,3 @@ if (a === b) return true | ||
} | ||
if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) { | ||
if (a.length === b.length && (Array.isArray(a) && Array.isArray(b) || aIsArgs && bIsArgs)) { | ||
var aKeys = Object.getOwnPropertyNames(a), bKeys = Object.getOwnPropertyNames(b) | ||
@@ -260,3 +554,3 @@ if (aKeys.length !== bKeys.length) return false | ||
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime() | ||
if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer) { | ||
if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer && a.length === b.length) { | ||
for (var i = 0; i < a.length; i++) { | ||
@@ -271,2 +565,3 @@ if (a[i] !== b[i]) return false | ||
} | ||
function throws(a, b){ | ||
@@ -285,34 +580,17 @@ try{ | ||
function isRunning() {return results != null} | ||
function Assert(value) { | ||
this.value = value | ||
this.i = results.length | ||
results.push({pass: null, context: "", message: "Incomplete assertion in the test definition starting at...", error: currentTestError, testError: currentTestError}) | ||
function succeed(i, message, error) { | ||
var result = results[i] | ||
result.pass = true | ||
result.message = message | ||
// for notSatisfies. Use the task.error for other passing assertions | ||
if (error != null) result.error = error | ||
} | ||
function Task(fn, err) { | ||
this.fn = fn | ||
this.err = err | ||
function fail(i, message, error) { | ||
var result = results[i] | ||
result.pass = false | ||
result.message = message | ||
result.error = error != null ? error : ensureStackTrace(new Error) | ||
} | ||
function define(name, verb, compare) { | ||
Assert.prototype[name] = function assert(value) { | ||
var self = this | ||
var message = serialize(self.value) + "\n " + verb + "\n" + serialize(value) | ||
if (compare(self.value, value)) succeed(self, message) | ||
else fail(self, message) | ||
return function(message) { | ||
if (!self.pass) self.message = message + "\n\n" + self.message | ||
} | ||
} | ||
} | ||
function succeed(assertion, message) { | ||
results[assertion.i].pass = true | ||
results[assertion.i].context = subjects.join(" > ") | ||
results[assertion.i].message = message | ||
} | ||
function fail(assertion, message, error) { | ||
results[assertion.i].pass = false | ||
results[assertion.i].context = subjects.join(" > ") | ||
results[assertion.i].message = message | ||
results[assertion.i].error = error != null ? error : ensureStackTrace(new Error) | ||
} | ||
function serialize(value) { | ||
@@ -324,5 +602,83 @@ if (hasProcess) return require("util").inspect(value) // eslint-disable-line global-require | ||
} | ||
function noTimeoutRightNow() { | ||
throw new Error("o.timeout must be called snchronously from within a test definition or a hook") | ||
// o.spy is functionally equivalent to this: | ||
// the extra complexity comes from compatibility issues | ||
// in ES5 environments where you can't overwrite fn.length | ||
// o.spy = function(fn) { | ||
// var spy = function() { | ||
// spy.this = this | ||
// spy.args = [].slice.call(arguments) | ||
// spy.calls.push({this: this, args: spy.args}) | ||
// spy.callCount++ | ||
// if (fn) return fn.apply(this, arguments) | ||
// } | ||
// if (fn) | ||
// Object.defineProperties(spy, { | ||
// length: {value: fn.length}, | ||
// name: {value: fn.name} | ||
// }) | ||
// spy.args = [] | ||
// spy.calls = [] | ||
// spy.callCount = 0 | ||
// return spy | ||
// } | ||
var spyFactoryCache = Object.create(null) | ||
function makeSpyFactory(name, length) { | ||
if (spyFactoryCache[name] == null) spyFactoryCache[name] = [] | ||
var args = Array.apply(null, {length: length}).map( | ||
function(_, i) {return "_" + i} | ||
).join(", "); | ||
var code = | ||
"'use strict';" + | ||
"var spy = (0, function " + name + "(" + args + ") {" + | ||
" return helper(this, [].slice.call(arguments), fn, spy)" + | ||
"});" + | ||
"return spy" | ||
return spyFactoryCache[name][length] = new Function("fn", "helper", code) | ||
} | ||
function getOrMakeSpyFactory(name, length) { | ||
return spyFactoryCache[name] && spyFactoryCache[name][length] || makeSpyFactory(name, length) | ||
} | ||
function spyHelper(self, args, fn, spy) { | ||
spy.this = self | ||
spy.args = args | ||
spy.calls.push({this: self, args: args}) | ||
spy.callCount++ | ||
if (fn) return fn.apply(self, args) | ||
} | ||
var supportsFunctionMutations = false; | ||
// eslint-disable-next-line no-empty, no-implicit-coercion | ||
try {supportsFunctionMutations = !!Object.defineProperties(function(){}, {name: {value: "a"},length: {value: 1}})} catch(_){} | ||
var supportsEval = false | ||
// eslint-disable-next-line no-new-func, no-empty | ||
try {supportsEval = Function("return true")()} catch(e){} | ||
o.spy = function spy(fn) { | ||
var name = "", length = 0 | ||
if (fn) name = fn.name, length = fn.length | ||
var spy = (!supportsFunctionMutations && supportsEval) | ||
? getOrMakeSpyFactory(name, length)(fn, spyHelper) | ||
: function(){return spyHelper(this, [].slice.call(arguments), fn, spy)} | ||
if (supportsFunctionMutations) Object.defineProperties(spy, { | ||
name: {value: name}, | ||
length: {value: length} | ||
}) | ||
spy.args = [] | ||
spy.calls = [] | ||
spy.callCount = 0 | ||
return spy | ||
} | ||
// Reporter | ||
var colorCodes = { | ||
@@ -333,2 +689,5 @@ red: "31m", | ||
} | ||
// console style for terminals | ||
// see https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences | ||
function highlight(message, color) { | ||
@@ -338,33 +697,48 @@ var code = colorCodes[color] || colorCodes.red; | ||
} | ||
// console style for the Browsers | ||
// see https://developer.mozilla.org/en-US/docs/Web/API/console#Styling_console_output | ||
function cStyle(color, bold) { | ||
return hasProcess||!color ? "" : "color:"+color+(bold ? ";font-weight:bold" : "") | ||
} | ||
function ensureStackTrace(error) { | ||
// mandatory to get a stack in IE 10 and 11 (and maybe other envs?) | ||
if (error.stack === undefined) try { throw error } catch(e) {return e} | ||
else return error | ||
function onlyWarning(onlyCalledAt) { | ||
var colors = Math.random() > 0.5 | ||
? { | ||
term: "red2", | ||
web: cStyle("red", true) | ||
} | ||
: { | ||
term: "re", | ||
web: cStyle("red") | ||
} | ||
if (onlyCalledAt && onlyCalledAt.length !== 0) { | ||
console.warn( | ||
highlight("\n/!\\ WARNING /!\\ o.only() called...\n", colors.term), | ||
colors.web, "" | ||
) | ||
console.warn(onlyCalledAt.join("\n")) | ||
console.warn( | ||
highlight("\n/!\\ WARNING /!\\ o.only()\n", colors.term), | ||
colors.web, "" | ||
) | ||
} | ||
} | ||
function getStackName(e, exp) { | ||
return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null | ||
} | ||
o.report = function (results) { | ||
var errCount = 0 | ||
o.report = function (results, stats) { | ||
if (stats == null) stats = {bailCount: 0, asyncSuccesses: 0} | ||
var errCount = -stats.bailCount | ||
for (var i = 0, r; r = results[i]; i++) { | ||
if (r.pass == null) { | ||
r.testError.stack = r.message + "\n" + o.cleanStackTrace(r.testError) | ||
r.testError.message = r.message | ||
throw r.testError | ||
} | ||
if (!r.pass) { | ||
var stackTrace = o.cleanStackTrace(r.error) | ||
var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1 | ||
if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || "" | ||
var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1 && stackTrace.indexOf("\n") === -1 | ||
if (couldHaveABetterStackTrace) stackTrace = r.task.error != null ? o.cleanStackTrace(r.task.error) : r.error.stack || "" | ||
console.error( | ||
(hasProcess ? "\n" : "") + | ||
highlight(r.context + ":", "red2") + "\n" + | ||
(r.task.timeoutLimbo ? "??? " : "") + | ||
highlight(r.task.context + ":", "red2") + "\n" + | ||
highlight(r.message, "red") + | ||
(stackTrace ? "\n" + stackTrace + "\n" : ""), | ||
cStyle("black", true), "", // reset to default | ||
cStyle("black", true), cStyle(null), // reset to default | ||
cStyle("red"), cStyle("black") | ||
@@ -376,25 +750,34 @@ ) | ||
var pl = results.length === 1 ? "" : "s" | ||
var resultSummary = (errCount === 0) ? | ||
highlight((pl ? "All " : "The ") + results.length + " assertion" + pl + " passed", "green"): | ||
highlight(errCount + " out of " + results.length + " assertion" + pl + " failed", "red2") | ||
var runningTime = " in " + Math.round(Date.now() - start) + "ms" | ||
console.log( | ||
(hasProcess ? "––––––\n" : "") + | ||
(name ? name + ": " : "") + resultSummary + runningTime, | ||
cStyle((errCount === 0 ? "green" : "red"), true), "" | ||
) | ||
return errCount | ||
} | ||
var oldTotal = " (old style total: " + (results.length + stats.asyncSuccesses) + ")" | ||
var total = results.length - stats.bailCount | ||
var message = [], log = [] | ||
if (hasProcess) { | ||
nextTickish = process.nextTick | ||
} else { | ||
nextTickish = function fakeFastNextTick(next) { | ||
if (stack++ < 5000) next() | ||
else setTimeout(next, stack = 0) | ||
if (hasProcess) message.push("––––––\n") | ||
if (name) message.push(name + ": ") | ||
if (errCount === 0 && stats.bailCount === 0) { | ||
message.push(highlight((pl ? "All " : "The ") + total + " assertion" + pl + " passed" + oldTotal, "green")) | ||
log.push(cStyle("green" , true), cStyle(null)) | ||
} else if (errCount === 0) { | ||
message.push((pl ? "All " : "The ") + total + " assertion" + pl + " passed" + oldTotal) | ||
} else { | ||
message.push(highlight(errCount + " out of " + total + " assertion" + pl + " failed" + oldTotal, "red2")) | ||
log.push(cStyle("red" , true), cStyle(null)) | ||
} | ||
if (stats.bailCount !== 0) { | ||
message.push(highlight(". Bailed out " + stats.bailCount + (stats.bailCount === 1 ? " time" : " times"), "red")) | ||
log.push(cStyle("red"), cStyle(null)) | ||
} | ||
log.unshift(message.join("")) | ||
console.log.apply(console, log) | ||
onlyWarning(stats.onlyCalledAt) | ||
return errCount + stats.bailCount | ||
} | ||
return o | ||
}) |
{ | ||
"name": "ospec", | ||
"version": "4.0.1", | ||
"version": "4.1.0", | ||
"description": "Noiseless testing framework", | ||
"main": "ospec.js", | ||
"directories": { | ||
"test": "tests" | ||
}, | ||
"keywords": [ "testing" ], | ||
"unpkg": "ospec.js", | ||
"keywords": [ | ||
"testing" | ||
], | ||
"author": "Leo Horie <leohorie@hotmail.com>", | ||
"license": "MIT", | ||
"bin": { | ||
"ospec": "./bin/ospec" | ||
}, | ||
"repository": "MithrilJS/mithril.js", | ||
"files": [ | ||
"ospec.js", | ||
"bin" | ||
], | ||
"bin": "./bin/ospec", | ||
"repository": "github:MithrilJS/ospec", | ||
"dependencies": { | ||
"glob": "^7.1.3" | ||
}, | ||
"scripts": { | ||
"test": "ospec tests/test-*.js", | ||
"test-api": "ospec tests/test-api.js", | ||
"test-cli": "ospec tests/test-cli.js", | ||
"lint": "eslint . bin/ospec" | ||
}, | ||
"devDependencies": { | ||
"compose-regexp": "0.4.0", | ||
"eslint": "^6.8.0", | ||
"ospec": "4.0.1" | ||
} | ||
} |
@@ -1,4 +0,13 @@ | ||
ospec [![npm Version](https://img.shields.io/npm/v/ospec.svg)](https://www.npmjs.com/package/ospec) [![npm License](https://img.shields.io/npm/l/ospec.svg)](https://www.npmjs.com/package/ospec) | ||
ospec [![npm Version](https://img.shields.io/npm/v/ospec.svg)](https://www.npmjs.com/package/ospec) [![npm License](https://img.shields.io/npm/l/ospec.svg)](https://www.npmjs.com/package/ospec) [![npm Downloads](https://img.shields.io/npm/dm/ospec.svg)](https://www.npmjs.com/package/ospec) [![Donate at OpenCollective](https://img.shields.io/opencollective/all/mithriljs.svg?colorB=brightgreen)](https://opencollective.com/mithriljs) | ||
===== | ||
<p align="center"> | ||
<a href="https://travis-ci.org/MithrilJS/ospec"> | ||
<img src="https://img.shields.io/travis/MithrilJS/ospec/master.svg" alt="Build Status"> | ||
</a> | ||
<a href="https://gitter.im/mithriljs/mithril.js"> | ||
<img src="https://img.shields.io/gitter/room/mithriljs/mithril.js.svg" alt="Gitter" /> | ||
</a> | ||
</p> | ||
[About](#about) | [Usage](#usage) | [CLI](#command-line-interface) | [API](#api) | [Goals](#goals) | ||
@@ -10,3 +19,3 @@ | ||
- ~360 LOC including the CLI runner | ||
- ~580 LOC including the CLI runner | ||
- terser and faster test code than with mocha, jasmine or tape | ||
@@ -175,3 +184,3 @@ - test code reads like bullet points | ||
```javascript | ||
o.spec("a spec that must timeout quickly", function(done, timeout) { | ||
o.spec("a spec that must timeout quickly", function() { | ||
// wait 20ms before bailing out of the tests of this suite and | ||
@@ -198,3 +207,3 @@ // its descendants | ||
```javascript | ||
o("setTimeout calls callback", function(done, timeout) { | ||
o("setTimeout calls callback", function(done) { | ||
o.timeout(500) //wait 500ms before bailing out of the test | ||
@@ -363,6 +372,6 @@ | ||
Finally, you may choose to load files or modules before any tests run (**note:** always add `--require` AFTER match patterns): | ||
Finally, you may choose to load files or modules before any tests run (**note:** always add `--preload` AFTER match patterns): | ||
``` | ||
ospec --require esm | ||
ospec --preload esm | ||
``` | ||
@@ -373,5 +382,9 @@ | ||
``` | ||
ospec '**/*.test.js' --ignore 'folder1/**' --require esm ./my-file.js | ||
ospec '**/*.test.js' --ignore 'folder1/**' --preload esm ./my-file.js | ||
``` | ||
### native mjs and module support | ||
For Node.js versions >= 13.2, `ospec` supports both ES6 modules and CommonJS packages out of the box. `--preload esm` is thus not needed in that case. | ||
### Run ospec directly from the command line: | ||
@@ -427,3 +440,3 @@ | ||
### void o(String title, Function([Function done [, Function timeout]]) assertions) | ||
### void o(String title, Function([Function done]) assertions) | ||
@@ -494,3 +507,3 @@ Defines a test. | ||
### void o.before(Function([Function done [, Function timeout]]) setup) | ||
### void o.before(Function([Function done]) setup) | ||
@@ -503,3 +516,3 @@ Defines code to be run at the beginning of a test group | ||
### void o.after(Function([Function done [, Function timeout]]) teardown) | ||
### void o.after(Function([Function done) teardown) | ||
@@ -512,3 +525,3 @@ Defines code to be run at the end of a test group | ||
### void o.beforeEach(Function([Function done [, Function timeout]]) setup) | ||
### void o.beforeEach(Function([Function done]) setup) | ||
@@ -521,3 +534,3 @@ Defines code to be run before each test in a group | ||
### void o.afterEach(Function([Function done [, Function timeout]]) teardown) | ||
### void o.afterEach(Function([Function done]) teardown) | ||
@@ -530,3 +543,3 @@ Defines code to be run after each test in a group | ||
### void o.only(String title, Function([Function done [, Function timeout]]) assertions) | ||
### void o.only(String title, Function([Function done]) assertions) | ||
@@ -584,2 +597,6 @@ Declares that only a single test should be run, instead of all of them | ||
### throwing Errors | ||
When an error is thrown some tests may be skipped. See the "run time semantics" for a detailed description of the bailout mechanism. | ||
--- | ||
@@ -659,4 +676,52 @@ | ||
--- | ||
## Run time model | ||
### Definitions: | ||
- A **test** is the function passed to `o("description", function test() {})`. | ||
- A **hook** is a function passed to `o.before()`, `o.after()`. `o.beforeEach()` and `o.afterEach()` | ||
- A **task** designates either a test or a hook. | ||
- A given test and its associated `beforeEach` and `afterEach` hooks form a **streak**. The `beforeEach` hooks run outermost first, the `afterEach` run outermost last. The hooks are optional, and are tied at test-definition time in the `o.spec()` calls that enclose the test. | ||
- A **spec** is a collection of streaks, specs, one `before` *hook* and one `after` *hook*. Each component is optional. Specs are defined with the `o.spec("spec name", function specDef() {})` calls. | ||
### The three phases | ||
For a given instance, an `ospec` run goes through three phases: | ||
1) test definitions | ||
1) test execution and results accumulation | ||
1) results presentation | ||
#### Test definition | ||
This phase is synchronous. `o.spec("spec name", function specDef() {})`, `o("test name", function test() {})` and hooks calls generate a tree of specs and tests. | ||
#### Test execution and results accumulation | ||
At test-run time, for each spec, the `before` hook is called if present, then nested specs the streak of each test, in definition order, then the `after` hook, if present. | ||
Test and hooks may contain assertions, which will populate the `results` array. | ||
#### Results presentation | ||
Once all tests have run or timed out, the results are presented. | ||
### Throwing errors and spec bail out | ||
While some testing libraries consider error thrown as assertions failure, `ospec` treats them as super-failures. Throwing will cause the current spec to be aborted, avoiding what can otherwise end up as pages of errors. What this means depends on when the error is thrown. Specifically: | ||
- A syntax error in a file causes the file to be ignored by the runner. | ||
- At test-definition time: | ||
- An error thrown at the root of a file will cause subsequent tests and specs to be ignored | ||
- An error thrown in a spec definition will cause the spec to be ignored. | ||
- At test-run time: | ||
- An error thrown in the `before` hook will cause the streaks and nested specs to be ignored. The `after` hook will run. | ||
- An error thrown in a task... | ||
- ...prevents further streaks and nested specs in the current spec from running. The `after` *hook* of the spec will run. | ||
- ...if thrown in a `beforeEach` hook of a streak, causes the streak to be hollowed out. Hooks defined in nested scopes and the actual test will not run. The `afterEach` hookcorresponding to the one that crashed will run though as will those defined in outer scopes. | ||
For every error thrown, a "bail out" failure is reported. | ||
--- | ||
@@ -663,0 +728,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
59319
733
3
3
6
687