refactor script compilation to make it easy to add new expressions #41 #53

This commit is contained in:
undergroundwires
2021-03-05 15:52:49 +01:00
parent 1f8a0cf9ab
commit 646db90585
42 changed files with 1312 additions and 582 deletions

View File

@@ -5,15 +5,15 @@ import { parseCategoryCollection } from '@/application/Parser/CategoryCollection
import { parseCategory } from '@/application/Parser/CategoryParser';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
import { mockEnumParser } from '../../stubs/EnumParserStub';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import { EnumParserStub } from '../../stubs/EnumParserStub';
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
import { FunctionDataStub } from '../../stubs/FunctionDataStub';
import { RecommendationLevel } from '../../../../src/domain/RecommendationLevel';
describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => {
@@ -74,7 +74,8 @@ describe('CategoryCollectionParser', () => {
// arrange
const collection = new CollectionDataStub();
const information = parseProjectInformation(process.env);
const expected = parseScriptingDefinition(collection.scripting, information);
const expected = new ScriptingDefinitionParser()
.parse(collection.scripting, information);
// act
const actual = parseCategoryCollection(collection, information).scripting;
// assert
@@ -89,7 +90,8 @@ describe('CategoryCollectionParser', () => {
const expectedName = 'os';
const collection = new CollectionDataStub()
.withOs(osText);
const parserMock = mockEnumParser(expectedName, osText, expectedOs);
const parserMock = new EnumParserStub<OperatingSystem>()
.setup(expectedName, osText, expectedOs);
const info = new ProjectInformationStub();
// act
const actual = parseCategoryCollection(collection, info, parserMock);

View File

@@ -0,0 +1,134 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { Expression } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { ExpressionArguments } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
describe('Expression', () => {
describe('ctor', () => {
describe('position', () => {
it('throws if undefined', () => {
// arrange
const expectedError = 'undefined position';
const position = undefined;
// 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);
// act
const actual = new ExpressionBuilder()
.withPosition(expected)
.build();
// assert
expect(actual.position).to.equal(expected);
});
});
describe('parameters', () => {
it('defaults to empty array if undefined', () => {
// arrange
const parameters = undefined;
// act
const actual = new ExpressionBuilder()
.withParameters(parameters)
.build();
// assert
expect(actual.parameters).to.have.lengthOf(0);
});
it('sets as expected', () => {
// arrange
const expected = [ 'firstParameterName', 'secondParameterName' ];
// act
const actual = new ExpressionBuilder()
.withParameters(expected)
.build();
// assert
expect(actual.parameters).to.deep.equal(expected);
});
});
describe('evaluator', () => {
it('throws if undefined', () => {
// arrange
const expectedError = 'undefined evaluator';
const evaluator = undefined;
// act
const act = () => new ExpressionBuilder()
.withEvaluator(evaluator)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('evaluate', () => {
it('returns result from evaluator', () => {
// arrange
const evaluatorMock: ExpressionEvaluator = (args) => JSON.stringify(args);
const givenArguments = { parameter1: 'value1', parameter2: 'value2' };
const expected = evaluatorMock(givenArguments);
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameters(Object.keys(givenArguments))
.build();
// arrange
const actual = sut.evaluate(givenArguments);
// assert
expect(expected).to.equal(actual);
});
it('filters unused arguments', () => {
// arrange
let actual: ExpressionArguments = {};
const evaluatorMock: ExpressionEvaluator = (providedArgs) => {
Object.keys(providedArgs)
.forEach((name) => actual = {...actual, [name]: providedArgs[name] });
return '';
};
const parameterNameToHave = 'parameterToHave';
const parameterNameToIgnore = 'parameterToIgnore';
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameters([ parameterNameToHave ])
.build();
const args: ExpressionArguments = {
[parameterNameToHave]: 'value-to-have',
[parameterNameToIgnore]: 'value-to-ignore',
};
const expected: ExpressionArguments = {
[parameterNameToHave]: args[parameterNameToHave],
};
// arrange
sut.evaluate(args);
// assert
expect(expected).to.deep.equal(actual);
});
});
});
class ExpressionBuilder {
private position: ExpressionPosition = new ExpressionPosition(0, 5);
private parameters: readonly string[] = new Array<string>();
public withPosition(position: ExpressionPosition) {
this.position = position;
return this;
}
public withEvaluator(evaluator: ExpressionEvaluator) {
this.evaluator = evaluator;
return this;
}
public withParameters(parameters: string[]) {
this.parameters = parameters;
return this;
}
public build() {
return new Expression(this.position, this.evaluator, this.parameters);
}
private evaluator: ExpressionEvaluator = () => '';
}

View File

@@ -0,0 +1,34 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
describe('ExpressionPosition', () => {
describe('ctor', () => {
it('sets as expected', () => {
// arrange
const expectedStart = 0;
const expectedEnd = 5;
// act
const sut = new ExpressionPosition(expectedStart, expectedEnd);
// assert
expect(sut.start).to.equal(expectedStart);
expect(sut.end).to.equal(expectedEnd);
});
describe('throws when invalid', () => {
// arrange
const testCases = [
{ start: 5, end: 5, error: 'no length (start = end = 5)' },
{ start: 5, end: 3, error: 'start (5) after end (3)' },
{ start: -1, end: 3, error: 'negative start position: -1' },
];
for (const testCase of testCases) {
it(testCase.error, () => {
// act
const act = () => new ExpressionPosition(testCase.start, testCase.end);
// assert
expect(act).to.throw(testCase.error);
});
}
});
});
});

View File

@@ -1,79 +1,127 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { ExpressionStub } from '../../../../../stubs/ExpressionStub';
import { ExpressionParserStub } from '../../../../../stubs/ExpressionParserStub';
describe('ExpressionsCompiler', () => {
describe('parameter substitution', () => {
describe('substitutes as expected', () => {
describe('compileExpressions', () => {
describe('combines expressions as expected', () => {
// arrange
const testCases = [ {
name: 'with different parameters',
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
parameters: {
firstParameter: 'llo',
secondParameter: 'world',
const code = 'part1 {{ a }} part2 {{ b }} part3';
const testCases = [
{
name: 'with ordered expressions',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
],
expected: 'part1 a part2 b part3',
},
expected: 'Hello world!',
}, {
name: 'with single parameter',
code: '{{ $parameter }}!',
parameters: {
parameter: 'Hodor',
{
name: 'unordered expressions',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
],
expected: 'part1 a part2 b part3',
},
expected: 'Hodor!',
}];
{
name: 'with no expressions',
expressions: [],
expected: code,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new MockableExpressionsCompiler();
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
const actual = sut.compileExpressions(testCase.code, testCase.parameters);
const actual = sut.compileExpressions(code);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('throws when expected value is not provided', () => {
it('passes arguments to expressions as expected', () => {
// arrange
const expected = {
parameter1: 'value1',
parameter2: 'value2',
};
const code = 'non-important';
const expressions = [
new ExpressionStub(),
new ExpressionStub(),
];
const expressionParserMock = new ExpressionParserStub()
.withResult(expressions);
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
sut.compileExpressions(code, expected);
// assert
expect(expressions[0].callHistory).to.have.lengthOf(1);
expect(expressions[0].callHistory[0]).to.equal(expected);
expect(expressions[1].callHistory).to.have.lengthOf(1);
expect(expressions[1].callHistory[0]).to.equal(expected);
});
describe('throws when expected argument is not provided', () => {
// arrange
const noParameterTestCases = [
{
name: 'empty parameters',
code: '{{ $parameter }}!',
parameters: {},
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: {},
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'undefined parameters',
code: '{{ $parameter }}!',
parameters: undefined,
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: undefined,
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'unnecessary parameter provided',
code: '{{ $parameter }}!',
parameters: {
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: {
unnecessaryParameter: 'unnecessaryValue',
},
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'undefined value',
code: '{{ $parameter }}!',
parameters: {
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: {
parameter: undefined,
},
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'multiple values are not',
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
parameters: {},
name: 'multiple values are not provided',
expressions: [
new ExpressionStub().withParameters('parameter1'),
new ExpressionStub().withParameters('parameter2', 'parameter3'),
],
args: {},
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
},
{
name: 'some values are provided',
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
parameters: {
expressions: [
new ExpressionStub().withParameters('parameter1'),
new ExpressionStub().withParameters('parameter2', 'parameter3'),
],
args: {
parameter2: 'value',
},
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
@@ -81,19 +129,33 @@ describe('ExpressionsCompiler', () => {
];
for (const testCase of noParameterTestCases) {
it(testCase.name, () => {
const sut = new MockableExpressionsCompiler();
const code = 'non-important-code';
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
const act = () => sut.compileExpressions(testCase.code, testCase.parameters);
const act = () => sut.compileExpressions(code, testCase.args);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
it('calls parser with expected code', () => {
// arrange
const expected = 'expected-code';
const expressionParserMock = new ExpressionParserStub();
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
sut.compileExpressions(expected);
// assert
expect(expressionParserMock.callHistory).to.have.lengthOf(1);
expect(expressionParserMock.callHistory[0]).to.equal(expected);
});
});
});
class MockableExpressionsCompiler extends ExpressionsCompiler {
constructor() {
super();
constructor(extractor: IExpressionParser) {
super(extractor);
}
}

View File

@@ -1,141 +0,0 @@
import 'mocha';
import { expect } from 'chai';
import { generateIlCode } from '@/application/Parser/Script/Compiler/Expressions/ILCode';
describe('ILCode', () => {
describe('getUniqueParameterNames', () => {
// arrange
const testCases = [
{
name: 'empty parameters: returns an empty array',
code: 'no expressions',
expected: [ ],
},
{
name: 'single parameter: returns expected for single usage',
code: '{{ $single }}',
expected: [ 'single' ],
},
{
name: 'single parameter: returns distinct values for repeating parameters',
code: '{{ $singleRepeating }}, {{ $singleRepeating }}',
expected: [ 'singleRepeating' ],
},
{
name: 'multiple parameters: returns expected for single usage of each',
code: '{{ $firstParameter }}, {{ $secondParameter }}',
expected: [ 'firstParameter', 'secondParameter' ],
},
{
name: 'multiple parameters: returns distinct values for repeating parameters',
code: '{{ $firstParameter }}, {{ $firstParameter }}, {{ $firstParameter }} {{ $secondParameter }}, {{ $secondParameter }}',
expected: [ 'firstParameter', 'secondParameter' ],
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const sut = generateIlCode(testCase.code);
const actual = sut.getUniqueParameterNames();
// assert
expect(actual).to.deep.equal(testCase.expected);
});
}
});
describe('substituteParameter', () => {
describe('substitutes by ignoring white spaces inside mustaches', () => {
// arrange
const mustacheVariations = [
'Hello {{ $test }}!',
'Hello {{$test }}!',
'Hello {{ $test}}!',
'Hello {{$test}}!'];
mustacheVariations.forEach((variation) => {
it(variation, () => {
// arrange
const ilCode = generateIlCode(variation);
const expected = 'Hello world!';
// act
const actual = ilCode
.substituteParameter('test', 'world')
.compile();
// assert
expect(actual).to.deep.equal(expected);
});
});
});
describe('substitutes as expected', () => {
// arrange
const testCases = [
{
name: 'single parameter',
code: 'Hello {{ $firstParameter }}!',
expected: 'Hello world!',
parameters: {
firstParameter: 'world',
},
},
{
name: 'single parameter repeated',
code: '{{ $firstParameter }} {{ $firstParameter }}!',
expected: 'hello hello!',
parameters: {
firstParameter: 'hello',
},
},
{
name: 'multiple parameters',
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
expected: 'Hello world!',
parameters: {
firstParameter: 'llo',
secondParameter: 'world',
},
},
{
name: 'multiple parameters repeated',
code: 'He{{ $firstParameter }} {{ $secondParameter }} and He{{ $firstParameter }} {{ $secondParameter }}!',
expected: 'Hello world and Hello world!',
parameters: {
firstParameter: 'llo',
secondParameter: 'world',
},
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
let ilCode = generateIlCode(testCase.code);
for (const parameterName of Object.keys(testCase.parameters)) {
const value = testCase.parameters[parameterName];
ilCode = ilCode.substituteParameter(parameterName, value);
}
const actual = ilCode.compile();
// assert
expect(actual).to.deep.equal(testCase.expected);
});
}
});
});
describe('compile', () => {
it('throws if there are expressions left', () => {
// arrange
const expectedError = 'unknown expression: "each"';
const code = '{{ each }}';
// act
const ilCode = generateIlCode(code);
const act = () => ilCode.compile();
// assert
expect(act).to.throw(expectedError);
});
it('returns code as it is if there are no expressions', () => {
// arrange
const expected = 'I should be the same!';
const ilCode = generateIlCode(expected);
// act
const actual = ilCode.compile();
// assert
expect(actual).to.equal(expected);
});
});
});

View File

@@ -0,0 +1,87 @@
import 'mocha';
import { expect } from 'chai';
import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
import { ExpressionStub } from '../../../../../../stubs/ExpressionStub';
describe('CompositeExpressionParser', () => {
describe('ctor', () => {
it('throws if one of the parsers is undefined', () => {
// arrange
const expectedError = 'undefined leaf';
const parsers: readonly IExpressionParser[] = [ undefined, mockParser() ];
// act
const act = () => new CompositeExpressionParser(parsers);
// assert
expect(act).to.throw(expectedError);
});
});
describe('findExpressions', () => {
describe('returns result from parsers as expected', () => {
// arrange
const pool = [
new ExpressionStub(), new ExpressionStub(), new ExpressionStub(),
new ExpressionStub(), new ExpressionStub(),
];
const testCases = [
{
name: 'from single parsing none',
parsers: [ mockParser() ],
expected: [],
},
{
name: 'from single parsing single',
parsers: [ mockParser(pool[0]) ],
expected: [ pool[0] ],
},
{
name: 'from single parsing multiple',
parsers: [ mockParser(pool[0], pool[1]) ],
expected: [ pool[0], pool[1] ],
},
{
name: 'from multiple parsers with each parsing single',
parsers: [
mockParser(pool[0]),
mockParser(pool[1]),
mockParser(pool[2]),
],
expected: [ pool[0], pool[1], pool[2] ],
},
{
name: 'from multiple parsers with each parsing multiple',
parsers: [
mockParser(pool[0], pool[1]),
mockParser(pool[2], pool[3], pool[4]) ],
expected: [ pool[0], pool[1], pool[2], pool[3], pool[4] ],
},
{
name: 'from multiple parsers with only some parsing',
parsers: [
mockParser(pool[0], pool[1]),
mockParser(),
mockParser(pool[2]),
mockParser(),
],
expected: [ pool[0], pool[1], pool[2] ],
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new CompositeExpressionParser(testCase.parsers);
// act
const result = sut.findExpressions('non-important-code');
// expect
expect(result).to.deep.equal(testCase.expected);
});
}
});
});
});
function mockParser(...result: IExpression[]): IExpressionParser {
return {
findExpressions: () => result,
};
}

View File

@@ -0,0 +1,122 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/RegexParser';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
describe('RegexParser', () => {
describe('findExpressions', () => {
describe('matches regex as expected', () => {
// arrange
const testCases = [
{
name: 'returns no result when regex does not match',
regex: /hello/g,
code: 'world',
},
{
name: 'returns expected when regex matches single',
regex: /hello/g,
code: 'hello world',
},
{
name: 'returns expected when regex matches multiple',
regex: /l/g,
code: 'hello world',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const expected = Array.from(testCase.code.matchAll(testCase.regex));
const matches = new Array<RegExpMatchArray>();
const builder = (m: RegExpMatchArray): IPrimitiveExpression => {
matches.push(m);
return mockPrimitiveExpression();
};
const sut = new RegexParserConcrete(testCase.regex, builder);
// act
const expressions = sut.findExpressions(testCase.code);
// assert
expect(expressions).to.have.lengthOf(matches.length);
expect(matches).to.deep.equal(expected);
});
}
});
it('sets evaluator as expected', () => {
// arrange
const expected = getEvaluatorStub();
const regex = /hello/g;
const code = 'hello';
const builder = (): IPrimitiveExpression => ({
evaluator: expected,
});
const sut = new RegexParserConcrete(regex, builder);
// act
const expressions = sut.findExpressions(code);
// assert
expect(expressions).to.have.lengthOf(1);
expect(expressions[0].evaluate === expected);
});
it('sets parameters as expected', () => {
// arrange
const expected = [ 'parameter1', 'parameter2' ];
const regex = /hello/g;
const code = 'hello';
const builder = (): IPrimitiveExpression => ({
evaluator: getEvaluatorStub(),
parameters: expected,
});
const sut = new RegexParserConcrete(regex, builder);
// act
const expressions = sut.findExpressions(code);
// assert
expect(expressions).to.have.lengthOf(1);
expect(expressions[0].parameters).to.equal(expected);
});
it('sets expected position', () => {
// arrange
const code = 'mate date in state is fate';
const regex = /ate/g;
const expected = [
new ExpressionPosition(1, 4),
new ExpressionPosition(6, 9),
new ExpressionPosition(15, 18),
new ExpressionPosition(23, 26),
];
const sut = new RegexParserConcrete(regex);
// act
const expressions = sut.findExpressions(code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(expected);
});
});
});
function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
return () => ({
evaluator: getEvaluatorStub(),
});
}
function getEvaluatorStub(): ExpressionEvaluator {
return () => undefined;
}
function mockPrimitiveExpression(): IPrimitiveExpression {
return {
evaluator: getEvaluatorStub(),
};
}
class RegexParserConcrete extends RegexParser {
protected regex: RegExp;
public constructor(
regex: RegExp,
private readonly builder = mockBuilder()) {
super();
this.regex = regex;
}
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
return this.builder(match);
}
}

View File

@@ -0,0 +1,69 @@
import 'mocha';
import { expect } from 'chai';
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { ExpressionArguments } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
describe('ParameterSubstitutionParser', () => {
it('finds at expected positions', () => {
// arrange
const testCases = [ {
name: 'single parameter',
code: '{{ $parameter }}!',
expected: [ new ExpressionPosition(0, 16) ],
}, {
name: 'different parameters',
code: 'He{{ $firstParameter }} {{ $secondParameter }}!!',
expected: [ new ExpressionPosition(2, 23), new ExpressionPosition(24, 46) ],
}];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new ParameterSubstitutionParser();
// act
const expressions = sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(testCase.expected);
});
}
});
it('evaluates as expected', () => {
const testCases = [ {
name: 'single parameter',
code: '{{ $parameter }}',
args: [ {
name: 'parameter',
value: 'Hello world',
}],
expected: [ 'Hello world' ],
},
{
name: 'different parameters',
code: '{{ $firstParameter }} {{ $secondParameter }}!',
args: [ {
name: 'firstParameter',
value: 'Hello',
},
{
name: 'firstParameter',
value: 'World',
}],
expected: [ 'Hello', 'World' ],
}];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new ParameterSubstitutionParser();
let args: ExpressionArguments = {};
for (const arg of testCase.args) {
args = {...args, [arg.name]: arg.value };
}
// act
const expressions = sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.evaluate(args));
expect(actual).to.deep.equal(testCase.expected);
});
}
});
});

View File

@@ -46,6 +46,18 @@ describe('FunctionsCompiler', () => {
// assert
expect(act).to.throw(expectedError);
});
it('throws when parameters is not an array of strings', () => {
// arrange
const parameterNameWithUnexpectedType = 5;
const func = FunctionDataStub.createWithCall()
.withParameters(parameterNameWithUnexpectedType as any);
const expectedError = `unexpected parameter name type in "${func.name}"`;
const sut = new MockableFunctionCompiler();
// act
const act = () => sut.compileFunctions([ func ]);
// assert
expect(act).to.throw(expectedError);
});
describe('throws when when function have duplicate code', () => {
it('code', () => {
// arrange

View File

@@ -10,103 +10,131 @@ import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
describe('FunctionCallCompiler', () => {
describe('compileCall', () => {
describe('call', () => {
it('throws with undefined call', () => {
// arrange
const expectedError = 'undefined call';
const call = undefined;
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call is not an object', () => {
// arrange
const expectedError = 'called function(s) must be an object';
const invalidCalls: readonly any[] = ['string', 33];
const sut = new MockableFunctionCallCompiler();
const functions = new SharedFunctionCollectionStub();
invalidCalls.forEach((invalidCall) => {
describe('parameter validation', () => {
describe('call', () => {
it('throws with undefined call', () => {
// arrange
const expectedError = 'undefined call';
const call = undefined;
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(invalidCall, functions);
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call is not an object', () => {
// arrange
const expectedError = 'called function(s) must be an object';
const invalidCalls: readonly any[] = ['string', 33];
const sut = new MockableFunctionCallCompiler();
const functions = new SharedFunctionCollectionStub();
invalidCalls.forEach((invalidCall) => {
// act
const act = () => sut.compileCall(invalidCall, functions);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if call sequence has undefined call', () => {
// arrange
const expectedError = 'undefined function call';
const call: FunctionCallData[] = [
{ function: 'function-name' },
undefined,
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call sequence has undefined function name', () => {
// arrange
const expectedError = 'empty function name called';
const call: FunctionCallData[] = [
{ function: 'function-name' },
{ function: undefined },
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call parameters does not match function parameters', () => {
// arrange
const functionName = 'test-function-name';
const testCases = [
{
name: 'an unexpected parameter instead',
functionParameters: [ 'another-parameter' ],
callParameters: [ 'unexpected-parameter' ],
expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`,
},
{
name: 'an unexpected parameter when none required',
functionParameters: undefined,
callParameters: [ 'unexpected-parameter' ],
expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`,
},
{
name: 'expected and unexpected parameter',
functionParameters: [ 'expected-parameter' ],
callParameters: [ 'expected-parameter', 'unexpected-parameter' ],
expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const func = new SharedFunctionStub()
.withName('test-function-name')
.withParameters(...testCase.functionParameters);
let params: FunctionCallParametersData = {};
for (const parameter of testCase.callParameters) {
params = {...params, [parameter]: 'defined-parameter-value '};
}
const call: FunctionCallData = { function: func.name, parameters: params };
const functions = new SharedFunctionCollectionStub().withFunction(func);
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
});
describe('functions', () => {
it('throws with undefined functions', () => {
// arrange
const expectedError = 'undefined functions';
const call: FunctionCallData = { function: 'function-call' };
const functions = undefined;
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if function does not exist', () => {
// arrange
const expectedError = 'function does not exist';
const call: FunctionCallData = { function: 'function-call' };
const functions: ISharedFunctionCollection = {
getFunctionByName: () => { throw new Error(expectedError); },
};
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if call sequence has undefined call', () => {
// arrange
const expectedError = 'undefined function call';
const call: FunctionCallData[] = [
{ function: 'function-name' },
undefined,
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call sequence has undefined function name', () => {
// arrange
const expectedError = 'empty function name called';
const call: FunctionCallData[] = [
{ function: 'function-name' },
{ function: undefined },
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call parameters does not match function parameters', () => {
// arrange
const unexpectedCallParameterName = 'unexpected-parameter-name';
const func = new SharedFunctionStub()
.withName('test-function-name')
.withParameters('another-parameter');
const expectedError = `function "${func.name}" has unexpected parameter(s) provided: "${unexpectedCallParameterName}"`;
const sut = new MockableFunctionCallCompiler();
const params: FunctionCallParametersData = {
[`${unexpectedCallParameterName}`]: 'unexpected-parameter-value',
};
const call: FunctionCallData = { function: func.name, parameters: params };
const functions = new SharedFunctionCollectionStub().withFunction(func);
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('functions', () => {
it('throws with undefined functions', () => {
// arrange
const expectedError = 'undefined functions';
const call: FunctionCallData = { function: 'function-call' };
const functions = undefined;
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if function does not exist', () => {
// arrange
const expectedError = 'function does not exist';
const call: FunctionCallData = { function: 'function-call' };
const functions: ISharedFunctionCollection = {
getFunctionByName: () => { throw new Error(expectedError); },
};
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('builds code as expected', () => {
describe('builds single call as expected', () => {

View File

@@ -123,9 +123,9 @@ describe('ScriptCompiler', () => {
// assert
expect(isUsed).to.equal(true);
});
it('rethrows error from ScriptCode with script name', () => {
it('rethrows error with script name', () => {
// arrange
const scriptName = 'scriptName'; // // arrange
const scriptName = 'scriptName';
const innerError = 'innerError';
const expectedError = `Script "${scriptName}" ${innerError}`;
const callCompiler: IFunctionCallCompiler = {
@@ -142,6 +142,24 @@ describe('ScriptCompiler', () => {
// assert
expect(act).to.throw(expectedError);
});
it('rethrows error from ScriptCode with script name', () => {
// arrange
const scriptName = 'scriptName';
const expectedError = `Script "${scriptName}" code is empty or undefined`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => ({ code: undefined, revertCode: undefined }),
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.withFunctionCallCompiler(callCompiler)
.build();
// act
const act = () => sut.compile(scriptData);
// assert
expect(act).to.throw(expectedError);
});
});
});

View File

@@ -6,7 +6,7 @@ import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
import { mockEnumParser } from '../../../stubs/EnumParserStub';
import { EnumParserStub } from '../../../stubs/EnumParserStub';
import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
@@ -104,7 +104,8 @@ describe('ScriptParser', () => {
const script = ScriptDataStub.createWithCode()
.withRecommend(levelText);
const parseContext = new CategoryCollectionParseContextStub();
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
const parserMock = new EnumParserStub<RecommendationLevel>()
.setup(expectedName, levelText, expectedLevel);
// act
const actual = parseScript(script, parseContext, parserMock);
// assert

View File

@@ -0,0 +1,96 @@
import 'mocha';
import { expect } from 'chai';
import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ProjectInformationStub } from '../../../stubs/ProjectInformationStub';
import { ExpressionsCompilerStub } from '../../../stubs/ExpressionsCompilerStub';
describe('CodeSubstituter', () => {
describe('throws with invalid parameters', () => {
// arrange
const testCases = [{
expectedError: 'undefined code',
parameters: {
code: undefined,
info: new ProjectInformationStub(),
}},
{
expectedError: 'undefined info',
parameters: {
code: 'non empty code',
info: undefined,
},
}];
for (const testCase of testCases) {
it(`throws "${testCase.expectedError}" as expected`, () => {
const sut = new CodeSubstituterBuilder().build();
// act
const act = () => sut.substitute(testCase.parameters.code, testCase.parameters.info);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
describe('substitutes parameters as expected values', () => {
// arrange
const info = new ProjectInformationStub();
const date = new Date();
const testCases = [
{
parameter: 'homepage',
argument: info.homepage,
},
{
parameter: 'version',
argument: info.version,
},
{
parameter: 'date',
argument: date.toUTCString(),
},
];
for (const testCase of testCases) {
it(`substitutes ${testCase.parameter} as expected`, () => {
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.withDate(date)
.build();
// act
sut.substitute('non empty code', info);
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].parameters[testCase.parameter]).to.equal(testCase.argument);
});
}
});
it('returns code as it is', () => {
// arrange
const expected = 'expected-code';
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.build();
// act
sut.substitute(expected, new ProjectInformationStub());
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].code).to.equal(expected);
});
});
class CodeSubstituterBuilder {
private compiler: IExpressionsCompiler = new ExpressionsCompilerStub();
private date = new Date();
public withCompiler(compiler: IExpressionsCompiler) {
this.compiler = compiler;
return this;
}
public withDate(date: Date) {
this.date = date;
return this;
}
public build() {
return new CodeSubstituter(this.compiler, this.date);
}
}

View File

@@ -0,0 +1,110 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import { IEnumParser } from '@/application/Common/Enum';
import { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ProjectInformationStub } from '../../../stubs/ProjectInformationStub';
import { EnumParserStub } from '../../../stubs/EnumParserStub';
import { ScriptingDefinitionDataStub } from '../../../stubs/ScriptingDefinitionDataStub';
import { CodeSubstituterStub } from '../../../stubs/CodeSubstituterStub';
describe('ScriptingDefinitionParser', () => {
describe('parseScriptingDefinition', () => {
it('throws when info is undefined', () => {
// arrange
const info = undefined;
const definition = new ScriptingDefinitionDataStub();
const sut = new ScriptingDefinitionParserBuilder()
.build();
// act
const act = () => sut.parse(definition, info);
// assert
expect(act).to.throw('undefined info');
});
it('throws when definition is undefined', () => {
// arrange
const info = new ProjectInformationStub();
const definition = undefined;
const sut = new ScriptingDefinitionParserBuilder()
.build();
// act
const act = () => sut.parse(definition, info);
// assert
expect(act).to.throw('undefined definition');
});
describe('language', () => {
it('parses as expected', () => {
// arrange
const expectedLanguage = ScriptingLanguage.batchfile;
const languageText = 'batchfile';
const expectedName = 'language';
const info = new ProjectInformationStub();
const definition = new ScriptingDefinitionDataStub()
.withLanguage(languageText);
const parserMock = new EnumParserStub<ScriptingLanguage>()
.setup(expectedName, languageText, expectedLanguage);
const sut = new ScriptingDefinitionParserBuilder()
.withParser(parserMock)
.build();
// act
const actual = sut.parse(definition, info);
// assert
expect(actual.language).to.equal(expectedLanguage);
});
});
describe('substitutes code as expected', () => {
// arrange
const code = 'hello';
const expected = 'substituted';
const testCases = [
{
name: 'startCode',
getActualValue: (result: IScriptingDefinition) => result.startCode,
data: new ScriptingDefinitionDataStub()
.withStartCode(code),
},
{
name: 'endCode',
getActualValue: (result: IScriptingDefinition) => result.endCode,
data: new ScriptingDefinitionDataStub()
.withEndCode(code),
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const info = new ProjectInformationStub();
const substituterMock = new CodeSubstituterStub()
.setup(code, info, expected);
const sut = new ScriptingDefinitionParserBuilder()
.withSubstituter(substituterMock)
.build();
// act
const definition = sut.parse(testCase.data, info);
// assert
const actual = testCase.getActualValue(definition);
expect(actual).to.equal(expected);
});
}
});
});
});
class ScriptingDefinitionParserBuilder {
private languageParser: IEnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
.setupDefaultValue(ScriptingLanguage.shellscript);
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
public withParser(parser: IEnumParser<ScriptingLanguage>) {
this.languageParser = parser;
return this;
}
public withSubstituter(substituter: ICodeSubstituter) {
this.codeSubstituter = substituter;
return this;
}
public build() {
return new ScriptingDefinitionParser(this.languageParser, this.codeSubstituter);
}
}

View File

@@ -1,151 +0,0 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
import { ProjectInformationStub } from './../../stubs/ProjectInformationStub';
import { mockEnumParser } from '../../stubs/EnumParserStub';
describe('ScriptingDefinitionParser', () => {
describe('parseScriptingDefinition', () => {
it('throws when info is undefined', () => {
// arrange
const info = undefined;
const definition = new ScriptingDefinitionBuilder().construct();
// act
const act = () => parseScriptingDefinition(definition, info);
// assert
expect(act).to.throw('undefined info');
});
it('throws when definition is undefined', () => {
// arrange
const info = new ProjectInformationStub();
const definition = undefined;
// act
const act = () => parseScriptingDefinition(definition, info);
// assert
expect(act).to.throw('undefined definition');
});
describe('language', () => {
it('parses as expected', () => {
// arrange
const expectedLanguage = ScriptingLanguage.batchfile;
const languageText = 'batchfile';
const expectedName = 'language';
const info = new ProjectInformationStub();
const definition = new ScriptingDefinitionBuilder()
.withLanguage(languageText)
.construct();
const parserMock = mockEnumParser(expectedName, languageText, expectedLanguage);
// act
const actual = parseScriptingDefinition(definition, info, new Date(), parserMock);
// assert
expect(actual.language).to.equal(expectedLanguage);
});
});
describe('fileExtension', () => {
it('parses as expected', () => {
// arrange
const expected = 'bat';
const info = new ProjectInformationStub();
const file = new ScriptingDefinitionBuilder()
.withExtension(expected).construct();
// act
const definition = parseScriptingDefinition(file, info);
// assert
const actual = definition.fileExtension;
expect(actual).to.equal(expected);
});
});
describe('startCode', () => {
it('sets as it is', () => {
// arrange
const expected = 'expected-start-code';
const info = new ProjectInformationStub();
const file = new ScriptingDefinitionBuilder().withStartCode(expected).construct();
// act
const definition = parseScriptingDefinition(file, info);
// assert
expect(definition.startCode).to.equal(expected);
});
it('substitutes as expected', () => {
// arrange
const code = 'homepage: {{ $homepage }}, version: {{ $version }}, date: {{ $date }}';
const homepage = 'https://cloudarchitecture.io';
const version = '1.0.2';
const date = new Date();
const expected = `homepage: ${homepage}, version: ${version}, date: ${date.toUTCString()}`;
const info = new ProjectInformationStub().withHomepageUrl(homepage).withVersion(version);
const file = new ScriptingDefinitionBuilder().withStartCode(code).construct();
// act
const definition = parseScriptingDefinition(file, info, date);
// assert
const actual = definition.startCode;
expect(actual).to.equal(expected);
});
});
describe('endCode', () => {
it('sets as it is', () => {
// arrange
const expected = 'expected-end-code';
const info = new ProjectInformationStub();
const file = new ScriptingDefinitionBuilder().withEndCode(expected).construct();
// act
const definition = parseScriptingDefinition(file, info);
// assert
expect(definition.endCode).to.equal(expected);
});
it('substitutes as expected', () => {
// arrange
const code = 'homepage: {{ $homepage }}, version: {{ $version }}, date: {{ $date }}';
const homepage = 'https://cloudarchitecture.io';
const version = '1.0.2';
const date = new Date();
const expected = `homepage: ${homepage}, version: ${version}, date: ${date.toUTCString()}`;
const info = new ProjectInformationStub().withHomepageUrl(homepage).withVersion(version);
const file = new ScriptingDefinitionBuilder().withEndCode(code).construct();
// act
const definition = parseScriptingDefinition(file, info, date);
// assert
const actual = definition.endCode;
expect(actual).to.equal(expected);
});
});
});
});
class ScriptingDefinitionBuilder {
private language = ScriptingLanguage[ScriptingLanguage.batchfile];
private fileExtension = 'bat';
private startCode = 'startCode';
private endCode = 'endCode';
public withLanguage(language: string): ScriptingDefinitionBuilder {
this.language = language;
return this;
}
public withStartCode(startCode: string): ScriptingDefinitionBuilder {
this.startCode = startCode;
return this;
}
public withEndCode(endCode: string): ScriptingDefinitionBuilder {
this.endCode = endCode;
return this;
}
public withExtension(extension: string): ScriptingDefinitionBuilder {
this.fileExtension = extension;
return this;
}
public construct(): ScriptingDefinitionData {
return {
language: this.language,
fileExtension: this.fileExtension,
startCode: this.startCode,
endCode: this.endCode,
};
}
}