Improve context for errors thrown by compiler
This commit introduces a custom error object to provide additional context for errors throwing during parsing and compiling operations, improving troubleshooting. By integrating error context handling, the error messages become more informative and user-friendly, providing sequence of trace with context to aid in troubleshooting. Changes include: - Introduce custom error object that extends errors with contextual information. This replaces previous usages of `AggregateError` which is not displayed well by browsers when logged. - Improve parsing functions to encapsulate error context with more details. - Increase unit test coverage and refactor the related code to be more testable.
This commit is contained in:
@@ -17,7 +17,7 @@ describe('FunctionCallArgument', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const parameterName = 'paramName';
|
||||
const expectedError = `missing argument value for "${parameterName}"`;
|
||||
const expectedError = `Missing argument value for the parameter "${parameterName}".`;
|
||||
const argumentValue = absentValue;
|
||||
// act
|
||||
const act = () => new FunctionCallArgumentBuilder()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* 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 { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
||||
import type { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
|
||||
@@ -17,7 +17,7 @@ import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('FunctionCallSequenceCompiler', () => {
|
||||
describe('instance', () => {
|
||||
itIsSingleton({
|
||||
itIsSingletonFactory({
|
||||
getter: () => FunctionCallSequenceCompiler.instance,
|
||||
expectedType: FunctionCallSequenceCompiler,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
|
||||
describe('NestedFunctionCallCompiler', () => {
|
||||
describe('canCompile', () => {
|
||||
@@ -43,12 +45,12 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
// arrange
|
||||
const argumentCompiler = new ArgumentCompilerStub();
|
||||
const expectedContext = new FunctionCallCompilationContextStub();
|
||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.withArgumentCompiler(argumentCompiler)
|
||||
.build();
|
||||
// act
|
||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
||||
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||
// assert
|
||||
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
||||
expect(calls).have.lengthOf(1);
|
||||
@@ -59,33 +61,37 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
// arrange
|
||||
const argumentCompiler = new ArgumentCompilerStub();
|
||||
const expectedContext = new FunctionCallCompilationContextStub();
|
||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const expectedParentCall = callToFrontFunc;
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.withArgumentCompiler(argumentCompiler)
|
||||
.build();
|
||||
// act
|
||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
||||
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||
// assert
|
||||
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
||||
expect(calls).have.lengthOf(1);
|
||||
const [,actualParentCall] = calls[0].args;
|
||||
expect(actualParentCall).to.equal(callToFrontFunc);
|
||||
expect(actualParentCall).to.equal(expectedParentCall);
|
||||
});
|
||||
it('uses correct nested call', () => {
|
||||
// arrange
|
||||
const argumentCompiler = new ArgumentCompilerStub();
|
||||
const expectedContext = new FunctionCallCompilationContextStub();
|
||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const {
|
||||
frontFunction, callToDeepFunc, callToFrontFunc,
|
||||
} = createSingleFuncCallingAnotherFunc();
|
||||
const expectedNestedCall = callToDeepFunc;
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.withArgumentCompiler(argumentCompiler)
|
||||
.build();
|
||||
// act
|
||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
||||
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||
// assert
|
||||
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
||||
expect(calls).have.lengthOf(1);
|
||||
const [actualNestedCall] = calls[0].args;
|
||||
expect(actualNestedCall).to.deep.equal(callToFrontFunc);
|
||||
expect(actualNestedCall).to.deep.equal(expectedNestedCall);
|
||||
});
|
||||
});
|
||||
describe('re-compilation with compiled args', () => {
|
||||
@@ -94,11 +100,11 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
const singleCallCompilerStub = new SingleCallCompilerStub();
|
||||
const expectedContext = new FunctionCallCompilationContextStub()
|
||||
.withSingleCallCompiler(singleCallCompilerStub);
|
||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
// act
|
||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
||||
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||
// assert
|
||||
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
|
||||
expect(calls).have.lengthOf(1);
|
||||
@@ -113,12 +119,12 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
const singleCallCompilerStub = new SingleCallCompilerStub();
|
||||
const context = new FunctionCallCompilationContextStub()
|
||||
.withSingleCallCompiler(singleCallCompilerStub);
|
||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.withArgumentCompiler(argumentCompilerStub)
|
||||
.build();
|
||||
// act
|
||||
compiler.compileFunction(frontFunc, callToFrontFunc, context);
|
||||
compiler.compileFunction(frontFunction, callToFrontFunc, context);
|
||||
// assert
|
||||
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
|
||||
expect(calls).have.lengthOf(1);
|
||||
@@ -140,9 +146,9 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
|
||||
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
|
||||
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
|
||||
const frontFunc = createSharedFunctionStubWithCalls()
|
||||
const frontFunction = createSharedFunctionStubWithCalls()
|
||||
.withCalls(callToDeepFunc1, callToDeepFunc2);
|
||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
|
||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name);
|
||||
const singleCallCompilerStub = new SingleCallCompilerStub()
|
||||
.withCallCompilationScenarios(singleCallCompilationScenario);
|
||||
const expectedContext = new FunctionCallCompilationContextStub()
|
||||
@@ -151,73 +157,105 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
.withArgumentCompiler(argumentCompiler)
|
||||
.build();
|
||||
// act
|
||||
const actualCodes = compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
||||
const actualCodes = compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||
// assert
|
||||
expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length);
|
||||
expect(actualCodes).to.have.members(expectedFlattenedCodes);
|
||||
});
|
||||
describe('error handling', () => {
|
||||
it('handles argument compiler errors', () => {
|
||||
describe('rethrows error from argument compiler', () => {
|
||||
// arrange
|
||||
const argumentCompilerError = new Error('Test error');
|
||||
const expectedInnerError = new Error(`Expected error from ${ArgumentCompilerStub.name}`);
|
||||
const calleeFunctionName = 'expectedCalleeFunctionName';
|
||||
const callerFunctionName = 'expectedCallerFunctionName';
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
callee: calleeFunctionName,
|
||||
caller: callerFunctionName,
|
||||
});
|
||||
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({
|
||||
frontFunctionName: callerFunctionName,
|
||||
deepFunctionName: calleeFunctionName,
|
||||
});
|
||||
const argumentCompilerStub = new ArgumentCompilerStub();
|
||||
argumentCompilerStub.createCompiledNestedCall = () => {
|
||||
throw argumentCompilerError;
|
||||
throw expectedInnerError;
|
||||
};
|
||||
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);
|
||||
const builder = new NestedFunctionCallCompilerBuilder()
|
||||
.withArgumentCompiler(argumentCompilerStub);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.build()
|
||||
.compileFunction(
|
||||
frontFunction,
|
||||
callToFrontFunc,
|
||||
new FunctionCallCompilationContextStub(),
|
||||
);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
it('handles single call compiler errors', () => {
|
||||
describe('rethrows error from single call compiler', () => {
|
||||
// arrange
|
||||
const singleCallCompilerError = new Error('Test error');
|
||||
const expectedInnerError = new Error(`Expected error from ${SingleCallCompilerStub.name}`);
|
||||
const calleeFunctionName = 'expectedCalleeFunctionName';
|
||||
const callerFunctionName = 'expectedCallerFunctionName';
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
callee: calleeFunctionName,
|
||||
caller: callerFunctionName,
|
||||
});
|
||||
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({
|
||||
frontFunctionName: callerFunctionName,
|
||||
deepFunctionName: calleeFunctionName,
|
||||
});
|
||||
const singleCallCompiler = new SingleCallCompilerStub();
|
||||
singleCallCompiler.compileSingleCall = () => {
|
||||
throw singleCallCompilerError;
|
||||
throw expectedInnerError;
|
||||
};
|
||||
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);
|
||||
const builder = new NestedFunctionCallCompilerBuilder();
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.build()
|
||||
.compileFunction(
|
||||
frontFunction,
|
||||
callToFrontFunc,
|
||||
context,
|
||||
);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createSingleFuncCallingAnotherFunc() {
|
||||
const deepFunc = createSharedFunctionStubWithCode();
|
||||
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name);
|
||||
const frontFunc = createSharedFunctionStubWithCalls().withCalls(callToDeepFunc);
|
||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
|
||||
function createSingleFuncCallingAnotherFunc(
|
||||
functionNames?: {
|
||||
readonly frontFunctionName?: string;
|
||||
readonly deepFunctionName?: string;
|
||||
},
|
||||
) {
|
||||
const deepFunction = createSharedFunctionStubWithCode()
|
||||
.withName(functionNames?.deepFunctionName ?? 'deep-function (is called by front-function)');
|
||||
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunction.name);
|
||||
const frontFunction = createSharedFunctionStubWithCalls()
|
||||
.withCalls(callToDeepFunc)
|
||||
.withName(functionNames?.frontFunctionName ?? 'front-function (calls deep-function)');
|
||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name);
|
||||
return {
|
||||
deepFunc,
|
||||
frontFunc,
|
||||
deepFunction,
|
||||
frontFunction,
|
||||
callToFrontFunc,
|
||||
callToDeepFunc,
|
||||
};
|
||||
@@ -226,14 +264,31 @@ function createSingleFuncCallingAnotherFunc() {
|
||||
class NestedFunctionCallCompilerBuilder {
|
||||
private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub();
|
||||
|
||||
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||
|
||||
public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this {
|
||||
this.argumentCompiler = argumentCompiler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||
this.wrapError = wrapError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): NestedFunctionCallCompiler {
|
||||
return new NestedFunctionCallCompiler(
|
||||
this.argumentCompiler,
|
||||
this.wrapError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildRethrowErrorMessage(
|
||||
functionNames: {
|
||||
readonly caller: string;
|
||||
readonly callee: string;
|
||||
},
|
||||
) {
|
||||
return `Failed to call '${functionNames.callee}' (callee function) from '${functionNames.caller}' (caller function).`;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { FunctionCallCompilationContext } from '@/application/Parser/Script
|
||||
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import type { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
|
||||
describe('AdaptiveFunctionCallCompiler', () => {
|
||||
describe('compileSingleCall', () => {
|
||||
@@ -28,40 +29,40 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
||||
functionParameters: ['expected-parameter'],
|
||||
callParameters: ['unexpected-parameter'],
|
||||
expectedError:
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
|
||||
+ '. Expected parameter(s): "expected-parameter"',
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
|
||||
+ '\nExpected parameter(s): "expected-parameter"',
|
||||
},
|
||||
{
|
||||
description: 'provided: multiple unexpected parameters, when: different one is expected',
|
||||
functionParameters: ['expected-parameter'],
|
||||
callParameters: ['unexpected-parameter1', 'unexpected-parameter2'],
|
||||
expectedError:
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"`
|
||||
+ '. Expected parameter(s): "expected-parameter"',
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2".`
|
||||
+ '\nExpected parameter(s): "expected-parameter"',
|
||||
},
|
||||
{
|
||||
description: 'provided: an unexpected parameter, when: multiple parameters are expected',
|
||||
functionParameters: ['expected-parameter1', 'expected-parameter2'],
|
||||
callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'],
|
||||
expectedError:
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
|
||||
+ '. Expected parameter(s): "expected-parameter1", "expected-parameter2"',
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
|
||||
+ '\nExpected parameter(s): "expected-parameter1", "expected-parameter2"',
|
||||
},
|
||||
{
|
||||
description: 'provided: an unexpected parameter, when: none required',
|
||||
functionParameters: [],
|
||||
callParameters: ['unexpected-call-parameter'],
|
||||
expectedError:
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
|
||||
+ '. Expected parameter(s): none',
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter".`
|
||||
+ '\nExpected parameter(s): none',
|
||||
},
|
||||
{
|
||||
description: 'provided: expected and unexpected parameter, when: one of them is expected',
|
||||
functionParameters: ['expected-parameter'],
|
||||
callParameters: ['expected-parameter', 'unexpected-parameter'],
|
||||
expectedError:
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
|
||||
+ '. Expected parameter(s): "expected-parameter"',
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
|
||||
+ '\nExpected parameter(s): "expected-parameter"',
|
||||
},
|
||||
];
|
||||
testCases.forEach(({
|
||||
@@ -88,7 +89,8 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
||||
// act
|
||||
const act = () => builder.compileSingleCall();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
const errorMessage = collectExceptionMessage(act);
|
||||
expect(errorMessage).to.include(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,38 +7,44 @@ import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/
|
||||
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/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
|
||||
describe('NestedFunctionArgumentCompiler', () => {
|
||||
describe('createCompiledNestedCall', () => {
|
||||
it('should handle error from expressions compiler', () => {
|
||||
describe('rethrows error from expressions compiler', () => {
|
||||
// arrange
|
||||
const expectedInnerError = new Error('child-');
|
||||
const parameterName = 'parameterName';
|
||||
const expectedErrorMessage = `Error when compiling argument for "${parameterName}"`;
|
||||
const nestedCall = new FunctionCallStub()
|
||||
.withFunctionName('nested-function-call')
|
||||
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
|
||||
.withArgument(parameterName, 'unimportant-value'));
|
||||
const parentCall = new FunctionCallStub()
|
||||
.withFunctionName('parent-function-call');
|
||||
const expressionsCompilerError = new Error('child-');
|
||||
const expectedError = new AggregateError(
|
||||
[expressionsCompilerError],
|
||||
`Error when compiling argument for "${parameterName}"`,
|
||||
);
|
||||
const expressionsCompiler = new ExpressionsCompilerStub();
|
||||
expressionsCompiler.compileExpressions = () => { throw expressionsCompilerError; };
|
||||
expressionsCompiler.compileExpressions = () => { throw expectedInnerError; };
|
||||
const builder = new NestedFunctionArgumentCompilerBuilder()
|
||||
.withParentFunctionCall(parentCall)
|
||||
.withNestedFunctionCall(nestedCall)
|
||||
.withExpressionsCompiler(expressionsCompiler);
|
||||
// act
|
||||
const act = () => builder.createCompiledNestedCall();
|
||||
// assert
|
||||
expectDeepThrowsError(act, expectedError);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.createCompiledNestedCall();
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('compilation', () => {
|
||||
describe('without arguments', () => {
|
||||
@@ -258,6 +264,8 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
|
||||
|
||||
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
|
||||
|
||||
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||
|
||||
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
|
||||
this.expressionsCompiler = expressionsCompiler;
|
||||
return this;
|
||||
@@ -278,8 +286,16 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||
this.wrapError = wrapError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public createCompiledNestedCall(): FunctionCall {
|
||||
const compiler = new NestedFunctionArgumentCompiler(this.expressionsCompiler);
|
||||
const compiler = new NestedFunctionArgumentCompiler(
|
||||
this.expressionsCompiler,
|
||||
this.wrapError,
|
||||
);
|
||||
return compiler.createCompiledNestedCall(
|
||||
this.nestedFunctionCall,
|
||||
this.parentFunctionCall,
|
||||
|
||||
@@ -7,8 +7,8 @@ describe('FunctionParameterCollection', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new FunctionParameterStub().withName('1'),
|
||||
new FunctionParameterStub().withName('2').withOptionality(true),
|
||||
new FunctionParameterStub().withName('3').withOptionality(false),
|
||||
new FunctionParameterStub().withName('2').withOptional(true),
|
||||
new FunctionParameterStub().withName('3').withOptional(false),
|
||||
];
|
||||
const sut = new FunctionParameterCollection();
|
||||
for (const parameter of expected) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { createFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
|
||||
|
||||
describe('FunctionParameterCollectionFactory', () => {
|
||||
describe('createFunctionParameterCollection', () => {
|
||||
describe('it is a transient factory', () => {
|
||||
itIsTransientFactory({
|
||||
getter: () => createFunctionParameterCollection(),
|
||||
expectedType: FunctionParameterCollection,
|
||||
});
|
||||
});
|
||||
it('returns an empty collection', () => {
|
||||
// arrange
|
||||
const expectedInitialParametersCount = 0;
|
||||
// act
|
||||
const collection = createFunctionParameterCollection();
|
||||
// assert
|
||||
expect(collection.all).to.have.lengthOf(expectedInitialParametersCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ describe('SharedFunctionCollection', () => {
|
||||
it('throws if function does not exist', () => {
|
||||
// arrange
|
||||
const name = 'unique-name';
|
||||
const expectedError = `called function is not defined "${name}"`;
|
||||
const expectedError = `Called function is not defined: "${name}"`;
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withName('unexpected-name');
|
||||
const sut = new SharedFunctionCollection();
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { FunctionData, CodeInstruction } from '@/application/collections/';
|
||||
import type { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { SharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser';
|
||||
import { SharedFunctionsParser, type FunctionParameterFactory } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser';
|
||||
import { createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
||||
|
||||
describe('SharedFunctionsParser', () => {
|
||||
describe('instance', () => {
|
||||
itIsSingleton({
|
||||
itIsSingletonFactory({
|
||||
getter: () => SharedFunctionsParser.instance,
|
||||
expectedType: SharedFunctionsParser,
|
||||
});
|
||||
@@ -127,7 +131,7 @@ describe('SharedFunctionsParser', () => {
|
||||
});
|
||||
});
|
||||
describe('throws when parameters type is not as expected', () => {
|
||||
const testCases = [
|
||||
const testScenarios = [
|
||||
{
|
||||
state: 'when not an array',
|
||||
invalidType: 5,
|
||||
@@ -137,7 +141,7 @@ describe('SharedFunctionsParser', () => {
|
||||
invalidType: ['a', { a: 'b' }],
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
for (const testCase of testScenarios) {
|
||||
it(testCase.state, () => {
|
||||
// arrange
|
||||
const func = createFunctionDataWithCode()
|
||||
@@ -170,25 +174,33 @@ describe('SharedFunctionsParser', () => {
|
||||
rules: expectedRules,
|
||||
});
|
||||
});
|
||||
it('rethrows including function name when FunctionParameter throws', () => {
|
||||
// arrange
|
||||
const invalidParameterName = 'invalid function p@r4meter name';
|
||||
const functionName = 'functionName';
|
||||
const message = collectExceptionMessage(
|
||||
() => new FunctionParameter(invalidParameterName, false),
|
||||
);
|
||||
const expectedError = `"${functionName}": ${message}`;
|
||||
const functionData = createFunctionDataWithCode()
|
||||
.withName(functionName)
|
||||
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
||||
|
||||
// act
|
||||
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||
.withFunctions([functionData])
|
||||
.parseFunctions();
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
describe('parameter creation', () => {
|
||||
describe('rethrows including function name when creating parameter throws', () => {
|
||||
// arrange
|
||||
const invalidParameterName = 'invalid-function-parameter-name';
|
||||
const functionName = 'functionName';
|
||||
const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`;
|
||||
const expectedInnerError = new Error('injected error');
|
||||
const parameterFactory: FunctionParameterFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const functionData = createFunctionDataWithCode()
|
||||
.withName(functionName)
|
||||
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
new ParseFunctionsCallerWithDefaults()
|
||||
.withFunctions([functionData])
|
||||
.withFunctionParameterFactory(parameterFactory)
|
||||
.withErrorWrapper(wrapError)
|
||||
.parseFunctions();
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('given empty functions, returns empty collection', () => {
|
||||
@@ -282,6 +294,18 @@ class ParseFunctionsCallerWithDefaults {
|
||||
|
||||
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
|
||||
|
||||
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||
|
||||
private parameterFactory: FunctionParameterFactory = (
|
||||
name: string,
|
||||
isOptional: boolean,
|
||||
) => new FunctionParameterStub()
|
||||
.withName(name)
|
||||
.withOptional(isOptional);
|
||||
|
||||
private parameterCollectionFactory
|
||||
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
|
||||
|
||||
public withSyntax(syntax: ILanguageSyntax) {
|
||||
this.syntax = syntax;
|
||||
return this;
|
||||
@@ -297,8 +321,32 @@ class ParseFunctionsCallerWithDefaults {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||
this.wrapError = wrapError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionParameterFactory(parameterFactory: FunctionParameterFactory): this {
|
||||
this.parameterFactory = parameterFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withParameterCollectionFactory(
|
||||
parameterCollectionFactory: FunctionParameterCollectionFactory,
|
||||
): this {
|
||||
this.parameterCollectionFactory = parameterCollectionFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public parseFunctions() {
|
||||
const sut = new SharedFunctionsParser(this.codeValidator);
|
||||
const sut = new SharedFunctionsParser(
|
||||
{
|
||||
codeValidator: this.codeValidator,
|
||||
wrapError: this.wrapError,
|
||||
createParameter: this.parameterFactory,
|
||||
createParameterCollection: this.parameterCollectionFactory,
|
||||
},
|
||||
);
|
||||
return sut.parseFunctions(this.functions, this.syntax);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user