Refactor to unify scripts/categories as Executable

This commit consolidates scripts and categories under a unified
'Executable' concept. This simplifies the architecture and improves code
readability.

- Introduce subfolders within `src/domain` to segregate domain elements.
- Update class and interface names by removing the 'I' prefix in
  alignment with new coding standards.
- Replace 'Node' with 'Executable' to clarify usage; reserve 'Node'
  exclusively for the UI's tree component.
This commit is contained in:
undergroundwires
2024-06-12 12:36:40 +02:00
parent 8becc7dbc4
commit c138f74460
230 changed files with 1120 additions and 1039 deletions

View File

@@ -0,0 +1,52 @@
import { describe, expect } from 'vitest';
import { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { testParameterName } from '../../../ParameterNameTestRunner';
describe('FunctionCallArgument', () => {
describe('ctor', () => {
describe('parameter name', () => {
testParameterName(
(parameterName) => new FunctionCallArgumentBuilder()
.withParameterName(parameterName)
.build()
.parameterName,
);
});
describe('throws if argument value is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const parameterName = 'paramName';
const expectedError = `Missing argument value for the parameter "${parameterName}".`;
const argumentValue = absentValue;
// act
const act = () => new FunctionCallArgumentBuilder()
.withParameterName(parameterName)
.withArgumentValue(argumentValue)
.build();
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
});
});
class FunctionCallArgumentBuilder {
private parameterName = 'default-parameter-name';
private argumentValue = 'default-argument-value';
public withParameterName(parameterName: string) {
this.parameterName = parameterName;
return this;
}
public withArgumentValue(argumentValue: string) {
this.argumentValue = argumentValue;
return this;
}
public build() {
return new FunctionCallArgument(this.parameterName, this.argumentValue);
}
}

View File

@@ -0,0 +1,138 @@
import { describe, it, expect } from 'vitest';
import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument';
describe('FunctionCallArgumentCollection', () => {
describe('addArgument', () => {
it('throws if parameter value is already provided', () => {
// arrange
const duplicateParameterName = 'duplicateParam';
const errorMessage = `argument value for parameter ${duplicateParameterName} is already provided`;
const arg1 = new FunctionCallArgumentStub().withParameterName(duplicateParameterName);
const arg2 = new FunctionCallArgumentStub().withParameterName(duplicateParameterName);
const sut = new FunctionCallArgumentCollection();
// act
sut.addArgument(arg1);
const act = () => sut.addArgument(arg2);
// assert
expect(act).to.throw(errorMessage);
});
});
describe('getAllParameterNames', () => {
describe('returns as expected', () => {
// arrange
const testCases: ReadonlyArray<{
readonly description: string;
readonly args: readonly IFunctionCallArgument[];
readonly expectedParameterNames: string[];
}> = [{
description: 'no args',
args: [],
expectedParameterNames: [],
}, {
description: 'with some args',
args: [
new FunctionCallArgumentStub().withParameterName('a-param-name'),
new FunctionCallArgumentStub().withParameterName('b-param-name')],
expectedParameterNames: ['a-param-name', 'b-param-name'],
}];
for (const testCase of testCases) {
it(testCase.description, () => {
const sut = new FunctionCallArgumentCollection();
// act
for (const arg of testCase.args) {
sut.addArgument(arg);
}
const actual = sut.getAllParameterNames();
// assert
expect(actual).to.deep.equal(testCase.expectedParameterNames);
});
}
});
});
describe('getArgument', () => {
describe('throws if parameter name is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing parameter name';
const sut = new FunctionCallArgumentCollection();
const parameterName = absentValue;
// act
const act = () => sut.getArgument(parameterName);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
it('throws if argument does not exist', () => {
// arrange
const parameterName = 'nonExistingParam';
const expectedError = `parameter does not exist: ${parameterName}`;
const sut = new FunctionCallArgumentCollection();
// act
const act = () => sut.getArgument(parameterName);
// assert
expect(act).to.throw(expectedError);
});
it('returns argument as expected', () => {
// arrange
const expected = new FunctionCallArgumentStub()
.withParameterName('expectedName')
.withArgumentValue('expectedValue');
const sut = new FunctionCallArgumentCollection();
// act
sut.addArgument(expected);
const actual = sut.getArgument(expected.parameterName);
// assert
expect(actual).to.equal(expected);
});
});
describe('hasArgument', () => {
describe('throws if parameter name is missing', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing parameter name';
const parameterName = absentValue;
const sut = new FunctionCallArgumentCollection();
// act
const act = () => sut.hasArgument(parameterName);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
describe('returns as expected', () => {
// arrange
const testCases = [{
name: 'argument exists',
parameter: 'existing-parameter-name',
args: [
new FunctionCallArgumentStub().withParameterName('existing-parameter-name'),
new FunctionCallArgumentStub().withParameterName('unrelated-parameter-name'),
],
expected: true,
},
{
name: 'argument does not exist',
parameter: 'not-existing-parameter-name',
args: [
new FunctionCallArgumentStub().withParameterName('unrelated-parameter-name-b'),
new FunctionCallArgumentStub().withParameterName('unrelated-parameter-name-a'),
],
expected: false,
}];
for (const testCase of testCases) {
it(`"${testCase.name}" returns "${testCase.expected}"`, () => {
const sut = new FunctionCallArgumentCollection();
// act
for (const arg of testCase.args) {
sut.addArgument(arg);
}
const actual = sut.hasArgument(testCase.parameter);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
});
});

View File

@@ -0,0 +1,104 @@
import { expect, describe, it } from 'vitest';
import { NewlineCodeSegmentMerger } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
describe('NewlineCodeSegmentMerger', () => {
describe('mergeCodeParts', () => {
describe('throws given empty segments', () => {
itEachAbsentCollectionValue<CompiledCode>((absentValue) => {
// arrange
const expectedError = 'missing segments';
const segments = absentValue;
const merger = new NewlineCodeSegmentMerger();
// act
const act = () => merger.mergeCodeParts(segments);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true, excludeNull: true });
});
describe('merges correctly', () => {
const testCases: ReadonlyArray<{
readonly description: string,
readonly segments: CompiledCodeStub[],
readonly expected: {
readonly code: string,
readonly revertCode?: string,
},
}> = [
{
description: 'given `code` and `revertCode`',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
new CompiledCodeStub().withCode('code2').withRevertCode('revert2'),
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
],
expected: {
code: 'code1\ncode2\ncode3',
revertCode: 'revert1\nrevert2\nrevert3',
},
},
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((absentTestCase) => ({
description: `filter out ${absentTestCase.valueName} \`revertCode\``,
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue),
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
],
expected: {
code: 'code1\ncode2\ncode3',
revertCode: 'revert1\nrevert3',
},
})),
...getAbsentStringTestCases({ excludeNull: true })
.map((emptyRevertCode) => ({
description: `given only \`code\` in segments with "${emptyRevertCode.valueName}" \`revertCode\``,
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(emptyRevertCode.absentValue),
new CompiledCodeStub().withCode('code2').withRevertCode(emptyRevertCode.absentValue),
],
expected: {
code: 'code1\ncode2',
revertCode: '',
},
})),
{
description: 'given mix of segments with only `code` or `revertCode`',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(''),
new CompiledCodeStub().withCode('').withRevertCode('revert2'),
new CompiledCodeStub().withCode('code3').withRevertCode(''),
],
expected: {
code: 'code1\ncode3',
revertCode: 'revert2',
},
},
{
description: 'given only `revertCode` in segments',
segments: [
new CompiledCodeStub().withCode('').withRevertCode('revert1'),
new CompiledCodeStub().withCode('').withRevertCode('revert2'),
],
expected: {
code: '',
revertCode: 'revert1\nrevert2',
},
},
];
for (const { segments, expected, description } of testCases) {
it(description, () => {
// arrange
const merger = new NewlineCodeSegmentMerger();
// act
const actual = merger.mergeCodeParts(segments);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revertCode);
});
}
});
});
});

View File

@@ -0,0 +1,223 @@
/* eslint-disable max-classes-per-file */
import { describe, it, expect } from 'vitest';
import { FunctionCallSequenceCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler';
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { SingleCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import type { CodeSegmentMerger } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub';
import { CodeSegmentMergerStub } from '@tests/unit/shared/Stubs/CodeSegmentMergerStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('FunctionCallSequenceCompiler', () => {
describe('instance', () => {
itIsSingletonFactory({
getter: () => FunctionCallSequenceCompiler.instance,
expectedType: FunctionCallSequenceCompiler,
});
});
describe('compileFunctionCalls', () => {
describe('parameter validation', () => {
describe('calls', () => {
describe('throws with missing call', () => {
itEachAbsentCollectionValue<FunctionCall>((absentValue) => {
// arrange
const expectedError = 'missing calls';
const calls = absentValue;
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true, excludeNull: true });
});
});
});
describe('invokes single call compiler correctly', () => {
describe('calls', () => {
it('with expected call', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedCall = new FunctionCallStub();
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withCalls([expectedCall]);
// act
builder.compileFunctionCalls();
// assert
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
expectExists(calledMethod);
expect(calledMethod.args[0]).to.equal(expectedCall);
});
it('with every call', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedCalls = [
new FunctionCallStub(), new FunctionCallStub(), new FunctionCallStub(),
];
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withCalls(expectedCalls);
// act
builder.compileFunctionCalls();
// assert
const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall');
expect(calledMethods).to.have.lengthOf(expectedCalls.length);
const callArguments = calledMethods.map((c) => c.args[0]);
expect(expectedCalls).to.have.members(callArguments);
});
});
describe('context', () => {
it('with expected functions', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedFunctions = new SharedFunctionCollectionStub();
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withFunctions(expectedFunctions);
// act
builder.compileFunctionCalls();
// assert
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
expectExists(calledMethod);
const actualFunctions = calledMethod.args[1].allFunctions;
expect(actualFunctions).to.equal(expectedFunctions);
});
it('with expected call sequence', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedCallSequence = [new FunctionCallStub(), new FunctionCallStub()];
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withCalls(expectedCallSequence);
// act
builder.compileFunctionCalls();
// assert
const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall');
expect(calledMethods).to.have.lengthOf(expectedCallSequence.length);
const calledSequenceArgs = calledMethods.map((call) => call.args[1].rootCallSequence);
expect(calledSequenceArgs.every((sequence) => sequence === expectedCallSequence));
});
it('with expected call compiler', () => {
// arrange
const expectedCompiler = new SingleCallCompilerStub();
const rootCallSequence = [new FunctionCallStub(), new FunctionCallStub()];
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(rootCallSequence)
.withSingleCallCompiler(expectedCompiler);
// act
builder.compileFunctionCalls();
// assert
const calledMethods = expectedCompiler.callHistory.filter((m) => m.methodName === 'compileSingleCall');
expect(calledMethods).to.have.lengthOf(rootCallSequence.length);
const compilerArgs = calledMethods.map((call) => call.args[1].singleCallCompiler);
expect(compilerArgs.every((compiler) => compiler === expectedCompiler));
});
});
});
describe('code segment merger', () => {
it('invokes code segment merger correctly', () => {
// arrange
const singleCallCompilationScenario = new Map<FunctionCall, CompiledCode[]>([
[new FunctionCallStub(), [new CompiledCodeStub()]],
[new FunctionCallStub(), [new CompiledCodeStub(), new CompiledCodeStub()]],
]);
const expectedFlattenedSegments = [...singleCallCompilationScenario.values()].flat();
const calls = [...singleCallCompilationScenario.keys()];
const singleCallCompiler = new SingleCallCompilerStub()
.withCallCompilationScenarios(singleCallCompilationScenario);
const codeSegmentMergerStub = new CodeSegmentMergerStub();
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls)
.withSingleCallCompiler(singleCallCompiler)
.withCodeSegmentMerger(codeSegmentMergerStub);
// act
builder.compileFunctionCalls();
// assert
const calledMethod = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts');
expectExists(calledMethod);
const [actualSegments] = calledMethod.args;
expect(expectedFlattenedSegments).to.have.lengthOf(actualSegments.length);
expect(expectedFlattenedSegments).to.have.deep.members(actualSegments);
});
it('returns code segment merger result', () => {
// arrange
const expectedResult = new CompiledCodeStub();
const codeSegmentMergerStub = new CodeSegmentMergerStub();
codeSegmentMergerStub.mergeCodeParts = () => expectedResult;
const builder = new FunctionCallSequenceCompilerBuilder()
.withCodeSegmentMerger(codeSegmentMergerStub);
// act
const actualResult = builder.compileFunctionCalls();
// assert
expect(actualResult).to.equal(expectedResult);
});
});
});
});
class FunctionCallSequenceCompilerBuilder {
private singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub();
private codeSegmentMerger: CodeSegmentMerger = new CodeSegmentMergerStub();
private functions: ISharedFunctionCollection = new SharedFunctionCollectionStub();
private calls: readonly FunctionCall[] = [
new FunctionCallStub(),
];
public withSingleCallCompiler(compiler: SingleCallCompiler): this {
this.singleCallCompiler = compiler;
return this;
}
public withCodeSegmentMerger(merger: CodeSegmentMerger): this {
this.codeSegmentMerger = merger;
return this;
}
public withCalls(calls: readonly FunctionCall[]): this {
this.calls = calls;
return this;
}
public withFunctions(functions: ISharedFunctionCollection): this {
this.functions = functions;
return this;
}
public compileFunctionCalls() {
const compiler = new TestableFunctionCallSequenceCompiler({
singleCallCompiler: this.singleCallCompiler,
codeSegmentMerger: this.codeSegmentMerger,
});
return compiler.compileFunctionCalls(
this.calls,
this.functions,
);
}
}
interface FunctionCallSequenceCompilerStubs {
readonly singleCallCompiler?: SingleCallCompiler;
readonly codeSegmentMerger: CodeSegmentMerger;
}
class TestableFunctionCallSequenceCompiler extends FunctionCallSequenceCompiler {
public constructor(options: FunctionCallSequenceCompilerStubs) {
super(
options.singleCallCompiler,
options.codeSegmentMerger,
);
}
}

View File

@@ -0,0 +1,294 @@
import { expect, describe, it } from 'vitest';
import { createSharedFunctionStubWithCalls, createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { NestedFunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler';
import type { ArgumentCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { ArgumentCompilerStub } from '@tests/unit/shared/Stubs/ArgumentCompilerStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
describe('NestedFunctionCallCompiler', () => {
describe('canCompile', () => {
it('returns `true` for code body function', () => {
// arrange
const expected = true;
const func = createSharedFunctionStubWithCalls()
.withSomeCalls();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
it('returns `false` for non-code body function', () => {
// arrange
const expected = false;
const func = createSharedFunctionStubWithCode();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
});
describe('compile', () => {
describe('argument compilation', () => {
it('uses correct context', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
// assert
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
expect(calls).have.lengthOf(1);
const [,,actualContext] = calls[0].args;
expect(actualContext).to.equal(expectedContext);
});
it('uses correct parent call', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const expectedParentCall = callToFrontFunc;
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
// assert
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
expect(calls).have.lengthOf(1);
const [,actualParentCall] = calls[0].args;
expect(actualParentCall).to.equal(expectedParentCall);
});
it('uses correct nested call', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const {
frontFunction, callToDeepFunc, callToFrontFunc,
} = createSingleFuncCallingAnotherFunc();
const expectedNestedCall = callToDeepFunc;
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
// assert
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
expect(calls).have.lengthOf(1);
const [actualNestedCall] = calls[0].args;
expect(actualNestedCall).to.deep.equal(expectedNestedCall);
});
});
describe('re-compilation with compiled args', () => {
it('uses correct context', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
// assert
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
expect(calls).have.lengthOf(1);
const [,actualContext] = calls[0].args;
expect(expectedContext).to.equal(actualContext);
});
it('uses compiled nested call', () => {
// arrange
const expectedCall = new FunctionCallStub();
const argumentCompilerStub = new ArgumentCompilerStub();
argumentCompilerStub.createCompiledNestedCall = () => expectedCall;
const singleCallCompilerStub = new SingleCallCompilerStub();
const context = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompilerStub)
.build();
// act
compiler.compileFunction(frontFunction, callToFrontFunc, context);
// assert
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
expect(calls).have.lengthOf(1);
const [actualNestedCall] = calls[0].args;
expect(expectedCall).to.equal(actualNestedCall);
});
});
it('flattens re-compiled functions', () => {
// arrange
const deepFunc1 = createSharedFunctionStubWithCode();
const deepFunc2 = createSharedFunctionStubWithCalls();
const callToDeepFunc1 = new FunctionCallStub().withFunctionName(deepFunc1.name);
const callToDeepFunc2 = new FunctionCallStub().withFunctionName(deepFunc2.name);
const singleCallCompilationScenario = new Map<FunctionCall, CompiledCode[]>([
[callToDeepFunc1, [new CompiledCodeStub()]],
[callToDeepFunc2, [new CompiledCodeStub(), new CompiledCodeStub()]],
]);
const argumentCompiler = new ArgumentCompilerStub()
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
const frontFunction = createSharedFunctionStubWithCalls()
.withCalls(callToDeepFunc1, callToDeepFunc2);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name);
const singleCallCompilerStub = new SingleCallCompilerStub()
.withCallCompilationScenarios(singleCallCompilationScenario);
const expectedContext = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
const actualCodes = compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
// assert
expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length);
expect(actualCodes).to.have.members(expectedFlattenedCodes);
});
describe('error handling', () => {
describe('rethrows error from argument compiler', () => {
// arrange
const expectedInnerError = new Error(`Expected error from ${ArgumentCompilerStub.name}`);
const calleeFunctionName = 'expectedCalleeFunctionName';
const callerFunctionName = 'expectedCallerFunctionName';
const expectedErrorMessage = buildRethrowErrorMessage({
callee: calleeFunctionName,
caller: callerFunctionName,
});
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({
frontFunctionName: callerFunctionName,
deepFunctionName: calleeFunctionName,
});
const argumentCompilerStub = new ArgumentCompilerStub();
argumentCompilerStub.createCompiledNestedCall = () => {
throw expectedInnerError;
};
const builder = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompilerStub);
itThrowsContextualError({
// act
throwingAction: (wrapError) => {
builder
.withErrorWrapper(wrapError)
.build()
.compileFunction(
frontFunction,
callToFrontFunc,
new FunctionCallCompilationContextStub(),
);
},
// assert
expectedWrappedError: expectedInnerError,
expectedContextMessage: expectedErrorMessage,
});
});
describe('rethrows error from single call compiler', () => {
// arrange
const expectedInnerError = new Error(`Expected error from ${SingleCallCompilerStub.name}`);
const calleeFunctionName = 'expectedCalleeFunctionName';
const callerFunctionName = 'expectedCallerFunctionName';
const expectedErrorMessage = buildRethrowErrorMessage({
callee: calleeFunctionName,
caller: callerFunctionName,
});
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({
frontFunctionName: callerFunctionName,
deepFunctionName: calleeFunctionName,
});
const singleCallCompiler = new SingleCallCompilerStub();
singleCallCompiler.compileSingleCall = () => {
throw expectedInnerError;
};
const context = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompiler);
const builder = new NestedFunctionCallCompilerBuilder();
itThrowsContextualError({
// act
throwingAction: (wrapError) => {
builder
.withErrorWrapper(wrapError)
.build()
.compileFunction(
frontFunction,
callToFrontFunc,
context,
);
},
// assert
expectedWrappedError: expectedInnerError,
expectedContextMessage: expectedErrorMessage,
});
});
});
});
});
function createSingleFuncCallingAnotherFunc(
functionNames?: {
readonly frontFunctionName?: string;
readonly deepFunctionName?: string;
},
) {
const deepFunction = createSharedFunctionStubWithCode()
.withName(functionNames?.deepFunctionName ?? 'deep-function (is called by front-function)');
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunction.name);
const frontFunction = createSharedFunctionStubWithCalls()
.withCalls(callToDeepFunc)
.withName(functionNames?.frontFunctionName ?? 'front-function (calls deep-function)');
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name);
return {
deepFunction,
frontFunction,
callToFrontFunc,
callToDeepFunc,
};
}
class NestedFunctionCallCompilerBuilder {
private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub();
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this {
this.argumentCompiler = argumentCompiler;
return this;
}
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
this.wrapError = wrapError;
return this;
}
public build(): NestedFunctionCallCompiler {
return new NestedFunctionCallCompiler(
this.argumentCompiler,
this.wrapError,
);
}
}
function buildRethrowErrorMessage(
functionNames: {
readonly caller: string;
readonly callee: string;
},
) {
return `Failed to call '${functionNames.callee}' (callee function) from '${functionNames.caller}' (caller function).`;
}

View File

@@ -0,0 +1,260 @@
import { expect, describe, it } from 'vitest';
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import type { FunctionCallParametersData } from '@/application/collections/';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { AdaptiveFunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler';
import type { SingleCallCompilerStrategy } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy';
import { SingleCallCompilerStrategyStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStrategyStub';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import type { SingleCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
describe('AdaptiveFunctionCallCompiler', () => {
describe('compileSingleCall', () => {
describe('throws if call parameters does not match function parameters', () => {
// arrange
const functionName = 'test-function-name';
const testCases: Array<{
readonly description: string,
readonly functionParameters: string[],
readonly callParameters: string[]
readonly expectedError: string;
}> = [
{
description: 'provided: single unexpected parameter, when: another expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
+ '\nExpected parameter(s): "expected-parameter"',
},
{
description: 'provided: multiple unexpected parameters, when: different one is expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter1', 'unexpected-parameter2'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2".`
+ '\nExpected parameter(s): "expected-parameter"',
},
{
description: 'provided: an unexpected parameter, when: multiple parameters are expected',
functionParameters: ['expected-parameter1', 'expected-parameter2'],
callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
+ '\nExpected parameter(s): "expected-parameter1", "expected-parameter2"',
},
{
description: 'provided: an unexpected parameter, when: none required',
functionParameters: [],
callParameters: ['unexpected-call-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter".`
+ '\nExpected parameter(s): none',
},
{
description: 'provided: expected and unexpected parameter, when: one of them is expected',
functionParameters: ['expected-parameter'],
callParameters: ['expected-parameter', 'unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
+ '\nExpected parameter(s): "expected-parameter"',
},
];
testCases.forEach(({
description, functionParameters, callParameters, expectedError,
}) => {
it(description, () => {
// arrange
const func = createSharedFunctionStubWithCode()
.withName('test-function-name')
.withParameterNames(...functionParameters);
const params = callParameters
.reduce((result, parameter) => {
return { ...result, [parameter]: 'defined-parameter-value' };
}, {} as FunctionCallParametersData);
const call = new FunctionCallStub()
.withFunctionName(func.name)
.withArguments(params);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withContext(new FunctionCallCompilationContextStub()
.withAllFunctions(
new SharedFunctionCollectionStub().withFunctions(func),
))
.withCall(call);
// act
const act = () => builder.compileSingleCall();
// assert
const errorMessage = collectExceptionMessage(act);
expect(errorMessage).to.include(expectedError);
});
});
});
describe('strategy selection', () => {
it('uses the matching strategy among multiple', () => {
// arrange
const matchedStrategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const unmatchedStrategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(false);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([matchedStrategy, unmatchedStrategy]);
// act
builder.compileSingleCall();
// assert
expect(matchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(1);
expect(unmatchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(0);
});
it('throws if multiple strategies can compile', () => {
// arrange
const expectedError = 'Multiple strategies found to compile the function call.';
const matchedStrategy1 = new SingleCallCompilerStrategyStub().withCanCompileResult(true);
const matchedStrategy2 = new SingleCallCompilerStrategyStub().withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder().withStrategies(
[matchedStrategy1, matchedStrategy2],
);
// act
const act = () => builder.compileSingleCall();
// assert
expect(act).to.throw(expectedError);
});
it('throws if no strategy can compile', () => {
// arrange
const expectedError = 'No strategies found to compile the function call.';
const unmatchedStrategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(false);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([unmatchedStrategy]);
// act
const act = () => builder.compileSingleCall();
// assert
expect(act).to.throw(expectedError);
});
});
describe('strategy invocation', () => {
it('passes correct function for compilation ability check', () => {
// arrange
const expectedFunction = createSharedFunctionStubWithCode();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withContext(new FunctionCallCompilationContextStub()
.withAllFunctions(
new SharedFunctionCollectionStub().withFunctions(expectedFunction),
))
.withCall(new FunctionCallStub().withFunctionName(expectedFunction.name))
.withStrategies([strategy]);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'canCompile');
expect(call).to.have.lengthOf(1);
expect(call[0].args[0]).to.equal(expectedFunction);
});
describe('compilation arguments', () => {
it('uses correct function', () => {
// arrange
const expectedFunction = createSharedFunctionStubWithCode();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withContext(new FunctionCallCompilationContextStub()
.withAllFunctions(
new SharedFunctionCollectionStub().withFunctions(expectedFunction),
))
.withCall(new FunctionCallStub().withFunctionName(expectedFunction.name))
.withStrategies([strategy]);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction');
expect(call).to.have.lengthOf(1);
const [actualFunction] = call[0].args;
expect(actualFunction).to.equal(expectedFunction);
});
it('uses correct call', () => {
// arrange
const expectedCall = new FunctionCallStub();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([strategy])
.withCall(expectedCall);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction');
expect(call).to.have.lengthOf(1);
const [,actualCall] = call[0].args;
expect(actualCall).to.equal(expectedCall);
});
it('uses correct context', () => {
// arrange
const expectedContext = new FunctionCallCompilationContextStub();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([strategy])
.withContext(expectedContext);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction');
expect(call).to.have.lengthOf(1);
const [,,actualContext] = call[0].args;
expect(actualContext).to.equal(expectedContext);
});
});
});
it('returns compiled code from strategy', () => {
// arrange
const expectedResult = [new CompiledCodeStub(), new CompiledCodeStub()];
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true)
.withCompiledFunctionResult(expectedResult);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([strategy]);
// act
const actualResult = builder.compileSingleCall();
// assert
expect(expectedResult).to.equal(actualResult);
});
});
});
class AdaptiveFunctionCallCompilerBuilder implements SingleCallCompiler {
private strategies: SingleCallCompilerStrategy[] = [
new SingleCallCompilerStrategyStub().withCanCompileResult(true),
];
private call: FunctionCall = new FunctionCallStub();
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
public withCall(call: FunctionCall): this {
this.call = call;
return this;
}
public withContext(context: FunctionCallCompilationContext): this {
this.context = context;
return this;
}
public withStrategies(strategies: SingleCallCompilerStrategy[]): this {
this.strategies = strategies;
return this;
}
public compileSingleCall() {
const compiler = new AdaptiveFunctionCallCompiler(this.strategies);
return compiler.compileSingleCall(
this.call,
this.context,
);
}
}

View File

@@ -0,0 +1,305 @@
import { expect, describe, it } from 'vitest';
import type { ArgumentCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import { NestedFunctionArgumentCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler';
import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
describe('NestedFunctionArgumentCompiler', () => {
describe('createCompiledNestedCall', () => {
describe('rethrows error from expressions compiler', () => {
// arrange
const expectedInnerError = new Error('child-');
const parameterName = 'parameterName';
const expectedErrorMessage = `Error when compiling argument for "${parameterName}"`;
const nestedCall = new FunctionCallStub()
.withFunctionName('nested-function-call')
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, 'unimportant-value'));
const parentCall = new FunctionCallStub()
.withFunctionName('parent-function-call');
const expressionsCompiler = new ExpressionsCompilerStub();
expressionsCompiler.compileExpressions = () => { throw expectedInnerError; };
const builder = new NestedFunctionArgumentCompilerBuilder()
.withParentFunctionCall(parentCall)
.withNestedFunctionCall(nestedCall)
.withExpressionsCompiler(expressionsCompiler);
itThrowsContextualError({
// act
throwingAction: (wrapError) => {
builder
.withErrorWrapper(wrapError)
.createCompiledNestedCall();
},
// assert
expectedWrappedError: expectedInnerError,
expectedContextMessage: expectedErrorMessage,
});
});
describe('compilation', () => {
describe('without arguments', () => {
it('matches nested call name', () => {
// arrange
const expectedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(expectedCall);
// act
const actualCall = builder.createCompiledNestedCall();
// assert
expect(actualCall.functionName).to.equal(expectedCall.functionName);
});
it('has no arguments or parameters', () => {
// arrange
const expectedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(expectedCall);
// act
const actualCall = builder.createCompiledNestedCall();
// assert
expect(actualCall.args.getAllParameterNames()).to.have.lengthOf(0);
});
it('does not compile expressions', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
const call = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(call)
.withExpressionsCompiler(expressionsCompilerStub);
// act
builder.createCompiledNestedCall();
// assert
expect(expressionsCompilerStub.callHistory).to.have.lengthOf(0);
});
});
describe('with arguments', () => {
it('matches nested call name', () => {
// arrange
const expectedName = 'expected-nested-function-call-name';
const nestedCall = new FunctionCallStub()
.withFunctionName(expectedName)
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withSomeArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(nestedCall);
// act
const call = builder.createCompiledNestedCall();
// assert
expect(call.functionName).to.equal(expectedName);
});
it('matches nested call parameters', () => {
// arrange
const expectedParameterNames = ['expectedFirstParameterName', 'expectedSecondParameterName'];
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArguments(expectedParameterNames.reduce((acc, name) => ({ ...acc, ...{ [name]: 'unimportant-value' } }), {})));
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(nestedCall);
// act
const call = builder.createCompiledNestedCall();
// assert
const actualParameterNames = call.args.getAllParameterNames();
expect(actualParameterNames.length).to.equal(expectedParameterNames.length);
expect(actualParameterNames).to.have.members(expectedParameterNames);
});
it('compiles args using parent parameters', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
const testParameterScenarios = [
{
parameterName: 'firstParameterName',
rawArgumentValue: 'first-raw-argument-value',
compiledArgumentValue: 'first-compiled-argument-value',
},
{
parameterName: 'secondParameterName',
rawArgumentValue: 'second-raw-argument-value',
compiledArgumentValue: 'second-compiled-argument-value',
},
];
const parentCall = new FunctionCallStub().withArgumentCollection(
new FunctionCallArgumentCollectionStub().withSomeArguments(),
);
testParameterScenarios.forEach(({ rawArgumentValue }) => {
expressionsCompilerStub.setup({
givenCode: rawArgumentValue,
givenArgs: parentCall.args,
result: testParameterScenarios.find(
(r) => r.rawArgumentValue === rawArgumentValue,
)?.compiledArgumentValue ?? 'unexpected arguments',
});
});
const nestedCallArgs = new FunctionCallArgumentCollectionStub()
.withArguments(testParameterScenarios.reduce((
acc,
{ parameterName, rawArgumentValue },
) => ({ ...acc, ...{ [parameterName]: rawArgumentValue } }), {}));
const nestedCall = new FunctionCallStub()
.withArgumentCollection(nestedCallArgs);
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.withParentFunctionCall(parentCall)
.withNestedFunctionCall(nestedCall);
// act
const compiledCall = builder.createCompiledNestedCall();
// assert
const expectedParameterNames = testParameterScenarios.map((p) => p.parameterName);
const actualParameterNames = compiledCall.args.getAllParameterNames();
expect(expectedParameterNames.length).to.equal(actualParameterNames.length);
expect(expectedParameterNames).to.have.members(actualParameterNames);
const getActualArgumentValue = (parameterName: string) => compiledCall
.args
.getArgument(parameterName)
.argumentValue;
testParameterScenarios.forEach(({ parameterName, compiledArgumentValue }) => {
expect(getActualArgumentValue(parameterName)).to.equal(compiledArgumentValue);
});
});
describe('when expression compiler returns empty', () => {
it('throws for required parameter', () => {
// arrange
const parameterName = 'requiredParameter';
const initialValue = 'initial-value';
const emptyCompiledExpression = '';
const expectedError = `Compilation resulted in empty value for required parameter: "${parameterName}"`;
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, initialValue));
const parentCall = new FunctionCallStub().withArgumentCollection(
new FunctionCallArgumentCollectionStub().withSomeArguments(),
);
const context = createContextWithParameter({
existingFunctionName: nestedCall.functionName,
existingParameterName: parameterName,
isExistingParameterOptional: false,
});
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: initialValue,
givenArgs: parentCall.args,
result: emptyCompiledExpression,
});
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.withParentFunctionCall(parentCall)
.withContext(context)
.withNestedFunctionCall(nestedCall);
// act
const act = () => builder.createCompiledNestedCall();
// assert
expect(act).to.throw(expectedError);
});
it('succeeds for optional parameter', () => {
// arrange
const parameterName = 'optionalParameter';
const initialValue = 'initial-value';
const emptyValue = '';
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, initialValue));
const parentCall = new FunctionCallStub().withArgumentCollection(
new FunctionCallArgumentCollectionStub().withSomeArguments(),
);
const context = createContextWithParameter({
existingFunctionName: nestedCall.functionName,
existingParameterName: parameterName,
isExistingParameterOptional: true,
});
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: initialValue,
givenArgs: parentCall.args,
result: emptyValue,
});
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.withParentFunctionCall(parentCall)
.withContext(context)
.withNestedFunctionCall(nestedCall);
// act
const compiledCall = builder.createCompiledNestedCall();
// assert
expect(compiledCall.args.hasArgument(parameterName)).toBeFalsy();
});
});
});
});
});
});
function createContextWithParameter(options: {
readonly existingFunctionName: string,
readonly existingParameterName: string,
readonly isExistingParameterOptional: boolean,
}): FunctionCallCompilationContext {
const parameters = new FunctionParameterCollectionStub()
.withParameterName(options.existingParameterName, options.isExistingParameterOptional);
const func = createSharedFunctionStubWithCode()
.withName(options.existingFunctionName)
.withParameters(parameters);
const functions = new SharedFunctionCollectionStub()
.withFunctions(func);
const context = new FunctionCallCompilationContextStub()
.withAllFunctions(functions);
return context;
}
class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub();
private nestedFunctionCall: FunctionCall = new FunctionCallStub();
private parentFunctionCall: FunctionCall = new FunctionCallStub();
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
this.expressionsCompiler = expressionsCompiler;
return this;
}
public withParentFunctionCall(parentFunctionCall: FunctionCall): this {
this.parentFunctionCall = parentFunctionCall;
return this;
}
public withNestedFunctionCall(nestedFunctionCall: FunctionCall): this {
this.nestedFunctionCall = nestedFunctionCall;
return this;
}
public withContext(context: FunctionCallCompilationContext): this {
this.context = context;
return this;
}
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
this.wrapError = wrapError;
return this;
}
public createCompiledNestedCall(): FunctionCall {
const compiler = new NestedFunctionArgumentCompiler(
this.expressionsCompiler,
this.wrapError,
);
return compiler.createCompiledNestedCall(
this.nestedFunctionCall,
this.parentFunctionCall,
this.context,
);
}
}

View File

@@ -0,0 +1,145 @@
import { expect, describe, it } from 'vitest';
import { InlineFunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler';
import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
describe('InlineFunctionCallCompiler', () => {
describe('canCompile', () => {
it('returns `true` if function has code body', () => {
// arrange
const expected = true;
const func = createSharedFunctionStubWithCode();
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
it('returns `false` if function does not have code body', () => {
// arrange
const expected = false;
const func = createSharedFunctionStubWithCalls();
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
});
describe('compile', () => {
it('throws if function body is not code', () => {
// arrange
const expectedError = 'Unexpected function body type.';
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const act = () => compiler.compileFunction(
createSharedFunctionStubWithCalls(),
new FunctionCallStub(),
);
// assert
expect(act).to.throw(expectedError);
});
it('compiles expressions with correct arguments', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
const expectedArgs = new FunctionCallArgumentCollectionStub();
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
compiler.compileFunction(
createSharedFunctionStubWithCode(),
new FunctionCallStub()
.withArgumentCollection(expectedArgs),
);
// assert
const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]);
expect(actualArgs.every((arg) => arg === expectedArgs));
});
describe('execute', () => {
it('creates compiled code with compiled `execute`', () => {
// arrange
const func = createSharedFunctionStubWithCode();
const args = new FunctionCallArgumentCollectionStub();
const expectedCode = 'expected-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.execute,
givenArgs: args,
result: expectedCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualCode = compiledCodes[0].code;
expect(actualCode).to.equal(expectedCode);
});
});
describe('revert', () => {
it('compiles to `undefined` when given `undefined`', () => {
// arrange
const expected = undefined;
const revertCode = undefined;
const func = createSharedFunctionStubWithCode()
.withRevertCode(revertCode);
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub());
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualRevertCode = compiledCodes[0].revertCode;
expect(actualRevertCode).to.equal(expected);
});
it('creates compiled revert code with compiled `revert`', () => {
// arrange
const revertCode = 'revert-code-input';
const func = createSharedFunctionStubWithCode()
.withRevertCode(revertCode);
const args = new FunctionCallArgumentCollectionStub();
const expectedRevertCode = 'expected-revert-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: revertCode,
givenArgs: args,
result: expectedRevertCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualRevertCode = compiledCodes[0].revertCode;
expect(actualRevertCode).to.equal(expectedRevertCode);
});
});
});
});
class InlineFunctionCallCompilerBuilder {
private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub();
public build(): InlineFunctionCallCompiler {
return new InlineFunctionCallCompiler(this.expressionsCompiler);
}
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
this.expressionsCompiler = expressionsCompiler;
return this;
}
}

View File

@@ -0,0 +1,84 @@
import { describe, it, expect } from 'vitest';
import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FunctionCallParser', () => {
describe('parseFunctionCalls', () => {
it('throws if call is not an object', () => {
// arrange
const expectedError = 'called function(s) must be an object';
const invalidCalls = ['string', 33, false];
invalidCalls.forEach((invalidCall) => {
// act
const act = () => parseFunctionCalls(invalidCall as never);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has undefined function name', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing function name in function call';
const data = [
new FunctionCallDataStub().withName('function-name'),
new FunctionCallDataStub().withName(absentValue),
];
// act
const act = () => parseFunctionCalls(data);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
it('parses single call as expected', () => {
// arrange
const expectedFunctionName = 'functionName';
const expectedParameterName = 'parameterName';
const expectedArgumentValue = 'argumentValue';
const data = new FunctionCallDataStub()
.withName(expectedFunctionName)
.withParameters({ [expectedParameterName]: expectedArgumentValue });
// act
const actual = parseFunctionCalls(data);
// assert
expect(actual).to.have.lengthOf(1);
const call = actual[0];
expect(call.functionName).to.equal(expectedFunctionName);
const { args } = call;
expect(args.getAllParameterNames()).to.have.lengthOf(1);
expect(args.hasArgument(expectedParameterName)).to.equal(
true,
`Does not include expected parameter: "${expectedParameterName}"\n`
+ `But includes: "${args.getAllParameterNames()}"`,
);
const argument = args.getArgument(expectedParameterName);
expect(argument.parameterName).to.equal(expectedParameterName);
expect(argument.argumentValue).to.equal(expectedArgumentValue);
});
it('parses multiple calls as expected', () => {
// arrange
const getFunctionName = (index: number) => `functionName${index}`;
const getParameterName = (index: number) => `parameterName${index}`;
const getArgumentValue = (index: number) => `argumentValue${index}`;
const createCall = (index: number) => new FunctionCallDataStub()
.withName(getFunctionName(index))
.withParameters({ [getParameterName(index)]: getArgumentValue(index) });
const calls = [createCall(0), createCall(1), createCall(2), createCall(3)];
// act
const actual = parseFunctionCalls(calls);
// assert
expect(actual).to.have.lengthOf(calls.length);
for (let i = 0; i < calls.length; i++) {
const call = actual[i];
const expectedParameterName = getParameterName(i);
const expectedArgumentValue = getArgumentValue(i);
expect(call.functionName).to.equal(getFunctionName(i));
expect(call.args.getAllParameterNames()).to.have.lengthOf(1);
expect(call.args.hasArgument(expectedParameterName)).to.equal(true);
const argument = call.args.getArgument(expectedParameterName);
expect(argument.parameterName).to.equal(expectedParameterName);
expect(argument.argumentValue).to.equal(expectedArgumentValue);
}
});
});
});

View File

@@ -0,0 +1,68 @@
import { describe, it, expect } from 'vitest';
import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall';
import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('ParsedFunctionCall', () => {
describe('ctor', () => {
describe('args', () => {
it('sets args as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
.withArgument('testParameter', 'testValue');
// act
const sut = new FunctionCallBuilder()
.withArgs(expected)
.build();
// assert
expect(sut.args).to.deep.equal(expected);
});
});
describe('functionName', () => {
describe('throws when function name is missing', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing function name in function call';
const functionName = absentValue;
// act
const act = () => new FunctionCallBuilder()
.withFunctionName(functionName)
.build();
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
it('sets function name as expected', () => {
// arrange
const expected = 'expectedFunctionName';
// act
const sut = new FunctionCallBuilder()
.withFunctionName(expected)
.build();
// assert
expect(sut.functionName).to.equal(expected);
});
});
});
});
class FunctionCallBuilder {
private functionName = 'functionName';
private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub();
public withFunctionName(functionName: string) {
this.functionName = functionName;
return this;
}
public withArgs(args: IReadOnlyFunctionCallArgumentCollection) {
this.args = args;
return this;
}
public build() {
return new ParsedFunctionCall(this.functionName, this.args);
}
}

View File

@@ -0,0 +1,26 @@
import type {
CallFunctionBody, CodeFunctionBody, SharedFunctionBody,
} from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import { FunctionBodyType } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
export function expectCodeFunctionBody(
body: SharedFunctionBody,
): asserts body is CodeFunctionBody {
expectBodyType(body, FunctionBodyType.Code);
}
export function expectCallsFunctionBody(
body: SharedFunctionBody,
): asserts body is CallFunctionBody {
expectBodyType(body, FunctionBodyType.Calls);
}
function expectBodyType(body: SharedFunctionBody, expectedType: FunctionBodyType) {
const actualType = body.type;
expect(actualType).to.equal(expectedType, formatAssertionMessage([
`Actual: ${FunctionBodyType[actualType]}`,
`Expected: ${FunctionBodyType[expectedType]}`,
`Body: ${JSON.stringify(body)}`,
]));
}

View File

@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest';
import { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter';
import { testParameterName } from '../../ParameterNameTestRunner';
describe('FunctionParameter', () => {
describe('name', () => {
testParameterName(
(parameterName) => new FunctionParameterBuilder()
.withName(parameterName)
.build()
.name,
);
});
describe('isOptional', () => {
describe('sets as expected', () => {
// arrange
const expectedValues = [true, false];
for (const expected of expectedValues) {
it(expected.toString(), () => {
// act
const sut = new FunctionParameterBuilder()
.withIsOptional(expected)
.build();
// expect
expect(sut.isOptional).to.equal(expected);
});
}
});
});
});
class FunctionParameterBuilder {
private name = 'parameterFromParameterBuilder';
private isOptional = false;
public withName(name: string) {
this.name = name;
return this;
}
public withIsOptional(isOptional: boolean) {
this.isOptional = isOptional;
return this;
}
public build() {
return new FunctionParameter(this.name, this.isOptional);
}
}

View File

@@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest';
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
describe('FunctionParameterCollection', () => {
it('all returns added parameters as expected', () => {
// arrange
const expected = [
new FunctionParameterStub().withName('1'),
new FunctionParameterStub().withName('2').withOptional(true),
new FunctionParameterStub().withName('3').withOptional(false),
];
const sut = new FunctionParameterCollection();
for (const parameter of expected) {
sut.addParameter(parameter);
}
// act
const actual = sut.all;
// assert
expect(expected).to.deep.equal(actual);
});
it('throws when function parameters have same names', () => {
// arrange
const parameterName = 'duplicate-parameter';
const expectedError = `duplicate parameter name: "${parameterName}"`;
const sut = new FunctionParameterCollection();
sut.addParameter(new FunctionParameterStub().withName(parameterName));
// act
const act = () => sut.addParameter(
new FunctionParameterStub().withName(parameterName),
);
// assert
expect(act).to.throw(expectedError);
});
});

View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { createFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
describe('FunctionParameterCollectionFactory', () => {
describe('createFunctionParameterCollection', () => {
describe('it is a transient factory', () => {
itIsTransientFactory({
getter: () => createFunctionParameterCollection(),
expectedType: FunctionParameterCollection,
});
});
it('returns an empty collection', () => {
// arrange
const expectedInitialParametersCount = 0;
// act
const collection = createFunctionParameterCollection();
// assert
expect(collection.all).to.have.lengthOf(expectedInitialParametersCount);
});
});
});

View File

@@ -0,0 +1,240 @@
import { describe, it, expect } from 'vitest';
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunction';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { type CallFunctionBody, FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import {
getAbsentStringTestCases, itEachAbsentCollectionValue,
itEachAbsentStringValue,
} from '@tests/unit/shared/TestCases/AbsentTests';
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
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);
});
describe('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);
}, { excludeNull: true, excludeUndefined: true });
});
});
});
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('createFunctionWithInlineCode', () => {
describe('code', () => {
it('sets as expected', () => {
// arrange
const expected = 'expected-code';
// act
const sut = new SharedFunctionBuilder()
.withCode(expected)
.createFunctionWithInlineCode();
// assert
expectCodeFunctionBody(sut.body);
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);
}, { excludeUndefined: true, excludeNull: true });
});
});
describe('revertCode', () => {
it('sets as expected', () => {
// arrange
const revertCodeTestValues: readonly (string | undefined)[] = [
'expected-revert-code',
...getAbsentStringTestCases({
excludeNull: true,
}).map((testCase) => testCase.absentValue),
];
for (const revertCode of revertCodeTestValues) {
// act
const sut = new SharedFunctionBuilder()
.withRevertCode(revertCode)
.createFunctionWithInlineCode();
// assert
expectCodeFunctionBody(sut.body);
expect(sut.body.code.revert).equal(revertCode);
}
});
});
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 as CallFunctionBody).calls).equal(expectedCalls);
});
});
describe('createCallerFunction', () => {
describe('rootCallSequence', () => {
it('sets as expected', () => {
// arrange
const expected = [
new FunctionCallStub().withFunctionName('firstFunction'),
new FunctionCallStub().withFunctionName('secondFunction'),
];
// act
const sut = new SharedFunctionBuilder()
.withRootCallSequence(expected)
.createCallerFunction();
// assert
expectCallsFunctionBody(sut.body);
expect(sut.body.calls).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentCollectionValue<FunctionCall>((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const rootCallSequence = absentValue;
const expectedError = `missing call sequence in function "${functionName}"`;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withRootCallSequence(rootCallSequence)
.createCallerFunction();
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true, excludeNull: true });
});
});
it('sets type as expected', () => {
// arrange
const expectedType = FunctionBodyType.Calls;
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
// assert
expect(sut.body.type).equal(expectedType);
});
});
});
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 FunctionCall[] = [new FunctionCallStub()];
private code = `[${SharedFunctionBuilder.name}] code`;
private revertCode: string | undefined = `[${SharedFunctionBuilder.name}] 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 | undefined) {
this.revertCode = revertCode;
return this;
}
public withRootCallSequence(callSequence: readonly FunctionCall[]) {
this.callSequence = callSequence;
return this;
}
}

View File

@@ -0,0 +1,77 @@
import { describe, it, expect } from 'vitest';
import { SharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection';
import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('SharedFunctionCollection', () => {
describe('addFunction', () => {
it('throws if function with same name already exists', () => {
// arrange
const functionName = 'duplicate-function';
const expectedError = `function with name ${functionName} already exists`;
const func = createSharedFunctionStubWithCode()
.withName('duplicate-function');
const sut = new SharedFunctionCollection();
sut.addFunction(func);
// act
const act = () => sut.addFunction(func);
// assert
expect(act).to.throw(expectedError);
});
});
describe('getFunctionByName', () => {
describe('throws if name is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing function name';
const sut = new SharedFunctionCollection();
// act
const act = () => sut.getFunctionByName(absentValue);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
it('throws if function does not exist', () => {
// arrange
const name = 'unique-name';
const expectedError = `Called function is not defined: "${name}"`;
const func = createSharedFunctionStubWithCode()
.withName('unexpected-name');
const sut = new SharedFunctionCollection();
sut.addFunction(func);
// act
const act = () => sut.getFunctionByName(name);
// assert
expect(act).to.throw(expectedError);
});
describe('returns existing function', () => {
it('when function with inline code is added', () => {
// arrange
const expected = createSharedFunctionStubWithCode()
.withName('expected-function-name');
const sut = new SharedFunctionCollection();
// act
sut.addFunction(expected);
const actual = sut.getFunctionByName(expected.name);
// assert
expect(actual).to.equal(expected);
});
it('when calling function is added', () => {
// arrange
const callee = createSharedFunctionStubWithCode()
.withName('calleeFunction');
const caller = createSharedFunctionStubWithCalls()
.withName('callerFunction')
.withCalls(new FunctionCallStub().withFunctionName(callee.name));
const sut = new SharedFunctionCollection();
// act
sut.addFunction(callee);
sut.addFunction(caller);
const actual = sut.getFunctionByName(caller.name);
// assert
expect(actual).to.equal(caller);
});
});
});
});

View File

@@ -0,0 +1,403 @@
import { describe, it, expect } from 'vitest';
import type { FunctionData, CodeInstruction } from '@/application/collections/';
import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionsParser, type FunctionParameterFactory } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
import { createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
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 { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
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 { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
describe('SharedFunctionsParser', () => {
describe('instance', () => {
itIsSingletonFactory({
getter: () => SharedFunctionsParser.instance,
expectedType: SharedFunctionsParser,
});
});
describe('parseFunctions', () => {
describe('validates functions', () => {
it('throws when functions have no names', () => {
// arrange
const invalidFunctions = [
createFunctionDataWithCode()
.withCode('test function 1')
.withName(' '), // Whitespace,
createFunctionDataWithCode()
.withCode('test function 2')
.withName(undefined as unknown as string), // Undefined
createFunctionDataWithCode()
.withCode('test function 3')
.withName(''), // Empty
];
const expectedError = `Some function(s) have no names:\n${invalidFunctions.map((f) => JSON.stringify(f)).join('\n')}`;
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(invalidFunctions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
it('throws when functions have same names', () => {
// arrange
const name = 'same-func-name';
const expectedError = `duplicate function name: "${name}"`;
const functions = [
createFunctionDataWithCode().withName(name),
createFunctionDataWithCode().withName(name),
];
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
describe('throws when when function have duplicate code', () => {
it('code', () => {
// arrange
const code = 'duplicate-code';
const expectedError = `duplicate "code" in functions: "${code}"`;
const functions = [
createFunctionDataWithoutCallOrCode().withName('func-1').withCode(code),
createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code),
];
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
it('revertCode', () => {
// arrange
const revertCode = 'duplicate-revert-code';
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
const functions = [
createFunctionDataWithoutCallOrCode()
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
createFunctionDataWithoutCallOrCode()
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
];
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
describe('ensures either call or code is defined', () => {
it('both code and call are defined', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
const invalidFunction = createFunctionDataWithoutCallOrCode()
.withName(functionName)
.withCode('code')
.withMockCall();
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([invalidFunction])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
it('neither code and call is defined', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
const invalidFunction = createFunctionDataWithoutCallOrCode()
.withName(functionName);
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([invalidFunction])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when parameters type is not as expected', () => {
const testScenarios = [
{
state: 'when not an array',
invalidType: 5,
},
{
state: 'when array but not of objects',
invalidType: ['a', { a: 'b' }],
},
];
for (const testCase of testScenarios) {
it(testCase.state, () => {
// arrange
const func = createFunctionDataWithCode()
.withParametersObject(testCase.invalidType as never);
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([func])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
}
});
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 ParseFunctionsCallerWithDefaults()
.withFunctions([functionData])
.withValidator(validator)
.parseFunctions();
// assert
validator.assertHistory({
validatedCodes: [functionData.code, functionData.revertCode],
rules: expectedRules,
});
});
describe('parameter creation', () => {
describe('rethrows including function name when creating parameter throws', () => {
// arrange
const invalidParameterName = 'invalid-function-parameter-name';
const functionName = 'functionName';
const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`;
const expectedInnerError = new Error('injected error');
const parameterFactory: FunctionParameterFactory = () => {
throw expectedInnerError;
};
const functionData = createFunctionDataWithCode()
.withName(functionName)
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
itThrowsContextualError({
// act
throwingAction: (wrapError) => {
new ParseFunctionsCallerWithDefaults()
.withFunctions([functionData])
.withFunctionParameterFactory(parameterFactory)
.withErrorWrapper(wrapError)
.parseFunctions();
},
// assert
expectedWrappedError: expectedInnerError,
expectedContextMessage: expectedErrorMessage,
});
});
});
});
describe('given empty functions, returns empty collection', () => {
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
// act
const actual = new ParseFunctionsCallerWithDefaults()
.withFunctions(absentValue)
.parseFunctions();
// assert
expect(actual).to.not.equal(undefined);
}, { excludeUndefined: true, excludeNull: true });
});
describe('function with inline code', () => {
it('parses single function with code as expected', () => {
// arrange
const name = 'function-name';
const expected = createFunctionDataWithoutCallOrCode()
.withName(name)
.withCode('expected-code')
.withRevertCode('expected-revert-code')
.withParameters(
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
);
// act
const collection = new ParseFunctionsCallerWithDefaults()
.withFunctions([expected])
.parseFunctions();
// expect
const actual = collection.getFunctionByName(name);
expectEqualName(expected, actual);
expectEqualParameters(expected, actual);
expectEqualFunctionWithInlineCode(expected, actual);
});
});
describe('function with calls', () => {
it('parses single function with call as expected', () => {
// arrange
const call = new FunctionCallDataStub()
.withName('calleeFunction')
.withParameters({ test: 'value' });
const data = createFunctionDataWithoutCallOrCode()
.withName('caller-function')
.withCall(call);
// act
const collection = new ParseFunctionsCallerWithDefaults()
.withFunctions([data])
.parseFunctions();
// expect
const actual = collection.getFunctionByName(data.name);
expectEqualName(data, actual);
expectEqualParameters(data, actual);
expectEqualCalls([call], actual);
});
it('parses multiple functions with call as expected', () => {
// arrange
const call1 = new FunctionCallDataStub()
.withName('calleeFunction1')
.withParameters({ param: 'value' });
const call2 = new FunctionCallDataStub()
.withName('calleeFunction2')
.withParameters({ param2: 'value2' });
const caller1 = createFunctionDataWithoutCallOrCode()
.withName('caller-function')
.withCall(call1);
const caller2 = createFunctionDataWithoutCallOrCode()
.withName('caller-function-2')
.withCall([call1, call2]);
// act
const collection = new ParseFunctionsCallerWithDefaults()
.withFunctions([caller1, caller2])
.parseFunctions();
// expect
const compiledCaller1 = collection.getFunctionByName(caller1.name);
expectEqualName(caller1, compiledCaller1);
expectEqualParameters(caller1, compiledCaller1);
expectEqualCalls([call1], compiledCaller1);
const compiledCaller2 = collection.getFunctionByName(caller2.name);
expectEqualName(caller2, compiledCaller2);
expectEqualParameters(caller2, compiledCaller2);
expectEqualCalls([call1, call2], compiledCaller2);
});
});
});
});
class ParseFunctionsCallerWithDefaults {
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
private codeValidator: ICodeValidator = new CodeValidatorStub();
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
private parameterFactory: FunctionParameterFactory = (
name: string,
isOptional: boolean,
) => new FunctionParameterStub()
.withName(name)
.withOptional(isOptional);
private parameterCollectionFactory
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
public withSyntax(syntax: ILanguageSyntax) {
this.syntax = syntax;
return this;
}
public withValidator(codeValidator: ICodeValidator) {
this.codeValidator = codeValidator;
return this;
}
public withFunctions(functions: readonly FunctionData[]) {
this.functions = functions;
return this;
}
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
this.wrapError = wrapError;
return this;
}
public withFunctionParameterFactory(parameterFactory: FunctionParameterFactory): this {
this.parameterFactory = parameterFactory;
return this;
}
public withParameterCollectionFactory(
parameterCollectionFactory: FunctionParameterCollectionFactory,
): this {
this.parameterCollectionFactory = parameterCollectionFactory;
return this;
}
public parseFunctions() {
const sut = new SharedFunctionsParser(
{
codeValidator: this.codeValidator,
wrapError: this.wrapError,
createParameter: this.parameterFactory,
createParameterCollection: this.parameterCollectionFactory,
},
);
return sut.parseFunctions(this.functions, this.syntax);
}
}
function expectEqualName(expected: FunctionData, actual: ISharedFunction): void {
expect(actual.name).to.equal(expected.name);
}
function expectEqualParameters(expected: FunctionData, actual: ISharedFunction): void {
const actualSimplifiedParameters = actual.parameters.all.map((parameter) => ({
name: parameter.name,
optional: parameter.isOptional,
}));
const expectedSimplifiedParameters = expected.parameters?.map((parameter) => ({
name: parameter.name,
optional: parameter.optional || false,
})) || [];
expect(expectedSimplifiedParameters).to.deep.equal(actualSimplifiedParameters, 'Unequal parameters');
}
function expectEqualFunctionWithInlineCode(
expected: CodeInstruction,
actual: ISharedFunction,
): void {
expect(actual.body, `function "${actual.name}" has no body`);
expectCodeFunctionBody(actual.body);
expect(actual.body.code, `function "${actual.name}" has no code`);
expect(actual.body.code.execute).to.equal(expected.code);
expect(actual.body.code.revert).to.equal(expected.revertCode);
}
function expectEqualCalls(
expected: FunctionCallDataStub[],
actual: ISharedFunction,
) {
expect(actual.body, `function "${actual.name}" has no body`);
expectCallsFunctionBody(actual.body);
expect(actual.body.calls, `function "${actual.name}" has no calls`);
const actualSimplifiedCalls = actual.body.calls
.map((call) => ({
function: call.functionName,
params: call.args.getAllParameterNames().map((name) => ({
name, value: call.args.getArgument(name).argumentValue,
})),
}));
const expectedSimplifiedCalls = expected
.map((call) => ({
function: call.function,
params: Object.keys(call.parameters).map((key) => (
{ name: key, value: call.parameters[key] }
)),
}));
expect(actualSimplifiedCalls).to.deep.equal(expectedSimplifiedCalls, 'Unequal calls');
}