mirror of
https://github.com/github/codeql-action.git
synced 2025-12-12 10:44:43 +08:00
The [release notes](https://github.com/avajs/ava/releases/tag/v4.3.3) mention compatibility with Node 18.8.
612 lines
15 KiB
JavaScript
612 lines
15 KiB
JavaScript
import concordance from 'concordance';
|
||
import isPromise from 'is-promise';
|
||
import plur from 'plur';
|
||
|
||
import {AssertionError, Assertions, checkAssertionMessage} from './assert.js';
|
||
import concordanceOptions from './concordance-options.js';
|
||
import nowAndTimers from './now-and-timers.cjs';
|
||
import parseTestArgs from './parse-test-args.js';
|
||
|
||
function formatErrorValue(label, error) {
|
||
const formatted = concordance.format(error, concordanceOptions);
|
||
return {label, formatted};
|
||
}
|
||
|
||
const captureSavedError = () => {
|
||
const limitBefore = Error.stackTraceLimit;
|
||
Error.stackTraceLimit = 1;
|
||
const error = new Error(); // eslint-disable-line unicorn/error-message
|
||
Error.stackTraceLimit = limitBefore;
|
||
return error;
|
||
};
|
||
|
||
const testMap = new WeakMap();
|
||
class ExecutionContext extends Assertions {
|
||
constructor(test) {
|
||
super({
|
||
pass() {
|
||
test.countPassedAssertion();
|
||
},
|
||
pending(promise) {
|
||
test.addPendingAssertion(promise);
|
||
},
|
||
fail(error) {
|
||
test.addFailedAssertion(error);
|
||
},
|
||
skip() {
|
||
test.countPassedAssertion();
|
||
},
|
||
compareWithSnapshot: options => test.compareWithSnapshot(options),
|
||
experiments: test.experiments,
|
||
disableSnapshots: test.isHook === true,
|
||
});
|
||
testMap.set(this, test);
|
||
|
||
this.snapshot.skip = () => {
|
||
test.skipSnapshot();
|
||
};
|
||
|
||
this.log = (...inputArgs) => {
|
||
const args = inputArgs.map(value => typeof value === 'string'
|
||
? value
|
||
: concordance.format(value, concordanceOptions));
|
||
if (args.length > 0) {
|
||
test.addLog(args.join(' '));
|
||
}
|
||
};
|
||
|
||
this.plan = count => {
|
||
test.plan(count, captureSavedError());
|
||
};
|
||
|
||
this.plan.skip = () => {};
|
||
|
||
this.timeout = (ms, message) => {
|
||
test.timeout(ms, message);
|
||
};
|
||
|
||
this.teardown = callback => {
|
||
test.addTeardown(callback);
|
||
};
|
||
|
||
this.try = async (...attemptArgs) => {
|
||
if (test.isHook) {
|
||
const error = new Error('`t.try()` can only be used in tests');
|
||
test.saveFirstError(error);
|
||
throw error;
|
||
}
|
||
|
||
const {args, implementation, title} = parseTestArgs(attemptArgs);
|
||
|
||
if (!implementation) {
|
||
throw new TypeError('Expected an implementation.');
|
||
}
|
||
|
||
if (Array.isArray(implementation)) {
|
||
throw new TypeError('AVA 4 no longer supports t.try() with multiple implementations.');
|
||
}
|
||
|
||
let attemptTitle;
|
||
if (!title.isSet || title.isEmpty) {
|
||
attemptTitle = `${test.title} ─ attempt ${test.attemptCount + 1}`;
|
||
} else if (title.isValid) {
|
||
attemptTitle = `${test.title} ─ ${title.value}`;
|
||
} else {
|
||
throw new TypeError('`t.try()` titles must be strings');
|
||
}
|
||
|
||
if (!test.registerUniqueTitle(attemptTitle)) {
|
||
throw new Error(`Duplicate test title: ${attemptTitle}`);
|
||
}
|
||
|
||
let committed = false;
|
||
let discarded = false;
|
||
|
||
const {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount} = await test.runAttempt(attemptTitle, t => implementation(t, ...args));
|
||
|
||
return {
|
||
errors,
|
||
logs: [...logs], // Don't allow modification of logs.
|
||
passed,
|
||
title: attemptTitle,
|
||
commit({retainLogs = true} = {}) {
|
||
if (committed) {
|
||
return;
|
||
}
|
||
|
||
if (discarded) {
|
||
test.saveFirstError(new Error('Can’t commit a result that was previously discarded'));
|
||
return;
|
||
}
|
||
|
||
committed = true;
|
||
test.finishAttempt({
|
||
assertCount,
|
||
commit: true,
|
||
deferredSnapshotRecordings,
|
||
errors,
|
||
logs,
|
||
passed,
|
||
retainLogs,
|
||
snapshotCount,
|
||
startingSnapshotCount,
|
||
});
|
||
},
|
||
discard({retainLogs = false} = {}) {
|
||
if (committed) {
|
||
test.saveFirstError(new Error('Can’t discard a result that was previously committed'));
|
||
return;
|
||
}
|
||
|
||
if (discarded) {
|
||
return;
|
||
}
|
||
|
||
discarded = true;
|
||
test.finishAttempt({
|
||
assertCount: 0,
|
||
commit: false,
|
||
deferredSnapshotRecordings,
|
||
errors,
|
||
logs,
|
||
passed,
|
||
retainLogs,
|
||
snapshotCount,
|
||
startingSnapshotCount,
|
||
});
|
||
},
|
||
};
|
||
};
|
||
}
|
||
|
||
get title() {
|
||
return testMap.get(this).title;
|
||
}
|
||
|
||
get context() {
|
||
return testMap.get(this).contextRef.get();
|
||
}
|
||
|
||
set context(context) {
|
||
testMap.get(this).contextRef.set(context);
|
||
}
|
||
|
||
get passed() {
|
||
const test = testMap.get(this);
|
||
return test.isHook ? test.testPassed : !test.assertError;
|
||
}
|
||
}
|
||
|
||
export default class Test {
|
||
constructor(options) {
|
||
this.contextRef = options.contextRef;
|
||
this.experiments = options.experiments || {};
|
||
this.failWithoutAssertions = options.failWithoutAssertions;
|
||
this.fn = options.fn;
|
||
this.isHook = options.isHook === true;
|
||
this.metadata = options.metadata;
|
||
this.title = options.title;
|
||
this.testPassed = options.testPassed;
|
||
this.registerUniqueTitle = options.registerUniqueTitle;
|
||
this.logs = [];
|
||
this.teardowns = [];
|
||
this.notifyTimeoutUpdate = options.notifyTimeoutUpdate;
|
||
|
||
const {snapshotBelongsTo = this.title, nextSnapshotIndex = 0} = options;
|
||
this.snapshotBelongsTo = snapshotBelongsTo;
|
||
this.nextSnapshotIndex = nextSnapshotIndex;
|
||
this.snapshotCount = 0;
|
||
|
||
const deferRecording = this.metadata.inline;
|
||
this.deferredSnapshotRecordings = [];
|
||
this.compareWithSnapshot = ({expected, message}) => {
|
||
this.snapshotCount++;
|
||
|
||
const belongsTo = snapshotBelongsTo;
|
||
const index = this.nextSnapshotIndex++;
|
||
const label = message;
|
||
|
||
const {record, ...result} = options.compareTestSnapshot({
|
||
belongsTo,
|
||
deferRecording,
|
||
expected,
|
||
index,
|
||
label,
|
||
taskIndex: this.metadata.taskIndex,
|
||
});
|
||
if (record) {
|
||
this.deferredSnapshotRecordings.push(record);
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
this.skipSnapshot = () => {
|
||
if (typeof options.skipSnapshot === 'function') {
|
||
const record = options.skipSnapshot({
|
||
belongsTo: snapshotBelongsTo,
|
||
index: this.nextSnapshotIndex,
|
||
deferRecording,
|
||
taskIndex: this.metadata.taskIndex,
|
||
});
|
||
if (record) {
|
||
this.deferredSnapshotRecordings.push(record);
|
||
}
|
||
}
|
||
|
||
this.nextSnapshotIndex++;
|
||
this.snapshotCount++;
|
||
this.countPassedAssertion();
|
||
};
|
||
|
||
this.runAttempt = async (title, fn) => {
|
||
if (this.finishing) {
|
||
this.saveFirstError(new Error('Running a `t.try()`, but the test has already finished'));
|
||
}
|
||
|
||
this.attemptCount++;
|
||
this.pendingAttemptCount++;
|
||
|
||
const {contextRef, snapshotBelongsTo, nextSnapshotIndex, snapshotCount: startingSnapshotCount} = this;
|
||
const attempt = new Test({
|
||
...options,
|
||
fn,
|
||
metadata: {...options.metadata, failing: false, inline: true},
|
||
contextRef: contextRef.copy(),
|
||
snapshotBelongsTo,
|
||
nextSnapshotIndex,
|
||
title,
|
||
});
|
||
|
||
const {deferredSnapshotRecordings, error, logs, passed, assertCount, snapshotCount} = await attempt.run();
|
||
const errors = error ? [error] : [];
|
||
return {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount};
|
||
};
|
||
|
||
this.assertCount = 0;
|
||
this.assertError = undefined;
|
||
this.attemptCount = 0;
|
||
this.calledEnd = false;
|
||
this.duration = null;
|
||
this.finishDueToInactivity = null;
|
||
this.finishDueToTimeout = null;
|
||
this.finishing = false;
|
||
this.pendingAssertionCount = 0;
|
||
this.pendingAttemptCount = 0;
|
||
this.planCount = null;
|
||
this.startedAt = 0;
|
||
this.timeoutMs = 0;
|
||
this.timeoutTimer = null;
|
||
}
|
||
|
||
createExecutionContext() {
|
||
return new ExecutionContext(this);
|
||
}
|
||
|
||
countPassedAssertion() {
|
||
if (this.finishing) {
|
||
this.saveFirstError(new Error('Assertion passed, but test has already finished'));
|
||
}
|
||
|
||
if (this.pendingAttemptCount > 0) {
|
||
this.saveFirstError(new Error('Assertion passed, but an attempt is pending. Use the attempt’s assertions instead'));
|
||
}
|
||
|
||
this.assertCount++;
|
||
this.refreshTimeout();
|
||
}
|
||
|
||
addLog(text) {
|
||
this.logs.push(text);
|
||
}
|
||
|
||
addPendingAssertion(promise) {
|
||
if (this.finishing) {
|
||
this.saveFirstError(new Error('Assertion started, but test has already finished'));
|
||
}
|
||
|
||
if (this.pendingAttemptCount > 0) {
|
||
this.saveFirstError(new Error('Assertion started, but an attempt is pending. Use the attempt’s assertions instead'));
|
||
}
|
||
|
||
this.assertCount++;
|
||
this.pendingAssertionCount++;
|
||
this.refreshTimeout();
|
||
|
||
promise
|
||
.catch(error => this.saveFirstError(error))
|
||
.then(() => {
|
||
this.pendingAssertionCount--;
|
||
this.refreshTimeout();
|
||
});
|
||
}
|
||
|
||
addFailedAssertion(error) {
|
||
if (this.finishing) {
|
||
this.saveFirstError(new Error('Assertion failed, but test has already finished'));
|
||
}
|
||
|
||
if (this.pendingAttemptCount > 0) {
|
||
this.saveFirstError(new Error('Assertion failed, but an attempt is pending. Use the attempt’s assertions instead'));
|
||
}
|
||
|
||
this.assertCount++;
|
||
this.refreshTimeout();
|
||
this.saveFirstError(error);
|
||
}
|
||
|
||
finishAttempt({commit, deferredSnapshotRecordings, errors, logs, passed, retainLogs, snapshotCount, startingSnapshotCount}) {
|
||
if (this.finishing) {
|
||
if (commit) {
|
||
this.saveFirstError(new Error('`t.try()` result was committed, but the test has already finished'));
|
||
} else {
|
||
this.saveFirstError(new Error('`t.try()` result was discarded, but the test has already finished'));
|
||
}
|
||
}
|
||
|
||
if (commit) {
|
||
this.assertCount++;
|
||
|
||
if (startingSnapshotCount === this.snapshotCount) {
|
||
this.snapshotCount += snapshotCount;
|
||
this.nextSnapshotIndex += snapshotCount;
|
||
for (const record of deferredSnapshotRecordings) {
|
||
record();
|
||
}
|
||
} else {
|
||
this.saveFirstError(new Error('Cannot commit `t.try()` result. Do not run concurrent snapshot assertions when using `t.try()`'));
|
||
}
|
||
}
|
||
|
||
this.pendingAttemptCount--;
|
||
|
||
if (commit && !passed) {
|
||
this.saveFirstError(errors[0]);
|
||
}
|
||
|
||
if (retainLogs) {
|
||
for (const log of logs) {
|
||
this.addLog(log);
|
||
}
|
||
}
|
||
|
||
this.refreshTimeout();
|
||
}
|
||
|
||
saveFirstError(error) {
|
||
if (!this.assertError) {
|
||
this.assertError = error;
|
||
}
|
||
}
|
||
|
||
plan(count, planError) {
|
||
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.planError = planError;
|
||
}
|
||
|
||
timeout(ms, message) {
|
||
const result = checkAssertionMessage('timeout', message);
|
||
if (result !== true) {
|
||
this.saveFirstError(result);
|
||
// Allow the timeout to be set even when the message is invalid.
|
||
message = '';
|
||
}
|
||
|
||
if (this.finishing) {
|
||
return;
|
||
}
|
||
|
||
this.clearTimeout();
|
||
this.timeoutMs = ms;
|
||
this.timeoutTimer = nowAndTimers.setTimeout(() => {
|
||
this.saveFirstError(new Error(message || 'Test timeout exceeded'));
|
||
|
||
if (this.finishDueToTimeout) {
|
||
this.finishDueToTimeout();
|
||
}
|
||
}, ms);
|
||
|
||
this.notifyTimeoutUpdate(this.timeoutMs);
|
||
}
|
||
|
||
refreshTimeout() {
|
||
if (!this.timeoutTimer) {
|
||
return;
|
||
}
|
||
|
||
if (this.timeoutTimer.refresh) {
|
||
this.timeoutTimer.refresh();
|
||
} else {
|
||
this.timeout(this.timeoutMs);
|
||
}
|
||
}
|
||
|
||
clearTimeout() {
|
||
nowAndTimers.clearTimeout(this.timeoutTimer);
|
||
this.timeoutTimer = null;
|
||
}
|
||
|
||
addTeardown(callback) {
|
||
if (this.isHook) {
|
||
this.saveFirstError(new Error('`t.teardown()` is not allowed in hooks'));
|
||
return;
|
||
}
|
||
|
||
if (this.finishing) {
|
||
this.saveFirstError(new Error('`t.teardown()` cannot be used during teardown'));
|
||
return;
|
||
}
|
||
|
||
if (typeof callback !== 'function') {
|
||
throw new TypeError('Expected a function');
|
||
}
|
||
|
||
this.teardowns.push(callback);
|
||
}
|
||
|
||
async runTeardowns() {
|
||
const teardowns = [...this.teardowns].reverse();
|
||
|
||
for (const teardown of teardowns) {
|
||
try {
|
||
await teardown(); // eslint-disable-line no-await-in-loop
|
||
} catch (error) {
|
||
this.saveFirstError(error);
|
||
}
|
||
}
|
||
}
|
||
|
||
verifyPlan() {
|
||
if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) {
|
||
this.saveFirstError(new AssertionError({
|
||
assertion: 'plan',
|
||
message: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`,
|
||
operator: '===',
|
||
savedError: this.planError,
|
||
}));
|
||
}
|
||
}
|
||
|
||
verifyAssertions() {
|
||
if (this.assertError) {
|
||
return;
|
||
}
|
||
|
||
if (this.pendingAttemptCount > 0) {
|
||
this.saveFirstError(new Error('Test finished, but not all attempts were committed or discarded'));
|
||
return;
|
||
}
|
||
|
||
if (this.pendingAssertionCount > 0) {
|
||
this.saveFirstError(new Error('Test finished, but an assertion is still pending'));
|
||
return;
|
||
}
|
||
|
||
if (this.failWithoutAssertions) {
|
||
if (this.planCount !== null) {
|
||
return; // `verifyPlan()` will report an error already.
|
||
}
|
||
|
||
if (this.assertCount === 0 && !this.calledEnd) {
|
||
this.saveFirstError(new Error('Test finished without running any assertions'));
|
||
}
|
||
}
|
||
}
|
||
|
||
callFn() {
|
||
try {
|
||
return {
|
||
ok: true,
|
||
retval: this.fn.call(null, this.createExecutionContext()),
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
ok: false,
|
||
error,
|
||
};
|
||
}
|
||
}
|
||
|
||
run() {
|
||
this.startedAt = nowAndTimers.now();
|
||
|
||
const result = this.callFn();
|
||
if (!result.ok) {
|
||
this.saveFirstError(new AssertionError({
|
||
message: 'Error thrown in test',
|
||
savedError: result.error instanceof Error && result.error,
|
||
values: [formatErrorValue('Error thrown in test:', result.error)],
|
||
}));
|
||
|
||
return this.finish();
|
||
}
|
||
|
||
const returnedObservable = result.retval !== null && typeof result.retval === 'object' && typeof result.retval.subscribe === 'function';
|
||
const returnedPromise = isPromise(result.retval);
|
||
|
||
let promise;
|
||
if (returnedObservable) {
|
||
promise = new Promise((resolve, reject) => {
|
||
result.retval.subscribe({
|
||
error: reject,
|
||
complete: () => resolve(),
|
||
});
|
||
});
|
||
} else if (returnedPromise) {
|
||
// `retval` can be any thenable, so convert to a proper promise.
|
||
promise = Promise.resolve(result.retval);
|
||
}
|
||
|
||
if (promise) {
|
||
return new Promise(resolve => {
|
||
this.finishDueToAttributedError = () => {
|
||
resolve(this.finish());
|
||
};
|
||
|
||
this.finishDueToTimeout = () => {
|
||
resolve(this.finish());
|
||
};
|
||
|
||
this.finishDueToInactivity = () => {
|
||
const error = returnedObservable
|
||
? new Error('Observable returned by test never completed')
|
||
: new Error('Promise returned by test never resolved');
|
||
this.saveFirstError(error);
|
||
resolve(this.finish());
|
||
};
|
||
|
||
promise
|
||
.catch(error => {
|
||
this.saveFirstError(new AssertionError({
|
||
message: 'Rejected promise returned by test',
|
||
savedError: error instanceof Error && error,
|
||
values: [formatErrorValue('Rejected promise returned by test. Reason:', error)],
|
||
}));
|
||
})
|
||
.then(() => resolve(this.finish()));
|
||
});
|
||
}
|
||
|
||
return this.finish();
|
||
}
|
||
|
||
async finish() {
|
||
this.finishing = true;
|
||
|
||
this.clearTimeout();
|
||
this.verifyPlan();
|
||
this.verifyAssertions();
|
||
await this.runTeardowns();
|
||
|
||
this.duration = nowAndTimers.now() - this.startedAt;
|
||
|
||
let error = this.assertError;
|
||
let passed = !error;
|
||
|
||
if (this.metadata.failing) {
|
||
passed = !passed;
|
||
|
||
error = passed ? null : new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
|
||
}
|
||
|
||
return {
|
||
deferredSnapshotRecordings: this.deferredSnapshotRecordings,
|
||
duration: this.duration,
|
||
error,
|
||
logs: this.logs,
|
||
metadata: this.metadata,
|
||
passed,
|
||
snapshotCount: this.snapshotCount,
|
||
assertCount: this.assertCount,
|
||
title: this.title,
|
||
};
|
||
}
|
||
}
|