Rework code validation to be bound to a context and not context-independent. It means that the generated code is validated based on different phases during the compilation. This is done by moving validation from `ScriptCode` constructor to a different callable function. It removes duplicate detection for function calls once a call is fully compiled, but still checks for duplicates inside each function body that has inline code. This allows for having duplicates in final scripts (thus relaxing the duplicate detection), e.g., when multiple calls to the same function is made. It fixes non-duplicates (when using common syntax) being misrepresented as duplicate lines. It improves the output of errors, such as printing valid lines, to give more context. This improvement also fixes empty line validation not showing the right empty lines in the error output. Empty line validation shows tabs and whitespaces more clearly. Finally, it adds more tests including tests for existing logic, such as singleton factories.
170 lines
6.0 KiB
TypeScript
170 lines
6.0 KiB
TypeScript
import 'mocha';
|
|
import { expect } from 'chai';
|
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
|
import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub';
|
|
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
|
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
|
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
|
|
|
describe('CodeValidator', () => {
|
|
describe('instance', () => {
|
|
itIsSingleton({
|
|
getter: () => CodeValidator.instance,
|
|
expectedType: CodeValidator,
|
|
});
|
|
});
|
|
describe('throwIfInvalid', () => {
|
|
describe('does not throw if code is absent', () => {
|
|
itEachAbsentStringValue((absentValue) => {
|
|
// arrange
|
|
const code = absentValue;
|
|
const sut = new CodeValidator();
|
|
// act
|
|
const act = () => sut.throwIfInvalid(code, [new CodeValidationRuleStub()]);
|
|
// assert
|
|
expect(act).to.not.throw();
|
|
});
|
|
});
|
|
describe('throws if rules are empty', () => {
|
|
itEachAbsentCollectionValue((absentValue) => {
|
|
// arrange
|
|
const expectedError = 'missing rules';
|
|
const rules = absentValue;
|
|
const sut = new CodeValidator();
|
|
// act
|
|
const act = () => sut.throwIfInvalid('code', rules);
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
});
|
|
describe('splits lines as expected', () => {
|
|
it('supports all line separators', () => {
|
|
// arrange
|
|
const expectedLineTexts = ['line1', 'line2', 'line3', 'line4'];
|
|
const code = 'line1\r\nline2\rline3\nline4';
|
|
const spy = new CodeValidationRuleStub();
|
|
const sut = new CodeValidator();
|
|
// act
|
|
sut.throwIfInvalid(code, [spy]);
|
|
// expect
|
|
expect(spy.receivedLines).has.lengthOf(1);
|
|
const actualLineTexts = spy.receivedLines[0].map((line) => line.text);
|
|
expect(actualLineTexts).to.deep.equal(expectedLineTexts);
|
|
});
|
|
it('uses 1-indexed line numbering', () => {
|
|
// arrange
|
|
const expectedIndexes = [1, 2, 3];
|
|
const code = ['line1', 'line2', 'line3'].join('\n');
|
|
const spy = new CodeValidationRuleStub();
|
|
const sut = new CodeValidator();
|
|
// act
|
|
sut.throwIfInvalid(code, [spy]);
|
|
// expect
|
|
expect(spy.receivedLines).has.lengthOf(1);
|
|
const actualLineIndexes = spy.receivedLines[0].map((line) => line.index);
|
|
expect(actualLineIndexes).to.deep.equal(expectedIndexes);
|
|
});
|
|
it('matches texts with indexes as expected', () => {
|
|
// arrange
|
|
const expected: readonly ICodeLine[] = [
|
|
{ index: 1, text: 'first' },
|
|
{ index: 2, text: 'second' },
|
|
];
|
|
const code = expected.map((line) => line.text).join('\n');
|
|
const spy = new CodeValidationRuleStub();
|
|
const sut = new CodeValidator();
|
|
// act
|
|
sut.throwIfInvalid(code, [spy]);
|
|
// expect
|
|
expect(spy.receivedLines).has.lengthOf(1);
|
|
expect(spy.receivedLines[0]).to.deep.equal(expected);
|
|
});
|
|
});
|
|
describe('throws invalid lines as expected', () => {
|
|
it('throws with invalid line from single rule', () => {
|
|
// arrange
|
|
const errorText = 'error';
|
|
const expectedError = new ExpectedErrorBuilder()
|
|
.withOkLine('line1')
|
|
.withErrorLine('line2', errorText)
|
|
.withOkLine('line3')
|
|
.withOkLine('line4')
|
|
.buildError();
|
|
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
|
const invalidLines: readonly IInvalidCodeLine[] = [
|
|
{ index: 2, error: errorText },
|
|
];
|
|
const rule = new CodeValidationRuleStub()
|
|
.withReturnValue(invalidLines);
|
|
const noopRule = new CodeValidationRuleStub()
|
|
.withReturnValue([]);
|
|
const sut = new CodeValidator();
|
|
// act
|
|
const act = () => sut.throwIfInvalid(code, [rule, noopRule]);
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('throws with combined invalid lines from multiple rules', () => {
|
|
// arrange
|
|
const firstError = 'firstError';
|
|
const secondError = 'firstError';
|
|
const expectedError = new ExpectedErrorBuilder()
|
|
.withOkLine('line1')
|
|
.withErrorLine('line2', firstError)
|
|
.withOkLine('line3')
|
|
.withErrorLine('line4', secondError)
|
|
.buildError();
|
|
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
|
const firstRuleError: readonly IInvalidCodeLine[] = [
|
|
{ index: 2, error: firstError },
|
|
];
|
|
const secondRuleError: readonly IInvalidCodeLine[] = [
|
|
{ index: 4, error: secondError },
|
|
];
|
|
const firstRule = new CodeValidationRuleStub().withReturnValue(firstRuleError);
|
|
const secondRule = new CodeValidationRuleStub().withReturnValue(secondRuleError);
|
|
const sut = new CodeValidator();
|
|
// act
|
|
const act = () => sut.throwIfInvalid(code, [firstRule, secondRule]);
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
class ExpectedErrorBuilder {
|
|
private lineCount = 0;
|
|
|
|
private outputLines = new Array<string>();
|
|
|
|
public withOkLine(text: string) {
|
|
return this.withNumberedLine(`✅ ${text}`);
|
|
}
|
|
|
|
public withErrorLine(text: string, error: string) {
|
|
return this
|
|
.withNumberedLine(`❌ ${text}`)
|
|
.withLine(`\t⟶ ${error}`);
|
|
}
|
|
|
|
public buildError(): string {
|
|
return [
|
|
'Errors with the code.',
|
|
...this.outputLines,
|
|
].join('\n');
|
|
}
|
|
|
|
private withLine(line: string) {
|
|
this.outputLines.push(line);
|
|
return this;
|
|
}
|
|
|
|
private withNumberedLine(text: string) {
|
|
this.lineCount += 1;
|
|
const lineNumber = `[${this.lineCount}]`;
|
|
return this.withLine(`${lineNumber} ${text}`);
|
|
}
|
|
}
|