Relax and improve code validation

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.
This commit is contained in:
undergroundwires
2022-10-29 20:03:06 +02:00
parent f4a7ca76b8
commit e8199932b4
44 changed files with 1095 additions and 392 deletions

View File

@@ -1,9 +1,7 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptCode } from '@/domain/IScriptCode';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { AbsentStringTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ScriptCode } from '@/domain/ScriptCode';
import { AbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
describe('ScriptCode', () => {
describe('code', () => {
@@ -39,116 +37,35 @@ describe('ScriptCode', () => {
});
}
});
describe('throws with invalid code in both "execute" or "revert"', () => {
// arrange
const testCases = [
{
testName: 'cannot construct with duplicate lines',
code: 'duplicate\nduplicate\nunique\nduplicate',
expectedMessage: 'Duplicates detected in script:\n❌ (0,1,3)\t[0] duplicate\n❌ (0,1,3)\t[1] duplicate\n✅ [2] unique\n❌ (0,1,3)\t[3] duplicate',
},
{
testName: 'cannot construct with empty lines',
code: 'line1\n\n\nline2',
expectedMessage: 'Script has empty lines:\n\n (0) line1\n (1) ❌\n (2) ❌\n (3) line2',
},
];
// act
const actions = testCases.flatMap((testCase) => ([
{
act: () => new ScriptCodeBuilder()
.withExecute(testCase.code)
.build(),
testName: `execute: ${testCase.testName}`,
expectedMessage: testCase.expectedMessage,
code: testCase.code,
},
{
act: () => new ScriptCodeBuilder()
.withRevert(testCase.code)
.build(),
testName: `revert: ${testCase.testName}`,
expectedMessage: `(revert): ${testCase.expectedMessage}`,
code: testCase.code,
},
]));
// assert
for (const action of actions) {
it(action.testName, () => {
expect(action.act).to.throw(action.expectedMessage, `Code used: ${action.code}`);
});
}
});
describe('sets as expected with valid "execute" or "revert"', () => {
// arrange
const syntax = new LanguageSyntaxStub()
.withCommonCodeParts(')', 'else', '(')
.withCommentDelimiters('#', '//');
const testCases = [
{
testName: 'code is a valid string',
testName: 'code and revert code is given',
code: 'valid code',
revertCode: 'valid revert-code',
},
{
testName: 'code consists of common code parts',
code: syntax.commonCodeParts.join(' '),
},
{
testName: 'code is a common code part',
code: syntax.commonCodeParts[0],
},
{
testName: `code with duplicated comment lines (${syntax.commentDelimiters[0]})`,
code: `${syntax.commentDelimiters[0]} comment\n${syntax.commentDelimiters[0]} comment`,
},
{
testName: `code with duplicated comment lines (${syntax.commentDelimiters[1]})`,
code: `${syntax.commentDelimiters[1]} comment\n${syntax.commentDelimiters[1]} comment`,
testName: 'only code is given but not revert code',
code: 'valid code',
revertCode: undefined,
},
];
// act
const actions = testCases.flatMap((testCase) => ([
{
testName: `execute: ${testCase.testName}`,
act: () => new ScriptCodeBuilder()
.withSyntax(syntax)
.withExecute(testCase.code)
.build(),
expect: (sut: IScriptCode) => sut.execute === testCase.code,
},
{
testName: `revert: ${testCase.testName}`,
act: () => new ScriptCodeBuilder()
.withSyntax(syntax)
.withRevert(testCase.code)
.build(),
expect: (sut: IScriptCode) => sut.revert === testCase.code,
},
]));
// assert
for (const action of actions) {
it(action.testName, () => {
const sut = action.act();
expect(action.expect(sut));
for (const testCase of testCases) {
it(testCase.testName, () => {
// act
const sut = new ScriptCodeBuilder()
.withExecute(testCase.code)
.withRevert(testCase.revertCode)
.build();
// assert
expect(sut.execute).to.equal(testCase.code);
expect(sut.revert).to.equal(testCase.revertCode);
});
}
});
});
describe('syntax', () => {
describe('throws if missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing syntax';
const syntax = absentValue;
// act
const act = () => new ScriptCodeBuilder()
.withSyntax(syntax)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
});
class ScriptCodeBuilder {
@@ -156,8 +73,6 @@ class ScriptCodeBuilder {
public revert = '';
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
public withExecute(execute: string) {
this.execute = execute;
return this;
@@ -168,16 +83,10 @@ class ScriptCodeBuilder {
return this;
}
public withSyntax(syntax: ILanguageSyntax) {
this.syntax = syntax;
return this;
}
public build(): ScriptCode {
return new ScriptCode(
this.execute,
this.revert,
this.syntax,
);
}
}