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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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).`;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user