Fix compiler bug with nested optional arguments

This commit fixes compiler bug where it fails when optional values are
compiled into absent values in nested calls.

- Throw exception with more context for easier future debugging.
- Add better validation of argument values for nested calls.
- Refactor `FunctionCallCompiler` for better clarity and modularize it
  to make it more maintainable and testable.
- Refactor related interface to not have `I` prefix, and
  function/variable names for better clarity.

Context:

Discovered this issue while attempting to call
`RunInlineCodeAsTrustedInstaller` which in turn invokes `RunPowerShell`
for issue #246. This led to the realization that despite parameters
flagged as optional, the nested argument compilation didn't support
them.
This commit is contained in:
undergroundwires
2023-09-16 16:11:41 +02:00
parent a1f2497381
commit 53222fd83c
49 changed files with 1938 additions and 772 deletions

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { expectThrowsError } from '@tests/unit/shared/Assertions/ExpectThrowsError';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
@@ -67,7 +67,7 @@ describe('NodeValidator', () => {
// act
const act = () => sut.assert(falsePredicate, message);
// assert
expectThrowsError(act, expected);
expectDeepThrowsError(act, expected);
});
it('does not throw if condition is true', () => {
// arrange
@@ -89,7 +89,7 @@ describe('NodeValidator', () => {
// act
const act = () => sut.throw(message);
// assert
expectThrowsError(act, expected);
expectDeepThrowsError(act, expected);
});
});
});

View File

@@ -2,7 +2,7 @@ import { describe, it } from 'vitest';
import { NodeDataError, INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests';
import { expectThrowsError } from '@tests/unit/shared/Assertions/ExpectThrowsError';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
export interface ITestScenario {
readonly act: () => void;
@@ -82,6 +82,6 @@ export function expectThrowsNodeError(
// act
const act = () => test.act();
// assert
expectThrowsError(act, expected);
expectDeepThrowsError(act, expected);
return this;
}

View File

@@ -0,0 +1,101 @@
import { expect, describe, it } from 'vitest';
import { NewlineCodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('NewlineCodeSegmentMerger', () => {
describe('mergeCodeParts', () => {
describe('throws given empty segments', () => {
itEachAbsentCollectionValue((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);
});
});
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().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',
},
})),
{
description: 'given only `code` in segments',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(''),
new CompiledCodeStub().withCode('code2').withRevertCode(''),
],
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

@@ -1,522 +0,0 @@
import { describe, it, expect } from 'vitest';
import type { FunctionCallParametersData } from '@/application/collections/';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
describe('FunctionCallCompiler', () => {
describe('instance', () => {
itIsSingleton({
getter: () => FunctionCallCompiler.instance,
expectedType: FunctionCallCompiler,
});
});
describe('compileCall', () => {
describe('parameter validation', () => {
describe('call', () => {
describe('throws with missing call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing calls';
const call = absentValue;
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has absent call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing function call';
const call = [
new FunctionCallStub(),
absentValue,
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call parameters does not match function parameters', () => {
// arrange
const functionName = 'test-function-name';
const testCases = [
{
name: 'provided: single unexpected parameter, when: another expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter"',
},
{
name: '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"`
+ '. Expected parameter(s): "expected-parameter"',
},
{
name: '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"`
+ '. Expected parameter(s): "expected-parameter1", "expected-parameter2"',
},
{
name: 'provided: an unexpected parameter, when: none required',
functionParameters: [],
callParameters: ['unexpected-call-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
+ '. Expected parameter(s): none',
},
{
name: '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"`
+ '. Expected parameter(s): "expected-parameter"',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const func = new SharedFunctionStub(FunctionBodyType.Code)
.withName('test-function-name')
.withParameterNames(...testCase.functionParameters);
const params = testCase.callParameters
.reduce((result, parameter) => {
return { ...result, [parameter]: 'defined-parameter-value ' };
}, {} as FunctionCallParametersData);
const call = new FunctionCallStub()
.withFunctionName(func.name)
.withArguments(params);
const functions = new SharedFunctionCollectionStub()
.withFunction(func);
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall([call], functions);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
});
describe('functions', () => {
describe('throws with missing functions', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing functions';
const call = new FunctionCallStub();
const functions = absentValue;
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall([call], functions);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if function does not exist', () => {
// arrange
const expectedError = 'function does not exist';
const call = new FunctionCallStub();
const functions: ISharedFunctionCollection = {
getFunctionByName: () => { throw new Error(expectedError); },
};
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall([call], functions);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('builds code as expected', () => {
describe('builds single call as expected', () => {
// arrange
const parametersTestCases = [
{
name: 'empty parameters',
parameters: [],
callArgs: { },
},
{
name: 'non-empty parameters',
parameters: ['param1', 'param2'],
callArgs: { param1: 'value1', param2: 'value2' },
},
];
for (const testCase of parametersTestCases) {
it(testCase.name, () => {
const expected = {
execute: 'expected code (execute)',
revert: 'expected code (revert)',
};
const func = new SharedFunctionStub(FunctionBodyType.Code)
.withParameterNames(...testCase.parameters);
const functions = new SharedFunctionCollectionStub().withFunction(func);
const call = new FunctionCallStub()
.withFunctionName(func.name)
.withArguments(testCase.callArgs);
const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs);
const { code } = func.body;
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ givenCode: code.execute, givenArgs: args, result: expected.execute })
.setup({ givenCode: code.revert, givenArgs: args, result: expected.revert });
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall([call], functions);
// assert
expect(actual.code).to.equal(expected.execute);
expect(actual.revertCode).to.equal(expected.revert);
});
}
});
it('builds call sequence as expected', () => {
// arrange
const firstFunction = new SharedFunctionStub(FunctionBodyType.Code)
.withName('first-function-name')
.withCode('first-function-code')
.withRevertCode('first-function-revert-code');
const secondFunction = new SharedFunctionStub(FunctionBodyType.Code)
.withName('second-function-name')
.withParameterNames('testParameter')
.withCode('second-function-code')
.withRevertCode('second-function-revert-code');
const secondCallArguments = { testParameter: 'testValue' };
const calls = [
new FunctionCallStub()
.withFunctionName(firstFunction.name)
.withArguments({}),
new FunctionCallStub()
.withFunctionName(secondFunction.name)
.withArguments(secondCallArguments),
];
const firstFunctionCallArgs = new FunctionCallArgumentCollectionStub();
const secondFunctionCallArgs = new FunctionCallArgumentCollectionStub()
.withArguments(secondCallArguments);
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs)
.setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs);
const expectedExecute = `${firstFunction.body.code.execute}\n${secondFunction.body.code.execute}`;
const expectedRevert = `${firstFunction.body.code.revert}\n${secondFunction.body.code.revert}`;
const functions = new SharedFunctionCollectionStub()
.withFunction(firstFunction)
.withFunction(secondFunction);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall(calls, functions);
// assert
expect(actual.code).to.equal(expectedExecute);
expect(actual.revertCode).to.equal(expectedRevert);
});
describe('can compile a call tree (function calling another)', () => {
describe('single deep function call', () => {
it('builds 2nd level of depth without arguments', () => {
// arrange
const emptyArgs = new FunctionCallArgumentCollectionStub();
const deepFunctionName = 'deepFunction';
const functions = {
deep: new SharedFunctionStub(FunctionBodyType.Code)
.withName(deepFunctionName)
.withCode('deep function code')
.withRevertCode('deep function final code'),
front: new SharedFunctionStub(FunctionBodyType.Calls)
.withName('frontFunction')
.withCalls(new FunctionCallStub()
.withFunctionName(deepFunctionName)
.withArgumentCollection(emptyArgs)),
};
const expected = {
code: 'final code',
revert: 'final revert code',
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({
givenCode: functions.deep.body.code.execute,
givenArgs: emptyArgs,
result: expected.code,
})
.setup({
givenCode: functions.deep.body.code.revert,
givenArgs: emptyArgs,
result: expected.revert,
});
const mainCall = new FunctionCallStub()
.withFunctionName(functions.front.name)
.withArgumentCollection(emptyArgs);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall(
[mainCall],
new SharedFunctionCollectionStub().withFunction(functions.deep, functions.front),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
it('builds 2nd level of depth by compiling arguments', () => {
// arrange
const scenario = {
front: {
functionName: 'frontFunction',
parameterName: 'frontFunctionParameterName',
args: {
fromMainCall: 'initial argument to be compiled',
toNextStatic: 'value from "front" to "deep" in function definition',
toNextCompiled: 'argument from "front" to "deep" (compiled)',
},
callArgs: {
initialFromMainCall: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.front.parameterName, scenario.front.args.fromMainCall),
expectedCallDeep: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.deep.parameterName, scenario.front.args.toNextCompiled),
},
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName(scenario.front.functionName)
.withParameterNames(scenario.front.parameterName)
.withCalls(new FunctionCallStub()
.withFunctionName(scenario.deep.functionName)
.withArgument(scenario.deep.parameterName, scenario.front.args.toNextStatic)),
},
deep: {
functionName: 'deepFunction',
parameterName: 'deepFunctionParameterName',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(scenario.deep.functionName)
.withParameterNames(scenario.deep.parameterName)
.withCode(`${scenario.deep.functionName} function code`)
.withRevertCode(`${scenario.deep.functionName} function revert code`),
},
};
const expected = {
code: 'final code',
revert: 'final revert code',
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ // Front ===args===> Deep
givenCode: scenario.front.args.toNextStatic,
givenArgs: scenario.front.callArgs.initialFromMainCall(),
result: scenario.front.args.toNextCompiled,
})
// set-up compiling of deep, compiled argument should be sent
.setup({
givenCode: scenario.deep.getFunction().body.code.execute,
givenArgs: scenario.front.callArgs.expectedCallDeep(),
result: expected.code,
})
.setup({
givenCode: scenario.deep.getFunction().body.code.revert,
givenArgs: scenario.front.callArgs.expectedCallDeep(),
result: expected.revert,
});
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall(
[
new FunctionCallStub()
.withFunctionName(scenario.front.functionName)
.withArgumentCollection(scenario.front.callArgs.initialFromMainCall()),
],
new SharedFunctionCollectionStub().withFunction(
scenario.deep.getFunction(),
scenario.front.getFunction(),
),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
it('builds 3rd level of depth by compiling arguments', () => {
// arrange
const scenario = {
first: {
functionName: 'firstFunction',
parameter: 'firstParameter',
args: {
fromMainCall: 'initial argument to be compiled',
toNextStatic: 'value from "first" to "second" in function definition',
toNextCompiled: 'argument from "first" to "second" (compiled)',
},
callArgs: {
initialFromMainCall: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.first.parameter, scenario.first.args.fromMainCall),
expectedToSecond: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.second.parameter, scenario.first.args.toNextCompiled),
},
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName(scenario.first.functionName)
.withParameterNames(scenario.first.parameter)
.withCalls(new FunctionCallStub()
.withFunctionName(scenario.second.functionName)
.withArgument(scenario.second.parameter, scenario.first.args.toNextStatic)),
},
second: {
functionName: 'secondFunction',
parameter: 'secondParameter',
args: {
toNextCompiled: 'argument second to third (compiled)',
toNextStatic: 'calling second to third',
},
callArgs: {
expectedToThird: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.third.parameter, scenario.second.args.toNextCompiled),
},
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName(scenario.second.functionName)
.withParameterNames(scenario.second.parameter)
.withCalls(new FunctionCallStub()
.withFunctionName(scenario.third.functionName)
.withArgument(scenario.third.parameter, scenario.second.args.toNextStatic)),
},
third: {
functionName: 'thirdFunction',
parameter: 'thirdParameter',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(scenario.third.functionName)
.withParameterNames(scenario.third.parameter)
.withCode(`${scenario.third.functionName} function code`)
.withRevertCode(`${scenario.third.functionName} function revert code`),
},
};
const expected = {
code: 'final code',
revert: 'final revert code',
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ // First ===args===> Second
givenCode: scenario.first.args.toNextStatic,
givenArgs: scenario.first.callArgs.initialFromMainCall(),
result: scenario.first.args.toNextCompiled,
})
.setup({ // Second ===args===> third
givenCode: scenario.second.args.toNextStatic,
givenArgs: scenario.first.callArgs.expectedToSecond(),
result: scenario.second.args.toNextCompiled,
})
// Compiling of third functions code with expected arguments
.setup({
givenCode: scenario.third.getFunction().body.code.execute,
givenArgs: scenario.second.callArgs.expectedToThird(),
result: expected.code,
})
.setup({
givenCode: scenario.third.getFunction().body.code.revert,
givenArgs: scenario.second.callArgs.expectedToThird(),
result: expected.revert,
});
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
const mainCall = new FunctionCallStub()
.withFunctionName(scenario.first.functionName)
.withArgumentCollection(scenario.first.callArgs.initialFromMainCall());
// act
const actual = sut.compileCall(
[mainCall],
new SharedFunctionCollectionStub().withFunction(
scenario.first.getFunction(),
scenario.second.getFunction(),
scenario.third.getFunction(),
),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
});
describe('multiple deep function calls', () => {
it('builds 2nd level of depth without arguments', () => {
// arrange
const emptyArgs = new FunctionCallArgumentCollectionStub();
const functions = {
call1: {
deep: {
functionName: 'deepFunction',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(functions.call1.deep.functionName)
.withCode('deep function (1) code')
.withRevertCode('deep function (1) final code'),
},
front: {
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName('frontFunction')
.withCalls(new FunctionCallStub()
.withFunctionName(functions.call1.deep.functionName)
.withArgumentCollection(emptyArgs)),
},
},
call2: {
deep: {
functionName: 'deepFunction2',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(functions.call2.deep.functionName)
.withCode('deep function (2) code')
.withRevertCode('deep function (2) final code'),
},
front: {
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName('frontFunction2')
.withCalls(new FunctionCallStub()
.withFunctionName(functions.call2.deep.functionName)
.withArgumentCollection(emptyArgs)),
},
},
getMainCall: () => [
new FunctionCallStub()
.withFunctionName(functions.call1.front.getFunction().name)
.withArgumentCollection(emptyArgs),
new FunctionCallStub()
.withFunctionName(functions.call2.front.getFunction().name)
.withArgumentCollection(emptyArgs),
],
getCollection: () => new SharedFunctionCollectionStub().withFunction(
functions.call1.deep.getFunction(),
functions.call1.front.getFunction(),
functions.call2.deep.getFunction(),
functions.call2.front.getFunction(),
),
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setupToReturnFunctionCode(functions.call1.deep.getFunction(), emptyArgs)
.setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
const expected = {
code: `${functions.call1.deep.getFunction().body.code.execute}\n${functions.call2.deep.getFunction().body.code.execute}`,
revert: `${functions.call1.deep.getFunction().body.code.revert}\n${functions.call2.deep.getFunction().body.code.revert}`,
};
// act
const actual = sut.compileCall(
functions.getMainCall(),
functions.getCollection(),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
});
});
});
});
});
class MockableFunctionCallCompiler extends FunctionCallCompiler {
constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) {
super(expressionsCompiler);
}
}

View File

@@ -0,0 +1,251 @@
/* eslint-disable max-classes-per-file */
import { describe, it, expect } from 'vitest';
import { FunctionCallSequenceCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { FunctionCall } from '@/application/Parser/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 { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
describe('FunctionCallSequenceCompiler', () => {
describe('instance', () => {
itIsSingleton({
getter: () => FunctionCallSequenceCompiler.instance,
expectedType: FunctionCallSequenceCompiler,
});
});
describe('compileFunctionCalls', () => {
describe('parameter validation', () => {
describe('calls', () => {
describe('throws with missing call', () => {
itEachAbsentObjectValue((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);
});
});
describe('throws if call sequence has absent call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing function call';
const calls = [
new FunctionCallStub(),
absentValue,
];
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('functions', () => {
describe('throws with missing functions', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing functions';
const functions = absentValue;
const builder = new FunctionCallSequenceCompilerBuilder()
.withFunctions(functions);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
});
});
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');
expect(calledMethod).toBeDefined();
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');
expect(calledMethod).toBeDefined();
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 [actualSegments] = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts').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,240 @@
import { expect, describe, it } from 'vitest';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { NestedFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler';
import { ArgumentCompiler } from '@/application/Parser/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 { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
describe('NestedFunctionCallCompiler', () => {
describe('canCompile', () => {
it('returns `true` for code body function', () => {
// arrange
const expected = true;
const func = new SharedFunctionStub(FunctionBodyType.Calls)
.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 = new SharedFunctionStub(FunctionBodyType.Code);
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 { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunc, 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 { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunc, 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(callToFrontFunc);
});
it('uses correct nested call', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunc, 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(callToFrontFunc);
});
});
describe('re-compilation with compiled args', () => {
it('uses correct context', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
compiler.compileFunction(frontFunc, 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 { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompilerStub)
.build();
// act
compiler.compileFunction(frontFunc, 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 = new SharedFunctionStub(FunctionBodyType.Code);
const deepFunc2 = new SharedFunctionStub(FunctionBodyType.Code);
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 frontFunc = new SharedFunctionStub(FunctionBodyType.Calls)
.withCalls(callToDeepFunc1, callToDeepFunc2);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.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(frontFunc, callToFrontFunc, expectedContext);
// assert
expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length);
expect(actualCodes).to.have.members(expectedFlattenedCodes);
});
describe('error handling', () => {
it('handles argument compiler errors', () => {
// arrange
const argumentCompilerError = new Error('Test error');
const argumentCompilerStub = new ArgumentCompilerStub();
argumentCompilerStub.createCompiledNestedCall = () => {
throw argumentCompilerError;
};
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const expectedError = new AggregateError(
[argumentCompilerError],
`Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`,
);
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompilerStub)
.build();
// act
const act = () => compiler.compileFunction(
frontFunc,
callToFrontFunc,
new FunctionCallCompilationContextStub(),
);
// assert
expectDeepThrowsError(act, expectedError);
});
it('handles single call compiler errors', () => {
// arrange
const singleCallCompilerError = new Error('Test error');
const singleCallCompiler = new SingleCallCompilerStub();
singleCallCompiler.compileSingleCall = () => {
throw singleCallCompilerError;
};
const context = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompiler);
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const expectedError = new AggregateError(
[singleCallCompilerError],
`Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`,
);
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
const act = () => compiler.compileFunction(
frontFunc,
callToFrontFunc,
context,
);
// assert
expectDeepThrowsError(act, expectedError);
});
});
});
});
function createSingleFuncCallingAnotherFunc() {
const deepFunc = new SharedFunctionStub(FunctionBodyType.Code);
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name);
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls).withCalls(callToDeepFunc);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
return {
deepFunc,
frontFunc,
callToFrontFunc,
callToDeepFunc,
};
}
class NestedFunctionCallCompilerBuilder {
private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub();
public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this {
this.argumentCompiler = argumentCompiler;
return this;
}
public build(): NestedFunctionCallCompiler {
return new NestedFunctionCallCompiler(
this.argumentCompiler,
);
}
}

View File

@@ -0,0 +1,259 @@
import { expect, describe, it } from 'vitest';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionStub } 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/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler';
import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy';
import { SingleCallCompilerStrategyStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStrategyStub';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
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"`
+ '. Expected 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"`
+ '. Expected 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"`
+ '. Expected 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"`
+ '. Expected 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"`
+ '. Expected parameter(s): "expected-parameter"',
},
];
testCases.forEach(({
description, functionParameters, callParameters, expectedError,
}) => {
it(description, () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code)
.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
expect(act).to.throw(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 = new SharedFunctionStub(FunctionBodyType.Code);
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 = new SharedFunctionStub(FunctionBodyType.Code);
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,290 @@
import { expect, describe, it } from 'vitest';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { NestedFunctionArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler';
import { IExpressionsCompiler } from '@/application/Parser/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 { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
describe('NestedFunctionArgumentCompiler', () => {
describe('createCompiledNestedCall', () => {
it('should handle error from expressions compiler', () => {
// arrange
const parameterName = '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 expressionsCompilerError = new Error('child-');
const expectedError = new AggregateError(
[expressionsCompilerError],
`Error when compiling argument for "${parameterName}"`,
);
const expressionsCompiler = new ExpressionsCompilerStub();
expressionsCompiler.compileExpressions = () => { throw expressionsCompilerError; };
const builder = new NestedFunctionArgumentCompilerBuilder()
.withParentFunctionCall(parentCall)
.withNestedFunctionCall(nestedCall)
.withExpressionsCompiler(expressionsCompiler);
// act
const act = () => builder.createCompiledNestedCall();
// assert
expectDeepThrowsError(act, expectedError);
});
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,
});
});
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 compiledValue = undefined;
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: compiledValue,
});
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 compiledValue = undefined;
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: compiledValue,
});
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 = new SharedFunctionStub(FunctionBodyType.Code)
.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();
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 createCompiledNestedCall(): FunctionCall {
const compiler = new NestedFunctionArgumentCompiler(this.expressionsCompiler);
return compiler.createCompiledNestedCall(
this.nestedFunctionCall,
this.parentFunctionCall,
this.context,
);
}
}

View File

@@ -0,0 +1,111 @@
import { expect, describe, it } from 'vitest';
import { InlineFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { IExpressionsCompiler } from '@/application/Parser/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 = new SharedFunctionStub(FunctionBodyType.Code);
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 = new SharedFunctionStub(FunctionBodyType.Calls);
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
});
describe('compile', () => {
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(
new SharedFunctionStub(FunctionBodyType.Code),
new FunctionCallStub()
.withArgumentCollection(expectedArgs),
);
// assert
const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]);
expect(actualArgs.every((arg) => arg === expectedArgs));
});
it('creates compiled code with compiled `execute`', () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code);
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);
});
it('creates compiled revert code with compiled `revert`', () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code);
const args = new FunctionCallArgumentCollectionStub();
const expectedRevertCode = 'expected-revert-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.revert,
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

@@ -1,10 +1,10 @@
import { describe, it, expect } from 'vitest';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FunctionCall', () => {
describe('ParsedFunctionCall', () => {
describe('ctor', () => {
describe('args', () => {
describe('throws when args is missing', () => {
@@ -76,6 +76,6 @@ class FunctionCallBuilder {
}
public build() {
return new FunctionCall(this.functionName, this.args);
return new ParsedFunctionCall(this.functionName, this.args);
}
}

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import {
@@ -132,7 +132,7 @@ describe('SharedFunction', () => {
});
});
describe('createCallerFunction', () => {
describe('callSequence', () => {
describe('rootCallSequence', () => {
it('sets as expected', () => {
// arrange
const expected = [
@@ -141,7 +141,7 @@ describe('SharedFunction', () => {
];
// act
const sut = new SharedFunctionBuilder()
.withCallSequence(expected)
.withRootCallSequence(expected)
.createCallerFunction();
// assert
expect(sut.body.calls).equal(expected);
@@ -150,12 +150,12 @@ describe('SharedFunction', () => {
itEachAbsentCollectionValue((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const callSequence = absentValue;
const rootCallSequence = absentValue;
const expectedError = `missing call sequence in function "${functionName}"`;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCallSequence(callSequence)
.withRootCallSequence(rootCallSequence)
.createCallerFunction();
// assert
expect(act).to.throw(expectedError);
@@ -206,7 +206,7 @@ class SharedFunctionBuilder {
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private callSequence: readonly IFunctionCall[] = [new FunctionCallStub()];
private callSequence: readonly FunctionCall[] = [new FunctionCallStub()];
private code = 'code';
@@ -249,7 +249,7 @@ class SharedFunctionBuilder {
return this;
}
public withCallSequence(callSequence: readonly IFunctionCall[]) {
public withRootCallSequence(callSequence: readonly FunctionCall[]) {
this.callSequence = callSequence;
return this;
}

View File

@@ -3,8 +3,8 @@ import type { FunctionData } from '@/application/collections/';
import { ScriptCode } from '@/domain/ScriptCode';
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
@@ -91,7 +91,7 @@ describe('ScriptCompiler', () => {
});
it('returns code as expected', () => {
// arrange
const expected: ICompiledCode = {
const expected: CompiledCode = {
code: 'expected-code',
revertCode: 'expected-revert-code',
};
@@ -152,8 +152,8 @@ describe('ScriptCompiler', () => {
const scriptName = 'scriptName';
const innerError = 'innerError';
const expectedError = `Script "${scriptName}" ${innerError}`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => { throw new Error(innerError); },
const callCompiler: FunctionCallCompiler = {
compileFunctionCalls: () => { throw new Error(innerError); },
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
@@ -170,13 +170,13 @@ describe('ScriptCompiler', () => {
// arrange
const scriptName = 'scriptName';
const syntax = new LanguageSyntaxStub();
const invalidCode: ICompiledCode = { code: undefined, revertCode: undefined };
const invalidCode: CompiledCode = { code: undefined, revertCode: undefined };
const realExceptionMessage = collectExceptionMessage(
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
);
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => invalidCode,
const callCompiler: FunctionCallCompiler = {
compileFunctionCalls: () => invalidCode,
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
@@ -226,7 +226,7 @@ class ScriptCompilerBuilder {
private sharedFunctionsParser: ISharedFunctionsParser = new SharedFunctionsParserStub();
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub();
private codeValidator: ICodeValidator = new CodeValidatorStub();
@@ -269,7 +269,7 @@ class ScriptCompilerBuilder {
return this;
}
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): ScriptCompilerBuilder {
this.callCompiler = callCompiler;
return this;
}

View File

@@ -67,7 +67,7 @@ describe('CodeSubstituter', () => {
sut.substitute('non empty code', info);
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
const { parameters } = compilerStub.callHistory[0];
const parameters = compilerStub.callHistory[0].args[1];
expect(parameters.hasArgument(testCase.parameter));
const { argumentValue } = parameters.getArgument(testCase.parameter);
expect(argumentValue).to.equal(testCase.argument);
@@ -85,7 +85,7 @@ describe('CodeSubstituter', () => {
sut.substitute(expected, new ProjectInformationStub());
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].code).to.equal(expected);
expect(compilerStub.callHistory[0].args[0]).to.equal(expected);
});
});

View File

@@ -1,7 +1,7 @@
import { expect } from 'vitest';
// `toThrowError` does not assert the error type (https://github.com/vitest-dev/vitest/blob/v0.34.2/docs/api/expect.md#tothrowerror)
export function expectThrowsError<T extends Error>(delegate: () => void, expected: T) {
export function expectDeepThrowsError<T extends Error>(delegate: () => void, expected: T) {
// arrange
if (!expected) {
throw new Error('missing expected');

View File

@@ -0,0 +1,37 @@
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from './FunctionCallStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class ArgumentCompilerStub
extends StubWithObservableMethodCalls<ArgumentCompiler>
implements ArgumentCompiler {
private readonly scenarios = new Array<ArgumentCompilationScenario>();
public createCompiledNestedCall(
nestedFunctionCall: FunctionCall,
parentFunctionCall: FunctionCall,
context: FunctionCallCompilationContext,
): FunctionCall {
this.registerMethodCall({
methodName: 'createCompiledNestedCall',
args: [nestedFunctionCall, parentFunctionCall, context],
});
const scenario = this.scenarios.find((s) => s.givenNestedFunctionCall === nestedFunctionCall);
if (scenario) {
return scenario.result;
}
return new FunctionCallStub();
}
public withScenario(scenario: ArgumentCompilationScenario): this {
this.scenarios.push(scenario);
return this;
}
}
interface ArgumentCompilationScenario {
readonly givenNestedFunctionCall: FunctionCall;
readonly result: FunctionCall;
}

View File

@@ -0,0 +1,16 @@
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { CompiledCodeStub } from './CompiledCodeStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class CodeSegmentMergerStub
extends StubWithObservableMethodCalls<CodeSegmentMerger>
implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
this.registerMethodCall({
methodName: 'mergeCodeParts',
args: [codeSegments],
});
return new CompiledCodeStub();
}
}

View File

@@ -0,0 +1,17 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
export class CompiledCodeStub implements CompiledCode {
public code = `${CompiledCodeStub.name}: code`;
public revertCode?: string = `${CompiledCodeStub.name}: revertCode`;
public withCode(code: string): this {
this.code = code;
return this;
}
public withRevertCode(revertCode?: string): this {
this.revertCode = revertCode;
return this;
}
}

View File

@@ -3,14 +3,14 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc
import { scrambledEqual } from '@/application/Common/Array';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class ExpressionsCompilerStub implements IExpressionsCompiler {
public readonly callHistory = new Array<{
code: string, parameters: IReadOnlyFunctionCallArgumentCollection }>();
export class ExpressionsCompilerStub
extends StubWithObservableMethodCalls<IExpressionsCompiler>
implements IExpressionsCompiler {
private readonly scenarios = new Array<ExpressionCompilationScenario>();
private readonly scenarios = new Array<ITestScenario>();
public setup(scenario: ITestScenario): ExpressionsCompilerStub {
public setup(scenario: ExpressionCompilationScenario): ExpressionsCompilerStub {
this.scenarios.push(scenario);
return this;
}
@@ -28,7 +28,10 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
code: string,
parameters: IReadOnlyFunctionCallArgumentCollection,
): string {
this.callHistory.push({ code, parameters });
this.registerMethodCall({
methodName: 'compileExpressions',
args: [code, parameters],
});
const scenario = this.scenarios.find(
(s) => s.givenCode === code && deepEqual(s.givenArgs, parameters),
);
@@ -43,7 +46,7 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
}
}
interface ITestScenario {
interface ExpressionCompilationScenario {
readonly givenCode: string;
readonly givenArgs: IReadOnlyFunctionCallArgumentCollection;
readonly result: string;

View File

@@ -5,7 +5,19 @@ import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection {
private args = new Array<IFunctionCallArgument>();
public withArgument(parameterName: string, argumentValue: string) {
public withEmptyArguments(): this {
this.args.length = 0;
return this;
}
public withSomeArguments(): this {
return this
.withArgument('firstTestParameterName', 'first-parameter-argument-value')
.withArgument('secondTestParameterName', 'second-parameter-argument-value')
.withArgument('thirdTestParameterName', 'third-parameter-argument-value');
}
public withArgument(parameterName: string, argumentValue: string): this {
const arg = new FunctionCallArgumentStub()
.withParameterName(parameterName)
.withArgumentValue(argumentValue);
@@ -13,7 +25,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
return this;
}
public withArguments(args: { readonly [index: string]: string }) {
public withArguments(args: { readonly [index: string]: string }): this {
for (const [name, value] of Object.entries(args)) {
this.withArgument(name, value);
}

View File

@@ -0,0 +1,27 @@
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { SingleCallCompilerStub } from './SingleCallCompilerStub';
import { FunctionCallStub } from './FunctionCallStub';
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
export class FunctionCallCompilationContextStub implements FunctionCallCompilationContext {
public allFunctions: ISharedFunctionCollection = new SharedFunctionCollectionStub();
public rootCallSequence: readonly FunctionCall[] = [
new FunctionCallStub(), new FunctionCallStub(),
];
public singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub();
public withSingleCallCompiler(singleCallCompiler: SingleCallCompiler): this {
this.singleCallCompiler = singleCallCompiler;
return this;
}
public withAllFunctions(allFunctions: ISharedFunctionCollection): this {
this.allFunctions = allFunctions;
return this;
}
}

View File

@@ -1,29 +1,29 @@
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
interface IScenario {
calls: IFunctionCall[];
calls: FunctionCall[];
functions: ISharedFunctionCollection;
result: ICompiledCode;
result: CompiledCode;
}
export class FunctionCallCompilerStub implements IFunctionCallCompiler {
export class FunctionCallCompilerStub implements FunctionCallCompiler {
public scenarios = new Array<IScenario>();
public setup(
calls: IFunctionCall[],
calls: FunctionCall[],
functions: ISharedFunctionCollection,
result: ICompiledCode,
result: CompiledCode,
) {
this.scenarios.push({ calls, functions, result });
}
public compileCall(
calls: IFunctionCall[],
public compileFunctionCalls(
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): ICompiledCode {
): CompiledCode {
const predefined = this.scenarios
.find((s) => areEqual(s.calls, calls) && s.functions === functions);
if (predefined) {
@@ -37,12 +37,12 @@ export class FunctionCallCompilerStub implements IFunctionCallCompiler {
}
function areEqual(
first: readonly IFunctionCall[],
second: readonly IFunctionCall[],
first: readonly FunctionCall[],
second: readonly FunctionCall[],
) {
const comparer = (a: IFunctionCall, b: IFunctionCall) => a.functionName
const comparer = (a: FunctionCall, b: FunctionCall) => a.functionName
.localeCompare(b.functionName);
const printSorted = (calls: readonly IFunctionCall[]) => JSON
const printSorted = (calls: readonly FunctionCall[]) => JSON
.stringify([...calls].sort(comparer));
return printSorted(first) === printSorted(second);
}

View File

@@ -1,7 +1,7 @@
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallArgumentCollectionStub } from './FunctionCallArgumentCollectionStub';
export class FunctionCallStub implements IFunctionCall {
export class FunctionCallStub implements FunctionCall {
public functionName = 'functionCallStub';
public args = new FunctionCallArgumentCollectionStub();

View File

@@ -5,7 +5,7 @@ import { SharedFunctionStub } from './SharedFunctionStub';
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
private readonly functions = new Map<string, ISharedFunction>();
public withFunction(...funcs: readonly ISharedFunction[]) {
public withFunctions(...funcs: readonly ISharedFunction[]): this {
for (const func of funcs) {
this.functions.set(func.name, func);
}
@@ -21,4 +21,12 @@ export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
.withCode('code by SharedFunctionCollectionStub')
.withRevertCode('revert-code by SharedFunctionCollectionStub');
}
public getRequiredParameterNames(functionName: string): string[] {
return this.getFunctionByName(functionName)
.parameters
.all
.filter((p) => !p.isOptional)
.map((p) => p.name);
}
}

View File

@@ -1,6 +1,6 @@
import { ISharedFunction, ISharedFunctionBody, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub';
import { FunctionCallStub } from './FunctionCallStub';
@@ -16,7 +16,7 @@ export class SharedFunctionStub implements ISharedFunction {
private bodyType: FunctionBodyType = FunctionBodyType.Code;
private calls: IFunctionCall[] = [new FunctionCallStub()];
private calls: FunctionCall[] = [new FunctionCallStub()];
constructor(type: FunctionBodyType) {
this.bodyType = type;
@@ -53,7 +53,11 @@ export class SharedFunctionStub implements ISharedFunction {
return this;
}
public withCalls(...calls: readonly IFunctionCall[]) {
public withSomeCalls() {
return this.withCalls(new FunctionCallStub(), new FunctionCallStub());
}
public withCalls(...calls: readonly FunctionCall[]) {
this.calls = [...calls];
return this;
}

View File

@@ -0,0 +1,45 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
import { CompiledCodeStub } from './CompiledCodeStub';
export class SingleCallCompilerStrategyStub
extends StubWithObservableMethodCalls<SingleCallCompilerStrategy>
implements SingleCallCompilerStrategy {
private canCompileResult = true;
private compiledFunctionResult: CompiledCode[] = [new CompiledCodeStub()];
public canCompile(func: ISharedFunction): boolean {
this.registerMethodCall({
methodName: 'canCompile',
args: [func],
});
return this.canCompileResult;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
this.registerMethodCall({
methodName: 'compileFunction',
args: [calledFunction, callToFunction, context],
});
return this.compiledFunctionResult;
}
public withCanCompileResult(canCompileResult: boolean): this {
this.canCompileResult = canCompileResult;
return this;
}
public withCompiledFunctionResult(compiledFunctionResult: CompiledCode[]): this {
this.compiledFunctionResult = compiledFunctionResult;
return this;
}
}

View File

@@ -0,0 +1,47 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
import { CompiledCodeStub } from './CompiledCodeStub';
interface CallCompilationScenario {
readonly givenCall: FunctionCall;
readonly result: CompiledCode[];
}
export class SingleCallCompilerStub
extends StubWithObservableMethodCalls<SingleCallCompiler>
implements SingleCallCompiler {
private readonly callCompilationScenarios = new Array<CallCompilationScenario>();
public withCallCompilationScenarios(scenarios: Map<FunctionCall, CompiledCode[]>): this {
for (const [call, result] of scenarios) {
this.withCallCompilationScenario({
givenCall: call,
result,
});
}
return this;
}
public withCallCompilationScenario(scenario: CallCompilationScenario): this {
this.callCompilationScenarios.push(scenario);
return this;
}
public compileSingleCall(
call: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
this.registerMethodCall({
methodName: 'compileSingleCall',
args: [call, context],
});
const callCompilation = this.callCompilationScenarios.find((s) => s.givenCall === call);
if (callCompilation) {
return callCompilation.result;
}
return [new CompiledCodeStub()];
}
}