Comparing version 4.1.7 to 4.2.0
353
ospec.js
"use strict" | ||
// const LOG = console.log | ||
// const p = (...args) => { | ||
// LOG(...args) | ||
// return args.pop() | ||
// } | ||
/* | ||
@@ -43,10 +49,19 @@ Ospec is made of four parts: | ||
// stack-managed globals | ||
var globalBail | ||
var globalContext = rootSpec | ||
var globalDepth = 1 | ||
var globalFile | ||
var globalTestOrHook = null | ||
var globalTimeout = noTimeoutRightNow | ||
var globalTimedOutAndPendingResolution = 0 | ||
// Are we in the process of baling out? | ||
var $bail = false | ||
// current spec | ||
var $context = rootSpec | ||
// spec nesting level | ||
var $depth = 1 | ||
// the current file name | ||
var $file | ||
// the task (test/hook) that is currently running | ||
var $task = null | ||
// the current o.timeout implementation | ||
var $timeout | ||
// count the total amount of tests that timed out and didn't complete after the fact before the run ends | ||
var $timedOutAndPendingResolution = 0 | ||
// are we using the v5+ API? | ||
var $localAssertions = false | ||
// Shared state, set only once, but initialization is delayed | ||
@@ -68,5 +83,2 @@ var results, stats, timeoutStackName | ||
function noTimeoutRightNow() { | ||
throw new Error("`o.timeout()` must be called synchronously from within a test definition or a hook") | ||
} | ||
@@ -118,9 +130,11 @@ function timeoutParamDeprecationNotice(n) { | ||
this.context = null | ||
this.file = globalFile | ||
this.file = $file | ||
// 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.depth = $depth + (hookName == null ? 1 : 0) | ||
this.doneTwiceError = !$localAssertions && validateDone(fn, err) || "A thenable should only be resolved once" | ||
this.error = err | ||
this.internal = err == null | ||
this.fn = fn | ||
this.hookName = hookName | ||
this.localAssertions = $localAssertions | ||
} | ||
@@ -130,4 +144,4 @@ | ||
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) | ||
if ($context[name].length > 0) throw new Error("Attempt to register o." + name + "() more than once. A spec can only have one hook of each kind") | ||
$context[name][0] = new Task(predicate, ensureStackTrace(new Error), name) | ||
} | ||
@@ -137,6 +151,6 @@ } | ||
function unique(subject) { | ||
if (hasOwn.call(globalContext.children, subject)) { | ||
if (hasOwn.call($context.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 += "*" | ||
while (hasOwn.call($context.children, subject)) subject += "*" | ||
} | ||
@@ -150,8 +164,67 @@ return subject | ||
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions") | ||
if ($task.localAssertions) throw new SyntaxError("Illegal global assertion, use a local `o()`") | ||
return new Assertion(subject) | ||
} else { | ||
subject = String(subject) | ||
globalContext.children[unique(subject)] = new Task(predicate, ensureStackTrace(new Error), null) | ||
$context.children[unique(subject)] = new Task(predicate, ensureStackTrace(new Error), null) | ||
} | ||
} | ||
function noSpecOrTestHasBeenDefined() { | ||
return $context === rootSpec | ||
&& Object.keys(rootSpec).every(k => { | ||
const item = rootSpec[k] | ||
return item == null || (Array.isArray(item) ? item : Object.keys(item)).length === 0 | ||
}) | ||
} | ||
o.globalAssertions = function(cb) { | ||
if (isRunning()) throw new SyntaxError("local/global modes can only be called before o.run()") | ||
if (cb === "override"){ | ||
// escape hatch for the CLI test suite | ||
// ideally we should use --preload that requires | ||
// in depth rethinking of the CLI test suite that I'd rather | ||
// avoid while changing the core at the same time. | ||
$localAssertions = false | ||
return | ||
} else if (cb == null) { | ||
if (noSpecOrTestHasBeenDefined()) { | ||
$localAssertions = false | ||
return | ||
} else { | ||
throw new SyntaxError("local/global mode can only be toggled before defining specs and tests") | ||
} | ||
} else { | ||
const previous = $localAssertions | ||
try { | ||
$localAssertions = false | ||
cb() | ||
} finally { | ||
$localAssertions = previous | ||
} | ||
} | ||
} | ||
o.localAssertions = function(cb) { | ||
if (isRunning()) throw new SyntaxError("local/global modes can only be called before o.run()") | ||
if (cb === "override"){ | ||
// escape hatch for the CLI test suite | ||
// ideally we should use --preload that requires | ||
// in depth rethinking of the CLI test suite that I'd rather | ||
// avoid while changing the core at the same time. | ||
$localAssertions = true | ||
return | ||
} if (cb == null) { | ||
if (noSpecOrTestHasBeenDefined()) { | ||
$localAssertions = true | ||
} else { | ||
throw new SyntaxError("local/global mode can only be toggled before defining specs and tests") | ||
} | ||
} else { | ||
const previous = $localAssertions | ||
try { | ||
$localAssertions = true | ||
cb() | ||
} finally { | ||
$localAssertions = previous | ||
} | ||
} | ||
} | ||
@@ -165,5 +238,5 @@ o.before = hook("before") | ||
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()") | ||
if (globalContext.specTimeout != null) throw new Error("A default timeout has already been defined in this context") | ||
if ($context.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") | ||
globalContext.specTimeout = t | ||
$context.specTimeout = t | ||
} | ||
@@ -176,16 +249,12 @@ | ||
// stack managed globals | ||
var parent = globalContext | ||
var parent = $context | ||
var name = unique(subject) | ||
globalContext = globalContext.children[name] = new Spec() | ||
globalDepth++ | ||
$context = $context.children[name] = new Spec() | ||
$depth++ | ||
try { | ||
predicate() | ||
} catch(e) { | ||
console.error(e) | ||
globalContext.children[name].children = {"> > BAILED OUT < < <": new Task(function(){ | ||
throw e | ||
}, ensureStackTrace(new Error), null)} | ||
} finally { | ||
$depth-- | ||
$context = parent | ||
} | ||
globalDepth-- | ||
globalContext = parent | ||
} | ||
@@ -217,3 +286,3 @@ | ||
o.timeout = function(n) { | ||
globalTimeout(n) | ||
$timeout(n) | ||
} | ||
@@ -250,12 +319,13 @@ | ||
return { | ||
file: globalTestOrHook.file, | ||
name: globalTestOrHook.context | ||
file: $task.file, | ||
name: $task.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 | ||
if (isRunning() || $context !== rootSpec) throw new Error("setting `o.metadata()` is only allowed at the root, at test definition time") | ||
$file = opts.file | ||
} | ||
} | ||
o.run = function(reporter) { | ||
if (rootSpec !== globalContext) throw new Error("`o.run()` can't be called from within a spec") | ||
if (rootSpec !== $context) throw new Error("`o.run()` can't be called from within a spec") | ||
if (isRunning()) throw new Error("`o.run()` has already been called") | ||
@@ -274,3 +344,3 @@ results = [] | ||
var finalize = new Task(function() { | ||
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/) | ||
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([w .]+?:d+:d+)/) | ||
if (typeof reporter === "function") reporter(results, stats) | ||
@@ -295,6 +365,6 @@ else { | ||
// stack-managed globals | ||
var previousBail = globalBail | ||
globalBail = function() {bailed = true; stats.bailCount++} | ||
var previousBail = $bail | ||
$bail = function() {bailed = true; stats.bailCount++} | ||
var restoreStack = new Task(function() { | ||
globalBail = previousBail | ||
$bail = previousBail | ||
}, null, null) | ||
@@ -310,3 +380,2 @@ | ||
) | ||
series( | ||
@@ -325,5 +394,4 @@ [].concat( | ||
if (bailed) return done() | ||
o.timeout(Infinity) | ||
subjects.push(key) | ||
var popSubjects = new Task(function pop() {subjects.pop(), done()}, null, null) | ||
var popSubjects = new Task(function pop() {subjects.pop(); done()}, null, null) | ||
if (spec.children[key] instanceof Task) { | ||
@@ -366,3 +434,2 @@ // this is a test | ||
var isHook = task.hookName != null | ||
var isInternal = task.error == null | ||
var taskStartTime = new Date | ||
@@ -372,19 +439,116 @@ | ||
var delay = defaultDelay | ||
var isAsync = false | ||
var isDone = false | ||
var isFinalized = false | ||
var hasMovedOn = false | ||
var hasConcluded = false | ||
var timeout | ||
if (!isInternal) { | ||
globalTestOrHook = task | ||
task.context = subjects.join(" > ") | ||
if (isHook) { | ||
task.context = "o." + task.hookName + Array.apply(null, {length: task.depth}).join("*") + "( " + task.context + " )" | ||
var isDone = false | ||
var isAsync = false | ||
var promises = [] | ||
if (task.internal) { | ||
// internal tasks still use the legacy done() system. | ||
// handled hereafter in a simplified fashion, without timeout | ||
// and bailout handling (let it crash) | ||
if (fn.length === 0) { | ||
fn() | ||
next() | ||
} | ||
else fn(function() { | ||
if (hasMovedOn) throw new Error("Internal Error, done() should only be called once") | ||
hasMovedOn = true | ||
next() | ||
}) | ||
return | ||
} | ||
globalTimeout = function timeout (t) { | ||
$task = task | ||
task.context = subjects.join(" > ") | ||
if (isHook) { | ||
task.context = "o." + task.hookName + Array.apply(null, {length: task.depth}).join("*") + "( " + task.context + " )" | ||
} | ||
$timeout = function timeout (t) { | ||
if (isAsync || hasConcluded || isDone) throw new Error("`o.timeout()` must be called synchronously from within a test definition or a hook") | ||
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument") | ||
delay = t | ||
} | ||
if (task.localAssertions) { | ||
var assert = function o(value) { | ||
return new Assertion(value, task) | ||
} | ||
assert.metadata = o.metadata | ||
assert.timeout = o.timeout | ||
assert.spy = createSpy(function(self, args, fn, spy) { | ||
if (hasConcluded) fail(new Assertion().i, "spy ran after its test was concluded\n"+fn.toString()) | ||
return globalSpyHelper(self, args, fn, spy) | ||
}) | ||
assert.o = assert | ||
Object.defineProperty(assert, "done", {get(){ | ||
let f, r | ||
promises.push(new Promise((_f, _r)=>{f = _f, r = _r})) | ||
function done(x){ | ||
return x == null ? f() : r(x) | ||
} | ||
return done | ||
}}) | ||
// runs when a test had an error or returned a promise | ||
const conclude = (err, threw) => { | ||
if (threw) { | ||
if (err instanceof Error) fail(new Assertion().i, err.message, err) | ||
else fail(new Assertion().i, String(err), null) | ||
$bail() | ||
if (task.hookName === "beforeEach") { | ||
while (!task.internal && tasks[cursor].depth > task.depth) cursor++ | ||
} | ||
} | ||
if (timeout !== undefined) { | ||
timeout = clearTimeout(timeout) | ||
} | ||
hasConcluded = true | ||
// if the timeout already expired, the suite has moved on. | ||
// Doing it again would be a bug. | ||
if (!hasMovedOn) moveOn() | ||
} | ||
// hops on to the next task after either conclusion or timeout, | ||
// whichever comes first | ||
const moveOn = () => { | ||
hasMovedOn = true | ||
if (isAsync) next() | ||
else nextTickish(next) | ||
} | ||
const startTimer = () => { | ||
timeout = setTimeout(function() { | ||
timeout = undefined | ||
fail(new Assertion().i, "async test timed out after " + delay + "ms", null) | ||
moveOn() | ||
}, Math.min(delay, 0x7fffffff)) | ||
} | ||
try { | ||
var result = fn(assert) | ||
if (result != null && typeof result.then === 'function') { | ||
// normalize thenables so that we only conclude once | ||
promises.push(Promise.resolve(result)) | ||
} | ||
if (promises.length > 0) { | ||
Promise.all(promises).then( | ||
function() {conclude()}, | ||
function(e) {conclude(e,true)} | ||
) | ||
isAsync = true | ||
startTimer() | ||
} else { | ||
hasConcluded = true | ||
moveOn() | ||
} | ||
} catch(e) { | ||
conclude(e, true) | ||
} | ||
return | ||
} | ||
// for the legacy API | ||
try { | ||
@@ -394,7 +558,7 @@ if (fn.length > 0) { | ||
} else { | ||
var p = fn() | ||
if (p && p.then) { | ||
var prm = fn() | ||
if (prm && prm.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)}) | ||
prm.then(function() { _done(null, false) }, function(e) {_done(e, true)}) | ||
} else { | ||
@@ -404,3 +568,3 @@ finalize(null, false, false) | ||
} | ||
if (!isFinalized) { | ||
if (!hasMovedOn) { | ||
// done()/_done() haven't been called synchronously | ||
@@ -412,6 +576,4 @@ isAsync = true | ||
catch (e) { | ||
if (isInternal) throw e | ||
else finalize(e, true, false) | ||
finalize(e, true, false) | ||
} | ||
globalTimeout = noTimeoutRightNow | ||
@@ -430,3 +592,3 @@ // public API, may only be called once from user code (or after the resolution | ||
if (isAsync && timeout === undefined) { | ||
globalTimedOutAndPendingResolution-- | ||
$timedOutAndPendingResolution-- | ||
console.warn( | ||
@@ -440,3 +602,3 @@ task.context | ||
if (!isFinalized) finalize(err, threw, false) | ||
if (!hasMovedOn) finalize(err, threw, false) | ||
} | ||
@@ -447,3 +609,3 @@ // called only for async tests | ||
timeout = undefined | ||
globalTimedOutAndPendingResolution++ | ||
$timedOutAndPendingResolution++ | ||
finalize("async test timed out after " + delay + "ms\nWarning: assertions starting with `???` may not be properly labelled", true, true) | ||
@@ -454,7 +616,7 @@ }, Math.min(delay, 0x7fffffff)) | ||
function finalize(err, threw, isTimeout) { | ||
if (isFinalized) { | ||
if (hasMovedOn) { | ||
// failsafe for hacking, should never happen in released code | ||
throw new Error("Multiple finalization") | ||
} | ||
isFinalized = true | ||
hasMovedOn = true | ||
@@ -465,5 +627,5 @@ if (threw) { | ||
if (!isTimeout) { | ||
globalBail() | ||
$bail() | ||
if (task.hookName === "beforeEach") { | ||
while (tasks[cursor].error != null && tasks[cursor].depth > task.depth) cursor++ | ||
while (!task.internal != null && tasks[cursor].depth > task.depth) cursor++ | ||
} | ||
@@ -488,8 +650,8 @@ } | ||
message: "Incomplete assertion in the test definition starting at...", | ||
error: globalTestOrHook.error, | ||
task: globalTestOrHook, | ||
timeoutLimbo: globalTimedOutAndPendingResolution === 0, | ||
error: $task.error, | ||
task: $task, | ||
timeoutLimbo: $timedOutAndPendingResolution === 0, | ||
// Deprecated | ||
context: (globalTimedOutAndPendingResolution === 0 ? "" : "??? ") + globalTestOrHook.context, | ||
testError: globalTestOrHook.error | ||
context: ($timedOutAndPendingResolution === 0 ? "" : "??? ") + $task.context, | ||
testError: $task.error | ||
}) | ||
@@ -513,2 +675,4 @@ } | ||
if (Array.isArray(message)) { | ||
// We got a tagged template literal, | ||
// we'll interpolate the dynamic values. | ||
var args = arguments | ||
@@ -538,2 +702,3 @@ message = message.reduce(function(acc, v, i) {return acc + args[i] + v}) | ||
}) | ||
Assertion.prototype._ = Assertion.prototype.satisfies | ||
define("notSatisfies", function notSatisfies(self, check) { | ||
@@ -549,3 +714,2 @@ try { | ||
}) | ||
function isArguments(a) { | ||
@@ -561,3 +725,3 @@ if ("callee" in a) { | ||
} | ||
function deepEqual(a, b) { | ||
@@ -681,3 +845,3 @@ if (a === b) return true | ||
function spyHelper(self, args, fn, spy) { | ||
function globalSpyHelper(self, args, fn, spy) { | ||
spy.this = self | ||
@@ -699,19 +863,23 @@ spy.args = args | ||
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 | ||
function createSpy(helper) { | ||
return function spy(fn) { | ||
var name = "", length = 0 | ||
if (fn) name = fn.name, length = fn.length | ||
var spy = (!supportsFunctionMutations && supportsEval) | ||
? getOrMakeSpyFactory(name, length)(fn, helper) | ||
: function(){return helper(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 | ||
} | ||
} | ||
o.spy = createSpy(globalSpyHelper) | ||
// Reporter | ||
@@ -724,2 +892,5 @@ var colorCodes = { | ||
// this is needed to work around the formating done by node see https://nodejs.org/api/util.html#utilformatformat-args | ||
function escapePercent(x){return String(x).replace(/%/g, "%%")} | ||
// console style for terminals | ||
@@ -729,3 +900,3 @@ // see https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences | ||
var code = colorCodes[color] || colorCodes.red; | ||
return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + message + "\x1b[0m" : message) : "%c" + message + "%c " | ||
return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + escapePercent(message) + "\x1b[0m" : escapePercent(message)) : "%c" + message + "%c " | ||
} | ||
@@ -732,0 +903,0 @@ |
{ | ||
"name": "ospec", | ||
"version": "4.1.7", | ||
"version": "4.2.0", | ||
"description": "Noiseless testing framework", | ||
@@ -25,6 +25,6 @@ "main": "ospec.js", | ||
"test": "ospec-stable tests/test-*.js", | ||
"test-api": "ospec-stable tests/test-api.js", | ||
"test-api": "ospec-stable tests/test-api-*.js", | ||
"test-cli": "ospec-stable tests/test-cli.js", | ||
"self-test": "node ./bin/ospec tests/test-*.js", | ||
"self-test-api": "node ./bin/ospec tests/test-api.js", | ||
"self-test-api": "node ./bin/ospec tests/test-api-*.js", | ||
"self-test-cli": "node ./bin/ospec tests/test-cli.js", | ||
@@ -38,4 +38,4 @@ "lint": "eslint --cache --ignore-pattern \"tests/fixtures/**/*.*\" . bin/ospec", | ||
"eslint": "^6.8.0", | ||
"ospec-stable": "npm:ospec@4.1.5" | ||
"ospec-stable": "npm:ospec@4.1.7" | ||
} | ||
} |
@@ -15,3 +15,3 @@ # ospec | ||
- ~660 LOC including the CLI runner | ||
- ~1100 LOC including the CLI runner<sup>1</sup> | ||
- terser and faster test code than with mocha, jasmine or tape | ||
@@ -30,2 +30,4 @@ - test code reads like bullet points | ||
Note: <sup>1</sup> ospec is currently in the process of changing some of its API surface. The legacy and updated APIs are both implemented right now to ease the transition, once legacy code has been removed we'll clock around 800 LOC. | ||
## Usage | ||
@@ -717,6 +719,8 @@ | ||
Ospec started as a bare bones test runner optimized for Leo Horie to write Mithril v1 with as little hasle as possible. It has since grown in capabilities and polish, and while we tried to keep some of the original spirit, the current incarnation is not as radically minimalist as the original. The state of the art in testing has also moved with the dominance of Jest over Jasmine and Mocha, and now Vitest coming up the horizon. | ||
- Do the most common things that the mocha/chai/sinon triad does without having to install 3 different libraries and several dozen dependencies | ||
- Disallow configuration in test-space: | ||
- Limit configuration in test-space: | ||
- Disallow ability to pick between API styles (BDD/TDD/Qunit, assert/should/expect, etc) | ||
- Disallow ability to add custom assertion types | ||
- No "magic" plugin system with global reach. Custom assertions need to be imported or defined lexically (e.g. in `o(value)._(matches(refence))`, `matches` can be resolved in file). | ||
- Provide a default simple reporter | ||
@@ -726,3 +730,3 @@ - Make assertion code terse, readable and self-descriptive | ||
Explicitly disallowing modularity and configuration in test-space has a few benefits: | ||
These restrictions have a few benefits: | ||
@@ -729,0 +733,0 @@ - tests always look the same, even across different projects and teams |
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
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
58704
873
734