diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-05-28 00:38:50 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-05-28 00:40:43 +0200 |
commit | 7fff4499fd915bcea3fa93b1aa8b35f4fe7a6027 (patch) | |
tree | 6de9a1aebd150a23b7f8c273ec657a5d0a18fe3e /node_modules/ava/lib/test.js | |
parent | 963b7a41feb29cc4be090a2446bdfe0c1f1bcd81 (diff) |
add linting (and some initial fixes)
Diffstat (limited to 'node_modules/ava/lib/test.js')
-rw-r--r-- | node_modules/ava/lib/test.js | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/node_modules/ava/lib/test.js b/node_modules/ava/lib/test.js new file mode 100644 index 000000000..a9b0fb1d9 --- /dev/null +++ b/node_modules/ava/lib/test.js @@ -0,0 +1,416 @@ +'use strict'; +const isGeneratorFn = require('is-generator-fn'); +const co = require('co-with-promise'); +const observableToPromise = require('observable-to-promise'); +const isPromise = require('is-promise'); +const isObservable = require('is-observable'); +const plur = require('plur'); +const assert = require('./assert'); +const formatAssertError = require('./format-assert-error'); +const globals = require('./globals'); + +class SkipApi { + constructor(test) { + this._test = test; + } +} + +const captureStack = start => { + const limitBefore = Error.stackTraceLimit; + Error.stackTraceLimit = 1; + const obj = {}; + Error.captureStackTrace(obj, start); + Error.stackTraceLimit = limitBefore; + return obj.stack; +}; + +class ExecutionContext { + constructor(test) { + this._test = test; + this.skip = new SkipApi(test); + } + + plan(ct) { + this._test.plan(ct, captureStack(this.plan)); + } + + get end() { + const end = this._test.bindEndCallback(); + const endFn = err => end(err, captureStack(endFn)); + return endFn; + } + + get title() { + return this._test.title; + } + + get context() { + const contextRef = this._test.contextRef; + return contextRef && contextRef.context; + } + + set context(context) { + const contextRef = this._test.contextRef; + + if (!contextRef) { + this._test.saveFirstError(new Error(`\`t.context\` is not available in ${this._test.metadata.type} tests`)); + return; + } + + contextRef.context = context; + } + + _throwsArgStart(assertion, file, line) { + this._test.trackThrows({assertion, file, line}); + } + _throwsArgEnd() { + this._test.trackThrows(null); + } +} +Object.defineProperty(ExecutionContext.prototype, 'context', {enumerable: true}); + +{ + const assertions = assert.wrapAssertions({ + pass(executionContext) { + executionContext._test.countPassedAssertion(); + }, + + pending(executionContext, promise) { + executionContext._test.addPendingAssertion(promise); + }, + + fail(executionContext, error) { + executionContext._test.addFailedAssertion(error); + } + }); + Object.assign(ExecutionContext.prototype, assertions); + + function skipFn() { + this._test.countPassedAssertion(); + } + Object.keys(assertions).forEach(el => { + SkipApi.prototype[el] = skipFn; + }); +} + +class Test { + constructor(options) { + this.contextRef = options.contextRef; + this.failWithoutAssertions = options.failWithoutAssertions; + this.fn = isGeneratorFn(options.fn) ? co.wrap(options.fn) : options.fn; + this.getSnapshotState = options.getSnapshotState; + this.metadata = options.metadata; + this.onResult = options.onResult; + this.title = options.title; + + this.assertCount = 0; + this.assertError = undefined; + this.calledEnd = false; + this.duration = null; + this.endCallbackFinisher = null; + this.finishDueToAttributedError = null; + this.finishDueToInactivity = null; + this.finishing = false; + this.pendingAssertionCount = 0; + this.pendingThrowsAssertion = null; + this.planCount = null; + this.startedAt = 0; + } + + bindEndCallback() { + if (this.metadata.callback) { + return (err, stack) => { + this.endCallback(err, stack); + }; + } + + throw new Error('`t.end()`` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`'); + } + + endCallback(err, stack) { + if (this.calledEnd) { + this.saveFirstError(new Error('`t.end()` called more than once')); + return; + } + this.calledEnd = true; + + if (err) { + this.saveFirstError(new assert.AssertionError({ + actual: err, + message: 'Callback called with an error', + stack, + values: [formatAssertError.formatWithLabel('Error:', err)] + })); + } + + if (this.endCallbackFinisher) { + this.endCallbackFinisher(); + } + } + + createExecutionContext() { + return new ExecutionContext(this); + } + + countPassedAssertion() { + if (this.finishing) { + this.saveFirstError(new Error('Assertion passed, but test has already finished')); + } + + this.assertCount++; + } + + addPendingAssertion(promise) { + if (this.finishing) { + this.saveFirstError(new Error('Assertion passed, but test has already finished')); + } + + this.assertCount++; + this.pendingAssertionCount++; + promise + .catch(err => this.saveFirstError(err)) + .then(() => this.pendingAssertionCount--); + } + + addFailedAssertion(error) { + if (this.finishing) { + this.saveFirstError(new Error('Assertion failed, but test has already finished')); + } + + this.assertCount++; + this.saveFirstError(error); + } + + saveFirstError(err) { + if (!this.assertError) { + this.assertError = err; + } + } + + plan(count, planStack) { + if (typeof count !== 'number') { + throw new TypeError('Expected a number'); + } + + this.planCount = count; + + // In case the `planCount` doesn't match `assertCount, we need the stack of + // this function to throw with a useful stack. + this.planStack = planStack; + } + + verifyPlan() { + if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) { + this.saveFirstError(new assert.AssertionError({ + assertion: 'plan', + message: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`, + operator: '===', + stack: this.planStack + })); + } + } + + verifyAssertions() { + if (!this.assertError) { + if (this.failWithoutAssertions && !this.calledEnd && this.planCount === null && this.assertCount === 0) { + this.saveFirstError(new Error('Test finished without running any assertions')); + } else if (this.pendingAssertionCount > 0) { + this.saveFirstError(new Error('Test finished, but an assertion is still pending')); + } + } + } + + trackThrows(pending) { + this.pendingThrowsAssertion = pending; + } + + detectImproperThrows(err) { + if (!this.pendingThrowsAssertion) { + return false; + } + + const pending = this.pendingThrowsAssertion; + this.pendingThrowsAssertion = null; + + const values = []; + if (err) { + values.push(formatAssertError.formatWithLabel(`The following error was thrown, possibly before \`t.${pending.assertion}()\` could be called:`, err)); + } + + this.saveFirstError(new assert.AssertionError({ + assertion: pending.assertion, + fixedSource: {file: pending.file, line: pending.line}, + improperUsage: true, + message: `Improper usage of \`t.${pending.assertion}()\` detected`, + stack: err instanceof Error && err.stack, + values + })); + return true; + } + + waitForPendingThrowsAssertion() { + return new Promise(resolve => { + this.finishDueToAttributedError = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToInactivity = () => { + this.detectImproperThrows(); + resolve(this.finishPromised()); + }; + + // Wait up to a second to see if an error can be attributed to the + // pending assertion. + globals.setTimeout(() => this.finishDueToInactivity(), 1000).unref(); + }); + } + + attributeLeakedError(err) { + if (!this.detectImproperThrows(err)) { + return false; + } + + this.finishDueToAttributedError(); + return true; + } + + callFn() { + try { + return { + ok: true, + retval: this.fn(this.createExecutionContext()) + }; + } catch (err) { + return { + ok: false, + error: err + }; + } + } + + run() { + this.startedAt = globals.now(); + + const result = this.callFn(); + if (!result.ok) { + if (!this.detectImproperThrows(result.error)) { + this.saveFirstError(new assert.AssertionError({ + message: 'Error thrown in test', + stack: result.error instanceof Error && result.error.stack, + values: [formatAssertError.formatWithLabel('Error:', result.error)] + })); + } + return this.finish(); + } + + const returnedObservable = isObservable(result.retval); + const returnedPromise = isPromise(result.retval); + + let promise; + if (returnedObservable) { + promise = observableToPromise(result.retval); + } else if (returnedPromise) { + // `retval` can be any thenable, so convert to a proper promise. + promise = Promise.resolve(result.retval); + } + + if (this.metadata.callback) { + if (returnedObservable || returnedPromise) { + const asyncType = returnedObservable ? 'observables' : 'promises'; + this.saveFirstError(new Error(`Do not return ${asyncType} from tests declared via \`test.cb(...)\`, if you want to return a promise simply declare the test via \`test(...)\``)); + return this.finish(); + } + + if (this.calledEnd) { + return this.finish(); + } + + return new Promise(resolve => { + this.endCallbackFinisher = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToAttributedError = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToInactivity = () => { + this.saveFirstError(new Error('`t.end()` was never called')); + resolve(this.finishPromised()); + }; + }); + } + + if (promise) { + return new Promise(resolve => { + this.finishDueToAttributedError = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToInactivity = () => { + const err = returnedObservable ? + new Error('Observable returned by test never completed') : + new Error('Promise returned by test never resolved'); + this.saveFirstError(err); + resolve(this.finishPromised()); + }; + + promise + .catch(err => { + if (!this.detectImproperThrows(err)) { + this.saveFirstError(new assert.AssertionError({ + message: 'Rejected promise returned by test', + stack: err instanceof Error && err.stack, + values: [formatAssertError.formatWithLabel('Rejection reason:', err)] + })); + } + }) + .then(() => resolve(this.finishPromised())); + }); + } + + return this.finish(); + } + + finish() { + this.finishing = true; + + if (!this.assertError && this.pendingThrowsAssertion) { + return this.waitForPendingThrowsAssertion(); + } + + this.verifyPlan(); + this.verifyAssertions(); + + this.duration = globals.now() - this.startedAt; + + let reason = this.assertError; + let passed = !reason; + + if (this.metadata.failing) { + passed = !passed; + + if (passed) { + reason = undefined; + } else { + reason = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing'); + } + } + + this.onResult({ + passed, + result: this, + reason + }); + + return passed; + } + + finishPromised() { + return new Promise(resolve => { + resolve(this.finish()); + }); + } +} + +module.exports = Test; |