Files
privacy.sexy/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.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

258 lines
8.1 KiB
TypeScript

import 'mocha';
import { expect } from 'chai';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import {
AbsentStringTestCases, itEachAbsentCollectionValue, itEachAbsentObjectValue,
itEachAbsentStringValue,
} from '@tests/unit/shared/TestCases/AbsentTests';
describe('SharedFunction', () => {
describe('SharedFunction', () => {
describe('name', () => {
runForEachFactoryMethod((build) => {
it('sets as expected', () => {
// arrange
const expected = 'expected-function-name';
const builder = new SharedFunctionBuilder()
.withName(expected);
// act
const sut = build(builder);
// assert
expect(sut.name).equal(expected);
});
it('throws when absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing function name';
const builder = new SharedFunctionBuilder()
.withName(absentValue);
// act
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
describe('parameters', () => {
runForEachFactoryMethod((build) => {
it('sets as expected', () => {
// arrange
const expected = new FunctionParameterCollectionStub()
.withParameterName('test-parameter');
const builder = new SharedFunctionBuilder()
.withParameters(expected);
// act
const sut = build(builder);
// assert
expect(sut.parameters).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing parameters';
const parameters = absentValue;
const builder = new SharedFunctionBuilder()
.withParameters(parameters);
// act
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
});
describe('createFunctionWithInlineCode', () => {
describe('code', () => {
it('sets as expected', () => {
// arrange
const expected = 'expected-code';
// act
const sut = new SharedFunctionBuilder()
.withCode(expected)
.createFunctionWithInlineCode();
// assert
expect(sut.body.code.execute).equal(expected);
});
describe('throws if absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const functionName = 'expected-function-name';
const expectedError = `undefined code in function "${functionName}"`;
const invalidValue = absentValue;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCode(invalidValue)
.createFunctionWithInlineCode();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('revertCode', () => {
it('sets as expected', () => {
// arrange
const testData = [
'expected-revert-code',
...AbsentStringTestCases.map((testCase) => testCase.absentValue),
];
for (const data of testData) {
// act
const sut = new SharedFunctionBuilder()
.withRevertCode(data)
.createFunctionWithInlineCode();
// assert
expect(sut.body.code.revert).equal(data);
}
});
});
it('sets type as expected', () => {
// arrange
const expectedType = FunctionBodyType.Code;
// act
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.type).equal(expectedType);
});
it('calls are undefined', () => {
// arrange
const expectedCalls = undefined;
// act
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.calls).equal(expectedCalls);
});
});
describe('createCallerFunction', () => {
describe('callSequence', () => {
it('sets as expected', () => {
// arrange
const expected = [
new FunctionCallStub().withFunctionName('firstFunction'),
new FunctionCallStub().withFunctionName('secondFunction'),
];
// act
const sut = new SharedFunctionBuilder()
.withCallSequence(expected)
.createCallerFunction();
// assert
expect(sut.body.calls).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentCollectionValue((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const callSequence = absentValue;
const expectedError = `missing call sequence in function "${functionName}"`;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCallSequence(callSequence)
.createCallerFunction();
// assert
expect(act).to.throw(expectedError);
});
});
});
it('sets type as expected', () => {
// arrange
const expectedType = FunctionBodyType.Calls;
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
// assert
expect(sut.body.type).equal(expectedType);
});
it('code is undefined', () => {
// arrange
const expectedCode = undefined;
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
// assert
expect(sut.body.code).equal(expectedCode);
});
});
});
function runForEachFactoryMethod(
act: (action: (sut: SharedFunctionBuilder) => ISharedFunction) => void,
): void {
describe('createCallerFunction', () => {
const action = (builder: SharedFunctionBuilder) => builder.createCallerFunction();
act(action);
});
describe('createFunctionWithInlineCode', () => {
const action = (builder: SharedFunctionBuilder) => builder.createFunctionWithInlineCode();
act(action);
});
}
/*
Using an abstraction here allows for easy refactorings in
parameters or moving between functional and object-oriented
solutions without refactorings all tests.
*/
class SharedFunctionBuilder {
private name = 'name';
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private callSequence: readonly IFunctionCall[] = [new FunctionCallStub()];
private code = 'code';
private revertCode = 'revert-code';
public createCallerFunction(): ISharedFunction {
return createCallerFunction(
this.name,
this.parameters,
this.callSequence,
);
}
public createFunctionWithInlineCode(): ISharedFunction {
return createFunctionWithInlineCode(
this.name,
this.parameters,
this.code,
this.revertCode,
);
}
public withName(name: string) {
this.name = name;
return this;
}
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;
return this;
}
public withCode(code: string) {
this.code = code;
return this;
}
public withRevertCode(revertCode: string) {
this.revertCode = revertCode;
return this;
}
public withCallSequence(callSequence: readonly IFunctionCall[]) {
this.callSequence = callSequence;
return this;
}
}