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

@@ -3,50 +3,68 @@ import { Category } from '@/domain/Category';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { ICategory, IScript } from '@/domain/ICategory';
describe('Category', () => {
describe('ctor', () => {
describe('throws when name is absent', () => {
describe('throws error if name is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing name';
const name = absentValue;
// act
const construct = () => new Category(5, name, [], [new CategoryStub(5)], []);
const construct = () => new CategoryBuilder()
.withName(name)
.build();
// assert
expect(construct).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
it('throws when has no children', () => {
it('throws error if no children are present', () => {
// arrange
const expectedError = 'A category must have at least one sub-category or script';
const construct = () => new Category(5, 'category', [], [], []);
const scriptChildren: readonly IScript[] = [];
const categoryChildren: readonly ICategory[] = [];
// act
const construct = () => new CategoryBuilder()
.withSubcategories(categoryChildren)
.withScripts(scriptChildren)
.build();
// assert
expect(construct).to.throw(expectedError);
});
});
describe('getAllScriptsRecursively', () => {
it('gets child scripts', () => {
it('retrieves direct child scripts', () => {
// arrange
const expected = [new ScriptStub('1'), new ScriptStub('2')];
const sut = new Category(0, 'category', [], [], expected);
const expectedScripts = [new ScriptStub('1'), new ScriptStub('2')];
const sut = new CategoryBuilder()
.withScripts(expectedScripts)
.build();
// act
const actual = sut.getAllScriptsRecursively();
// assert
expect(actual).to.have.deep.members(expected);
expect(actual).to.have.deep.members(expectedScripts);
});
it('gets child categories', () => {
it('retrieves scripts from direct child categories', () => {
// arrange
const expectedScriptIds = ['1', '2', '3', '4'];
const categories = [
new CategoryStub(31).withScriptIds('1', '2'),
new CategoryStub(32).withScriptIds('3', '4'),
];
const sut = new Category(0, 'category', [], categories, []);
const sut = new CategoryBuilder()
.withScripts([])
.withSubcategories(categories)
.build();
// act
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
const actualIds = sut
.getAllScriptsRecursively()
.map((s) => s.id);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
it('gets child scripts and categories', () => {
it('retrieves scripts from both direct children and child categories', () => {
// arrange
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
const categories = [
@@ -54,13 +72,18 @@ describe('Category', () => {
new CategoryStub(32).withScriptIds('3', '4'),
];
const scripts = [new ScriptStub('5'), new ScriptStub('6')];
const sut = new Category(0, 'category', [], categories, scripts);
const sut = new CategoryBuilder()
.withSubcategories(categories)
.withScripts(scripts)
.build();
// act
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
const actualIds = sut
.getAllScriptsRecursively()
.map((s) => s.id);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
it('gets child categories recursively', () => {
it('retrieves scripts from nested categories recursively', () => {
// arrange
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
const categories = [
@@ -83,45 +106,111 @@ describe('Category', () => {
),
];
// assert
const sut = new Category(0, 'category', [], categories, []);
const sut = new CategoryBuilder()
.withScripts([])
.withSubcategories(categories)
.build();
// act
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
const actualIds = sut
.getAllScriptsRecursively()
.map((s) => s.id);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
});
describe('includes', () => {
it('return false when does not include', () => {
it('returns false for scripts not included', () => {
// assert
const expectedResult = false;
const script = new ScriptStub('3');
const sut = new Category(0, 'category', [], [new CategoryStub(33).withScriptIds('1', '2')], []);
const childCategory = new CategoryStub(33)
.withScriptIds('1', '2');
const sut = new CategoryBuilder()
.withSubcategories([childCategory])
.build();
// act
const actual = sut.includes(script);
// assert
expect(actual).to.equal(false);
expect(actual).to.equal(expectedResult);
});
it('return true when includes as subscript', () => {
it('returns true for scripts directly included', () => {
// assert
const expectedResult = true;
const script = new ScriptStub('3');
const sut = new Category(0, 'category', [], [
new CategoryStub(33).withScript(script).withScriptIds('non-related'),
], []);
const childCategory = new CategoryStub(33)
.withScript(script)
.withScriptIds('non-related');
const sut = new CategoryBuilder()
.withSubcategories([childCategory])
.build();
// act
const actual = sut.includes(script);
// assert
expect(actual).to.equal(true);
expect(actual).to.equal(expectedResult);
});
it('return true when includes as nested category script', () => {
it('returns true for scripts included in nested categories', () => {
// assert
const expectedResult = true;
const script = new ScriptStub('3');
const innerCategory = new CategoryStub(22)
const childCategory = new CategoryStub(22)
.withScriptIds('non-related')
.withCategory(new CategoryStub(33).withScript(script));
const sut = new Category(11, 'category', [], [innerCategory], []);
const sut = new CategoryBuilder()
.withSubcategories([childCategory])
.build();
// act
const actual = sut.includes(script);
// assert
expect(actual).to.equal(true);
expect(actual).to.equal(expectedResult);
});
});
});
class CategoryBuilder {
private id = 3264;
private name = 'test-script';
private docs: ReadonlyArray<string> = [];
private subcategories: ReadonlyArray<ICategory> = [];
private scripts: ReadonlyArray<IScript> = [
new ScriptStub(`[${CategoryBuilder.name}] script`),
];
public withId(id: number): this {
this.id = id;
return this;
}
public withName(name: string): this {
this.name = name;
return this;
}
public withDocs(docs: ReadonlyArray<string>): this {
this.docs = docs;
return this;
}
public withScripts(scripts: ReadonlyArray<IScript>): this {
this.scripts = scripts;
return this;
}
public withSubcategories(subcategories: ReadonlyArray<ICategory>): this {
this.subcategories = subcategories;
return this;
}
public build(): Category {
return new Category({
id: this.id,
name: this.name,
docs: this.docs,
subcategories: this.subcategories,
scripts: this.scripts,
});
}
}

View File

@@ -8,7 +8,7 @@ import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
describe('Script', () => {
describe('ctor', () => {
describe('scriptCode', () => {
it('sets as expected', () => {
it('assigns code correctly', () => {
// arrange
const expected = new ScriptCodeStub();
const sut = new ScriptBuilder()
@@ -43,7 +43,7 @@ describe('Script', () => {
});
});
describe('level', () => {
it('cannot construct with invalid wrong value', () => {
it('throws when constructed with invalid level', () => {
// arrange
const invalidValue: RecommendationLevel = 55 as never;
const expectedError = 'invalid level';
@@ -54,7 +54,7 @@ describe('Script', () => {
// assert
expect(construct).to.throw(expectedError);
});
it('sets undefined as expected', () => {
it('handles undefined level correctly', () => {
// arrange
const expected = undefined;
// act
@@ -64,7 +64,7 @@ describe('Script', () => {
// assert
expect(sut.level).to.equal(expected);
});
it('sets as expected', () => {
it('correctly assigns valid recommendation levels', () => {
// arrange
for (const expected of getEnumValues(RecommendationLevel)) {
// act
@@ -78,7 +78,7 @@ describe('Script', () => {
});
});
describe('docs', () => {
it('sets as expected', () => {
it('correctly assigns docs', () => {
// arrange
const expected = ['doc1', 'doc2'];
// act
@@ -130,11 +130,11 @@ class ScriptBuilder {
}
public build(): Script {
return new Script(
this.name,
this.code,
this.docs,
this.level,
);
return new Script({
name: this.name,
code: this.code,
docs: this.docs,
level: this.level,
});
}
}

View File

@@ -0,0 +1,52 @@
import { createScriptCode } from '@/domain/ScriptCodeFactory';
describe('ScriptCodeFactory', () => {
describe('createScriptCode', () => {
it('generates script code with given `code`', () => {
// arrange
const expectedCode = 'expected code';
const context = new TestContext()
.withCode(expectedCode);
// act
const code = context.createScriptCode();
// assert
const actualCode = code.execute;
expect(actualCode).to.equal(expectedCode);
});
it('generates script code with given `revertCode`', () => {
// arrange
const expectedRevertCode = 'expected revert code';
const context = new TestContext()
.withRevertCode(expectedRevertCode);
// act
const code = context.createScriptCode();
// assert
const actualRevertCode = code.revert;
expect(actualRevertCode).to.equal(expectedRevertCode);
});
});
});
class TestContext {
private code = `[${TestContext}] code`;
private revertCode = `[${TestContext}] revertCode`;
public withCode(code: string): this {
this.code = code;
return this;
}
public withRevertCode(revertCode: string): this {
this.revertCode = revertCode;
return this;
}
public createScriptCode(): ReturnType<typeof createScriptCode> {
return createScriptCode(
this.code,
this.revertCode,
);
}
}