Skip to content

Commit

Permalink
feat(config): add onComplete hook
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Nov 26, 2020
1 parent 19373e9 commit d053d2f
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 2 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ module.exports = {

/** Optional boolean flag used to set CI mode. process.env.CI is used when not set. */
CI: false,

/**
* Optional callback invoked once scan is complete.
*
* @param {{
* repository: string,
* repositoryOwner: string,
* rule: string,
* message: string,
* path: string,
* link: string,
* extension: string,
* source: string,
* error: (string|undefined),
* }[]} results Results of the scan, if any
* @returns {Promise<void>|void}
*/
onComplete: async function onComplete(results) {
// Extend the process with custom features, e.g. send results to email, create issues to Github...
},
}
```

Expand Down
18 changes: 18 additions & 0 deletions eslint-remote-tester.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,22 @@ module.exports = {
},
extends: ['eslint:recommended'],
},

/**
* Optional callback invoked once scan is complete.
*
* @param {{
* repository: string,
* repositoryOwner: string,
* rule: string,
* message: string,
* path: string,
* link: string,
* extension: string,
* source: string,
* error: (string|undefined),
* }[]} results Results of the scan, if any
* @returns {Promise<void>|void}
*/
onComplete: undefined,
};
20 changes: 20 additions & 0 deletions lib/config/config-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,24 @@ export const CONFIGURATION_FILE_TEMPLATE =
/** Optional boolean flag used to set CI mode. process.env.CI is used when not set. */
CI: false,
/**
* Optional callback invoked once scan is complete.
*
* @param {{
* repository: string,
* repositoryOwner: string,
* rule: string,
* message: string,
* path: string,
* link: string,
* extension: string,
* source: string,
* error: (string|undefined),
* }[]} results Results of the scan, if any
* @returns {Promise<void>|void}
*/
onComplete: async function onComplete(results) {
},
}`;
3 changes: 3 additions & 0 deletions lib/config/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Linter } from 'eslint';

import { ResultTemplateOptions } from '@file-client/result-templates';

export type ResultParser = 'plaintext' | 'markdown';

/** Contents of the `eslint-remote-tester.config.js` */
Expand All @@ -12,4 +14,5 @@ export interface Config {
concurrentTasks: number;
eslintrc: Linter.Config;
CI: boolean;
onComplete?: (results: ResultTemplateOptions[]) => Promise<void> | void;
}
5 changes: 5 additions & 0 deletions lib/config/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function constructAndValidateConfiguration(
concurrentTasks,
eslintrc,
CI,
onComplete,
} = configToValidate;

const config = { ...configToValidate };
Expand Down Expand Up @@ -79,6 +80,10 @@ export default function constructAndValidateConfiguration(
config.concurrentTasks = DEFAULT_CONCURRENT_TASKS;
}

if (onComplete && typeof onComplete !== 'function') {
errors.push(`onComplete (${onComplete}) should be a function`);
}

if (errors.length) {
console.log(
chalk.red(
Expand Down
1 change: 1 addition & 0 deletions lib/file-client/result-templates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ResultParser } from '@config/types';
import { LintMessage } from '@engine/types';

// Note that this is part of public API
export interface ResultTemplateOptions {
repository: string;
repositoryOwner: string;
Expand Down
31 changes: 30 additions & 1 deletion lib/progress-logger/progress-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import chalk from 'chalk';
import * as Templates from './log-templates';
import { LogMessage, Task, Listeners, Listener, ListenerType } from './types';
import config from '@config';
import { ResultsStore } from '@file-client';

const CI_KEEP_ALIVE_INTERVAL_MS = 4.5 * 60 * 1000;
const DEFAULT_COLOR = (text: string) => text;
Expand Down Expand Up @@ -101,7 +102,35 @@ class ProgressLogger {
clearInterval(this.ciKeepAliveIntervalHandle);
}

this.listeners.exit.forEach(listener => listener());
const onError = (error: Error) =>
console.error(
[
`Error occured while calling onComplete callback`,
error.stack,
].join('\n')
);
const notifyListeners = () =>
this.listeners.exit.forEach(listener => listener());

let exitPromise = Promise.resolve();

if (config.onComplete) {
const results = ResultsStore.getResults();
try {
const onCompletePromise = config.onComplete(results);

if (onCompletePromise instanceof Promise) {
exitPromise = onCompletePromise;
}
} catch (e) {
onError(e);
}
}

exitPromise.then(notifyListeners).catch(error => {
onError(error);
notifyListeners();
});
}

/**
Expand Down
3 changes: 3 additions & 0 deletions test/integration/eslint-remote-tester.integration.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ module.exports = {
'local-rules/some-unstable-rule': 'error',
},
},
onComplete: function onComplete(results) {
global.onComplete(results);
},
};
31 changes: 31 additions & 0 deletions test/integration/integration.ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
INTEGRATION_REPO_NAME,
getStdoutWriteCalls,
sanitizeStackTrace,
getOnCompleteCalls,
} from '../utils';
import { CACHE_LOCATION } from '@file-client';

Expand Down Expand Up @@ -159,4 +160,34 @@ describe('CI mode', () => {
test('exits process with error code', async () => {
expect(process.exit).toHaveBeenCalledWith(1);
});

test('calls onComplete hook with the results', async () => {
const [onCompleteCalls] = getOnCompleteCalls();
const [result] = onCompleteCalls;

expect(result.extension).toBe('js');
expect(result.link).toBe(
`https://github.com/${INTEGRATION_REPO_OWNER}/${INTEGRATION_REPO_NAME}/blob/HEAD/expected-to-crash-linter.js#L2`
);
expect(result.rule).toBe('unable-to-parse-rule-id');
expect(result.message).toBe(
"Cannot read property 'someAttribute' of undefined\nOccurred while linting <text>:2"
);
expect(result.path).toBe(
`${INTEGRATION_REPO_OWNER}/${INTEGRATION_REPO_NAME}/expected-to-crash-linter.js`
);
expect(result.error).toBeTruthy();

// Each result should have attributes defined
onCompleteCalls.forEach(result => {
expect(result.repository).toBeTruthy();
expect(result.repositoryOwner).toBeTruthy();
expect(result.rule).toBeTruthy();
expect(result.message).toBeTruthy();
expect(result.path).toBeTruthy();
expect(result.link).toBeTruthy();
expect(result.extension).toBeTruthy();
expect(result.source).toBeTruthy();
});
});
});
31 changes: 31 additions & 0 deletions test/integration/integration.cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
INTEGRATION_REPO_OWNER,
getStdoutWriteCalls,
sanitizeStackTrace,
getOnCompleteCalls,
} from '../utils';
import { RESULTS_LOCATION, CACHE_LOCATION } from '@file-client';

Expand Down Expand Up @@ -158,4 +159,34 @@ describe('CLI', () => {
expect(secondRunWrites.some(call => /PULLING/.test(call))).toBe(true);
expect(fs.existsSync(cachedRepository)).toBe(true);
});

test('calls onComplete hook with the results', async () => {
const [onCompleteCalls] = getOnCompleteCalls();
const [result] = onCompleteCalls;

expect(result.extension).toBe('js');
expect(result.link).toBe(
`https://github.com/${INTEGRATION_REPO_OWNER}/${INTEGRATION_REPO_NAME}/blob/HEAD/expected-to-crash-linter.js#L2`
);
expect(result.rule).toBe('unable-to-parse-rule-id');
expect(result.message).toBe(
"Cannot read property 'someAttribute' of undefined\nOccurred while linting <text>:2"
);
expect(result.path).toBe(
`${INTEGRATION_REPO_OWNER}/${INTEGRATION_REPO_NAME}/expected-to-crash-linter.js`
);
expect(result.error).toBeTruthy();

// Each result should have attributes defined
onCompleteCalls.forEach(result => {
expect(result.repository).toBeTruthy();
expect(result.repositoryOwner).toBeTruthy();
expect(result.rule).toBeTruthy();
expect(result.message).toBeTruthy();
expect(result.path).toBeTruthy();
expect(result.link).toBeTruthy();
expect(result.extension).toBeTruthy();
expect(result.source).toBeTruthy();
});
});
});
4 changes: 3 additions & 1 deletion test/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const actualWrite = process.stdout.write;
const mockedExit = (jest.fn() as any) as typeof actualExit;
const mockedWrite = jest.fn();

(global as any).onComplete = jest.fn();

beforeEach(() => {
global.console.log = mockedLog;
global.process.exit = mockedExit;
Expand All @@ -18,7 +20,7 @@ beforeEach(() => {
global.process.stdout.rows = 9999;
});

afterEach(async () => {
afterEach(() => {
global.console.log = actualLog;
global.process.exit = actualExit;
global.process.stdout.write = actualWrite;
Expand Down
21 changes: 21 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { ResultTemplateOptions } from '@file-client/result-templates';

export const INTEGRARION_CONFIGURATION_LOCATION =
'test/integration/eslint-remote-tester.integration.config.js';
export const INTEGRATION_REPO_OWNER = 'AriPerkkio';
export const INTEGRATION_REPO_NAME =
'eslint-remote-tester-integration-test-target';

declare const global: { onComplete: jest.Mock };

/**
* Import the actual production build and run it
*/
Expand All @@ -12,6 +16,7 @@ export async function runProductionBuild(): Promise<void> {
// Clear possible previous runs results
resetStdoutMethodCalls();
resetExitCalls();
resetOnCompleteCalls();
jest.resetModules();

const { __handleForTests } = require('../dist/index');
Expand Down Expand Up @@ -105,6 +110,22 @@ export function resetExitCalls(): void {
((process.exit as any) as jest.Mock).mockClear();
}

/**
* Get call arguments of `global.onComplete`
*/
export function getOnCompleteCalls(): ResultTemplateOptions[][] {
const calls: ResultTemplateOptions[][][] = global.onComplete.mock.calls;

return calls.map(argumentArray => argumentArray[0]);
}

/**
* Reset captured calls of `global.onComplete`
*/
export function resetOnCompleteCalls(): void {
global.onComplete.mockClear();
}

/**
* Sanitize possible stack traces for sensitive paths
* - Removes absolute root path from stack traces, e.g.
Expand Down

0 comments on commit d053d2f

Please sign in to comment.