Skip to content

Commit

Permalink
Trim compiler error output for better readability
Browse files Browse the repository at this point in the history
Previously, compiler outputted whole executable in error context. This
caused long and hard to read error messages, especially when the
executable is a long category with many children. This commit improves
readability by trimming the error output.

Changes:

- Trim the error output (max characters: 1000).
- Improve indenting and newlines.
  • Loading branch information
undergroundwires committed Jun 21, 2024
1 parent ed93614 commit 78c62cf
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,26 @@ export const createExecutableContextErrorMessage: ExecutableContextErrorMessageC
message += `${ExecutableType[context.type]}: `;
}
message += errorMessage;
message += `\n${getErrorContextDetails(context)}`;
message += `\n\n${getErrorContextDetails(context)}`;
return message;
};

function getErrorContextDetails(context: ExecutableErrorContext): string {
let output = `Self: ${printExecutable(context.self)}`;
let output = `Executable: ${formatExecutable(context.self)}`;
if (context.parentCategory) {
output += `\nParent: ${printExecutable(context.parentCategory)}`;
output += `\n\nParent category: ${formatExecutable(context.parentCategory)}`;
}
return output;
}

function printExecutable(executable: ExecutableData): string {
return JSON.stringify(executable, undefined, 2);
function formatExecutable(executable: ExecutableData): string {
if (!executable) {
return 'Executable data is missing.';
}
const maxLength = 1000;
let output = JSON.stringify(executable, undefined, 2);
if (output.length > maxLength) {
output = `${output.substring(0, maxLength)}\n... [Rest of the executable trimmed]`;
}
return output;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { describe, it, expect } from 'vitest';
import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub';
import { createExecutableContextErrorMessage } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage';
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';

describe('ExecutableErrorContextMessage', () => {
describe('createExecutableContextErrorMessage', () => {
it('includes the specified error message', () => {
// arrange
const expectedErrorMessage = 'expected error message';
const context = new TestContext()
.withErrorMessage(expectedErrorMessage);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.include(expectedErrorMessage);
});
it('includes the type of executable', () => {
// arrange
const executableType = ExecutableType.Category;
const expectedType = ExecutableType[executableType];
const errorContext: ExecutableErrorContext = {
type: executableType,
self: new CategoryDataStub(),
};
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.include(expectedType);
});
it('includes details of the self executable', () => {
// arrange
const expectedName = 'expected name';
const selfExecutable = new CategoryDataStub()
.withName(expectedName);
const errorContext: ExecutableErrorContext = {
type: ExecutableType.Category,
self: selfExecutable,
};
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.include(expectedName);
});
it('includes details of the parent category', () => {
// arrange
const expectedName = 'expected parent name';
const parentCategoryData = new CategoryDataStub()
.withName(expectedName);
const errorContext: ExecutableErrorContext = {
type: ExecutableType.Category,
self: new CategoryDataStub(),
parentCategory: parentCategoryData,
};
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.include(expectedName);
});
it('constructs the complete message format correctly', () => {
// arrange
const errorMessage = 'expected error message';
const expectedName = 'expected name';
const expectedFormat = new RegExp(`^${escapeRegExp(errorMessage)}\\s+Executable:\\s+{\\s+"name":\\s+"${escapeRegExp(expectedName)}"\\s+}`);
const errorContext: ExecutableErrorContext = {
self: {
name: expectedName,
} as unknown as ExecutableErrorContext['self'],
};
const context = new TestContext()
.withErrorContext(errorContext)
.withErrorMessage(errorMessage);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.match(expectedFormat);
});
describe('output trimming', () => {
const totalLongTextDataCharacters = 5000;
const expectedTrimmedText = '[Rest of the executable trimmed]';
const longName = 'a'.repeat(totalLongTextDataCharacters);
const testScenarios: readonly {
readonly description: string;
readonly errorContext: ExecutableErrorContext;
} [] = [
{
description: 'long text from parent category data',
errorContext: {
type: ExecutableType.Category,
self: new CategoryDataStub(),
parentCategory: new CategoryDataStub().withName(longName),
},
},
{
description: 'long text from self executable data',
errorContext: {
type: ExecutableType.Category,
self: new CategoryDataStub().withName(longName),
},
},
];
testScenarios.forEach(({
description, errorContext,
}) => {
it(description, () => {
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.include(expectedTrimmedText);
expect(actualMessage).to.have.length.lessThan(totalLongTextDataCharacters);
});
});
});
describe('missing data handling', () => {
it('generates a message when the executable type is undefined', () => {
// arrange
const errorContext: ExecutableErrorContext = {
type: undefined,
self: new CategoryDataStub(),
};
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.have.length.greaterThan(0);
});
it('generates a message when executable data is missing', () => {
// arrange
const errorContext: ExecutableErrorContext = {
type: undefined,
self: undefined as unknown as ExecutableErrorContext['self'],
};
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.have.length.greaterThan(0);
});
it('generates a message when parent category is missing', () => {
// arrange
const errorContext: ExecutableErrorContext = {
type: undefined,
self: new CategoryDataStub(),
parentCategory: undefined,
};
const context = new TestContext()
.withErrorContext(errorContext);
// act
const actualMessage = context.createExecutableContextErrorMessage();
// assert
expect(actualMessage).to.have.length.greaterThan(0);
});
});
});
});

class TestContext {
private errorMessage = `[${TestContext.name}] error message`;

private errorContext: ExecutableErrorContext = createExecutableErrorContextStub();

public withErrorMessage(errorMessage: string): this {
this.errorMessage = errorMessage;
return this;
}

public withErrorContext(context: ExecutableErrorContext): this {
this.errorContext = context;
return this;
}

public createExecutableContextErrorMessage() {
return createExecutableContextErrorMessage(
this.errorMessage,
this.errorContext,
);
}
}

function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');// $& means the whole matched string
}

0 comments on commit 78c62cf

Please sign in to comment.