Skip to content

Commit

Permalink
fix(test): cover pid file, signal helpers, improve coverage for others
Browse files Browse the repository at this point in the history
  • Loading branch information
ssube committed Mar 31, 2020
1 parent f751652 commit 76e2ba4
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 27 deletions.
11 changes: 11 additions & 0 deletions docs/api/js-utils.asynctracker.filter.md
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@apextoaster/js-utils](./js-utils.md) &gt; [AsyncTracker](./js-utils.asynctracker.md) &gt; [filter](./js-utils.asynctracker.filter.md)

## AsyncTracker.filter property

<b>Signature:</b>

```typescript
filter: Optional<StackFilter>;
```
2 changes: 1 addition & 1 deletion docs/api/js-utils.asynctracker.getstack.md
Expand Up @@ -7,7 +7,7 @@
<b>Signature:</b>

```typescript
static getStack(): string;
getStack(): string;
```
<b>Returns:</b>

Expand Down
3 changes: 2 additions & 1 deletion docs/api/js-utils.asynctracker.md
Expand Up @@ -24,6 +24,7 @@ export declare class AsyncTracker

| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [filter](./js-utils.asynctracker.filter.md) | | <code>Optional&lt;StackFilter&gt;</code> | |
| [size](./js-utils.asynctracker.size.md) | | <code>number</code> | |

## Methods
Expand All @@ -34,5 +35,5 @@ export declare class AsyncTracker
| [disable()](./js-utils.asynctracker.disable.md) | | |
| [dump()](./js-utils.asynctracker.dump.md) | | |
| [enable()](./js-utils.asynctracker.enable.md) | | |
| [getStack()](./js-utils.asynctracker.getstack.md) | <code>static</code> | |
| [getStack()](./js-utils.asynctracker.getstack.md) | | |

2 changes: 2 additions & 0 deletions docs/api/js-utils.isnil.md
Expand Up @@ -4,6 +4,8 @@

## isNil() function

Check if a value is nil.

<b>Signature:</b>

```typescript
Expand Down
4 changes: 2 additions & 2 deletions docs/api/js-utils.md
Expand Up @@ -44,13 +44,13 @@
| [getOrDefault(map, key, defaultValue)](./js-utils.getordefault.md) | |
| [getTestLogger(verbose)](./js-utils.gettestlogger.md) | |
| [isDebug()](./js-utils.isdebug.md) | |
| [isNil(val)](./js-utils.isnil.md) | |
| [isNil(val)](./js-utils.isnil.md) | Check if a value is nil. |
| [leftPad(val, min, fill)](./js-utils.leftpad.md) | |
| [makeDict(map)](./js-utils.makedict.md) | Turns a map or dict into a dict |
| [makeMap(val)](./js-utils.makemap.md) | Clone a map or map-like object into a new map. |
| [mergeList(parts)](./js-utils.mergelist.md) | Merge arguments, which may or may not be arrays, into one return that is definitely an array. |
| [mergeMap(target, source)](./js-utils.mergemap.md) | |
| [mustCoalesce(values)](./js-utils.mustcoalesce.md) | Return the first value that is not nil. |
| [mustCoalesce(values)](./js-utils.mustcoalesce.md) | Return the first value that is not nil.<!-- -->TODO: rename to mustDefault |
| [mustExist(val)](./js-utils.mustexist.md) | Assert that a variable is not nil and return the value. |
| [mustFind(list, predicate)](./js-utils.mustfind.md) | Find a value matching the given predicate or throw. |
| [mustGet(map, key)](./js-utils.mustget.md) | Get an element from a Map and guard against nil values. |
Expand Down
2 changes: 2 additions & 0 deletions docs/api/js-utils.mustcoalesce.md
Expand Up @@ -6,6 +6,8 @@

Return the first value that is not nil.

TODO: rename to mustDefault

<b>Signature:</b>

```typescript
Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -28,6 +28,7 @@
"@types/chai-as-promised": "7.1.2",
"@types/lodash": "4.14.149",
"@types/mocha": "7.0.2",
"@types/mock-fs": "^4.10.0",
"@types/node": "13.9.5",
"@types/sinon-chai": "3.2.3",
"@types/source-map-support": "0.5.1",
Expand All @@ -45,6 +46,7 @@
"eslint-plugin-sonarjs": "0.5.0",
"lodash": "4.17.15",
"mocha": "7.1.1",
"mock-fs": "^4.11.0",
"noicejs": "3.0.1",
"nyc": "15.0.0",
"rollup": "2.3.1",
Expand Down
29 changes: 18 additions & 11 deletions src/AsyncTracker.ts
@@ -1,6 +1,6 @@
import { AsyncHook, createHook } from 'async_hooks';

import { isNil } from './utils';
import { isNil, Optional } from './utils';
import { isDebug } from './utils/Env';

export interface TrackedResource {
Expand All @@ -9,22 +9,16 @@ export interface TrackedResource {
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
*/
export class AsyncTracker {
public static getStack(): string {
const err = new Error();
if (isNil(err.stack)) {
return 'no stack trace available';
} else {
return err.stack; // TODO: filterStack(err.stack);
}
}

public filter: Optional<StackFilter>;
private readonly hook: AsyncHook;
private readonly resources: Map<number, TrackedResource>;

Expand All @@ -35,7 +29,7 @@ export class AsyncTracker {
this.resources.delete(id);
},
init: (id: number, type: string, triggerAsyncId: number) => {
const source = AsyncTracker.getStack();
const source = this.getStack();
// @TODO: exclude async hooks, including this one
this.resources.set(id, {
source,
Expand All @@ -49,6 +43,19 @@ export class AsyncTracker {
});
}

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();
}
Expand Down
3 changes: 2 additions & 1 deletion src/utils/Async.ts
@@ -1,4 +1,5 @@
import { TimeoutError } from '../error/TimeoutError';
import { PredicateC0 } from '.';

/**
* Resolve after a set amount of time.
Expand All @@ -24,7 +25,7 @@ export function timeout<T>(ms: number, oper: Promise<T>): Promise<T> {
return Promise.race([limit, oper]);
}

export async function waitFor(cb: () => boolean, step: number, count: number): Promise<void> {
export async function waitFor(cb: PredicateC0, step: number, count: number): Promise<void> {
let accum = 0;
while (accum < count) {
await defer(step);
Expand Down
33 changes: 29 additions & 4 deletions src/utils/index.ts
Expand Up @@ -6,26 +6,49 @@ import { NotFoundError } from '../error/NotFoundError';
/* eslint-disable-next-line @typescript-eslint/ban-types */
export type Nil = null | undefined;

export type SortAfter = 1;
export type SortBefore = -1;
export type SortEqual = 0;
export type SortOrder = SortAfter | SortBefore | SortEqual;

/**
* Value that may be nil.
*/
export type Optional<T> = T | Nil;

/**
* Comparison (filter) predicate for a single value.
* Comparison predicate for arity 0 - assert?
*/
export type PredicateC0 = () => boolean;

/**
* Comparison predicate for arity 1 - filter.
*/
export type PredicateC1<TVal> = (val: TVal, idx: number, list: Array<TVal>) => boolean;

/**
* Comparison (sort) predicate for two values.
* Comparison predicate for arity 2 - sort.
*/
export type PredicateC2<TVal> = (pval: TVal, nval: TVal, idx: number, list: Array<TVal>) => SortOrder;

/**
* Transform predicate for arity 0 - constructor.
*/
export type PredicateC2<TVal> = (pval: TVal, nval: TVal, idx: number, list: Array<TVal>) => number;
export type PredicateR0<TVal> = () => TVal;

/**
* Reduction predicate for two values.
* Transform predicate for arity 1 - map.
*/
export type PredicateR1<TVal> = (val: TVal, idx: number, list: Array<TVal>) => TVal;

/**
* Transform predicate for arity 2 - reduce.
*/
export type PredicateR2<TVal> = (pval: TVal, nval: TVal, idx: number, list: Array<TVal>) => TVal;

/**
* Check if a value is nil.
*/
/* eslint-disable-next-line @typescript-eslint/ban-types */
export function isNil<T>(val: Optional<T>): val is Nil {
/* eslint-disable-next-line no-null/no-null */
Expand Down Expand Up @@ -113,6 +136,8 @@ export function mustExist<T>(val: Optional<T>): T {

/**
* Return the first value that is not nil.
*
* TODO: rename to mustDefault
*/
export function mustCoalesce<T>(...values: Array<Optional<T>>): T {
return mustFind(values, doesExist);
Expand Down
25 changes: 25 additions & 0 deletions test/utils/TestMap.ts
Expand Up @@ -13,6 +13,7 @@ import {
setOrPush,
mergeMap,
pushMergeMap,
normalizeMap,
} from '../../src/utils/Map';
import { describeLeaks, itLeaks } from '../helpers/async';

Expand Down Expand Up @@ -206,4 +207,28 @@ describeLeaks('map utils', async () => {
})).to.deep.equal(singleItem);
});
});

describe('normalize map helper', () => {
it('should convert values into arrays of strings', () => {
const banVal = [Symbol()];
const initial = new Map<string, unknown>([
['bar', 'bin'],
['ban', banVal],
['toad', {
toString() {
return 'too';
},
}],
]);

const normalized = normalizeMap(initial);

expect(normalized.bar).to.deep.equal(['bin']);
expect(normalized.ban).to.equal(banVal);
expect(normalized.toad).to.deep.equal(['too']);
});

/* ['foo', 1] */
xit('should convert numbers into string values');
});
});
42 changes: 42 additions & 0 deletions test/utils/TestPidFile.ts
@@ -0,0 +1,42 @@
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)
);
});
27 changes: 21 additions & 6 deletions test/utils/TestReflect.ts
@@ -1,18 +1,33 @@
import { expect } from 'chai';
import { getMethods } from '../../src/utils/Reflect';
import { getMethods, getConstructor, constructorName } from '../../src/utils/Reflect';

class Test {
public foo() { /* noop */ }
public bar() { /* noop */ }
}

describe('reflect utils', () => {
describe('get methods helper', () => {
it('should collect method functions', () => {
class Test {
public foo() { /* noop */ }
public bar() { /* noop */ }
}

const methods = getMethods(new Test()).values();

/* eslint-disable @typescript-eslint/unbound-method */
expect(methods).to.include(Test.prototype.foo);
expect(methods).to.include(Test.prototype.bar);
});
});

describe('get constructor helper', () => {
it('should get the constructor from an instance', () => {
const instance = new Test();
expect(getConstructor(instance)).to.equal(Test);
});
});

describe('get constructor name helper', () => {
it('should get the constructor name from an instance', () => {
const instance = new Test();
expect(constructorName(instance)).to.equal(Test.name);
});
});
});
20 changes: 20 additions & 0 deletions test/utils/TestSignal.ts
@@ -0,0 +1,20 @@
import { expect } from 'chai';

import { timeout } from '../../src/utils/Async';
import { signal, SIGNAL_RESET } from '../../src/utils/Signal';
import { describeLeaks, itLeaks } from '../helpers/async';

const MAX_SIGNAL_TIME = 500;

describeLeaks('signal utils', async () => {
itLeaks('should wait for a signal', async () => {
const signalPromise = signal(SIGNAL_RESET);

process.kill(process.pid, SIGNAL_RESET);
await timeout(MAX_SIGNAL_TIME, signalPromise);

const signalValue = await signalPromise;

expect(signalValue).to.equal(SIGNAL_RESET);
});
});

0 comments on commit 76e2ba4

Please sign in to comment.