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:
undergroundwires
2024-05-25 13:55:30 +02:00
parent 7794846185
commit 4212c7b9e0
78 changed files with 3346 additions and 1268 deletions

View File

@@ -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()

View File

@@ -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,
});

View File

@@ -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).`;
}

View File

@@ -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);
});
});
});

View File

@@ -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,