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,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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user