execution-engine
Version:
A TypeScript library for tracing and visualizing code execution workflows.
344 lines (343 loc) • 17.1 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
const trace_decorator_1 = require("./trace.decorator");
describe('trace decorator', () => {
const executionTraceExpectation = {
id: expect.any(String),
inputs: expect.any(Array),
startTime: expect.any(Date),
endTime: expect.any(Date),
duration: expect.any(Number),
elapsedTime: expect.any(String)
};
const successfulExecutionTraceExpectation = {
...executionTraceExpectation,
outputs: expect.anything()
};
const failedExecutionTraceExpectation = {
...executionTraceExpectation,
errors: expect.anything()
};
describe('Tracing synchronous functions', () => {
const onTraceEventMock = jest.fn();
const onTraceEventMockThrows = jest.fn();
class SyncClass {
constructor() {
this.greetingFrom = ['hello from class'];
}
helloWorld() {
this.greetingFrom.push('hello from method');
return 'Hello World';
}
helloWorldHandlerThrows() {
this.greetingFrom.push('hello from method');
return 'Hello World';
}
}
__decorate([
(0, trace_decorator_1.trace)(function (...args) {
this.greetingFrom.push('hello from trace handler');
onTraceEventMock(...args);
expect(this.greetingFrom).toEqual(['hello from class', 'hello from method', 'hello from trace handler']);
})
], SyncClass.prototype, "helloWorld", null);
__decorate([
(0, trace_decorator_1.trace)((...args) => {
onTraceEventMockThrows(...args);
throw new Error('hello but I throw!');
})
], SyncClass.prototype, "helloWorldHandlerThrows", null);
it('should trace a synchronous function and verify context', () => {
const instance = new SyncClass();
expect(instance.helloWorld()).toBe('Hello World');
expect(onTraceEventMock).toHaveBeenCalledTimes(1);
expect(onTraceEventMock).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'SyncClass',
method: 'helloWorld',
name: 'helloWorld',
parameters: [],
isAsync: false,
isBound: false
}),
...successfulExecutionTraceExpectation
}));
});
it('should trace a synchronous function and verify helloWorldHandlerThrows', () => {
const instance = new SyncClass();
expect(() => instance.helloWorldHandlerThrows()).toThrowError('hello but I throw!');
expect(onTraceEventMockThrows).toHaveBeenCalledTimes(1);
expect(onTraceEventMockThrows).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'SyncClass',
method: 'helloWorldHandlerThrows',
name: 'helloWorldHandlerThrows',
parameters: [],
isAsync: false,
isBound: false
}),
...successfulExecutionTraceExpectation
}));
});
});
describe('Asynchronous functions', () => {
const asyncTraceHandlerMock = jest.fn();
class AsyncClass {
constructor() {
this.greetingFrom = ['hello from class'];
}
async helloWorldAsync() {
this.greetingFrom.push('hello from async method');
return 'Hello World async';
}
}
__decorate([
(0, trace_decorator_1.trace)(function (...args) {
this.greetingFrom.push('hello from async trace handler');
asyncTraceHandlerMock(...args);
expect(this.greetingFrom).toEqual(['hello from class', 'hello from async method', 'hello from async trace handler']);
})
], AsyncClass.prototype, "helloWorldAsync", null);
it('should trace an async function', async () => {
const instance = new AsyncClass();
const response = await instance.helloWorldAsync();
expect(response).toEqual('Hello World async');
expect(asyncTraceHandlerMock).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'AsyncClass',
method: 'helloWorldAsync',
name: 'helloWorldAsync',
parameters: [],
isAsync: true,
isBound: false
}),
...successfulExecutionTraceExpectation
}));
});
});
describe('Tracing function onTraceEventMock and traceContext', () => {
const traceContextDivision = { context: { metadata: { requestId: '12345' } } };
const traceContextFetchData = { context: { metadata: { requestId: '6789' } } };
const onTraceEventDivisionMock = jest.fn();
const onTraceEventFetchDataMock = jest.fn();
function empty() {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
return originalMethod.apply(this, args);
};
};
}
class MyClass {
divisionFunction(x, y, traceContext = {}) {
if (y === 0) {
traceContext['narratives'] = [`Throwing because division of ${x} by ${y}`];
throw new Error('Throwing because division by zero is not allowed.');
}
traceContext['narratives'] = [`Calculating the division of ${x} by ${y}`];
return x / y;
}
async fetchDataFunction(url, traceContext = {}) {
traceContext['narratives'] = [`Fetching data from ${url}`];
if (!url.startsWith('http')) {
traceContext['narratives'] = [`Throwing because the URL ${url} is invalid`];
throw new Error('Invalid URL provided.');
}
return { data: 'Success' };
}
divisionFunctionOnAnotherDecorator(x, y, traceContext = {}) {
return this.divisionFunction(x, y, traceContext);
}
async fetchDataFunctionOnAnotherDecorator(url, traceContext = {}) {
return this.fetchDataFunction(url, traceContext);
}
async fetchDataFunctionOnAnotherDecoratorCoughtError(url, traceContext = {}) {
return this.fetchDataFunction(url, traceContext);
}
}
__decorate([
(0, trace_decorator_1.trace)(onTraceEventDivisionMock, traceContextDivision, { contextKey: '__execution', injectContextInArgs: true })
], MyClass.prototype, "divisionFunction", null);
__decorate([
(0, trace_decorator_1.trace)(onTraceEventFetchDataMock, traceContextFetchData, { contextKey: '__execution', injectContextInArgs: true })
], MyClass.prototype, "fetchDataFunction", null);
__decorate([
(0, trace_decorator_1.trace)(onTraceEventDivisionMock, traceContextDivision, { injectContextInArgs: true }),
empty()
], MyClass.prototype, "divisionFunctionOnAnotherDecorator", null);
__decorate([
(0, trace_decorator_1.trace)(onTraceEventFetchDataMock, traceContextFetchData, { injectContextInArgs: true }),
empty()
], MyClass.prototype, "fetchDataFunctionOnAnotherDecorator", null);
__decorate([
(0, trace_decorator_1.trace)(onTraceEventFetchDataMock, traceContextFetchData, { errorStrategy: 'catch', injectContextInArgs: true }),
empty()
], MyClass.prototype, "fetchDataFunctionOnAnotherDecoratorCoughtError", null);
beforeEach(() => {
onTraceEventFetchDataMock.mockClear();
onTraceEventDivisionMock.mockClear();
});
it('should sync trace successfully and pass correct onTraceEventMock and traceContext', () => {
const classInstance = new MyClass();
const response = classInstance.divisionFunction(1, 2);
expect(onTraceEventDivisionMock).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'divisionFunction',
name: 'divisionFunction',
parameters: ['x', 'y', 'traceContext = {}'],
isAsync: false,
isBound: false
}),
...successfulExecutionTraceExpectation,
...traceContextDivision,
narratives: ['Calculating the division of 1 by 2']
}));
expect(response).toEqual(0.5);
});
it('should sync trace errors and pass correct onTraceEventMock and traceContext', () => {
expect(() => new MyClass().divisionFunction(1, 0)).toThrow('Throwing because division by zero is not allowed.');
expect(onTraceEventDivisionMock).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'divisionFunction',
name: 'divisionFunction',
parameters: ['x', 'y', 'traceContext = {}'],
isAsync: false,
isBound: false
}),
...failedExecutionTraceExpectation,
...traceContextDivision,
narratives: ['Throwing because division of 1 by 0']
}));
});
it('should async trace successfully and pass correct onTraceEventMock and traceContext', async () => {
const response = await new MyClass().fetchDataFunction('https://api.example.com/data');
expect(onTraceEventFetchDataMock).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'fetchDataFunction',
name: 'fetchDataFunction',
parameters: ['url', 'traceContext = {}'],
isAsync: true,
isBound: false
}),
...successfulExecutionTraceExpectation,
...traceContextFetchData,
narratives: ['Fetching data from https://api.example.com/data']
}));
expect(response).toMatchObject({ data: 'Success' });
});
it('should async trace errors and pass correct onTraceEventMock and traceContext', async () => {
await expect(new MyClass().fetchDataFunction('invalid-url')).rejects.toThrow('Invalid URL provided.');
expect(onTraceEventFetchDataMock).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'fetchDataFunction',
name: 'fetchDataFunction',
parameters: ['url', 'traceContext = {}'],
isAsync: true,
isBound: false
}),
...failedExecutionTraceExpectation,
...traceContextFetchData,
narratives: ['Throwing because the URL invalid-url is invalid']
}));
});
it('should async trace successfully divisionFunctionOnAnotherDecorator', async () => {
const classInstance = new MyClass();
const response = classInstance.divisionFunctionOnAnotherDecorator(1, 2);
expect(onTraceEventDivisionMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'divisionFunctionOnAnotherDecorator',
name: 'anonymous',
parameters: ['...args'],
isAsync: false,
isBound: false
}),
...successfulExecutionTraceExpectation,
...traceContextDivision,
isPromise: false,
narratives: ['Calculating the division of 1 by 2']
}));
expect(response).toEqual(0.5);
});
it('should async trace error divisionFunctionOnAnotherDecorator', async () => {
expect(() => new MyClass().divisionFunctionOnAnotherDecorator(1, 0)).toThrow('Throwing because division by zero is not allowed.');
expect(onTraceEventDivisionMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'divisionFunctionOnAnotherDecorator',
name: 'anonymous',
parameters: ['...args'],
isAsync: false,
isBound: false
}),
...failedExecutionTraceExpectation,
...traceContextDivision,
isPromise: false,
narratives: ['Throwing because division of 1 by 0']
}));
});
it('should async trace successfully fetchDataFunctionOnAnotherDecorator', async () => {
const response = await new MyClass().fetchDataFunctionOnAnotherDecorator('https://api.example.com/data');
expect(onTraceEventFetchDataMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'fetchDataFunctionOnAnotherDecorator',
name: 'anonymous',
parameters: ['...args'],
isAsync: false,
isBound: false
}),
...successfulExecutionTraceExpectation,
...traceContextFetchData,
isPromise: true,
narratives: ['Fetching data from https://api.example.com/data']
}));
expect(response).toMatchObject({ data: 'Success' });
});
it('should async trace error fetchDataFunctionOnAnotherDecorator', async () => {
await expect(new MyClass().fetchDataFunctionOnAnotherDecorator('invalid-url')).rejects.toThrow('Invalid URL provided.');
expect(onTraceEventFetchDataMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'fetchDataFunctionOnAnotherDecorator',
name: 'anonymous',
parameters: ['...args'],
isAsync: false,
isBound: false
}),
...failedExecutionTraceExpectation,
...traceContextFetchData,
isPromise: true,
narratives: ['Throwing because the URL invalid-url is invalid']
}));
});
it('should async trace error fetchDataFunctionOnAnotherDecorator cought', async () => {
const response = await new MyClass().fetchDataFunctionOnAnotherDecoratorCoughtError('invalid-url');
expect(onTraceEventFetchDataMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
metadata: expect.objectContaining({
class: 'MyClass',
method: 'fetchDataFunctionOnAnotherDecoratorCoughtError',
name: 'anonymous',
parameters: ['...args'],
isAsync: false,
isBound: false
}),
...failedExecutionTraceExpectation,
...traceContextFetchData,
isPromise: true,
narratives: ['Throwing because the URL invalid-url is invalid']
}));
expect(response).toMatchObject([{ code: undefined, message: 'Invalid URL provided.', name: 'Error' }]);
});
});
});