Refactor code to comply with ESLint rules

Major refactoring using ESLint with rules from AirBnb and Vue.

Enable most of the ESLint rules and do necessary linting in the code.
Also add more information for rules that are disabled to describe what
they are and why they are disabled.

Allow logging (`console.log`) in test files, and in development mode
(e.g. when working with `npm run serve`), but disable it when
environment is production (as pre-configured by Vue). Also add flag
(`--mode production`) in `lint:eslint` command so production linting is
executed earlier in lifecycle.

Disable rules that requires a separate work. Such as ESLint rules that
are broken in TypeScript: no-useless-constructor (eslint/eslint#14118)
and no-shadow (eslint/eslint#13014).
This commit is contained in:
undergroundwires
2022-01-02 18:20:14 +01:00
parent 96265b75de
commit 5b1fbe1e2f
341 changed files with 16126 additions and 15101 deletions

View File

@@ -12,235 +12,242 @@ import { PipelineCompilerStub } from '@tests/unit/stubs/PipelineCompilerStub';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
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);
expect(actual.parameters.all);
expect(actual.parameters.all.length).to.equal(0);
});
it('sets as expected', () => {
// arrange
const expected = new FunctionParameterCollectionStub()
.withParameterName('firstParameterName')
.withParameterName('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('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('evaluate', () => {
describe('throws with invalid arguments', () => {
const testCases = [
{
name: 'throws if arguments is undefined',
context: undefined,
expectedError: 'undefined context',
},
{
name: 'throws when some of the required args are not provided',
sut: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false),
context: new ExpressionEvaluationContextStub()
.withArgs(new FunctionCallArgumentCollectionStub().withArgument('b', 'provided')),
expectedError: 'argument values are provided for required parameters: "a", "c"',
},
{
name: 'throws when none of the required args are not provided',
sut: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b'], false),
context: new ExpressionEvaluationContextStub()
.withArgs(new FunctionCallArgumentCollectionStub().withArgument('c', 'unrelated')),
expectedError: 'argument values are provided for required parameters: "a", "b"',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// arrange
const sutBuilder = new ExpressionBuilder();
if (testCase.sut) {
testCase.sut(sutBuilder);
}
const sut = sutBuilder.build();
// act
const act = () => sut.evaluate(testCase.context);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
it('returns result from evaluator', () => {
// arrange
const evaluatorMock: ExpressionEvaluator = (c) =>
`"${c
.args
.getAllParameterNames()
.map((name) => context.args.getArgument(name))
.map((arg) => `${arg.parameterName}': '${arg.argumentValue}'`)
.join('", "')}"`;
const givenArguments = new FunctionCallArgumentCollectionStub()
.withArgument('parameter1', 'value1')
.withArgument('parameter2', 'value2');
const expectedParameterNames = givenArguments.getAllParameterNames();
const context = new ExpressionEvaluationContextStub()
.withArgs(givenArguments);
const expected = evaluatorMock(context);
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameterNames(expectedParameterNames)
.build();
// arrange
const actual = sut.evaluate(context);
// assert
expect(expected).to.equal(actual,
`\nGiven arguments: ${JSON.stringify(givenArguments)}\n` +
`\nExpected parameter names: ${JSON.stringify(expectedParameterNames)}\n`,
);
});
it('sends pipeline compiler as it is', () => {
// arrange
const expected = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(expected);
let actual: IPipelineCompiler;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.pipelineCompiler;
return '';
};
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.build();
// arrange
sut.evaluate(context);
// assert
expect(expected).to.equal(actual);
});
describe('filters unused parameters', () => {
// arrange
const testCases = [
{
name: 'with a provided argument',
expressionParameters: new FunctionParameterCollectionStub()
.withParameterName('parameterToHave', false),
arguments: new FunctionCallArgumentCollectionStub()
.withArgument('parameterToHave', 'value-to-have')
.withArgument('parameterToIgnore', 'value-to-ignore'),
expectedArguments: [
new FunctionCallArgumentStub()
.withParameterName('parameterToHave').withArgumentValue('value-to-have'),
],
},
{
name: 'without a provided argument',
expressionParameters: new FunctionParameterCollectionStub()
.withParameterName('parameterToHave', false)
.withParameterName('parameterToIgnore', true),
arguments: new FunctionCallArgumentCollectionStub()
.withArgument('parameterToHave', 'value-to-have'),
expectedArguments: [
new FunctionCallArgumentStub()
.withParameterName('parameterToHave').withArgumentValue('value-to-have'),
],
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
let actual: IReadOnlyFunctionCallArgumentCollection;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.args;
return '';
};
const context = new ExpressionEvaluationContextStub()
.withArgs(testCase.arguments);
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameters(testCase.expressionParameters)
.build();
// act
sut.evaluate(context);
// assert
const actualArguments = actual.getAllParameterNames().map((name) => actual.getArgument(name));
expect(actualArguments).to.deep.equal(testCase.expectedArguments);
});
}
});
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);
expect(actual.parameters.all);
expect(actual.parameters.all.length).to.equal(0);
});
it('sets as expected', () => {
// arrange
const expected = new FunctionParameterCollectionStub()
.withParameterName('firstParameterName')
.withParameterName('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', () => {
describe('throws with invalid arguments', () => {
const testCases = [
{
name: 'throws if arguments is undefined',
context: undefined,
expectedError: 'undefined context',
},
{
name: 'throws when some of the required args are not provided',
sut: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false),
context: new ExpressionEvaluationContextStub()
.withArgs(new FunctionCallArgumentCollectionStub().withArgument('b', 'provided')),
expectedError: 'argument values are provided for required parameters: "a", "c"',
},
{
name: 'throws when none of the required args are not provided',
sut: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b'], false),
context: new ExpressionEvaluationContextStub()
.withArgs(new FunctionCallArgumentCollectionStub().withArgument('c', 'unrelated')),
expectedError: 'argument values are provided for required parameters: "a", "b"',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// arrange
const sutBuilder = new ExpressionBuilder();
if (testCase.sut) {
testCase.sut(sutBuilder);
}
const sut = sutBuilder.build();
// act
const act = () => sut.evaluate(testCase.context);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
it('returns result from evaluator', () => {
// arrange
const evaluatorMock: ExpressionEvaluator = (c) => `"${c
.args
.getAllParameterNames()
.map((name) => context.args.getArgument(name))
.map((arg) => `${arg.parameterName}': '${arg.argumentValue}'`)
.join('", "')}"`;
const givenArguments = new FunctionCallArgumentCollectionStub()
.withArgument('parameter1', 'value1')
.withArgument('parameter2', 'value2');
const expectedParameterNames = givenArguments.getAllParameterNames();
const context = new ExpressionEvaluationContextStub()
.withArgs(givenArguments);
const expected = evaluatorMock(context);
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameterNames(expectedParameterNames)
.build();
// arrange
const actual = sut.evaluate(context);
// assert
expect(expected).to.equal(actual, printMessage());
function printMessage(): string {
return `\nGiven arguments: ${JSON.stringify(givenArguments)}\n`
+ `\nExpected parameter names: ${JSON.stringify(expectedParameterNames)}\n`;
}
});
it('sends pipeline compiler as it is', () => {
// arrange
const expected = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(expected);
let actual: IPipelineCompiler;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.pipelineCompiler;
return '';
};
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.build();
// arrange
sut.evaluate(context);
// assert
expect(expected).to.equal(actual);
});
describe('filters unused parameters', () => {
// arrange
const testCases = [
{
name: 'with a provided argument',
expressionParameters: new FunctionParameterCollectionStub()
.withParameterName('parameterToHave', false),
arguments: new FunctionCallArgumentCollectionStub()
.withArgument('parameterToHave', 'value-to-have')
.withArgument('parameterToIgnore', 'value-to-ignore'),
expectedArguments: [
new FunctionCallArgumentStub()
.withParameterName('parameterToHave').withArgumentValue('value-to-have'),
],
},
{
name: 'without a provided argument',
expressionParameters: new FunctionParameterCollectionStub()
.withParameterName('parameterToHave', false)
.withParameterName('parameterToIgnore', true),
arguments: new FunctionCallArgumentCollectionStub()
.withArgument('parameterToHave', 'value-to-have'),
expectedArguments: [
new FunctionCallArgumentStub()
.withParameterName('parameterToHave').withArgumentValue('value-to-have'),
],
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
let actual: IReadOnlyFunctionCallArgumentCollection;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.args;
return '';
};
const context = new ExpressionEvaluationContextStub()
.withArgs(testCase.arguments);
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameters(testCase.expressionParameters)
.build();
// act
sut.evaluate(context);
// assert
const actualArguments = actual.getAllParameterNames()
.map((name) => actual.getArgument(name));
expect(actualArguments).to.deep.equal(testCase.expectedArguments);
});
}
});
});
});
class ExpressionBuilder {
private position: ExpressionPosition = new ExpressionPosition(0, 5);
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private position: ExpressionPosition = new ExpressionPosition(0, 5);
public withPosition(position: ExpressionPosition) {
this.position = position;
return this;
}
public withEvaluator(evaluator: ExpressionEvaluator) {
this.evaluator = evaluator;
return this;
}
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;
return this;
}
public withParameterName(parameterName: string, isOptional: boolean = true) {
const collection = new FunctionParameterCollectionStub()
.withParameterName(parameterName, isOptional);
return this.withParameters(collection);
}
public withParameterNames(parameterNames: string[], isOptional: boolean = true) {
const collection = new FunctionParameterCollectionStub()
.withParameterNames(parameterNames, isOptional);
return this.withParameters(collection);
}
public build() {
return new Expression(this.position, this.evaluator, this.parameters);
}
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private evaluator: ExpressionEvaluator = () => '';
public withPosition(position: ExpressionPosition) {
this.position = position;
return this;
}
public withEvaluator(evaluator: ExpressionEvaluator) {
this.evaluator = evaluator;
return this;
}
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;
return this;
}
public withParameterName(parameterName: string, isOptional = true) {
const collection = new FunctionParameterCollectionStub()
.withParameterName(parameterName, isOptional);
return this.withParameters(collection);
}
public withParameterNames(parameterNames: string[], isOptional = true) {
const collection = new FunctionParameterCollectionStub()
.withParameterNames(parameterNames, isOptional);
return this.withParameters(collection);
}
public build() {
return new Expression(this.position, this.evaluator, this.parameters);
}
private evaluator: ExpressionEvaluator = () => '';
}

View File

@@ -6,60 +6,63 @@ import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressi
import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub';
import { PipelineCompilerStub } from '@tests/unit/stubs/PipelineCompilerStub';
describe('ExpressionEvaluationContext', () => {
describe('ctor', () => {
describe('args', () => {
it('throws if args are undefined', () => {
// arrange
const expectedError = 'undefined args';
const builder = new ExpressionEvaluationContextBuilder()
.withArgs(undefined);
// act
const act = () => builder.build();
// assert
expect(act).throw(expectedError);
});
it('sets as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
.withArgument('expectedParameter', 'expectedValue');
const builder = new ExpressionEvaluationContextBuilder()
.withArgs(expected);
// act
const sut = builder.build();
// assert
const actual = sut.args;
expect(actual).to.equal(expected);
});
});
describe('pipelineCompiler', () => {
it('sets as expected', () => {
// arrange
const expected = new PipelineCompilerStub();
const builder = new ExpressionEvaluationContextBuilder()
.withPipelineCompiler(expected);
// act
const sut = builder.build();
// assert
expect(sut.pipelineCompiler).to.equal(expected);
});
});
describe('ctor', () => {
describe('args', () => {
it('throws if args are undefined', () => {
// arrange
const expectedError = 'undefined args';
const builder = new ExpressionEvaluationContextBuilder()
.withArgs(undefined);
// act
const act = () => builder.build();
// assert
expect(act).throw(expectedError);
});
it('sets as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
.withArgument('expectedParameter', 'expectedValue');
const builder = new ExpressionEvaluationContextBuilder()
.withArgs(expected);
// act
const sut = builder.build();
// assert
const actual = sut.args;
expect(actual).to.equal(expected);
});
});
describe('pipelineCompiler', () => {
it('sets as expected', () => {
// arrange
const expected = new PipelineCompilerStub();
const builder = new ExpressionEvaluationContextBuilder()
.withPipelineCompiler(expected);
// act
const sut = builder.build();
// assert
expect(sut.pipelineCompiler).to.equal(expected);
});
});
});
});
class ExpressionEvaluationContextBuilder {
private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub();
private pipelineCompiler: IPipelineCompiler = new PipelineCompilerStub();
public withArgs(args: IReadOnlyFunctionCallArgumentCollection) {
this.args = args;
return this;
}
public withPipelineCompiler(pipelineCompiler: IPipelineCompiler) {
this.pipelineCompiler = pipelineCompiler;
return this;
}
public build(): IExpressionEvaluationContext {
return new ExpressionEvaluationContext(this.args, this.pipelineCompiler);
}
private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub();
private pipelineCompiler: IPipelineCompiler = new PipelineCompilerStub();
public withArgs(args: IReadOnlyFunctionCallArgumentCollection) {
this.args = args;
return this;
}
public withPipelineCompiler(pipelineCompiler: IPipelineCompiler) {
this.pipelineCompiler = pipelineCompiler;
return this;
}
public build(): IExpressionEvaluationContext {
return new ExpressionEvaluationContext(this.args, this.pipelineCompiler);
}
}

View File

@@ -3,32 +3,32 @@ 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);
});
}
});
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

@@ -7,181 +7,181 @@ import { ExpressionParserStub } from '@tests/unit/stubs/ExpressionParserStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub';
describe('ExpressionsCompiler', () => {
describe('compileExpressions', () => {
describe('returns code when it is empty or undefined', () => {
// arrange
const testCases = [{
name: 'empty',
value: '',
}, {
name: 'undefined',
value: undefined,
},
];
for (const test of testCases) {
it(`given ${test.name}`, () => {
const expected = test.value;
const sut = new SystemUnderTest();
const args = new FunctionCallArgumentCollectionStub();
// act
const value = sut.compileExpressions(test.value, args);
// assert
expect(value).to.equal(expected);
});
}
});
describe('combines expressions as expected', () => {
// arrange
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',
},
{
name: 'unordered expressions',
expressions: [
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
],
expected: 'part1 a part2 b part3',
},
{
name: 'with an optional expected argument that is not provided',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a')
.withParameterNames(['optionalParameter'], true),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b')
.withParameterNames(['optionalParameterTwo'], true),
],
expected: 'part1 a part2 b part3',
},
{
name: 'with no expressions',
expressions: [],
expected: code,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const args = new FunctionCallArgumentCollectionStub();
const sut = new SystemUnderTest(expressionParserMock);
// act
const actual = sut.compileExpressions(code, args);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('arguments', () => {
it('passes arguments to expressions as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
.withArgument('test-arg', 'test-value');
const code = 'non-important';
const expressions = [
new ExpressionStub(),
new ExpressionStub(),
];
const expressionParserMock = new ExpressionParserStub()
.withResult(expressions);
const sut = new SystemUnderTest(expressionParserMock);
// act
sut.compileExpressions(code, expected);
// assert
expect(expressions[0].callHistory).to.have.lengthOf(1);
expect(expressions[0].callHistory[0].args).to.equal(expected);
expect(expressions[1].callHistory).to.have.lengthOf(1);
expect(expressions[1].callHistory[0].args).to.equal(expected);
});
it('throws if arguments is undefined', () => {
// arrange
const expectedError = 'undefined args, send empty collection instead';
const args = undefined;
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 expected argument is not provided but used in code', () => {
// arrange
const testCases = [
{
name: 'empty parameters',
expressions: [
new ExpressionStub().withParameterNames(['parameter'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
},
{
name: 'unnecessary parameter is provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter'], false),
],
args: new FunctionCallArgumentCollectionStub()
.withArgument('unnecessaryParameter', 'unnecessaryValue'),
expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
},
{
name: 'multiple values are not provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter1'], false),
new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3" but used in code',
},
{
name: 'some values are provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter1'], false),
new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
],
args: new FunctionCallArgumentCollectionStub()
.withArgument('parameter2', 'value'),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const code = 'non-important-code';
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const sut = new SystemUnderTest(expressionParserMock);
// act
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 SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
sut.compileExpressions(expected, args);
// assert
expect(expressionParserMock.callHistory).to.have.lengthOf(1);
expect(expressionParserMock.callHistory[0]).to.equal(expected);
describe('compileExpressions', () => {
describe('returns code when it is empty or undefined', () => {
// arrange
const testCases = [{
name: 'empty',
value: '',
}, {
name: 'undefined',
value: undefined,
},
];
for (const test of testCases) {
it(`given ${test.name}`, () => {
const expected = test.value;
const sut = new SystemUnderTest();
const args = new FunctionCallArgumentCollectionStub();
// act
const value = sut.compileExpressions(test.value, args);
// assert
expect(value).to.equal(expected);
});
}
});
describe('combines expressions as expected', () => {
// arrange
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',
},
{
name: 'unordered expressions',
expressions: [
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
],
expected: 'part1 a part2 b part3',
},
{
name: 'with an optional expected argument that is not provided',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a')
.withParameterNames(['optionalParameter'], true),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b')
.withParameterNames(['optionalParameterTwo'], true),
],
expected: 'part1 a part2 b part3',
},
{
name: 'with no expressions',
expressions: [],
expected: code,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const args = new FunctionCallArgumentCollectionStub();
const sut = new SystemUnderTest(expressionParserMock);
// act
const actual = sut.compileExpressions(code, args);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('arguments', () => {
it('passes arguments to expressions as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
.withArgument('test-arg', 'test-value');
const code = 'non-important';
const expressions = [
new ExpressionStub(),
new ExpressionStub(),
];
const expressionParserMock = new ExpressionParserStub()
.withResult(expressions);
const sut = new SystemUnderTest(expressionParserMock);
// act
sut.compileExpressions(code, expected);
// assert
expect(expressions[0].callHistory).to.have.lengthOf(1);
expect(expressions[0].callHistory[0].args).to.equal(expected);
expect(expressions[1].callHistory).to.have.lengthOf(1);
expect(expressions[1].callHistory[0].args).to.equal(expected);
});
it('throws if arguments is undefined', () => {
// arrange
const expectedError = 'undefined args, send empty collection instead';
const args = undefined;
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 expected argument is not provided but used in code', () => {
// arrange
const testCases = [
{
name: 'empty parameters',
expressions: [
new ExpressionStub().withParameterNames(['parameter'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
},
{
name: 'unnecessary parameter is provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter'], false),
],
args: new FunctionCallArgumentCollectionStub()
.withArgument('unnecessaryParameter', 'unnecessaryValue'),
expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
},
{
name: 'multiple values are not provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter1'], false),
new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3" but used in code',
},
{
name: 'some values are provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter1'], false),
new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
],
args: new FunctionCallArgumentCollectionStub()
.withArgument('parameter2', 'value'),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const code = 'non-important-code';
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const sut = new SystemUnderTest(expressionParserMock);
// act
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 SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
sut.compileExpressions(expected, args);
// assert
expect(expressionParserMock.callHistory).to.have.lengthOf(1);
expect(expressionParserMock.callHistory[0]).to.equal(expected);
});
});
});
class SystemUnderTest extends ExpressionsCompiler {
constructor(extractor: IExpressionParser = new ExpressionParserStub()) {
super(extractor);
}
constructor(extractor: IExpressionParser = new ExpressionParserStub()) {
super(extractor);
}
}

View File

@@ -6,82 +6,82 @@ import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/
import { ExpressionStub } from '@tests/unit/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('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);
});
}
});
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,
};
return {
findExpressions: () => result,
};
}

View File

@@ -3,133 +3,153 @@ import { expect } from 'chai';
import { ExpressionRegexBuilder } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder';
describe('ExpressionRegexBuilder', () => {
describe('expectCharacters', () => {
describe('escape single as expected', () => {
const charactersToEscape = [ '.', '$' ];
for (const character of charactersToEscape) {
it(character, () => {
runRegExTest(
// act
(act) => act.expectCharacters(character),
// assert
`\\${character}`);
});
}
});
it('escapes multiple as expected', () => {
runRegExTest(
// act
(act) => act.expectCharacters('.I have no $$.'),
// assert
'\\.I have no \\$\\\$\\.');
});
it('adds as expected', () => {
runRegExTest(
// act
(act) => act.expectCharacters('return as it is'),
// assert
'return as it is');
describe('expectCharacters', () => {
describe('escape single as expected', () => {
const charactersToEscape = ['.', '$'];
for (const character of charactersToEscape) {
it(character, () => {
runRegExTest(
// act
(act) => act.expectCharacters(character),
// assert
`\\${character}`,
);
});
}
});
it('expectOneOrMoreWhitespaces', () => {
it('escapes multiple as expected', () => {
runRegExTest(
// act
(act) => act.expectCharacters('.I have no $$.'),
// assert
'\\.I have no \\$\\$\\.',
);
});
it('adds as expected', () => {
runRegExTest(
// act
(act) => act.expectCharacters('return as it is'),
// assert
'return as it is',
);
});
});
it('expectOneOrMoreWhitespaces', () => {
runRegExTest(
// act
(act) => act.expectOneOrMoreWhitespaces(),
// assert
'\\s+',
);
});
it('matchPipeline', () => {
runRegExTest(
// act
(act) => act.matchPipeline(),
// assert
'\\s*(\\|\\s*.+?)?',
);
});
it('matchUntilFirstWhitespace', () => {
runRegExTest(
// act
(act) => act.matchUntilFirstWhitespace(),
// assert
'([^|\\s]+)',
);
});
it('matchAnythingExceptSurroundingWhitespaces', () => {
runRegExTest(
// act
(act) => act.matchAnythingExceptSurroundingWhitespaces(),
// assert
'\\s*(.+?)\\s*',
);
});
it('expectExpressionStart', () => {
runRegExTest(
// act
(act) => act.expectExpressionStart(),
// assert
'{{\\s*',
);
});
it('expectExpressionEnd', () => {
runRegExTest(
// act
(act) => act.expectExpressionEnd(),
// assert
'\\s*}}',
);
});
describe('buildRegExp', () => {
it('sets global flag', () => {
// arrange
const expected = 'g';
const sut = new ExpressionRegexBuilder()
.expectOneOrMoreWhitespaces();
// act
const actual = sut.buildRegExp().flags;
// assert
expect(actual).to.equal(expected);
});
describe('can combine multiple parts', () => {
it('with', () => {
runRegExTest(
// act
(act) => act.expectOneOrMoreWhitespaces(),
// assert
'\\s+');
});
it('matchPipeline', () => {
(sut) => sut
// act
// {{ $with }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace()
.expectExpressionEnd()
// scope
.matchAnythingExceptSurroundingWhitespaces()
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd(),
// assert
'{{\\s*with\\s+\\$([^|\\s]+)\\s*}}\\s*(.+?)\\s*{{\\s*end\\s*}}',
);
});
it('scoped substitution', () => {
runRegExTest(
// act
(act) => act.matchPipeline(),
// assert
'\\s*(\\|\\s*.+?)?');
});
it('matchUntilFirstWhitespace', () => {
(sut) => sut
// act
.expectExpressionStart().expectCharacters('.')
.matchPipeline()
.expectExpressionEnd(),
// assert
'{{\\s*\\.\\s*(\\|\\s*.+?)?\\s*}}',
);
});
it('parameter substitution', () => {
runRegExTest(
// act
(act) => act.matchUntilFirstWhitespace(),
// assert
'([^|\\s]+)');
});
it('matchAnythingExceptSurroundingWhitespaces', () => {
runRegExTest(
// act
(act) => act.matchAnythingExceptSurroundingWhitespaces(),
// assert
'\\s*(.+?)\\s*');
});
it('expectExpressionStart', () => {
runRegExTest(
// act
(act) => act.expectExpressionStart(),
// assert
'{{\\s*');
});
it('expectExpressionEnd', () => {
runRegExTest(
// act
(act) => act.expectExpressionEnd(),
// assert
'\\s*}}');
});
describe('buildRegExp', () => {
it('sets global flag', () => {
// arrange
const expected = 'g';
const sut = new ExpressionRegexBuilder()
.expectOneOrMoreWhitespaces();
// act
const actual = sut.buildRegExp().flags;
// assert
expect(actual).to.equal(expected);
});
describe('can combine multiple parts', () => {
it('with', () => {
runRegExTest((sut) => sut
// act
.expectExpressionStart().expectCharacters('with').expectOneOrMoreWhitespaces().expectCharacters('$')
.matchUntilFirstWhitespace()
.expectExpressionEnd()
.matchAnythingExceptSurroundingWhitespaces()
.expectExpressionStart().expectCharacters('end').expectExpressionEnd(),
// assert
'{{\\s*with\\s+\\$([^|\\s]+)\\s*}}\\s*(.+?)\\s*{{\\s*end\\s*}}',
);
});
it('scoped substitution', () => {
runRegExTest((sut) => sut
// act
.expectExpressionStart().expectCharacters('.')
.matchPipeline()
.expectExpressionEnd(),
// assert
'{{\\s*\\.\\s*(\\|\\s*.+?)?\\s*}}',
);
});
it('parameter substitution', () => {
runRegExTest((sut) => sut
// act
.expectExpressionStart().expectCharacters('$')
.matchUntilFirstWhitespace()
.matchPipeline()
.expectExpressionEnd(),
// assert
'{{\\s*\\$([^|\\s]+)\\s*(\\|\\s*.+?)?\\s*}}',
);
});
});
(sut) => sut
// act
.expectExpressionStart().expectCharacters('$')
.matchUntilFirstWhitespace()
.matchPipeline()
.expectExpressionEnd(),
// assert
'{{\\s*\\$([^|\\s]+)\\s*(\\|\\s*.+?)?\\s*}}',
);
});
});
});
});
function runRegExTest(
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
expected: string,
) {
// arrange
const sut = new ExpressionRegexBuilder();
// act
const actual = act(sut).buildRegExp().source;
// assert
expect(actual).to.equal(expected);
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
expected: string,
) {
// arrange
const sut = new ExpressionRegexBuilder();
// act
const actual = act(sut).buildRegExp().source;
// assert
expect(actual).to.equal(expected);
}

View File

@@ -6,145 +6,148 @@ import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Express
import { FunctionParameterStub } from '@tests/unit/stubs/FunctionParameterStub';
describe('RegexParser', () => {
describe('findExpressions', () => {
describe('throws when code is unexpected', () => {
// arrange
const testCases = [
{
name: 'undefined',
value: undefined,
expectedError: 'undefined code',
},
{
name: 'empty',
value: '',
expectedError: 'undefined code',
},
];
for (const testCase of testCases) {
it(`given ${testCase.name}`, () => {
const sut = new RegexParserConcrete(/unimportant/);
// act
const act = () => sut.findExpressions(testCase.value);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
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 = [
new FunctionParameterStub().withName('parameter1').withOptionality(true),
new FunctionParameterStub().withName('parameter2').withOptionality(false),
];
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.all).to.deep.equal(expected);
});
it('sets expected position', () => {
// arrange
const code = 'mate date in state is fate';
const regex = /ate/g;
const expected = [
new ExpressionPosition(1, 4),
new ExpressionPosition(6, 9),
new ExpressionPosition(15, 18),
new ExpressionPosition(23, 26),
];
const sut = new RegexParserConcrete(regex);
// act
const expressions = sut.findExpressions(code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(expected);
describe('findExpressions', () => {
describe('throws when code is unexpected', () => {
// arrange
const testCases = [
{
name: 'undefined',
value: undefined,
expectedError: 'undefined code',
},
{
name: 'empty',
value: '',
expectedError: 'undefined code',
},
];
for (const testCase of testCases) {
it(`given ${testCase.name}`, () => {
const sut = new RegexParserConcrete(/unimportant/);
// act
const act = () => sut.findExpressions(testCase.value);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
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 = [
new FunctionParameterStub().withName('parameter1').withOptionality(true),
new FunctionParameterStub().withName('parameter2').withOptionality(false),
];
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.all).to.deep.equal(expected);
});
it('sets expected position', () => {
// arrange
const code = 'mate date in state is fate';
const regex = /ate/g;
const expected = [
new ExpressionPosition(1, 4),
new ExpressionPosition(6, 9),
new ExpressionPosition(15, 18),
new ExpressionPosition(23, 26),
];
const sut = new RegexParserConcrete(regex);
// act
const expressions = sut.findExpressions(code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(expected);
});
});
});
function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
return () => ({
evaluator: getEvaluatorStub(),
});
return () => ({
evaluator: getEvaluatorStub(),
});
}
function getEvaluatorStub(): ExpressionEvaluator {
return () => undefined;
return () => undefined;
}
function mockPrimitiveExpression(): IPrimitiveExpression {
return {
evaluator: getEvaluatorStub(),
};
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);
}
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

@@ -1,31 +1,31 @@
import 'mocha';
import { runPipeTests } from './PipeTestRunner';
import { EscapeDoubleQuotes } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes';
import { runPipeTests } from './PipeTestRunner';
describe('EscapeDoubleQuotes', () => {
// arrange
const sut = new EscapeDoubleQuotes();
// act
runPipeTests(sut, [
{
name: 'using "',
input: 'hello "world"',
expectedOutput: 'hello "^""world"^""',
},
{
name: 'not using any double quotes',
input: 'hello world',
expectedOutput: 'hello world',
},
{
name: 'consecutive double quotes',
input: '""hello world""',
expectedOutput: '"^"""^""hello world"^"""^""',
},
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
]);
// arrange
const sut = new EscapeDoubleQuotes();
// act
runPipeTests(sut, [
{
name: 'using "',
input: 'hello "world"',
expectedOutput: 'hello "^""world"^""',
},
{
name: 'not using any double quotes',
input: 'hello world',
expectedOutput: 'hello world',
},
{
name: 'consecutive double quotes',
input: '""hello world""',
expectedOutput: '"^"""^""hello world"^"""^""',
},
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
]);
});

View File

@@ -3,462 +3,461 @@ import { InlinePowerShell } from '@/application/Parser/Script/Compiler/Expressio
import { IPipeTestCase, runPipeTests } from './PipeTestRunner';
describe('InlinePowerShell', () => {
// arrange
const sut = new InlinePowerShell();
// act
runPipeTests(sut, [
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
...prefixTests('newline', getNewLineCases()),
...prefixTests('comment', getCommentCases()),
...prefixTests('here-string', hereStringCases()),
...prefixTests('backtick', backTickCases()),
]);
// arrange
const sut = new InlinePowerShell();
// act
runPipeTests(sut, [
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
...prefixTests('newline', getNewLineCases()),
...prefixTests('comment', getCommentCases()),
...prefixTests('here-string', hereStringCases()),
...prefixTests('backtick', backTickCases()),
]);
});
function hereStringCases(): IPipeTestCase[] {
const expectLinesInDoubleQuotes = (...lines: string[]) => lines.join('`r`n');
const expectLinesInSingleQuotes = (...lines: string[]) => lines.join('\'+"`r`n"+\'');
return [
{
name: 'adds newlines for double quotes',
input: getWindowsLines(
'@"',
'Lorem',
'ipsum',
'dolor sit amet',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'"Lorem',
'ipsum',
'dolor sit amet"',
),
},
{
name: 'adds newlines for single quotes',
input: getWindowsLines(
'@\'',
'Lorem',
'ipsum',
'dolor sit amet',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'\'Lorem',
'ipsum',
'dolor sit amet\'',
),
},
{
name: 'does not match with character after here string header',
input: getWindowsLines(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
expectedOutput: getSingleLinedOutput(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
},
{
name: 'does not match if there\'s character before here-string terminator',
input: getWindowsLines(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
expectedOutput: getSingleLinedOutput(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
},
{
name: 'does not match with different here-string header/terminator',
input: getWindowsLines(
'@\'',
'lorem',
'"@',
),
expectedOutput: getSingleLinedOutput(
'@\'',
'lorem',
'"@',
),
},
{
name: 'matches with inner single quoted here-string',
input: getWindowsLines(
'$hasInnerDoubleQuotedTerminator = @"',
'inner text',
'@\'',
'inner terminator text',
'\'@',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'$hasInnerDoubleQuotedTerminator = "inner text',
'@\'',
'inner terminator text',
'\'@"',
),
},
{
name: 'matches with inner double quoted string',
input: getWindowsLines(
'$hasInnerSingleQuotedTerminator = @\'',
'inner text',
'@"',
'inner terminator text',
'"@',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'$hasInnerSingleQuotedTerminator = \'inner text',
'@"',
'inner terminator text',
'"@\'',
),
},
{
name: 'matches if there\'s character after here-string terminator',
input: getWindowsLines(
'@\'',
'lorem',
'\'@ after',
),
expectedOutput: expectLinesInSingleQuotes(
'\'lorem\' after',
),
},
{
name: 'escapes double quotes inside double quotes',
input: getWindowsLines(
'@"',
'For help, type "get-help"',
'"@',
),
expectedOutput: '"For help, type `"get-help`""',
},
{
name: 'escapes single quotes inside single quotes',
input: getWindowsLines(
'@\'',
'For help, type \'get-help\'',
'\'@',
),
expectedOutput: '\'For help, type \'\'get-help\'\'\'',
},
{
name: 'converts when here-string header is not at line start',
input: getWindowsLines(
'$page = [XML] @"',
'multi-lined',
'and "quoted"',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'$page = [XML] "multi-lined',
'and `"quoted`""',
),
},
{
name: 'trims after here-string header',
input: getWindowsLines(
'@" \t',
'text with whitespaces at here-string start',
'"@',
),
expectedOutput: '"text with whitespaces at here-string start"',
},
{
name: 'preserves whitespaces in lines',
input: getWindowsLines(
'@\'',
'\ttext with tabs around\t\t',
' text with whitespaces around ',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'\'\ttext with tabs around\t\t',
' text with whitespaces around \'',
),
},
];
const expectLinesInDoubleQuotes = (...lines: string[]) => lines.join('`r`n');
const expectLinesInSingleQuotes = (...lines: string[]) => lines.join('\'+"`r`n"+\'');
return [
{
name: 'adds newlines for double quotes',
input: getWindowsLines(
'@"',
'Lorem',
'ipsum',
'dolor sit amet',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'"Lorem',
'ipsum',
'dolor sit amet"',
),
},
{
name: 'adds newlines for single quotes',
input: getWindowsLines(
'@\'',
'Lorem',
'ipsum',
'dolor sit amet',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'\'Lorem',
'ipsum',
'dolor sit amet\'',
),
},
{
name: 'does not match with character after here string header',
input: getWindowsLines(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
expectedOutput: getSingleLinedOutput(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
},
{
name: 'does not match if there\'s character before here-string terminator',
input: getWindowsLines(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
expectedOutput: getSingleLinedOutput(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
},
{
name: 'does not match with different here-string header/terminator',
input: getWindowsLines(
'@\'',
'lorem',
'"@',
),
expectedOutput: getSingleLinedOutput(
'@\'',
'lorem',
'"@',
),
},
{
name: 'matches with inner single quoted here-string',
input: getWindowsLines(
'$hasInnerDoubleQuotedTerminator = @"',
'inner text',
'@\'',
'inner terminator text',
'\'@',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'$hasInnerDoubleQuotedTerminator = "inner text',
'@\'',
'inner terminator text',
'\'@"',
),
},
{
name: 'matches with inner double quoted string',
input: getWindowsLines(
'$hasInnerSingleQuotedTerminator = @\'',
'inner text',
'@"',
'inner terminator text',
'"@',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'$hasInnerSingleQuotedTerminator = \'inner text',
'@"',
'inner terminator text',
'"@\'',
),
},
{
name: 'matches if there\'s character after here-string terminator',
input: getWindowsLines(
'@\'',
'lorem',
'\'@ after',
),
expectedOutput: expectLinesInSingleQuotes(
'\'lorem\' after',
),
},
{
name: 'escapes double quotes inside double quotes',
input: getWindowsLines(
'@"',
'For help, type "get-help"',
'"@',
),
expectedOutput: '"For help, type `"get-help`""',
},
{
name: 'escapes single quotes inside single quotes',
input: getWindowsLines(
'@\'',
'For help, type \'get-help\'',
'\'@',
),
expectedOutput: '\'For help, type \'\'get-help\'\'\'',
},
{
name: 'converts when here-string header is not at line start',
input: getWindowsLines(
'$page = [XML] @"',
'multi-lined',
'and "quoted"',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'$page = [XML] "multi-lined',
'and `"quoted`""',
),
},
{
name: 'trims after here-string header',
input: getWindowsLines(
'@" \t',
'text with whitespaces at here-string start',
'"@',
),
expectedOutput: '"text with whitespaces at here-string start"',
},
{
name: 'preserves whitespaces in lines',
input: getWindowsLines(
'@\'',
'\ttext with tabs around\t\t',
' text with whitespaces around ',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'\'\ttext with tabs around\t\t',
' text with whitespaces around \'',
),
},
];
}
function backTickCases(): IPipeTestCase[] {
return [
{
name: 'wraps newlines with trailing backtick',
input: getWindowsLines(
'Get-Service * `',
'| Format-Table -AutoSize',
),
expectedOutput: 'Get-Service * | Format-Table -AutoSize',
},
{
name: 'wraps newlines with trailing backtick and different line endings',
input: 'Get-Service `\n'
+ '* `\r'
+ '| Sort-Object StartType `\r\n'
+ '| Format-Table -AutoSize'
,
expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
},
{
name: 'trims tabs and whitespaces on next lines when wrapping with trailing backtick',
input: getWindowsLines(
'Get-Service * `',
'\t| Sort-Object StartType `',
' | Format-Table -AutoSize',
),
expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
},
{
name: 'does not wrap without whitespace before backtick',
input: getWindowsLines(
'Get-Service *`',
'| Format-Table -AutoSize',
),
expectedOutput: getSingleLinedOutput(
'Get-Service *`',
'| Format-Table -AutoSize',
),
},
{
name: 'does not wrap with characters after',
input: getWindowsLines(
'line start ` after',
'should not be wrapped',
),
expectedOutput: getSingleLinedOutput(
'line start ` after',
'should not be wrapped',
),
},
];
return [
{
name: 'wraps newlines with trailing backtick',
input: getWindowsLines(
'Get-Service * `',
'| Format-Table -AutoSize',
),
expectedOutput: 'Get-Service * | Format-Table -AutoSize',
},
{
name: 'wraps newlines with trailing backtick and different line endings',
input: 'Get-Service `\n'
+ '* `\r'
+ '| Sort-Object StartType `\r\n'
+ '| Format-Table -AutoSize',
expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
},
{
name: 'trims tabs and whitespaces on next lines when wrapping with trailing backtick',
input: getWindowsLines(
'Get-Service * `',
'\t| Sort-Object StartType `',
' | Format-Table -AutoSize',
),
expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
},
{
name: 'does not wrap without whitespace before backtick',
input: getWindowsLines(
'Get-Service *`',
'| Format-Table -AutoSize',
),
expectedOutput: getSingleLinedOutput(
'Get-Service *`',
'| Format-Table -AutoSize',
),
},
{
name: 'does not wrap with characters after',
input: getWindowsLines(
'line start ` after',
'should not be wrapped',
),
expectedOutput: getSingleLinedOutput(
'line start ` after',
'should not be wrapped',
),
},
];
}
function getCommentCases(): IPipeTestCase[] {
return [
{
name: 'converts hash comments in the line end',
input: getWindowsLines(
'$text = "Hello"\t# Comment after tab',
'$text+= #Comment without space after hash',
'Write-Host $text# Comment without space before hash',
),
expectedOutput: getSingleLinedOutput(
'$text = "Hello"\t<# Comment after tab #>',
'$text+= <# Comment without space after hash #>',
'Write-Host $text<# Comment without space before hash #>',
),
},
{
name: 'converts hash comment line',
input: getWindowsLines(
'# Comment in first line',
'Write-Host "Hello"',
'# Comment in the middle',
'Write-Host "World"',
'# Consecutive comments',
'# Last line comment without line ending in the end',
),
expectedOutput: getSingleLinedOutput(
'<# Comment in first line #>',
'Write-Host "Hello"',
'<# Comment in the middle #>',
'Write-Host "World"',
'<# Consecutive comments #>',
'<# Last line comment without line ending in the end #>',
),
},
{
name: 'can convert comment with inline comment parts inside',
input: getWindowsLines(
'$text+= #Comment with < inside',
'$text+= #Comment ending with >',
'$text+= #Comment with <# inline comment #>',
),
expectedOutput: getSingleLinedOutput(
'$text+= <# Comment with < inside #>',
'$text+= <# Comment ending with > #>',
'$text+= <# Comment with <# inline comment #> #>',
),
},
{
name: 'can convert comment with inline comment parts around', // Pretty uncommon
input: getWindowsLines(
'Write-Host "hi" # Comment ending line inline comment but not one #>',
'Write-Host "hi" #>Comment starting like inline comment end but not one',
// Following line does not compile as valid PowerShell referring to missing #> for inline comment
'Write-Host "hi" <#Comment starting like inline comment start but not one',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "hi" <# Comment ending line inline comment but not one #> #>',
'Write-Host "hi" <# >Comment starting like inline comment end but not one #>',
'Write-Host "hi" <<# Comment starting like inline comment start but not one #>',
),
},
{
name: 'converts empty hash comment',
input: getWindowsLines(
'Write-Host "Comment without text" #',
'Write-Host "Non-empty line"',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "Comment without text" <##>',
'Write-Host "Non-empty line"',
),
},
{
name: 'adds whitespaces around to match',
input: getWindowsLines(
'#Comment line with no whitespaces around',
'Write-Host "Hello"#Comment in the end with no whitespaces around',
),
expectedOutput: getSingleLinedOutput(
'<# Comment line with no whitespaces around #>',
'Write-Host "Hello"<# Comment in the end with no whitespaces around #>',
),
},
{
name: 'trims whitespaces around comment',
input: getWindowsLines(
'# Comment with whitespaces around ',
'#\tComment with tabs around\t\t',
'#\t Comment with tabs and whitespaces around \t \t',
),
expectedOutput: getSingleLinedOutput(
'<# Comment with whitespaces around #>',
'<# Comment with tabs around #>',
'<# Comment with tabs and whitespaces around #>',
),
},
{
name: 'does not convert block comments',
input: getWindowsLines(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
expectedOutput: getSingleLinedOutput(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
},
{
name: 'does not process if there are no multi lines',
input: 'Write-Host \"expected\" # as it is!',
expectedOutput: 'Write-Host \"expected\" # as it is!',
},
];
return [
{
name: 'converts hash comments in the line end',
input: getWindowsLines(
'$text = "Hello"\t# Comment after tab',
'$text+= #Comment without space after hash',
'Write-Host $text# Comment without space before hash',
),
expectedOutput: getSingleLinedOutput(
'$text = "Hello"\t<# Comment after tab #>',
'$text+= <# Comment without space after hash #>',
'Write-Host $text<# Comment without space before hash #>',
),
},
{
name: 'converts hash comment line',
input: getWindowsLines(
'# Comment in first line',
'Write-Host "Hello"',
'# Comment in the middle',
'Write-Host "World"',
'# Consecutive comments',
'# Last line comment without line ending in the end',
),
expectedOutput: getSingleLinedOutput(
'<# Comment in first line #>',
'Write-Host "Hello"',
'<# Comment in the middle #>',
'Write-Host "World"',
'<# Consecutive comments #>',
'<# Last line comment without line ending in the end #>',
),
},
{
name: 'can convert comment with inline comment parts inside',
input: getWindowsLines(
'$text+= #Comment with < inside',
'$text+= #Comment ending with >',
'$text+= #Comment with <# inline comment #>',
),
expectedOutput: getSingleLinedOutput(
'$text+= <# Comment with < inside #>',
'$text+= <# Comment ending with > #>',
'$text+= <# Comment with <# inline comment #> #>',
),
},
{
name: 'can convert comment with inline comment parts around', // Pretty uncommon
input: getWindowsLines(
'Write-Host "hi" # Comment ending line inline comment but not one #>',
'Write-Host "hi" #>Comment starting like inline comment end but not one',
// Following line does not compile as valid PowerShell due to missing #> for inline comment.
'Write-Host "hi" <#Comment starting like inline comment start but not one',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "hi" <# Comment ending line inline comment but not one #> #>',
'Write-Host "hi" <# >Comment starting like inline comment end but not one #>',
'Write-Host "hi" <<# Comment starting like inline comment start but not one #>',
),
},
{
name: 'converts empty hash comment',
input: getWindowsLines(
'Write-Host "Comment without text" #',
'Write-Host "Non-empty line"',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "Comment without text" <##>',
'Write-Host "Non-empty line"',
),
},
{
name: 'adds whitespaces around to match',
input: getWindowsLines(
'#Comment line with no whitespaces around',
'Write-Host "Hello"#Comment in the end with no whitespaces around',
),
expectedOutput: getSingleLinedOutput(
'<# Comment line with no whitespaces around #>',
'Write-Host "Hello"<# Comment in the end with no whitespaces around #>',
),
},
{
name: 'trims whitespaces around comment',
input: getWindowsLines(
'# Comment with whitespaces around ',
'#\tComment with tabs around\t\t',
'#\t Comment with tabs and whitespaces around \t \t',
),
expectedOutput: getSingleLinedOutput(
'<# Comment with whitespaces around #>',
'<# Comment with tabs around #>',
'<# Comment with tabs and whitespaces around #>',
),
},
{
name: 'does not convert block comments',
input: getWindowsLines(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
expectedOutput: getSingleLinedOutput(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
},
{
name: 'does not process if there are no multi lines',
input: 'Write-Host "expected" # as it is!',
expectedOutput: 'Write-Host "expected" # as it is!',
},
];
}
function getNewLineCases(): IPipeTestCase[] {
return [
{
name: 'no new line',
input: 'Write-Host \'Hello, World!\'',
expectedOutput: 'Write-Host \'Hello, World!\'',
},
{
name: '\\n new line',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\nforeach ($thing in $things) {'
+ '\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\n double empty lines are ignored',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\n\nforeach ($thing in $things) {'
+ '\n\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\n\n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\r new line',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\rforeach ($thing in $things) {'
+ '\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\r and \\n newlines combined',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\r\nforeach ($thing in $things) {'
+ '\n\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\r}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: 'trims whitespaces on lines',
input:
' $things = Get-ChildItem C:\\Windows\\ '
+ '\nforeach ($thing in $things) {'
+ '\n\tWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r \n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
];
return [
{
name: 'no new line',
input: 'Write-Host \'Hello, World!\'',
expectedOutput: 'Write-Host \'Hello, World!\'',
},
{
name: '\\n new line',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\nforeach ($thing in $things) {'
+ '\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\n double empty lines are ignored',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\n\nforeach ($thing in $things) {'
+ '\n\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\n\n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\r new line',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\rforeach ($thing in $things) {'
+ '\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\r and \\n newlines combined',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\r\nforeach ($thing in $things) {'
+ '\n\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\r}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: 'trims whitespaces on lines',
input:
' $things = Get-ChildItem C:\\Windows\\ '
+ '\nforeach ($thing in $things) {'
+ '\n\tWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r \n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
];
}
function prefixTests(prefix: string, tests: IPipeTestCase[]): IPipeTestCase[] {
return tests.map((test) => ({
name: `[${prefix}] ${test.name}`,
input: test.input,
expectedOutput: test.expectedOutput,
}));
return tests.map((test) => ({
name: `[${prefix}] ${test.name}`,
input: test.input,
expectedOutput: test.expectedOutput,
}));
}
function getWindowsLines(...lines: string[]) {
return lines.join('\r\n');
return lines.join('\r\n');
}
function getSingleLinedOutput(...lines: string[]) {
return lines.map((line) => line.trim()).join('; ');
return lines.map((line) => line.trim()).join('; ');
}

View File

@@ -3,18 +3,18 @@ import { expect } from 'chai';
import { IPipe } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipe';
export interface IPipeTestCase {
readonly name: string;
readonly input: string;
readonly expectedOutput: string;
readonly name: string;
readonly input: string;
readonly expectedOutput: string;
}
export function runPipeTests(sut: IPipe, testCases: IPipeTestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const actual = sut.apply(testCase.input);
// assert
expect(actual).to.equal(testCase.expectedOutput);
});
}
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const actual = sut.apply(testCase.input);
// assert
expect(actual).to.equal(testCase.expectedOutput);
});
}
}

View File

@@ -4,110 +4,110 @@ import { PipeFactory } from '@/application/Parser/Script/Compiler/Expressions/Pi
import { PipeStub } from '@tests/unit/stubs/PipeStub';
describe('PipeFactory', () => {
describe('ctor', () => {
it('throws when instances with same name is registered', () => {
// arrange
const duplicateName = 'duplicateName';
const expectedError = `Pipe name must be unique: "${duplicateName}"`;
const pipes = [
new PipeStub().withName(duplicateName),
new PipeStub().withName('uniqueName'),
new PipeStub().withName(duplicateName),
];
// act
const act = () => new PipeFactory(pipes);
// expect
expect(act).to.throw(expectedError);
});
it('throws when a pipe is undefined', () => {
// arrange
const expectedError = 'undefined pipe in list';
const pipes = [ new PipeStub(), undefined ];
// 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) ]);
// assert
testPipeNameValidation(act);
});
describe('ctor', () => {
it('throws when instances with same name is registered', () => {
// arrange
const duplicateName = 'duplicateName';
const expectedError = `Pipe name must be unique: "${duplicateName}"`;
const pipes = [
new PipeStub().withName(duplicateName),
new PipeStub().withName('uniqueName'),
new PipeStub().withName(duplicateName),
];
// act
const act = () => new PipeFactory(pipes);
// expect
expect(act).to.throw(expectedError);
});
describe('get', () => {
describe('throws when name is invalid', () => {
// arrange
const sut = new PipeFactory();
// act
const act = (invalidName: string) => sut.get(invalidName);
// assert
testPipeNameValidation(act);
});
it('gets registered instance when it exists', () => {
// arrange
const expected = new PipeStub().withName('expectedName');
const pipes = [ expected, new PipeStub().withName('instanceToConfuse') ];
const sut = new PipeFactory(pipes);
// act
const actual = sut.get(expected.name);
// expect
expect(actual).to.equal(expected);
});
it('throws when instance does not exist', () => {
// arrange
const missingName = 'missingName';
const expectedError = `Unknown pipe: "${missingName}"`;
const pipes = [ ];
const sut = new PipeFactory(pipes);
// act
const act = () => sut.get(missingName);
// expect
expect(act).to.throw(expectedError);
});
it('throws when a pipe is undefined', () => {
// arrange
const expectedError = 'undefined pipe in list';
const pipes = [new PipeStub(), undefined];
// 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)]);
// assert
testPipeNameValidation(act);
});
});
describe('get', () => {
describe('throws when name is invalid', () => {
// arrange
const sut = new PipeFactory();
// act
const act = (invalidName: string) => sut.get(invalidName);
// assert
testPipeNameValidation(act);
});
it('gets registered instance when it exists', () => {
// arrange
const expected = new PipeStub().withName('expectedName');
const pipes = [expected, new PipeStub().withName('instanceToConfuse')];
const sut = new PipeFactory(pipes);
// act
const actual = sut.get(expected.name);
// expect
expect(actual).to.equal(expected);
});
it('throws when instance does not exist', () => {
// arrange
const missingName = 'missingName';
const expectedError = `Unknown pipe: "${missingName}"`;
const pipes = [];
const sut = new PipeFactory(pipes);
// act
const act = () => sut.get(missingName);
// expect
expect(act).to.throw(expectedError);
});
});
});
function testPipeNameValidation(testRunner: (invalidName: string) => void) {
const testCases = [
{
exceptionBuilder: () => 'empty pipe name',
values: [ null, undefined , ''],
},
{
exceptionBuilder: (name: string) => `Pipe name should be camelCase: "${name}"`,
values: [
'PascalCase',
'snake-case',
'includesNumb3rs',
'includes Whitespace',
'noSpec\'ial',
],
},
];
for (const testCase of testCases) {
for (const invalidName of testCase.values) {
it(`invalid name (${printValue(invalidName)}) throws`, () => {
// arrange
const expectedError = testCase.exceptionBuilder(invalidName);
// act
const act = () => testRunner(invalidName);
// expect
expect(act).to.throw(expectedError);
});
}
const testCases = [
{
exceptionBuilder: () => 'empty pipe name',
values: [null, undefined, ''],
},
{
exceptionBuilder: (name: string) => `Pipe name should be camelCase: "${name}"`,
values: [
'PascalCase',
'snake-case',
'includesNumb3rs',
'includes Whitespace',
'noSpec\'ial',
],
},
];
for (const testCase of testCases) {
for (const invalidName of testCase.values) {
it(`invalid name (${printValue(invalidName)}) throws`, () => {
// arrange
const expectedError = testCase.exceptionBuilder(invalidName);
// act
const act = () => testRunner(invalidName);
// expect
expect(act).to.throw(expectedError);
});
}
}
}
function printValue(value: string) {
switch (value) {
case undefined:
return 'undefined';
case null:
return 'null';
case '':
return 'empty';
default:
return value;
}
switch (value) {
case undefined:
return 'undefined';
case null:
return 'null';
case '':
return 'empty';
default:
return value;
}
}

View File

@@ -7,132 +7,134 @@ import { PipeStub } from '@tests/unit/stubs/PipeStub';
import { PipeFactoryStub } from '@tests/unit/stubs/PipeFactoryStub';
describe('PipelineCompiler', () => {
describe('compile', () => {
describe('throws for invalid arguments', () => {
interface ITestCase {
name: string;
act: (test: PipelineTestRunner) => PipelineTestRunner;
expectedError: string;
}
const testCases: ITestCase[] = [
{
name: '"value" is empty',
act: (test) => test.withValue(''),
expectedError: 'undefined value',
},
{
name: '"value" is undefined',
act: (test) => test.withValue(undefined),
expectedError: 'undefined value',
},
{
name: '"pipeline" is empty',
act: (test) => test.withPipeline(''),
expectedError: 'undefined pipeline',
},
{
name: '"pipeline" is undefined',
act: (test) => test.withPipeline(undefined),
expectedError: 'undefined pipeline',
},
{
name: '"pipeline" does not start with pipe',
act: (test) => test.withPipeline('pipeline |'),
expectedError: 'pipeline does not start with pipe',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const runner = new PipelineTestRunner();
testCase.act(runner);
const act = () => runner.compile();
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
describe('compiles pipeline as expected', () => {
const testCases = [
{
name: 'compiles single pipe as expected',
pipes: [
new PipeStub().withName('doublePrint').withApplier((value) => `${value}-${value}`),
],
pipeline: '| doublePrint',
value: 'value',
expected: 'value-value',
},
{
name: 'compiles multiple pipes as expected',
pipes: [
new PipeStub().withName('prependLetterA').withApplier((value) => `A-${value}`),
new PipeStub().withName('prependLetterB').withApplier((value) => `B-${value}`),
],
pipeline: '| prependLetterA | prependLetterB',
value: 'value',
expected: 'B-A-value',
},
{
name: 'compiles with relaxed whitespace placing',
pipes: [
new PipeStub().withName('appendNumberOne').withApplier((value) => `${value}1`),
new PipeStub().withName('appendNumberTwo').withApplier((value) => `${value}2`),
new PipeStub().withName('appendNumberThree').withApplier((value) => `${value}3`),
],
pipeline: ' | appendNumberOne|appendNumberTwo| appendNumberThree',
value: 'value',
expected: 'value123',
},
{
name: 'can reuse same pipe',
pipes: [
new PipeStub().withName('removeFirstChar').withApplier((value) => `${value.slice(1)}`),
],
pipeline: ' | removeFirstChar | removeFirstChar | removeFirstChar',
value: 'value',
expected: 'ue',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// arrange
const runner =
new PipelineTestRunner()
.withValue(testCase.value)
.withPipeline(testCase.pipeline)
.withFactory(new PipeFactoryStub().withPipes(testCase.pipes));
// act
const actual = runner.compile();
// expect
expect(actual).to.equal(testCase.expected);
});
}
describe('compile', () => {
describe('throws for invalid arguments', () => {
interface ITestCase {
name: string;
act: (test: PipelineTestRunner) => PipelineTestRunner;
expectedError: string;
}
const testCases: ITestCase[] = [
{
name: '"value" is empty',
act: (test) => test.withValue(''),
expectedError: 'undefined value',
},
{
name: '"value" is undefined',
act: (test) => test.withValue(undefined),
expectedError: 'undefined value',
},
{
name: '"pipeline" is empty',
act: (test) => test.withPipeline(''),
expectedError: 'undefined pipeline',
},
{
name: '"pipeline" is undefined',
act: (test) => test.withPipeline(undefined),
expectedError: 'undefined pipeline',
},
{
name: '"pipeline" does not start with pipe',
act: (test) => test.withPipeline('pipeline |'),
expectedError: 'pipeline does not start with pipe',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const runner = new PipelineTestRunner();
testCase.act(runner);
const act = () => runner.compile();
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
describe('compiles pipeline as expected', () => {
const testCases = [
{
name: 'compiles single pipe as expected',
pipes: [
new PipeStub().withName('doublePrint').withApplier((value) => `${value}-${value}`),
],
pipeline: '| doublePrint',
value: 'value',
expected: 'value-value',
},
{
name: 'compiles multiple pipes as expected',
pipes: [
new PipeStub().withName('prependLetterA').withApplier((value) => `A-${value}`),
new PipeStub().withName('prependLetterB').withApplier((value) => `B-${value}`),
],
pipeline: '| prependLetterA | prependLetterB',
value: 'value',
expected: 'B-A-value',
},
{
name: 'compiles with relaxed whitespace placing',
pipes: [
new PipeStub().withName('appendNumberOne').withApplier((value) => `${value}1`),
new PipeStub().withName('appendNumberTwo').withApplier((value) => `${value}2`),
new PipeStub().withName('appendNumberThree').withApplier((value) => `${value}3`),
],
pipeline: ' | appendNumberOne|appendNumberTwo| appendNumberThree',
value: 'value',
expected: 'value123',
},
{
name: 'can reuse same pipe',
pipes: [
new PipeStub().withName('removeFirstChar').withApplier((value) => `${value.slice(1)}`),
],
pipeline: ' | removeFirstChar | removeFirstChar | removeFirstChar',
value: 'value',
expected: 'ue',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// arrange
const runner = new PipelineTestRunner()
.withValue(testCase.value)
.withPipeline(testCase.pipeline)
.withFactory(new PipeFactoryStub().withPipes(testCase.pipes));
// act
const actual = runner.compile();
// expect
expect(actual).to.equal(testCase.expected);
});
}
});
});
});
class PipelineTestRunner implements IPipelineCompiler {
private value: string = 'non-empty-value';
private pipeline: string = '| validPipeline';
private factory: IPipeFactory = new PipeFactoryStub();
private value = 'non-empty-value';
public withValue(value: string) {
this.value = value;
return this;
}
public withPipeline(pipeline: string) {
this.pipeline = pipeline;
return this;
}
public withFactory(factory: IPipeFactory) {
this.factory = factory;
return this;
}
private pipeline = '| validPipeline';
public compile(): string {
const sut = new PipelineCompiler(this.factory);
return sut.compile(this.value, this.pipeline);
}
private factory: IPipeFactory = new PipeFactoryStub();
public withValue(value: string) {
this.value = value;
return this;
}
public withPipeline(pipeline: string) {
this.pipeline = pipeline;
return this;
}
public withFactory(factory: IPipeFactory) {
this.factory = factory;
return this;
}
public compile(): string {
const sut = new PipelineCompiler(this.factory);
return sut.compile(this.value, this.pipeline);
}
}

View File

@@ -4,64 +4,64 @@ import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Express
import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner';
describe('ParameterSubstitutionParser', () => {
const sut = new ParameterSubstitutionParser();
const runner = new SyntaxParserTestsRunner(sut);
describe('finds as expected', () => {
runner.expectPosition(
{
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) ],
},
{
name: 'tolerates lack of spaces around brackets',
code: 'He{{$firstParameter}}!!',
expected: [new ExpressionPosition(2, 21) ],
},
{
name: 'does not tolerate space after dollar sign',
code: 'He{{ $ firstParameter }}!!',
expected: [ ],
},
);
});
describe('evaluates as expected', () => {
runner.expectResults(
{
name: 'single parameter',
code: '{{ $parameter }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: [ 'Hello world' ],
},
{
name: 'different parameters',
code: '{{ $firstParameter }} {{ $secondParameter }}!',
args: (args) => args
.withArgument('firstParameter', 'Hello')
.withArgument('secondParameter', 'World'),
expected: [ 'Hello', 'World' ],
},
{
name: 'same parameters used twice',
code: '{{ $letterH }}e{{ $letterL }}{{ $letterL }}o Wor{{ $letterL }}d!',
args: (args) => args
.withArgument('letterL', 'l')
.withArgument('letterH', 'H'),
expected: [ 'H', 'l', 'l', 'l' ],
},
);
});
describe('compiles pipes as expected', () => {
runner.expectPipeHits({
codeBuilder: (pipeline) => `{{ $argument${pipeline}}}`,
parameterName: 'argument',
parameterValue: 'value',
});
const sut = new ParameterSubstitutionParser();
const runner = new SyntaxParserTestsRunner(sut);
describe('finds as expected', () => {
runner.expectPosition(
{
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)],
},
{
name: 'tolerates lack of spaces around brackets',
code: 'He{{$firstParameter}}!!',
expected: [new ExpressionPosition(2, 21)],
},
{
name: 'does not tolerate space after dollar sign',
code: 'He{{ $ firstParameter }}!!',
expected: [],
},
);
});
describe('evaluates as expected', () => {
runner.expectResults(
{
name: 'single parameter',
code: '{{ $parameter }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: ['Hello world'],
},
{
name: 'different parameters',
code: '{{ $firstParameter }} {{ $secondParameter }}!',
args: (args) => args
.withArgument('firstParameter', 'Hello')
.withArgument('secondParameter', 'World'),
expected: ['Hello', 'World'],
},
{
name: 'same parameters used twice',
code: '{{ $letterH }}e{{ $letterL }}{{ $letterL }}o Wor{{ $letterL }}d!',
args: (args) => args
.withArgument('letterL', 'l')
.withArgument('letterH', 'H'),
expected: ['H', 'l', 'l', 'l'],
},
);
});
describe('compiles pipes as expected', () => {
runner.expectPipeHits({
codeBuilder: (pipeline) => `{{ $argument${pipeline}}}`,
parameterName: 'argument',
parameterValue: 'value',
});
});
});

View File

@@ -7,116 +7,121 @@ import { ExpressionEvaluationContextStub } from '@tests/unit/stubs/ExpressionEva
import { PipelineCompilerStub } from '@tests/unit/stubs/PipelineCompilerStub';
export class SyntaxParserTestsRunner {
constructor(private readonly sut: IExpressionParser) {
constructor(private readonly sut: IExpressionParser) {
}
public expectPosition(...testCases: IExpectPositionTestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const expressions = this.sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(testCase.expected);
});
}
public expectPosition(...testCases: IExpectPositionTestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const expressions = this.sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(testCase.expected);
});
}
return this;
return this;
}
public expectResults(...testCases: IExpectResultTestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
// arrange
const args = testCase.args(new FunctionCallArgumentCollectionStub());
const context = new ExpressionEvaluationContextStub()
.withArgs(args);
// act
const expressions = this.sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.evaluate(context));
expect(actual).to.deep.equal(testCase.expected);
});
}
public expectResults(...testCases: IExpectResultTestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
// arrange
const args = testCase.args(new FunctionCallArgumentCollectionStub());
const context = new ExpressionEvaluationContextStub()
.withArgs(args);
// act
const expressions = this.sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.evaluate(context));
expect(actual).to.deep.equal(testCase.expected);
});
}
return this;
return this;
}
public expectPipeHits(data: IExpectPipeHitTestData) {
for (const validPipePart of PipeTestCases.ValidValues) {
this.expectHitPipePart(validPipePart, data);
}
public expectPipeHits(data: IExpectPipeHitTestData) {
for (const validPipePart of PipeTestCases.ValidValues) {
this.expectHitPipePart(validPipePart, data);
}
for (const invalidPipePart of PipeTestCases.InvalidValues) {
this.expectMissPipePart(invalidPipePart, data);
}
}
private expectHitPipePart(pipeline: string, data: IExpectPipeHitTestData) {
it(`"${pipeline}" hits`, () => {
// arrange
const expectedPipePart = pipeline.trim();
const code = data.codeBuilder(pipeline);
const args = new FunctionCallArgumentCollectionStub()
.withArgument(data.parameterName, data.parameterValue);
const pipelineCompiler = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(pipelineCompiler)
.withArgs(args);
// act
const expressions = this.sut.findExpressions(code);
expressions[0].evaluate(context);
// assert
expect(expressions).has.lengthOf(1);
expect(pipelineCompiler.compileHistory).has.lengthOf(1);
const actualPipeNames = pipelineCompiler.compileHistory[0].pipeline;
const actualValue = pipelineCompiler.compileHistory[0].value;
expect(actualPipeNames).to.equal(expectedPipePart);
expect(actualValue).to.equal(data.parameterValue);
});
}
private expectMissPipePart(pipeline: string, data: IExpectPipeHitTestData) {
it(`"${pipeline}" misses`, () => {
// arrange
const args = new FunctionCallArgumentCollectionStub()
.withArgument(data.parameterName, data.parameterValue);
const pipelineCompiler = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(pipelineCompiler)
.withArgs(args);
const code = data.codeBuilder(pipeline);
// act
const expressions = this.sut.findExpressions(code);
expressions[0]?.evaluate(context); // Because an expression may include another with pipes
// assert
expect(pipelineCompiler.compileHistory).has.lengthOf(0);
});
for (const invalidPipePart of PipeTestCases.InvalidValues) {
this.expectMissPipePart(invalidPipePart, data);
}
}
private expectHitPipePart(pipeline: string, data: IExpectPipeHitTestData) {
it(`"${pipeline}" hits`, () => {
// arrange
const expectedPipePart = pipeline.trim();
const code = data.codeBuilder(pipeline);
const args = new FunctionCallArgumentCollectionStub()
.withArgument(data.parameterName, data.parameterValue);
const pipelineCompiler = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(pipelineCompiler)
.withArgs(args);
// act
const expressions = this.sut.findExpressions(code);
expressions[0].evaluate(context);
// assert
expect(expressions).has.lengthOf(1);
expect(pipelineCompiler.compileHistory).has.lengthOf(1);
const actualPipeNames = pipelineCompiler.compileHistory[0].pipeline;
const actualValue = pipelineCompiler.compileHistory[0].value;
expect(actualPipeNames).to.equal(expectedPipePart);
expect(actualValue).to.equal(data.parameterValue);
});
}
private expectMissPipePart(pipeline: string, data: IExpectPipeHitTestData) {
it(`"${pipeline}" misses`, () => {
// arrange
const args = new FunctionCallArgumentCollectionStub()
.withArgument(data.parameterName, data.parameterValue);
const pipelineCompiler = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(pipelineCompiler)
.withArgs(args);
const code = data.codeBuilder(pipeline);
// act
const expressions = this.sut.findExpressions(code);
expressions[0]?.evaluate(context); // Because an expression may include another with pipes
// assert
expect(pipelineCompiler.compileHistory).has.lengthOf(0);
});
}
}
interface IExpectResultTestCase {
name: string;
code: string;
args: (builder: FunctionCallArgumentCollectionStub) => FunctionCallArgumentCollectionStub;
expected: readonly string[];
name: string;
code: string;
args: (builder: FunctionCallArgumentCollectionStub) => FunctionCallArgumentCollectionStub;
expected: readonly string[];
}
interface IExpectPositionTestCase {
name: string;
code: string;
expected: readonly ExpressionPosition[];
name: string;
code: string;
expected: readonly ExpressionPosition[];
}
interface IExpectPipeHitTestData {
codeBuilder: (pipeline: string) => string;
parameterName: string;
parameterValue: string;
codeBuilder: (pipeline: string) => string;
parameterName: string;
parameterValue: string;
}
const PipeTestCases = {
ValidValues: [
// Single pipe with different whitespace combinations
' | pipe1', ' |pipe1', '|pipe1', ' |pipe1', ' | pipe1',
ValidValues: [
// Single pipe with different whitespace combinations
' | pipe1', ' |pipe1', '|pipe1', ' |pipe1', ' | pipe1',
// Double pipes with different whitespace combinations
' | pipe1 | pipe2', '| pipe1|pipe2', '|pipe1|pipe2', ' |pipe1 |pipe2', '| pipe1 | pipe2| pipe3 |pipe4',
// Double pipes with different whitespace combinations
' | pipe1 | pipe2', '| pipe1|pipe2', '|pipe1|pipe2', ' |pipe1 |pipe2', '| pipe1 | pipe2| pipe3 |pipe4',
// Wrong cases, but should match anyway and let pipelineCompiler throw errors
'| pip€', '| pip{e} ',
],
InvalidValues: [
' pipe1 |pipe2', ' pipe1',
],
// Wrong cases, but should match anyway and let pipelineCompiler throw errors
'| pip€', '| pip{e} ',
],
InvalidValues: [
' pipe1 |pipe2', ' pipe1',
],
};

View File

@@ -4,180 +4,178 @@ import { WithParser } from '@/application/Parser/Script/Compiler/Expressions/Syn
import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner';
describe('WithParser', () => {
const sut = new WithParser();
const runner = new SyntaxParserTestsRunner(sut);
describe('finds as expected', () => {
runner.expectPosition(
{
name: 'when no scope is not used',
code: 'hello {{ with $parameter }}no usage{{ end }} here',
expected: [ new ExpressionPosition(6, 44) ],
},
{
name: 'when scope is used',
code: 'used here ({{ with $parameter }}value: {{.}}{{ end }})',
expected: [ new ExpressionPosition(11, 53) ],
},
{
name: 'when used twice',
code: 'first: {{ with $parameter }}value: {{ . }}{{ end }}, second: {{ with $parameter }}no usage{{ end }}',
expected: [ new ExpressionPosition(7, 51), new ExpressionPosition(61, 99) ],
},
{
name: 'tolerate lack of whitespaces',
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
expected: [ new ExpressionPosition(15, 55) ],
},
);
const sut = new WithParser();
const runner = new SyntaxParserTestsRunner(sut);
describe('finds as expected', () => {
runner.expectPosition(
{
name: 'when no scope is not used',
code: 'hello {{ with $parameter }}no usage{{ end }} here',
expected: [new ExpressionPosition(6, 44)],
},
{
name: 'when scope is used',
code: 'used here ({{ with $parameter }}value: {{.}}{{ end }})',
expected: [new ExpressionPosition(11, 53)],
},
{
name: 'when used twice',
code: 'first: {{ with $parameter }}value: {{ . }}{{ end }}, second: {{ with $parameter }}no usage{{ end }}',
expected: [new ExpressionPosition(7, 51), new ExpressionPosition(61, 99)],
},
{
name: 'tolerate lack of whitespaces',
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
expected: [new ExpressionPosition(15, 55)],
},
);
});
describe('ignores when syntax is wrong', () => {
describe('ignores expression if "with" syntax is wrong', () => {
runner.expectPosition(
{
name: 'does not tolerate whitespace after with',
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
expected: [],
},
{
name: 'does not tolerate whitespace before dollar',
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
expected: [],
},
{
name: 'wrong text at scope end',
code: '{{ with$parameter}}value: {{ . }}{{ fin }}',
expected: [],
},
{
name: 'wrong text at expression start',
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
expected: [],
},
);
});
describe('ignores when syntax is wrong', () => {
describe('ignores expression if "with" syntax is wrong', () => {
runner.expectPosition(
{
name: 'does not tolerate whitespace after with',
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
expected: [ ],
},
{
name: 'does not tolerate whitespace before dollar',
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
expected: [ ],
},
{
name: 'wrong text at scope end',
code: '{{ with$parameter}}value: {{ . }}{{ fin }}',
expected: [ ],
},
{
name: 'wrong text at expression start',
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
expected: [ ],
},
);
});
describe('does not render argument if substitution syntax is wrong', () => {
runner.expectResults(
{
name: 'comma used instead of dot',
code: '{{ with $parameter }}Hello {{ , }}{{ end }}',
args: (args) => args
.withArgument('parameter', 'world!'),
expected: [ 'Hello {{ , }}' ],
},
{
name: 'single brackets instead of double',
code: '{{ with $parameter }}Hello { . }{{ end }}',
args: (args) => args
.withArgument('parameter', 'world!'),
expected: [ 'Hello { . }' ],
},
{
name: 'double dots instead of single',
code: '{{ with $parameter }}Hello {{ .. }}{{ end }}',
args: (args) => args
.withArgument('parameter', 'world!'),
expected: [ 'Hello {{ .. }}' ],
},
);
});
describe('does not render argument if substitution syntax is wrong', () => {
runner.expectResults(
{
name: 'comma used instead of dot',
code: '{{ with $parameter }}Hello {{ , }}{{ end }}',
args: (args) => args
.withArgument('parameter', 'world!'),
expected: ['Hello {{ , }}'],
},
{
name: 'single brackets instead of double',
code: '{{ with $parameter }}Hello { . }{{ end }}',
args: (args) => args
.withArgument('parameter', 'world!'),
expected: ['Hello { . }'],
},
{
name: 'double dots instead of single',
code: '{{ with $parameter }}Hello {{ .. }}{{ end }}',
args: (args) => args
.withArgument('parameter', 'world!'),
expected: ['Hello {{ .. }}'],
},
);
});
describe('renders scope conditionally', () => {
describe('does not render scope if argument is undefined', () => {
runner.expectResults(
{
name: 'does not render when value is undefined',
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
.withArgument('parameter', undefined),
expected: [ '' ],
},
{
name: 'does not render when value is empty',
code: '{{ with $parameter }}dark {{.}}{{ end }}',
args: (args) => args
.withArgument('parameter', ''),
expected: [ '' ],
},
{
name: 'does not render when argument is not provided',
code: '{{ with $parameter }}dark{{ end }}',
args: (args) => args,
expected: [ '' ],
},
);
});
describe('render scope when variable has value', () => {
runner.expectResults(
{
name: 'renders scope even if value is not used',
code: '{{ with $parameter }}Hello world!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello'),
expected: [ 'Hello world!' ],
},
{
name: 'renders value when it has value',
code: '{{ with $parameter }}{{ . }} world!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello'),
expected: [ 'Hello world!' ],
},
{
name: 'renders value when whitespaces around brackets are missing',
code: '{{ with $parameter }}{{.}} world!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello'),
expected: [ 'Hello world!' ],
},
{
name: 'renders value multiple times when it\'s used multiple times',
code: '{{ with $letterL }}He{{ . }}{{ . }}o wor{{ . }}d!{{ end }}',
args: (args) => args
.withArgument('letterL', 'l'),
expected: [ 'Hello world!' ],
},
);
});
});
describe('renders scope conditionally', () => {
describe('does not render scope if argument is undefined', () => {
runner.expectResults(
{
name: 'does not render when value is undefined',
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
.withArgument('parameter', undefined),
expected: [''],
},
{
name: 'does not render when value is empty',
code: '{{ with $parameter }}dark {{.}}{{ end }}',
args: (args) => args
.withArgument('parameter', ''),
expected: [''],
},
{
name: 'does not render when argument is not provided',
code: '{{ with $parameter }}dark{{ end }}',
args: (args) => args,
expected: [''],
},
);
});
describe('ignores trailing and leading whitespaces and newlines inside scope', () => {
runner.expectResults(
{
name: 'does not render trailing whitespace after value',
code: '{{ with $parameter }}{{ . }}! {{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: [ 'Hello world!' ],
},
{
name: 'does not render trailing newline after value',
code: '{{ with $parameter }}{{ . }}!\r\n{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: [ 'Hello world!' ],
},
{
name: 'does not render leading newline before value',
code: '{{ with $parameter }}\r\n{{ . }}!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: [ 'Hello world!' ],
},
{
name: 'does not render leading whitespace before value',
code: '{{ with $parameter }} {{ . }}!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: [ 'Hello world!' ],
},
);
describe('render scope when variable has value', () => {
runner.expectResults(
{
name: 'renders scope even if value is not used',
code: '{{ with $parameter }}Hello world!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello'),
expected: ['Hello world!'],
},
{
name: 'renders value when it has value',
code: '{{ with $parameter }}{{ . }} world!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello'),
expected: ['Hello world!'],
},
{
name: 'renders value when whitespaces around brackets are missing',
code: '{{ with $parameter }}{{.}} world!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello'),
expected: ['Hello world!'],
},
{
name: 'renders value multiple times when it\'s used multiple times',
code: '{{ with $letterL }}He{{ . }}{{ . }}o wor{{ . }}d!{{ end }}',
args: (args) => args
.withArgument('letterL', 'l'),
expected: ['Hello world!'],
},
);
});
describe('compiles pipes in scope as expected', () => {
runner.expectPipeHits({
codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`,
parameterName: 'argument',
parameterValue: 'value',
});
});
describe('ignores trailing and leading whitespaces and newlines inside scope', () => {
runner.expectResults(
{
name: 'does not render trailing whitespace after value',
code: '{{ with $parameter }}{{ . }}! {{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: ['Hello world!'],
},
{
name: 'does not render trailing newline after value',
code: '{{ with $parameter }}{{ . }}!\r\n{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: ['Hello world!'],
},
{
name: 'does not render leading newline before value',
code: '{{ with $parameter }}\r\n{{ . }}!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: ['Hello world!'],
},
{
name: 'does not render leading whitespace before value',
code: '{{ with $parameter }} {{ . }}!{{ end }}',
args: (args) => args
.withArgument('parameter', 'Hello world'),
expected: ['Hello world!'],
},
);
});
describe('compiles pipes in scope as expected', () => {
runner.expectPipeHits({
codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`,
parameterName: 'argument',
parameterValue: 'value',
});
});
});