remove: async test tracker and pid file utils
BREAKING CHANGE: this removes the test helpers, which were not well tested and required the `async_hooks` module, and the PID file helpers, which introduced a requirement on `fs` that could not be easily polyfilled. This should make the library easier to use in browsers and bundlers.
This commit is contained in:
parent
a7cf22de07
commit
e34641a42d
|
@ -1,96 +0,0 @@
|
|||
import { AsyncHook, createHook } from 'async_hooks';
|
||||
|
||||
import { isDebug } from './Env';
|
||||
import { isNil, Optional } from './Maybe';
|
||||
|
||||
export interface TrackedResource {
|
||||
source: string;
|
||||
triggerAsyncId: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type StackFilter = (stack: string) => string;
|
||||
|
||||
/**
|
||||
* Async resource tracker using node's internal hooks.
|
||||
*
|
||||
* This probably won't work in a browser. It does not hold references to the resource, to avoid leaks.
|
||||
* Adapted from https://gist.github.com/boneskull/7fe75b63d613fa940db7ec990a5f5843#file-async-dump-js
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class AsyncTracker {
|
||||
public filter: Optional<StackFilter>;
|
||||
private readonly hook: AsyncHook;
|
||||
private readonly resources: Map<number, TrackedResource>;
|
||||
|
||||
constructor() {
|
||||
this.resources = new Map();
|
||||
this.hook = createHook({
|
||||
destroy: (id: number) => {
|
||||
this.resources.delete(id);
|
||||
},
|
||||
init: (id: number, type: string, triggerAsyncId: number) => {
|
||||
const source = this.getStack();
|
||||
// @TODO: exclude async hooks, including this one
|
||||
this.resources.set(id, {
|
||||
source,
|
||||
triggerAsyncId,
|
||||
type,
|
||||
});
|
||||
},
|
||||
promiseResolve: (id: number) => {
|
||||
this.resources.delete(id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a filtered version of the current call stack. This creates a new error to generate the
|
||||
* stack trace and will be quite slow.
|
||||
*/
|
||||
public getStack(): string {
|
||||
const err = new Error();
|
||||
if (isNil(err.stack)) {
|
||||
return 'no stack trace available';
|
||||
}
|
||||
|
||||
if (isNil(this.filter)) {
|
||||
return err.stack;
|
||||
}
|
||||
|
||||
return this.filter(err.stack);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.resources.clear();
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.hook.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a listing of all tracked resources. When debug mode is enabled (DEBUG=TRUE), include
|
||||
* stack traces.
|
||||
*/
|
||||
/* eslint-disable no-console, no-invalid-this */
|
||||
public dump() {
|
||||
console.error(`tracking ${this.resources.size} async resources`);
|
||||
this.resources.forEach((res, id) => {
|
||||
console.error(`${id}: ${res.type}`);
|
||||
if (isDebug()) {
|
||||
console.error(res.source);
|
||||
console.error('\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.hook.enable();
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.resources.size;
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { open, unlink, write } from 'fs';
|
||||
import { pid } from 'process';
|
||||
|
||||
import { doesExist, Optional } from './Maybe';
|
||||
|
||||
type OptionalErrno = Optional<NodeJS.ErrnoException>;
|
||||
|
||||
/**
|
||||
* Write the current process ID to a file at the given `path`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function writePid(path: string): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
open(path, 'wx', (openErr: OptionalErrno, fd: number) => {
|
||||
if (doesExist(openErr)) {
|
||||
rej(openErr);
|
||||
} else {
|
||||
write(fd, pid.toString(), 0, 'utf8', (writeErr: OptionalErrno) => {
|
||||
if (doesExist(writeErr)) {
|
||||
rej(writeErr);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the file at the given `path`.
|
||||
*/
|
||||
export async function removePid(path: string): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
unlink(path, (err: OptionalErrno) => {
|
||||
if (doesExist(err)) {
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -5,9 +5,6 @@ export { NotFoundError } from './error/NotFoundError';
|
|||
export { NotImplementedError } from './error/NotImplementedError';
|
||||
export { TimeoutError } from './error/TimeoutError';
|
||||
|
||||
export {
|
||||
AsyncTracker,
|
||||
} from './AsyncTracker';
|
||||
export {
|
||||
ArrayMapper,
|
||||
ArrayMapperOptions,
|
||||
|
@ -71,10 +68,6 @@ export {
|
|||
mustExist,
|
||||
mustFind,
|
||||
} from './Maybe';
|
||||
export {
|
||||
removePid,
|
||||
writePid,
|
||||
} from './PidFile';
|
||||
export {
|
||||
constructorName,
|
||||
getConstructor,
|
||||
|
|
|
@ -2,10 +2,9 @@ import { expect } from 'chai';
|
|||
import { spy } from 'sinon';
|
||||
|
||||
import { main } from '../src/app';
|
||||
import { describeLeaks, itLeaks } from './helpers/async';
|
||||
|
||||
describeLeaks('app', async () => {
|
||||
itLeaks('should log a message', async () => {
|
||||
describe('app', async () => {
|
||||
it('should log a message', async () => {
|
||||
/* tslint:disable-next-line:no-console no-unbound-method */
|
||||
const logSpy = spy(console, 'log');
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { describeLeaks, itLeaks } from './helpers/async';
|
||||
|
||||
describeLeaks('test helpers', async () => {
|
||||
itLeaks('should wrap suites');
|
||||
itLeaks('should wrap tests');
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
import { AsyncTracker } from '../../src/AsyncTracker';
|
||||
import { isNil } from '../../src/Maybe';
|
||||
import { isDebug } from '../../src/Env';
|
||||
|
||||
// this will pull Mocha internals out of the stacks
|
||||
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
|
||||
const { stackTraceFilter } = require('mocha/lib/utils');
|
||||
|
||||
type AsyncMochaTest = (this: Mocha.Context | void) => Promise<void>;
|
||||
type AsyncMochaSuite = (this: Mocha.Suite) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Describe a suite of async tests. This wraps mocha's describe to track async resources and report leaks.
|
||||
*/
|
||||
export function describeLeaks(description: string, cb: AsyncMochaSuite): Mocha.Suite {
|
||||
return describe(description, function trackSuite(this: Mocha.Suite) {
|
||||
const tracker = new AsyncTracker();
|
||||
tracker.filter = stackTraceFilter;
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.enable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.disable();
|
||||
const leaked = tracker.size;
|
||||
|
||||
// @TODO: this should only exclude the single Immediate set by the Tracker
|
||||
if (leaked > 1) {
|
||||
tracker.dump();
|
||||
const msg = `test leaked ${leaked - 1} async resources`;
|
||||
if (isDebug()) {
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.warn(msg);
|
||||
}
|
||||
}
|
||||
|
||||
tracker.clear();
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line no-invalid-this */
|
||||
const suite: PromiseLike<void> | undefined = cb.call(this);
|
||||
if (isNil(suite) || !Reflect.has(suite, 'then')) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`test suite '${description}' did not return a promise`);
|
||||
}
|
||||
|
||||
return suite;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an asynchronous test with unhandled rejection guards.
|
||||
*
|
||||
* This function may not have any direct test coverage. It is too simple to reasonably mock.
|
||||
*/
|
||||
export function itLeaks(expectation: string, cb?: AsyncMochaTest): Mocha.Test {
|
||||
if (isNil(cb)) {
|
||||
return it(expectation);
|
||||
}
|
||||
|
||||
return it(expectation, function trackTest(this: Mocha.Context) {
|
||||
return new Promise<unknown>((res, rej) => {
|
||||
/* eslint-disable-next-line no-invalid-this */
|
||||
cb.call(this).then((value: unknown) => {
|
||||
res(value);
|
||||
}, (err: Error) => {
|
||||
rej(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { ArrayMapper } from '../../src/ArrayMapper';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
describeLeaks('utils', async () => {
|
||||
describeLeaks('array mapper', async () => {
|
||||
itLeaks('should take initial args', async () => {
|
||||
describe('utils', async () => {
|
||||
describe('array mapper', async () => {
|
||||
it('should take initial args', async () => {
|
||||
const mapper = new ArrayMapper({
|
||||
rest: 'others',
|
||||
skip: 0,
|
||||
|
@ -18,7 +17,7 @@ describeLeaks('utils', async () => {
|
|||
expect(results.get('others'), 'rest should be collected').to.deep.equal(['3', '4']);
|
||||
});
|
||||
|
||||
itLeaks('should always include rest arg', async () => {
|
||||
it('should always include rest arg', async () => {
|
||||
const mapper = new ArrayMapper({
|
||||
rest: 'empty',
|
||||
skip: 0,
|
||||
|
@ -29,7 +28,7 @@ describeLeaks('utils', async () => {
|
|||
expect(results.get('empty'), 'rest key should be empty').to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
itLeaks('should skit initial args', async () => {
|
||||
it('should skit initial args', async () => {
|
||||
const mapper = new ArrayMapper({
|
||||
rest: 'empty',
|
||||
skip: 3,
|
||||
|
|
|
@ -2,14 +2,13 @@ import { expect } from 'chai';
|
|||
|
||||
import { defer, timeout } from '../../src/Async';
|
||||
import { TimeoutError } from '../../src/error/TimeoutError';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
describeLeaks('async utils', async () => {
|
||||
describeLeaks('defer', async () => {
|
||||
itLeaks('should resolve', async () => expect(defer(10, true)).to.eventually.equal(true));
|
||||
describe('async utils', async () => {
|
||||
describe('defer', async () => {
|
||||
it('should resolve', async () => expect(defer(10, true)).to.eventually.equal(true));
|
||||
});
|
||||
|
||||
describeLeaks('timeout', async () => {
|
||||
itLeaks('should reject slow promises', async () => expect(timeout(10, defer(20))).to.eventually.be.rejectedWith(TimeoutError));
|
||||
describe('timeout', async () => {
|
||||
it('should reject slow promises', async () => expect(timeout(10, defer(20))).to.eventually.be.rejectedWith(TimeoutError));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { concat, encode } from '../../src/Buffer';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
describeLeaks('buffer utils', async () => {
|
||||
describeLeaks('concat', async () => {
|
||||
itLeaks('should append chunk buffers', async () => {
|
||||
describe('buffer utils', async () => {
|
||||
describe('concat', async () => {
|
||||
it('should append chunk buffers', async () => {
|
||||
expect(concat([
|
||||
Buffer.from('hello'),
|
||||
Buffer.from('world'),
|
||||
|
@ -13,18 +12,18 @@ describeLeaks('buffer utils', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
describeLeaks('encode', async () => {
|
||||
itLeaks('should encode chunk buffers', async () => {
|
||||
describe('encode', async () => {
|
||||
it('should encode chunk buffers', async () => {
|
||||
expect(encode([
|
||||
Buffer.from('hello world'),
|
||||
], 'utf-8')).to.equal('hello world');
|
||||
});
|
||||
|
||||
itLeaks('should encode no buffers', async () => {
|
||||
it('should encode no buffers', async () => {
|
||||
expect(encode([], 'utf-8')).to.equal('');
|
||||
});
|
||||
|
||||
itLeaks('should encode empty buffers', async () => {
|
||||
it('should encode empty buffers', async () => {
|
||||
expect(encode([
|
||||
new Buffer(0),
|
||||
], 'utf-8')).to.equal('');
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { Checklist, ChecklistMode } from '../../src/Checklist';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
const EXISTING_ITEM = 'foo';
|
||||
const MISSING_ITEM = 'bin';
|
||||
const TEST_DATA = [EXISTING_ITEM, 'bar'];
|
||||
|
||||
// tslint:disable:no-duplicate-functions
|
||||
describeLeaks('checklist', async () => {
|
||||
describeLeaks('exclude mode', async () => {
|
||||
itLeaks('should check for present values', async () => {
|
||||
describe('checklist', async () => {
|
||||
describe('exclude mode', async () => {
|
||||
it('should check for present values', async () => {
|
||||
const list = new Checklist({
|
||||
data: TEST_DATA,
|
||||
mode: ChecklistMode.EXCLUDE,
|
||||
|
@ -18,7 +17,7 @@ describeLeaks('checklist', async () => {
|
|||
expect(list.check(EXISTING_ITEM)).to.equal(false);
|
||||
});
|
||||
|
||||
itLeaks('should check for missing values', async () => {
|
||||
it('should check for missing values', async () => {
|
||||
const list = new Checklist({
|
||||
data: TEST_DATA,
|
||||
mode: ChecklistMode.EXCLUDE,
|
||||
|
@ -27,8 +26,8 @@ describeLeaks('checklist', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
describeLeaks('include mode', async () => {
|
||||
itLeaks('should check for present values', async () => {
|
||||
describe('include mode', async () => {
|
||||
it('should check for present values', async () => {
|
||||
const list = new Checklist<string>({
|
||||
data: TEST_DATA,
|
||||
mode: ChecklistMode.INCLUDE,
|
||||
|
@ -36,7 +35,7 @@ describeLeaks('checklist', async () => {
|
|||
expect(list.check(EXISTING_ITEM)).to.equal(true);
|
||||
});
|
||||
|
||||
itLeaks('should check for missing values', async () => {
|
||||
it('should check for missing values', async () => {
|
||||
const list = new Checklist<string>({
|
||||
data: TEST_DATA,
|
||||
mode: ChecklistMode.INCLUDE,
|
||||
|
|
|
@ -3,7 +3,6 @@ import { expect } from 'chai';
|
|||
import { ChildProcessError } from '../../src';
|
||||
import { ChildStreams, waitForChild } from '../../src/Child';
|
||||
import { mustExist, Optional } from '../../src/Maybe';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
type Closer = (status: number) => Promise<void>;
|
||||
|
||||
|
@ -29,9 +28,9 @@ function createChild(): ChildStreams & { closer: Optional<Closer> } {
|
|||
} as any;
|
||||
}
|
||||
|
||||
describeLeaks('child process utils', async () => {
|
||||
describeLeaks('wait for child helper', async () => {
|
||||
itLeaks('should read stdout data', async () => {
|
||||
describe('child process utils', async () => {
|
||||
describe('wait for child helper', async () => {
|
||||
it('should read stdout data', async () => {
|
||||
const child = createChild();
|
||||
|
||||
const resultPromise = waitForChild(child);
|
||||
|
@ -41,10 +40,10 @@ describeLeaks('child process utils', async () => {
|
|||
expect(result.status).to.equal(0);
|
||||
});
|
||||
|
||||
itLeaks('should read stderr data');
|
||||
itLeaks('should resolve on success status');
|
||||
it('should read stderr data');
|
||||
it('should resolve on success status');
|
||||
|
||||
itLeaks('should reject on failure status', async () => {
|
||||
it('should reject on failure status', async () => {
|
||||
const child = createChild();
|
||||
|
||||
const resultPromise = waitForChild(child);
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
pushMergeMap,
|
||||
normalizeMap,
|
||||
} from '../../src/Map';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
const DEFAULT_VALUE = 'default';
|
||||
const mapKey = 'key';
|
||||
|
@ -29,22 +28,22 @@ const multiItem = new Map([
|
|||
/* eslint-enable */
|
||||
]);
|
||||
|
||||
describeLeaks('map utils', async () => {
|
||||
describeLeaks('make dict', async () => {
|
||||
itLeaks('should return an empty dict for nil values', async () => {
|
||||
describe('map utils', async () => {
|
||||
describe('make dict', async () => {
|
||||
it('should return an empty dict for nil values', async () => {
|
||||
/* eslint-disable-next-line no-null/no-null */
|
||||
expect(makeDict(null)).to.deep.equal({});
|
||||
expect(makeDict(undefined)).to.deep.equal({});
|
||||
});
|
||||
|
||||
itLeaks('should return an existing dict', async () => {
|
||||
it('should return an existing dict', async () => {
|
||||
const input = {};
|
||||
expect(makeDict(input)).to.equal(input);
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('make map', async () => {
|
||||
itLeaks('should convert objects to maps', async () => {
|
||||
describe('make map', async () => {
|
||||
it('should convert objects to maps', async () => {
|
||||
const data = {
|
||||
bar: '2',
|
||||
foo: '1',
|
||||
|
@ -55,44 +54,44 @@ describeLeaks('map utils', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
describeLeaks('must get helper', async () => {
|
||||
itLeaks('should get existing keys', async () => {
|
||||
describe('must get helper', async () => {
|
||||
it('should get existing keys', async () => {
|
||||
expect(mustGet(singleItem, mapKey)).to.equal(mapValue);
|
||||
});
|
||||
|
||||
itLeaks('should throw on missing keys', async () => {
|
||||
it('should throw on missing keys', async () => {
|
||||
expect(() => {
|
||||
mustGet(singleItem, 'nope');
|
||||
}).to.throw(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('get head helper', async () => {
|
||||
itLeaks('should get the first item from existing keys', async () => {
|
||||
describe('get head helper', async () => {
|
||||
it('should get the first item from existing keys', async () => {
|
||||
expect(getHead(multiItem, mapKey)).to.equal(mapValue);
|
||||
});
|
||||
|
||||
itLeaks('should throw on missing keys', async () => {
|
||||
it('should throw on missing keys', async () => {
|
||||
expect(() => {
|
||||
getHead(multiItem, 'nope');
|
||||
}).to.throw(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('get head or default helper', async () => {
|
||||
itLeaks('should get the first item from existing keys', async () => {
|
||||
describe('get head or default helper', async () => {
|
||||
it('should get the first item from existing keys', async () => {
|
||||
expect(getHeadOrDefault(multiItem, mapKey, 'nope')).to.equal(mapValue);
|
||||
});
|
||||
|
||||
itLeaks('should get the default for missing keys', async () => {
|
||||
it('should get the default for missing keys', async () => {
|
||||
expect(getHeadOrDefault(multiItem, 'nope', mapValue)).to.equal(mapValue);
|
||||
});
|
||||
|
||||
itLeaks('should return the default value for nil values', async () => {
|
||||
it('should return the default value for nil values', async () => {
|
||||
expect(getHeadOrDefault(multiItem, 'nilValue', mapValue)).to.equal(mapValue);
|
||||
});
|
||||
|
||||
itLeaks('should return the default value for nil keys', async () => {
|
||||
it('should return the default value for nil keys', async () => {
|
||||
expect(getHeadOrDefault(multiItem, 'nilKey', mapValue)).to.equal(mapValue);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import { expect } from 'chai';
|
||||
import mockFS from 'mock-fs';
|
||||
|
||||
import { removePid, writePid } from '../../src';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
const PID_PATH = 'foo';
|
||||
const PID_NAME = 'foo/test.pid';
|
||||
|
||||
describeLeaks('pid file utils', async () => {
|
||||
beforeEach(() => {
|
||||
mockFS({
|
||||
[PID_PATH]: mockFS.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
itLeaks('should create a marker', async () => {
|
||||
await writePid(PID_NAME);
|
||||
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
itLeaks('should not replace an existing marker', async () => {
|
||||
await writePid(PID_NAME);
|
||||
return expect(writePid(PID_PATH)).to.eventually.be.rejectedWith(Error);
|
||||
});
|
||||
|
||||
itLeaks('should remove an existing marker', async () => {
|
||||
await writePid(PID_NAME);
|
||||
await removePid(PID_NAME);
|
||||
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
itLeaks('should fail to remove a missing marker', async () =>
|
||||
expect(removePid(PID_PATH)).to.eventually.be.rejectedWith(Error)
|
||||
);
|
||||
});
|
|
@ -2,12 +2,11 @@ import { expect } from 'chai';
|
|||
|
||||
import { timeout } from '../../src/Async';
|
||||
import { signal, SIGNAL_RESET } from '../../src/Signal';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
const MAX_SIGNAL_TIME = 500;
|
||||
|
||||
describeLeaks('signal utils', async () => {
|
||||
itLeaks('should wait for a signal', async () => {
|
||||
describe('signal utils', async () => {
|
||||
it('should wait for a signal', async () => {
|
||||
const signalPromise = signal(SIGNAL_RESET);
|
||||
|
||||
process.kill(process.pid, SIGNAL_RESET);
|
||||
|
|
|
@ -1,39 +1,38 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { leftPad, trim } from '../../src/String';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
const TEST_SHORT = 'hello';
|
||||
const TEST_LONG = 'hello world';
|
||||
|
||||
describeLeaks('left pad helper', async () => {
|
||||
itLeaks('should prepend padding', async () => {
|
||||
describe('left pad helper', async () => {
|
||||
it('should prepend padding', async () => {
|
||||
expect(leftPad('test')).to.equal('0000test');
|
||||
});
|
||||
|
||||
itLeaks('should return long strings as-is', async () => {
|
||||
it('should return long strings as-is', async () => {
|
||||
const long = 'testing-words';
|
||||
expect(leftPad(long, 8)).to.equal(long);
|
||||
});
|
||||
|
||||
itLeaks('should use padding string', async () => {
|
||||
it('should use padding string', async () => {
|
||||
expect(leftPad('test', 8, 'too')).to.equal('toottest', 'must repeat and truncate the padding string');
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('trim helper', async () => {
|
||||
itLeaks('should return strings shorter than max', async () => {
|
||||
describe('trim helper', async () => {
|
||||
it('should return strings shorter than max', async () => {
|
||||
expect(trim('yes', 5)).to.equal('yes', 'shorter than max');
|
||||
expect(trim(TEST_SHORT, 5)).to.equal(TEST_SHORT, 'equal to max');
|
||||
});
|
||||
|
||||
itLeaks('should trim strings longer than max', async () => {
|
||||
it('should trim strings longer than max', async () => {
|
||||
expect(trim(TEST_LONG, 3, '...')).to.equal('...');
|
||||
expect(trim(TEST_LONG, 5)).to.equal('he...');
|
||||
expect(trim(TEST_LONG, 8)).to.equal('hello...');
|
||||
});
|
||||
|
||||
itLeaks('should not add tail when max is small', async () => {
|
||||
it('should not add tail when max is small', async () => {
|
||||
expect(trim(TEST_SHORT, 2, '...')).to.equal('he');
|
||||
expect(trim(TEST_LONG, 5, 'very long tail')).to.equal(TEST_SHORT);
|
||||
expect(trim(TEST_SHORT, 8, 'very long tail')).to.equal(TEST_SHORT);
|
||||
|
|
|
@ -1,62 +1,61 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { ensureArray, hasItems } from '../../src';
|
||||
import { NotFoundError } from '../../src/error/NotFoundError';
|
||||
import { countOf, defaultWhen, filterNil, mustCoalesce, mustFind } from '../../src/Maybe';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
import { hasItems, ensureArray } from '../../src';
|
||||
|
||||
describeLeaks('utils', async () => {
|
||||
describeLeaks('count list', async () => {
|
||||
itLeaks('should count a single item', async () => {
|
||||
describe('utils', async () => {
|
||||
describe('count list', async () => {
|
||||
it('should count a single item', async () => {
|
||||
expect(countOf(1)).to.equal(1, 'numbers');
|
||||
expect(countOf('')).to.equal(1, 'empty strings');
|
||||
expect(countOf('123')).to.equal(1, 'other strings');
|
||||
});
|
||||
|
||||
itLeaks('should count an array of items', async () => {
|
||||
it('should count an array of items', async () => {
|
||||
expect(countOf([1])).to.equal(1, 'single item list');
|
||||
expect(countOf([1, 2, 3])).to.equal(3, 'multi item list');
|
||||
});
|
||||
|
||||
itLeaks('should count an unknown argument as 0', async () => {
|
||||
it('should count an unknown argument as 0', async () => {
|
||||
expect(countOf(undefined)).to.equal(0, 'undefined');
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
expect(countOf(null)).to.equal(0, 'null');
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('filter nil', async () => {
|
||||
itLeaks('should remove nil items', async () => {
|
||||
describe('filter nil', async () => {
|
||||
it('should remove nil items', async () => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
expect(filterNil([1, undefined, 2, null, 3])).to.deep.equal([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('must find helper', async () => {
|
||||
itLeaks('should return matching item', async () => {
|
||||
describe('must find helper', async () => {
|
||||
it('should return matching item', async () => {
|
||||
expect(mustFind([1, 2, 3], (val) => (val % 2) === 0)).to.equal(2);
|
||||
});
|
||||
|
||||
itLeaks('should throw if no item matches', async () => {
|
||||
it('should throw if no item matches', async () => {
|
||||
expect(() => {
|
||||
mustFind([1, 2, 3], (val) => val === 4);
|
||||
}).to.throw(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('default when', async () => {
|
||||
itLeaks('should return the first item when the condition is true', async () => {
|
||||
describe('default when', async () => {
|
||||
it('should return the first item when the condition is true', async () => {
|
||||
expect(defaultWhen(true, 1, 2)).to.equal(1);
|
||||
});
|
||||
|
||||
itLeaks('should return the second item otherwise', async () => {
|
||||
it('should return the second item otherwise', async () => {
|
||||
expect(defaultWhen(false, 1, 2)).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describeLeaks('must coalesce helper', async () => {
|
||||
describe('must coalesce helper', async () => {
|
||||
/* eslint-disable no-null/no-null */
|
||||
itLeaks('should return the first existent value', async () => {
|
||||
it('should return the first existent value', async () => {
|
||||
expect(mustCoalesce(null, null, 3, null)).to.equal(3);
|
||||
expect(mustCoalesce(null, null, undefined, 'string')).to.equal('string');
|
||||
expect(mustCoalesce(null, undefined, [], null)).to.deep.equal([]);
|
||||
|
@ -64,19 +63,19 @@ describeLeaks('utils', async () => {
|
|||
/* eslint-enable no-null/no-null */
|
||||
});
|
||||
|
||||
describeLeaks('has items helper', async () => {
|
||||
itLeaks('should return false for non-array values', async () => {
|
||||
describe('has items helper', async () => {
|
||||
it('should return false for non-array values', async () => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
expect(hasItems({} as any)).to.equal(false);
|
||||
expect(hasItems(4 as any)).to.equal(false);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
});
|
||||
|
||||
itLeaks('should return false for empty arrays', async () => {
|
||||
it('should return false for empty arrays', async () => {
|
||||
expect(hasItems([])).to.equal(false);
|
||||
});
|
||||
|
||||
itLeaks('should return true for arrays with elements', async () => {
|
||||
it('should return true for arrays with elements', async () => {
|
||||
expect(hasItems([
|
||||
true,
|
||||
false,
|
||||
|
@ -84,14 +83,14 @@ describeLeaks('utils', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
describeLeaks('ensure array helper', async () => {
|
||||
itLeaks('should convert nil values to arrays', async () => {
|
||||
describe('ensure array helper', async () => {
|
||||
it('should convert nil values to arrays', async () => {
|
||||
/* eslint-disable-next-line no-null/no-null */
|
||||
expect(ensureArray(null)).to.deep.equal([]);
|
||||
expect(ensureArray(undefined)).to.deep.equal([]);
|
||||
});
|
||||
|
||||
itLeaks('should copy arrays', async () => {
|
||||
it('should copy arrays', async () => {
|
||||
const data: Array<number> = [];
|
||||
const copy = ensureArray(data);
|
||||
|
||||
|
|
Loading…
Reference in New Issue