Add validation for max line length in compiler
This commit adds validation logic in compiler to check for max allowed characters per line for scripts. This allows preventing bugs caused by limitation of terminal emulators. Other supporting changes: - Rename/refactor related code for clarity and better maintainability. - Drop `I` prefix from interfaces to align with latest convention. - Refactor CodeValidator to be functional rather than object-oriented for simplicity. - Refactor syntax definition construction to be functional and be part of rule for better separation of concerns. - Refactored validation logic to use an enum-based factory pattern for improved maintainability and scalability.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import type { ScriptCompilerFactory } from '@/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory';
|
||||
import { createCategoryCollectionContext } from '@/application/Parser/Executable/CategoryCollectionContext';
|
||||
import { createScriptCompilerFactorySpy } from '@tests/unit/shared/Stubs/ScriptCompilerFactoryStub';
|
||||
|
||||
describe('CategoryCollectionContext', () => {
|
||||
describe('createCategoryCollectionContext', () => {
|
||||
describe('functionsData', () => {
|
||||
describe('can create with absent data', () => {
|
||||
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
|
||||
// arrange
|
||||
const context = new TextContext()
|
||||
.withData(absentValue);
|
||||
// act
|
||||
const act = () => context.create();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('compiler', () => {
|
||||
it('constructed with correct functions', () => {
|
||||
// arrange
|
||||
const expectedFunctions = [createFunctionDataWithCode()];
|
||||
const compilerSpy = createScriptCompilerFactorySpy();
|
||||
const context = new TextContext()
|
||||
.withData(expectedFunctions)
|
||||
.withScriptCompilerFactory(compilerSpy.instance);
|
||||
// act
|
||||
const actualContext = context.create();
|
||||
// assert
|
||||
const actualCompiler = actualContext.compiler;
|
||||
const compilerParameters = compilerSpy.getInitParameters(actualCompiler);
|
||||
const actualFunctions = compilerParameters?.categoryContext.functions;
|
||||
expect(actualFunctions).to.equal(expectedFunctions);
|
||||
});
|
||||
it('constructed with correct language', () => {
|
||||
// arrange
|
||||
const expectedLanguage = ScriptingLanguage.batchfile;
|
||||
const compilerSpy = createScriptCompilerFactorySpy();
|
||||
const context = new TextContext()
|
||||
.withLanguage(expectedLanguage)
|
||||
.withScriptCompilerFactory(compilerSpy.instance);
|
||||
// act
|
||||
const actualContext = context.create();
|
||||
// assert
|
||||
const actualCompiler = actualContext.compiler;
|
||||
const compilerParameters = compilerSpy.getInitParameters(actualCompiler);
|
||||
const actualLanguage = compilerParameters?.categoryContext.language;
|
||||
expect(actualLanguage).to.equal(expectedLanguage);
|
||||
});
|
||||
});
|
||||
describe('language', () => {
|
||||
it('set from syntax factory', () => {
|
||||
// arrange
|
||||
const expectedLanguage = ScriptingLanguage.shellscript;
|
||||
const context = new TextContext()
|
||||
.withLanguage(expectedLanguage);
|
||||
// act
|
||||
const actualContext = context.create();
|
||||
// assert
|
||||
const actualLanguage = actualContext.language;
|
||||
expect(actualLanguage).to.equal(expectedLanguage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TextContext {
|
||||
private functionsData: readonly FunctionData[] | undefined = [createFunctionDataWithCode()];
|
||||
|
||||
private language: ScriptingLanguage = ScriptingLanguage.shellscript;
|
||||
|
||||
private scriptCompilerFactory: ScriptCompilerFactory = createScriptCompilerFactorySpy().instance;
|
||||
|
||||
public withScriptCompilerFactory(scriptCompilerFactory: ScriptCompilerFactory): this {
|
||||
this.scriptCompilerFactory = scriptCompilerFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withData(data: readonly FunctionData[] | undefined): this {
|
||||
this.functionsData = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public create(): ReturnType<typeof createCategoryCollectionContext> {
|
||||
return createCategoryCollectionContext(
|
||||
this.functionsData,
|
||||
this.language,
|
||||
this.scriptCompilerFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ISyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Syntax/ISyntaxFactory';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler';
|
||||
import { createCollectionUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { createSyntaxFactoryStub } from '@tests/unit/shared/Stubs/SyntaxFactoryStub';
|
||||
|
||||
describe('CategoryCollectionSpecificUtilities', () => {
|
||||
describe('createCollectionUtilities', () => {
|
||||
describe('functionsData', () => {
|
||||
describe('can create with absent data', () => {
|
||||
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
|
||||
// arrange
|
||||
const context = new TextContext()
|
||||
.withData(absentValue);
|
||||
// act
|
||||
const act = () => context.createCollectionUtilities();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('compiler', () => {
|
||||
it('constructed as expected', () => {
|
||||
// arrange
|
||||
const functionsData = [createFunctionDataWithCode()];
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
const expected = new ScriptCompiler({
|
||||
functions: functionsData,
|
||||
syntax,
|
||||
});
|
||||
const language = ScriptingLanguage.shellscript;
|
||||
const factoryMock = createSyntaxFactoryStub(language, syntax);
|
||||
const definition = new ScriptingDefinitionStub()
|
||||
.withLanguage(language);
|
||||
const context = new TextContext()
|
||||
.withData(functionsData)
|
||||
.withScripting(definition)
|
||||
.withSyntaxFactory(factoryMock);
|
||||
// act
|
||||
const utilities = context.createCollectionUtilities();
|
||||
// assert
|
||||
const actual = utilities.compiler;
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('syntax', () => {
|
||||
it('set from syntax factory', () => {
|
||||
// arrange
|
||||
const language = ScriptingLanguage.shellscript;
|
||||
const expected = new LanguageSyntaxStub();
|
||||
const factoryMock = createSyntaxFactoryStub(language, expected);
|
||||
const definition = new ScriptingDefinitionStub()
|
||||
.withLanguage(language);
|
||||
const context = new TextContext()
|
||||
.withScripting(definition)
|
||||
.withSyntaxFactory(factoryMock);
|
||||
// act
|
||||
const utilities = context.createCollectionUtilities();
|
||||
// assert
|
||||
const actual = utilities.syntax;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TextContext {
|
||||
private functionsData: readonly FunctionData[] | undefined = [createFunctionDataWithCode()];
|
||||
|
||||
private scripting: IScriptingDefinition = new ScriptingDefinitionStub();
|
||||
|
||||
private syntaxFactory: ISyntaxFactory = createSyntaxFactoryStub();
|
||||
|
||||
public withScripting(scripting: IScriptingDefinition): this {
|
||||
this.scripting = scripting;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withData(data: readonly FunctionData[] | undefined): this {
|
||||
this.functionsData = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSyntaxFactory(syntaxFactory: ISyntaxFactory): this {
|
||||
this.syntaxFactory = syntaxFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public createCollectionUtilities(): ReturnType<typeof createCollectionUtilities> {
|
||||
return createCollectionUtilities(
|
||||
this.functionsData,
|
||||
this.scripting,
|
||||
this.syntaxFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { CategoryData, ExecutableData } from '@/application/collections/';
|
||||
import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
||||
import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||
import { CategoryCollectionContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionContextStub';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
@@ -357,9 +357,9 @@ describe('CategoryParser', () => {
|
||||
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
||||
expect(actualParsedScripts).to.have.members(expectedScripts);
|
||||
});
|
||||
it('parses all scripts with correct utilities', () => {
|
||||
it('parses all scripts with correct context', () => {
|
||||
// arrange
|
||||
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const expectedContext = new CategoryCollectionContextStub();
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childrenData = [
|
||||
createScriptDataWithCode(),
|
||||
@@ -372,24 +372,24 @@ describe('CategoryParser', () => {
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCollectionUtilities(expected)
|
||||
.withCollectionContext(expectedContext)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
const actualUtilities = actualParsedScripts.map(
|
||||
const actualContext = actualParsedScripts.map(
|
||||
(s) => scriptParser.getParseParameters(s)[1],
|
||||
);
|
||||
expect(
|
||||
actualUtilities.every(
|
||||
(actual) => actual === expected,
|
||||
actualContext.every(
|
||||
(actual) => actual === expectedContext,
|
||||
),
|
||||
formatAssertionMessage([
|
||||
`Expected all elements to be ${JSON.stringify(expected)}`,
|
||||
`Expected all elements to be ${JSON.stringify(expectedContext)}`,
|
||||
'All elements:',
|
||||
indentText(JSON.stringify(actualUtilities)),
|
||||
indentText(JSON.stringify(actualContext)),
|
||||
]),
|
||||
).to.equal(true);
|
||||
});
|
||||
@@ -464,8 +464,7 @@ describe('CategoryParser', () => {
|
||||
class TestContext {
|
||||
private data: CategoryData = new CategoryDataStub();
|
||||
|
||||
private collectionUtilities:
|
||||
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
|
||||
private collectionContext: CategoryCollectionContextStub = new CategoryCollectionContextStub();
|
||||
|
||||
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
|
||||
|
||||
@@ -482,10 +481,10 @@ class TestContext {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCollectionUtilities(
|
||||
collectionUtilities: CategoryCollectionSpecificUtilitiesStub,
|
||||
public withCollectionContext(
|
||||
collectionContext: CategoryCollectionContextStub,
|
||||
): this {
|
||||
this.collectionUtilities = collectionUtilities;
|
||||
this.collectionContext = collectionContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -517,7 +516,7 @@ class TestContext {
|
||||
public parseCategory() {
|
||||
return parseCategory(
|
||||
this.data,
|
||||
this.collectionUtilities,
|
||||
this.collectionContext,
|
||||
{
|
||||
createCategory: this.categoryFactory,
|
||||
wrapError: this.errorWrapper,
|
||||
|
||||
@@ -9,12 +9,8 @@ import { createFunctionDataWithCall, createFunctionDataWithCode, createFunctionD
|
||||
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import type { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||
@@ -27,6 +23,8 @@ import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/
|
||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
||||
import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser';
|
||||
import { createFunctionParameterParserStub } from '@tests/unit/shared/Stubs/FunctionParameterParserStub';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
||||
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
||||
|
||||
describe('SharedFunctionsParser', () => {
|
||||
@@ -161,22 +159,53 @@ describe('SharedFunctionsParser', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
it('validates function code as expected when code is defined', () => {
|
||||
// arrange
|
||||
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
|
||||
const functionData = createFunctionDataWithCode()
|
||||
.withCode('expected code to be validated')
|
||||
.withRevertCode('expected revert code to be validated');
|
||||
const validator = new CodeValidatorStub();
|
||||
// act
|
||||
new TestContext()
|
||||
.withFunctions([functionData])
|
||||
.withValidator(validator)
|
||||
.parseFunctions();
|
||||
// assert
|
||||
validator.assertHistory({
|
||||
validatedCodes: [functionData.code, functionData.revertCode],
|
||||
rules: expectedRules,
|
||||
describe('code validation', () => {
|
||||
it('validates function code', () => {
|
||||
// arrange
|
||||
const expectedCode = 'expected code to be validated';
|
||||
const expectedRevertCode = 'expected revert code to be validated';
|
||||
const functionData = createFunctionDataWithCode()
|
||||
.withCode(expectedCode)
|
||||
.withRevertCode(expectedRevertCode);
|
||||
const expectedCodes: readonly string[] = [expectedCode, expectedRevertCode];
|
||||
const validator = new CodeValidatorStub();
|
||||
// act
|
||||
new TestContext()
|
||||
.withFunctions([functionData])
|
||||
.withValidator(validator.get())
|
||||
.parseFunctions();
|
||||
// assert
|
||||
validator.assertValidatedCodes(expectedCodes);
|
||||
});
|
||||
it('applies correct validation rules', () => {
|
||||
// arrange
|
||||
const expectedRules: readonly CodeValidationRule[] = [
|
||||
CodeValidationRule.NoEmptyLines,
|
||||
CodeValidationRule.NoDuplicatedLines,
|
||||
];
|
||||
const functionData = createFunctionDataWithCode();
|
||||
const validator = new CodeValidatorStub();
|
||||
// act
|
||||
new TestContext()
|
||||
.withFunctions([functionData])
|
||||
.withValidator(validator.get())
|
||||
.parseFunctions();
|
||||
// assert
|
||||
validator.assertValidatedRules(expectedRules);
|
||||
});
|
||||
it('validates for correct scripting language', () => {
|
||||
// arrange
|
||||
const expectedLanguage: ScriptingLanguage = ScriptingLanguage.shellscript;
|
||||
const functionData = createFunctionDataWithCode();
|
||||
const validator = new CodeValidatorStub();
|
||||
// act
|
||||
new TestContext()
|
||||
.withFunctions([functionData])
|
||||
.withValidator(validator.get())
|
||||
.withLanguage(expectedLanguage)
|
||||
.parseFunctions();
|
||||
// assert
|
||||
validator.assertValidatedLanguage(expectedLanguage);
|
||||
});
|
||||
});
|
||||
describe('parameter creation', () => {
|
||||
@@ -406,9 +435,10 @@ describe('SharedFunctionsParser', () => {
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
private language: ScriptingLanguage = ScriptingLanguage.batchfile;
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
private codeValidator: CodeValidator = new CodeValidatorStub()
|
||||
.get();
|
||||
|
||||
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
|
||||
|
||||
@@ -421,12 +451,12 @@ class TestContext {
|
||||
private parameterCollectionFactory
|
||||
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
|
||||
|
||||
public withSyntax(syntax: ILanguageSyntax): this {
|
||||
this.syntax = syntax;
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withValidator(codeValidator: ICodeValidator): this {
|
||||
public withValidator(codeValidator: CodeValidator): this {
|
||||
this.codeValidator = codeValidator;
|
||||
return this;
|
||||
}
|
||||
@@ -461,7 +491,7 @@ class TestContext {
|
||||
public parseFunctions(): ReturnType<typeof parseSharedFunctions> {
|
||||
return parseSharedFunctions(
|
||||
this.functions,
|
||||
this.syntax,
|
||||
this.language,
|
||||
{
|
||||
codeValidator: this.codeValidator,
|
||||
wrapError: this.wrapError,
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler';
|
||||
import type { FunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub';
|
||||
import { createSharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub';
|
||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||
import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||
import type { SharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
|
||||
|
||||
describe('ScriptCompiler', () => {
|
||||
describe('canCompile', () => {
|
||||
it('returns true if "call" is defined', () => {
|
||||
// arrange
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
const script = createScriptDataWithCall();
|
||||
// act
|
||||
const actual = sut.canCompile(script);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('returns false if "call" is undefined', () => {
|
||||
// arrange
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
const script = createScriptDataWithCode();
|
||||
// act
|
||||
const actual = sut.canCompile(script);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('compile', () => {
|
||||
it('throws if script does not have body', () => {
|
||||
// arrange
|
||||
const expectedError = 'Script does include any calls.';
|
||||
const scriptData = createScriptDataWithCode();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('code construction', () => {
|
||||
it('returns code from the factory', () => {
|
||||
// arrange
|
||||
const expectedCode = new ScriptCodeStub();
|
||||
const scriptCodeFactory = () => expectedCode;
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.build();
|
||||
// act
|
||||
const actualCode = sut.compile(createScriptDataWithCall());
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
it('creates code correctly', () => {
|
||||
// arrange
|
||||
const expectedCode = 'expected-code';
|
||||
const expectedRevertCode = 'expected-revert-code';
|
||||
let actualCode: string | undefined;
|
||||
let actualRevertCode: string | undefined;
|
||||
const scriptCodeFactory = (code: string, revertCode: string) => {
|
||||
actualCode = code;
|
||||
actualRevertCode = revertCode;
|
||||
return new ScriptCodeStub();
|
||||
};
|
||||
const call = new FunctionCallDataStub();
|
||||
const script = createScriptDataWithCall(call);
|
||||
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||
const functionParserMock = createSharedFunctionsParserStub();
|
||||
functionParserMock.setup(functions, compiledFunctions);
|
||||
const callCompilerMock = new FunctionCallCompilerStub();
|
||||
callCompilerMock.setup(
|
||||
parseFunctionCalls(call),
|
||||
compiledFunctions,
|
||||
new CompiledCodeStub()
|
||||
.withCode(expectedCode)
|
||||
.withRevertCode(expectedRevertCode),
|
||||
);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.withSharedFunctionsParser(functionParserMock.parser)
|
||||
.withFunctionCallCompiler(callCompilerMock)
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.build();
|
||||
// act
|
||||
sut.compile(script);
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parses functions as expected', () => {
|
||||
it('parses functions with expected syntax', () => {
|
||||
// arrange
|
||||
const expected: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
const functionParserMock = createSharedFunctionsParserStub();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withSyntax(expected)
|
||||
.withSharedFunctionsParser(functionParserMock.parser)
|
||||
.build();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
const parserCalls = functionParserMock.callHistory;
|
||||
expect(parserCalls.length).to.equal(1);
|
||||
expect(parserCalls[0].syntax).to.equal(expected);
|
||||
});
|
||||
it('parses given functions', () => {
|
||||
// arrange
|
||||
const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const functionParserMock = createSharedFunctionsParserStub();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(...expectedFunctions)
|
||||
.withSharedFunctionsParser(functionParserMock.parser)
|
||||
.build();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
const parserCalls = functionParserMock.callHistory;
|
||||
expect(parserCalls.length).to.equal(1);
|
||||
expect(parserCalls[0].functions).to.deep.equal(expectedFunctions);
|
||||
});
|
||||
});
|
||||
describe('rethrows error with script name', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||
const expectedInnerError = new Error();
|
||||
const callCompiler: FunctionCallCompiler = {
|
||||
compileFunctionCalls: () => { throw expectedInnerError; },
|
||||
};
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withFunctionCallCompiler(callCompiler);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.build()
|
||||
.compile(scriptData);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows error from script code factory with script name', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||
const expectedInnerError = new Error();
|
||||
const scriptCodeFactory: ScriptCodeFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withScriptCodeFactory(scriptCodeFactory);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.build()
|
||||
.compile(scriptData);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
it('validates compiled code as expected', () => {
|
||||
// arrange
|
||||
const expectedRules = [
|
||||
NoEmptyLines,
|
||||
// Allow duplicated lines to enable calling same function multiple times
|
||||
];
|
||||
const expectedExecuteCode = 'execute code to be validated';
|
||||
const expectedRevertCode = 'revert code to be validated';
|
||||
const scriptData = createScriptDataWithCall();
|
||||
const validator = new CodeValidatorStub();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withCodeValidator(validator)
|
||||
.withFunctionCallCompiler(
|
||||
new FunctionCallCompilerStub()
|
||||
.withDefaultCompiledCode(
|
||||
new CompiledCodeStub()
|
||||
.withCode(expectedExecuteCode)
|
||||
.withRevertCode(expectedRevertCode),
|
||||
),
|
||||
)
|
||||
.build();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
validator.assertHistory({
|
||||
validatedCodes: [expectedExecuteCode, expectedRevertCode],
|
||||
rules: expectedRules,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ScriptCompilerBuilder {
|
||||
private static createFunctions(...names: string[]): FunctionData[] {
|
||||
return names.map((functionName) => {
|
||||
return createFunctionDataWithCode().withName(functionName);
|
||||
});
|
||||
}
|
||||
|
||||
private functions: FunctionData[] | undefined;
|
||||
|
||||
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
|
||||
private sharedFunctionsParser: SharedFunctionsParser = createSharedFunctionsParserStub().parser;
|
||||
|
||||
private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub();
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
|
||||
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||
|
||||
private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
|
||||
defaultCodePrefix: ScriptCompilerBuilder.name,
|
||||
});
|
||||
|
||||
public withFunctions(...functions: FunctionData[]): this {
|
||||
this.functions = functions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSomeFunctions(): this {
|
||||
this.functions = ScriptCompilerBuilder.createFunctions('test-function');
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionNames(...functionNames: string[]): this {
|
||||
this.functions = ScriptCompilerBuilder.createFunctions(...functionNames);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withEmptyFunctions(): this {
|
||||
this.functions = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSyntax(syntax: ILanguageSyntax): this {
|
||||
this.syntax = syntax;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSharedFunctionsParser(
|
||||
sharedFunctionsParser: SharedFunctionsParser,
|
||||
): this {
|
||||
this.sharedFunctionsParser = sharedFunctionsParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCodeValidator(
|
||||
codeValidator: ICodeValidator,
|
||||
): this {
|
||||
this.codeValidator = codeValidator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): this {
|
||||
this.callCompiler = callCompiler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||
this.wrapError = wrapError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
|
||||
this.scriptCodeFactory = scriptCodeFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): ScriptCompiler {
|
||||
if (!this.functions) {
|
||||
throw new Error('Function behavior not defined');
|
||||
}
|
||||
return new ScriptCompiler(
|
||||
{
|
||||
functions: this.functions,
|
||||
syntax: this.syntax,
|
||||
},
|
||||
{
|
||||
sharedFunctionsParser: this.sharedFunctionsParser,
|
||||
callCompiler: this.callCompiler,
|
||||
codeValidator: this.codeValidator,
|
||||
wrapError: this.wrapError,
|
||||
scriptCodeFactory: this.scriptCodeFactory,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { createScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory';
|
||||
import type { FunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub';
|
||||
import { createSharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub';
|
||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||
import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import type { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||
import type { SharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
|
||||
import type { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
||||
|
||||
describe('ScriptCompilerFactory', () => {
|
||||
describe('createScriptCompiler', () => {
|
||||
describe('canCompile', () => {
|
||||
it('returns true if "call" is defined', () => {
|
||||
// arrange
|
||||
const sut = new TestContext()
|
||||
.withEmptyFunctions()
|
||||
.create();
|
||||
const script = createScriptDataWithCall();
|
||||
// act
|
||||
const actual = sut.canCompile(script);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('returns false if "call" is undefined', () => {
|
||||
// arrange
|
||||
const sut = new TestContext()
|
||||
.withEmptyFunctions()
|
||||
.create();
|
||||
const script = createScriptDataWithCode();
|
||||
// act
|
||||
const actual = sut.canCompile(script);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('compile', () => {
|
||||
it('throws if script does not have body', () => {
|
||||
// arrange
|
||||
const expectedError = 'Script does include any calls.';
|
||||
const scriptData = createScriptDataWithCode();
|
||||
const sut = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.create();
|
||||
// act
|
||||
const act = () => sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('code construction', () => {
|
||||
it('returns code from the factory', () => {
|
||||
// arrange
|
||||
const expectedCode = new ScriptCodeStub();
|
||||
const scriptCodeFactory = () => expectedCode;
|
||||
const sut = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.create();
|
||||
// act
|
||||
const actualCode = sut.compile(createScriptDataWithCall());
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
it('creates code correctly', () => {
|
||||
// arrange
|
||||
const expectedCode = 'expected-code';
|
||||
const expectedRevertCode = 'expected-revert-code';
|
||||
let actualCode: string | undefined;
|
||||
let actualRevertCode: string | undefined;
|
||||
const scriptCodeFactory = (code: string, revertCode: string) => {
|
||||
actualCode = code;
|
||||
actualRevertCode = revertCode;
|
||||
return new ScriptCodeStub();
|
||||
};
|
||||
const call = new FunctionCallDataStub();
|
||||
const script = createScriptDataWithCall(call);
|
||||
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||
const functionParserMock = createSharedFunctionsParserStub();
|
||||
functionParserMock.setup(functions, compiledFunctions);
|
||||
const callCompilerMock = new FunctionCallCompilerStub();
|
||||
callCompilerMock.setup(
|
||||
parseFunctionCalls(call),
|
||||
compiledFunctions,
|
||||
new CompiledCodeStub()
|
||||
.withCode(expectedCode)
|
||||
.withRevertCode(expectedRevertCode),
|
||||
);
|
||||
const sut = new TestContext()
|
||||
.withFunctions(...functions)
|
||||
.withSharedFunctionsParser(functionParserMock.parser)
|
||||
.withFunctionCallCompiler(callCompilerMock)
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.create();
|
||||
// act
|
||||
sut.compile(script);
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
});
|
||||
});
|
||||
describe('parses functions as expected', () => {
|
||||
it('parses functions with expected language', () => {
|
||||
// arrange
|
||||
const expectedLanguage = ScriptingLanguage.batchfile;
|
||||
const functionParserMock = createSharedFunctionsParserStub();
|
||||
const sut = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withLanguage(expectedLanguage)
|
||||
.withSharedFunctionsParser(functionParserMock.parser)
|
||||
.create();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
const parserCalls = functionParserMock.callHistory;
|
||||
expect(parserCalls.length).to.equal(1);
|
||||
const actualLanguage = parserCalls[0].language;
|
||||
expect(actualLanguage).to.equal(expectedLanguage);
|
||||
});
|
||||
it('parses given functions', () => {
|
||||
// arrange
|
||||
const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const functionParserMock = createSharedFunctionsParserStub();
|
||||
const sut = new TestContext()
|
||||
.withFunctions(...expectedFunctions)
|
||||
.withSharedFunctionsParser(functionParserMock.parser)
|
||||
.create();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
const parserCalls = functionParserMock.callHistory;
|
||||
expect(parserCalls.length).to.equal(1);
|
||||
expect(parserCalls[0].functions).to.deep.equal(expectedFunctions);
|
||||
});
|
||||
});
|
||||
describe('rethrows error with script name', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||
const expectedInnerError = new Error();
|
||||
const callCompiler: FunctionCallCompiler = {
|
||||
compileFunctionCalls: () => { throw expectedInnerError; },
|
||||
};
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const builder = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withFunctionCallCompiler(callCompiler);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.create()
|
||||
.compile(scriptData);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows error from script code factory with script name', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||
const expectedInnerError = new Error();
|
||||
const scriptCodeFactory: ScriptCodeFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const builder = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withScriptCodeFactory(scriptCodeFactory);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.create()
|
||||
.compile(scriptData);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('compiled code validation', () => {
|
||||
it('validates compiled code', () => {
|
||||
// arrange
|
||||
const expectedExecuteCode = 'execute code to be validated';
|
||||
const expectedRevertCode = 'revert code to be validated';
|
||||
const scriptData = createScriptDataWithCall();
|
||||
const validator = new CodeValidatorStub();
|
||||
const sut = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withCodeValidator(validator.get())
|
||||
.withFunctionCallCompiler(
|
||||
new FunctionCallCompilerStub()
|
||||
.withDefaultCompiledCode(
|
||||
new CompiledCodeStub()
|
||||
.withCode(expectedExecuteCode)
|
||||
.withRevertCode(expectedRevertCode),
|
||||
),
|
||||
)
|
||||
.create();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
validator.assertValidatedCodes([
|
||||
expectedExecuteCode, expectedRevertCode,
|
||||
]);
|
||||
});
|
||||
it('applies correct validation rules', () => {
|
||||
// arrange
|
||||
const expectedRules: readonly CodeValidationRule[] = [
|
||||
CodeValidationRule.NoEmptyLines,
|
||||
CodeValidationRule.NoTooLongLines,
|
||||
// Allow duplicated lines to enable calling same function multiple times
|
||||
];
|
||||
const scriptData = createScriptDataWithCall();
|
||||
const validator = new CodeValidatorStub();
|
||||
const sut = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withCodeValidator(validator.get())
|
||||
.create();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
validator.assertValidatedRules(expectedRules);
|
||||
});
|
||||
it('validates for correct scripting language', () => {
|
||||
// arrange
|
||||
const expectedLanguage: ScriptingLanguage = ScriptingLanguage.shellscript;
|
||||
const scriptData = createScriptDataWithCall();
|
||||
const validator = new CodeValidatorStub();
|
||||
const sut = new TestContext()
|
||||
.withSomeFunctions()
|
||||
.withLanguage(expectedLanguage)
|
||||
.withCodeValidator(validator.get())
|
||||
.create();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
validator.assertValidatedLanguage(expectedLanguage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private static createFunctions(...names: string[]): FunctionData[] {
|
||||
return names.map((functionName) => {
|
||||
return createFunctionDataWithCode().withName(functionName);
|
||||
});
|
||||
}
|
||||
|
||||
private functions: FunctionData[] | undefined;
|
||||
|
||||
private language: ScriptingLanguage = ScriptingLanguage.batchfile;
|
||||
|
||||
private sharedFunctionsParser: SharedFunctionsParser = createSharedFunctionsParserStub().parser;
|
||||
|
||||
private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub();
|
||||
|
||||
private codeValidator: CodeValidator = new CodeValidatorStub()
|
||||
.get();
|
||||
|
||||
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||
|
||||
private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
|
||||
defaultCodePrefix: TestContext.name,
|
||||
});
|
||||
|
||||
public withFunctions(...functions: FunctionData[]): this {
|
||||
this.functions = functions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSomeFunctions(): this {
|
||||
this.functions = TestContext.createFunctions('test-function');
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionNames(...functionNames: string[]): this {
|
||||
this.functions = TestContext.createFunctions(...functionNames);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withEmptyFunctions(): this {
|
||||
this.functions = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSharedFunctionsParser(
|
||||
sharedFunctionsParser: SharedFunctionsParser,
|
||||
): this {
|
||||
this.sharedFunctionsParser = sharedFunctionsParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCodeValidator(
|
||||
codeValidator: CodeValidator,
|
||||
): this {
|
||||
this.codeValidator = codeValidator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): this {
|
||||
this.callCompiler = callCompiler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||
this.wrapError = wrapError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
|
||||
this.scriptCodeFactory = scriptCodeFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public create(): ScriptCompiler {
|
||||
if (!this.functions) {
|
||||
throw new Error('Function behavior not defined');
|
||||
}
|
||||
return createScriptCompiler({
|
||||
categoryContext: {
|
||||
functions: this.functions,
|
||||
language: this.language,
|
||||
},
|
||||
utilities: {
|
||||
sharedFunctionsParser: this.sharedFunctionsParser,
|
||||
callCompiler: this.callCompiler,
|
||||
codeValidator: this.codeValidator,
|
||||
wrapError: this.wrapError,
|
||||
scriptCodeFactory: this.scriptCodeFactory,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,9 @@ import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub'
|
||||
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import type { EnumParser } from '@/application/Common/Enum';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import type { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
||||
@@ -26,11 +23,13 @@ import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||
import { CategoryCollectionContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionContextStub';
|
||||
import type { CategoryCollectionContext } from '@/application/Parser/Executable/CategoryCollectionContext';
|
||||
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import type { ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
||||
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
|
||||
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
|
||||
|
||||
@@ -330,13 +329,13 @@ describe('ScriptParser', () => {
|
||||
const script = createScriptDataWithCode();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script, expectedCode);
|
||||
const collectionUtilities = new CategoryCollectionSpecificUtilitiesStub()
|
||||
const collectionContext = new CategoryCollectionContextStub()
|
||||
.withCompiler(compiler);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withData(script)
|
||||
.withCollectionUtilities(collectionUtilities)
|
||||
.withCollectionContext(collectionContext)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
@@ -344,33 +343,12 @@ describe('ScriptParser', () => {
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
});
|
||||
describe('syntax', () => {
|
||||
it('set from the context', () => { // tests through script validation logic
|
||||
// arrange
|
||||
const commentDelimiter = 'should not throw';
|
||||
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
||||
const collectionUtilities = new CategoryCollectionSpecificUtilitiesStub()
|
||||
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
|
||||
const script = createScriptDataWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode);
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withData(script)
|
||||
.withCollectionUtilities(collectionUtilities);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('validates a expected', () => {
|
||||
it('validates script with inline code (that is not compiled)', () => {
|
||||
// arrange
|
||||
const expectedRules = [
|
||||
NoEmptyLines,
|
||||
NoDuplicatedLines,
|
||||
];
|
||||
const expectedCode = 'expected code to be validated';
|
||||
const expectedRevertCode = 'expected revert code to be validated';
|
||||
const expectedCodeCalls = [
|
||||
const expectedCodeCalls: readonly string[] = [
|
||||
expectedCode,
|
||||
expectedRevertCode,
|
||||
];
|
||||
@@ -383,35 +361,55 @@ describe('ScriptParser', () => {
|
||||
// act
|
||||
new TestContext()
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.withCodeValidator(validator)
|
||||
.withCodeValidator(validator.get())
|
||||
.parseScript();
|
||||
// assert
|
||||
validator.assertHistory({
|
||||
validatedCodes: expectedCodeCalls,
|
||||
rules: expectedRules,
|
||||
});
|
||||
validator.assertValidatedCodes(expectedCodeCalls);
|
||||
});
|
||||
it('does not validate compiled code', () => {
|
||||
// arrange
|
||||
const expectedRules = [];
|
||||
const expectedCodeCalls = [];
|
||||
const validator = new CodeValidatorStub();
|
||||
const script = createScriptDataWithCall();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script, new ScriptCodeStub());
|
||||
const collectionUtilities = new CategoryCollectionSpecificUtilitiesStub()
|
||||
const collectionContext = new CategoryCollectionContextStub()
|
||||
.withCompiler(compiler);
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(script)
|
||||
.withCodeValidator(validator)
|
||||
.withCollectionUtilities(collectionUtilities)
|
||||
.withCodeValidator(validator.get())
|
||||
.withCollectionContext(collectionContext)
|
||||
.parseScript();
|
||||
// assert
|
||||
validator.assertHistory({
|
||||
validatedCodes: expectedCodeCalls,
|
||||
rules: expectedRules,
|
||||
});
|
||||
const calls = validator.callHistory;
|
||||
expect(calls).to.have.lengthOf(0);
|
||||
});
|
||||
it('validates with correct rules', () => {
|
||||
const expectedRules: readonly CodeValidationRule[] = [
|
||||
CodeValidationRule.NoEmptyLines,
|
||||
CodeValidationRule.NoDuplicatedLines,
|
||||
CodeValidationRule.NoTooLongLines,
|
||||
];
|
||||
const validator = new CodeValidatorStub();
|
||||
// act
|
||||
new TestContext()
|
||||
.withCodeValidator(validator.get())
|
||||
.parseScript();
|
||||
// assert
|
||||
validator.assertValidatedRules(expectedRules);
|
||||
});
|
||||
it('validates with correct language', () => {
|
||||
const expectedLanguage: ScriptingLanguage = ScriptingLanguage.batchfile;
|
||||
const validator = new CodeValidatorStub();
|
||||
const collectionContext = new CategoryCollectionContextStub()
|
||||
.withLanguage(expectedLanguage);
|
||||
// act
|
||||
new TestContext()
|
||||
.withCodeValidator(validator.get())
|
||||
.withCollectionContext(collectionContext)
|
||||
.parseScript();
|
||||
// assert
|
||||
validator.assertValidatedLanguage(expectedLanguage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -461,15 +459,15 @@ describe('ScriptParser', () => {
|
||||
class TestContext {
|
||||
private data: ScriptData = createScriptDataWithCode();
|
||||
|
||||
private collectionUtilities
|
||||
: CategoryCollectionSpecificUtilities = new CategoryCollectionSpecificUtilitiesStub();
|
||||
private collectionContext
|
||||
: CategoryCollectionContext = new CategoryCollectionContextStub();
|
||||
|
||||
private levelParser: EnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||
.setupDefaultValue(RecommendationLevel.Standard);
|
||||
|
||||
private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy;
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
private codeValidator: CodeValidator = new CodeValidatorStub().get();
|
||||
|
||||
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||
|
||||
@@ -481,7 +479,7 @@ class TestContext {
|
||||
defaultCodePrefix: TestContext.name,
|
||||
});
|
||||
|
||||
public withCodeValidator(codeValidator: ICodeValidator): this {
|
||||
public withCodeValidator(codeValidator: CodeValidator): this {
|
||||
this.codeValidator = codeValidator;
|
||||
return this;
|
||||
}
|
||||
@@ -491,10 +489,10 @@ class TestContext {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCollectionUtilities(
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
public withCollectionContext(
|
||||
collectionContext: CategoryCollectionContext,
|
||||
): this {
|
||||
this.collectionUtilities = collectionUtilities;
|
||||
this.collectionContext = collectionContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -531,7 +529,7 @@ class TestContext {
|
||||
public parseScript(): ReturnType<typeof parseScript> {
|
||||
return parseScript(
|
||||
this.data,
|
||||
this.collectionUtilities,
|
||||
this.collectionContext,
|
||||
{
|
||||
levelParser: this.levelParser,
|
||||
createScript: this.scriptFactory,
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { describe } from 'vitest';
|
||||
import { analyzeDuplicateLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import type { SyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/SyntaxFactory';
|
||||
import { SyntaxFactoryStub } from '@tests/unit/shared/Stubs/SyntaxFactoryStub';
|
||||
import { createCodeLines } from './CreateCodeLines';
|
||||
import { expectSameInvalidCodeLines } from './ExpectSameInvalidCodeLines';
|
||||
|
||||
describe('AnalyzeDuplicateLines', () => {
|
||||
describe('analyzeDuplicateLines', () => {
|
||||
it('returns no results for unique lines', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([]);
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'unique1', /* 2 */ 'unique2', /* 3 */ 'unique3', /* 4 */ 'unique4',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies single duplicated line', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([1, 2, 4]);
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'duplicate', /* 2 */ 'duplicate', /* 3 */ 'unique', /* 4 */ 'duplicate',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies multiple duplicated lines', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([1, 4], [2, 6]);
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'duplicate1', /* 2 */ 'duplicate2', /* 3 */ 'unique',
|
||||
/* 4 */ 'duplicate1', /* 5 */ 'unique2', /* 6 */ 'duplicate2',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
describe('syntax handling', () => {
|
||||
it('uses correct language for syntax creation', () => {
|
||||
// arrange
|
||||
const expectedLanguage = ScriptingLanguage.batchfile;
|
||||
let actualLanguage: ScriptingLanguage | undefined;
|
||||
const factory: SyntaxFactory = (language) => {
|
||||
actualLanguage = language;
|
||||
return new LanguageSyntaxStub();
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withLanguage(expectedLanguage)
|
||||
.withSyntaxFactory(factory);
|
||||
// act
|
||||
context.analyze();
|
||||
// assert
|
||||
expect(actualLanguage).to.equal(expectedLanguage);
|
||||
});
|
||||
describe('common code parts', () => {
|
||||
it('ignores multiple occurrences of common code parts', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([3, 4]);
|
||||
const syntax = new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('good', 'also-good');
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'good', /* 2 */ 'good', /* 3 */ 'bad', /* 4 */ 'bad',
|
||||
/* 5 */ 'good', /* 6 */ 'also-good', /* 7 */ 'also-good', /* 8 */ 'unique',
|
||||
])
|
||||
.withSyntaxFactory(() => syntax);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('ignores common code parts used in same line', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([1, 2]);
|
||||
const syntax = new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('good2', 'good1');
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'bad', /* 2 */ 'bad', /* 3 */ 'good1 good2',
|
||||
/* 4 */ 'good1 good2', /* 5 */ 'good2 good1', /* 6 */ 'good2 good1',
|
||||
])
|
||||
.withSyntaxFactory(() => syntax);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('detects duplicates with common parts and unique words', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([4, 5], [8, 9]);
|
||||
const syntax = new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('common-part1', 'common-part2');
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'common-part1', /* 2 */ 'common-part1', /* 3 */ 'common-part1 common-part2',
|
||||
/* 4 */ 'common-part1 unique', /* 5 */ 'common-part1 unique', /* 6 */ 'common-part2',
|
||||
/* 7 */ 'common-part2 common-part1', /* 8 */ 'unique common-part2', /* 9 */ 'unique common-part2',
|
||||
])
|
||||
.withSyntaxFactory(() => syntax);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
});
|
||||
describe('comment handling', () => {
|
||||
it('ignores lines starting with comment delimiters', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([3, 5]);
|
||||
const syntax = new LanguageSyntaxStub()
|
||||
.withCommentDelimiters('#', '//');
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ '#abc', /* 2 */ '#abc', /* 3 */ 'abc', /* 4 */ 'unique',
|
||||
/* 5 */ 'abc', /* 6 */ '//abc', /* 7 */ '//abc', /* 8 */ '//unique',
|
||||
/* 9 */ '#unique',
|
||||
])
|
||||
.withSyntaxFactory(() => syntax);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('detects duplicates when comments are not at line start', () => {
|
||||
// arrange
|
||||
const expected = createExpectedDuplicateLineErrors([1, 2], [3, 4]);
|
||||
const syntax = new LanguageSyntaxStub()
|
||||
.withCommentDelimiters('#');
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'test #comment', /* 2 */ 'test #comment', /* 3 */ 'test2 # comment',
|
||||
/* 4 */ 'test2 # comment',
|
||||
])
|
||||
.withSyntaxFactory(() => syntax);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createExpectedDuplicateLineErrors(
|
||||
...lines: readonly ReadonlyArray<number>[]
|
||||
): InvalidCodeLine[] {
|
||||
return lines.flatMap((occurrenceIndices): readonly InvalidCodeLine[] => occurrenceIndices
|
||||
.map((index): InvalidCodeLine => ({
|
||||
lineNumber: index,
|
||||
error: `Line is duplicated at line numbers ${occurrenceIndices.join(',')}.`,
|
||||
})));
|
||||
}
|
||||
|
||||
export class TestContext {
|
||||
private codeLines: readonly CodeLine[] = createCodeLines(['test-code-line']);
|
||||
|
||||
private language = ScriptingLanguage.batchfile;
|
||||
|
||||
private syntaxFactory: SyntaxFactory = new SyntaxFactoryStub().get();
|
||||
|
||||
public withLines(lines: readonly string[]): this {
|
||||
this.codeLines = createCodeLines(lines);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSyntaxFactory(syntaxFactory: SyntaxFactory): this {
|
||||
this.syntaxFactory = syntaxFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public analyze() {
|
||||
return analyzeDuplicateLines(
|
||||
this.codeLines,
|
||||
this.language,
|
||||
this.syntaxFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe } from 'vitest';
|
||||
import { analyzeEmptyLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeEmptyLines';
|
||||
import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { createCodeLines } from './CreateCodeLines';
|
||||
import { expectSameInvalidCodeLines } from './ExpectSameInvalidCodeLines';
|
||||
|
||||
describe('AnalyzeEmptyLines', () => {
|
||||
describe('analyzeEmptyLines', () => {
|
||||
it('returns no results for non-empty lines', () => {
|
||||
// arrange
|
||||
const expected: InvalidCodeLine[] = [];
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'non-empty-line1', /* 2 */ 'none-empty-line2',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies single empty line', () => {
|
||||
// arrange
|
||||
const expected: InvalidCodeLine[] = [
|
||||
{ lineNumber: 2, error: 'Empty line' },
|
||||
];
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'first line', /* 2 */ '', /* 3 */ 'third line',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies multiple empty lines', () => {
|
||||
// arrange
|
||||
const expected: InvalidCodeLine[] = [2, 4].map((index): InvalidCodeLine => ({ lineNumber: index, error: 'Empty line' }));
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'first line', /* 2 */ '', /* 3 */ 'third line', /* 4 */ '',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies lines with only spaces', () => {
|
||||
// arrange
|
||||
const expected: InvalidCodeLine[] = [
|
||||
{ lineNumber: 2, error: 'Empty line: "{whitespace}{whitespace}"' },
|
||||
];
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'first line', /* 2 */ ' ', /* 3 */ 'third line',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies lines with only tabs', () => {
|
||||
// arrange
|
||||
const expected: InvalidCodeLine[] = [
|
||||
{ lineNumber: 2, error: 'Empty line: "{tab}{tab}"' },
|
||||
];
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'first line', /* 2 */ '\t\t', /* 3 */ 'third line',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
it('identifies lines with mixed spaces and tabs', () => {
|
||||
// arrange
|
||||
const expected: InvalidCodeLine[] = [
|
||||
{ lineNumber: 2, error: 'Empty line: "{tab}{whitespace}{tab}"' },
|
||||
{ lineNumber: 4, error: 'Empty line: "{whitespace}{tab}{whitespace}"' },
|
||||
];
|
||||
const context = new TestContext()
|
||||
.withLines([
|
||||
/* 1 */ 'first line', /* 2 */ '\t \t', /* 3 */ 'third line', /* 4 */ ' \t ',
|
||||
]);
|
||||
// act
|
||||
const actual = context.analyze();
|
||||
// assert
|
||||
expectSameInvalidCodeLines(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export class TestContext {
|
||||
private codeLines: readonly CodeLine[] = createCodeLines(['test-code-line']);
|
||||
|
||||
private language = ScriptingLanguage.batchfile;
|
||||
|
||||
public withLines(lines: readonly string[]): this {
|
||||
this.codeLines = createCodeLines(lines);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public analyze() {
|
||||
return analyzeEmptyLines(
|
||||
this.codeLines,
|
||||
this.language,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe } from 'vitest';
|
||||
import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { analyzeTooLongLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines';
|
||||
import { createCodeLines } from './CreateCodeLines';
|
||||
import { 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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('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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error for unsupported language', () => {
|
||||
// arrange
|
||||
const context = new TestContext()
|
||||
.withLanguage('unsupported' as unknown as ScriptingLanguage)
|
||||
.withLines(['A', 'B', 'C']);
|
||||
// act & assert
|
||||
expect(() => context.analyze()).to.throw('Unsupported language: unsupported');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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(' ');
|
||||
}
|
||||
|
||||
class TestContext {
|
||||
private codeLines: readonly CodeLine[] = createCodeLines(['test-code-line']);
|
||||
|
||||
private language = ScriptingLanguage.batchfile;
|
||||
|
||||
public withLines(lines: readonly string[]): this {
|
||||
this.codeLines = createCodeLines(lines);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public analyze() {
|
||||
return analyzeTooLongLines(
|
||||
this.codeLines,
|
||||
this.language,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { CodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
|
||||
export function createCodeLines(lines: readonly string[]): CodeLine[] {
|
||||
return lines.map((lineText, index): CodeLine => (
|
||||
{
|
||||
lineNumber: index + 1,
|
||||
text: lineText,
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect } from 'vitest';
|
||||
import type { InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
|
||||
export function expectSameInvalidCodeLines(
|
||||
expected: readonly InvalidCodeLine[],
|
||||
actual: readonly InvalidCodeLine[],
|
||||
) {
|
||||
expect(sort(expected)).to.deep.equal(sort(actual));
|
||||
}
|
||||
|
||||
function sort(lines: readonly InvalidCodeLine[]) { // To ignore order
|
||||
return Array.from(lines).sort((a, b) => a.lineNumber - b.lineNumber);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe } from 'vitest';
|
||||
import { BatchFileSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/BatchFileSyntax';
|
||||
import { runLanguageSyntaxTests } from './LanguageSyntaxTestRunner';
|
||||
|
||||
describe('BatchFileSyntax', () => {
|
||||
runLanguageSyntaxTests(
|
||||
() => new BatchFileSyntax(),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
||||
|
||||
export function runLanguageSyntaxTests(createSyntax: () => LanguageSyntax) {
|
||||
describe('commentDelimiters', () => {
|
||||
it('returns defined value', () => {
|
||||
// arrange
|
||||
const sut = createSyntax();
|
||||
// act
|
||||
const value = sut.commentDelimiters;
|
||||
// assert
|
||||
expect(value);
|
||||
});
|
||||
});
|
||||
describe('commonCodeParts', () => {
|
||||
it('returns defined value', () => {
|
||||
// arrange
|
||||
const sut = createSyntax();
|
||||
// act
|
||||
const value = sut.commonCodeParts;
|
||||
// assert
|
||||
expect(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe } from 'vitest';
|
||||
import { ShellScriptSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/ShellScriptSyntax';
|
||||
import { runLanguageSyntaxTests } from './LanguageSyntaxTestRunner';
|
||||
|
||||
describe('ShellScriptSyntax', () => {
|
||||
runLanguageSyntaxTests(
|
||||
() => new ShellScriptSyntax(),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe } from 'vitest';
|
||||
import { createSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/SyntaxFactory';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ShellScriptSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/ShellScriptSyntax';
|
||||
import { BatchFileSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/BatchFileSyntax';
|
||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
||||
import type { Constructible } from '@/TypeHelpers';
|
||||
|
||||
describe('SyntaxFactory', () => {
|
||||
describe('createSyntax', () => {
|
||||
it('throws given invalid language', () => {
|
||||
// arrange
|
||||
const invalidLanguage = 5 as ScriptingLanguage;
|
||||
const expectedErrorMessage = `Invalid language: "${ScriptingLanguage[invalidLanguage]}"`;
|
||||
// act
|
||||
const act = () => createSyntax(invalidLanguage);
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
describe('creates syntax for supported languages', () => {
|
||||
const languageTestScenarios: Record<ScriptingLanguage, Constructible<LanguageSyntax>> = {
|
||||
[ScriptingLanguage.batchfile]: BatchFileSyntax,
|
||||
[ScriptingLanguage.shellscript]: ShellScriptSyntax,
|
||||
};
|
||||
Object.entries(languageTestScenarios).forEach(([key, value]) => {
|
||||
// arrange
|
||||
const scriptingLanguage = Number(key) as ScriptingLanguage;
|
||||
const expectedType = value;
|
||||
it(`gets correct type for "${ScriptingLanguage[scriptingLanguage]}" language`, () => {
|
||||
// act
|
||||
const syntax = createSyntax(scriptingLanguage);
|
||||
// assert
|
||||
expect(syntax).to.be.instanceOf(expectedType);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,182 +1,226 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub';
|
||||
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||
import type { ICodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeLine';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule';
|
||||
import { CodeValidationAnalyzerStub } from '@tests/unit/shared/Stubs/CodeValidationAnalyzerStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
import { validateCode } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
||||
import type { ValidationRuleAnalyzerFactory } from '@/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory';
|
||||
|
||||
describe('CodeValidator', () => {
|
||||
describe('instance', () => {
|
||||
itIsSingletonFactory({
|
||||
getter: () => CodeValidator.instance,
|
||||
expectedType: CodeValidator,
|
||||
describe('validateCode', () => {
|
||||
describe('does not throw if code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const code = absentValue;
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withCode(code)
|
||||
.validate();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('line splitting', () => {
|
||||
it('supports all line separators', () => {
|
||||
// arrange
|
||||
const expectedLineTexts = ['line1', 'line2', 'line3', 'line4'];
|
||||
const code = 'line1\r\nline2\rline3\nline4';
|
||||
const analyzer = new CodeValidationAnalyzerStub();
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
||||
// act
|
||||
new TestContext()
|
||||
.withCode(code)
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// expect
|
||||
expect(analyzer.receivedLines).has.lengthOf(1);
|
||||
const actualLineTexts = analyzer.receivedLines[0].map((line) => line.text);
|
||||
expect(actualLineTexts).to.deep.equal(expectedLineTexts);
|
||||
});
|
||||
it('uses 1-indexed line numbering', () => {
|
||||
// arrange
|
||||
const expectedLineNumbers = [1, 2, 3];
|
||||
const code = ['line1', 'line2', 'line3'].join('\n');
|
||||
const analyzer = new CodeValidationAnalyzerStub();
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
||||
// act
|
||||
new TestContext()
|
||||
.withCode(code)
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// expect
|
||||
expect(analyzer.receivedLines).has.lengthOf(1);
|
||||
const actualLineIndexes = analyzer.receivedLines[0].map((line) => line.lineNumber);
|
||||
expect(actualLineIndexes).to.deep.equal(expectedLineNumbers);
|
||||
});
|
||||
it('includes empty lines in count', () => {
|
||||
// arrange
|
||||
const expectedEmptyLineCount = 4;
|
||||
const code = '\n'.repeat(expectedEmptyLineCount - 1);
|
||||
const analyzer = new CodeValidationAnalyzerStub();
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
||||
// act
|
||||
new TestContext()
|
||||
.withCode(code)
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// expect
|
||||
expect(analyzer.receivedLines).has.lengthOf(1);
|
||||
const actualLines = analyzer.receivedLines[0];
|
||||
expect(actualLines).to.have.lengthOf(expectedEmptyLineCount);
|
||||
});
|
||||
it('correctly matches line numbers with text', () => {
|
||||
// arrange
|
||||
const expected: readonly CodeLine[] = [
|
||||
{ lineNumber: 1, text: 'first' },
|
||||
{ lineNumber: 2, text: 'second' },
|
||||
];
|
||||
const code = expected.map((line) => line.text).join('\n');
|
||||
const analyzer = new CodeValidationAnalyzerStub();
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
||||
// act
|
||||
new TestContext()
|
||||
.withCode(code)
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// expect
|
||||
expect(analyzer.receivedLines).has.lengthOf(1);
|
||||
expect(analyzer.receivedLines[0]).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('throwIfInvalid', () => {
|
||||
describe('does not throw if code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const code = absentValue;
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
const act = () => sut.throwIfInvalid(code, [new CodeValidationRuleStub()]);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
it('analyzes lines for correct language', () => {
|
||||
// arrange
|
||||
const expectedLanguage = ScriptingLanguage.batchfile;
|
||||
const analyzers = [
|
||||
new CodeValidationAnalyzerStub(),
|
||||
new CodeValidationAnalyzerStub(),
|
||||
new CodeValidationAnalyzerStub(),
|
||||
];
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => analyzers.map((s) => s.get());
|
||||
// act
|
||||
new TestContext()
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// assert
|
||||
const actualLanguages = analyzers.flatMap((a) => a.receivedLanguages);
|
||||
const unexpectedLanguages = actualLanguages.filter((l) => l !== expectedLanguage);
|
||||
expect(unexpectedLanguages).to.have.lengthOf(0);
|
||||
});
|
||||
describe('throwing invalid lines', () => {
|
||||
it('throws error for invalid line from single rule', () => {
|
||||
// arrange
|
||||
const errorText = 'error';
|
||||
const expectedError = constructExpectedValidationErrorMessage([
|
||||
{ text: 'line1' },
|
||||
{ text: 'line2', error: errorText },
|
||||
{ text: 'line3' },
|
||||
{ text: 'line4' },
|
||||
]);
|
||||
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
||||
const invalidLines: readonly InvalidCodeLine[] = [
|
||||
{ lineNumber: 2, error: errorText },
|
||||
];
|
||||
const invalidAnalyzer = new CodeValidationAnalyzerStub()
|
||||
.withReturnValue(invalidLines);
|
||||
const noopAnalyzer = new CodeValidationAnalyzerStub()
|
||||
.withReturnValue([]);
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [
|
||||
invalidAnalyzer, noopAnalyzer,
|
||||
].map((s) => s.get());
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withCode(code)
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws if rules are empty', () => {
|
||||
itEachAbsentCollectionValue<ICodeValidationRule>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing rules';
|
||||
const rules = absentValue;
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
const act = () => sut.throwIfInvalid('code', rules);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
describe('splits lines as expected', () => {
|
||||
it('supports all line separators', () => {
|
||||
// arrange
|
||||
const expectedLineTexts = ['line1', 'line2', 'line3', 'line4'];
|
||||
const code = 'line1\r\nline2\rline3\nline4';
|
||||
const spy = new CodeValidationRuleStub();
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
sut.throwIfInvalid(code, [spy]);
|
||||
// expect
|
||||
expect(spy.receivedLines).has.lengthOf(1);
|
||||
const actualLineTexts = spy.receivedLines[0].map((line) => line.text);
|
||||
expect(actualLineTexts).to.deep.equal(expectedLineTexts);
|
||||
});
|
||||
it('uses 1-indexed line numbering', () => {
|
||||
// arrange
|
||||
const expectedIndexes = [1, 2, 3];
|
||||
const code = ['line1', 'line2', 'line3'].join('\n');
|
||||
const spy = new CodeValidationRuleStub();
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
sut.throwIfInvalid(code, [spy]);
|
||||
// expect
|
||||
expect(spy.receivedLines).has.lengthOf(1);
|
||||
const actualLineIndexes = spy.receivedLines[0].map((line) => line.index);
|
||||
expect(actualLineIndexes).to.deep.equal(expectedIndexes);
|
||||
});
|
||||
it('counts empty lines', () => {
|
||||
// arrange
|
||||
const expectedTotalEmptyLines = 4;
|
||||
const code = '\n'.repeat(expectedTotalEmptyLines - 1);
|
||||
const spy = new CodeValidationRuleStub();
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
sut.throwIfInvalid(code, [spy]);
|
||||
// expect
|
||||
expect(spy.receivedLines).has.lengthOf(1);
|
||||
const actualLines = spy.receivedLines[0];
|
||||
expect(actualLines).to.have.lengthOf(expectedTotalEmptyLines);
|
||||
});
|
||||
it('matches texts with indexes as expected', () => {
|
||||
// arrange
|
||||
const expected: readonly ICodeLine[] = [
|
||||
{ index: 1, text: 'first' },
|
||||
{ index: 2, text: 'second' },
|
||||
];
|
||||
const code = expected.map((line) => line.text).join('\n');
|
||||
const spy = new CodeValidationRuleStub();
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
sut.throwIfInvalid(code, [spy]);
|
||||
// expect
|
||||
expect(spy.receivedLines).has.lengthOf(1);
|
||||
expect(spy.receivedLines[0]).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('throws invalid lines as expected', () => {
|
||||
it('throws with invalid line from single rule', () => {
|
||||
// arrange
|
||||
const errorText = 'error';
|
||||
const expectedError = new ExpectedErrorBuilder()
|
||||
.withOkLine('line1')
|
||||
.withErrorLine('line2', errorText)
|
||||
.withOkLine('line3')
|
||||
.withOkLine('line4')
|
||||
.buildError();
|
||||
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
||||
const invalidLines: readonly IInvalidCodeLine[] = [
|
||||
{ index: 2, error: errorText },
|
||||
];
|
||||
const rule = new CodeValidationRuleStub()
|
||||
.withReturnValue(invalidLines);
|
||||
const noopRule = new CodeValidationRuleStub()
|
||||
.withReturnValue([]);
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
const act = () => sut.throwIfInvalid(code, [rule, noopRule]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws with combined invalid lines from multiple rules', () => {
|
||||
// arrange
|
||||
const firstError = 'firstError';
|
||||
const secondError = 'firstError';
|
||||
const expectedError = new ExpectedErrorBuilder()
|
||||
.withOkLine('line1')
|
||||
.withErrorLine('line2', firstError)
|
||||
.withOkLine('line3')
|
||||
.withErrorLine('line4', secondError)
|
||||
.buildError();
|
||||
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
||||
const firstRuleError: readonly IInvalidCodeLine[] = [
|
||||
{ index: 2, error: firstError },
|
||||
];
|
||||
const secondRuleError: readonly IInvalidCodeLine[] = [
|
||||
{ index: 4, error: secondError },
|
||||
];
|
||||
const firstRule = new CodeValidationRuleStub().withReturnValue(firstRuleError);
|
||||
const secondRule = new CodeValidationRuleStub().withReturnValue(secondRuleError);
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
const act = () => sut.throwIfInvalid(code, [firstRule, secondRule]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws error with combined invalid lines from multiple rules', () => {
|
||||
// arrange
|
||||
const firstError = 'firstError';
|
||||
const secondError = 'firstError';
|
||||
const expectedError = constructExpectedValidationErrorMessage([
|
||||
{ text: 'line1' },
|
||||
{ text: 'line2', error: firstError },
|
||||
{ text: 'line3' },
|
||||
{ text: 'line4', error: secondError },
|
||||
]);
|
||||
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
||||
const firstRuleError: readonly InvalidCodeLine[] = [
|
||||
{ lineNumber: 2, error: firstError },
|
||||
];
|
||||
const secondRuleError: readonly InvalidCodeLine[] = [
|
||||
{ lineNumber: 4, error: secondError },
|
||||
];
|
||||
const firstRule = new CodeValidationAnalyzerStub().withReturnValue(firstRuleError);
|
||||
const secondRule = new CodeValidationAnalyzerStub().withReturnValue(secondRuleError);
|
||||
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [
|
||||
firstRule, secondRule,
|
||||
].map((s) => s.get());
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withCode(code)
|
||||
.withAnalyzerFactory(analyzerFactory)
|
||||
.validate();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ExpectedErrorBuilder {
|
||||
private lineCount = 0;
|
||||
function constructExpectedValidationErrorMessage(
|
||||
lines: readonly {
|
||||
readonly text: string,
|
||||
readonly error?: string,
|
||||
}[],
|
||||
): string {
|
||||
return [
|
||||
'Errors with the code.',
|
||||
...lines.flatMap((line, index): string[] => {
|
||||
const textPrefix = line.error ? '❌' : '✅';
|
||||
const lineNumber = `[${index + 1}]`;
|
||||
const formattedLine = `${lineNumber} ${textPrefix} ${line.text}`;
|
||||
return [
|
||||
formattedLine,
|
||||
...(line.error ? [indentText(`⟶ ${line.error}`)] : []),
|
||||
];
|
||||
}),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private outputLines = new Array<string>();
|
||||
class TestContext {
|
||||
private code = `[${TestContext.name}] code`;
|
||||
|
||||
public withOkLine(text: string) {
|
||||
return this.withNumberedLine(`✅ ${text}`);
|
||||
}
|
||||
private language: ScriptingLanguage = ScriptingLanguage.batchfile;
|
||||
|
||||
public withErrorLine(text: string, error: string) {
|
||||
return this
|
||||
.withNumberedLine(`❌ ${text}`)
|
||||
.withLine(indentText(`⟶ ${error}`));
|
||||
}
|
||||
private rules: readonly CodeValidationRule[] = [CodeValidationRule.NoDuplicatedLines];
|
||||
|
||||
public buildError(): string {
|
||||
return [
|
||||
'Errors with the code.',
|
||||
...this.outputLines,
|
||||
].join('\n');
|
||||
}
|
||||
private analyzerFactory: ValidationRuleAnalyzerFactory = () => [
|
||||
new CodeValidationAnalyzerStub().get(),
|
||||
];
|
||||
|
||||
private withLine(line: string) {
|
||||
this.outputLines.push(line);
|
||||
public withCode(code: string): this {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
private withNumberedLine(text: string) {
|
||||
this.lineCount += 1;
|
||||
const lineNumber = `[${this.lineCount}]`;
|
||||
return this.withLine(`${lineNumber} ${text}`);
|
||||
public withRules(rules: readonly CodeValidationRule[]): this {
|
||||
this.rules = rules;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAnalyzerFactory(analyzerFactory: ValidationRuleAnalyzerFactory): this {
|
||||
this.analyzerFactory = analyzerFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public validate(): ReturnType<typeof validateCode> {
|
||||
return validateCode(
|
||||
this.code,
|
||||
this.language,
|
||||
this.rules,
|
||||
this.analyzerFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { it, expect } from 'vitest';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule';
|
||||
import type { ICodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeLine';
|
||||
|
||||
interface ICodeValidationRuleTestCase {
|
||||
testName: string;
|
||||
codeLines: readonly string[];
|
||||
expected: readonly IInvalidCodeLine[];
|
||||
sut: ICodeValidationRule;
|
||||
}
|
||||
|
||||
export function testCodeValidationRule(testCases: readonly ICodeValidationRuleTestCase[]) {
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.testName, () => {
|
||||
// arrange
|
||||
const { sut } = testCase;
|
||||
const codeLines = createCodeLines(testCase.codeLines);
|
||||
// act
|
||||
const actual = sut.analyze(codeLines);
|
||||
// assert
|
||||
function sort(lines: readonly IInvalidCodeLine[]) { // To ignore order
|
||||
return Array.from(lines).sort((a, b) => a.index - b.index);
|
||||
}
|
||||
expect(sort(actual)).to.deep.equal(sort(testCase.expected));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createCodeLines(lines: readonly string[]): ICodeLine[] {
|
||||
return lines.map((lineText, index): ICodeLine => (
|
||||
{
|
||||
index: index + 1,
|
||||
text: lineText,
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { describe } from 'vitest';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import type { IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule';
|
||||
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||
|
||||
describe('NoDuplicatedLines', () => {
|
||||
describe('analyze', () => {
|
||||
testCodeValidationRule([
|
||||
{
|
||||
testName: 'no results when code is valid',
|
||||
codeLines: ['unique1', 'unique2', 'unique3', 'unique4'],
|
||||
expected: [],
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||
},
|
||||
{
|
||||
testName: 'detects single duplicated line as expected',
|
||||
codeLines: ['duplicate', 'duplicate', 'unique', 'duplicate'],
|
||||
expected: expectInvalidCodeLines([1, 2, 4]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||
},
|
||||
{
|
||||
testName: 'detects multiple duplicated lines as expected',
|
||||
codeLines: ['duplicate1', 'duplicate2', 'unique', 'duplicate1', 'unique2', 'duplicate2'],
|
||||
expected: expectInvalidCodeLines([1, 4], [2, 6]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||
},
|
||||
{
|
||||
testName: 'common code parts: does not detect multiple common code part usages as duplicates',
|
||||
codeLines: ['good', 'good', 'bad', 'bad', 'good', 'also-good', 'also-good', 'unique'],
|
||||
expected: expectInvalidCodeLines([3, 4]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('good', 'also-good')),
|
||||
},
|
||||
{
|
||||
testName: 'common code parts: does not detect multiple common code part used in same code line as duplicates',
|
||||
codeLines: ['bad', 'bad', 'good1 good2', 'good1 good2', 'good2 good1', 'good2 good1'],
|
||||
expected: expectInvalidCodeLines([1, 2]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('good2', 'good1')),
|
||||
},
|
||||
{
|
||||
testName: 'common code parts: detects when common code parts used in conjunction with unique words',
|
||||
codeLines: [
|
||||
'common-part1', 'common-part1', 'common-part1 common-part2', 'common-part1 unique', 'common-part1 unique',
|
||||
'common-part2', 'common-part2 common-part1', 'unique common-part2', 'unique common-part2',
|
||||
],
|
||||
expected: expectInvalidCodeLines([4, 5], [8, 9]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('common-part1', 'common-part2')),
|
||||
},
|
||||
{
|
||||
testName: 'comments: does not when lines start with comment',
|
||||
codeLines: ['#abc', '#abc', 'abc', 'unique', 'abc', '//abc', '//abc', '//unique', '#unique'],
|
||||
expected: expectInvalidCodeLines([3, 5]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommentDelimiters('#', '//')),
|
||||
},
|
||||
{
|
||||
testName: 'comments: does when comments come after lien start',
|
||||
codeLines: ['test #comment', 'test #comment', 'test2 # comment', 'test2 # comment'],
|
||||
expected: expectInvalidCodeLines([1, 2], [3, 4]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommentDelimiters('#')),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function expectInvalidCodeLines(
|
||||
...lines: readonly ReadonlyArray<number>[]
|
||||
): IInvalidCodeLine[] {
|
||||
return lines.flatMap((occurrenceIndices): readonly IInvalidCodeLine[] => occurrenceIndices
|
||||
.map((index): IInvalidCodeLine => ({
|
||||
index,
|
||||
error: `Line is duplicated at line numbers ${occurrenceIndices.join(',')}.`,
|
||||
})));
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe } from 'vitest';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||
|
||||
describe('NoEmptyLines', () => {
|
||||
describe('analyze', () => {
|
||||
testCodeValidationRule([
|
||||
{
|
||||
testName: 'no results when code is valid',
|
||||
codeLines: ['non-empty-line1', 'none-empty-line2'],
|
||||
expected: [],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for empty line',
|
||||
codeLines: ['first line', '', 'third line'],
|
||||
expected: [{ index: 2, error: 'Empty line' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for multiple empty lines',
|
||||
codeLines: ['first line', '', 'third line', ''],
|
||||
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for whitespace-only lines',
|
||||
codeLines: ['first line', ' ', 'third line'],
|
||||
expected: [{ index: 2, error: 'Empty line: "{whitespace}{whitespace}"' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for tab-only lines',
|
||||
codeLines: ['first line', '\t\t', 'third line'],
|
||||
expected: [{ index: 2, error: 'Empty line: "{tab}{tab}"' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for lines that consists of whitespace and tabs',
|
||||
codeLines: ['first line', '\t \t', 'third line', ' \t '],
|
||||
expected: [{ index: 2, error: 'Empty line: "{tab}{whitespace}{tab}"' }, { index: 4, error: 'Empty line: "{whitespace}{tab}{whitespace}"' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ShellScriptSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ShellScriptSyntax';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { BatchFileSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/BatchFileSyntax';
|
||||
|
||||
function getSystemsUnderTest(): ILanguageSyntax[] {
|
||||
return [new BatchFileSyntax(), new ShellScriptSyntax()];
|
||||
}
|
||||
|
||||
describe('ConcreteSyntaxes', () => {
|
||||
describe('commentDelimiters', () => {
|
||||
for (const sut of getSystemsUnderTest()) {
|
||||
it(`${sut.constructor.name} returns defined value`, () => {
|
||||
// act
|
||||
const value = sut.commentDelimiters;
|
||||
// assert
|
||||
expect(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('commonCodeParts', () => {
|
||||
for (const sut of getSystemsUnderTest()) {
|
||||
it(`${sut.constructor.name} returns defined value`, () => {
|
||||
// act
|
||||
const value = sut.commonCodeParts;
|
||||
// assert
|
||||
expect(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { describe } from 'vitest';
|
||||
import { SyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ShellScriptSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ShellScriptSyntax';
|
||||
import { ScriptingLanguageFactoryTestRunner } from '@tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner';
|
||||
import { BatchFileSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/BatchFileSyntax';
|
||||
|
||||
describe('SyntaxFactory', () => {
|
||||
const sut = new SyntaxFactory();
|
||||
const runner = new ScriptingLanguageFactoryTestRunner()
|
||||
.expectInstance(ScriptingLanguage.shellscript, ShellScriptSyntax)
|
||||
.expectInstance(ScriptingLanguage.batchfile, BatchFileSyntax);
|
||||
runner.testCreateMethod(sut);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
||||
import { analyzeEmptyLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeEmptyLines';
|
||||
import { analyzeDuplicateLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines';
|
||||
import { analyzeTooLongLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeTooLongLines';
|
||||
import { createValidationAnalyzers } from '@/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory';
|
||||
import type { CodeValidationAnalyzer } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
|
||||
describe('ValidationRuleAnalyzerFactory', () => {
|
||||
describe('createValidationAnalyzers', () => {
|
||||
it('throws error when no rules are provided', () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'missing rules';
|
||||
const rules: readonly CodeValidationRule[] = [];
|
||||
const context = new TestContext()
|
||||
.withRules(rules);
|
||||
// act
|
||||
const act = () => context.create();
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('creates correct analyzers for all valid rules', () => {
|
||||
// arrange
|
||||
const expectedAnalyzersForRules: Record<CodeValidationRule, CodeValidationAnalyzer> = {
|
||||
[CodeValidationRule.NoEmptyLines]: analyzeEmptyLines,
|
||||
[CodeValidationRule.NoDuplicatedLines]: analyzeDuplicateLines,
|
||||
[CodeValidationRule.NoTooLongLines]: analyzeTooLongLines,
|
||||
};
|
||||
const givenRules: CodeValidationRule[] = Object
|
||||
.keys(expectedAnalyzersForRules)
|
||||
.map((r) => Number(r) as CodeValidationRule);
|
||||
const context = new TestContext()
|
||||
.withRules(givenRules);
|
||||
// act
|
||||
const actualAnalyzers = context.create();
|
||||
// assert
|
||||
expect(actualAnalyzers).to.have.lengthOf(Object.entries(expectedAnalyzersForRules).length);
|
||||
const expectedAnalyzers = Object.values(expectedAnalyzersForRules);
|
||||
expect(actualAnalyzers).to.deep.equal(expectedAnalyzers);
|
||||
});
|
||||
|
||||
it('throws error for unknown rule', () => {
|
||||
// arrange
|
||||
const unknownRule = 9999 as CodeValidationRule;
|
||||
const expectedErrorMessage = `Unknown rule: ${unknownRule}`;
|
||||
const context = new TestContext()
|
||||
.withRules([unknownRule]);
|
||||
// arrange
|
||||
const act = () => context.create();
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('throws error for duplicate rules', () => {
|
||||
// arrange
|
||||
const duplicate1 = CodeValidationRule.NoEmptyLines;
|
||||
const duplicate2 = CodeValidationRule.NoDuplicatedLines;
|
||||
const rules: CodeValidationRule[] = [
|
||||
duplicate1, duplicate1,
|
||||
duplicate2, duplicate2,
|
||||
];
|
||||
const expectedErrorMessage: string = [
|
||||
'Duplicate rules are not allowed.',
|
||||
`Duplicates found: ${CodeValidationRule[duplicate1]} (2 times), ${CodeValidationRule[duplicate2]} (2 times)`,
|
||||
].join(' ');
|
||||
const context = new TestContext()
|
||||
.withRules(rules);
|
||||
// act
|
||||
const act = () => context.create();
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('handles single rule correctly', () => {
|
||||
// arrange
|
||||
const givenRule = CodeValidationRule.NoEmptyLines;
|
||||
const expectedAnalyzer = analyzeEmptyLines;
|
||||
const context = new TestContext()
|
||||
.withRules([givenRule]);
|
||||
// act
|
||||
const analyzers = context.create();
|
||||
// assert
|
||||
expect(analyzers).to.have.lengthOf(1);
|
||||
expect(analyzers[0]).toBe(expectedAnalyzer);
|
||||
});
|
||||
|
||||
it('handles multiple unique rules correctly', () => {
|
||||
// arrange
|
||||
const expectedRuleAnalyzerPairs = new Map<CodeValidationRule, CodeValidationAnalyzer>([
|
||||
[CodeValidationRule.NoEmptyLines, analyzeEmptyLines],
|
||||
[CodeValidationRule.NoDuplicatedLines, analyzeDuplicateLines],
|
||||
]);
|
||||
const rules = Array.from(expectedRuleAnalyzerPairs.keys());
|
||||
const context = new TestContext()
|
||||
.withRules(rules);
|
||||
// act
|
||||
const actualAnalyzers = context.create();
|
||||
// assert
|
||||
expect(actualAnalyzers).to.have.lengthOf(expectedRuleAnalyzerPairs.size);
|
||||
actualAnalyzers.forEach((analyzer, index) => {
|
||||
const rule = rules[index];
|
||||
const expectedAnalyzer = expectedRuleAnalyzerPairs.get(rule);
|
||||
expect(analyzer).to.equal(expectedAnalyzer, formatAssertionMessage([
|
||||
`Analyzer for rule ${CodeValidationRule[rule]} does not match the expected analyzer`,
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private rules: readonly CodeValidationRule[] = [CodeValidationRule.NoDuplicatedLines];
|
||||
|
||||
public withRules(rules: readonly CodeValidationRule[]): this {
|
||||
this.rules = rules;
|
||||
return this;
|
||||
}
|
||||
|
||||
public create(): ReturnType<typeof createValidationAnalyzers> {
|
||||
return createValidationAnalyzers(this.rules);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user