Files
privacy.sexy/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts
undergroundwires e8199932b4 Relax and improve code validation
Rework code validation to be bound to a context and not
context-independent. It means that the generated code is validated based
on different phases during the compilation. This is done by moving
validation from `ScriptCode` constructor to a different callable
function.

It removes duplicate detection for function calls once a call is fully
compiled, but still checks for duplicates inside each function body that
has inline code. This allows for having duplicates in final scripts
(thus relaxing the duplicate detection), e.g., when multiple calls to
the same function is made.

It fixes non-duplicates (when using common syntax) being misrepresented
as duplicate lines.

It improves the output of errors, such as printing valid lines, to give
more context. This improvement also fixes empty line validation not
showing the right empty lines in the error output. Empty line validation
shows tabs and whitespaces more clearly.

Finally, it adds more tests including tests for existing logic, such as
singleton factories.
2022-10-29 20:03:06 +02:00

301 lines
11 KiB
TypeScript

import 'mocha';
import { expect } from 'chai';
import type { FunctionData } from '@/application/collections/';
import { ScriptCode } from '@/domain/ScriptCode';
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub';
import { SharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
describe('ScriptCompiler', () => {
describe('ctor', () => {
describe('throws if syntax is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing syntax';
const syntax = absentValue;
// act
const act = () => new ScriptCompilerBuilder()
.withSomeFunctions()
.withSyntax(syntax)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('canCompile', () => {
describe('throws if script is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing script';
const argument = absentValue;
const builder = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
// act
const act = () => builder.canCompile(argument);
// assert
expect(act).to.throw(expectedError);
});
});
it('returns true if "call" is defined', () => {
// arrange
const sut = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
const script = ScriptDataStub.createWithCall();
// 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 = ScriptDataStub.createWithCode();
// act
const actual = sut.canCompile(script);
// assert
expect(actual).to.equal(false);
});
});
describe('compile', () => {
describe('throws if script is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing script';
const argument = absentValue;
const builder = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
// act
const act = () => builder.compile(argument);
// assert
expect(act).to.throw(expectedError);
});
});
it('returns code as expected', () => {
// arrange
const expected: ICompiledCode = {
code: 'expected-code',
revertCode: 'expected-revert-code',
};
const call = new FunctionCallDataStub();
const script = ScriptDataStub.createWithCall(call);
const functions = [FunctionDataStub.createWithCode().withName('existing-func')];
const compiledFunctions = new SharedFunctionCollectionStub();
const functionParserMock = new SharedFunctionsParserStub();
functionParserMock.setup(functions, compiledFunctions);
const callCompilerMock = new FunctionCallCompilerStub();
callCompilerMock.setup(parseFunctionCalls(call), compiledFunctions, expected);
const sut = new ScriptCompilerBuilder()
.withFunctions(...functions)
.withSharedFunctionsParser(functionParserMock)
.withFunctionCallCompiler(callCompilerMock)
.build();
// act
const code = sut.compile(script);
// assert
expect(code.execute).to.equal(expected.code);
expect(code.revert).to.equal(expected.revertCode);
});
describe('parses functions as expected', () => {
it('parses functions with expected syntax', () => {
// arrange
const expected: ILanguageSyntax = new LanguageSyntaxStub();
const parser = new SharedFunctionsParserStub();
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.withSyntax(expected)
.withSharedFunctionsParser(parser)
.build();
const scriptData = ScriptDataStub.createWithCall();
// act
sut.compile(scriptData);
// assert
expect(parser.callHistory.length).to.equal(1);
expect(parser.callHistory[0].syntax).to.equal(expected);
});
it('parses given functions', () => {
// arrange
const expectedFunctions = [FunctionDataStub.createWithCode().withName('existing-func')];
const parser = new SharedFunctionsParserStub();
const sut = new ScriptCompilerBuilder()
.withFunctions(...expectedFunctions)
.withSharedFunctionsParser(parser)
.build();
const scriptData = ScriptDataStub.createWithCall();
// act
sut.compile(scriptData);
// assert
expect(parser.callHistory.length).to.equal(1);
expect(parser.callHistory[0].functions).to.deep.equal(expectedFunctions);
});
});
it('rethrows error with script name', () => {
// arrange
const scriptName = 'scriptName';
const innerError = 'innerError';
const expectedError = `Script "${scriptName}" ${innerError}`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => { throw new Error(innerError); },
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.withFunctionCallCompiler(callCompiler)
.build();
// act
const act = () => sut.compile(scriptData);
// assert
expect(act).to.throw(expectedError);
});
it('rethrows error from ScriptCode with script name', () => {
// arrange
const scriptName = 'scriptName';
const syntax = new LanguageSyntaxStub();
const invalidCode: ICompiledCode = { code: undefined, revertCode: undefined };
const realExceptionMessage = collectExceptionMessage(
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
);
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => invalidCode,
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.withFunctionCallCompiler(callCompiler)
.withSyntax(syntax)
.build();
// act
const act = () => sut.compile(scriptData);
// assert
expect(act).to.throw(expectedError);
});
it('validates compiled code as expected', () => {
// arrange
const expectedRules = [
NoEmptyLines,
// Allow duplicated lines to enable calling same function multiple times
];
const scriptData = ScriptDataStub.createWithCall();
const validator = new CodeValidatorStub();
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.withCodeValidator(validator)
.build();
// act
const compilationResult = sut.compile(scriptData);
// assert
validator.assertHistory({
validatedCodes: [compilationResult.execute, compilationResult.revert],
rules: expectedRules,
});
});
});
});
class ScriptCompilerBuilder {
private static createFunctions(...names: string[]): FunctionData[] {
return names.map((functionName) => {
return FunctionDataStub.createWithCode().withName(functionName);
});
}
private functions: FunctionData[];
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
private sharedFunctionsParser: ISharedFunctionsParser = new SharedFunctionsParserStub();
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
private codeValidator: ICodeValidator = new CodeValidatorStub();
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
this.functions = functions;
return this;
}
public withSomeFunctions(): ScriptCompilerBuilder {
this.functions = ScriptCompilerBuilder.createFunctions('test-function');
return this;
}
public withFunctionNames(...functionNames: string[]): ScriptCompilerBuilder {
this.functions = ScriptCompilerBuilder.createFunctions(...functionNames);
return this;
}
public withEmptyFunctions(): ScriptCompilerBuilder {
this.functions = [];
return this;
}
public withSyntax(syntax: ILanguageSyntax): ScriptCompilerBuilder {
this.syntax = syntax;
return this;
}
public withSharedFunctionsParser(
sharedFunctionsParser: ISharedFunctionsParser,
): ScriptCompilerBuilder {
this.sharedFunctionsParser = sharedFunctionsParser;
return this;
}
public withCodeValidator(
codeValidator: ICodeValidator,
): ScriptCompilerBuilder {
this.codeValidator = codeValidator;
return this;
}
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
this.callCompiler = callCompiler;
return this;
}
public build(): ScriptCompiler {
if (!this.functions) {
throw new Error('Function behavior not defined');
}
return new ScriptCompiler(
this.functions,
this.syntax,
this.sharedFunctionsParser,
this.callCompiler,
this.codeValidator,
);
}
}
function collectExceptionMessage(action: () => unknown) {
let message = '';
try {
action();
} catch (e) {
message = e.message;
}
return message;
}