Refactor to enforce strictNullChecks

This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
This commit is contained in:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -9,7 +9,7 @@ import LinuxData from '@/application/collections/linux.yaml';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
import { getAbsentCollectionTestCases, getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { CategoryCollectionParserStub } from '@tests/unit/shared/Stubs/CategoryCollectionParserStub';
@@ -65,8 +65,8 @@ describe('ApplicationParser', () => {
sut.parseApplication();
// assert
expect(collectionParserStub.arguments).to.have.length.above(0);
const actualyUsedInfos = collectionParserStub.arguments.map((arg) => arg.info);
expect(actualyUsedInfos.every((info) => info === expectedInformation));
const actuallyUsedInfos = collectionParserStub.arguments.map((arg) => arg.info);
expect(actuallyUsedInfos.every((info) => info === expectedInformation));
});
});
describe('metadata', () => {
@@ -152,15 +152,15 @@ describe('ApplicationParser', () => {
describe('throws when data is invalid', () => {
// arrange
const testCases = [
...getAbsentCollectionTestCases<CollectionData>().map((testCase) => ({
...getAbsentCollectionTestCases<CollectionData>(
{
excludeUndefined: true,
excludeNull: true,
},
).map((testCase) => ({
name: `given absent collection "${testCase.valueName}"`,
value: testCase.absentValue,
expectedError: 'missing collections',
})).filter((test) => test.value !== undefined /* the default value is set */),
...getAbsentObjectTestCases().map((testCase) => ({
name: `given absent item "${testCase.valueName}"`,
value: [testCase.absentValue],
expectedError: 'missing collection provided',
})),
];
for (const testCase of testCases) {
@@ -185,9 +185,9 @@ class ApplicationParserBuilder {
private projectInformationParser
: typeof parseProjectInformation = new ProjectInformationParserStub().getStub();
private metadata: IAppMetadata = new AppMetadataStub();
private metadata: IAppMetadata | undefined = new AppMetadataStub();
private collectionsData: CollectionData[] = [new CollectionDataStub()];
private collectionsData: CollectionData[] | undefined = [new CollectionDataStub()];
public withCategoryCollectionParser(
categoryCollectionParser: CategoryCollectionParserType,
@@ -204,13 +204,13 @@ class ApplicationParserBuilder {
}
public withMetadata(
metadata: IAppMetadata,
metadata: IAppMetadata | undefined,
): this {
this.metadata = metadata;
return this;
}
public withCollectionsData(collectionsData: CollectionData[]): this {
public withCollectionsData(collectionsData: CollectionData[] | undefined): this {
this.collectionsData = collectionsData;
return this;
}

View File

@@ -10,27 +10,17 @@ import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformat
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { CategoryData } from '@/application/collections/';
describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => {
describe('throws with absent content', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing content';
const info = new ProjectInformationStub();
// act
const act = () => parseCategoryCollection(absentValue, info);
// assert
expect(act).to.throw(expectedError);
});
});
describe('actions', () => {
describe('throws with absent actions', () => {
itEachAbsentObjectValue((absentValue) => {
itEachAbsentCollectionValue<CategoryData>((absentValue) => {
// arrange
const expectedError = 'content does not define any action';
const collection = new CollectionDataStub()
@@ -40,18 +30,7 @@ describe('CategoryCollectionParser', () => {
const act = () => parseCategoryCollection(collection, info);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws when has no actions', () => {
// arrange
const expectedError = 'content does not define any action';
const collection = new CollectionDataStub()
.withActions([]);
const info = new ProjectInformationStub();
// act
const act = () => parseCategoryCollection(collection, info);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true, excludeNull: true });
});
it('parses actions', () => {
// arrange
@@ -110,17 +89,18 @@ describe('CategoryCollectionParser', () => {
const expectedCode = 'code-from-the-function';
const functionName = 'function-name';
const scriptName = 'script-name';
const script = ScriptDataStub.createWithCall()
const script = createScriptDataWithCall()
.withCall(new FunctionCallDataStub().withName(functionName).withParameters({}))
.withName(scriptName);
const func = FunctionDataStub.createWithCode().withParametersObject([])
const func = createFunctionDataWithCode()
.withParametersObject([])
.withName(functionName)
.withCode(expectedCode);
const category = new CategoryDataStub()
.withChildren([script,
ScriptDataStub.createWithCode().withName('2')
createScriptDataWithCode().withName('2')
.withRecommendationLevel(RecommendationLevel.Standard),
ScriptDataStub.createWithCode()
createScriptDataWithCode()
.withName('3').withRecommendationLevel(RecommendationLevel.Strict),
]);
const collection = new CollectionDataStub()
@@ -130,7 +110,7 @@ describe('CategoryCollectionParser', () => {
// act
const actual = parseCategoryCollection(collection, info);
// assert
const actualScript = actual.findScript(scriptName);
const actualScript = actual.getScript(scriptName);
const actualCode = actualScript.code.execute;
expect(actualCode).to.equal(expectedCode);
});

View File

@@ -4,15 +4,15 @@ import { CategoryFactoryType, parseCategory } from '@/application/Parser/Categor
import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
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, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { Category } from '@/domain/Category';
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
describe('CategoryParser', () => {
describe('parseCategory', () => {
@@ -30,7 +30,7 @@ describe('CategoryParser', () => {
});
});
describe('throws when category children is absent', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<CategoryOrScriptData>((absentValue) => {
// arrange
const categoryName = 'test';
const expectedMessage = `"${categoryName}" has no children.`;
@@ -41,7 +41,7 @@ describe('CategoryParser', () => {
const test = createTest(category);
// assert
expectThrowsNodeError(test, expectedMessage);
});
}, { excludeUndefined: true, excludeNull: true });
});
describe('throws when category child is missing', () => {
new NodeValidationTestRunner()
@@ -140,19 +140,6 @@ describe('CategoryParser', () => {
}, expectedError);
});
});
describe('throws when context is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing context';
const context = absentValue;
// act
const act = () => new TestBuilder()
.withContext(context)
.parseCategory();
// assert
expect(act).to.throw(expectedError);
});
});
it('returns expected docs', () => {
// arrange
const url = 'https://privacy.sexy';
@@ -170,7 +157,7 @@ describe('CategoryParser', () => {
describe('parses expected subscript', () => {
it('single script with code', () => {
// arrange
const script = ScriptDataStub.createWithCode();
const script = createScriptDataWithCode();
const context = new CategoryCollectionParseContextStub();
const expected = [parseScript(script, context)];
const category = new CategoryDataStub()
@@ -186,7 +173,7 @@ describe('CategoryParser', () => {
});
it('single script with function call', () => {
// arrange
const script = ScriptDataStub.createWithCall();
const script = createScriptDataWithCall();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script);
const context = new CategoryCollectionParseContextStub()
@@ -205,8 +192,8 @@ describe('CategoryParser', () => {
});
it('multiple scripts with function call and code', () => {
// arrange
const callableScript = ScriptDataStub.createWithCall();
const scripts = [callableScript, ScriptDataStub.createWithCode()];
const callableScript = createScriptDataWithCall();
const scripts = [callableScript, createScriptDataWithCode()];
const category = new CategoryDataStub()
.withChildren(scripts);
const compiler = new ScriptCompilerStub()
@@ -234,8 +221,7 @@ describe('CategoryParser', () => {
new CategoryDataStub()
.withName('sub-category')
.withChildren([
ScriptDataStub
.createWithoutCallOrCodes()
createScriptDataWithoutCallOrCodes()
.withCode(duplicatedCode),
]),
]);
@@ -253,7 +239,7 @@ describe('CategoryParser', () => {
// arrange
const expected = [new CategoryDataStub()
.withName('test category')
.withChildren([ScriptDataStub.createWithCode()]),
.withChildren([createScriptDataWithCode()]),
];
const category = new CategoryDataStub()
.withName('category name')
@@ -276,7 +262,7 @@ class TestBuilder {
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
private factory: CategoryFactoryType = undefined;
private factory?: CategoryFactoryType = undefined;
public withData(data: CategoryData) {
this.data = data;

View File

@@ -1,20 +1,10 @@
import { describe, it, expect } from 'vitest';
import type { DocumentableData } from '@/application/collections/';
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('DocumentationParser', () => {
describe('parseDocs', () => {
describe('throws when node is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing documentable';
// act
const act = () => parseDocs(absentValue);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when single documentation is missing', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
@@ -24,7 +14,7 @@ describe('DocumentationParser', () => {
const act = () => parseDocs(node);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
describe('throws when type is unexpected', () => {
// arrange

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';

View File

@@ -2,7 +2,7 @@ import { describe, it } from 'vitest';
import { NodeDataError, INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
export interface ITestScenario {
readonly act: () => void;

View File

@@ -5,43 +5,30 @@ import { CategoryCollectionParseContext } from '@/application/Parser/Script/Cate
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { FunctionData } from '@/application/collections/';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
describe('CategoryCollectionParseContext', () => {
describe('ctor', () => {
describe('functionsData', () => {
describe('can create with absent data', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
// arrange
const scripting = new ScriptingDefinitionStub();
// act
const act = () => new CategoryCollectionParseContext(absentValue, scripting);
// assert
expect(act).to.not.throw();
});
});
});
describe('scripting', () => {
describe('throws when missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing scripting';
const scripting = absentValue;
const functionsData = [FunctionDataStub.createWithCode()];
// act
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true });
});
});
});
describe('compiler', () => {
it('constructed as expected', () => {
// arrange
const functionsData = [FunctionDataStub.createWithCode()];
const functionsData = [createFunctionDataWithCode()];
const syntax = new LanguageSyntaxStub();
const expected = new ScriptCompiler(functionsData, syntax);
const language = ScriptingLanguage.shellscript;

View File

@@ -9,25 +9,13 @@ import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/Expres
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('Expression', () => {
describe('ctor', () => {
describe('position', () => {
describe('throws when missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing position';
const position = absentValue;
// act
const act = () => new ExpressionBuilder()
.withPosition(position)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
it('sets as expected', () => {
// arrange
const expected = new ExpressionPosition(0, 5);
@@ -52,7 +40,7 @@ describe('Expression', () => {
expect(actual.parameters);
expect(actual.parameters.all);
expect(actual.parameters.all.length).to.equal(0);
});
}, { excludeNull: true });
});
it('sets as expected', () => {
// arrange
@@ -67,21 +55,6 @@ describe('Expression', () => {
expect(actual.parameters).to.deep.equal(expected);
});
});
describe('evaluator', () => {
describe('throws if missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing evaluator';
const evaluator = absentValue;
// act
const act = () => new ExpressionBuilder()
.withEvaluator(evaluator)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
});
describe('evaluate', () => {
describe('throws with invalid arguments', () => {
@@ -91,11 +64,6 @@ describe('Expression', () => {
expectedError: string,
sutBuilder?: (builder: ExpressionBuilder) => ExpressionBuilder,
}[] = [
...getAbsentObjectTestCases().map((testCase) => ({
name: `throws if arguments is ${testCase.valueName}`,
context: testCase.absentValue,
expectedError: 'missing context',
})),
{
name: 'throws when some of the required args are not provided',
sutBuilder: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false),
@@ -159,7 +127,7 @@ describe('Expression', () => {
const expected = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(expected);
let actual: IPipelineCompiler;
let actual: IPipelineCompiler | undefined;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.pipelineCompiler;
return '';
@@ -170,6 +138,7 @@ describe('Expression', () => {
// arrange
sut.evaluate(context);
// assert
expectExists(actual);
expect(expected).to.equal(actual);
});
describe('filters unused parameters', () => {
@@ -202,7 +171,7 @@ describe('Expression', () => {
];
for (const testCase of testCases) {
it(testCase.name, () => {
let actual: IReadOnlyFunctionCallArgumentCollection;
let actual: IReadOnlyFunctionCallArgumentCollection | undefined;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.args;
return '';
@@ -216,8 +185,10 @@ describe('Expression', () => {
// act
sut.evaluate(context);
// assert
const actualArguments = actual.getAllParameterNames()
.map((name) => actual.getArgument(name));
expectExists(actual);
const collection = actual;
const actualArguments = collection.getAllParameterNames()
.map((name) => collection.getArgument(name));
expect(actualArguments).to.deep.equal(testCase.expectedArguments);
});
}
@@ -228,7 +199,7 @@ describe('Expression', () => {
class ExpressionBuilder {
private position: ExpressionPosition = new ExpressionPosition(0, 5);
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private parameters?: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
public withPosition(position: ExpressionPosition) {
this.position = position;
@@ -240,7 +211,7 @@ class ExpressionBuilder {
return this;
}
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
public withParameters(parameters: IReadOnlyFunctionParameterCollection | undefined) {
this.parameters = parameters;
return this;
}
@@ -261,5 +232,5 @@ class ExpressionBuilder {
return new Expression(this.position, this.evaluator, this.parameters);
}
private evaluator: ExpressionEvaluator = () => '';
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
}

View File

@@ -4,24 +4,10 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('ExpressionEvaluationContext', () => {
describe('ctor', () => {
describe('args', () => {
describe('throws if args is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing args, send empty collection instead.';
const args = absentValue;
// act
const act = () => new ExpressionEvaluationContextBuilder()
.withArgs(args)
.build();
// assert
expect(act).throw(expectedError);
});
});
it('sets as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()

View File

@@ -0,0 +1,102 @@
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';
describe('ExpressionPositionFactory', () => {
describe('createPositionFromRegexFullMatch', () => {
it(`creates ${ExpressionPosition.name} instance`, () => {
// arrange
const expectedType = ExpressionPosition;
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
matchIndex: 5,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position).to.be.instanceOf(expectedType);
});
it('creates a position with the correct start position', () => {
// arrange
const expectedStartPosition = 5;
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
matchIndex: expectedStartPosition,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position.start).toBe(expectedStartPosition);
});
it('creates a position with the correct end position', () => {
// arrange
const startPosition = 3;
const matchedString = 'matched string';
const expectedEndPosition = startPosition + matchedString.length;
const fakeMatch = createRegexMatch({
fullMatch: matchedString,
matchIndex: startPosition,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position.end).to.equal(expectedEndPosition);
});
it('creates correct position with capturing groups', () => {
// arrange
const startPosition = 20;
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
capturingGroups: ['group1', 'group2'],
matchIndex: startPosition,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position.start).toBe(startPosition);
expect(position.end).toBe(startPosition + fakeMatch[0].length);
});
describe('invalid values', () => {
it('throws an error if match.index is undefined', () => {
// arrange
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
matchIndex: undefined,
});
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
// act
const act = () => createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(act).to.throw(expectedError);
});
it('throws an error for empty match', () => {
// arrange
const fakeMatch = createRegexMatch({
fullMatch: '',
matchIndex: 0,
});
const expectedError = `Regex match is empty: ${JSON.stringify(fakeMatch)}`;
// act
const act = () => createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
function createRegexMatch(options?: {
readonly fullMatch?: string,
readonly capturingGroups?: readonly string[],
readonly matchIndex?: number,
}): RegExpMatchArray {
const fullMatch = options?.fullMatch ?? 'fake match';
const capturingGroups = options?.capturingGroups ?? [];
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
fakeMatch.index = options?.matchIndex;
return fakeMatch;
}

View File

@@ -4,22 +4,23 @@ import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressi
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
import { ExpressionParserStub } from '@tests/unit/shared/Stubs/ExpressionParserStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
describe('ExpressionsCompiler', () => {
describe('compileExpressions', () => {
describe('returns code when code is absent', () => {
describe('returns empty string when code is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expected = absentValue;
const expected = '';
const code = absentValue;
const sut = new SystemUnderTest();
const args = new FunctionCallArgumentCollectionStub();
// act
const value = sut.compileExpressions(absentValue, args);
const value = sut.compileExpressions(code, args);
// assert
expect(value).to.equal(expected);
});
}, { excludeNull: true, excludeUndefined: true });
});
describe('can compile nested expressions', () => {
it('when one expression is evaluated to a text that contains another expression', () => {
@@ -189,19 +190,6 @@ describe('ExpressionsCompiler', () => {
+ `Not equal: ${actualArgs.filter((arg) => arg !== expected)}`,
);
});
describe('throws if arguments is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing args, send empty collection instead.';
const args = absentValue;
const expressionParserMock = new ExpressionParserStub();
const sut = new SystemUnderTest(expressionParserMock);
// act
const act = () => sut.compileExpressions('code', args);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('throws when expressions are invalid', () => {
describe('throws when expected argument is not provided but used in code', () => {

View File

@@ -3,29 +3,20 @@ import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Ex
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('CompositeExpressionParser', () => {
describe('ctor', () => {
it('throws if null parsers given', () => {
// arrange
const expectedError = 'missing leafs';
const parsers = null;
// act
const act = () => new CompositeExpressionParser(parsers);
// assert
expect(act).to.throw(expectedError);
});
describe('throws if one of the parsers is undefined', () => {
itEachAbsentObjectValue((absentValue) => {
describe('throws when parsers are missing', () => {
itEachAbsentCollectionValue<IExpressionParser>((absentCollection) => {
// arrange
const expectedError = 'missing leaf';
const parsers: readonly IExpressionParser[] = [absentValue, mockParser()];
const expectedError = 'missing leafs';
const parsers = absentCollection;
// act
const act = () => new CompositeExpressionParser(parsers);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
});
describe('findExpressions', () => {

View File

@@ -4,6 +4,7 @@ import { IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/C
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';
describe('RegexParser', () => {
describe('findExpressions', () => {
@@ -16,7 +17,7 @@ describe('RegexParser', () => {
const act = () => sut.findExpressions(absentValue);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
it('throws when position is invalid', () => {
// arrange
@@ -30,17 +31,19 @@ describe('RegexParser', () => {
];
const sut = new RegexParserConcrete(regexMatchingEmpty);
// act
let error: string;
let errorMessage: string | undefined;
try {
sut.findExpressions(code);
} catch (err) {
error = err.message;
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: ${error}`,
+ `Actual error: ${errorMessage}`,
);
});
describe('matches regex as expected', () => {
@@ -139,7 +142,7 @@ function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
});
}
function getEvaluatorStub(): ExpressionEvaluator {
return () => undefined;
return () => `[${getEvaluatorStub.name}] evaluated code`;
}
function mockPrimitiveExpression(): IPrimitiveExpression {

View File

@@ -8,6 +8,12 @@ describe('EscapeDoubleQuotes', () => {
const sut = new EscapeDoubleQuotes();
// act
runPipeTests(sut, [
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: 'returns as it is when if input is missing',
input: testCase.absentValue,
expectedOutput: testCase.absentValue,
})),
{
name: 'using "',
input: 'hello "world"',
@@ -23,10 +29,5 @@ describe('EscapeDoubleQuotes', () => {
input: '""hello world""',
expectedOutput: '"^"""^""hello world"^"""^""',
},
...getAbsentStringTestCases().map((testCase) => ({
name: 'returns as it is when if input is missing',
input: testCase.absentValue,
expectedOutput: testCase.absentValue,
})),
]);
});

View File

@@ -1,5 +1,6 @@
import { describe } from 'vitest';
import { InlinePowerShell } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { IPipeTestCase, runPipeTests } from './PipeTestRunner';
describe('InlinePowerShell', () => {
@@ -7,11 +8,12 @@ describe('InlinePowerShell', () => {
const sut = new InlinePowerShell();
// act
runPipeTests(sut, [
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: 'returns as it is when if input is missing',
input: testCase.absentValue,
expectedOutput: '',
})),
...prefixTests('newline', getNewLineCases()),
...prefixTests('comment', getCommentCases()),
...prefixTests('here-string', hereStringCases()),

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { PipeFactory } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory';
import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub';
import { getAbsentStringTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
describe('PipeFactory', () => {
describe('ctor', () => {
@@ -19,26 +19,6 @@ describe('PipeFactory', () => {
// expect
expect(act).to.throw(expectedError);
});
describe('throws when a pipe is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing pipe in list';
const pipes = [new PipeStub(), absentValue];
// act
const act = () => new PipeFactory(pipes);
// expect
expect(act).to.throw(expectedError);
});
});
it('throws when pipes are null', () => {
// arrange
const expectedError = 'missing pipes';
const pipes = null;
// act
const act = () => new PipeFactory(pipes);
// expect
expect(act).to.throw(expectedError);
});
describe('throws when name is invalid', () => {
// act
const act = (invalidName: string) => new PipeFactory([new PipeStub().withName(invalidName)]);
@@ -82,11 +62,12 @@ describe('PipeFactory', () => {
function testPipeNameValidation(testRunner: (invalidName: string) => void) {
const testCases = [
// Validate missing value
...getAbsentStringTestCases().map((testCase) => ({
name: `empty pipe name (${testCase.valueName})`,
value: testCase.absentValue,
expectedError: 'empty pipe name',
})),
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: `empty pipe name (${testCase.valueName})`,
value: testCase.absentValue,
expectedError: 'empty pipe name',
})),
// Validate camelCase
...[
'PascalCase',

View File

@@ -10,21 +10,23 @@ describe('PipelineCompiler', () => {
describe('compile', () => {
describe('throws for invalid arguments', () => {
interface ITestCase {
name: string;
act: (test: PipelineTestRunner) => PipelineTestRunner;
expectedError: string;
readonly name: string;
readonly act: (test: PipelineTestRunner) => PipelineTestRunner;
readonly expectedError: string;
}
const testCases: ITestCase[] = [
...getAbsentStringTestCases().map((testCase) => ({
name: `"value" is ${testCase.valueName}`,
act: (test) => test.withValue(testCase.absentValue),
expectedError: 'missing value',
})),
...getAbsentStringTestCases().map((testCase) => ({
name: `"pipeline" is ${testCase.valueName}`,
act: (test) => test.withPipeline(testCase.absentValue),
expectedError: 'missing pipeline',
})),
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: `"value" is ${testCase.valueName}`,
act: (test) => test.withValue(testCase.absentValue),
expectedError: 'missing value',
})),
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: `"pipeline" is ${testCase.valueName}`,
act: (test) => test.withPipeline(testCase.absentValue),
expectedError: 'missing pipeline',
})),
{
name: '"pipeline" does not start with pipe',
act: (test) => test.withPipeline('pipeline |'),

View File

@@ -119,13 +119,14 @@ describe('WithParser', () => {
describe('conditional rendering based on argument value', () => {
describe('does not render scope', () => {
runner.expectResults(
...getAbsentStringTestCases().map((testCase) => ({
name: `does not render when value is "${testCase.valueName}"`,
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
.withArgument('parameter', testCase.absentValue),
expected: [''],
})),
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: `does not render when value is "${testCase.valueName}"`,
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
.withArgument('parameter', testCase.absentValue),
expected: [''],
})),
{
name: 'does not render when argument is not provided',
code: '{{ with $parameter }}dark{{ end }}',
@@ -136,13 +137,14 @@ describe('WithParser', () => {
});
describe('renders scope', () => {
runner.expectResults(
...getAbsentStringTestCases().map((testCase) => ({
name: `does not render when value is "${testCase.valueName}"`,
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
.withArgument('parameter', testCase.absentValue),
expected: [''],
})),
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: `does not render when value is "${testCase.valueName}"`,
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
.withArgument('parameter', testCase.absentValue),
expected: [''],
})),
{
name: 'does not render when argument is not provided',
code: '{{ with $parameter }}dark{{ end }}',

View File

@@ -26,7 +26,7 @@ describe('FunctionCallArgument', () => {
.build();
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
});
});

View File

@@ -1,22 +1,10 @@
import { describe, it, expect } from 'vitest';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FunctionCallArgumentCollection', () => {
describe('addArgument', () => {
describe('throws if argument is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const errorMessage = 'missing argument';
const arg = absentValue;
const sut = new FunctionCallArgumentCollection();
// act
const act = () => sut.addArgument(arg);
// assert
expect(act).to.throw(errorMessage);
});
});
it('throws if parameter value is already provided', () => {
// arrange
const duplicateParameterName = 'duplicateParam';
@@ -70,7 +58,7 @@ describe('FunctionCallArgumentCollection', () => {
const act = () => sut.getArgument(parameterName);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
it('throws if argument does not exist', () => {
// arrange
@@ -106,7 +94,7 @@ describe('FunctionCallArgumentCollection', () => {
const act = () => sut.hasArgument(parameterName);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
describe('returns as expected', () => {
// arrange

View File

@@ -2,11 +2,12 @@ import { expect, describe, it } from 'vitest';
import { NewlineCodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
describe('NewlineCodeSegmentMerger', () => {
describe('mergeCodeParts', () => {
describe('throws given empty segments', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<CompiledCode>((absentValue) => {
// arrange
const expectedError = 'missing segments';
const segments = absentValue;
@@ -15,7 +16,7 @@ describe('NewlineCodeSegmentMerger', () => {
const act = () => merger.mergeCodeParts(segments);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
describe('merges correctly', () => {
const testCases: ReadonlyArray<{
@@ -38,29 +39,31 @@ describe('NewlineCodeSegmentMerger', () => {
revertCode: 'revert1\nrevert2\nrevert3',
},
},
...getAbsentStringTestCases().map((absentTestCase) => ({
description: `filter out ${absentTestCase.valueName} \`revertCode\``,
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue),
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
],
expected: {
code: 'code1\ncode2\ncode3',
revertCode: 'revert1\nrevert3',
},
})),
{
description: 'given only `code` in segments',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(''),
new CompiledCodeStub().withCode('code2').withRevertCode(''),
],
expected: {
code: 'code1\ncode2',
revertCode: '',
},
},
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((absentTestCase) => ({
description: `filter out ${absentTestCase.valueName} \`revertCode\``,
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue),
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
],
expected: {
code: 'code1\ncode2\ncode3',
revertCode: 'revert1\nrevert3',
},
})),
...getAbsentStringTestCases({ excludeNull: true })
.map((emptyRevertCode) => ({
description: `given only \`code\` in segments with "${emptyRevertCode.valueName}" \`revertCode\``,
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(emptyRevertCode.absentValue),
new CompiledCodeStub().withCode('code2').withRevertCode(emptyRevertCode.absentValue),
],
expected: {
code: 'code1\ncode2',
revertCode: '',
},
})),
{
description: 'given mix of segments with only `code` or `revertCode`',
segments: [

View File

@@ -2,7 +2,7 @@
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 { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
@@ -13,6 +13,7 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
import { CodeSegmentMergerStub } from '@tests/unit/shared/Stubs/CodeSegmentMergerStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('FunctionCallSequenceCompiler', () => {
describe('instance', () => {
@@ -25,7 +26,7 @@ describe('FunctionCallSequenceCompiler', () => {
describe('parameter validation', () => {
describe('calls', () => {
describe('throws with missing call', () => {
itEachAbsentObjectValue((absentValue) => {
itEachAbsentCollectionValue<FunctionCall>((absentValue) => {
// arrange
const expectedError = 'missing calls';
const calls = absentValue;
@@ -35,38 +36,7 @@ describe('FunctionCallSequenceCompiler', () => {
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has absent call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing function call';
const calls = [
new FunctionCallStub(),
absentValue,
];
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('functions', () => {
describe('throws with missing functions', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing functions';
const functions = absentValue;
const builder = new FunctionCallSequenceCompilerBuilder()
.withFunctions(functions);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
});
});
@@ -84,7 +54,7 @@ describe('FunctionCallSequenceCompiler', () => {
// assert
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
expect(calledMethod).toBeDefined();
expectExists(calledMethod);
expect(calledMethod.args[0]).to.equal(expectedCall);
});
it('with every call', () => {
@@ -118,7 +88,7 @@ describe('FunctionCallSequenceCompiler', () => {
// assert
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
expect(calledMethod).toBeDefined();
expectExists(calledMethod);
const actualFunctions = calledMethod.args[1].allFunctions;
expect(actualFunctions).to.equal(expectedFunctions);
});
@@ -173,7 +143,9 @@ describe('FunctionCallSequenceCompiler', () => {
// act
builder.compileFunctionCalls();
// assert
const [actualSegments] = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts').args;
const calledMethod = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts');
expectExists(calledMethod);
const [actualSegments] = calledMethod.args;
expect(expectedFlattenedSegments).to.have.lengthOf(actualSegments.length);
expect(expectedFlattenedSegments).to.have.deep.members(actualSegments);
});

View File

@@ -1,6 +1,5 @@
import { expect, describe, it } from 'vitest';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { createSharedFunctionStubWithCalls, createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { NestedFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { ArgumentCompilerStub } from '@tests/unit/shared/Stubs/ArgumentCompilerStub';
@@ -10,14 +9,14 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
describe('NestedFunctionCallCompiler', () => {
describe('canCompile', () => {
it('returns `true` for code body function', () => {
// arrange
const expected = true;
const func = new SharedFunctionStub(FunctionBodyType.Calls)
const func = createSharedFunctionStubWithCalls()
.withSomeCalls();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
@@ -29,7 +28,7 @@ describe('NestedFunctionCallCompiler', () => {
it('returns `false` for non-code body function', () => {
// arrange
const expected = false;
const func = new SharedFunctionStub(FunctionBodyType.Code);
const func = createSharedFunctionStubWithCode();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
@@ -129,8 +128,8 @@ describe('NestedFunctionCallCompiler', () => {
});
it('flattens re-compiled functions', () => {
// arrange
const deepFunc1 = new SharedFunctionStub(FunctionBodyType.Code);
const deepFunc2 = new SharedFunctionStub(FunctionBodyType.Code);
const deepFunc1 = createSharedFunctionStubWithCode();
const deepFunc2 = createSharedFunctionStubWithCalls();
const callToDeepFunc1 = new FunctionCallStub().withFunctionName(deepFunc1.name);
const callToDeepFunc2 = new FunctionCallStub().withFunctionName(deepFunc2.name);
const singleCallCompilationScenario = new Map<FunctionCall, CompiledCode[]>([
@@ -141,7 +140,7 @@ describe('NestedFunctionCallCompiler', () => {
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls)
const frontFunc = createSharedFunctionStubWithCalls()
.withCalls(callToDeepFunc1, callToDeepFunc2);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
const singleCallCompilerStub = new SingleCallCompilerStub()
@@ -212,9 +211,9 @@ describe('NestedFunctionCallCompiler', () => {
});
function createSingleFuncCallingAnotherFunc() {
const deepFunc = new SharedFunctionStub(FunctionBodyType.Code);
const deepFunc = createSharedFunctionStubWithCode();
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name);
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls).withCalls(callToDeepFunc);
const frontFunc = createSharedFunctionStubWithCalls().withCalls(callToDeepFunc);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
return {
deepFunc,

View File

@@ -1,6 +1,5 @@
import { expect, describe, it } from 'vitest';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import type { FunctionCallParametersData } from '@/application/collections/';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
@@ -70,7 +69,7 @@ describe('AdaptiveFunctionCallCompiler', () => {
}) => {
it(description, () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code)
const func = createSharedFunctionStubWithCode()
.withName('test-function-name')
.withParameterNames(...functionParameters);
const params = callParameters
@@ -137,7 +136,7 @@ describe('AdaptiveFunctionCallCompiler', () => {
describe('strategy invocation', () => {
it('passes correct function for compilation ability check', () => {
// arrange
const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code);
const expectedFunction = createSharedFunctionStubWithCode();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
@@ -157,7 +156,7 @@ describe('AdaptiveFunctionCallCompiler', () => {
describe('compilation arguments', () => {
it('uses correct function', () => {
// arrange
const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code);
const expectedFunction = createSharedFunctionStubWithCode();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()

View File

@@ -7,12 +7,11 @@ import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expre
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/unit/shared/Assertions/ExpectDeepThrowsError';
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
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 { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
describe('NestedFunctionArgumentCompiler', () => {
describe('createCompiledNestedCall', () => {
@@ -132,7 +131,7 @@ describe('NestedFunctionArgumentCompiler', () => {
givenArgs: parentCall.args,
result: testParameterScenarios.find(
(r) => r.rawArgumentValue === rawArgumentValue,
).compiledArgumentValue,
)?.compiledArgumentValue ?? 'unexpected arguments',
});
});
const nestedCallArgs = new FunctionCallArgumentCollectionStub()
@@ -166,7 +165,7 @@ describe('NestedFunctionArgumentCompiler', () => {
// arrange
const parameterName = 'requiredParameter';
const initialValue = 'initial-value';
const compiledValue = undefined;
const emptyCompiledExpression = '';
const expectedError = `Compilation resulted in empty value for required parameter: "${parameterName}"`;
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
@@ -183,7 +182,7 @@ describe('NestedFunctionArgumentCompiler', () => {
.setup({
givenCode: initialValue,
givenArgs: parentCall.args,
result: compiledValue,
result: emptyCompiledExpression,
});
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
@@ -199,7 +198,7 @@ describe('NestedFunctionArgumentCompiler', () => {
// arrange
const parameterName = 'optionalParameter';
const initialValue = 'initial-value';
const compiledValue = undefined;
const emptyValue = '';
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, initialValue));
@@ -215,7 +214,7 @@ describe('NestedFunctionArgumentCompiler', () => {
.setup({
givenCode: initialValue,
givenArgs: parentCall.args,
result: compiledValue,
result: emptyValue,
});
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
@@ -240,7 +239,7 @@ function createContextWithParameter(options: {
}): FunctionCallCompilationContext {
const parameters = new FunctionParameterCollectionStub()
.withParameterName(options.existingParameterName, options.isExistingParameterOptional);
const func = new SharedFunctionStub(FunctionBodyType.Code)
const func = createSharedFunctionStubWithCode()
.withName(options.existingFunctionName)
.withParameters(parameters);
const functions = new SharedFunctionCollectionStub()

View File

@@ -1,7 +1,6 @@
import { expect, describe, it } from 'vitest';
import { InlineFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
@@ -12,7 +11,7 @@ describe('InlineFunctionCallCompiler', () => {
it('returns `true` if function has code body', () => {
// arrange
const expected = true;
const func = new SharedFunctionStub(FunctionBodyType.Code);
const func = createSharedFunctionStubWithCode();
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
@@ -23,7 +22,7 @@ describe('InlineFunctionCallCompiler', () => {
it('returns `false` if function does not have code body', () => {
// arrange
const expected = false;
const func = new SharedFunctionStub(FunctionBodyType.Calls);
const func = createSharedFunctionStubWithCalls();
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
@@ -33,6 +32,19 @@ describe('InlineFunctionCallCompiler', () => {
});
});
describe('compile', () => {
it('throws if function body is not code', () => {
// arrange
const expectedError = 'Unexpected function body type.';
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const act = () => compiler.compileFunction(
createSharedFunctionStubWithCalls(),
new FunctionCallStub(),
);
// assert
expect(act).to.throw(expectedError);
});
it('compiles expressions with correct arguments', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
@@ -42,7 +54,7 @@ describe('InlineFunctionCallCompiler', () => {
.build();
// act
compiler.compileFunction(
new SharedFunctionStub(FunctionBodyType.Code),
createSharedFunctionStubWithCode(),
new FunctionCallStub()
.withArgumentCollection(expectedArgs),
);
@@ -50,49 +62,71 @@ describe('InlineFunctionCallCompiler', () => {
const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]);
expect(actualArgs.every((arg) => arg === expectedArgs));
});
it('creates compiled code with compiled `execute`', () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code);
const args = new FunctionCallArgumentCollectionStub();
const expectedCode = 'expected-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.execute,
givenArgs: args,
result: expectedCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualCode = compiledCodes[0].code;
expect(actualCode).to.equal(expectedCode);
describe('execute', () => {
it('creates compiled code with compiled `execute`', () => {
// arrange
const func = createSharedFunctionStubWithCode();
const args = new FunctionCallArgumentCollectionStub();
const expectedCode = 'expected-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.execute,
givenArgs: args,
result: expectedCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualCode = compiledCodes[0].code;
expect(actualCode).to.equal(expectedCode);
});
});
it('creates compiled revert code with compiled `revert`', () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code);
const args = new FunctionCallArgumentCollectionStub();
const expectedRevertCode = 'expected-revert-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.revert,
givenArgs: args,
result: expectedRevertCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualRevertCode = compiledCodes[0].revertCode;
expect(actualRevertCode).to.equal(expectedRevertCode);
describe('revert', () => {
it('compiles to `undefined` when given `undefined`', () => {
// arrange
const expected = undefined;
const revertCode = undefined;
const func = createSharedFunctionStubWithCode()
.withRevertCode(revertCode);
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub());
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualRevertCode = compiledCodes[0].revertCode;
expect(actualRevertCode).to.equal(expected);
});
it('creates compiled revert code with compiled `revert`', () => {
// arrange
const revertCode = 'revert-code-input';
const func = createSharedFunctionStubWithCode()
.withRevertCode(revertCode);
const args = new FunctionCallArgumentCollectionStub();
const expectedRevertCode = 'expected-revert-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: revertCode,
givenArgs: args,
result: expectedRevertCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualRevertCode = compiledCodes[0].revertCode;
expect(actualRevertCode).to.equal(expectedRevertCode);
});
});
});
});

View File

@@ -1,21 +1,10 @@
import { describe, it, expect } from 'vitest';
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FunctionCallParser', () => {
describe('parseFunctionCalls', () => {
describe('throws with missing call data', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing call data';
const call = absentValue;
// act
const act = () => parseFunctionCalls(call);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if call is not an object', () => {
// arrange
const expectedError = 'called function(s) must be an object';
@@ -27,20 +16,6 @@ describe('FunctionCallParser', () => {
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has undefined call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing call data';
const data = [
new FunctionCallDataStub(),
absentValue,
];
// act
const act = () => parseFunctionCalls(data);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has undefined function name', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
@@ -53,7 +28,7 @@ describe('FunctionCallParser', () => {
const act = () => parseFunctionCalls(data);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
it('parses single call as expected', () => {
// arrange

View File

@@ -2,24 +2,11 @@ import { describe, it, expect } from 'vitest';
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('ParsedFunctionCall', () => {
describe('ctor', () => {
describe('args', () => {
describe('throws when args is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing args';
const args = absentValue;
// act
const act = () => new FunctionCallBuilder()
.withArgs(args)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
it('sets args as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
@@ -44,7 +31,7 @@ describe('ParsedFunctionCall', () => {
.build();
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
it('sets function name as expected', () => {
// arrange

View File

@@ -0,0 +1,29 @@
import {
CallFunctionBody, CodeFunctionBody, FunctionBodyType, SharedFunctionBody,
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
export function expectCodeFunctionBody(
body: SharedFunctionBody,
): asserts body is CodeFunctionBody {
expectBodyType(body, FunctionBodyType.Code);
}
export function expectCallsFunctionBody(
body: SharedFunctionBody,
): asserts body is CallFunctionBody {
expectBodyType(body, FunctionBodyType.Calls);
}
function expectBodyType(body: SharedFunctionBody, expectedType: FunctionBodyType) {
const actualType = body.type;
expect(actualType).to.equal(
expectedType,
[
'\n---',
`Actual: ${FunctionBodyType[actualType]}`,
`Expected: ${FunctionBodyType[expectedType]}`,
`Body: ${JSON.stringify(body)}`,
'---\n\n',
].join('\n'),
);
}

View File

@@ -1,7 +1,6 @@
import { describe, it, expect } from 'vitest';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FunctionParameterCollection', () => {
it('all returns added parameters as expected', () => {
@@ -33,18 +32,4 @@ describe('FunctionParameterCollection', () => {
// assert
expect(act).to.throw(expectedError);
});
describe('addParameter', () => {
describe('throws if parameter is undefined', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing parameter';
const value = absentValue;
const sut = new FunctionParameterCollection();
// act
const act = () => sut.addParameter(value);
// assert
expect(act).to.throw(expectedError);
});
});
});
});

View File

@@ -4,11 +4,12 @@ import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/Functi
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import {
getAbsentStringTestCases, itEachAbsentCollectionValue, itEachAbsentObjectValue,
getAbsentStringTestCases, itEachAbsentCollectionValue,
itEachAbsentStringValue,
} from '@tests/unit/shared/TestCases/AbsentTests';
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
describe('SharedFunction', () => {
describe('SharedFunction', () => {
@@ -34,7 +35,7 @@ describe('SharedFunction', () => {
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
});
});
@@ -51,19 +52,6 @@ describe('SharedFunction', () => {
// assert
expect(sut.parameters).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing parameters';
const parameters = absentValue;
const builder = new SharedFunctionBuilder()
.withParameters(parameters);
// act
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
});
@@ -77,6 +65,7 @@ describe('SharedFunction', () => {
.withCode(expected)
.createFunctionWithInlineCode();
// assert
expectCodeFunctionBody(sut.body);
expect(sut.body.code.execute).equal(expected);
});
describe('throws if absent', () => {
@@ -92,23 +81,26 @@ describe('SharedFunction', () => {
.createFunctionWithInlineCode();
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
});
describe('revertCode', () => {
it('sets as expected', () => {
// arrange
const testData = [
const revertCodeTestValues: readonly (string | undefined)[] = [
'expected-revert-code',
...getAbsentStringTestCases().map((testCase) => testCase.absentValue),
...getAbsentStringTestCases({
excludeNull: true,
}).map((testCase) => testCase.absentValue),
];
for (const data of testData) {
for (const revertCode of revertCodeTestValues) {
// act
const sut = new SharedFunctionBuilder()
.withRevertCode(data)
.withRevertCode(revertCode)
.createFunctionWithInlineCode();
// assert
expect(sut.body.code.revert).equal(data);
expectCodeFunctionBody(sut.body);
expect(sut.body.code.revert).equal(revertCode);
}
});
});
@@ -128,7 +120,7 @@ describe('SharedFunction', () => {
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.calls).equal(expectedCalls);
expect((sut.body as CallFunctionBody).calls).equal(expectedCalls);
});
});
describe('createCallerFunction', () => {
@@ -144,10 +136,11 @@ describe('SharedFunction', () => {
.withRootCallSequence(expected)
.createCallerFunction();
// assert
expectCallsFunctionBody(sut.body);
expect(sut.body.calls).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<FunctionCall>((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const rootCallSequence = absentValue;
@@ -159,7 +152,7 @@ describe('SharedFunction', () => {
.createCallerFunction();
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
});
it('sets type as expected', () => {
@@ -171,15 +164,6 @@ describe('SharedFunction', () => {
// assert
expect(sut.body.type).equal(expectedType);
});
it('code is undefined', () => {
// arrange
const expectedCode = undefined;
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
// assert
expect(sut.body.code).equal(expectedCode);
});
});
});
@@ -208,9 +192,9 @@ class SharedFunctionBuilder {
private callSequence: readonly FunctionCall[] = [new FunctionCallStub()];
private code = 'code';
private code = `[${SharedFunctionBuilder.name}] code`;
private revertCode = 'revert-code';
private revertCode: string | undefined = `[${SharedFunctionBuilder.name}] revert-code`;
public createCallerFunction(): ISharedFunction {
return createCallerFunction(
@@ -244,7 +228,7 @@ class SharedFunctionBuilder {
return this;
}
public withRevertCode(revertCode: string) {
public withRevertCode(revertCode: string | undefined) {
this.revertCode = revertCode;
return this;
}

View File

@@ -1,29 +1,16 @@
import { describe, it, expect } from 'vitest';
import { SharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/SharedFunctionCollection';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('SharedFunctionCollection', () => {
describe('addFunction', () => {
describe('throws if function is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing function';
const func = absentValue;
const sut = new SharedFunctionCollection();
// act
const act = () => sut.addFunction(func);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if function with same name already exists', () => {
// arrange
const functionName = 'duplicate-function';
const expectedError = `function with name ${functionName} already exists`;
const func = new SharedFunctionStub(FunctionBodyType.Code)
const func = createSharedFunctionStubWithCode()
.withName('duplicate-function');
const sut = new SharedFunctionCollection();
sut.addFunction(func);
@@ -43,13 +30,13 @@ describe('SharedFunctionCollection', () => {
const act = () => sut.getFunctionByName(absentValue);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeNull: true, excludeUndefined: true });
});
it('throws if function does not exist', () => {
// arrange
const name = 'unique-name';
const expectedError = `called function is not defined "${name}"`;
const func = new SharedFunctionStub(FunctionBodyType.Code)
const func = createSharedFunctionStubWithCode()
.withName('unexpected-name');
const sut = new SharedFunctionCollection();
sut.addFunction(func);
@@ -61,7 +48,7 @@ describe('SharedFunctionCollection', () => {
describe('returns existing function', () => {
it('when function with inline code is added', () => {
// arrange
const expected = new SharedFunctionStub(FunctionBodyType.Code)
const expected = createSharedFunctionStubWithCode()
.withName('expected-function-name');
const sut = new SharedFunctionCollection();
// act
@@ -72,9 +59,9 @@ describe('SharedFunctionCollection', () => {
});
it('when calling function is added', () => {
// arrange
const callee = new SharedFunctionStub(FunctionBodyType.Code)
const callee = createSharedFunctionStubWithCode()
.withName('calleeFunction');
const caller = new SharedFunctionStub(FunctionBodyType.Calls)
const caller = createSharedFunctionStubWithCalls()
.withName('callerFunction')
.withCalls(new FunctionCallStub().withFunctionName(callee.name));
const sut = new SharedFunctionCollection();

View File

@@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest';
import type { FunctionData } from '@/application/collections/';
import type { FunctionData, CodeInstruction } from '@/application/collections/';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
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, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
@@ -14,6 +14,8 @@ import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import { 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 { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
describe('SharedFunctionsParser', () => {
describe('instance', () => {
@@ -23,41 +25,14 @@ describe('SharedFunctionsParser', () => {
});
});
describe('parseFunctions', () => {
describe('throws if syntax is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing syntax';
const syntax = absentValue;
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withSyntax(syntax)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
describe('validates functions', () => {
describe('throws if one of the functions is undefined', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'some functions are undefined';
const functions = [FunctionDataStub.createWithCode(), absentValue];
const sut = new ParseFunctionsCallerWithDefaults();
// act
const act = () => sut
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
it('throws when functions have same names', () => {
// arrange
const name = 'same-func-name';
const expectedError = `duplicate function name: "${name}"`;
const functions = [
FunctionDataStub.createWithCode().withName(name),
FunctionDataStub.createWithCode().withName(name),
createFunctionDataWithCode().withName(name),
createFunctionDataWithCode().withName(name),
];
// act
const act = () => new ParseFunctionsCallerWithDefaults()
@@ -72,8 +47,8 @@ describe('SharedFunctionsParser', () => {
const code = 'duplicate-code';
const expectedError = `duplicate "code" in functions: "${code}"`;
const functions = [
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
createFunctionDataWithoutCallOrCode().withName('func-1').withCode(code),
createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code),
];
// act
const act = () => new ParseFunctionsCallerWithDefaults()
@@ -87,9 +62,9 @@ describe('SharedFunctionsParser', () => {
const revertCode = 'duplicate-revert-code';
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
const functions = [
FunctionDataStub.createWithoutCallOrCodes()
createFunctionDataWithoutCallOrCode()
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
FunctionDataStub.createWithoutCallOrCodes()
createFunctionDataWithoutCallOrCode()
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
];
// act
@@ -105,7 +80,7 @@ describe('SharedFunctionsParser', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
const invalidFunction = createFunctionDataWithoutCallOrCode()
.withName(functionName)
.withCode('code')
.withMockCall();
@@ -120,7 +95,7 @@ describe('SharedFunctionsParser', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
const invalidFunction = createFunctionDataWithoutCallOrCode()
.withName(functionName);
// act
const act = () => new ParseFunctionsCallerWithDefaults()
@@ -144,8 +119,7 @@ describe('SharedFunctionsParser', () => {
for (const testCase of testCases) {
it(testCase.state, () => {
// arrange
const func = FunctionDataStub
.createWithCall()
const func = createFunctionDataWithCode()
.withParametersObject(testCase.invalidType as never);
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
// act
@@ -160,8 +134,7 @@ describe('SharedFunctionsParser', () => {
it('validates function code as expected when code is defined', () => {
// arrange
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
const functionData = FunctionDataStub
.createWithCode()
const functionData = createFunctionDataWithCode()
.withCode('expected code to be validated')
.withRevertCode('expected revert code to be validated');
const validator = new CodeValidatorStub();
@@ -180,13 +153,11 @@ describe('SharedFunctionsParser', () => {
// arrange
const invalidParameterName = 'invalid function p@r4meter name';
const functionName = 'functionName';
let parameterException: Error;
try {
// eslint-disable-next-line no-new
new FunctionParameter(invalidParameterName, false);
} catch (e) { parameterException = e; }
const expectedError = `"${functionName}": ${parameterException.message}`;
const functionData = FunctionDataStub.createWithCode()
const message = collectExceptionMessage(
() => new FunctionParameter(invalidParameterName, false),
);
const expectedError = `"${functionName}": ${message}`;
const functionData = createFunctionDataWithCode()
.withName(functionName)
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
@@ -200,21 +171,20 @@ describe('SharedFunctionsParser', () => {
});
});
describe('given empty functions, returns empty collection', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
// act
const actual = new ParseFunctionsCallerWithDefaults()
.withFunctions(absentValue)
.parseFunctions();
// assert
expect(actual).to.not.equal(undefined);
});
}, { excludeUndefined: true, excludeNull: true });
});
describe('function with inline code', () => {
it('parses single function with code as expected', () => {
// arrange
const name = 'function-name';
const expected = FunctionDataStub
.createWithoutCallOrCodes()
const expected = createFunctionDataWithoutCallOrCode()
.withName(name)
.withCode('expected-code')
.withRevertCode('expected-revert-code')
@@ -239,7 +209,7 @@ describe('SharedFunctionsParser', () => {
const call = new FunctionCallDataStub()
.withName('calleeFunction')
.withParameters({ test: 'value' });
const data = FunctionDataStub.createWithoutCallOrCodes()
const data = createFunctionDataWithoutCallOrCode()
.withName('caller-function')
.withCall(call);
// act
@@ -260,10 +230,10 @@ describe('SharedFunctionsParser', () => {
const call2 = new FunctionCallDataStub()
.withName('calleeFunction2')
.withParameters({ param2: 'value2' });
const caller1 = FunctionDataStub.createWithoutCallOrCodes()
const caller1 = createFunctionDataWithoutCallOrCode()
.withName('caller-function')
.withCall(call1);
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
const caller2 = createFunctionDataWithoutCallOrCode()
.withName('caller-function-2')
.withCall([call1, call2]);
// act
@@ -289,7 +259,7 @@ class ParseFunctionsCallerWithDefaults {
private codeValidator: ICodeValidator = new CodeValidatorStub();
private functions: readonly FunctionData[] = [FunctionDataStub.createWithCode()];
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
public withSyntax(syntax: ILanguageSyntax) {
this.syntax = syntax;
@@ -312,11 +282,11 @@ class ParseFunctionsCallerWithDefaults {
}
}
function expectEqualName(expected: FunctionDataStub, actual: ISharedFunction): void {
function expectEqualName(expected: FunctionData, actual: ISharedFunction): void {
expect(actual.name).to.equal(expected.name);
}
function expectEqualParameters(expected: FunctionDataStub, actual: ISharedFunction): void {
function expectEqualParameters(expected: FunctionData, actual: ISharedFunction): void {
const actualSimplifiedParameters = actual.parameters.all.map((parameter) => ({
name: parameter.name,
optional: parameter.isOptional,
@@ -329,10 +299,11 @@ function expectEqualParameters(expected: FunctionDataStub, actual: ISharedFuncti
}
function expectEqualFunctionWithInlineCode(
expected: FunctionData,
expected: CodeInstruction,
actual: ISharedFunction,
): void {
expect(actual.body, `function "${actual.name}" has no body`);
expectCodeFunctionBody(actual.body);
expect(actual.body.code, `function "${actual.name}" has no code`);
expect(actual.body.code.execute).to.equal(expected.code);
expect(actual.body.code.revert).to.equal(expected.revertCode);
@@ -343,6 +314,7 @@ function expectEqualCalls(
actual: ISharedFunction,
) {
expect(actual.body, `function "${actual.name}" has no body`);
expectCallsFunctionBody(actual.body);
expect(actual.body.calls, `function "${actual.name}" has no calls`);
const actualSimplifiedCalls = actual.body.calls
.map((call) => ({

View File

@@ -1,11 +1,10 @@
import { describe, it, expect } from 'vitest';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
export function testParameterName(action: (parameterName: string) => string) {
describe('name', () => {
describe('sets as expected', () => {
// arrange
const expectedValues = [
const expectedValues: readonly string[] = [
'lowercase',
'onlyLetters',
'l3tt3rsW1thNumb3rs',
@@ -21,29 +20,33 @@ export function testParameterName(action: (parameterName: string) => string) {
});
describe('throws if invalid', () => {
// arrange
const testCases = [
...getAbsentStringTestCases().map((test) => ({
name: test.valueName,
value: test.absentValue,
expectedError: 'missing parameter name',
})),
const testScenarios: readonly {
readonly description: string;
readonly value: string;
readonly expectedError: string;
}[] = [
{
name: 'has @',
description: 'empty Name',
value: '',
expectedError: 'missing parameter name',
},
{
description: 'has @',
value: 'b@d',
expectedError: 'parameter name must be alphanumeric but it was "b@d"',
},
{
name: 'has {',
description: 'has {',
value: 'b{a}d',
expectedError: 'parameter name must be alphanumeric but it was "b{a}d"',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
for (const { description, value, expectedError } of testScenarios) {
it(description, () => {
// act
const act = () => action(testCase.value);
const act = () => action(value);
// assert
expect(act).to.throw(testCase.expectedError);
expect(act).to.throw(expectedError);
});
}
});

View File

@@ -6,57 +6,28 @@ import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Fun
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub';
import { SharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
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';
describe('ScriptCompiler', () => {
describe('ctor', () => {
describe('throws if syntax is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing syntax';
const syntax = absentValue;
// act
const act = () => new ScriptCompilerBuilder()
.withSomeFunctions()
.withSyntax(syntax)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('canCompile', () => {
describe('throws if script is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing script';
const argument = absentValue;
const builder = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
// act
const act = () => builder.canCompile(argument);
// assert
expect(act).to.throw(expectedError);
});
});
it('returns true if "call" is defined', () => {
// arrange
const sut = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
const script = ScriptDataStub.createWithCall();
const script = createScriptDataWithCall();
// act
const actual = sut.canCompile(script);
// assert
@@ -67,7 +38,7 @@ describe('ScriptCompiler', () => {
const sut = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
const script = ScriptDataStub.createWithCode();
const script = createScriptDataWithCode();
// act
const actual = sut.canCompile(script);
// assert
@@ -75,19 +46,17 @@ describe('ScriptCompiler', () => {
});
});
describe('compile', () => {
describe('throws if script is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing script';
const argument = absentValue;
const builder = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
// act
const act = () => builder.compile(argument);
// assert
expect(act).to.throw(expectedError);
});
it('throws if script does not have body', () => {
// arrange
const expectedError = 'Script does include any calls.';
const scriptData = createScriptDataWithCode();
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.build();
// act
const act = () => sut.compile(scriptData);
// assert
expect(act).to.throw(expectedError);
});
it('returns code as expected', () => {
// arrange
@@ -96,8 +65,8 @@ describe('ScriptCompiler', () => {
revertCode: 'expected-revert-code',
};
const call = new FunctionCallDataStub();
const script = ScriptDataStub.createWithCall(call);
const functions = [FunctionDataStub.createWithCode().withName('existing-func')];
const script = createScriptDataWithCall(call);
const functions = [createFunctionDataWithCode().withName('existing-func')];
const compiledFunctions = new SharedFunctionCollectionStub();
const functionParserMock = new SharedFunctionsParserStub();
functionParserMock.setup(functions, compiledFunctions);
@@ -124,7 +93,7 @@ describe('ScriptCompiler', () => {
.withSyntax(expected)
.withSharedFunctionsParser(parser)
.build();
const scriptData = ScriptDataStub.createWithCall();
const scriptData = createScriptDataWithCall();
// act
sut.compile(scriptData);
// assert
@@ -133,13 +102,13 @@ describe('ScriptCompiler', () => {
});
it('parses given functions', () => {
// arrange
const expectedFunctions = [FunctionDataStub.createWithCode().withName('existing-func')];
const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')];
const parser = new SharedFunctionsParserStub();
const sut = new ScriptCompilerBuilder()
.withFunctions(...expectedFunctions)
.withSharedFunctionsParser(parser)
.build();
const scriptData = ScriptDataStub.createWithCall();
const scriptData = createScriptDataWithCall();
// act
sut.compile(scriptData);
// assert
@@ -155,7 +124,7 @@ describe('ScriptCompiler', () => {
const callCompiler: FunctionCallCompiler = {
compileFunctionCalls: () => { throw new Error(innerError); },
};
const scriptData = ScriptDataStub.createWithCall()
const scriptData = createScriptDataWithCall()
.withName(scriptName);
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
@@ -170,7 +139,8 @@ describe('ScriptCompiler', () => {
// arrange
const scriptName = 'scriptName';
const syntax = new LanguageSyntaxStub();
const invalidCode: CompiledCode = { code: undefined, revertCode: undefined };
const invalidCode = new CompiledCodeStub()
.withCode('' /* invalid code (empty string) */);
const realExceptionMessage = collectExceptionMessage(
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
);
@@ -178,7 +148,7 @@ describe('ScriptCompiler', () => {
const callCompiler: FunctionCallCompiler = {
compileFunctionCalls: () => invalidCode,
};
const scriptData = ScriptDataStub.createWithCall()
const scriptData = createScriptDataWithCall()
.withName(scriptName);
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
@@ -196,7 +166,7 @@ describe('ScriptCompiler', () => {
NoEmptyLines,
// Allow duplicated lines to enable calling same function multiple times
];
const scriptData = ScriptDataStub.createWithCall();
const scriptData = createScriptDataWithCall();
const validator = new CodeValidatorStub();
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
@@ -216,11 +186,11 @@ describe('ScriptCompiler', () => {
class ScriptCompilerBuilder {
private static createFunctions(...names: string[]): FunctionData[] {
return names.map((functionName) => {
return FunctionDataStub.createWithCode().withName(functionName);
return createFunctionDataWithCode().withName(functionName);
});
}
private functions: FunctionData[];
private functions: FunctionData[] | undefined;
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
@@ -230,46 +200,46 @@ class ScriptCompilerBuilder {
private codeValidator: ICodeValidator = new CodeValidatorStub();
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
public withFunctions(...functions: FunctionData[]): this {
this.functions = functions;
return this;
}
public withSomeFunctions(): ScriptCompilerBuilder {
public withSomeFunctions(): this {
this.functions = ScriptCompilerBuilder.createFunctions('test-function');
return this;
}
public withFunctionNames(...functionNames: string[]): ScriptCompilerBuilder {
public withFunctionNames(...functionNames: string[]): this {
this.functions = ScriptCompilerBuilder.createFunctions(...functionNames);
return this;
}
public withEmptyFunctions(): ScriptCompilerBuilder {
public withEmptyFunctions(): this {
this.functions = [];
return this;
}
public withSyntax(syntax: ILanguageSyntax): ScriptCompilerBuilder {
public withSyntax(syntax: ILanguageSyntax): this {
this.syntax = syntax;
return this;
}
public withSharedFunctionsParser(
sharedFunctionsParser: ISharedFunctionsParser,
): ScriptCompilerBuilder {
): this {
this.sharedFunctionsParser = sharedFunctionsParser;
return this;
}
public withCodeValidator(
codeValidator: ICodeValidator,
): ScriptCompilerBuilder {
): this {
this.codeValidator = codeValidator;
return this;
}
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): ScriptCompilerBuilder {
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): this {
this.callCompiler = callCompiler;
return this;
}
@@ -287,13 +257,3 @@ class ScriptCompilerBuilder {
);
}
}
function collectExceptionMessage(action: () => unknown) {
let message = '';
try {
action();
} catch (e) {
message = e.message;
}
return message;
}

View File

@@ -4,9 +4,9 @@ import { parseScript, ScriptFactoryType } from '@/application/Parser/Script/Scri
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
@@ -25,7 +25,7 @@ describe('ScriptParser', () => {
it('parses name as expected', () => {
// arrange
const expected = 'test-expected-name';
const script = ScriptDataStub.createWithCode()
const script = createScriptDataWithCode()
.withName(expected);
// act
const actual = new TestBuilder()
@@ -37,7 +37,7 @@ describe('ScriptParser', () => {
it('parses docs as expected', () => {
// arrange
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
const script = ScriptDataStub.createWithCode()
const script = createScriptDataWithCode()
.withDocs(docs);
const expected = parseDocs(script);
// act
@@ -51,7 +51,7 @@ describe('ScriptParser', () => {
describe('accepts absent level', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const script = ScriptDataStub.createWithCode()
const script = createScriptDataWithCode()
.withRecommend(absentValue);
// act
const actual = new TestBuilder()
@@ -59,14 +59,14 @@ describe('ScriptParser', () => {
.parseScript();
// assert
expect(actual.level).to.equal(undefined);
});
}, { excludeNull: true });
});
it('parses level as expected', () => {
// arrange
const expectedLevel = RecommendationLevel.Standard;
const expectedName = 'level';
const levelText = 'standard';
const script = ScriptDataStub.createWithCode()
const script = createScriptDataWithCode()
.withRecommend(levelText);
const parserMock = new EnumParserStub<RecommendationLevel>()
.setup(expectedName, levelText, expectedLevel);
@@ -83,8 +83,7 @@ describe('ScriptParser', () => {
it('parses "execute" as expected', () => {
// arrange
const expected = 'expected-code';
const script = ScriptDataStub
.createWithCode()
const script = createScriptDataWithCode()
.withCode(expected);
// act
const parsed = new TestBuilder()
@@ -97,8 +96,7 @@ describe('ScriptParser', () => {
it('parses "revert" as expected', () => {
// arrange
const expected = 'expected-revert-code';
const script = ScriptDataStub
.createWithCode()
const script = createScriptDataWithCode()
.withRevertCode(expected);
// act
const parsed = new TestBuilder()
@@ -109,23 +107,10 @@ describe('ScriptParser', () => {
expect(actual).to.equal(expected);
});
describe('compiler', () => {
describe('throws when context is not defined', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedMessage = 'missing context';
const context: ICategoryCollectionParseContext = absentValue;
// act
const act = () => new TestBuilder()
.withContext(context)
.parseScript();
// assert
expect(act).to.throw(expectedMessage);
});
});
it('gets code from compiler', () => {
// arrange
const expected = new ScriptCodeStub();
const script = ScriptDataStub.createWithCode();
const script = createScriptDataWithCode();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script, expected);
const parseContext = new CategoryCollectionParseContextStub()
@@ -147,8 +132,7 @@ describe('ScriptParser', () => {
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
const parseContext = new CategoryCollectionParseContextStub()
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
const script = ScriptDataStub
.createWithoutCallOrCodes()
const script = createScriptDataWithoutCallOrCodes()
.withCode(duplicatedCode);
// act
const act = () => new TestBuilder()
@@ -166,8 +150,7 @@ describe('ScriptParser', () => {
NoDuplicatedLines,
];
const validator = new CodeValidatorStub();
const script = ScriptDataStub
.createWithCode()
const script = createScriptDataWithCode()
.withCode('expected code to be validated')
.withRevertCode('expected revert code to be validated');
// act
@@ -186,8 +169,7 @@ describe('ScriptParser', () => {
const expectedRules = [];
const expectedCodeCalls = [];
const validator = new CodeValidatorStub();
const script = ScriptDataStub
.createWithCall();
const script = createScriptDataWithCall();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script, new ScriptCodeStub());
const parseContext = new CategoryCollectionParseContextStub()
@@ -222,7 +204,7 @@ describe('ScriptParser', () => {
new NodeValidationTestRunner()
.testInvalidNodeName((invalidName) => {
return createTest(
ScriptDataStub.createWithCall().withName(invalidName),
createScriptDataWithCall().withName(invalidName),
);
})
.testMissingNodeData((node) => {
@@ -231,30 +213,30 @@ describe('ScriptParser', () => {
.runThrowingCase({
name: 'throws when both function call and code are defined',
scenario: createTest(
ScriptDataStub.createWithCall().withCode('code'),
createScriptDataWithCall().withCode('code'),
),
expectedMessage: 'Cannot define both "call" and "code".',
expectedMessage: 'Both "call" and "code" are defined.',
})
.runThrowingCase({
name: 'throws when both function call and revertCode are defined',
scenario: createTest(
ScriptDataStub.createWithCall().withRevertCode('revert-code'),
createScriptDataWithCall().withRevertCode('revert-code'),
),
expectedMessage: 'Cannot define "revertCode" if "call" is defined.',
expectedMessage: 'Both "call" and "revertCode" are defined.',
})
.runThrowingCase({
name: 'throws when neither call or revertCode are defined',
scenario: createTest(
ScriptDataStub.createWithoutCallOrCodes(),
createScriptDataWithoutCallOrCodes(),
),
expectedMessage: 'Must define either "call" or "code".',
expectedMessage: 'Neither "call" or "code" is defined.',
});
});
it(`rethrows exception if ${Script.name} cannot be constructed`, () => {
// arrange
const expectedError = 'script creation failed';
const factoryMock: ScriptFactoryType = () => { throw new Error(expectedError); };
const data = ScriptDataStub.createWithCode();
const data = createScriptDataWithCode();
// act
const act = () => new TestBuilder()
.withData(data)
@@ -274,14 +256,14 @@ describe('ScriptParser', () => {
});
class TestBuilder {
private data: ScriptData = ScriptDataStub.createWithCode();
private data: ScriptData = createScriptDataWithCode();
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
private parser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
.setupDefaultValue(RecommendationLevel.Standard);
private factory: ScriptFactoryType = undefined;
private factory?: ScriptFactoryType = undefined;
private codeValidator: ICodeValidator = new CodeValidatorStub();

View File

@@ -4,7 +4,7 @@ import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationR
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
import { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
describe('CodeValidator', () => {
describe('instance', () => {
@@ -23,10 +23,10 @@ describe('CodeValidator', () => {
const act = () => sut.throwIfInvalid(code, [new CodeValidationRuleStub()]);
// assert
expect(act).to.not.throw();
});
}, { excludeNull: true, excludeUndefined: true });
});
describe('throws if rules are empty', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<ICodeValidationRule>((absentValue) => {
// arrange
const expectedError = 'missing rules';
const rules = absentValue;
@@ -35,7 +35,7 @@ describe('CodeValidator', () => {
const act = () => sut.throwIfInvalid('code', rules);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
describe('splits lines as expected', () => {
it('supports all line separators', () => {

View File

@@ -1,24 +1,10 @@
import { describe, expect } from 'vitest';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { describe } from 'vitest';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
describe('NoDuplicatedLines', () => {
describe('ctor', () => {
describe('throws if syntax is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing syntax';
const syntax = absentValue;
// act
const act = () => new NoDuplicatedLines(syntax);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('analyze', () => {
testCodeValidationRule([
{

View File

@@ -23,12 +23,6 @@ describe('NoEmptyLines', () => {
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
sut: new NoEmptyLines(),
},
{
testName: 'shows error for undefined and null lines',
codeLines: ['first line', undefined, 'third line', null],
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
sut: new NoEmptyLines(),
},
{
testName: 'shows error for whitespace-only lines',
codeLines: ['first line', ' ', 'third line'],

View File

@@ -3,40 +3,21 @@ import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSu
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { getAbsentObjectTestCases, getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('CodeSubstituter', () => {
describe('throws with invalid parameters', () => {
// arrange
const testCases = [
...getAbsentStringTestCases().map((testCase) => ({
name: `given code: ${testCase.valueName}`,
expectedError: 'missing code',
parameters: {
code: testCase.absentValue,
info: new ProjectInformationStub(),
},
})),
...getAbsentObjectTestCases().map((testCase) => ({
name: `given info: ${testCase.valueName}`,
expectedError: 'missing info',
parameters: {
code: 'non empty code',
info: testCase.absentValue,
},
})),
];
for (const testCase of testCases) {
it(`${testCase.name} throws "${testCase.expectedError}"`, () => {
// arrange
const sut = new CodeSubstituterBuilder().build();
const { code, info } = testCase.parameters;
// act
const act = () => sut.substitute(code, info);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
describe('throws if code is empty', () => {
itEachAbsentStringValue((emptyCode) => {
// arrange
const expectedError = 'missing code';
const code = emptyCode;
const info = new ProjectInformationStub();
const sut = new CodeSubstituterBuilder().build();
// act
const act = () => sut.substitute(code, info);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
describe('substitutes parameters as expected values', () => {
// arrange

View File

@@ -8,38 +8,9 @@ import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformat
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
import { CodeSubstituterStub } from '@tests/unit/shared/Stubs/CodeSubstituterStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('ScriptingDefinitionParser', () => {
describe('parseScriptingDefinition', () => {
describe('throws when info is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing info';
const info = absentValue;
const definition = new ScriptingDefinitionDataStub();
const sut = new ScriptingDefinitionParserBuilder()
.build();
// act
const act = () => sut.parse(definition, info);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when definition is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing definition';
const info = new ProjectInformationStub();
const definition = absentValue;
const sut = new ScriptingDefinitionParserBuilder()
.build();
// act
const act = () => sut.parse(definition, info);
// assert
expect(act).to.throw(expectedError);
});
});
describe('language', () => {
it('parses as expected', () => {
// arrange