Files
privacy.sexy/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.spec.ts
undergroundwires dc5c87376b 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.
2024-08-27 11:32:52 +02:00

366 lines
14 KiB
TypeScript

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