diff --git a/ospec/ospec.js b/ospec/ospec.js index 56003566..05a1ef95 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -7,6 +7,7 @@ else window.o = m() var spec = {}, subjects = [], results, only = null, 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 var hooks = { __before: true, __beforeEach: true, @@ -17,8 +18,11 @@ else window.o = m() if (name != null) spec[name] = ctx = {} function o(subject, predicate) { - if (predicate === undefined) return new Assert(subject) - else { + if (predicate === undefined) { + if (!isRunning()) throw new Error("Assertions should not occur outside test definitions") + return new Assert(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 (hasOwn.call(hooks, subject)) throw new Error("'" + subject + "' is a reserved test name") if (subject.slice(0, 2) === "__") console.warn("test names starting with '__' are reserved for internal use\n" + o.cleanStackTrace(ensureStackTrace(new Error))) @@ -133,9 +137,10 @@ else window.o = m() if (cursor === tasks.length) return var task = tasks[cursor++] - var current = cursor var fn = task.fn + currentTestError = task.err var timeout = 0, delay = defaultDelay, s = new Date + var current = cursor var arg globalTimeout = setDelay @@ -150,9 +155,9 @@ else window.o = m() } // for internal use only function finalizeAsync(err) { - if (err) { - if (err instanceof Error) record(err.message, err, task.err) - else record(String(err), null, task.err) + if (err != null) { + 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() @@ -256,34 +261,32 @@ else window.o = m() function isRunning() {return results != null} function Assert(value) { - if (!isRunning()) throw new Error("Assertions should not occur outside test definitions") 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) { - // Tasks defined internally don't have an `err`, and may be created at run-time. - if (err != null && isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`") 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, fallbackError) { - var result = {pass: message === null} - if (result.pass === false) { - result.context = subjects.join(" > ") - result.message = message - result.error = error != null ? error : ensureStackTrace(new Error) - result.fallbackError = fallbackError - } - 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) @@ -318,10 +321,15 @@ else window.o = m() 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) var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1 - if (couldHaveABetterStackTrace) stackTrace = r.fallbackError != null ? o.cleanStackTrace(r.fallbackError) : r.error.stack || "" + if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || "" console.error( (hasProcess ? "\n" : "") + highlight(r.context + ":", "red2") + "\n" + diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 35d60e0a..b988898c 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -3,6 +3,37 @@ var callAsync = require("../../test-utils/callAsync") var o = require("../ospec") + +// this throws an async error that can't be caught in browsers +if (typeof process !== "undefined") { + o("incomplete assertion", function(done) { + var stackMatcher = /([\w\.\\\/\-]+):(\d+):/ + // /!\ this test relies on the `new Error` expression being six lines + // above the `oo("test", function(){...})` call. + var matches = (new Error).stack.match(stackMatcher) + if (matches != null) { + var name = matches[1] + var num = Number(matches[2]) + } + var oo = o.new() + oo("test", function() { + oo("incomplete") + }) + oo.run(function(results) { + o(results.length).equals(1) + o(results[0].message).equals("Incomplete assertion in the test definition starting at...") + o(results[0].pass).equals(null) + var stack = o.cleanStackTrace(results[0].testError) + var matches2 = stack && stack.match(stackMatcher) + if (matches != null && matches2 != null) { + o(matches[1]).equals(name) + o(Number(matches2[2])).equals(num + 6) + } + done() + }) + }) +} + o("o.only", function(done) { var oo = o.new() @@ -47,7 +78,6 @@ o.spec("reporting", function() { o(results.length).equals(2)("Two results") o("error" in results[0] && "pass" in results[0]).equals(true)("error and pass keys present in failing result") - o(!("error" in results[1]) && "pass" in results[1]).equals(true)("only pass key present in passing result") o(results[0].pass).equals(false)("Test meant to fail has failed") o(results[1].pass).equals(true)("Test meant to pass has passed") @@ -260,8 +290,8 @@ o.spec("ospec", function() { o(results[0].pass).equals(false) // todo test cleaned up results[0].error stack trace for the presence // of the timeout stack entry - o(results[0].fallbackError instanceof Error).equals(true) - o(o.cleanStackTrace(results[0].fallbackError).indexOf("test-ospec.js:" + (line + 3) + ":")).notEquals(-1) + o(results[0].testError instanceof Error).equals(true) + o(o.cleanStackTrace(results[0].testError).indexOf("test-ospec.js:" + (line + 3) + ":")).notEquals(-1) done() })) @@ -284,8 +314,8 @@ o.spec("ospec", function() { oo.run((function(results) { o(results.length).equals(1) o(results[0].pass).equals(false) - o(results[0].fallbackError instanceof Error).equals(true) - o(o.cleanStackTrace(results[0].fallbackError).indexOf("test-ospec.js:" + (line + 3) + ":")).notEquals(-1) + o(results[0].testError instanceof Error).equals(true) + o(o.cleanStackTrace(results[0].testError).indexOf("test-ospec.js:" + (line + 3) + ":")).notEquals(-1) done() }))