Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 18 additions & 42 deletions lib/assert.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {isNativeError} from 'node:util/types';

import concordance from 'concordance';
import isPromise from 'is-promise';

import {AssertionError, getAssertionStack} from './assertion-error.js';
import concordanceOptions from './concordance-options.js';
import concordance from './concordance.js';
import {CIRCULAR_SELECTOR, isLikeSelector, selectComparable} from './like-selector.js';
import {SnapshotError, VersionMismatchError} from './snapshot-manager.js';

Expand All @@ -13,19 +14,19 @@ function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
const {insertLine, deleteLine} = options.theme.string.diff;
return {
label: `Difference (${diffGutters.actual}${deleteLine.open}actual${deleteLine.close}, ${diffGutters.expected}${insertLine.open}expected${insertLine.close}):`,
formatted: concordance.diffDescriptors(actualDescriptor, expectedDescriptor, options),
formatted: concordance().diffDescriptors(actualDescriptor, expectedDescriptor, options),
};
}

function formatDescriptorWithLabel(label, descriptor) {
return {
label,
formatted: concordance.formatDescriptor(descriptor, concordanceOptions),
formatted: concordance().formatDescriptor(descriptor, concordanceOptions),
};
}

function formatWithLabel(label, value) {
return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions));
return formatDescriptorWithLabel(label, concordance().describe(value, concordanceOptions));
}

const noop = () => {};
Expand All @@ -34,24 +35,6 @@ const notImplemented = () => {
throw new Error('not implemented');
};

export class AssertionError extends Error {
constructor(message = '', {
assertion,
assertionStack = getAssertionStack(AssertionError),
formattedDetails = [],
improperUsage = null,
cause,
} = {}) {
super(message, {cause});
this.name = 'AssertionError';

this.assertion = assertion;
this.assertionStack = assertionStack;
this.improperUsage = improperUsage;
this.formattedDetails = formattedDetails;
}
}

export function checkAssertionMessage(message, assertion) {
if (message === undefined || typeof message === 'string') {
return true;
Expand All @@ -63,15 +46,6 @@ export function checkAssertionMessage(message, assertion) {
});
}

export function getAssertionStack(constructorOpt = getAssertionStack) {
const {stackTraceLimit: limitBefore} = Error;
Error.stackTraceLimit = Number.POSITIVE_INFINITY;
const temporary = {};
Error.captureStackTrace(temporary, constructorOpt);
Error.stackTraceLimit = limitBefore;
return temporary.stack;
}

function validateExpectations(assertion, expectations, numberArgs) { // eslint-disable-line complexity
if (numberArgs === 1 || expectations === null || expectations === undefined) {
if (expectations === null) {
Expand Down Expand Up @@ -297,9 +271,9 @@ export class Assertions {
return pass();
}

const result = concordance.compare(actual, expected, concordanceOptions);
const actualDescriptor = result.actual ?? concordance.describe(actual, concordanceOptions);
const expectedDescriptor = result.expected ?? concordance.describe(expected, concordanceOptions);
const result = concordance().compare(actual, expected, concordanceOptions);
const actualDescriptor = result.actual ?? concordance().describe(actual, concordanceOptions);
const expectedDescriptor = result.expected ?? concordance().describe(expected, concordanceOptions);

if (result.pass) {
throw fail(new AssertionError(message, {
Expand Down Expand Up @@ -330,13 +304,13 @@ export class Assertions {
this.deepEqual = withSkip((actual, expected, message) => {
assertMessage(message, 't.deepEqual()');

const result = concordance.compare(actual, expected, concordanceOptions);
const result = concordance().compare(actual, expected, concordanceOptions);
if (result.pass) {
return pass();
}

const actualDescriptor = result.actual ?? concordance.describe(actual, concordanceOptions);
const expectedDescriptor = result.expected ?? concordance.describe(expected, concordanceOptions);
const actualDescriptor = result.actual ?? concordance().describe(actual, concordanceOptions);
const expectedDescriptor = result.expected ?? concordance().describe(expected, concordanceOptions);
throw fail(new AssertionError(message, {
assertion: 't.deepEqual()',
formattedDetails: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)],
Expand All @@ -346,9 +320,9 @@ export class Assertions {
this.notDeepEqual = withSkip((actual, expected, message) => {
assertMessage(message, 't.notDeepEqual()');

const result = concordance.compare(actual, expected, concordanceOptions);
const result = concordance().compare(actual, expected, concordanceOptions);
if (result.pass) {
const actualDescriptor = result.actual ?? concordance.describe(actual, concordanceOptions);
const actualDescriptor = result.actual ?? concordance().describe(actual, concordanceOptions);
throw fail(new AssertionError(message, {
assertion: 't.notDeepEqual()',
formattedDetails: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)],
Expand Down Expand Up @@ -382,13 +356,13 @@ export class Assertions {
throw error;
}

const result = concordance.compare(comparable, selector, concordanceOptions);
const result = concordance().compare(comparable, selector, concordanceOptions);
if (result.pass) {
return pass();
}

const actualDescriptor = result.actual ?? concordance.describe(comparable, concordanceOptions);
const expectedDescriptor = result.expected ?? concordance.describe(selector, concordanceOptions);
const actualDescriptor = result.actual ?? concordance().describe(comparable, concordanceOptions);
const expectedDescriptor = result.expected ?? concordance().describe(selector, concordanceOptions);
throw fail(new AssertionError(message, {
assertion: 't.like()',
formattedDetails: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)],
Expand Down Expand Up @@ -810,3 +784,5 @@ export class Assertions {
});
}
}

export {AssertionError, getAssertionStack} from './assertion-error.js';
26 changes: 26 additions & 0 deletions lib/assertion-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function getAssertionStack(constructorOpt = getAssertionStack) {
const {stackTraceLimit: limitBefore} = Error;
Error.stackTraceLimit = Number.POSITIVE_INFINITY;
const temporary = {};
Error.captureStackTrace(temporary, constructorOpt);
Error.stackTraceLimit = limitBefore;
return temporary.stack;
}

export class AssertionError extends Error {
constructor(message = '', {
assertion,
assertionStack = getAssertionStack(AssertionError),
formattedDetails = [],
improperUsage = null,
cause,
} = {}) {
super(message, {cause});
this.name = 'AssertionError';

this.assertion = assertion;
this.assertionStack = assertionStack;
this.improperUsage = improperUsage;
this.formattedDetails = formattedDetails;
}
}
12 changes: 12 additions & 0 deletions lib/concordance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {createRequire} from 'node:module';

// Concordance (and its lodash dependency) is expensive to evaluate, so it's loaded lazily on first use.
// Workers running passing simple assertions, plain `t.log` calls and snapshot-free files never touch it,
// and the main process only needs it to format the occasional non-native error.
const require = createRequire(import.meta.url);

let concordance;
export default function loadConcordance() {
concordance ??= require('concordance');
return concordance;
}
11 changes: 9 additions & 2 deletions lib/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,15 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
let finished = false;

const emitter = new Emittery();
// `stateChange` is high-frequency and consumed synchronously, so it bypasses Emittery's per-emit async overhead.
// `connectSharedWorker` is rare and stays on Emittery.
const stateChangeListeners = new Set();
const emitStateChange = evt => {
if (!finished) {
emitter.emit('stateChange', Object.assign(evt, {testFile: file}));
const data = Object.assign(evt, {testFile: file});
for (const listener of stateChangeListeners) {
listener(data);
}
}
};

Expand Down Expand Up @@ -172,7 +178,8 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
},

onStateChange(listener) {
return emitter.on('stateChange', ({data}) => listener(data));
stateChangeListeners.add(listener);
return () => stateChangeListeners.delete(listener);
},
};
}
3 changes: 2 additions & 1 deletion lib/provider-manager.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as globs from './globs.js';
import pkg from './pkg.js';

// Provides an integer representation of the protocol level. This is internal to a particular AVA installation, and
Expand All @@ -14,6 +13,8 @@ const levelsByProtocol = Object.assign(Object.create(null), {

async function load(providerModule, projectDir, selectProtocol = () => true) {
const ava = {version: pkg.version};
// Loaded lazily so that workers running plain JavaScript don't pay for the glob stack they never use.
const globs = await import('./globs.js');
const {default: makeProvider} = await import(providerModule);

let fatal;
Expand Down
10 changes: 7 additions & 3 deletions lib/run-status.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import v8 from 'node:v8';

import Emittery from 'emittery';

const copyStats = stats => v8.deserialize(v8.serialize(stats));
// A purpose-built deep copy. The stats object is flat counters plus a `byFile` map of flat counters,
// so this is far cheaper than a generic structured clone and runs on every state change.
const copyStats = stats => ({
...stats,
byFile: new Map(Array.from(stats.byFile, ([file, fileStats]) => [file, {...fileStats}])),
parallelRuns: stats.parallelRuns && {...stats.parallelRuns},
});

export default class RunStatus extends Emittery {
constructor(files, parallelRuns, selectionInsights) {
Expand Down
8 changes: 4 additions & 4 deletions lib/serialize-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import path from 'node:path';
import {pathToFileURL} from 'node:url';
import {isNativeError} from 'node:util/types';

import concordance from 'concordance';
import StackUtils from 'stack-utils';

import {AssertionError} from './assert.js';
import {AssertionError} from './assertion-error.js';
import concordanceOptions from './concordance-options.js';
import concordance from './concordance.js';

function isAvaAssertionError(source) {
return source instanceof AssertionError;
Expand Down Expand Up @@ -57,7 +57,7 @@ export default function serializeError(error, {testFile = null} = {}) {
return {
type: 'unknown',
originalError: error, // Note that the main process receives a structured clone.
formattedError: concordance.formatDescriptor(concordance.describe(error, concordanceOptions), concordanceOptions),
formattedError: concordance().formatDescriptor(concordance().describe(error, concordanceOptions), concordanceOptions),
};
}

Expand Down Expand Up @@ -90,7 +90,7 @@ export default function serializeError(error, {testFile = null} = {}) {
type: 'ava',
assertion: error.assertion,
improperUsage: error.improperUsage,
formattedCause: error.cause ? concordance.formatDescriptor(concordance.describe(error.cause, concordanceOptions), concordanceOptions) : null,
formattedCause: error.cause ? concordance().formatDescriptor(concordance().describe(error.cause, concordanceOptions), concordanceOptions) : null,
formattedDetails: error.formattedDetails,
source: extractSource(error.assertionStack, testFile),
stack: isNativeError(error.cause) ? error.cause.stack : error.assertionStack,
Expand Down
14 changes: 7 additions & 7 deletions lib/snapshot-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import zlib from 'node:zlib';
import {decode as decodeCbor, encode as encodeCbor, TypeEncoderMap} from 'cbor2';
import {writeArray, writeUint8Array} from 'cbor2/encoder';
import {sortLengthFirstDeterministic} from 'cbor2/sorts';
import concordance from 'concordance';
import indentString from 'indent-string';
import memoize from 'memoize';
import slash from 'slash';
import writeFileAtomic from 'write-file-atomic';

import {snapshotManager as concordanceOptions} from './concordance-options.js';
import concordance from './concordance.js';

// Increment if encoding layout or Concordance serialization versions change. Previous AVA versions will not be able to
// decode buffers generated by a newer version, so changing this value will require a major version bump of AVA itself.
Expand Down Expand Up @@ -94,7 +94,7 @@ function formatEntry(snapshot, index) {
} = snapshot;

const description = data
? concordance.formatDescriptor(concordance.deserialize(Buffer.from(data.buffer, data.byteOffset, data.byteLength)), concordanceOptions)
? concordance().formatDescriptor(concordance().deserialize(Buffer.from(data.buffer, data.byteOffset, data.byteLength)), concordanceOptions)
: '<No Data>';

const blockquote = label.split(/\n/).map(line => '> ' + line).join('\n');
Expand Down Expand Up @@ -312,9 +312,9 @@ class Manager {
return {pass: true};
}

const actual = concordance.deserialize(Buffer.from(data.buffer, data.byteOffset, data.byteLength), concordanceOptions);
const expected = concordance.describe(options.expected, concordanceOptions);
const pass = concordance.compareDescriptors(actual, expected);
const actual = concordance().deserialize(Buffer.from(data.buffer, data.byteOffset, data.byteLength), concordanceOptions);
const expected = concordance().describe(options.expected, concordanceOptions);
const pass = concordance().compareDescriptors(actual, expected);

return {actual, expected, pass};
}
Expand All @@ -340,8 +340,8 @@ class Manager {

deferRecord(options) {
const {expected, belongsTo, label, index} = options;
const descriptor = concordance.describe(expected, concordanceOptions);
const buffer = concordance.serialize(descriptor);
const descriptor = concordance().describe(expected, concordanceOptions);
const buffer = concordance().serialize(descriptor);
const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);

return () => { // Must be called in order!
Expand Down
6 changes: 3 additions & 3 deletions lib/test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import concordance from 'concordance';
import isPromise from 'is-promise';
import plur from 'plur';

import {
AssertionError, Assertions, checkAssertionMessage, getAssertionStack,
} from './assert.js';
import concordanceOptions from './concordance-options.js';
import concordance from './concordance.js';
import * as nowAndTimers from './now-and-timers.js';
import parseTestArgs from './parse-test-args.js';

Expand All @@ -24,7 +24,7 @@ function isExternalAssertError(error) {
}

function formatErrorValue(label, error) {
const formatted = concordance.format(error, concordanceOptions);
const formatted = concordance().format(error, concordanceOptions);
return {label, formatted};
}

Expand Down Expand Up @@ -68,7 +68,7 @@ class ExecutionContext extends Assertions {
this.log = (...inputArgs) => {
const args = inputArgs.map(value => typeof value === 'string'
? value
: concordance.format(value, concordanceOptions));
: concordance().format(value, concordanceOptions));
if (args.length > 0) {
test.addLog(args.join(' '));
}
Expand Down
Loading
Loading