Merge branch 'next'
This commit is contained in:
commit
2635070734
79 changed files with 7993 additions and 3507 deletions
279
ospec/ospec.js
279
ospec/ospec.js
|
|
@ -1,11 +1,12 @@
|
|||
/* eslint-disable global-require, no-bitwise, no-process-exit */
|
||||
"use strict"
|
||||
;(function(m) {
|
||||
if (typeof module !== "undefined") module["exports"] = m()
|
||||
else window.o = m()
|
||||
})(function init(name) {
|
||||
var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty
|
||||
|
||||
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
|
||||
var globalTimeout = noTimeoutRightNow
|
||||
var currentTestError = null
|
||||
if (name != null) spec[name] = ctx = {}
|
||||
|
||||
try {throw new Error} catch (e) {
|
||||
|
|
@ -13,19 +14,25 @@ else window.o = m()
|
|||
}
|
||||
function o(subject, predicate) {
|
||||
if (predicate === undefined) {
|
||||
if (results == null) throw new Error("Assertions should not occur outside test definitions")
|
||||
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions")
|
||||
return new Assert(subject)
|
||||
}
|
||||
else if (results == null) {
|
||||
ctx[unique(subject)] = predicate
|
||||
} else {
|
||||
throw new Error("Test definition shouldn't be nested. To group tests use `o.spec()`")
|
||||
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))
|
||||
}
|
||||
}
|
||||
o.before = hook("__before")
|
||||
o.after = hook("__after")
|
||||
o.beforeEach = hook("__beforeEach")
|
||||
o.afterEach = hook("__afterEach")
|
||||
o.before = hook("\x01before")
|
||||
o.after = hook("\x01after")
|
||||
o.beforeEach = hook("\x01beforeEach")
|
||||
o.afterEach = hook("\x01afterEach")
|
||||
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 (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument")
|
||||
ctx["\x01specTimeout"] = t
|
||||
}
|
||||
o.new = init
|
||||
o.spec = function(subject, predicate) {
|
||||
var parent = ctx
|
||||
|
|
@ -34,13 +41,18 @@ else window.o = m()
|
|||
ctx = parent
|
||||
}
|
||||
o.only = function(subject, predicate, silent) {
|
||||
if (!silent) console.log(highlight("/!\\ WARNING /!\\ o.only() mode"))
|
||||
o(subject, only = predicate)
|
||||
if (!silent) console.log(
|
||||
highlight("/!\\ WARNING /!\\ o.only() mode") + "\n" + o.cleanStackTrace(ensureStackTrace(new Error)) + "\n",
|
||||
cStyle("red"), ""
|
||||
)
|
||||
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)
|
||||
|
|
@ -51,6 +63,7 @@ else window.o = m()
|
|||
name: {value: fn.name}
|
||||
})
|
||||
spy.args = []
|
||||
spy.calls = []
|
||||
spy.callCount = 0
|
||||
return spy
|
||||
}
|
||||
|
|
@ -67,104 +80,125 @@ else window.o = m()
|
|||
}
|
||||
if (ospecFileName == null) return stack.join("\n")
|
||||
// skip ospec-related entries on the stack
|
||||
while (stack[i].indexOf(ospecFileName) !== -1) i++
|
||||
// now we're in user code
|
||||
while (stack[i] != null && stack[i].indexOf(ospecFileName) !== -1) i++
|
||||
// now we're in user code (or past the stack end)
|
||||
return stack[i]
|
||||
}
|
||||
o.timeout = function(n) {
|
||||
globalTimeout(n)
|
||||
}
|
||||
o.run = function(reporter) {
|
||||
results = []
|
||||
start = new Date
|
||||
test(spec, [], [], function() {
|
||||
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)
|
||||
if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit
|
||||
}
|
||||
})
|
||||
})
|
||||
}, null), 200 /*default timeout delay*/)
|
||||
|
||||
function test(spec, pre, post, finalize) {
|
||||
pre = [].concat(pre, spec["__beforeEach"] || [])
|
||||
post = [].concat(spec["__afterEach"] || [], post)
|
||||
series([].concat(spec["__before"] || [], Object.keys(spec).map(function(key) {
|
||||
return function(done, timeout) {
|
||||
timeout(Infinity)
|
||||
|
||||
if (key.slice(0, 2) === "__") return done()
|
||||
if (only !== null && spec[key] !== only && typeof only === typeof spec[key]) return done()
|
||||
subjects.push(key)
|
||||
var type = typeof spec[key]
|
||||
if (type === "object") test(spec[key], pre, post, pop)
|
||||
if (type === "function") series([].concat(pre, spec[key], post, pop))
|
||||
|
||||
function pop() {
|
||||
subjects.pop()
|
||||
done()
|
||||
}
|
||||
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))
|
||||
}
|
||||
}), spec["__after"] || [], finalize))
|
||||
return tasks
|
||||
}, []), spec["\x01after"] || [], finalize), defaultDelay)
|
||||
}
|
||||
|
||||
function series(fns) {
|
||||
function series(tasks, defaultDelay) {
|
||||
var cursor = 0
|
||||
next()
|
||||
|
||||
function next() {
|
||||
if (cursor === fns.length) return
|
||||
if (cursor === tasks.length) return
|
||||
|
||||
var task = tasks[cursor++]
|
||||
var fn = task.fn
|
||||
currentTestError = task.err
|
||||
var timeout = 0, delay = defaultDelay, s = new Date
|
||||
var current = cursor
|
||||
var arg
|
||||
|
||||
globalTimeout = setDelay
|
||||
|
||||
var fn = fns[cursor++]
|
||||
var timeout = 0, delay = 200, s = new Date
|
||||
var isDone = false
|
||||
|
||||
// public API, may only be called once from use code (or after returned Promise resolution)
|
||||
function done(err) {
|
||||
if (err) {
|
||||
if (err.message) record(err.message, err)
|
||||
else record(err)
|
||||
subjects.pop()
|
||||
next()
|
||||
}
|
||||
if (timeout !== undefined) {
|
||||
timeout = clearTimeout(timeout)
|
||||
if (delay !== Infinity) record(null)
|
||||
if (!isDone) next()
|
||||
else throw new Error("`" + arg + "()` should only be called once")
|
||||
isDone = true
|
||||
}
|
||||
else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms")
|
||||
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)
|
||||
}
|
||||
// for internal use only
|
||||
function finalizeAsync(err) {
|
||||
if (err == null) {
|
||||
if (task.err != null) succeed(new Assert)
|
||||
} else {
|
||||
if (err instanceof Error) fail(new Assert, err.message, err)
|
||||
else fail(new Assert, String(err), null)
|
||||
}
|
||||
if (timeout !== undefined) timeout = clearTimeout(timeout)
|
||||
if (current === cursor) next()
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
timeout = setTimeout(function() {
|
||||
timeout = undefined
|
||||
record("async test timed out")
|
||||
next()
|
||||
finalizeAsync("async test timed out after " + delay + "ms")
|
||||
}, Math.min(delay, 2147483647))
|
||||
}
|
||||
|
||||
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()
|
||||
var arg = (body.match(/\(([\w$]+)/) || body.match(/([\w$]+)\s*=>/) || []).pop()
|
||||
if (body.indexOf(arg) === body.lastIndexOf(arg)) throw new Error("`" + arg + "()` should be called at least once")
|
||||
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
|
||||
}
|
||||
try {
|
||||
fn(done, function(t) {delay = t})
|
||||
fn(done, setDelay)
|
||||
}
|
||||
catch (e) {
|
||||
done(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 {
|
||||
var p = fn()
|
||||
if (p && p.then) {
|
||||
startTimer()
|
||||
p.then(function() { done() }, done)
|
||||
} else {
|
||||
nextTickish(next)
|
||||
} else {
|
||||
try{
|
||||
var p = fn()
|
||||
if (p && p.then) {
|
||||
startTimer()
|
||||
p.then(function() { done() }, done)
|
||||
} else {
|
||||
nextTickish(next)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
globalTimeout = noTimeoutRightNow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +212,7 @@ else window.o = m()
|
|||
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] = predicate
|
||||
ctx[name] = new Task(predicate, ensureStackTrace(new Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +229,7 @@ else window.o = m()
|
|||
}
|
||||
function deepEqual(a, b) {
|
||||
if (a === b) return true
|
||||
if (a === null ^ b === null || a === undefined ^ b === undefined) return false
|
||||
if (a === null ^ b === null || a === undefined ^ b === undefined) return false // eslint-disable-line no-bitwise
|
||||
if (typeof a === "object" && typeof b === "object") {
|
||||
var aIsArgs = isArguments(a), bIsArgs = isArguments(b)
|
||||
if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) {
|
||||
|
|
@ -227,54 +261,99 @@ else window.o = m()
|
|||
return false
|
||||
}
|
||||
|
||||
function Assert(value) {this.value = value}
|
||||
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 Task(fn, err) {
|
||||
this.fn = fn
|
||||
this.err = err
|
||||
}
|
||||
function define(name, verb, compare) {
|
||||
Assert.prototype[name] = function assert(value) {
|
||||
if (compare(this.value, value)) record(null)
|
||||
else record(serialize(this.value) + "\n" + verb + "\n" + serialize(value))
|
||||
if (compare(this.value, value)) succeed(this)
|
||||
else fail(this, serialize(this.value) + "\n " + verb + "\n" + serialize(value))
|
||||
var self = this
|
||||
return function(message) {
|
||||
var result = results[results.length - 1]
|
||||
result.message = message + "\n\n" + result.message
|
||||
if (!self.pass) self.message = message + "\n\n" + self.message
|
||||
}
|
||||
}
|
||||
}
|
||||
function record(message, error) {
|
||||
var result = {pass: message === null}
|
||||
if (result.pass === false) {
|
||||
if (error == null) {
|
||||
error = new Error
|
||||
if (error.stack === undefined) new function() {try {throw error} catch (e) {error = e}}
|
||||
}
|
||||
result.context = subjects.join(" > ")
|
||||
result.message = message
|
||||
result.error = error
|
||||
|
||||
}
|
||||
results.push(result)
|
||||
function succeed(assertion) {
|
||||
results[assertion.i].pass = true
|
||||
}
|
||||
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) {
|
||||
if (hasProcess) return require("util").inspect(value)
|
||||
if (hasProcess) return require("util").inspect(value) // eslint-disable-line global-require
|
||||
if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value)
|
||||
else if (typeof value === "function") return value.name || "<anonymous function>"
|
||||
try {return JSON.stringify(value)} catch (e) {return String(value)}
|
||||
}
|
||||
function highlight(message) {
|
||||
return hasProcess ? "\x1b[31m" + message + "\x1b[0m" : "%c" + message + "%c "
|
||||
function noTimeoutRightNow() {
|
||||
throw new Error("o.timeout must be called snchronously from within a test definition or a hook")
|
||||
}
|
||||
var colorCodes = {
|
||||
red: "31m",
|
||||
red2: "31;1m",
|
||||
green: "32;1m"
|
||||
}
|
||||
function highlight(message, color) {
|
||||
var code = colorCodes[color] || colorCodes.red;
|
||||
return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + message + "\x1b[0m" : message) : "%c" + message + "%c "
|
||||
}
|
||||
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 getStackName(e, exp) {
|
||||
return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null
|
||||
}
|
||||
|
||||
o.report = function (results) {
|
||||
var errCount = 0
|
||||
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)
|
||||
console.error(r.context + ":\n" + highlight(r.message) + (stackTrace ? "\n\n" + stackTrace + "\n\n" : ""), hasProcess ? "" : "color:red", hasProcess ? "" : "color:black")
|
||||
var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1
|
||||
if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || ""
|
||||
console.error(
|
||||
(hasProcess ? "\n" : "") +
|
||||
highlight(r.context + ":", "red2") + "\n" +
|
||||
highlight(r.message, "red") +
|
||||
(stackTrace ? "\n" + stackTrace + "\n" : ""),
|
||||
|
||||
cStyle("black", true), "", // reset to default
|
||||
cStyle("red"), cStyle("black")
|
||||
)
|
||||
errCount++
|
||||
}
|
||||
}
|
||||
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(
|
||||
(name ? name + ": " : "") +
|
||||
results.length + " assertions completed in " + Math.round(new Date - start) + "ms, " +
|
||||
"of which " + results.filter(function(result){return result.error}).length + " failed"
|
||||
(hasProcess ? "––––––\n" : "") +
|
||||
(name ? name + ": " : "") + resultSummary + runningTime,
|
||||
cStyle((errCount === 0 ? "green" : "red"), true), ""
|
||||
)
|
||||
return errCount
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue