From 4482f709fc3a2fdf7ef926555422b608735d0cb8 Mon Sep 17 00:00:00 2001 From: Nikita Agupov Date: Sat, 31 Oct 2020 18:09:47 +0300 Subject: [PATCH 1/3] #7 common: removed managing with process, refactored validators --- config/jest/setup/console.setup.ts | 4 ++ jest.config.js | 7 +- src/core/core.spec.ts | 77 ++++++++------------- src/core/core.ts | 45 ++++-------- src/lib/lib.ts | 25 +------ src/linter/linter.spec.ts | 22 ++++++ src/linter/linter.ts | 39 +++++------ src/token-extractor/index.ts | 1 + src/token-extractor/token-extractor.spec.ts | 25 +++++++ src/token-extractor/token-extractor.ts | 24 +++++++ 10 files changed, 142 insertions(+), 127 deletions(-) create mode 100644 config/jest/setup/console.setup.ts create mode 100644 src/linter/linter.spec.ts create mode 100644 src/token-extractor/index.ts create mode 100644 src/token-extractor/token-extractor.spec.ts create mode 100644 src/token-extractor/token-extractor.ts diff --git a/config/jest/setup/console.setup.ts b/config/jest/setup/console.setup.ts new file mode 100644 index 0000000..f87fd19 --- /dev/null +++ b/config/jest/setup/console.setup.ts @@ -0,0 +1,4 @@ +global.console.log = jest.fn(); +global.console.info = jest.fn(); +global.console.warn = jest.fn(); +global.console.error = jest.fn(); diff --git a/jest.config.js b/jest.config.js index bc8b9eb..96939bc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,6 +5,9 @@ module.exports = { '!src/core/index.ts', '!cli.ts' ], + setupFiles: [ + '/config/jest/setup/console.setup.ts' + ], coveragePathIgnorePatterns: [ '.*\\.d\\.ts' ], @@ -28,9 +31,7 @@ module.exports = { 'node' ], globals: { - 'ts-jest': { - tsConfig: 'tsconfig.spec.json' - } + 'ts-jest': { tsConfig: 'tsconfig.spec.json' } }, collectCoverage: true, coverageThreshold: { diff --git a/src/core/core.spec.ts b/src/core/core.spec.ts index fdfcf64..cabc493 100644 --- a/src/core/core.spec.ts +++ b/src/core/core.spec.ts @@ -20,104 +20,91 @@ describe('Core', () => { beforeEach(() => { process = prepareConfig(process) as Process; - console.error = jest.fn(); - console.info = jest.fn(); }); it('should be failed with errored body, empty prefix and a lack of module', () => { process = commitWith('some error message', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + expect(() => validate()).toThrowError(); }); it('should be failed if there is a lack of issue prefix', () => { process = commitWith('common: changed structure of project', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + expect(() => validate()).toThrowError(); }); it('should be failed with present tense verb', () => { process = commitWith('#12 common: fix problem', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + expect(() => validate()).toThrowError(); }); it('should be passed with correct message in nested module', () => { - process = commitWith('#12 actions/panel: fixed openening cards', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + process = commitWith('#12 actions/panel: fixed opening cards', process); + expect(() => validate()).not.toThrowError(); }); it('should be failed if issue prefix is not in file', () => { - process = commitWith('COREUI-20 actions/panel: fixed openening cards', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + process = commitWith('COREUI-20 actions/panel: fixed opening cards', process); + expect(() => validate()).toThrowError(); }); it('should be passed if issue prefix is in file', () => { - process = commitWith('TAX-228 actions/panel: fixed openening cards', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + process = commitWith('TAX-228 actions/panel: fixed opening cards', process); + expect(() => validate()).not.toThrowError(); }); it('should be passed if alternative issue prefix is in file', () => { process = commitWith('proposal/commit-linter common: added library', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + expect(() => validate()).not.toThrowError(); }); it('should be passed if issue prefix is in file', () => { - process = commitWith('TAX-228 actions/panel: fixed openening cards', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + process = commitWith('TAX-228 actions/panel: fixed opening cards', process); + expect(() => validate()).not.toThrowError(); + }); + + it('should be failed if issue prefix is written in different case', () => { + process = commitWith('tax-228 actions/panel: fixed opening cards', process); + expect(() => validate()).toThrowError(); }); it('should be failed if action starts with capital letter', () => { - process = commitWith('TAX-228 actions/panel: Fixed openening cards', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + process = commitWith('TAX-228 actions/panel: Fixed opening cards', process); + expect(() => validate()).toThrowError(); }); it('should be passed if commit message includes big letters and points in the middle', () => { process = commitWith('#12 common: added rule about proposals to README.md', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + expect(() => validate()).not.toThrowError(); }); it('should be failed if there is a point in the end of commit message', () => { process = commitWith('#12 common: removed all errors.', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + expect(() => validate()).toThrowError(); }); it('should be passed with correct message in multiple modules', () => { - process = commitWith('#12 actions: fixed openening cards; common/icons: added close icon', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + process = commitWith('#12 actions: fixed opening cards; common/icons: added close icon', process); + expect(() => validate()).not.toThrowError(); }); it('should be passed with ignore pattern in merge', () => { process = commitWith('Merge branch \'dev\'', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + expect(() => validate()).not.toThrowError(); }); it('should be passed with ignore pattern in auto', () => { process = commitWith('auto/ci: set version 1.2.5', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + expect(() => validate()).not.toThrowError(); }); it('should be failed if there are no body', () => { process = commitWith('#12 actions', process); - validate(); - expect(getFirstExitStatus(process)).toBe(1); + expect(() => validate()).toThrowError(); }); it('should be passed with microfix issue prefix', () => { process = commitWith('microfix common: fixed dependency number', process); - validate(); - expect(getFirstExitStatus(process)).toBe(0); + expect(() => validate()).not.toThrowError(); }); it('should be passed for config without prefixes', () => { @@ -125,18 +112,12 @@ describe('Core', () => { process, './src/core/spec-assets/spec.config.without-prefix.json' ) as Process; - process = commitWith('cards: fixed padding in title', process); - validate(); + process = commitWith('cards: fixed padding in title', process); - expect(getFirstExitStatus(process)).toBe(0); + expect(() => validate()).not.toThrowError(); }); - function getFirstExitStatus(process: Process): number { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (process.exit as any as Mock).mock.calls[0][0]; - } - function prepareConfig(process: Process, customConfigPath?: string): Process | { exit: Mock } { const config = customConfigPath || './src/core/spec-assets/spec.config.json'; diff --git a/src/core/core.ts b/src/core/core.ts index fbc19b6..1f1d990 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -1,42 +1,27 @@ -import { Config } from '../types'; -import { TokenDictionary } from './core.interface'; -import { exitWithSuccess, getCommitMessage, getConfig } from '../lib'; -import { StatusMessage } from '../common/const'; -import { checkForBodies, checkForIgnoreMatching, checkForIssuePrefix } from '../linter'; +import { getCommitMessage, getConfig } from '../lib'; +import { checkForBodies, checkForIssuePrefix, isMessageShouldBeIgnored } from '../linter'; +import { getTokensFrom } from '../token-extractor'; /** - * A main function for commit linting. If there are no any errors exit process with 0, else with 1. + * A main function for commit linting. */ export function validate(): void { const config = getConfig(); const commitMessage = getCommitMessage(); const tokens = getTokensFrom(commitMessage, config); - checkForIgnoreMatching(config, tokens); - checkForIssuePrefix(config, tokens); - checkForBodies(config, tokens); + const isIgnored = isMessageShouldBeIgnored(config, tokens); - exitWithSuccess(StatusMessage.VALID); -} - -/** - * Parse tokens from commit message. - * | COREUI-220123 common: added the ability to parse library; card: added user | - * | | - * | | - * | | - * - * @param message - * @param config - */ -function getTokensFrom(message: string, config: Config): TokenDictionary { - const wholeString = message; + if (isIgnored) { + return; + } - const hasIssuePrefixes = config.issuePrefixes && !config.issuePrefixes.includes('.*'); - const [issuePrefix, ...rest] = hasIssuePrefixes ? message.split(' ') : ['', message]; - const bodies = rest.join(' ') - .split(';') - .map(body => body.trim()); + const checkers = [checkForIssuePrefix, checkForBodies]; - return { wholeString, issuePrefix, bodies }; + for (let i = 0; i < checkers.length; i++) { + const error = checkers[i](config, tokens); + if (error) { + throw new Error(error); + } + } } diff --git a/src/lib/lib.ts b/src/lib/lib.ts index 6d939c9..6ed2c0f 100644 --- a/src/lib/lib.ts +++ b/src/lib/lib.ts @@ -2,26 +2,6 @@ import { ArgumentParser } from '@eigenspace/argument-parser'; import fs from 'fs'; import { Config, Dictionary } from '../types'; -/** - * Exit from process with success and print info message in console - * - * @param message - */ -function exitWithSuccess(message: string): void { - console.info(message); - process.exit(0); -} - -/** - * Exit from process with error message and print error message in console - * - * @param message - */ -function exitWithError(message: string): void { - console.error(message); - process.exit(1); -} - const DEFAULT_COMMIT_CONFIG_PATH = '.commit-linter.config.json'; /** @@ -80,8 +60,7 @@ function getConfigContentFrom(path?: string): Config | undefined { const content = fs.readFileSync(path, 'utf8'); config = JSON.parse(content); } catch (err) { - exitWithError(`Failed to load ${path}`); - return; + throw new Error(`Failed to load ${path}`); } return config; @@ -121,8 +100,6 @@ function merge(...objects: any[]): T { } export { - exitWithError, - exitWithSuccess, getCommitMessage, getConfigContentFrom, merge diff --git a/src/linter/linter.spec.ts b/src/linter/linter.spec.ts new file mode 100644 index 0000000..aba3b6a --- /dev/null +++ b/src/linter/linter.spec.ts @@ -0,0 +1,22 @@ +import { checkForIssuePrefix } from './linter'; + +describe('Linter', () => { + + describe('#checkForIssuePrefix', () => { + + it('should pass validating prefix', () => { + const config = { issuePrefixes: ['SC-[0-9]+'] }; + const result = checkForIssuePrefix(config, { issuePrefix: 'SC-123' }); + expect(result).toBeNull(); + }); + + it('should return error for a prefix in another case', () => { + const config = { issuePrefixes: ['SC-[0-9]+'] }; + + const result = checkForIssuePrefix(config, { issuePrefix: 'sc-123' }); + + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 3f5646e..a004a11 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -1,10 +1,12 @@ import { Config } from '../types'; import { TokenDictionary } from '../core/core.interface'; -import { exitWithError, exitWithSuccess } from '../lib'; -import { ErrorMessage, StatusMessage, StringValue } from '../common/const'; +import { ErrorMessage, StringValue } from '../common/const'; + +type ValidationError = string; +type ValidationResult = ValidationError | null; /** - * Exit from process with code 0 if commit message matches with some of ignore patterns. + * Return true if message should be ignored. * * For instance, if there is a ignore pattern '^Merge .*' commit with message "Merge to dev" will * be accepted. @@ -12,44 +14,37 @@ import { ErrorMessage, StatusMessage, StringValue } from '../common/const'; * @param config * @param wholeString */ -export function checkForIgnoreMatching(config: Config, { wholeString = '' }: TokenDictionary): void { - if ((config.ignore || []).some((rule: string) => wholeString.match(rule))) { - exitWithSuccess(StatusMessage.VALID); - } +export function isMessageShouldBeIgnored(config: Config, { wholeString = '' }: TokenDictionary): boolean { + return (config.ignore || []).some((rule: string) => wholeString.match(rule)); } /** - * Exit from process with error code if issue prefix doesn't match any of patterns. + * Return error message if checking was failed. * * @param config * @param issuePrefix */ -export function checkForIssuePrefix(config: Config, { issuePrefix = '' }: TokenDictionary): void { +export function checkForIssuePrefix(config: Config, { issuePrefix = '' }: TokenDictionary): ValidationResult { if ((config.issuePrefixes || []).some(value => issuePrefix.match(value))) { - return; + return null; } - exitWithError( - ErrorMessage.ISSUE_PREFIX_ERROR.replace(StringValue.ISSUE_PREFIX, issuePrefix) - .replace(StringValue.DOC_LINK, config.linkToRule as string) - ); + return ErrorMessage.ISSUE_PREFIX_ERROR.replace(StringValue.ISSUE_PREFIX, issuePrefix) + .replace(StringValue.DOC_LINK, config.linkToRule as string); } /** - * Exit from process with error code if at least one of bodies doesn't match rule. - * Also prints in console list of not matched bodies. + * Return error message if checking was failed. * * @param config * @param bodies */ -export function checkForBodies(config: Config, { bodies = [] }: TokenDictionary): void { +export function checkForBodies(config: Config, { bodies = [] }: TokenDictionary): ValidationResult { if (bodies.every(body => body.match(config.body as RegExp))) { - return; + return null; } const notMatchedBodies = bodies.filter(body => !body.match(config.body as RegExp)); - exitWithError( - ErrorMessage.BODIES_ERROR.replace(StringValue.BODIES, notMatchedBodies.join('", "')) - .replace(StringValue.DOC_LINK, config.linkToRule as string) - ); + return ErrorMessage.BODIES_ERROR.replace(StringValue.BODIES, notMatchedBodies.join('", "')) + .replace(StringValue.DOC_LINK, config.linkToRule as string); } diff --git a/src/token-extractor/index.ts b/src/token-extractor/index.ts new file mode 100644 index 0000000..f3aacdd --- /dev/null +++ b/src/token-extractor/index.ts @@ -0,0 +1 @@ +export * from './token-extractor'; diff --git a/src/token-extractor/token-extractor.spec.ts b/src/token-extractor/token-extractor.spec.ts new file mode 100644 index 0000000..69e0ef6 --- /dev/null +++ b/src/token-extractor/token-extractor.spec.ts @@ -0,0 +1,25 @@ +import { getTokensFrom } from './token-extractor'; + +describe('TokenExtractor', () => { + + describe('#getTokensFrom', () => { + + it('should extract issue prefix in upper case', () => { + const config = { issuePrefixes: ['SC-[0-9]+'] }; + const message = 'SC-123 common: message'; + + const result = getTokensFrom(message, config); + + expect(result.issuePrefix).toEqual('SC-123'); + }); + + it('should extract issue prefix in lower case', () => { + const config = { issuePrefixes: ['sc-[0-9]+'] }; + const message = 'sc-123 common: message'; + + const result = getTokensFrom(message, config); + + expect(result.issuePrefix).toEqual('sc-123'); + }); + }); +}); \ No newline at end of file diff --git a/src/token-extractor/token-extractor.ts b/src/token-extractor/token-extractor.ts new file mode 100644 index 0000000..329c882 --- /dev/null +++ b/src/token-extractor/token-extractor.ts @@ -0,0 +1,24 @@ +import { Config } from '../types'; +import { TokenDictionary } from '../core/core.interface'; + +/** + * Parse tokens from commit message. + * | COREUI-220123 common: added the ability to parse library; card: added user | + * | | + * | | + * | | + * + * @param message + * @param config + */ +export function getTokensFrom(message: string, config: Config): TokenDictionary { + const wholeString = message; + + const hasIssuePrefixes = config.issuePrefixes && !config.issuePrefixes.includes('.*'); + const [issuePrefix, ...rest] = hasIssuePrefixes ? message.split(' ') : ['', message]; + const bodies = rest.join(' ') + .split(';') + .map(body => body.trim()); + + return { wholeString, issuePrefix, bodies }; +} \ No newline at end of file From b4620a9b100fee7bcd08557c00bd755e14f8a014 Mon Sep 17 00:00:00 2001 From: Nikita Agupov Date: Sun, 1 Nov 2020 09:55:54 +0300 Subject: [PATCH 2/3] #7 common: fixed comment Co-authored-by: Daniil Sitdikov --- src/token-extractor/token-extractor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/token-extractor/token-extractor.ts b/src/token-extractor/token-extractor.ts index 329c882..0782ea9 100644 --- a/src/token-extractor/token-extractor.ts +++ b/src/token-extractor/token-extractor.ts @@ -2,7 +2,7 @@ import { Config } from '../types'; import { TokenDictionary } from '../core/core.interface'; /** - * Parse tokens from commit message. + * Parses tokens from the commit message. * | COREUI-220123 common: added the ability to parse library; card: added user | * | | * | | @@ -21,4 +21,4 @@ export function getTokensFrom(message: string, config: Config): TokenDictionary .map(body => body.trim()); return { wholeString, issuePrefix, bodies }; -} \ No newline at end of file +} From d882a27b8b8e2559935d3874c3e8483519967e2a Mon Sep 17 00:00:00 2001 From: Nikita Agupov Date: Sun, 1 Nov 2020 09:56:57 +0300 Subject: [PATCH 3/3] #7 common: replaced for to forEach for checking errors Co-authored-by: Daniil Sitdikov --- src/core/core.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/core.ts b/src/core/core.ts index 1f1d990..c0456f9 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -18,7 +18,12 @@ export function validate(): void { const checkers = [checkForIssuePrefix, checkForBodies]; - for (let i = 0; i < checkers.length; i++) { + checkers.forEach(checker => { + const error = checker(config, tokens); + if (error) { + throw new Error(error); + } + }); const error = checkers[i](config, tokens); if (error) { throw new Error(error);