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:
@@ -1,258 +1,395 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { CategoryData, CategoryOrScriptData } from '@/application/collections/';
|
||||
import { type CategoryFactoryType, parseCategory } from '@/application/Parser/CategoryParser';
|
||||
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
||||
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
||||
import { type CategoryFactory, parseCategory } from '@/application/Parser/CategoryParser';
|
||||
import { type ScriptParser } from '@/application/Parser/Script/ScriptParser';
|
||||
import { type DocsParser } from '@/application/Parser/DocumentationParser';
|
||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { expectThrowsNodeError, type ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
||||
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { NodeDataType } from '@/application/Parser/NodeValidation/NodeDataType';
|
||||
import type { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||
import type { NodeDataValidatorFactory } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||
import { NodeDataValidatorStub, createNodeDataValidatorFactoryStub } from '@tests/unit/shared/Stubs/NodeDataValidatorStub';
|
||||
import type { CategoryNodeErrorContext, UnknownNodeErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { createCategoryFactorySpy } from '@tests/unit/shared/Stubs/CategoryFactoryStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { itThrowsContextualError } from './ContextualErrorTester';
|
||||
import { itValidatesName, itValidatesDefinedData, itAsserts } from './NodeDataValidationTester';
|
||||
import { generateDataValidationTestScenarios } from './DataValidationTestScenarioGenerator';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
describe('invalid category data', () => {
|
||||
describe('validates script data', () => {
|
||||
describe('satisfies shared node tests', () => {
|
||||
new NodeValidationTestRunner()
|
||||
.testInvalidNodeName((invalidName) => {
|
||||
return createTest(
|
||||
new CategoryDataStub().withName(invalidName),
|
||||
);
|
||||
})
|
||||
.testMissingNodeData((node) => {
|
||||
return createTest(node as CategoryData);
|
||||
});
|
||||
});
|
||||
describe('throws when category children is absent', () => {
|
||||
itEachAbsentCollectionValue<CategoryOrScriptData>((absentValue) => {
|
||||
// arrange
|
||||
const categoryName = 'test';
|
||||
const expectedMessage = `"${categoryName}" has no children.`;
|
||||
const category = new CategoryDataStub()
|
||||
.withName(categoryName)
|
||||
.withChildren(absentValue);
|
||||
// act
|
||||
const test = createTest(category);
|
||||
// assert
|
||||
expectThrowsNodeError(test, expectedMessage);
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
describe('throws when category child is missing', () => {
|
||||
new NodeValidationTestRunner()
|
||||
.testMissingNodeData((missingNode) => {
|
||||
// arrange
|
||||
const invalidChildNode = missingNode;
|
||||
const parent = new CategoryDataStub()
|
||||
.withName('parent')
|
||||
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
||||
return ({
|
||||
// act
|
||||
act: () => new TestBuilder()
|
||||
.withData(parent)
|
||||
.parseCategory(),
|
||||
// assert
|
||||
expectedContext: {
|
||||
selfNode: invalidChildNode,
|
||||
parentNode: parent,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws when node is neither a category or a script', () => {
|
||||
// arrange
|
||||
const expectedError = 'Node is neither a category or a script.';
|
||||
const invalidChildNode = { property: 'non-empty-value' } as never as CategoryOrScriptData;
|
||||
const parent = new CategoryDataStub()
|
||||
.withName('parent')
|
||||
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
||||
describe('validation', () => {
|
||||
describe('validates for name', () => {
|
||||
// arrange
|
||||
const expectedName = 'expected category name to be validated';
|
||||
const category = new CategoryDataStub()
|
||||
.withName(expectedName);
|
||||
const expectedContext: CategoryNodeErrorContext = {
|
||||
type: NodeDataType.Category,
|
||||
selfNode: category,
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
const test: ITestScenario = {
|
||||
// act
|
||||
act: () => new TestBuilder()
|
||||
.withData(parent)
|
||||
.parseCategory(),
|
||||
// assert
|
||||
expectedContext: {
|
||||
selfNode: invalidChildNode,
|
||||
parentNode: parent,
|
||||
},
|
||||
};
|
||||
new TestBuilder()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expectThrowsNodeError(test, expectedError);
|
||||
return {
|
||||
expectedNameToValidate: expectedName,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
});
|
||||
describe('throws when category child is invalid category', () => {
|
||||
new NodeValidationTestRunner().testInvalidNodeName((invalidName) => {
|
||||
// arrange
|
||||
const invalidChildNode = new CategoryDataStub()
|
||||
.withName(invalidName);
|
||||
const parent = new CategoryDataStub()
|
||||
.withName('parent')
|
||||
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
||||
return ({
|
||||
// act
|
||||
act: () => new TestBuilder()
|
||||
.withData(parent)
|
||||
.parseCategory(),
|
||||
// assert
|
||||
expectedContext: {
|
||||
type: NodeType.Category,
|
||||
selfNode: invalidChildNode,
|
||||
parentNode: parent,
|
||||
});
|
||||
describe('validates for defined data', () => {
|
||||
// arrange
|
||||
const category = new CategoryDataStub();
|
||||
const expectedContext: CategoryNodeErrorContext = {
|
||||
type: NodeDataType.Category,
|
||||
selfNode: category,
|
||||
};
|
||||
itValidatesDefinedData(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
return {
|
||||
expectedDataToValidate: category,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('validates that category has some children', () => {
|
||||
const categoryName = 'test';
|
||||
const testScenarios = generateDataValidationTestScenarios<CategoryData>({
|
||||
expectFail: getAbsentCollectionTestCases<CategoryOrScriptData>().map(({
|
||||
valueName, absentValue: absentCollectionValue,
|
||||
}) => ({
|
||||
description: `with \`${valueName}\` value as children`,
|
||||
data: new CategoryDataStub()
|
||||
.withName(categoryName)
|
||||
.withChildren(absentCollectionValue as unknown as CategoryOrScriptData[]),
|
||||
})),
|
||||
expectPass: [{
|
||||
description: 'has single children',
|
||||
data: new CategoryDataStub()
|
||||
.withName(categoryName)
|
||||
.withChildren([createScriptDataWithCode()]),
|
||||
}],
|
||||
});
|
||||
testScenarios.forEach(({
|
||||
description, expectedPass, data: categoryData,
|
||||
}) => {
|
||||
describe(description, () => {
|
||||
itAsserts({
|
||||
expectedConditionResult: expectedPass,
|
||||
test: (validatorFactory) => {
|
||||
const expectedMessage = `"${categoryName}" has no children.`;
|
||||
const expectedContext: CategoryNodeErrorContext = {
|
||||
type: NodeDataType.Category,
|
||||
selfNode: categoryData,
|
||||
};
|
||||
// act
|
||||
try {
|
||||
new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
} catch { /* It may throw due to assertions not being evaluated */ }
|
||||
// assert
|
||||
return {
|
||||
expectedErrorMessage: expectedMessage,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
function createTest(category: CategoryData): ITestScenario {
|
||||
return {
|
||||
act: () => new TestBuilder()
|
||||
.withData(category)
|
||||
.parseCategory(),
|
||||
expectedContext: {
|
||||
type: NodeType.Category,
|
||||
selfNode: category,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
it(`rethrows exception if ${Category.name} cannot be constructed`, () => {
|
||||
describe('validates that a child is a category or a script', () => {
|
||||
// arrange
|
||||
const expectedError = 'category creation failed';
|
||||
const factoryMock: CategoryFactoryType = () => { throw new Error(expectedError); };
|
||||
const data = new CategoryDataStub();
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withData(data)
|
||||
.withFactory(factoryMock)
|
||||
.parseCategory();
|
||||
// expect
|
||||
expectThrowsNodeError({
|
||||
act,
|
||||
expectedContext: {
|
||||
type: NodeType.Category,
|
||||
selfNode: data,
|
||||
},
|
||||
}, expectedError);
|
||||
const testScenarios = generateDataValidationTestScenarios<CategoryOrScriptData>({
|
||||
expectFail: [{
|
||||
description: 'child has incorrect properties',
|
||||
data: { property: 'non-empty-value' } as unknown as CategoryOrScriptData,
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'child is a category',
|
||||
data: new CategoryDataStub(),
|
||||
},
|
||||
{
|
||||
description: 'child is a script with call',
|
||||
data: createScriptDataWithCall(),
|
||||
},
|
||||
{
|
||||
description: 'child is a script with code',
|
||||
data: createScriptDataWithCode(),
|
||||
},
|
||||
],
|
||||
});
|
||||
testScenarios.forEach(({
|
||||
description, expectedPass, data: childData,
|
||||
}) => {
|
||||
describe(description, () => {
|
||||
itAsserts({
|
||||
expectedConditionResult: expectedPass,
|
||||
test: (validatorFactory) => {
|
||||
const expectedError = 'Node is neither a category or a script.';
|
||||
const parent = new CategoryDataStub()
|
||||
.withName('parent')
|
||||
.withChildren([new CategoryDataStub().withName('valid child'), childData]);
|
||||
const expectedContext: UnknownNodeErrorContext = {
|
||||
selfNode: childData,
|
||||
parentNode: parent,
|
||||
};
|
||||
// act
|
||||
new TestBuilder()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
return {
|
||||
expectedErrorMessage: expectedError,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('validates children recursively', () => {
|
||||
describe('validates (1th-level) child data', () => {
|
||||
// arrange
|
||||
const expectedName = 'child category';
|
||||
const child = new CategoryDataStub()
|
||||
.withName(expectedName);
|
||||
const parent = new CategoryDataStub()
|
||||
.withName('parent')
|
||||
.withChildren([child]);
|
||||
const expectedContext: UnknownNodeErrorContext = {
|
||||
selfNode: child,
|
||||
parentNode: parent,
|
||||
};
|
||||
itValidatesDefinedData(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
return {
|
||||
expectedDataToValidate: child,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('validates that (2nd-level) child name', () => {
|
||||
// arrange
|
||||
const expectedName = 'grandchild category';
|
||||
const grandChild = new CategoryDataStub()
|
||||
.withName(expectedName);
|
||||
const child = new CategoryDataStub()
|
||||
.withChildren([grandChild])
|
||||
.withName('child category');
|
||||
const parent = new CategoryDataStub()
|
||||
.withName('parent')
|
||||
.withChildren([child]);
|
||||
const expectedContext: CategoryNodeErrorContext = {
|
||||
type: NodeDataType.Category,
|
||||
selfNode: grandChild,
|
||||
parentNode: child,
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
return {
|
||||
expectedNameToValidate: expectedName,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('returns expected docs', () => {
|
||||
describe('rethrows exception if category factory fails', () => {
|
||||
// arrange
|
||||
const givenData = new CategoryDataStub();
|
||||
const expectedContextMessage = 'Failed to parse category.';
|
||||
const expectedError = new Error();
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const validatorStub = new NodeDataValidatorStub();
|
||||
validatorStub.createContextualErrorMessage = (message) => message;
|
||||
const factoryMock: CategoryFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestBuilder()
|
||||
.withCategoryFactory(factoryMock)
|
||||
.withValidatorFactory(() => validatorStub)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withData(givenData)
|
||||
.parseCategory();
|
||||
},
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
it('parses docs correctly', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const expected = parseDocs({ docs: url });
|
||||
const category = new CategoryDataStub()
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withDocs(url);
|
||||
const parseDocs: DocsParser = (data) => {
|
||||
return [
|
||||
`parsed docs: ${JSON.stringify(data)}`,
|
||||
];
|
||||
};
|
||||
const expectedDocs = parseDocs(categoryData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(category)
|
||||
.parseCategory()
|
||||
.docs;
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.withDocsParser(parseDocs)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualDocs = getInitParameters(actualCategory)?.docs;
|
||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||
});
|
||||
describe('parses expected subscript', () => {
|
||||
it('single script with code', () => {
|
||||
it('parses single script correctly', () => {
|
||||
// arrange
|
||||
const script = createScriptDataWithCode();
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
const expected = [parseScript(script, context)];
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([script]);
|
||||
const expectedScript = new ScriptStub('expected script');
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childScriptData = createScriptDataWithCode();
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren([childScriptData]);
|
||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(category)
|
||||
.withContext(context)
|
||||
.parseCategory()
|
||||
.scripts;
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.have.lengthOf(1);
|
||||
const actualScript = actualScripts[0];
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
it('single script with function call', () => {
|
||||
it('parses multiple scripts correctly', () => {
|
||||
// arrange
|
||||
const script = createScriptDataWithCall();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script);
|
||||
const context = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
const expected = [parseScript(script, context)];
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([script]);
|
||||
const expectedScripts = [
|
||||
new ScriptStub('expected-first-script'),
|
||||
new ScriptStub('expected-second-script'),
|
||||
];
|
||||
const childrenData = [
|
||||
createScriptDataWithCall(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const scriptParser = new ScriptParserStub();
|
||||
childrenData.forEach((_, index) => {
|
||||
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||
});
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(category)
|
||||
.withContext(context)
|
||||
.parseCategory()
|
||||
.scripts;
|
||||
const actualCategory = new TestBuilder()
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
||||
expect(actualParsedScripts).to.have.members(expectedScripts);
|
||||
});
|
||||
it('multiple scripts with function call and code', () => {
|
||||
it('parses all scripts with correct context', () => {
|
||||
// arrange
|
||||
const callableScript = createScriptDataWithCall();
|
||||
const scripts = [callableScript, createScriptDataWithCode()];
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren(scripts);
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(callableScript);
|
||||
const context = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
const expected = scripts.map((script) => parseScript(script, context));
|
||||
const expectedParseContext = new CategoryCollectionParseContextStub();
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childrenData = [
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(category)
|
||||
.withContext(context)
|
||||
.parseCategory()
|
||||
.scripts;
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withContext(expectedParseContext)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('script is created with right context', () => { // test through script validation logic
|
||||
// arrange
|
||||
const commentDelimiter = 'should not throw';
|
||||
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([
|
||||
new CategoryDataStub()
|
||||
.withName('sub-category')
|
||||
.withChildren([
|
||||
createScriptDataWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode),
|
||||
]),
|
||||
]);
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withData(category)
|
||||
.withContext(parseContext)
|
||||
.parseCategory()
|
||||
.scripts;
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
const actualParseContexts = actualParsedScripts.map(
|
||||
(s) => scriptParser.getParseParameters(s)[1],
|
||||
);
|
||||
expect(
|
||||
actualParseContexts.every(
|
||||
(actualParseContext) => actualParseContext === expectedParseContext,
|
||||
),
|
||||
formatAssertionMessage([
|
||||
`Expected all elements to be ${JSON.stringify(expectedParseContext)}`,
|
||||
'All elements:',
|
||||
indentText(JSON.stringify(actualParseContexts)),
|
||||
]),
|
||||
).to.equal(true);
|
||||
});
|
||||
});
|
||||
it('returns expected subcategories', () => {
|
||||
// arrange
|
||||
const expected = [new CategoryDataStub()
|
||||
.withName('test category')
|
||||
.withChildren([createScriptDataWithCode()]),
|
||||
];
|
||||
const category = new CategoryDataStub()
|
||||
const expectedChildCategory = new CategoryStub(33);
|
||||
const childCategoryData = new CategoryDataStub()
|
||||
.withName('expected child category')
|
||||
.withChildren([createScriptDataWithCode()]);
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withName('category name')
|
||||
.withChildren(expected);
|
||||
.withChildren([childCategoryData]);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(category)
|
||||
.parseCategory()
|
||||
.subCategories;
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory((parameters) => {
|
||||
if (parameters.name === childCategoryData.category) {
|
||||
return expectedChildCategory;
|
||||
}
|
||||
return categoryFactorySpy(parameters);
|
||||
})
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(1);
|
||||
expect(actual[0].name).to.equal(expected[0].category);
|
||||
expect(actual[0].scripts.length).to.equal(expected[0].children.length);
|
||||
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||
expectExists(actualSubcategories);
|
||||
expect(actualSubcategories).to.have.lengthOf(1);
|
||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -262,24 +399,62 @@ class TestBuilder {
|
||||
|
||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||
|
||||
private factory?: CategoryFactoryType = undefined;
|
||||
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
|
||||
|
||||
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||
|
||||
private validatorFactory: NodeDataValidatorFactory = createNodeDataValidatorFactoryStub;
|
||||
|
||||
private docsParser: DocsParser = () => ['docs'];
|
||||
|
||||
private scriptParser: ScriptParser = new ScriptParserStub().get();
|
||||
|
||||
public withData(data: CategoryData) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withContext(context: ICategoryCollectionParseContext) {
|
||||
public withContext(context: ICategoryCollectionParseContext): this {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFactory(factory: CategoryFactoryType) {
|
||||
this.factory = factory;
|
||||
public withCategoryFactory(categoryFactory: CategoryFactory): this {
|
||||
this.categoryFactory = categoryFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withValidatorFactory(validatorFactory: NodeDataValidatorFactory): this {
|
||||
this.validatorFactory = validatorFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this {
|
||||
this.errorWrapper = errorWrapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptParser(scriptParser: ScriptParser): this {
|
||||
this.scriptParser = scriptParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocsParser(docsParser: DocsParser): this {
|
||||
this.docsParser = docsParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public parseCategory() {
|
||||
return parseCategory(this.data, this.context, this.factory);
|
||||
return parseCategory(
|
||||
this.data,
|
||||
this.context,
|
||||
{
|
||||
createCategory: this.categoryFactory,
|
||||
wrapError: this.errorWrapper,
|
||||
createValidator: this.validatorFactory,
|
||||
parseScript: this.scriptParser,
|
||||
parseDocs: this.docsParser,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
121
tests/unit/application/Parser/ContextualError.spec.ts
Normal file
121
tests/unit/application/Parser/ContextualError.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
import { wrapErrorWithAdditionalContext } from '@/application/Parser/ContextualError';
|
||||
|
||||
describe('wrapErrorWithAdditionalContext', () => {
|
||||
it('preserves the original error when wrapped', () => {
|
||||
// arrange
|
||||
const expectedError = new Error();
|
||||
const context = new TestContext()
|
||||
.withError(expectedError);
|
||||
// act
|
||||
const error = context.wrap();
|
||||
// assert
|
||||
const actualError = extractInnerErrorFromContextualError(error);
|
||||
expect(actualError).to.equal(expectedError);
|
||||
});
|
||||
it('maintains the original error when re-wrapped', () => {
|
||||
// arrange
|
||||
const expectedError = new Error();
|
||||
|
||||
// act
|
||||
const firstError = new TestContext()
|
||||
.withError(expectedError)
|
||||
.withAdditionalContext('first error')
|
||||
.wrap();
|
||||
const secondError = new TestContext()
|
||||
.withError(firstError)
|
||||
.withAdditionalContext('second error')
|
||||
.wrap();
|
||||
|
||||
// assert
|
||||
const actualError = extractInnerErrorFromContextualError(secondError);
|
||||
expect(actualError).to.equal(expectedError);
|
||||
});
|
||||
it(`the object extends ${CustomError.name}`, () => {
|
||||
// arrange
|
||||
const expected = CustomError;
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.wrap();
|
||||
// assert
|
||||
expect(error).to.be.an.instanceof(expected);
|
||||
});
|
||||
describe('error message construction', () => {
|
||||
it('includes the message from the original error', () => {
|
||||
// arrange
|
||||
const expectedOriginalErrorMessage = 'Message from the inner error';
|
||||
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.withError(new Error(expectedOriginalErrorMessage))
|
||||
.wrap();
|
||||
|
||||
// assert
|
||||
expect(error.message).contains(expectedOriginalErrorMessage);
|
||||
});
|
||||
it('appends provided additional context to the error message', () => {
|
||||
// arrange
|
||||
const expectedAdditionalContext = 'Expected additional context message';
|
||||
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.withAdditionalContext(expectedAdditionalContext)
|
||||
.wrap();
|
||||
|
||||
// assert
|
||||
expect(error.message).contains(expectedAdditionalContext);
|
||||
});
|
||||
it('appends multiple contexts to the error message in sequential order', () => {
|
||||
// arrange
|
||||
const expectedFirstContext = 'First context';
|
||||
const expectedSecondContext = 'Second context';
|
||||
|
||||
// act
|
||||
const firstError = new TestContext()
|
||||
.withAdditionalContext(expectedFirstContext)
|
||||
.wrap();
|
||||
const secondError = new TestContext()
|
||||
.withError(firstError)
|
||||
.withAdditionalContext(expectedSecondContext)
|
||||
.wrap();
|
||||
|
||||
// assert
|
||||
const messageLines = secondError.message.split('\n');
|
||||
expect(messageLines).to.contain(`1: ${expectedFirstContext}`);
|
||||
expect(messageLines).to.contain(`2: ${expectedSecondContext}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private error: Error = new Error();
|
||||
|
||||
private additionalContext = `[${TestContext.name}] additional context`;
|
||||
|
||||
public withError(error: Error) {
|
||||
this.error = error;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAdditionalContext(additionalContext: string) {
|
||||
this.additionalContext = additionalContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public wrap(): ReturnType<typeof wrapErrorWithAdditionalContext> {
|
||||
return wrapErrorWithAdditionalContext(
|
||||
this.error,
|
||||
this.additionalContext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function extractInnerErrorFromContextualError(error: Error): Error {
|
||||
const innerErrorProperty = 'innerError';
|
||||
if (!(innerErrorProperty in error)) {
|
||||
throw new Error(`${innerErrorProperty} property is missing`);
|
||||
}
|
||||
const actualError = error[innerErrorProperty];
|
||||
return actualError as Error;
|
||||
}
|
||||
53
tests/unit/application/Parser/ContextualErrorTester.ts
Normal file
53
tests/unit/application/Parser/ContextualErrorTester.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||
|
||||
interface ContextualErrorTestScenario {
|
||||
readonly throwingAction: (wrapError: ErrorWithContextWrapper) => void;
|
||||
readonly expectedWrappedError: Error;
|
||||
readonly expectedContextMessage: string;
|
||||
}
|
||||
|
||||
export function itThrowsContextualError(
|
||||
testScenario: ContextualErrorTestScenario,
|
||||
) {
|
||||
it('throws wrapped error', () => {
|
||||
// arrange
|
||||
const expectedError = new Error();
|
||||
const wrapperStub = new ErrorWrapperStub()
|
||||
.withError(expectedError);
|
||||
// act
|
||||
const act = () => testScenario.throwingAction(wrapperStub.get());
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('wraps internal error', () => {
|
||||
// arrange
|
||||
const expectedInternalError = testScenario.expectedWrappedError;
|
||||
const wrapperStub = new ErrorWrapperStub();
|
||||
// act
|
||||
try {
|
||||
testScenario.throwingAction(wrapperStub.get());
|
||||
} catch { /* Swallow */ }
|
||||
// assert
|
||||
expect(wrapperStub.lastError).to.deep.equal(expectedInternalError);
|
||||
});
|
||||
it('includes expected context', () => {
|
||||
// arrange
|
||||
const { expectedContextMessage: expectedContext } = testScenario;
|
||||
const wrapperStub = new ErrorWrapperStub();
|
||||
// act
|
||||
try {
|
||||
testScenario.throwingAction(wrapperStub.get());
|
||||
} catch { /* Swallow */ }
|
||||
// assert
|
||||
expectExists(wrapperStub.lastContext);
|
||||
expect(wrapperStub.lastContext).to.equal(expectedContext, formatAssertionMessage([
|
||||
'Unexpected additional context (additional message added to the wrapped error).',
|
||||
`Actual additional context:\n${indentText(wrapperStub.lastContext)}`,
|
||||
`Expected additional context:\n${indentText(expectedContext)}`,
|
||||
]));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface DataValidationTestScenario<T> {
|
||||
readonly description: string;
|
||||
readonly data: T;
|
||||
readonly expectedPass: boolean;
|
||||
readonly expectedMessage?: string;
|
||||
}
|
||||
|
||||
export function generateDataValidationTestScenarios<T>(
|
||||
...conditionBasedScenarios: DataValidationConditionBasedTestScenario<T>[]
|
||||
): DataValidationTestScenario<T>[] {
|
||||
return conditionBasedScenarios.flatMap((conditionScenario) => [
|
||||
conditionScenario.expectFail.map((failDefinition): DataValidationTestScenario<T> => ({
|
||||
description: `fails: "${failDefinition.description}"`,
|
||||
data: failDefinition.data,
|
||||
expectedPass: false,
|
||||
expectedMessage: conditionScenario.assertErrorMessage,
|
||||
})),
|
||||
conditionScenario.expectPass.map((passDefinition): DataValidationTestScenario<T> => ({
|
||||
description: `passes: "${passDefinition.description}"`,
|
||||
data: passDefinition.data,
|
||||
expectedPass: true,
|
||||
expectedMessage: conditionScenario.assertErrorMessage,
|
||||
})),
|
||||
].flat());
|
||||
}
|
||||
|
||||
interface DataValidationConditionBasedTestScenario<T> {
|
||||
readonly assertErrorMessage?: string;
|
||||
readonly expectPass: readonly DataValidationScenarioDefinition<T>[];
|
||||
readonly expectFail: readonly DataValidationScenarioDefinition<T>[];
|
||||
}
|
||||
|
||||
interface DataValidationScenarioDefinition<T> {
|
||||
readonly description: string;
|
||||
readonly data: T;
|
||||
}
|
||||
213
tests/unit/application/Parser/NodeDataValidationTester.ts
Normal file
213
tests/unit/application/Parser/NodeDataValidationTester.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { it } from 'vitest';
|
||||
import type { NodeDataValidator, NodeDataValidatorFactory } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||
import type { NodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||
import { NodeDataValidatorStub } from '@tests/unit/shared/Stubs/NodeDataValidatorStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import type { CategoryOrScriptData } from '@/application/collections/';
|
||||
import type { FunctionKeys } from '@/TypeHelpers';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
|
||||
type NodeValidationTestFunction<TExpectation> = (
|
||||
factory: NodeDataValidatorFactory,
|
||||
) => TExpectation;
|
||||
|
||||
interface ValidNameExpectation {
|
||||
readonly expectedNameToValidate: string;
|
||||
readonly expectedErrorContext: NodeDataErrorContext;
|
||||
}
|
||||
|
||||
export function itValidatesName(
|
||||
test: NodeValidationTestFunction<ValidNameExpectation>,
|
||||
) {
|
||||
it('validates for name', () => {
|
||||
// arrange
|
||||
const validator = new NodeDataValidatorStub();
|
||||
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||
// act
|
||||
test(factoryStub);
|
||||
// assert
|
||||
const call = validator.callHistory.find((c) => c.methodName === 'assertValidName');
|
||||
expectExists(call);
|
||||
});
|
||||
it('validates for name with correct name', () => {
|
||||
// arrange
|
||||
const validator = new NodeDataValidatorStub();
|
||||
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||
// act
|
||||
const expectation = test(factoryStub);
|
||||
// assert
|
||||
const expectedName = expectation.expectedNameToValidate;
|
||||
const names = validator.callHistory
|
||||
.filter((c) => c.methodName === 'assertValidName')
|
||||
.flatMap((c) => c.args[0]);
|
||||
expect(names).to.include(expectedName);
|
||||
});
|
||||
it('validates for name with correct context', () => {
|
||||
expectCorrectContextForFunctionCall({
|
||||
methodName: 'assertValidName',
|
||||
act: test,
|
||||
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface ValidDataExpectation {
|
||||
readonly expectedDataToValidate: CategoryOrScriptData;
|
||||
readonly expectedErrorContext: NodeDataErrorContext;
|
||||
}
|
||||
|
||||
export function itValidatesDefinedData(
|
||||
test: NodeValidationTestFunction<ValidDataExpectation>,
|
||||
) {
|
||||
it('validates data', () => {
|
||||
// arrange
|
||||
const validator = new NodeDataValidatorStub();
|
||||
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||
// act
|
||||
test(factoryStub);
|
||||
// assert
|
||||
const call = validator.callHistory.find((c) => c.methodName === 'assertDefined');
|
||||
expectExists(call);
|
||||
});
|
||||
it('validates data with correct data', () => {
|
||||
// arrange
|
||||
const validator = new NodeDataValidatorStub();
|
||||
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||
// act
|
||||
const expectation = test(factoryStub);
|
||||
// assert
|
||||
const expectedData = expectation.expectedDataToValidate;
|
||||
const calls = validator.callHistory.filter((c) => c.methodName === 'assertDefined');
|
||||
const names = calls.flatMap((c) => c.args[0]);
|
||||
expect(names).to.include(expectedData);
|
||||
});
|
||||
it('validates data with correct context', () => {
|
||||
expectCorrectContextForFunctionCall({
|
||||
methodName: 'assertDefined',
|
||||
act: test,
|
||||
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface AssertionExpectation {
|
||||
readonly expectedErrorMessage: string;
|
||||
readonly expectedErrorContext: NodeDataErrorContext;
|
||||
}
|
||||
|
||||
export function itAsserts(
|
||||
testScenario: {
|
||||
readonly test: NodeValidationTestFunction<AssertionExpectation>,
|
||||
readonly expectedConditionResult: boolean;
|
||||
},
|
||||
) {
|
||||
it('asserts with correct message', () => {
|
||||
// arrange
|
||||
const validator = new NodeDataValidatorStub()
|
||||
.withAssertThrowsOnFalseCondition(false);
|
||||
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||
// act
|
||||
const expectation = testScenario.test(factoryStub);
|
||||
// assert
|
||||
const expectedError = expectation.expectedErrorMessage;
|
||||
const calls = validator.callHistory.filter((c) => c.methodName === 'assert');
|
||||
const actualMessages = calls.map((call) => {
|
||||
const [, message] = call.args;
|
||||
return message;
|
||||
});
|
||||
expect(actualMessages).to.include(expectedError);
|
||||
});
|
||||
it('asserts with correct context', () => {
|
||||
expectCorrectContextForFunctionCall({
|
||||
methodName: 'assert',
|
||||
act: testScenario.test,
|
||||
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||
});
|
||||
});
|
||||
it('asserts with correct condition result', () => {
|
||||
// arrange
|
||||
const expectedEvaluationResult = testScenario.expectedConditionResult;
|
||||
const validator = new NodeDataValidatorStub()
|
||||
.withAssertThrowsOnFalseCondition(false);
|
||||
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||
// act
|
||||
const expectation = testScenario.test(factoryStub);
|
||||
// assert
|
||||
const assertCalls = validator.callHistory
|
||||
.filter((call) => call.methodName === 'assert');
|
||||
expect(assertCalls).to.have.length.greaterThan(0);
|
||||
const assertCallsWithMessage = assertCalls
|
||||
.filter((call) => {
|
||||
const [, message] = call.args;
|
||||
return message === expectation.expectedErrorMessage;
|
||||
});
|
||||
expect(assertCallsWithMessage).to.have.length.greaterThan(0);
|
||||
const evaluationResults = assertCallsWithMessage
|
||||
.map((call) => {
|
||||
const [predicate] = call.args;
|
||||
return predicate as (() => boolean);
|
||||
})
|
||||
.map((predicate) => predicate());
|
||||
expect(evaluationResults).to.include(expectedEvaluationResult);
|
||||
});
|
||||
}
|
||||
|
||||
function expectCorrectContextForFunctionCall<T>(testScenario: {
|
||||
methodName: FunctionKeys<NodeDataValidator>,
|
||||
act: NodeValidationTestFunction<T>,
|
||||
expectContext: (actionResult: T) => NodeDataErrorContext,
|
||||
}) {
|
||||
// arrange
|
||||
const { methodName } = testScenario;
|
||||
const createdValidators = new Array<{
|
||||
readonly validator: NodeDataValidatorStub;
|
||||
readonly context: NodeDataErrorContext;
|
||||
}>();
|
||||
const factoryStub: NodeDataValidatorFactory = (context) => {
|
||||
const validator = new NodeDataValidatorStub()
|
||||
.withAssertThrowsOnFalseCondition(false);
|
||||
createdValidators.push(({
|
||||
validator,
|
||||
context,
|
||||
}));
|
||||
return validator;
|
||||
};
|
||||
// act
|
||||
const actionResult = testScenario.act(factoryStub);
|
||||
// assert
|
||||
const expectedContext = testScenario.expectContext(actionResult);
|
||||
const providedContexts = createdValidators
|
||||
.filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName))
|
||||
.map((v) => v.context);
|
||||
expectDeepIncludes( // to.deep.include is not working
|
||||
providedContexts,
|
||||
expectedContext,
|
||||
formatAssertionMessage([
|
||||
'Error context mismatch.',
|
||||
'Provided contexts do not include the expected context.',
|
||||
'Expected context:',
|
||||
indentText(JSON.stringify(expectedContext, undefined, 2)),
|
||||
'Provided contexts:',
|
||||
indentText(JSON.stringify(providedContexts, undefined, 2)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function expectDeepIncludes<T>(
|
||||
array: readonly T[],
|
||||
item: T,
|
||||
message: string,
|
||||
) {
|
||||
const serializeItem = (c) => JSON.stringify(c);
|
||||
const serializedContexts = array.map((c) => serializeItem(c));
|
||||
const serializedExpectedContext = serializeItem(item);
|
||||
expect(serializedContexts).to.include(serializedExpectedContext, formatAssertionMessage([
|
||||
'Error context mismatch.',
|
||||
'Provided contexts do not include the expected context.',
|
||||
'Expected context:',
|
||||
indentText(JSON.stringify(message, undefined, 2)),
|
||||
'Provided contexts:',
|
||||
indentText(JSON.stringify(message, undefined, 2)),
|
||||
]));
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { type INodeDataErrorContext, NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
|
||||
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
|
||||
describe('NodeDataError', () => {
|
||||
it('sets message as expected', () => {
|
||||
// arrange
|
||||
const message = 'message';
|
||||
const context = new NodeDataErrorContextStub();
|
||||
const expected = `[${NodeType[context.type]}] ${message}`;
|
||||
// act
|
||||
const sut = new NodeDataErrorBuilder()
|
||||
.withContext(context)
|
||||
.withMessage(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.message).to.include(expected);
|
||||
});
|
||||
it('sets context as expected', () => {
|
||||
// arrange
|
||||
const expected = new NodeDataErrorContextStub();
|
||||
// act
|
||||
const sut = new NodeDataErrorBuilder()
|
||||
.withContext(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.context).to.equal(expected);
|
||||
});
|
||||
it('extends CustomError', () => {
|
||||
// arrange
|
||||
const expected = CustomError;
|
||||
// act
|
||||
const sut = new NodeDataErrorBuilder()
|
||||
.build();
|
||||
// assert
|
||||
expect(sut).to.be.an.instanceof(expected);
|
||||
});
|
||||
});
|
||||
|
||||
class NodeDataErrorBuilder {
|
||||
private message = 'error';
|
||||
|
||||
private context: INodeDataErrorContext = new NodeDataErrorContextStub();
|
||||
|
||||
public withContext(context: INodeDataErrorContext) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withMessage(message: string) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): NodeDataError {
|
||||
return new NodeDataError(this.message, this.context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import type { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||
import { createNodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
||||
import type { NodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { ContextualNodeDataValidator, createNodeDataValidator, type NodeDataValidator } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||
import type { NodeContextErrorMessageCreator } from '@/application/Parser/NodeValidation/NodeDataErrorContextMessage';
|
||||
import { getAbsentObjectTestCases, getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('createNodeDataValidator', () => {
|
||||
it(`returns an instance of ${ContextualNodeDataValidator.name}`, () => {
|
||||
// arrange
|
||||
const context = createNodeDataErrorContextStub();
|
||||
// act
|
||||
const validator = createNodeDataValidator(context);
|
||||
// assert
|
||||
expect(validator).to.be.instanceOf(ContextualNodeDataValidator);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodeDataValidator', () => {
|
||||
describe('assertValidName', () => {
|
||||
describe('throws when name is invalid', () => {
|
||||
// arrange
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly invalidName: unknown;
|
||||
readonly expectedMessage: string;
|
||||
}[] = [
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
description: `missing name (${testCase.valueName})`,
|
||||
invalidName: testCase.absentValue,
|
||||
expectedMessage: 'missing name',
|
||||
})),
|
||||
{
|
||||
description: 'invalid type',
|
||||
invalidName: 33,
|
||||
expectedMessage: 'Name (33) is not a string but number.',
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, invalidName, expectedMessage }) => {
|
||||
describe(`given "${description}"`, () => {
|
||||
itThrowsCorrectly({
|
||||
// act
|
||||
throwingAction: (sut) => {
|
||||
sut.assertValidName(invalidName as string);
|
||||
},
|
||||
// assert
|
||||
expectedMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('does not throw when name is valid', () => {
|
||||
// arrange
|
||||
const validName = 'validName';
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.assertValidName(validName);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('assertDefined', () => {
|
||||
describe('throws when node data is missing', () => {
|
||||
// arrange
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly invalidData: unknown;
|
||||
}[] = [
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
description: `absent object (${testCase.valueName})`,
|
||||
invalidData: testCase.absentValue,
|
||||
})),
|
||||
{
|
||||
description: 'empty object',
|
||||
invalidData: {},
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, invalidData }) => {
|
||||
describe(`given "${description}"`, () => {
|
||||
const expectedMessage = 'missing node data';
|
||||
itThrowsCorrectly({
|
||||
// act
|
||||
throwingAction: (sut: NodeDataValidator) => {
|
||||
sut.assertDefined(invalidData as NodeData);
|
||||
},
|
||||
// assert
|
||||
expectedMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('does not throw if node data is defined', () => {
|
||||
// arrange
|
||||
const definedNode = new CategoryDataStub();
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.assertDefined(definedNode);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('assert', () => {
|
||||
describe('throws if validation fails', () => {
|
||||
const falsePredicate = () => false;
|
||||
const expectedErrorMessage = 'expected error';
|
||||
// assert
|
||||
itThrowsCorrectly({
|
||||
// act
|
||||
throwingAction: (sut: NodeDataValidator) => {
|
||||
sut.assert(falsePredicate, expectedErrorMessage);
|
||||
},
|
||||
// assert
|
||||
expectedMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
it('does not throw if validation succeeds', () => {
|
||||
// arrange
|
||||
const truePredicate = () => true;
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.assert(truePredicate, 'ignored error');
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('createContextualErrorMessage', () => {
|
||||
it('creates using the correct error message', () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'expected error';
|
||||
const errorMessageBuilder: NodeContextErrorMessageCreator = (message) => message;
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.withErrorMessageCreator(errorMessageBuilder)
|
||||
.build();
|
||||
// act
|
||||
const actualErrorMessage = sut.createContextualErrorMessage(expectedErrorMessage);
|
||||
// assert
|
||||
expect(actualErrorMessage).to.equal(expectedErrorMessage);
|
||||
});
|
||||
it('creates using the correct context', () => {
|
||||
// arrange
|
||||
const expectedContext = createNodeDataErrorContextStub();
|
||||
let actualContext: NodeDataErrorContext | undefined;
|
||||
const errorMessageBuilder: NodeContextErrorMessageCreator = (_, context) => {
|
||||
actualContext = context;
|
||||
return '';
|
||||
};
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.withContext(expectedContext)
|
||||
.withErrorMessageCreator(errorMessageBuilder)
|
||||
.build();
|
||||
// act
|
||||
sut.createContextualErrorMessage('unimportant');
|
||||
// assert
|
||||
expect(actualContext).to.equal(expectedContext);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type ValidationThrowingFunction = (
|
||||
sut: ContextualNodeDataValidator,
|
||||
) => void;
|
||||
|
||||
interface ValidationThrowingTestScenario {
|
||||
readonly throwingAction: ValidationThrowingFunction,
|
||||
readonly expectedMessage: string;
|
||||
}
|
||||
|
||||
function itThrowsCorrectly(
|
||||
testScenario: ValidationThrowingTestScenario,
|
||||
): void {
|
||||
it('throws an error', () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'Injected error message';
|
||||
const errorMessageBuilder: NodeContextErrorMessageCreator = () => expectedErrorMessage;
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.withErrorMessageCreator(errorMessageBuilder)
|
||||
.build();
|
||||
// act
|
||||
const action = () => testScenario.throwingAction(sut);
|
||||
// assert
|
||||
expect(action).to.throw();
|
||||
});
|
||||
it('throws with the correct error message', () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = testScenario.expectedMessage;
|
||||
const errorMessageBuilder: NodeContextErrorMessageCreator = (message) => message;
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.withErrorMessageCreator(errorMessageBuilder)
|
||||
.build();
|
||||
// act
|
||||
const action = () => testScenario.throwingAction(sut);
|
||||
// assert
|
||||
const actualErrorMessage = collectExceptionMessage(action);
|
||||
expect(actualErrorMessage).to.equal(expectedErrorMessage);
|
||||
});
|
||||
it('throws with the correct context', () => {
|
||||
// arrange
|
||||
const expectedContext = createNodeDataErrorContextStub();
|
||||
const serializeContext = (context: NodeDataErrorContext) => JSON.stringify(context);
|
||||
const errorMessageBuilder:
|
||||
NodeContextErrorMessageCreator = (_, context) => serializeContext(context);
|
||||
const sut = new NodeValidatorBuilder()
|
||||
.withContext(expectedContext)
|
||||
.withErrorMessageCreator(errorMessageBuilder)
|
||||
.build();
|
||||
// act
|
||||
const action = () => testScenario.throwingAction(sut);
|
||||
// assert
|
||||
const expectedSerializedContext = serializeContext(expectedContext);
|
||||
const actualSerializedContext = collectExceptionMessage(action);
|
||||
expect(expectedSerializedContext).to.equal(actualSerializedContext);
|
||||
});
|
||||
}
|
||||
|
||||
class NodeValidatorBuilder {
|
||||
private errorContext: NodeDataErrorContext = createNodeDataErrorContextStub();
|
||||
|
||||
private errorMessageCreator: NodeContextErrorMessageCreator = () => `[${NodeValidatorBuilder.name}] stub error message`;
|
||||
|
||||
public withErrorMessageCreator(errorMessageCreator: NodeContextErrorMessageCreator): this {
|
||||
this.errorMessageCreator = errorMessageCreator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withContext(errorContext: NodeDataErrorContext): this {
|
||||
this.errorContext = errorContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): ContextualNodeDataValidator {
|
||||
return new ContextualNodeDataValidator(
|
||||
this.errorContext,
|
||||
this.errorMessageCreator,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
|
||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
||||
import type { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||
import { NodeValidationTestRunner } from './NodeValidatorTestRunner';
|
||||
|
||||
describe('NodeValidator', () => {
|
||||
describe('assertValidName', () => {
|
||||
describe('throws if invalid', () => {
|
||||
// arrange
|
||||
const context = new NodeDataErrorContextStub();
|
||||
const sut = new NodeValidator(context);
|
||||
// act
|
||||
const act = (invalidName: string) => sut.assertValidName(invalidName);
|
||||
// assert
|
||||
new NodeValidationTestRunner()
|
||||
.testInvalidNodeName((invalidName) => ({
|
||||
act: () => act(invalidName),
|
||||
expectedContext: context,
|
||||
}));
|
||||
});
|
||||
it('does not throw if valid', () => {
|
||||
// arrange
|
||||
const validName = 'validName';
|
||||
const sut = new NodeValidator(new NodeDataErrorContextStub());
|
||||
// act
|
||||
const act = () => sut.assertValidName(validName);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('assertDefined', () => {
|
||||
describe('throws if missing', () => {
|
||||
// arrange
|
||||
const context = new NodeDataErrorContextStub();
|
||||
const sut = new NodeValidator(context);
|
||||
// act
|
||||
const act = (undefinedNode: NodeData) => sut.assertDefined(undefinedNode);
|
||||
// assert
|
||||
new NodeValidationTestRunner()
|
||||
.testMissingNodeData((invalidName) => ({
|
||||
act: () => act(invalidName),
|
||||
expectedContext: context,
|
||||
}));
|
||||
});
|
||||
it('does not throw if defined', () => {
|
||||
// arrange
|
||||
const definedNode = mockNode();
|
||||
const sut = new NodeValidator(new NodeDataErrorContextStub());
|
||||
// act
|
||||
const act = () => sut.assertDefined(definedNode);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('assert', () => {
|
||||
it('throws expected error if condition is false', () => {
|
||||
// arrange
|
||||
const message = 'error';
|
||||
const falsePredicate = () => false;
|
||||
const context = new NodeDataErrorContextStub();
|
||||
const expected = new NodeDataError(message, context);
|
||||
const sut = new NodeValidator(context);
|
||||
// act
|
||||
const act = () => sut.assert(falsePredicate, message);
|
||||
// assert
|
||||
expectDeepThrowsError(act, expected);
|
||||
});
|
||||
it('does not throw if condition is true', () => {
|
||||
// arrange
|
||||
const truePredicate = () => true;
|
||||
const sut = new NodeValidator(new NodeDataErrorContextStub());
|
||||
// act
|
||||
const act = () => sut.assert(truePredicate, 'ignored error');
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('throw', () => {
|
||||
it('throws expected error', () => {
|
||||
// arrange
|
||||
const message = 'error';
|
||||
const context = new NodeDataErrorContextStub();
|
||||
const expected = new NodeDataError(message, context);
|
||||
const sut = new NodeValidator(context);
|
||||
// act
|
||||
const act = () => sut.throw(message);
|
||||
// assert
|
||||
expectDeepThrowsError(act, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockNode() {
|
||||
return new CategoryDataStub();
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import { NodeDataError, type INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
|
||||
import type { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||
import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
|
||||
export interface ITestScenario {
|
||||
readonly act: () => void;
|
||||
readonly expectedContext: INodeDataErrorContext;
|
||||
}
|
||||
|
||||
export class NodeValidationTestRunner {
|
||||
public testInvalidNodeName(
|
||||
testBuildPredicate: (invalidName: string) => ITestScenario,
|
||||
) {
|
||||
describe('throws given invalid names', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
testName: `missing name (${testCase.valueName})`,
|
||||
nameValue: testCase.absentValue,
|
||||
expectedMessage: 'missing name',
|
||||
})),
|
||||
{
|
||||
testName: 'invalid type',
|
||||
nameValue: 33,
|
||||
expectedMessage: 'Name (33) is not a string but number.',
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(`given "${testCase.testName}"`, () => {
|
||||
const test = testBuildPredicate(testCase.nameValue as never);
|
||||
expectThrowsNodeError(test, testCase.expectedMessage);
|
||||
});
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public testMissingNodeData(
|
||||
testBuildPredicate: (missingNode: NodeData) => ITestScenario,
|
||||
) {
|
||||
describe('throws given missing node data', () => {
|
||||
itEachAbsentTestCase([
|
||||
...getAbsentObjectTestCases(),
|
||||
{
|
||||
valueName: 'empty object',
|
||||
absentValue: {},
|
||||
},
|
||||
], (absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing node data';
|
||||
// act
|
||||
const test = testBuildPredicate(absentValue as NodeData);
|
||||
// assert
|
||||
expectThrowsNodeError(test, expectedError);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public runThrowingCase(
|
||||
testCase: {
|
||||
readonly name: string,
|
||||
readonly scenario: ITestScenario,
|
||||
readonly expectedMessage: string
|
||||
},
|
||||
) {
|
||||
it(testCase.name, () => {
|
||||
expectThrowsNodeError(testCase.scenario, testCase.expectedMessage);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function expectThrowsNodeError(
|
||||
test: ITestScenario,
|
||||
expectedMessage: string,
|
||||
) {
|
||||
// arrange
|
||||
const expected = new NodeDataError(expectedMessage, test.expectedContext);
|
||||
// act
|
||||
const act = () => test.act();
|
||||
// assert
|
||||
expectDeepThrowsError(act, expected);
|
||||
return this;
|
||||
}
|
||||
@@ -229,7 +229,11 @@ class ExpressionBuilder {
|
||||
}
|
||||
|
||||
public build() {
|
||||
return new Expression(this.position, this.evaluator, this.parameters);
|
||||
return new Expression({
|
||||
position: this.position,
|
||||
evaluator: this.evaluator,
|
||||
parameters: this.parameters,
|
||||
});
|
||||
}
|
||||
|
||||
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createPositionFromRegexFullMatch } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
|
||||
|
||||
describe('ExpressionPositionFactory', () => {
|
||||
describe('createPositionFromRegexFullMatch', () => {
|
||||
it(`creates ${ExpressionPosition.name} instance`, () => {
|
||||
describe('it is a transient factory', () => {
|
||||
// arrange
|
||||
const expectedType = ExpressionPosition;
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: 5,
|
||||
});
|
||||
const fakeMatch = createRegexMatch();
|
||||
// act
|
||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
||||
const create = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(position).to.be.instanceOf(expectedType);
|
||||
itIsTransientFactory({
|
||||
getter: create,
|
||||
expectedType: ExpressionPosition,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a position with the correct start position', () => {
|
||||
// arrange
|
||||
const expectedStartPosition = 5;
|
||||
@@ -63,10 +62,8 @@ describe('ExpressionPositionFactory', () => {
|
||||
describe('invalid values', () => {
|
||||
it('throws an error if match.index is undefined', () => {
|
||||
// arrange
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: undefined,
|
||||
});
|
||||
const fakeMatch = createRegexMatch();
|
||||
fakeMatch.index = undefined;
|
||||
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
|
||||
// act
|
||||
const act = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||
@@ -94,9 +91,9 @@ function createRegexMatch(options?: {
|
||||
readonly capturingGroups?: readonly string[],
|
||||
readonly matchIndex?: number,
|
||||
}): RegExpMatchArray {
|
||||
const fullMatch = options?.fullMatch ?? 'fake match';
|
||||
const fullMatch = options?.fullMatch ?? 'default fake match';
|
||||
const capturingGroups = options?.capturingGroups ?? [];
|
||||
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
|
||||
fakeMatch.index = options?.matchIndex;
|
||||
fakeMatch.index = options?.matchIndex ?? 0;
|
||||
return fakeMatch;
|
||||
}
|
||||
@@ -1,168 +1,438 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
|
||||
import { type IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser';
|
||||
import type {
|
||||
ExpressionEvaluator, ExpressionInitParameters,
|
||||
} from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
|
||||
import {
|
||||
type PrimitiveExpression, RegexParser, type ExpressionFactory, type RegexParserUtilities,
|
||||
} from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser';
|
||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||
import type { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
||||
import type { ExpressionPositionFactory } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||
|
||||
describe('RegexParser', () => {
|
||||
describe('findExpressions', () => {
|
||||
describe('throws when code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
describe('error handling', () => {
|
||||
describe('throws when code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing code';
|
||||
const sut = new RegexParserConcrete({
|
||||
regex: /unimportant/,
|
||||
});
|
||||
// act
|
||||
const act = () => sut.findExpressions(absentValue);
|
||||
// assert
|
||||
const errorMessage = collectExceptionMessage(act);
|
||||
expect(errorMessage).to.include(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('rethrows regex match errors', () => {
|
||||
// arrange
|
||||
const expectedError = 'missing code';
|
||||
const sut = new RegexParserConcrete(/unimportant/);
|
||||
// act
|
||||
const act = () => sut.findExpressions(absentValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
const expectedMatchError = new TypeError('String.prototype.matchAll called with a non-global RegExp argument');
|
||||
const expectedMessage = 'Failed to match regex.';
|
||||
const expectedCodeInMessage = 'unimportant code content';
|
||||
const expectedRegexInMessage = /failing-regex-because-it-is-non-global/;
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
expectedWrappedError: expectedMatchError,
|
||||
});
|
||||
});
|
||||
describe('rethrows expression building errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to build expression.';
|
||||
const expectedInnerError = new Error('Expected error from building expression');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingExpressionBuilder = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
builder: throwingExpressionBuilder,
|
||||
utilities: {
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows position creation errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to create position.';
|
||||
const expectedInnerError = new Error('Expected error from position factory');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingPositionFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
createPosition: throwingPositionFactory,
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows parameter creation errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to create parameters.';
|
||||
const expectedInnerError = new Error('Expected error from parameter collection factory');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingParameterCollectionFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
createParameterCollection: throwingParameterCollectionFactory,
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows expression creation errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to create expression.';
|
||||
const expectedInnerError = new Error('Expected error from expression factory');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingExpressionFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
createExpression: throwingExpressionFactory,
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws when position is invalid', () => {
|
||||
describe('handles matched regex correctly', () => {
|
||||
// arrange
|
||||
const regexMatchingEmpty = /^/gm; /* expressions cannot be empty */
|
||||
const code = 'unimportant';
|
||||
const expectedErrorParts = [
|
||||
`[${RegexParserConcrete.constructor.name}]`,
|
||||
'invalid script position',
|
||||
`Regex: ${regexMatchingEmpty}`,
|
||||
`Code: ${code}`,
|
||||
];
|
||||
const sut = new RegexParserConcrete(regexMatchingEmpty);
|
||||
// act
|
||||
let errorMessage: string | undefined;
|
||||
try {
|
||||
sut.findExpressions(code);
|
||||
} catch (err) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
// assert
|
||||
expectExists(errorMessage);
|
||||
const error = errorMessage; // workaround for ts(18048): possibly 'undefined'
|
||||
expect(
|
||||
expectedErrorParts.every((part) => error.includes(part)),
|
||||
`Expected parts: ${expectedErrorParts.join(', ')}`
|
||||
+ `Actual error: ${errorMessage}`,
|
||||
);
|
||||
});
|
||||
describe('matches regex as expected', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly regex: RegExp;
|
||||
readonly code: string;
|
||||
}[] = [
|
||||
{
|
||||
name: 'returns no result when regex does not match',
|
||||
description: 'non-matching regex',
|
||||
regex: /hello/g,
|
||||
code: 'world',
|
||||
},
|
||||
{
|
||||
name: 'returns expected when regex matches single',
|
||||
description: 'single regex match',
|
||||
regex: /hello/g,
|
||||
code: 'hello world',
|
||||
},
|
||||
{
|
||||
name: 'returns expected when regex matches multiple',
|
||||
description: 'multiple regex matches',
|
||||
regex: /l/g,
|
||||
code: 'hello world',
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const expected = Array.from(testCase.code.matchAll(testCase.regex));
|
||||
const matches = new Array<RegExpMatchArray>();
|
||||
const builder = (m: RegExpMatchArray): IPrimitiveExpression => {
|
||||
matches.push(m);
|
||||
return mockPrimitiveExpression();
|
||||
};
|
||||
const sut = new RegexParserConcrete(testCase.regex, builder);
|
||||
// act
|
||||
const expressions = sut.findExpressions(testCase.code);
|
||||
// assert
|
||||
expect(expressions).to.have.lengthOf(matches.length);
|
||||
expect(matches).to.deep.equal(expected);
|
||||
testScenarios.forEach(({
|
||||
description, code, regex,
|
||||
}) => {
|
||||
describe(description, () => {
|
||||
it('generates expressions for all matches', () => {
|
||||
// arrange
|
||||
const expectedTotalExpressions = Array.from(code.matchAll(regex)).length;
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
});
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
const actualTotalExpressions = expressions.length;
|
||||
expect(actualTotalExpressions).to.equal(
|
||||
expectedTotalExpressions,
|
||||
formatAssertionMessage([
|
||||
`Expected ${actualTotalExpressions} expressions due to ${expectedTotalExpressions} matches`,
|
||||
`Expressions:\n${indentText(JSON.stringify(expressions, undefined, 2))}`,
|
||||
]),
|
||||
);
|
||||
});
|
||||
it('builds primitive expressions for each match', () => {
|
||||
const expected = Array.from(code.matchAll(regex));
|
||||
const matches = new Array<RegExpMatchArray>();
|
||||
const builder = (m: RegExpMatchArray): PrimitiveExpression => {
|
||||
matches.push(m);
|
||||
return createPrimitiveExpressionStub();
|
||||
};
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
builder,
|
||||
});
|
||||
// act
|
||||
sut.findExpressions(code);
|
||||
// assert
|
||||
expect(matches).to.deep.equal(expected);
|
||||
});
|
||||
it('sets positions correctly from matches', () => {
|
||||
// arrange
|
||||
const expectedMatches = [...code.matchAll(regex)];
|
||||
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||
const serializeRegexMatch = (match: RegExpMatchArray) => `[startPos:${match?.index ?? 'none'},length:${match?.[0]?.length ?? 'none'}]`;
|
||||
const positionsForMatches = new Map<string, ExpressionPosition>(expectedMatches.map(
|
||||
(expectedMatch) => [serializeRegexMatch(expectedMatch), new ExpressionPosition(1, 4)],
|
||||
));
|
||||
const createPositionMock: ExpressionPositionFactory = (match) => {
|
||||
const position = positionsForMatches.get(serializeRegexMatch(match));
|
||||
return position ?? new ExpressionPosition(66, 666);
|
||||
};
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
utilities: {
|
||||
createExpression,
|
||||
createPosition: createPositionMock,
|
||||
},
|
||||
});
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
const expectedPositions = [...positionsForMatches.values()];
|
||||
const actualPositions = expressions.map((e) => getInitParameters(e)?.position);
|
||||
expect(actualPositions).to.deep.equal(expectedPositions, formatAssertionMessage([
|
||||
'Actual positions do not match the expected positions.',
|
||||
`Expected total positions: ${expectedPositions.length} (due to ${expectedMatches.length} regex matches)`,
|
||||
`Actual total positions: ${actualPositions.length}`,
|
||||
`Expected positions:\n${indentText(JSON.stringify(expectedPositions, undefined, 2))}`,
|
||||
`Actual positions:\n${indentText(JSON.stringify(actualPositions, undefined, 2))}`,
|
||||
]));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
it('sets evaluator as expected', () => {
|
||||
// arrange
|
||||
const expected = getEvaluatorStub();
|
||||
const regex = /hello/g;
|
||||
const code = 'hello';
|
||||
const builder = (): IPrimitiveExpression => ({
|
||||
evaluator: expected,
|
||||
});
|
||||
const sut = new RegexParserConcrete(regex, builder);
|
||||
});
|
||||
it('sets evaluator correctly from expression', () => {
|
||||
// arrange
|
||||
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||
const expectedEvaluate = createEvaluatorStub();
|
||||
const { code, regex } = createCodeAndRegexMatchingOnce();
|
||||
const builder = (): PrimitiveExpression => ({
|
||||
evaluator: expectedEvaluate,
|
||||
});
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
builder,
|
||||
utilities: {
|
||||
createExpression,
|
||||
},
|
||||
});
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
expect(expressions).to.have.lengthOf(1);
|
||||
expect(expressions[0].evaluate === expected);
|
||||
const actualEvaluate = getInitParameters(expressions[0])?.evaluator;
|
||||
expect(actualEvaluate).to.equal(expectedEvaluate);
|
||||
});
|
||||
it('sets parameters as expected', () => {
|
||||
it('sets parameters correctly from expression', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new FunctionParameterStub().withName('parameter1').withOptionality(true),
|
||||
new FunctionParameterStub().withName('parameter2').withOptionality(false),
|
||||
const expectedParameters: IReadOnlyFunctionParameterCollection['all'] = [
|
||||
new FunctionParameterStub().withName('parameter1').withOptional(true),
|
||||
new FunctionParameterStub().withName('parameter2').withOptional(false),
|
||||
];
|
||||
const regex = /hello/g;
|
||||
const code = 'hello';
|
||||
const builder = (): IPrimitiveExpression => ({
|
||||
evaluator: getEvaluatorStub(),
|
||||
parameters: expected,
|
||||
const builder = (): PrimitiveExpression => ({
|
||||
evaluator: createEvaluatorStub(),
|
||||
parameters: expectedParameters,
|
||||
});
|
||||
const parameterCollection = new FunctionParameterCollectionStub();
|
||||
const parameterCollectionFactoryStub
|
||||
: FunctionParameterCollectionFactory = () => parameterCollection;
|
||||
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
builder,
|
||||
utilities: {
|
||||
createExpression,
|
||||
createParameterCollection: parameterCollectionFactoryStub,
|
||||
},
|
||||
});
|
||||
const sut = new RegexParserConcrete(regex, builder);
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
expect(expressions).to.have.lengthOf(1);
|
||||
expect(expressions[0].parameters.all).to.deep.equal(expected);
|
||||
});
|
||||
it('sets expected position', () => {
|
||||
// arrange
|
||||
const code = 'mate date in state is fate';
|
||||
const regex = /ate/g;
|
||||
const expected = [
|
||||
new ExpressionPosition(1, 4),
|
||||
new ExpressionPosition(6, 9),
|
||||
new ExpressionPosition(15, 18),
|
||||
new ExpressionPosition(23, 26),
|
||||
];
|
||||
const sut = new RegexParserConcrete(regex);
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
const actual = expressions.map((e) => e.position);
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualParameters = getInitParameters(expressions[0])?.parameters;
|
||||
expect(actualParameters).to.equal(parameterCollection);
|
||||
expect(actualParameters?.all).to.deep.equal(expectedParameters);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
|
||||
return () => ({
|
||||
evaluator: getEvaluatorStub(),
|
||||
});
|
||||
}
|
||||
function getEvaluatorStub(): ExpressionEvaluator {
|
||||
return () => `[${getEvaluatorStub.name}] evaluated code`;
|
||||
function buildRethrowErrorMessage(
|
||||
expectedContext: {
|
||||
readonly message: string;
|
||||
readonly regex: RegExp;
|
||||
readonly code: string;
|
||||
},
|
||||
): string {
|
||||
return [
|
||||
expectedContext.message,
|
||||
`Class name: ${RegexParserConcrete.name}`,
|
||||
`Regex pattern used: ${expectedContext.regex}`,
|
||||
`Code: ${expectedContext.code}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function mockPrimitiveExpression(): IPrimitiveExpression {
|
||||
function createExpressionFactorySpy() {
|
||||
const createdExpressions = new Map<IExpression, ExpressionInitParameters>();
|
||||
const createExpression: ExpressionFactory = (parameters) => {
|
||||
const expression = new ExpressionStub();
|
||||
createdExpressions.set(expression, parameters);
|
||||
return expression;
|
||||
};
|
||||
return {
|
||||
evaluator: getEvaluatorStub(),
|
||||
createExpression,
|
||||
getInitParameters: (expression) => createdExpressions.get(expression),
|
||||
};
|
||||
}
|
||||
|
||||
function createBuilderStub(): (match: RegExpMatchArray) => PrimitiveExpression {
|
||||
return () => ({
|
||||
evaluator: createEvaluatorStub(),
|
||||
});
|
||||
}
|
||||
function createEvaluatorStub(): ExpressionEvaluator {
|
||||
return () => `[${createEvaluatorStub.name}] evaluated code`;
|
||||
}
|
||||
|
||||
function createPrimitiveExpressionStub(): PrimitiveExpression {
|
||||
return {
|
||||
evaluator: createEvaluatorStub(),
|
||||
};
|
||||
}
|
||||
|
||||
function createCodeAndRegexMatchingOnce() {
|
||||
const code = 'expected code in context';
|
||||
const regex = /code/g;
|
||||
return { code, regex };
|
||||
}
|
||||
|
||||
class RegexParserConcrete extends RegexParser {
|
||||
private readonly builder: RegexParser['buildExpression'];
|
||||
|
||||
protected regex: RegExp;
|
||||
|
||||
public constructor(
|
||||
regex: RegExp,
|
||||
private readonly builder = mockBuilder(),
|
||||
) {
|
||||
super();
|
||||
this.regex = regex;
|
||||
public constructor(parameters?: {
|
||||
regex?: RegExp,
|
||||
builder?: RegexParser['buildExpression'],
|
||||
utilities?: Partial<RegexParserUtilities>,
|
||||
}) {
|
||||
super({
|
||||
wrapError: parameters?.utilities?.wrapError
|
||||
?? (() => new Error(`[${RegexParserConcrete}] wrapped error`)),
|
||||
createPosition: parameters?.utilities?.createPosition
|
||||
?? (() => new ExpressionPosition(0, 5)),
|
||||
createExpression: parameters?.utilities?.createExpression
|
||||
?? (() => new ExpressionStub()),
|
||||
createParameterCollection: parameters?.utilities?.createParameterCollection
|
||||
?? (() => new FunctionParameterCollectionStub()),
|
||||
});
|
||||
this.builder = parameters?.builder ?? createBuilderStub();
|
||||
this.regex = parameters?.regex ?? /unimportant/g;
|
||||
}
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
|
||||
return this.builder(match);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||
import type { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import type { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
@@ -17,8 +15,13 @@ import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICod
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||
import { itThrowsContextualError } from '../../ContextualErrorTester';
|
||||
|
||||
describe('ScriptCompiler', () => {
|
||||
describe('canCompile', () => {
|
||||
@@ -58,31 +61,59 @@ describe('ScriptCompiler', () => {
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns code as expected', () => {
|
||||
// arrange
|
||||
const expected: CompiledCode = {
|
||||
code: 'expected-code',
|
||||
revertCode: 'expected-revert-code',
|
||||
};
|
||||
const call = new FunctionCallDataStub();
|
||||
const script = createScriptDataWithCall(call);
|
||||
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||
const functionParserMock = new SharedFunctionsParserStub();
|
||||
functionParserMock.setup(functions, compiledFunctions);
|
||||
const callCompilerMock = new FunctionCallCompilerStub();
|
||||
callCompilerMock.setup(parseFunctionCalls(call), compiledFunctions, expected);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.withSharedFunctionsParser(functionParserMock)
|
||||
.withFunctionCallCompiler(callCompilerMock)
|
||||
.build();
|
||||
// act
|
||||
const code = sut.compile(script);
|
||||
// assert
|
||||
expect(code.execute).to.equal(expected.code);
|
||||
expect(code.revert).to.equal(expected.revertCode);
|
||||
describe('code construction', () => {
|
||||
it('returns code from the factory', () => {
|
||||
// arrange
|
||||
const expectedCode = new ScriptCodeStub();
|
||||
const scriptCodeFactory = () => expectedCode;
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.build();
|
||||
// act
|
||||
const actualCode = sut.compile(createScriptDataWithCall());
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
it('creates code correctly', () => {
|
||||
// arrange
|
||||
const expectedCode = 'expected-code';
|
||||
const expectedRevertCode = 'expected-revert-code';
|
||||
let actualCode: string | undefined;
|
||||
let actualRevertCode: string | undefined;
|
||||
const scriptCodeFactory = (code: string, revertCode: string) => {
|
||||
actualCode = code;
|
||||
actualRevertCode = revertCode;
|
||||
return new ScriptCodeStub();
|
||||
};
|
||||
const call = new FunctionCallDataStub();
|
||||
const script = createScriptDataWithCall(call);
|
||||
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||
const functionParserMock = new SharedFunctionsParserStub();
|
||||
functionParserMock.setup(functions, compiledFunctions);
|
||||
const callCompilerMock = new FunctionCallCompilerStub();
|
||||
callCompilerMock.setup(
|
||||
parseFunctionCalls(call),
|
||||
compiledFunctions,
|
||||
new CompiledCodeStub()
|
||||
.withCode(expectedCode)
|
||||
.withRevertCode(expectedRevertCode),
|
||||
);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.withSharedFunctionsParser(functionParserMock)
|
||||
.withFunctionCallCompiler(callCompilerMock)
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.build();
|
||||
// act
|
||||
sut.compile(script);
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parses functions as expected', () => {
|
||||
it('parses functions with expected syntax', () => {
|
||||
// arrange
|
||||
@@ -116,49 +147,57 @@ describe('ScriptCompiler', () => {
|
||||
expect(parser.callHistory[0].functions).to.deep.equal(expectedFunctions);
|
||||
});
|
||||
});
|
||||
it('rethrows error with script name', () => {
|
||||
describe('rethrows error with script name', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const innerError = 'innerError';
|
||||
const expectedError = `Script "${scriptName}" ${innerError}`;
|
||||
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||
const expectedInnerError = new Error();
|
||||
const callCompiler: FunctionCallCompiler = {
|
||||
compileFunctionCalls: () => { throw new Error(innerError); },
|
||||
compileFunctionCalls: () => { throw expectedInnerError; },
|
||||
};
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withFunctionCallCompiler(callCompiler)
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
.withFunctionCallCompiler(callCompiler);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.build()
|
||||
.compile(scriptData);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
it('rethrows error from ScriptCode with script name', () => {
|
||||
describe('rethrows error from script code factory with script name', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
const invalidCode = new CompiledCodeStub()
|
||||
.withCode('' /* invalid code (empty string) */);
|
||||
const realExceptionMessage = collectExceptionMessage(
|
||||
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
|
||||
);
|
||||
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
|
||||
const callCompiler: FunctionCallCompiler = {
|
||||
compileFunctionCalls: () => invalidCode,
|
||||
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||
const expectedInnerError = new Error();
|
||||
const scriptCodeFactory: ScriptCodeFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withFunctionCallCompiler(callCompiler)
|
||||
.withSyntax(syntax)
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
.withScriptCodeFactory(scriptCodeFactory);
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
builder
|
||||
.withErrorWrapper(wrapError)
|
||||
.build()
|
||||
.compile(scriptData);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
it('validates compiled code as expected', () => {
|
||||
// arrange
|
||||
@@ -166,17 +205,27 @@ describe('ScriptCompiler', () => {
|
||||
NoEmptyLines,
|
||||
// Allow duplicated lines to enable calling same function multiple times
|
||||
];
|
||||
const expectedExecuteCode = 'execute code to be validated';
|
||||
const expectedRevertCode = 'revert code to be validated';
|
||||
const scriptData = createScriptDataWithCall();
|
||||
const validator = new CodeValidatorStub();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withCodeValidator(validator)
|
||||
.withFunctionCallCompiler(
|
||||
new FunctionCallCompilerStub()
|
||||
.withDefaultCompiledCode(
|
||||
new CompiledCodeStub()
|
||||
.withCode(expectedExecuteCode)
|
||||
.withRevertCode(expectedRevertCode),
|
||||
),
|
||||
)
|
||||
.build();
|
||||
// act
|
||||
const compilationResult = sut.compile(scriptData);
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
validator.assertHistory({
|
||||
validatedCodes: [compilationResult.execute, compilationResult.revert],
|
||||
validatedCodes: [expectedExecuteCode, expectedRevertCode],
|
||||
rules: expectedRules,
|
||||
});
|
||||
});
|
||||
@@ -200,6 +249,12 @@ class ScriptCompilerBuilder {
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
|
||||
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||
|
||||
private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
|
||||
defaultCodePrefix: ScriptCompilerBuilder.name,
|
||||
});
|
||||
|
||||
public withFunctions(...functions: FunctionData[]): this {
|
||||
this.functions = functions;
|
||||
return this;
|
||||
@@ -244,6 +299,16 @@ class ScriptCompilerBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||
this.wrapError = wrapError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
|
||||
this.scriptCodeFactory = scriptCodeFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): ScriptCompiler {
|
||||
if (!this.functions) {
|
||||
throw new Error('Function behavior not defined');
|
||||
@@ -254,6 +319,8 @@ class ScriptCompilerBuilder {
|
||||
this.sharedFunctionsParser,
|
||||
this.callCompiler,
|
||||
this.codeValidator,
|
||||
this.wrapError,
|
||||
this.scriptCodeFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ScriptData } from '@/application/collections/';
|
||||
import { parseScript, type ScriptFactoryType } from '@/application/Parser/Script/ScriptParser';
|
||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
||||
import { parseScript, type ScriptFactory } from '@/application/Parser/Script/ScriptParser';
|
||||
import { type DocsParser } from '@/application/Parser/DocumentationParser';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import type { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
@@ -11,54 +11,88 @@ import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { expectThrowsNodeError, type ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
||||
import { Script } from '@/domain/Script';
|
||||
import type { IEnumParser } from '@/application/Common/Enum';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||
import type { NodeDataValidatorFactory } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||
import { NodeDataValidatorStub, createNodeDataValidatorFactoryStub } from '@tests/unit/shared/Stubs/NodeDataValidatorStub';
|
||||
import { NodeDataType } from '@/application/Parser/NodeValidation/NodeDataType';
|
||||
import type { ScriptNodeErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { itThrowsContextualError } from '../ContextualErrorTester';
|
||||
import { itAsserts, itValidatesDefinedData, itValidatesName } from '../NodeDataValidationTester';
|
||||
import { generateDataValidationTestScenarios } from '../DataValidationTestScenarioGenerator';
|
||||
|
||||
describe('ScriptParser', () => {
|
||||
describe('parseScript', () => {
|
||||
it('parses name as expected', () => {
|
||||
it('parses name correctly', () => {
|
||||
// arrange
|
||||
const expected = 'test-expected-name';
|
||||
const script = createScriptDataWithCode()
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withName(expected);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
const actualScript = new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.name).to.equal(expected);
|
||||
const actualName = getInitParameters(actualScript)?.name;
|
||||
expect(actualName).to.equal(expected);
|
||||
});
|
||||
it('parses docs as expected', () => {
|
||||
it('parses docs correctly', () => {
|
||||
// arrange
|
||||
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||
const script = createScriptDataWithCode()
|
||||
.withDocs(docs);
|
||||
const expected = parseDocs(script);
|
||||
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withDocs(expectedDocs);
|
||||
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
const actualScript = new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.withDocsParser(docsParser)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.docs).to.deep.equal(expected);
|
||||
const actualDocs = getInitParameters(actualScript)?.docs;
|
||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||
});
|
||||
it('gets script from the factory', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('expected-script');
|
||||
const scriptFactory: ScriptFactory = () => expectedScript;
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withScriptFactory(scriptFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
describe('level', () => {
|
||||
describe('accepts absent level', () => {
|
||||
describe('generated `undefined` level if given absent value', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const script = createScriptDataWithCode()
|
||||
const expectedLevel = undefined;
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withRecommend(absentValue);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
const actualScript = new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.level).to.equal(undefined);
|
||||
const actualLevel = getInitParameters(actualScript)?.level;
|
||||
expect(actualLevel).to.equal(expectedLevel);
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
it('parses level as expected', () => {
|
||||
@@ -66,63 +100,94 @@ describe('ScriptParser', () => {
|
||||
const expectedLevel = RecommendationLevel.Standard;
|
||||
const expectedName = 'level';
|
||||
const levelText = 'standard';
|
||||
const script = createScriptDataWithCode()
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withRecommend(levelText);
|
||||
const parserMock = new EnumParserStub<RecommendationLevel>()
|
||||
.setup(expectedName, levelText, expectedLevel);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
const actualScript = new TestContext()
|
||||
.withData(scriptData)
|
||||
.withParser(parserMock)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.level).to.equal(expectedLevel);
|
||||
const actualLevel = getInitParameters(actualScript)?.level;
|
||||
expect(actualLevel).to.equal(expectedLevel);
|
||||
});
|
||||
});
|
||||
describe('code', () => {
|
||||
it('parses "execute" as expected', () => {
|
||||
it('creates from script code factory', () => {
|
||||
// arrange
|
||||
const expected = 'expected-code';
|
||||
const script = createScriptDataWithCode()
|
||||
.withCode(expected);
|
||||
const expectedCode = new ScriptCodeStub();
|
||||
const scriptCodeFactory: ScriptCodeFactory = () => expectedCode;
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const parsed = new TestBuilder()
|
||||
.withData(script)
|
||||
const actualScript = new TestContext()
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actual = parsed.code.execute;
|
||||
expect(actual).to.equal(expected);
|
||||
const actualCode = getInitParameters(actualScript)?.code;
|
||||
expect(expectedCode).to.equal(actualCode);
|
||||
});
|
||||
it('parses "revert" as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-revert-code';
|
||||
const script = createScriptDataWithCode()
|
||||
.withRevertCode(expected);
|
||||
// act
|
||||
const parsed = new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actual = parsed.code.revert;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
describe('compiler', () => {
|
||||
it('gets code from compiler', () => {
|
||||
describe('parses code correctly', () => {
|
||||
it('parses "execute" as expected', () => {
|
||||
// arrange
|
||||
const expected = new ScriptCodeStub();
|
||||
const script = createScriptDataWithCode();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script, expected);
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
const expectedCode = 'expected-code';
|
||||
let actualCode: string | undefined;
|
||||
const scriptCodeFactory: ScriptCodeFactory = (code) => {
|
||||
actualCode = code;
|
||||
return new ScriptCodeStub();
|
||||
};
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withCode(expectedCode);
|
||||
// act
|
||||
const parsed = new TestBuilder()
|
||||
.withData(script)
|
||||
.withContext(parseContext)
|
||||
new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actual = parsed.code;
|
||||
expect(actual).to.equal(expected);
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
it('parses "revert" as expected', () => {
|
||||
// arrange
|
||||
const expectedRevertCode = 'expected-revert-code';
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withRevertCode(expectedRevertCode);
|
||||
let actualRevertCode: string | undefined;
|
||||
const scriptCodeFactory: ScriptCodeFactory = (_, revertCode) => {
|
||||
actualRevertCode = revertCode;
|
||||
return new ScriptCodeStub();
|
||||
};
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
});
|
||||
});
|
||||
describe('compiler', () => {
|
||||
it('compiles the code through the compiler', () => {
|
||||
// arrange
|
||||
const expectedCode = new ScriptCodeStub();
|
||||
const script = createScriptDataWithCode();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script, expectedCode);
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withData(script)
|
||||
.withContext(parseContext)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actualCode = getInitParameters(actualScript)?.code;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
});
|
||||
describe('syntax', () => {
|
||||
@@ -135,7 +200,7 @@ describe('ScriptParser', () => {
|
||||
const script = createScriptDataWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode);
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
const act = () => new TestContext()
|
||||
.withData(script)
|
||||
.withContext(parseContext);
|
||||
// assert
|
||||
@@ -149,18 +214,26 @@ describe('ScriptParser', () => {
|
||||
NoEmptyLines,
|
||||
NoDuplicatedLines,
|
||||
];
|
||||
const expectedCode = 'expected code to be validated';
|
||||
const expectedRevertCode = 'expected revert code to be validated';
|
||||
const expectedCodeCalls = [
|
||||
expectedCode,
|
||||
expectedRevertCode,
|
||||
];
|
||||
const validator = new CodeValidatorStub();
|
||||
const script = createScriptDataWithCode()
|
||||
.withCode('expected code to be validated')
|
||||
.withRevertCode('expected revert code to be validated');
|
||||
const scriptCodeFactory = createScriptCodeFactoryStub({
|
||||
scriptCode: new ScriptCodeStub()
|
||||
.withExecute(expectedCode)
|
||||
.withRevert(expectedRevertCode),
|
||||
});
|
||||
// act
|
||||
new TestBuilder()
|
||||
.withData(script)
|
||||
new TestContext()
|
||||
.withScriptCodeFactory(scriptCodeFactory)
|
||||
.withCodeValidator(validator)
|
||||
.parseScript();
|
||||
// assert
|
||||
validator.assertHistory({
|
||||
validatedCodes: [script.code, script.revertCode],
|
||||
validatedCodes: expectedCodeCalls,
|
||||
rules: expectedRules,
|
||||
});
|
||||
});
|
||||
@@ -175,7 +248,7 @@ describe('ScriptParser', () => {
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(script)
|
||||
.withCodeValidator(validator)
|
||||
.withContext(parseContext)
|
||||
@@ -188,111 +261,250 @@ describe('ScriptParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('invalid script data', () => {
|
||||
describe('validates script data', () => {
|
||||
describe('validation', () => {
|
||||
describe('validates for name', () => {
|
||||
// arrange
|
||||
const createTest = (script: ScriptData): ITestScenario => ({
|
||||
act: () => new TestBuilder()
|
||||
const expectedName = 'expected script name to be validated';
|
||||
const script = createScriptDataWithCall()
|
||||
.withName(expectedName);
|
||||
const expectedContext: ScriptNodeErrorContext = {
|
||||
type: NodeDataType.Script,
|
||||
selfNode: script,
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(script)
|
||||
.parseScript(),
|
||||
expectedContext: {
|
||||
type: NodeType.Script,
|
||||
selfNode: script,
|
||||
},
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
return {
|
||||
expectedNameToValidate: expectedName,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
});
|
||||
// act and assert
|
||||
new NodeValidationTestRunner()
|
||||
.testInvalidNodeName((invalidName) => {
|
||||
return createTest(
|
||||
createScriptDataWithCall().withName(invalidName),
|
||||
);
|
||||
})
|
||||
.testMissingNodeData((node) => {
|
||||
return createTest(node as ScriptData);
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when both function call and code are defined',
|
||||
scenario: createTest(
|
||||
createScriptDataWithCall().withCode('code'),
|
||||
),
|
||||
expectedMessage: 'Both "call" and "code" are defined.',
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when both function call and revertCode are defined',
|
||||
scenario: createTest(
|
||||
createScriptDataWithCall().withRevertCode('revert-code'),
|
||||
),
|
||||
expectedMessage: 'Both "call" and "revertCode" are defined.',
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when neither call or revertCode are defined',
|
||||
scenario: createTest(
|
||||
createScriptDataWithoutCallOrCodes(),
|
||||
),
|
||||
expectedMessage: 'Neither "call" or "code" is defined.',
|
||||
});
|
||||
});
|
||||
it(`rethrows exception if ${Script.name} cannot be constructed`, () => {
|
||||
describe('validates for defined data', () => {
|
||||
// arrange
|
||||
const expectedError = 'script creation failed';
|
||||
const factoryMock: ScriptFactoryType = () => { throw new Error(expectedError); };
|
||||
const data = createScriptDataWithCode();
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withData(data)
|
||||
.withFactory(factoryMock)
|
||||
.parseScript();
|
||||
// expect
|
||||
expectThrowsNodeError({
|
||||
act,
|
||||
expectedContext: {
|
||||
type: NodeType.Script,
|
||||
selfNode: data,
|
||||
const expectedScript = createScriptDataWithCall();
|
||||
const expectedContext: ScriptNodeErrorContext = {
|
||||
type: NodeDataType.Script,
|
||||
selfNode: expectedScript,
|
||||
};
|
||||
itValidatesDefinedData(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(expectedScript)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
return {
|
||||
expectedDataToValidate: expectedScript,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
}, expectedError);
|
||||
);
|
||||
});
|
||||
describe('validates data', () => {
|
||||
// arrange
|
||||
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
|
||||
{
|
||||
assertErrorMessage: 'Neither "call" or "code" is defined.',
|
||||
expectFail: [{
|
||||
description: 'with no call or code',
|
||||
data: createScriptDataWithoutCallOrCodes(),
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'with call',
|
||||
data: createScriptDataWithCall(),
|
||||
},
|
||||
{
|
||||
description: 'with code',
|
||||
data: createScriptDataWithCode(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
|
||||
expectFail: [{
|
||||
description: 'with both call and revertCode',
|
||||
data: createScriptDataWithCall()
|
||||
.withRevertCode('revert-code'),
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'with call, without revertCode',
|
||||
data: createScriptDataWithCall()
|
||||
.withRevertCode(undefined),
|
||||
},
|
||||
{
|
||||
description: 'with revertCode, without call',
|
||||
data: createScriptDataWithCode()
|
||||
.withRevertCode('revert code'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
assertErrorMessage: 'Both "call" and "code" are defined.',
|
||||
expectFail: [{
|
||||
description: 'with both call and code',
|
||||
data: createScriptDataWithCall()
|
||||
.withCode('code'),
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'with call, without code',
|
||||
data: createScriptDataWithCall()
|
||||
.withCode(''),
|
||||
},
|
||||
{
|
||||
description: 'with code, without call',
|
||||
data: createScriptDataWithCode()
|
||||
.withCode('code'),
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
testScenarios.forEach(({
|
||||
description, expectedPass, data: scriptData, expectedMessage,
|
||||
}) => {
|
||||
describe(description, () => {
|
||||
itAsserts({
|
||||
expectedConditionResult: expectedPass,
|
||||
test: (validatorFactory) => {
|
||||
const expectedContext: ScriptNodeErrorContext = {
|
||||
type: NodeDataType.Script,
|
||||
selfNode: scriptData,
|
||||
};
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(scriptData)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
expectExists(expectedMessage);
|
||||
return {
|
||||
expectedErrorMessage: expectedMessage,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('rethrows exception if script factory fails', () => {
|
||||
// arrange
|
||||
const givenData = createScriptDataWithCode();
|
||||
const expectedContextMessage = 'Failed to parse script.';
|
||||
const expectedError = new Error();
|
||||
const validatorFactory: NodeDataValidatorFactory = () => {
|
||||
const validatorStub = new NodeDataValidatorStub();
|
||||
validatorStub.createContextualErrorMessage = (message) => message;
|
||||
return validatorStub;
|
||||
};
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const factoryMock: ScriptFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestContext()
|
||||
.withScriptFactory(factoryMock)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.withData(givenData)
|
||||
.parseScript();
|
||||
},
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestBuilder {
|
||||
class TestContext {
|
||||
private data: ScriptData = createScriptDataWithCode();
|
||||
|
||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||
|
||||
private parser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||
private levelParser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||
.setupDefaultValue(RecommendationLevel.Standard);
|
||||
|
||||
private factory?: ScriptFactoryType = undefined;
|
||||
private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy;
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
|
||||
public withCodeValidator(codeValidator: ICodeValidator) {
|
||||
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||
|
||||
private validatorFactory: NodeDataValidatorFactory = createNodeDataValidatorFactoryStub;
|
||||
|
||||
private docsParser: DocsParser = () => ['docs'];
|
||||
|
||||
private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
|
||||
defaultCodePrefix: TestContext.name,
|
||||
});
|
||||
|
||||
public withCodeValidator(codeValidator: ICodeValidator): this {
|
||||
this.codeValidator = codeValidator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withData(data: ScriptData) {
|
||||
public withData(data: ScriptData): this {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withContext(context: ICategoryCollectionParseContext) {
|
||||
public withContext(context: ICategoryCollectionParseContext): this {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withParser(parser: IEnumParser<RecommendationLevel>) {
|
||||
this.parser = parser;
|
||||
public withParser(parser: IEnumParser<RecommendationLevel>): this {
|
||||
this.levelParser = parser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFactory(factory: ScriptFactoryType) {
|
||||
this.factory = factory;
|
||||
public withScriptFactory(scriptFactory: ScriptFactory): this {
|
||||
this.scriptFactory = scriptFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withValidatorFactory(validatorFactory: NodeDataValidatorFactory): this {
|
||||
this.validatorFactory = validatorFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this {
|
||||
this.errorWrapper = errorWrapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
|
||||
this.scriptCodeFactory = scriptCodeFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocsParser(docsParser: DocsParser): this {
|
||||
this.docsParser = docsParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public parseScript(): Script {
|
||||
return parseScript(this.data, this.context, this.parser, this.factory, this.codeValidator);
|
||||
return parseScript(
|
||||
this.data,
|
||||
this.context,
|
||||
{
|
||||
levelParser: this.levelParser,
|
||||
createScript: this.scriptFactory,
|
||||
codeValidator: this.codeValidator,
|
||||
wrapError: this.errorWrapper,
|
||||
createValidator: this.validatorFactory,
|
||||
createCode: this.scriptCodeFactory,
|
||||
parseDocs: this.docsParser,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { describe, it, expect } from 'vitest';
|
||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||
import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub';
|
||||
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||
import type { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||
|
||||
describe('CodeValidator', () => {
|
||||
describe('instance', () => {
|
||||
itIsSingleton({
|
||||
itIsSingletonFactory({
|
||||
getter: () => CodeValidator.instance,
|
||||
expectedType: CodeValidator,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user