diff --git a/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.ts b/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.ts index fc8c5da1..2450d0ec 100644 --- a/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.ts +++ b/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.ts @@ -12,7 +12,7 @@ export const analyzeTooLongLines: CodeValidationAnalyzer = ( lineNumber: line.lineNumber, error: [ `Line is too long (${line.text.length}).`, - `It exceed maximum allowed length ${maxLineLength}.`, + `It exceed maximum allowed length ${maxLineLength} by ${line.text.length - maxLineLength} characters.`, 'This may cause bugs due to unintended trimming by operating system, shells or terminal emulators.', ].join(' '), })); @@ -39,6 +39,6 @@ function getMaxAllowedLineLength(language: ScriptingLanguage): number { */ return 1048576; // Minimum value for reliability default: - throw new Error(`Unsupported language: ${language}`); + throw new Error(`Unsupported language: ${ScriptingLanguage[language]} (${language})`); } } diff --git a/src/application/Parser/Executable/Script/Validation/CodeValidator.ts b/src/application/Parser/Executable/Script/Validation/CodeValidator.ts index f89e5c8a..1bc3826a 100644 --- a/src/application/Parser/Executable/Script/Validation/CodeValidator.ts +++ b/src/application/Parser/Executable/Script/Validation/CodeValidator.ts @@ -46,9 +46,33 @@ function formatLines( ): string { return lines.map((line) => { const badLine = invalidLines.find((invalidLine) => invalidLine.lineNumber === line.lineNumber); - if (!badLine) { - return `[${line.lineNumber}] ✅ ${line.text}`; - } - return `[${badLine.lineNumber}] ❌ ${line.text}\n\t⟶ ${badLine.error}`; + return formatLine({ + lineNumber: line.lineNumber, + text: line.text, + error: badLine?.error, + }); }).join('\n'); } +function formatLine( + line: { + readonly lineNumber: number; + readonly text: string; + readonly error?: string; + }, +): string { + let text = `[${line.lineNumber}] `; + text += line.error ? '❌' : '✅'; + text += ` ${trimLine(line.text)}`; + if (line.error) { + text += `\n\t⟶ ${line.error}`; + } + return text; +} + +function trimLine(line: string) { + const maxLength = 500; + if (line.length > maxLength) { + line = `${line.substring(0, maxLength)}... [Rest of the line trimmed]`; + } + return line; +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.spec.ts index ac4cd120..ad3d2413 100644 --- a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines.spec.ts @@ -3,161 +3,173 @@ import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/ import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { analyzeTooLongLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines'; import { createCodeLines } from './CreateCodeLines'; -import { expectSameInvalidCodeLines } from './ExpectSameInvalidCodeLines'; +import { expectInvalidCodeLines, expectSameInvalidCodeLines } from './ExpectSameInvalidCodeLines'; describe('AnalyzeTooLongLines', () => { describe('analyzeTooLongLines', () => { - describe('batchfile', () => { - const MAX_BATCHFILE_LENGTH = 8191; - - it('returns no results for lines within the maximum length', () => { - // arrange - const expected: InvalidCodeLine[] = []; - const context = new TestContext() - .withLanguage(ScriptingLanguage.batchfile) - .withLines([ - 'A'.repeat(MAX_BATCHFILE_LENGTH), - 'B'.repeat(8000), - 'C'.repeat(100), - ]); - // act - const actual = context.analyze(); - // assert - expectSameInvalidCodeLines(actual, expected); + describe('returns no results for lines within the maximum length', () => { + createScriptLanguageScenarios().forEach((scenario) => { + it(scenario.description, () => { + // arrange + const expected: InvalidCodeLine[] = []; + const context = new TestContext() + .withLanguage(scenario.language) + .withLines([ + 'A'.repeat(scenario.maxLength), + 'B'.repeat(scenario.maxLength - 1), + 'C'.repeat(100), + ]); + // act + const actual = context.analyze(); + // assert + expectSameInvalidCodeLines(actual, expected); + }); }); + }); - it('identifies a single line exceeding maximum length', () => { - // arrange - const expectedLength = MAX_BATCHFILE_LENGTH + 1; - const expected: InvalidCodeLine[] = [{ - lineNumber: 2, - error: createTooLongLineError(expectedLength, MAX_BATCHFILE_LENGTH), - }]; - const context = new TestContext() - .withLanguage(ScriptingLanguage.batchfile) - .withLines([ - 'A'.repeat(MAX_BATCHFILE_LENGTH), - 'B'.repeat(expectedLength), - 'C'.repeat(100), - ]); - // act - const actual = context.analyze(); - // assert - expectSameInvalidCodeLines(actual, expected); + describe('identifies a single line exceeding maximum length', () => { + createScriptLanguageScenarios().forEach((scenario) => { + it(scenario.description, () => { + // arrange + const expectedLength = scenario.maxLength + 1; + const expectedLineNumber = 2; + const context = new TestContext() + .withLanguage(scenario.language) + .withLines([ + 'A'.repeat(scenario.maxLength), + 'B'.repeat(expectedLength), + 'C'.repeat(100), + ]); + // act + const actual = context.analyze(); + // assert + expectInvalidCodeLines(actual, [expectedLineNumber]); + }); }); + }); - it('identifies multiple lines exceeding maximum length', () => { - // arrange - const expectedLength1 = MAX_BATCHFILE_LENGTH + 1; - const expectedLength2 = MAX_BATCHFILE_LENGTH + 2; - const expected: InvalidCodeLine[] = [ - { - lineNumber: 1, - error: createTooLongLineError(expectedLength1, MAX_BATCHFILE_LENGTH), - }, - { - lineNumber: 3, - error: createTooLongLineError(expectedLength2, MAX_BATCHFILE_LENGTH), - }, - ]; - const context = new TestContext() - .withLanguage(ScriptingLanguage.batchfile) - .withLines([ - 'A'.repeat(expectedLength1), - 'B'.repeat(MAX_BATCHFILE_LENGTH), - 'C'.repeat(expectedLength2), - ]); - // act - const actual = context.analyze(); - // assert - expectSameInvalidCodeLines(actual, expected); + describe('identifies multiple lines exceeding maximum length', () => { + createScriptLanguageScenarios().forEach((scenario) => { + it(scenario.description, () => { + // arrange + const expectedInvalidLines: readonly { + readonly lineNumber: number, + readonly length: number, + }[] = [ + { lineNumber: 1, length: scenario.maxLength + 1 }, + { lineNumber: 3, length: scenario.maxLength + 2 }, + ]; + const context = new TestContext() + .withLanguage(scenario.language) + .withLines([ + 'A'.repeat(expectedInvalidLines[0].length), + 'B'.repeat(scenario.maxLength), + 'C'.repeat(expectedInvalidLines[1].length), + ]); + // act + const actual = context.analyze(); + // assert + expectInvalidCodeLines(actual, expectedInvalidLines.map((l) => l.lineNumber)); + }); }); }); - describe('shellscript', () => { - const MAX_SHELLSCRIPT_LENGTH = 1048576; - - it('returns no results for lines within the maximum length', () => { - // arrange - const expected: InvalidCodeLine[] = []; - const context = new TestContext() - .withLanguage(ScriptingLanguage.shellscript) - .withLines([ - 'A'.repeat(MAX_SHELLSCRIPT_LENGTH), - 'B'.repeat(1000000), - 'C'.repeat(100), - ]); - // act - const actual = context.analyze(); - // assert - expectSameInvalidCodeLines(actual, expected); + describe('error construction', () => { + describe('outputs actual character length', () => { + createScriptLanguageScenarios().forEach((scenario) => { + it(scenario.description, () => { + // arrange + const expectedLength = scenario.maxLength + 1; + const expectedErrorPart = `Line is too long (${expectedLength}).`; + const context = new TestContext() + .withLanguage(scenario.language) + .withLines([ + 'B'.repeat(expectedLength), + ]); + // act + const actual = context.analyze(); + // assert + expect(actual).to.have.lengthOf(1); + const actualError = actual[0].error; + expect(actualError).to.include(expectedErrorPart); + }); + }); }); - - it('identifies a single line exceeding maximum length', () => { - // arrange - const expectedLength = MAX_SHELLSCRIPT_LENGTH + 1; - const expected: InvalidCodeLine[] = [{ - lineNumber: 2, - error: createTooLongLineError(expectedLength, MAX_SHELLSCRIPT_LENGTH), - }]; - const context = new TestContext() - .withLanguage(ScriptingLanguage.shellscript) - .withLines([ - 'A'.repeat(MAX_SHELLSCRIPT_LENGTH), - 'B'.repeat(expectedLength), - 'C'.repeat(100), - ]); - // act - const actual = context.analyze(); - // assert - expectSameInvalidCodeLines(actual, expected); + describe('outputs maximum allowed character length', () => { + createScriptLanguageScenarios().forEach((scenario) => { + it(scenario.description, () => { + // arrange + const expectedLength = scenario.maxLength + 1; + const expectedErrorPart = `It exceed maximum allowed length ${scenario.maxLength}`; + const context = new TestContext() + .withLanguage(scenario.language) + .withLines([ + 'B'.repeat(expectedLength), + ]); + // act + const actual = context.analyze(); + // assert + expect(actual).to.have.lengthOf(1); + const actualError = actual[0].error; + expect(actualError).to.include(expectedErrorPart); + }); + }); }); - - it('identifies multiple lines exceeding maximum length', () => { - // arrange - const expectedLength1 = MAX_SHELLSCRIPT_LENGTH + 1; - const expectedLength2 = MAX_SHELLSCRIPT_LENGTH + 2; - const expected: InvalidCodeLine[] = [ - { - lineNumber: 1, - error: createTooLongLineError(expectedLength1, MAX_SHELLSCRIPT_LENGTH), - }, - { - lineNumber: 3, - error: createTooLongLineError(expectedLength2, MAX_SHELLSCRIPT_LENGTH), - }, - ]; - const context = new TestContext() - .withLanguage(ScriptingLanguage.shellscript) - .withLines([ - 'A'.repeat(expectedLength1), - 'B'.repeat(MAX_SHELLSCRIPT_LENGTH), - 'C'.repeat(expectedLength2), - ]); - // act - const actual = context.analyze(); - // assert - expectSameInvalidCodeLines(actual, expected); + describe('outputs total exceeding characters', () => { + createScriptLanguageScenarios().forEach((scenario) => { + it(scenario.description, () => { + // arrange + const expectedExceedingCharacters = 5; + const expectedErrorPart = `by ${expectedExceedingCharacters} characters.`; + const lineLength = scenario.maxLength + expectedExceedingCharacters; + const context = new TestContext() + .withLanguage(scenario.language) + .withLines([ + 'B'.repeat(lineLength), + ]); + // act + const actual = context.analyze(); + // assert + expect(actual).to.have.lengthOf(1); + const actualError = actual[0].error; + expect(actualError).to.include(expectedErrorPart); + }); + }); }); }); it('throws an error for unsupported language', () => { // arrange + const unsupportedLanguage = 'unsupported' as unknown as ScriptingLanguage; + const expectedError = `Unsupported language: ${ScriptingLanguage[unsupportedLanguage]} (${unsupportedLanguage})`; const context = new TestContext() .withLanguage('unsupported' as unknown as ScriptingLanguage) .withLines(['A', 'B', 'C']); - // act & assert - expect(() => context.analyze()).to.throw('Unsupported language: unsupported'); + // act + const act = () => context.analyze(); + // assert + expect(act).to.throw(expectedError); }); }); }); -function createTooLongLineError(actualLength: number, maxAllowedLength: number): string { - return [ - `Line is too long (${actualLength}).`, - `It exceed maximum allowed length ${maxAllowedLength}.`, - 'This may cause bugs due to unintended trimming by operating system, shells or terminal emulators.', - ].join(' '); +interface ScriptLanguageScenario { + readonly description: string; + readonly maxLength: number; + readonly language: ScriptingLanguage; +} + +function createScriptLanguageScenarios(): readonly ScriptLanguageScenario[] { + const maxLengths: Record< // `Record` catches missing entries at compile-time + ScriptingLanguage, number> = { + [ScriptingLanguage.batchfile]: 8191, + [ScriptingLanguage.shellscript]: 1048576, + }; + return Object.entries(maxLengths).map(([language, length]): ScriptLanguageScenario => ({ + description: `${ScriptingLanguage[language]} (max: ${length})`, + language: Number.parseInt(language, 10) as ScriptingLanguage, + maxLength: length, + })); } class TestContext { diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts index 948489e7..dd99e3c2 100644 --- a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts +++ b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts @@ -1,11 +1,32 @@ import { expect } from 'vitest'; import type { InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; export function expectSameInvalidCodeLines( expected: readonly InvalidCodeLine[], actual: readonly InvalidCodeLine[], ) { - expect(sort(expected)).to.deep.equal(sort(actual)); + const sortedExpected = sort(expected); + const sortedActual = sort(actual); + + expect(sortedActual).to.deep.equal(sortedExpected, formatAssertionMessage([ + 'Invalid code lines do not match', + `Expected: ${JSON.stringify(sortedExpected, null, 2)}`, + `Actual: ${JSON.stringify(sortedActual, null, 2)}`, + ])); +} + +export function expectInvalidCodeLines( + actual: readonly InvalidCodeLine[], + expectedInvalidLineNumbers: readonly number[], +) { + const sortedActualLineNumbers = actual.map((a) => a.lineNumber).sort(); + const sortedExpectedLineNumbers = [...expectedInvalidLineNumbers].sort(); + expect(sortedActualLineNumbers).to.deep.equal(sortedExpectedLineNumbers, formatAssertionMessage([ + 'Invalid line numbers do not match.', + `Expected (${sortedExpectedLineNumbers.length}): ${sortedExpectedLineNumbers.join(', ')}`, + `Actual (${sortedActualLineNumbers.length}): ${sortedActualLineNumbers.join(', ')}`, + ])); } function sort(lines: readonly InvalidCodeLine[]) { // To ignore order