Skip to content

Commit

Permalink
Replace Lodash with built-in syntax, libraries, and some code
Browse files Browse the repository at this point in the history
Co-authored-by: Mark Wubben <[email protected]>
  • Loading branch information
live627 and novemberborn authored Oct 4, 2021
1 parent 55c430d commit d36806a
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 49 deletions.
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

0 comments on commit d36806a

Please sign in to comment.