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:
undergroundwires
2024-08-27 11:32:52 +02:00
parent db090f3696
commit dc5c87376b
65 changed files with 2217 additions and 1350 deletions

View File

@@ -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,

View File

@@ -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,
},
);
}
}

View File

@@ -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,
},
});
}
}