Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Lodash with built-in syntax, libraries, and some code #2851

Merged
merged 9 commits into from
Oct 4, 2021
2 changes: 1 addition & 1 deletion docs/01-writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ test('context data is foo', t => {
});
```

Context created in `.before()` hooks is [cloned](https://www.npmjs.com/package/lodash.clone) before it is passed to `.beforeEach()` hooks and / or tests. The `.after()` and `.after.always()` hooks receive the original context value.
If `.before()` hooks treat `t.context` as an object, a shallow copy is made and passed to `.beforeEach()` hooks and / or tests. Other types of values are passed as-is. The `.after()` and `.after.always()` hooks receive the original context value.

For `.beforeEach()`, `.afterEach()` and `.afterEach.always()` hooks the context is *not* shared between different tests, allowing you to set up data such that it will not leak to other tests.

Expand Down
58 changes: 42 additions & 16 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import arrify from 'arrify';
import chunkd from 'chunkd';
import commonPathPrefix from 'common-path-prefix';
import Emittery from 'emittery';
import debounce from 'lodash/debounce.js';
import ms from 'ms';
import pMap from 'p-map';
import resolveCwd from 'resolve-cwd';
Expand Down Expand Up @@ -43,6 +42,39 @@ function getFilePathPrefix(files) {
return commonPathPrefix(files);
}

class TimeoutTrigger {
constructor(fn, waitMs = 0) {
this.fn = fn.bind(null);
this.ignoreUntil = 0;
this.waitMs = waitMs;
this.timer = undefined;
}

debounce() {
if (this.timer === undefined) {
this.timer = setTimeout(() => this.trigger(), this.waitMs);
} else {
this.timer.refresh();
}
}

discard() {
// N.B. this.timer is not cleared so if debounce() is called after it will
// not run again.
clearTimeout(this.timer);
}

ignoreFor(periodMs) {
this.ignoreUntil = Math.max(this.ignoreUntil, Date.now() + periodMs);
}

trigger() {
if (Date.now() >= this.ignoreUntil) {
this.fn();
}
}
}

export default class Api extends Emittery {
constructor(options) {
super();
Expand Down Expand Up @@ -73,17 +105,11 @@ export default class Api extends Emittery {
let bailed = false;
const pendingWorkers = new Set();
const timedOutWorkerFiles = new Set();
let restartTimer;
let ignoreTimeoutsUntil = 0;
let timeoutTrigger;
if (apiOptions.timeout && !apiOptions.debug) {
const timeout = ms(apiOptions.timeout);

restartTimer = debounce(() => {
if (Date.now() < ignoreTimeoutsUntil) {
restartTimer();
return;
}

timeoutTrigger = new TimeoutTrigger(() => {
// If failFast is active, prevent new test files from running after
// the current ones are exited.
if (failFast) {
Expand All @@ -98,7 +124,7 @@ export default class Api extends Emittery {
}
}, timeout);
} else {
restartTimer = Object.assign(() => {}, {cancel() {}});
timeoutTrigger = new TimeoutTrigger(() => {});
}

this._interruptHandler = () => {
Expand All @@ -111,7 +137,7 @@ export default class Api extends Emittery {
bailed = true;

// Make sure we don't run the timeout handler
restartTimer.cancel();
timeoutTrigger.cancel();

runStatus.emitStateChange({type: 'interrupt'});

Expand Down Expand Up @@ -180,9 +206,9 @@ export default class Api extends Emittery {

runStatus.on('stateChange', record => {
if (record.testFile && !timedOutWorkerFiles.has(record.testFile)) {
// Restart the timer whenever there is activity from workers that
// Debounce the timer whenever there is activity from workers that
// haven't already timed out.
restartTimer();
timeoutTrigger.debounce();
}

if (failFast && (record.type === 'hook-failed' || record.type === 'test-failed' || record.type === 'worker-failed')) {
Expand Down Expand Up @@ -242,7 +268,7 @@ export default class Api extends Emittery {
const worker = fork(file, options, apiOptions.nodeArguments);
worker.onStateChange(data => {
if (data.type === 'test-timeout-configured' && !apiOptions.debug) {
ignoreTimeoutsUntil = Math.max(ignoreTimeoutsUntil, Date.now() + data.period);
timeoutTrigger.ignoreFor(data.period);
}
});
runStatus.observeWorker(worker, file, {selectingLines: lineNumbers.length > 0});
Expand All @@ -252,7 +278,7 @@ export default class Api extends Emittery {
worker.promise.then(() => {
pendingWorkers.delete(worker);
});
restartTimer();
timeoutTrigger.debounce();

await worker.promise;
}, {concurrency, stopOnError: false});
Expand All @@ -270,7 +296,7 @@ export default class Api extends Emittery {
}
}

restartTimer.cancel();
timeoutTrigger.discard();
return runStatus;
}

Expand Down
7 changes: 1 addition & 6 deletions lib/concordance-options.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {inspect} from 'node:util';

import ansiStyles from 'ansi-styles';
import cloneDeepWith from 'lodash/cloneDeepWith.js';
import stripAnsi from 'strip-ansi';

import {chalk} from './chalk.js';
Expand Down Expand Up @@ -85,11 +84,7 @@ const colorTheme = {
undefined: ansiStyles.yellow,
};

const plainTheme = cloneDeepWith(colorTheme, value => {
if (typeof value === 'string') {
return stripAnsi(value);
}
});
const plainTheme = JSON.parse(JSON.stringify(colorTheme), value => typeof value === 'string' ? stripAnsi(value) : value);

const theme = chalk.level > 0 ? colorTheme : plainTheme;

Expand Down
5 changes: 2 additions & 3 deletions lib/context-ref.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import clone from 'lodash/clone.js';

export default class ContextRef {
constructor() {
this.value = {};
Expand Down Expand Up @@ -27,7 +25,8 @@ class LateBinding extends ContextRef {

get() {
if (!this.bound) {
this.set(clone(this.ref.get()));
const value = this.ref.get();
this.set(value !== null && typeof value === 'object' ? {...value} : value);
}

return super.get();
Expand Down
13 changes: 6 additions & 7 deletions lib/line-numbers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import flatten from 'lodash/flatten.js';
import picomatch from 'picomatch';

const NUMBER_REGEX = /^\d+$/;
Expand All @@ -17,8 +16,8 @@ const parseNumber = string => Number.parseInt(string, 10);
const removeAllWhitespace = string => string.replace(/\s/g, '');
const range = (start, end) => Array.from({length: end - start + 1}).fill(start).map((element, index) => element + index);

const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(flatten(
suffix.split(',').map(part => {
const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(
suffix.split(',').flatMap(part => {
if (NUMBER_REGEX.test(part)) {
return parseNumber(part);
}
Expand All @@ -33,7 +32,7 @@ const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(flatten(

return range(start, end);
}),
)));
));

export function splitPatternAndLineNumbers(pattern) {
const parts = pattern.split(DELIMITER);
Expand All @@ -50,9 +49,9 @@ export function splitPatternAndLineNumbers(pattern) {
}

export function getApplicableLineNumbers(normalizedFilePath, filter) {
return sortNumbersAscending(distinctArray(flatten(
return sortNumbersAscending(distinctArray(
filter
.filter(({pattern, lineNumbers}) => lineNumbers && picomatch.isMatch(normalizedFilePath, pattern))
.map(({lineNumbers}) => lineNumbers),
)));
.flatMap(({lineNumbers}) => lineNumbers),
));
}
7 changes: 5 additions & 2 deletions lib/run-status.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import v8 from 'node:v8';

import Emittery from 'emittery';
import cloneDeep from 'lodash/cloneDeep.js';

const copyStats = stats => v8.deserialize(v8.serialize(stats));

export default class RunStatus extends Emittery {
constructor(files, parallelRuns) {
Expand Down Expand Up @@ -146,7 +149,7 @@ export default class RunStatus extends Emittery {
}

if (changedStats) {
this.emit('stateChange', {type: 'stats', stats: cloneDeep(stats)});
this.emit('stateChange', {type: 'stats', stats: copyStats(stats)});
}

this.emit('stateChange', event);
Expand Down
18 changes: 9 additions & 9 deletions lib/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import nodePath from 'node:path';

import chokidar_ from 'chokidar';
import createDebug from 'debug';
import diff from 'lodash/difference.js';
import flatten from 'lodash/flatten.js';

import {chalk} from './chalk.js';
import {applyTestFileFilter, classify, getChokidarIgnorePatterns} from './globs.js';
Expand Down Expand Up @@ -114,7 +112,7 @@ export default class Watcher {
if (runOnlyExclusive) {
// The test files that previously contained exclusive tests are always
// run, together with the remaining specific files.
const remainingFiles = diff(specificFiles, exclusiveFiles);
const remainingFiles = specificFiles.filter(file => !exclusiveFiles.includes(file));
specificFiles = [...this.filesWithExclusiveTests, ...remainingFiles];
}

Expand Down Expand Up @@ -404,21 +402,23 @@ export default class Watcher {
}

const dirtyHelpersAndSources = [];
const dirtyTests = [];
const addedOrChangedTests = [];
const unlinkedTests = [];
for (const filePath of dirtyPaths) {
const {isIgnoredByWatcher, isTest} = classify(filePath, this.globs);
if (!isIgnoredByWatcher) {
if (isTest) {
dirtyTests.push(filePath);
if (dirtyStates[filePath] === 'unlink') {
unlinkedTests.push(filePath);
} else {
addedOrChangedTests.push(filePath);
}
} else {
dirtyHelpersAndSources.push(filePath);
}
}
}

const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink');
const unlinkedTests = diff(dirtyTests, addedOrChangedTests);

this.cleanUnlinkedTests(unlinkedTests);

// No need to rerun tests if the only change is that tests were deleted
Expand Down Expand Up @@ -448,6 +448,6 @@ export default class Watcher {
}

// Run all affected tests
this.run([...new Set([...addedOrChangedTests, ...flatten(testsByHelpersOrSource)])]);
this.run([...new Set([addedOrChangedTests, testsByHelpersOrSource].flat(2))]);
}
}
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
"is-error": "^2.2.2",
"is-plain-object": "^5.0.0",
"is-promise": "^4.0.0",
"lodash": "^4.17.21",
"matcher": "^4.0.0",
"mem": "^9.0.1",
"ms": "^2.1.3",
Expand Down
7 changes: 4 additions & 3 deletions test/helpers/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {fileURLToPath} from 'node:url';

import test from '@ava/test';
import execa from 'execa';
import defaultsDeep from 'lodash/defaultsDeep.js';
import replaceString from 'replace-string';

const cliPath = fileURLToPath(new URL('../../entrypoints/cli.mjs', import.meta.url));
Expand Down Expand Up @@ -45,15 +44,17 @@ const forwardErrorOutput = async from => {

export const fixture = async (args, options = {}) => {
const workingDir = options.cwd || cwd();
const running = execa.node(cliPath, args, defaultsDeep({
const running = execa.node(cliPath, args, {
...options,
env: {
...options.env,
AVA_EMIT_RUN_STATUS_OVER_IPC: 'I\'ll find a payphone baby / Take some time to talk to you',
TEST_AVA_IMPORT_FROM,
},
cwd: workingDir,
serialization: 'advanced',
nodeOptions: ['--require', ttySimulator],
}, options));
});

// Besides buffering stderr, if this environment variable is set, also pipe
// to stderr. This can be useful when debugging the tests.
Expand Down