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:
@@ -1,11 +1,11 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
import { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||||
|
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
public readonly compiler: IScriptCompiler;
|
public readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function compileCode(
|
|||||||
compiler: IExpressionsCompiler,
|
compiler: IExpressionsCompiler,
|
||||||
): ICompiledFunctionCall {
|
): ICompiledFunctionCall {
|
||||||
return {
|
return {
|
||||||
code: compiler.compileExpressions(code.do, args),
|
code: compiler.compileExpressions(code.execute, args),
|
||||||
revertCode: compiler.compileExpressions(code.revert, args),
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export enum FunctionBodyType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCode {
|
export interface IFunctionCode {
|
||||||
readonly do: string;
|
readonly execute: string;
|
||||||
readonly revert?: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
export interface ISharedFunctionsParser {
|
export interface ISharedFunctionsParser {
|
||||||
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
parseFunctions(
|
||||||
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
): ISharedFunctionCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IFunctionCall } from './Call/IFunctionCall';
|
import { IFunctionCall } from './Call/IFunctionCall';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||||
} from './ISharedFunction';
|
} from './ISharedFunction';
|
||||||
@@ -25,7 +26,7 @@ export function createFunctionWithInlineCode(
|
|||||||
throw new Error(`undefined code in function "${name}"`);
|
throw new Error(`undefined code in function "${name}"`);
|
||||||
}
|
}
|
||||||
const content: IFunctionCode = {
|
const content: IFunctionCode = {
|
||||||
do: code,
|
execute: code,
|
||||||
revert: revertCode,
|
revert: revertCode,
|
||||||
};
|
};
|
||||||
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
@@ -12,16 +17,20 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
|
|||||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||||
|
|
||||||
|
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
||||||
|
|
||||||
public parseFunctions(
|
public parseFunctions(
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
): ISharedFunctionCollection {
|
): ISharedFunctionCollection {
|
||||||
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions || !functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
return functions
|
return functions
|
||||||
.map((func) => parseFunction(func))
|
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
||||||
.reduce((acc, func) => {
|
.reduce((acc, func) => {
|
||||||
acc.addFunction(func);
|
acc.addFunction(func);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -29,10 +38,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunction(data: FunctionData): ISharedFunction {
|
function parseFunction(
|
||||||
|
data: FunctionData,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
validator: ICodeValidator,
|
||||||
|
): ISharedFunction {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data);
|
const parameters = parseParameters(data);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
|
validateCode(data, syntax, validator);
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
}
|
}
|
||||||
// Has call
|
// Has call
|
||||||
@@ -40,6 +54,19 @@ function parseFunction(data: FunctionData): ISharedFunction {
|
|||||||
return createCallerFunction(name, parameters, calls);
|
return createCallerFunction(name, parameters, calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCode(
|
||||||
|
data: FunctionData,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
validator: ICodeValidator,
|
||||||
|
): void {
|
||||||
|
[data.code, data.revertCode].forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
return (data.parameters || [])
|
return (data.parameters || [])
|
||||||
.map((parameter) => {
|
.map((parameter) => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||||
@@ -8,18 +12,20 @@ import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompi
|
|||||||
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||||
|
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
|
||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
private readonly functions: ISharedFunctionCollection;
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[] | undefined,
|
||||||
private readonly syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
@@ -35,13 +41,19 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
try {
|
try {
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||||
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
return new ScriptCode(
|
return new ScriptCode(
|
||||||
compiledCode.code,
|
compiledCode.code,
|
||||||
compiledCode.revertCode,
|
compiledCode.revertCode,
|
||||||
this.syntax,
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error(`Script "${script.name}" ${error.message}`);
|
throw Error(`Script "${script.name}" ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
|
||||||
|
[compiledCode.code, compiledCode.revertCode].forEach(
|
||||||
|
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export interface ICategoryCollectionParseContext {
|
export interface ICategoryCollectionParseContext {
|
||||||
readonly compiler: IScriptCompiler;
|
readonly compiler: IScriptCompiler;
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData } from '@/application/collections/';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { parseDocs } from '../DocumentationParser';
|
import { parseDocs } from '../DocumentationParser';
|
||||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||||
import { NodeType } from '../NodeValidation/NodeType';
|
import { NodeType } from '../NodeValidation/NodeType';
|
||||||
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
@@ -15,6 +20,7 @@ export function parseScript(
|
|||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
levelParser = createEnumParser(RecommendationLevel),
|
levelParser = createEnumParser(RecommendationLevel),
|
||||||
scriptFactory: ScriptFactoryType = ScriptFactory,
|
scriptFactory: ScriptFactoryType = ScriptFactory,
|
||||||
|
codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
): Script {
|
): Script {
|
||||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
@@ -22,7 +28,7 @@ export function parseScript(
|
|||||||
try {
|
try {
|
||||||
const script = scriptFactory(
|
const script = scriptFactory(
|
||||||
/* name: */ data.name,
|
/* name: */ data.name,
|
||||||
/* code: */ parseCode(data, context),
|
/* code: */ parseCode(data, context, codeValidator),
|
||||||
/* docs: */ parseDocs(data),
|
/* docs: */ parseDocs(data),
|
||||||
/* level: */ parseLevel(data.recommend, levelParser),
|
/* level: */ parseLevel(data.recommend, levelParser),
|
||||||
);
|
);
|
||||||
@@ -42,11 +48,30 @@ function parseLevel(
|
|||||||
return parser.parseEnum(level, 'level');
|
return parser.parseEnum(level, 'level');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
function parseCode(
|
||||||
|
script: ScriptData,
|
||||||
|
context: ICategoryCollectionParseContext,
|
||||||
|
codeValidator: ICodeValidator,
|
||||||
|
): IScriptCode {
|
||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
const code = new ScriptCode(script.code, script.revertCode);
|
||||||
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateHardcodedCodeWithoutCalls(
|
||||||
|
scriptCode: ScriptCode,
|
||||||
|
codeValidator: ICodeValidator,
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
) {
|
||||||
|
[scriptCode.execute, scriptCode.revert].forEach(
|
||||||
|
(code) => codeValidator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateScript(script: ScriptData, validator: NodeValidator) {
|
function validateScript(script: ScriptData, validator: NodeValidator) {
|
||||||
|
|||||||
46
src/application/Parser/Script/Validation/CodeValidator.ts
Normal file
46
src/application/Parser/Script/Validation/CodeValidator.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
|
||||||
|
import { ICodeValidator } from './ICodeValidator';
|
||||||
|
import { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export class CodeValidator implements ICodeValidator {
|
||||||
|
public static readonly instance: ICodeValidator = new CodeValidator();
|
||||||
|
|
||||||
|
public throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void {
|
||||||
|
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
||||||
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = extractLines(code);
|
||||||
|
const invalidLines = rules.flatMap((rule) => rule.analyze(lines));
|
||||||
|
if (invalidLines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const errorText = `Errors with the code.\n${printLines(lines, invalidLines)}`;
|
||||||
|
throw new Error(errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLines(code: string): ICodeLine[] {
|
||||||
|
return code
|
||||||
|
.split(/\r\n|\r|\n/)
|
||||||
|
.map((lineText, lineIndex): ICodeLine => ({
|
||||||
|
index: lineIndex + 1,
|
||||||
|
text: lineText,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function printLines(
|
||||||
|
lines: readonly ICodeLine[],
|
||||||
|
invalidLines: readonly IInvalidCodeLine[],
|
||||||
|
): string {
|
||||||
|
return lines.map((line) => {
|
||||||
|
const badLine = invalidLines.find((invalidLine) => invalidLine.index === line.index);
|
||||||
|
if (!badLine) {
|
||||||
|
return `[${line.index}] ✅ ${line.text}`;
|
||||||
|
}
|
||||||
|
return `[${badLine.index}] ❌ ${line.text}\n\t⟶ ${badLine.error}`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
4
src/application/Parser/Script/Validation/ICodeLine.ts
Normal file
4
src/application/Parser/Script/Validation/ICodeLine.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly text: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ICodeLine } from './ICodeLine';
|
||||||
|
|
||||||
|
export interface IInvalidCodeLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICodeValidationRule {
|
||||||
|
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ICodeValidationRule } from './ICodeValidationRule';
|
||||||
|
|
||||||
|
export interface ICodeValidator {
|
||||||
|
throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { ICodeLine } from '../ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
|
constructor(private readonly syntax: ILanguageSyntax) {
|
||||||
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.map((line): IDuplicateAnalyzedLine => ({
|
||||||
|
index: line.index,
|
||||||
|
isIgnored: shouldIgnoreLine(line.text, this.syntax),
|
||||||
|
occurrenceIndices: lines
|
||||||
|
.filter((other) => other.text === line.text)
|
||||||
|
.map((duplicatedLine) => duplicatedLine.index),
|
||||||
|
}))
|
||||||
|
.filter((line) => hasInvalidDuplicates(line))
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: `Line is duplicated at line numbers ${line.occurrenceIndices.join(',')}.`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDuplicateAnalyzedLine {
|
||||||
|
readonly index: number;
|
||||||
|
readonly occurrenceIndices: readonly number[];
|
||||||
|
readonly isIgnored: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInvalidDuplicates(line: IDuplicateAnalyzedLine): boolean {
|
||||||
|
return !line.isIgnored && line.occurrenceIndices.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
|
const lowerCaseCodeLine = codeLine.toLowerCase();
|
||||||
|
const isCommentLine = () => syntax.commentDelimiters.some(
|
||||||
|
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
||||||
|
);
|
||||||
|
const consistsOfFrequentCommands = () => {
|
||||||
|
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
||||||
|
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||||
|
};
|
||||||
|
return isCommentLine() || consistsOfFrequentCommands();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ICodeLine } from '../ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
|
export class NoEmptyLines implements ICodeValidationRule {
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
return lines
|
||||||
|
.filter((line) => (line.text?.trim().length ?? 0) === 0)
|
||||||
|
.map((line): IInvalidCodeLine => ({
|
||||||
|
index: line.index,
|
||||||
|
error: (() => {
|
||||||
|
if (!line.text) {
|
||||||
|
return 'Empty line';
|
||||||
|
}
|
||||||
|
const markedText = line.text
|
||||||
|
.replaceAll(' ', '{whitespace}')
|
||||||
|
.replaceAll('\t', '{tab}');
|
||||||
|
return `Empty line: "${markedText}"`;
|
||||||
|
})(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||||
const PowerShellCommonCodeParts = ['{', '}'];
|
const PowerShellCommonCodeParts = ['{', '}'];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ILanguageSyntax {
|
||||||
|
readonly commentDelimiters: string[];
|
||||||
|
readonly commonCodeParts: string[];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
import { ILanguageSyntax } from './ILanguageSyntax';
|
||||||
|
|
||||||
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = ['#'];
|
public readonly commentDelimiters = ['#'];
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
@@ -4,25 +4,18 @@ export class ScriptCode implements IScriptCode {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string,
|
||||||
syntax: ILanguageSyntax,
|
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
validateCode(execute);
|
||||||
validateCode(execute, syntax);
|
validateRevertCode(revert, execute);
|
||||||
validateRevertCode(revert, execute, syntax);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILanguageSyntax {
|
function validateRevertCode(revertCode: string, execute: string) {
|
||||||
readonly commentDelimiters: string[];
|
|
||||||
readonly commonCodeParts: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
|
|
||||||
if (!revertCode) {
|
if (!revertCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
validateCode(revertCode, syntax);
|
validateCode(revertCode);
|
||||||
if (execute === revertCode) {
|
if (execute === revertCode) {
|
||||||
throw new Error('Code itself and its reverting code cannot be the same');
|
throw new Error('Code itself and its reverting code cannot be the same');
|
||||||
}
|
}
|
||||||
@@ -31,54 +24,8 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
function validateCode(code: string): void {
|
||||||
if (!code || code.length === 0) {
|
if (!code || code.length === 0) {
|
||||||
throw new Error('missing code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
ensureNoEmptyLines(code);
|
|
||||||
ensureCodeHasUniqueLines(code, syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoEmptyLines(code: string): void {
|
|
||||||
const lines = code.split(/\r\n|\r|\n/);
|
|
||||||
if (lines.some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
|
||||||
const allLines = code.split(/\r\n|\r|\n/);
|
|
||||||
const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
|
|
||||||
if (checkedLines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printDuplicatedLines(allLines: string[]) {
|
|
||||||
return allLines
|
|
||||||
.map((line, index) => {
|
|
||||||
const occurrenceIndices = allLines
|
|
||||||
.map((e, i) => (e === line ? i : ''))
|
|
||||||
.filter(String);
|
|
||||||
const isDuplicate = occurrenceIndices.length > 1;
|
|
||||||
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
|
||||||
return `${indicator}[${index}] ${line}`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
|
||||||
const lowerCaseCodeLine = codeLine.toLowerCase();
|
|
||||||
const isCommentLine = () => syntax.commentDelimiters.some(
|
|
||||||
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
|
||||||
);
|
|
||||||
const consistsOfFrequentCommands = () => {
|
|
||||||
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
|
||||||
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
|
||||||
};
|
|
||||||
return isCommentLine() || consistsOfFrequentCommands();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ISyntaxFactory } from '@/application/Parser/Script/Syntax/ISyntaxFactory';
|
import { ISyntaxFactory } from '@/application/Parser/Script/Validation/Syntax/ISyntaxFactory';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { CategoryCollectionParseContext } from '@/application/Parser/Script/CategoryCollectionParseContext';
|
import { CategoryCollectionParseContext } from '@/application/Parser/Script/CategoryCollectionParseContext';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|
||||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||||
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||||
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
describe('CategoryCollectionParseContext', () => {
|
describe('CategoryCollectionParseContext', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
|
|||||||
@@ -11,8 +11,15 @@ import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/Fun
|
|||||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||||
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||||
|
|
||||||
describe('FunctionCallCompiler', () => {
|
describe('FunctionCallCompiler', () => {
|
||||||
|
describe('instance', () => {
|
||||||
|
itIsSingleton({
|
||||||
|
getter: () => FunctionCallCompiler.instance,
|
||||||
|
expectedType: FunctionCallCompiler,
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('compileCall', () => {
|
describe('compileCall', () => {
|
||||||
describe('parameter validation', () => {
|
describe('parameter validation', () => {
|
||||||
describe('call', () => {
|
describe('call', () => {
|
||||||
@@ -172,7 +179,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs);
|
const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs);
|
||||||
const { code } = func.body;
|
const { code } = func.body;
|
||||||
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||||
.setup({ givenCode: code.do, givenArgs: args, result: expected.execute })
|
.setup({ givenCode: code.execute, givenArgs: args, result: expected.execute })
|
||||||
.setup({ givenCode: code.revert, givenArgs: args, result: expected.revert });
|
.setup({ givenCode: code.revert, givenArgs: args, result: expected.revert });
|
||||||
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
||||||
// act
|
// act
|
||||||
@@ -209,7 +216,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||||
.setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs)
|
.setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs)
|
||||||
.setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs);
|
.setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs);
|
||||||
const expectedExecute = `${firstFunction.body.code.do}\n${secondFunction.body.code.do}`;
|
const expectedExecute = `${firstFunction.body.code.execute}\n${secondFunction.body.code.execute}`;
|
||||||
const expectedRevert = `${firstFunction.body.code.revert}\n${secondFunction.body.code.revert}`;
|
const expectedRevert = `${firstFunction.body.code.revert}\n${secondFunction.body.code.revert}`;
|
||||||
const functions = new SharedFunctionCollectionStub()
|
const functions = new SharedFunctionCollectionStub()
|
||||||
.withFunction(firstFunction)
|
.withFunction(firstFunction)
|
||||||
@@ -244,7 +251,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
};
|
};
|
||||||
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||||
.setup({
|
.setup({
|
||||||
givenCode: functions.deep.body.code.do,
|
givenCode: functions.deep.body.code.execute,
|
||||||
givenArgs: emptyArgs,
|
givenArgs: emptyArgs,
|
||||||
result: expected.code,
|
result: expected.code,
|
||||||
})
|
})
|
||||||
@@ -312,7 +319,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
})
|
})
|
||||||
// set-up compiling of deep, compiled argument should be sent
|
// set-up compiling of deep, compiled argument should be sent
|
||||||
.setup({
|
.setup({
|
||||||
givenCode: scenario.deep.getFunction().body.code.do,
|
givenCode: scenario.deep.getFunction().body.code.execute,
|
||||||
givenArgs: scenario.front.callArgs.expectedCallDeep(),
|
givenArgs: scenario.front.callArgs.expectedCallDeep(),
|
||||||
result: expected.code,
|
result: expected.code,
|
||||||
})
|
})
|
||||||
@@ -407,7 +414,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
})
|
})
|
||||||
// Compiling of third functions code with expected arguments
|
// Compiling of third functions code with expected arguments
|
||||||
.setup({
|
.setup({
|
||||||
givenCode: scenario.third.getFunction().body.code.do,
|
givenCode: scenario.third.getFunction().body.code.execute,
|
||||||
givenArgs: scenario.second.callArgs.expectedToThird(),
|
givenArgs: scenario.second.callArgs.expectedToThird(),
|
||||||
result: expected.code,
|
result: expected.code,
|
||||||
})
|
})
|
||||||
@@ -491,7 +498,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
.setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs);
|
.setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs);
|
||||||
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
||||||
const expected = {
|
const expected = {
|
||||||
code: `${functions.call1.deep.getFunction().body.code.do}\n${functions.call2.deep.getFunction().body.code.do}`,
|
code: `${functions.call1.deep.getFunction().body.code.execute}\n${functions.call2.deep.getFunction().body.code.execute}`,
|
||||||
revert: `${functions.call1.deep.getFunction().body.code.revert}\n${functions.call2.deep.getFunction().body.code.revert}`,
|
revert: `${functions.call1.deep.getFunction().body.code.revert}\n${functions.call2.deep.getFunction().body.code.revert}`,
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
|
|||||||
@@ -12,174 +12,174 @@ import {
|
|||||||
} from '@tests/unit/shared/TestCases/AbsentTests';
|
} from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('SharedFunction', () => {
|
describe('SharedFunction', () => {
|
||||||
describe('name', () => {
|
describe('SharedFunction', () => {
|
||||||
runForEachFactoryMethod((build) => {
|
describe('name', () => {
|
||||||
it('sets as expected', () => {
|
runForEachFactoryMethod((build) => {
|
||||||
// arrange
|
|
||||||
const expected = 'expected-function-name';
|
|
||||||
const builder = new SharedFunctionBuilder()
|
|
||||||
.withName(expected);
|
|
||||||
// act
|
|
||||||
const sut = build(builder);
|
|
||||||
// assert
|
|
||||||
expect(sut.name).equal(expected);
|
|
||||||
});
|
|
||||||
it('throws when absent', () => {
|
|
||||||
itEachAbsentStringValue((absentValue) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'missing function name';
|
|
||||||
const builder = new SharedFunctionBuilder()
|
|
||||||
.withName(absentValue);
|
|
||||||
// act
|
|
||||||
const act = () => build(builder);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('parameters', () => {
|
|
||||||
runForEachFactoryMethod((build) => {
|
|
||||||
it('sets as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const expected = new FunctionParameterCollectionStub()
|
|
||||||
.withParameterName('test-parameter');
|
|
||||||
const builder = new SharedFunctionBuilder()
|
|
||||||
.withParameters(expected);
|
|
||||||
// act
|
|
||||||
const sut = build(builder);
|
|
||||||
// assert
|
|
||||||
expect(sut.parameters).equal(expected);
|
|
||||||
});
|
|
||||||
describe('throws if missing', () => {
|
|
||||||
itEachAbsentObjectValue((absentValue) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'missing parameters';
|
|
||||||
const parameters = absentValue;
|
|
||||||
const builder = new SharedFunctionBuilder()
|
|
||||||
.withParameters(parameters);
|
|
||||||
// act
|
|
||||||
const act = () => build(builder);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('body', () => {
|
|
||||||
describe('createFunctionWithInlineCode', () => {
|
|
||||||
describe('code', () => {
|
|
||||||
it('sets as expected', () => {
|
it('sets as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'expected-code';
|
const expected = 'expected-function-name';
|
||||||
|
const builder = new SharedFunctionBuilder()
|
||||||
|
.withName(expected);
|
||||||
// act
|
// act
|
||||||
const sut = new SharedFunctionBuilder()
|
const sut = build(builder);
|
||||||
.withCode(expected)
|
|
||||||
.createFunctionWithInlineCode();
|
|
||||||
// assert
|
// assert
|
||||||
expect(sut.body.code.do).equal(expected);
|
expect(sut.name).equal(expected);
|
||||||
});
|
});
|
||||||
describe('throws if absent', () => {
|
it('throws when absent', () => {
|
||||||
itEachAbsentStringValue((absentValue) => {
|
itEachAbsentStringValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const functionName = 'expected-function-name';
|
const expectedError = 'missing function name';
|
||||||
const expectedError = `undefined code in function "${functionName}"`;
|
const builder = new SharedFunctionBuilder()
|
||||||
const invalidValue = absentValue;
|
.withName(absentValue);
|
||||||
// act
|
// act
|
||||||
const act = () => new SharedFunctionBuilder()
|
const act = () => build(builder);
|
||||||
.withName(functionName)
|
|
||||||
.withCode(invalidValue)
|
|
||||||
.createFunctionWithInlineCode();
|
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('revertCode', () => {
|
|
||||||
it('sets as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const testData = [
|
|
||||||
'expected-revert-code',
|
|
||||||
...AbsentStringTestCases.map((testCase) => testCase.absentValue),
|
|
||||||
];
|
|
||||||
for (const data of testData) {
|
|
||||||
// act
|
|
||||||
const sut = new SharedFunctionBuilder()
|
|
||||||
.withRevertCode(data)
|
|
||||||
.createFunctionWithInlineCode();
|
|
||||||
// assert
|
|
||||||
expect(sut.body.code.revert).equal(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('sets type as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedType = FunctionBodyType.Code;
|
|
||||||
// act
|
|
||||||
const sut = new SharedFunctionBuilder()
|
|
||||||
.createFunctionWithInlineCode();
|
|
||||||
// assert
|
|
||||||
expect(sut.body.type).equal(expectedType);
|
|
||||||
});
|
|
||||||
it('calls are undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedCalls = undefined;
|
|
||||||
// act
|
|
||||||
const sut = new SharedFunctionBuilder()
|
|
||||||
.createFunctionWithInlineCode();
|
|
||||||
// assert
|
|
||||||
expect(sut.body.calls).equal(expectedCalls);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('createCallerFunction', () => {
|
describe('parameters', () => {
|
||||||
describe('callSequence', () => {
|
runForEachFactoryMethod((build) => {
|
||||||
it('sets as expected', () => {
|
it('sets as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = [
|
const expected = new FunctionParameterCollectionStub()
|
||||||
new FunctionCallStub().withFunctionName('firstFunction'),
|
.withParameterName('test-parameter');
|
||||||
new FunctionCallStub().withFunctionName('secondFunction'),
|
const builder = new SharedFunctionBuilder()
|
||||||
];
|
.withParameters(expected);
|
||||||
// act
|
// act
|
||||||
const sut = new SharedFunctionBuilder()
|
const sut = build(builder);
|
||||||
.withCallSequence(expected)
|
|
||||||
.createCallerFunction();
|
|
||||||
// assert
|
// assert
|
||||||
expect(sut.body.calls).equal(expected);
|
expect(sut.parameters).equal(expected);
|
||||||
});
|
});
|
||||||
describe('throws if missing', () => {
|
describe('throws if missing', () => {
|
||||||
itEachAbsentCollectionValue((absentValue) => {
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const functionName = 'invalidFunction';
|
const expectedError = 'missing parameters';
|
||||||
const callSequence = absentValue;
|
const parameters = absentValue;
|
||||||
const expectedError = `missing call sequence in function "${functionName}"`;
|
const builder = new SharedFunctionBuilder()
|
||||||
|
.withParameters(parameters);
|
||||||
// act
|
// act
|
||||||
const act = () => new SharedFunctionBuilder()
|
const act = () => build(builder);
|
||||||
.withName(functionName)
|
|
||||||
.withCallSequence(callSequence)
|
|
||||||
.createCallerFunction();
|
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('sets type as expected', () => {
|
});
|
||||||
|
});
|
||||||
|
describe('createFunctionWithInlineCode', () => {
|
||||||
|
describe('code', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedType = FunctionBodyType.Calls;
|
const expected = 'expected-code';
|
||||||
// act
|
// act
|
||||||
const sut = new SharedFunctionBuilder()
|
const sut = new SharedFunctionBuilder()
|
||||||
.createCallerFunction();
|
.withCode(expected)
|
||||||
|
.createFunctionWithInlineCode();
|
||||||
// assert
|
// assert
|
||||||
expect(sut.body.type).equal(expectedType);
|
expect(sut.body.code.execute).equal(expected);
|
||||||
});
|
});
|
||||||
it('code is undefined', () => {
|
describe('throws if absent', () => {
|
||||||
|
itEachAbsentStringValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'expected-function-name';
|
||||||
|
const expectedError = `undefined code in function "${functionName}"`;
|
||||||
|
const invalidValue = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => new SharedFunctionBuilder()
|
||||||
|
.withName(functionName)
|
||||||
|
.withCode(invalidValue)
|
||||||
|
.createFunctionWithInlineCode();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('revertCode', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedCode = undefined;
|
const testData = [
|
||||||
|
'expected-revert-code',
|
||||||
|
...AbsentStringTestCases.map((testCase) => testCase.absentValue),
|
||||||
|
];
|
||||||
|
for (const data of testData) {
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withRevertCode(data)
|
||||||
|
.createFunctionWithInlineCode();
|
||||||
|
// assert
|
||||||
|
expect(sut.body.code.revert).equal(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('sets type as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedType = FunctionBodyType.Code;
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.createFunctionWithInlineCode();
|
||||||
|
// assert
|
||||||
|
expect(sut.body.type).equal(expectedType);
|
||||||
|
});
|
||||||
|
it('calls are undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCalls = undefined;
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.createFunctionWithInlineCode();
|
||||||
|
// assert
|
||||||
|
expect(sut.body.calls).equal(expectedCalls);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('createCallerFunction', () => {
|
||||||
|
describe('callSequence', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [
|
||||||
|
new FunctionCallStub().withFunctionName('firstFunction'),
|
||||||
|
new FunctionCallStub().withFunctionName('secondFunction'),
|
||||||
|
];
|
||||||
// act
|
// act
|
||||||
const sut = new SharedFunctionBuilder()
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.withCallSequence(expected)
|
||||||
.createCallerFunction();
|
.createCallerFunction();
|
||||||
// assert
|
// assert
|
||||||
expect(sut.body.code).equal(expectedCode);
|
expect(sut.body.calls).equal(expected);
|
||||||
});
|
});
|
||||||
|
describe('throws if missing', () => {
|
||||||
|
itEachAbsentCollectionValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'invalidFunction';
|
||||||
|
const callSequence = absentValue;
|
||||||
|
const expectedError = `missing call sequence in function "${functionName}"`;
|
||||||
|
// act
|
||||||
|
const act = () => new SharedFunctionBuilder()
|
||||||
|
.withName(functionName)
|
||||||
|
.withCallSequence(callSequence)
|
||||||
|
.createCallerFunction();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('sets type as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedType = FunctionBodyType.Calls;
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.createCallerFunction();
|
||||||
|
// assert
|
||||||
|
expect(sut.body.type).equal(expectedType);
|
||||||
|
});
|
||||||
|
it('code is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = undefined;
|
||||||
|
// act
|
||||||
|
const sut = new SharedFunctionBuilder()
|
||||||
|
.createCallerFunction();
|
||||||
|
// assert
|
||||||
|
expect(sut.body.code).equal(expectedCode);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,18 +8,46 @@ import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterD
|
|||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||||
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
|
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||||
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
describe('SharedFunctionsParser', () => {
|
describe('SharedFunctionsParser', () => {
|
||||||
|
describe('instance', () => {
|
||||||
|
itIsSingleton({
|
||||||
|
getter: () => SharedFunctionsParser.instance,
|
||||||
|
expectedType: SharedFunctionsParser,
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('parseFunctions', () => {
|
describe('parseFunctions', () => {
|
||||||
|
describe('throws if syntax is missing', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing syntax';
|
||||||
|
const syntax = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withSyntax(syntax)
|
||||||
|
.parseFunctions();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('validates functions', () => {
|
describe('validates functions', () => {
|
||||||
describe('throws if one of the functions is undefined', () => {
|
describe('throws if one of the functions is undefined', () => {
|
||||||
itEachAbsentObjectValue((absentValue) => {
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'some functions are undefined';
|
const expectedError = 'some functions are undefined';
|
||||||
const functions = [FunctionDataStub.createWithCode(), absentValue];
|
const functions = [FunctionDataStub.createWithCode(), absentValue];
|
||||||
const sut = new SharedFunctionsParser();
|
const sut = new ParseFunctionsCallerWithDefaults();
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions(functions);
|
const act = () => sut
|
||||||
|
.withFunctions(functions)
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -32,9 +60,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
FunctionDataStub.createWithCode().withName(name),
|
FunctionDataStub.createWithCode().withName(name),
|
||||||
FunctionDataStub.createWithCode().withName(name),
|
FunctionDataStub.createWithCode().withName(name),
|
||||||
];
|
];
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions(functions);
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions(functions)
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -47,9 +76,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
|
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
|
||||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
|
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
|
||||||
];
|
];
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions(functions);
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions(functions)
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -63,9 +93,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
FunctionDataStub.createWithoutCallOrCodes()
|
FunctionDataStub.createWithoutCallOrCodes()
|
||||||
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
||||||
];
|
];
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions(functions);
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions(functions)
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -79,9 +110,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
.withName(functionName)
|
.withName(functionName)
|
||||||
.withCode('code')
|
.withCode('code')
|
||||||
.withMockCall();
|
.withMockCall();
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions([invalidFunction]);
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([invalidFunction])
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -91,9 +123,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
||||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
.withName(functionName);
|
.withName(functionName);
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions([invalidFunction]);
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([invalidFunction])
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -116,14 +149,34 @@ describe('SharedFunctionsParser', () => {
|
|||||||
.createWithCall()
|
.createWithCall()
|
||||||
.withParametersObject(testCase.invalidType as never);
|
.withParametersObject(testCase.invalidType as never);
|
||||||
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
|
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseFunctions([func]);
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([func])
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
it('validates function code as expected when code is defined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
|
||||||
|
const functionData = FunctionDataStub
|
||||||
|
.createWithCode()
|
||||||
|
.withCode('expected code to be validated')
|
||||||
|
.withRevertCode('expected revert code to be validated');
|
||||||
|
const validator = new CodeValidatorStub();
|
||||||
|
// act
|
||||||
|
new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([functionData])
|
||||||
|
.withValidator(validator)
|
||||||
|
.parseFunctions();
|
||||||
|
// assert
|
||||||
|
validator.assertHistory({
|
||||||
|
validatedCodes: [functionData.code, functionData.revertCode],
|
||||||
|
rules: expectedRules,
|
||||||
|
});
|
||||||
|
});
|
||||||
it('rethrows including function name when FunctionParameter throws', () => {
|
it('rethrows including function name when FunctionParameter throws', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const invalidParameterName = 'invalid function p@r4meter name';
|
const invalidParameterName = 'invalid function p@r4meter name';
|
||||||
@@ -139,8 +192,9 @@ describe('SharedFunctionsParser', () => {
|
|||||||
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const sut = new SharedFunctionsParser();
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
const act = () => sut.parseFunctions([functionData]);
|
.withFunctions([functionData])
|
||||||
|
.parseFunctions();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
@@ -148,10 +202,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
});
|
});
|
||||||
describe('given empty functions, returns empty collection', () => {
|
describe('given empty functions, returns empty collection', () => {
|
||||||
itEachAbsentCollectionValue((absentValue) => {
|
itEachAbsentCollectionValue((absentValue) => {
|
||||||
// arrange
|
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const actual = sut.parseFunctions(absentValue);
|
const actual = new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions(absentValue)
|
||||||
|
.parseFunctions();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.not.equal(undefined);
|
expect(actual).to.not.equal(undefined);
|
||||||
});
|
});
|
||||||
@@ -169,9 +223,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
|
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
|
||||||
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
|
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
|
||||||
);
|
);
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const collection = sut.parseFunctions([expected]);
|
const collection = new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([expected])
|
||||||
|
.parseFunctions();
|
||||||
// expect
|
// expect
|
||||||
const actual = collection.getFunctionByName(name);
|
const actual = collection.getFunctionByName(name);
|
||||||
expectEqualName(expected, actual);
|
expectEqualName(expected, actual);
|
||||||
@@ -188,9 +243,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
const data = FunctionDataStub.createWithoutCallOrCodes()
|
const data = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
.withName('caller-function')
|
.withName('caller-function')
|
||||||
.withCall(call);
|
.withCall(call);
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const collection = sut.parseFunctions([data]);
|
const collection = new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([data])
|
||||||
|
.parseFunctions();
|
||||||
// expect
|
// expect
|
||||||
const actual = collection.getFunctionByName(data.name);
|
const actual = collection.getFunctionByName(data.name);
|
||||||
expectEqualName(data, actual);
|
expectEqualName(data, actual);
|
||||||
@@ -211,9 +267,10 @@ describe('SharedFunctionsParser', () => {
|
|||||||
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
|
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
|
||||||
.withName('caller-function-2')
|
.withName('caller-function-2')
|
||||||
.withCall([call1, call2]);
|
.withCall([call1, call2]);
|
||||||
const sut = new SharedFunctionsParser();
|
|
||||||
// act
|
// act
|
||||||
const collection = sut.parseFunctions([caller1, caller2]);
|
const collection = new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions([caller1, caller2])
|
||||||
|
.parseFunctions();
|
||||||
// expect
|
// expect
|
||||||
const compiledCaller1 = collection.getFunctionByName(caller1.name);
|
const compiledCaller1 = collection.getFunctionByName(caller1.name);
|
||||||
expectEqualName(caller1, compiledCaller1);
|
expectEqualName(caller1, compiledCaller1);
|
||||||
@@ -228,6 +285,34 @@ describe('SharedFunctionsParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class ParseFunctionsCallerWithDefaults {
|
||||||
|
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||||
|
|
||||||
|
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||||
|
|
||||||
|
private functions: readonly FunctionData[] = [FunctionDataStub.createWithCode()];
|
||||||
|
|
||||||
|
public withSyntax(syntax: ILanguageSyntax) {
|
||||||
|
this.syntax = syntax;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withValidator(codeValidator: ICodeValidator) {
|
||||||
|
this.codeValidator = codeValidator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withFunctions(functions: readonly FunctionData[]) {
|
||||||
|
this.functions = functions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseFunctions() {
|
||||||
|
const sut = new SharedFunctionsParser(this.codeValidator);
|
||||||
|
return sut.parseFunctions(this.functions, this.syntax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function expectEqualName(expected: FunctionDataStub, actual: ISharedFunction): void {
|
function expectEqualName(expected: FunctionDataStub, actual: ISharedFunction): void {
|
||||||
expect(actual.name).to.equal(expected.name);
|
expect(actual.name).to.equal(expected.name);
|
||||||
}
|
}
|
||||||
@@ -250,7 +335,7 @@ function expectEqualFunctionWithInlineCode(
|
|||||||
): void {
|
): void {
|
||||||
expect(actual.body, `function "${actual.name}" has no body`);
|
expect(actual.body, `function "${actual.name}" has no body`);
|
||||||
expect(actual.body.code, `function "${actual.name}" has no code`);
|
expect(actual.body.code, `function "${actual.name}" has no code`);
|
||||||
expect(actual.body.code.do).to.equal(expected.code);
|
expect(actual.body.code.execute).to.equal(expected.code);
|
||||||
expect(actual.body.code.revert).to.equal(expected.revertCode);
|
expect(actual.body.code.revert).to.equal(expected.revertCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { ILanguageSyntax, ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||||
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
||||||
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
|
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
|
||||||
@@ -15,6 +15,10 @@ import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFun
|
|||||||
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
|
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
|
||||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
|
||||||
describe('ScriptCompiler', () => {
|
describe('ScriptCompiler', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
@@ -111,28 +115,38 @@ describe('ScriptCompiler', () => {
|
|||||||
expect(code.execute).to.equal(expected.code);
|
expect(code.execute).to.equal(expected.code);
|
||||||
expect(code.revert).to.equal(expected.revertCode);
|
expect(code.revert).to.equal(expected.revertCode);
|
||||||
});
|
});
|
||||||
it('creates with expected syntax', () => {
|
describe('parses functions as expected', () => {
|
||||||
// arrange
|
it('parses functions with expected syntax', () => {
|
||||||
let isUsed = false;
|
// arrange
|
||||||
const syntax: ILanguageSyntax = {
|
const expected: ILanguageSyntax = new LanguageSyntaxStub();
|
||||||
get commentDelimiters() {
|
const parser = new SharedFunctionsParserStub();
|
||||||
isUsed = true;
|
const sut = new ScriptCompilerBuilder()
|
||||||
return [];
|
.withSomeFunctions()
|
||||||
},
|
.withSyntax(expected)
|
||||||
get commonCodeParts() {
|
.withSharedFunctionsParser(parser)
|
||||||
isUsed = true;
|
.build();
|
||||||
return [];
|
const scriptData = ScriptDataStub.createWithCall();
|
||||||
},
|
// act
|
||||||
};
|
sut.compile(scriptData);
|
||||||
const sut = new ScriptCompilerBuilder()
|
// assert
|
||||||
.withSomeFunctions()
|
expect(parser.callHistory.length).to.equal(1);
|
||||||
.withSyntax(syntax)
|
expect(parser.callHistory[0].syntax).to.equal(expected);
|
||||||
.build();
|
});
|
||||||
const scriptData = ScriptDataStub.createWithCall();
|
it('parses given functions', () => {
|
||||||
// act
|
// arrange
|
||||||
sut.compile(scriptData);
|
const expectedFunctions = [FunctionDataStub.createWithCode().withName('existing-func')];
|
||||||
// assert
|
const parser = new SharedFunctionsParserStub();
|
||||||
expect(isUsed).to.equal(true);
|
const sut = new ScriptCompilerBuilder()
|
||||||
|
.withFunctions(...expectedFunctions)
|
||||||
|
.withSharedFunctionsParser(parser)
|
||||||
|
.build();
|
||||||
|
const scriptData = ScriptDataStub.createWithCall();
|
||||||
|
// act
|
||||||
|
sut.compile(scriptData);
|
||||||
|
// assert
|
||||||
|
expect(parser.callHistory.length).to.equal(1);
|
||||||
|
expect(parser.callHistory[0].functions).to.deep.equal(expectedFunctions);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('rethrows error with script name', () => {
|
it('rethrows error with script name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -159,7 +173,7 @@ describe('ScriptCompiler', () => {
|
|||||||
const syntax = new LanguageSyntaxStub();
|
const syntax = new LanguageSyntaxStub();
|
||||||
const invalidCode: ICompiledCode = { code: undefined, revertCode: undefined };
|
const invalidCode: ICompiledCode = { code: undefined, revertCode: undefined };
|
||||||
const realExceptionMessage = collectExceptionMessage(
|
const realExceptionMessage = collectExceptionMessage(
|
||||||
() => new ScriptCode(invalidCode.code, invalidCode.revertCode, syntax),
|
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
|
||||||
);
|
);
|
||||||
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
|
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
|
||||||
const callCompiler: IFunctionCallCompiler = {
|
const callCompiler: IFunctionCallCompiler = {
|
||||||
@@ -177,6 +191,26 @@ describe('ScriptCompiler', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
|
it('validates compiled code as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedRules = [
|
||||||
|
NoEmptyLines,
|
||||||
|
// Allow duplicated lines to enable calling same function multiple times
|
||||||
|
];
|
||||||
|
const scriptData = ScriptDataStub.createWithCall();
|
||||||
|
const validator = new CodeValidatorStub();
|
||||||
|
const sut = new ScriptCompilerBuilder()
|
||||||
|
.withSomeFunctions()
|
||||||
|
.withCodeValidator(validator)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const compilationResult = sut.compile(scriptData);
|
||||||
|
// assert
|
||||||
|
validator.assertHistory({
|
||||||
|
validatedCodes: [compilationResult.execute, compilationResult.revert],
|
||||||
|
rules: expectedRules,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,6 +229,8 @@ class ScriptCompilerBuilder {
|
|||||||
|
|
||||||
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
|
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
|
||||||
|
|
||||||
|
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||||
|
|
||||||
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
|
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
|
||||||
this.functions = functions;
|
this.functions = functions;
|
||||||
return this;
|
return this;
|
||||||
@@ -227,6 +263,13 @@ class ScriptCompilerBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withCodeValidator(
|
||||||
|
codeValidator: ICodeValidator,
|
||||||
|
): ScriptCompilerBuilder {
|
||||||
|
this.codeValidator = codeValidator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
|
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
|
||||||
this.callCompiler = callCompiler;
|
this.callCompiler = callCompiler;
|
||||||
return this;
|
return this;
|
||||||
@@ -239,8 +282,9 @@ class ScriptCompilerBuilder {
|
|||||||
return new ScriptCompiler(
|
return new ScriptCompiler(
|
||||||
this.functions,
|
this.functions,
|
||||||
this.syntax,
|
this.syntax,
|
||||||
this.callCompiler,
|
|
||||||
this.sharedFunctionsParser,
|
this.sharedFunctionsParser,
|
||||||
|
this.callCompiler,
|
||||||
|
this.codeValidator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
|||||||
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { IEnumParser } from '@/application/Common/Enum';
|
import { IEnumParser } from '@/application/Common/Enum';
|
||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
|
||||||
describe('ScriptParser', () => {
|
describe('ScriptParser', () => {
|
||||||
describe('parseScript', () => {
|
describe('parseScript', () => {
|
||||||
@@ -155,6 +159,53 @@ describe('ScriptParser', () => {
|
|||||||
expect(act).to.not.throw();
|
expect(act).to.not.throw();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('validates a expected', () => {
|
||||||
|
it('validates script with inline code (that is not compiled)', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedRules = [
|
||||||
|
NoEmptyLines,
|
||||||
|
NoDuplicatedLines,
|
||||||
|
];
|
||||||
|
const validator = new CodeValidatorStub();
|
||||||
|
const script = ScriptDataStub
|
||||||
|
.createWithCode()
|
||||||
|
.withCode('expected code to be validated')
|
||||||
|
.withRevertCode('expected revert code to be validated');
|
||||||
|
// act
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(script)
|
||||||
|
.withCodeValidator(validator)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
validator.assertHistory({
|
||||||
|
validatedCodes: [script.code, script.revertCode],
|
||||||
|
rules: expectedRules,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('does not validate compiled code', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedRules = [];
|
||||||
|
const expectedCodeCalls = [];
|
||||||
|
const validator = new CodeValidatorStub();
|
||||||
|
const script = ScriptDataStub
|
||||||
|
.createWithCall();
|
||||||
|
const compiler = new ScriptCompilerStub()
|
||||||
|
.withCompileAbility(script, new ScriptCodeStub());
|
||||||
|
const parseContext = new CategoryCollectionParseContextStub()
|
||||||
|
.withCompiler(compiler);
|
||||||
|
// act
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(script)
|
||||||
|
.withCodeValidator(validator)
|
||||||
|
.withContext(parseContext)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
validator.assertHistory({
|
||||||
|
validatedCodes: expectedCodeCalls,
|
||||||
|
rules: expectedRules,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('invalid script data', () => {
|
describe('invalid script data', () => {
|
||||||
describe('validates script data', () => {
|
describe('validates script data', () => {
|
||||||
@@ -233,6 +284,13 @@ class TestBuilder {
|
|||||||
|
|
||||||
private factory: ScriptFactoryType = undefined;
|
private factory: ScriptFactoryType = undefined;
|
||||||
|
|
||||||
|
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||||
|
|
||||||
|
public withCodeValidator(codeValidator: ICodeValidator) {
|
||||||
|
this.codeValidator = codeValidator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public withData(data: ScriptData) {
|
public withData(data: ScriptData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
return this;
|
return this;
|
||||||
@@ -254,6 +312,6 @@ class TestBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public parseScript(): Script {
|
public parseScript(): Script {
|
||||||
return parseScript(this.data, this.context, this.parser, this.factory);
|
return parseScript(this.data, this.context, this.parser, this.factory, this.codeValidator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||||
|
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
||||||
|
|
||||||
|
interface ICodeValidationRuleTestCase {
|
||||||
|
testName: string;
|
||||||
|
codeLines: readonly string[];
|
||||||
|
expected: readonly IInvalidCodeLine[];
|
||||||
|
sut: ICodeValidationRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testCodeValidationRule(testCases: readonly ICodeValidationRuleTestCase[]) {
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
it(testCase.testName, () => {
|
||||||
|
// arrange
|
||||||
|
const { sut } = testCase;
|
||||||
|
const codeLines = createCodeLines(testCase.codeLines);
|
||||||
|
// act
|
||||||
|
const actual = sut.analyze(codeLines);
|
||||||
|
// assert
|
||||||
|
function sort(lines: readonly IInvalidCodeLine[]) { // To ignore order
|
||||||
|
return Array.from(lines).sort((a, b) => a.index - b.index);
|
||||||
|
}
|
||||||
|
expect(sort(actual)).to.deep.equal(sort(testCase.expected));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCodeLines(lines: readonly string[]): ICodeLine[] {
|
||||||
|
return lines.map((lineText, index): ICodeLine => (
|
||||||
|
{
|
||||||
|
index: index + 1,
|
||||||
|
text: lineText,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
|
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||||
|
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||||
|
|
||||||
|
describe('NoDuplicatedLines', () => {
|
||||||
|
describe('ctor', () => {
|
||||||
|
describe('throws if syntax is missing', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing syntax';
|
||||||
|
const syntax = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => new NoDuplicatedLines(syntax);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('analyze', () => {
|
||||||
|
testCodeValidationRule([
|
||||||
|
{
|
||||||
|
testName: 'no results when code is valid',
|
||||||
|
codeLines: ['unique1', 'unique2', 'unique3', 'unique4'],
|
||||||
|
expected: [],
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'detects single duplicated line as expected',
|
||||||
|
codeLines: ['duplicate', 'duplicate', 'unique', 'duplicate'],
|
||||||
|
expected: expectInvalidCodeLines([1, 2, 4]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'detects multiple duplicated lines as expected',
|
||||||
|
codeLines: ['duplicate1', 'duplicate2', 'unique', 'duplicate1', 'unique2', 'duplicate2'],
|
||||||
|
expected: expectInvalidCodeLines([1, 4], [2, 6]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'common code parts: does not detect multiple common code part usages as duplicates',
|
||||||
|
codeLines: ['good', 'good', 'bad', 'bad', 'good', 'also-good', 'also-good', 'unique'],
|
||||||
|
expected: expectInvalidCodeLines([3, 4]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||||
|
.withCommonCodeParts('good', 'also-good')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'common code parts: does not detect multiple common code part used in same code line as duplicates',
|
||||||
|
codeLines: ['bad', 'bad', 'good1 good2', 'good1 good2', 'good2 good1', 'good2 good1'],
|
||||||
|
expected: expectInvalidCodeLines([1, 2]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||||
|
.withCommonCodeParts('good2', 'good1')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'common code parts: detects when common code parts used in conjunction with unique words',
|
||||||
|
codeLines: [
|
||||||
|
'common-part1', 'common-part1', 'common-part1 common-part2', 'common-part1 unique', 'common-part1 unique',
|
||||||
|
'common-part2', 'common-part2 common-part1', 'unique common-part2', 'unique common-part2',
|
||||||
|
],
|
||||||
|
expected: expectInvalidCodeLines([4, 5], [8, 9]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||||
|
.withCommonCodeParts('common-part1', 'common-part2')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'comments: does not when lines start with comment',
|
||||||
|
codeLines: ['#abc', '#abc', 'abc', 'unique', 'abc', '//abc', '//abc', '//unique', '#unique'],
|
||||||
|
expected: expectInvalidCodeLines([3, 5]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||||
|
.withCommentDelimiters('#', '//')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'comments: does when comments come after lien start',
|
||||||
|
codeLines: ['test #comment', 'test #comment', 'test2 # comment', 'test2 # comment'],
|
||||||
|
expected: expectInvalidCodeLines([1, 2], [3, 4]),
|
||||||
|
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||||
|
.withCommentDelimiters('#')),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function expectInvalidCodeLines(
|
||||||
|
...lines: readonly ReadonlyArray<number>[]
|
||||||
|
): IInvalidCodeLine[] {
|
||||||
|
return lines.flatMap((occurrenceIndices): readonly IInvalidCodeLine[] => occurrenceIndices
|
||||||
|
.map((index): IInvalidCodeLine => ({
|
||||||
|
index,
|
||||||
|
error: `Line is duplicated at line numbers ${occurrenceIndices.join(',')}.`,
|
||||||
|
})));
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
|
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||||
|
|
||||||
|
describe('NoEmptyLines', () => {
|
||||||
|
describe('analyze', () => {
|
||||||
|
testCodeValidationRule([
|
||||||
|
{
|
||||||
|
testName: 'no results when code is valid',
|
||||||
|
codeLines: ['non-empty-line1', 'none-empty-line2'],
|
||||||
|
expected: [],
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'shows error for empty line',
|
||||||
|
codeLines: ['first line', '', 'third line'],
|
||||||
|
expected: [{ index: 2, error: 'Empty line' }],
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'shows error for multiple empty lines',
|
||||||
|
codeLines: ['first line', '', 'third line', ''],
|
||||||
|
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'shows error for undefined and null lines',
|
||||||
|
codeLines: ['first line', undefined, 'third line', null],
|
||||||
|
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'shows error for whitespace-only lines',
|
||||||
|
codeLines: ['first line', ' ', 'third line'],
|
||||||
|
expected: [{ index: 2, error: 'Empty line: "{whitespace}{whitespace}"' }],
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'shows error for tab-only lines',
|
||||||
|
codeLines: ['first line', '\t\t', 'third line'],
|
||||||
|
expected: [{ index: 2, error: 'Empty line: "{tab}{tab}"' }],
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'shows error for lines that consists of whitespace and tabs',
|
||||||
|
codeLines: ['first line', '\t \t', 'third line', ' \t '],
|
||||||
|
expected: [{ index: 2, error: 'Empty line: "{tab}{whitespace}{tab}"' }, { index: 4, error: 'Empty line: "{whitespace}{tab}{whitespace}"' }],
|
||||||
|
sut: new NoEmptyLines(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ShellScriptSyntax } from '@/application/Parser/Script/Validation/Syntax/ShellScriptSyntax';
|
||||||
import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
|
import { BatchFileSyntax } from '@/application/Parser/Script/Validation/Syntax/BatchFileSyntax';
|
||||||
|
|
||||||
function getSystemsUnderTest(): ILanguageSyntax[] {
|
function getSystemsUnderTest(): ILanguageSyntax[] {
|
||||||
return [new BatchFileSyntax(), new ShellScriptSyntax()];
|
return [new BatchFileSyntax(), new ShellScriptSyntax()];
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { SyntaxFactory } from '@/application/Parser/Script/Syntax/SyntaxFactory';
|
import { SyntaxFactory } from '@/application/Parser/Script/Validation/Syntax/SyntaxFactory';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
|
import { ShellScriptSyntax } from '@/application/Parser/Script/Validation/Syntax/ShellScriptSyntax';
|
||||||
import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
|
|
||||||
import { ScriptingLanguageFactoryTestRunner } from '@tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner';
|
import { ScriptingLanguageFactoryTestRunner } from '@tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner';
|
||||||
|
import { BatchFileSyntax } from '@/application/Parser/Script/Validation/Syntax/BatchFileSyntax';
|
||||||
|
|
||||||
describe('SyntaxFactory', () => {
|
describe('SyntaxFactory', () => {
|
||||||
const sut = new SyntaxFactory();
|
const sut = new SyntaxFactory();
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { AbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
|
||||||
import { AbsentStringTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
||||||
|
|
||||||
describe('ScriptCode', () => {
|
describe('ScriptCode', () => {
|
||||||
describe('code', () => {
|
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"', () => {
|
describe('sets as expected with valid "execute" or "revert"', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const syntax = new LanguageSyntaxStub()
|
|
||||||
.withCommonCodeParts(')', 'else', '(')
|
|
||||||
.withCommentDelimiters('#', '//');
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
testName: 'code is a valid string',
|
testName: 'code and revert code is given',
|
||||||
code: 'valid code',
|
code: 'valid code',
|
||||||
|
revertCode: 'valid revert-code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'code consists of common code parts',
|
testName: 'only code is given but not revert code',
|
||||||
code: syntax.commonCodeParts.join(' '),
|
code: 'valid code',
|
||||||
},
|
revertCode: undefined,
|
||||||
{
|
|
||||||
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`,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// 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
|
// assert
|
||||||
for (const action of actions) {
|
for (const testCase of testCases) {
|
||||||
it(action.testName, () => {
|
it(testCase.testName, () => {
|
||||||
const sut = action.act();
|
// act
|
||||||
expect(action.expect(sut));
|
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 {
|
class ScriptCodeBuilder {
|
||||||
@@ -156,8 +73,6 @@ class ScriptCodeBuilder {
|
|||||||
|
|
||||||
public revert = '';
|
public revert = '';
|
||||||
|
|
||||||
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
|
||||||
|
|
||||||
public withExecute(execute: string) {
|
public withExecute(execute: string) {
|
||||||
this.execute = execute;
|
this.execute = execute;
|
||||||
return this;
|
return this;
|
||||||
@@ -168,16 +83,10 @@ class ScriptCodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withSyntax(syntax: ILanguageSyntax) {
|
|
||||||
this.syntax = syntax;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public build(): ScriptCode {
|
public build(): ScriptCode {
|
||||||
return new ScriptCode(
|
return new ScriptCode(
|
||||||
this.execute,
|
this.execute,
|
||||||
this.revert,
|
this.revert,
|
||||||
this.syntax,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||||
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
|
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { ScriptCompilerStub } from './ScriptCompilerStub';
|
import { ScriptCompilerStub } from './ScriptCompilerStub';
|
||||||
import { LanguageSyntaxStub } from './LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from './LanguageSyntaxStub';
|
||||||
|
|
||||||
|
|||||||
18
tests/unit/shared/Stubs/CodeValidationRuleStub.ts
Normal file
18
tests/unit/shared/Stubs/CodeValidationRuleStub.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
||||||
|
import { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||||
|
|
||||||
|
export class CodeValidationRuleStub implements ICodeValidationRule {
|
||||||
|
public readonly receivedLines = new Array<readonly ICodeLine[]>();
|
||||||
|
|
||||||
|
private returnValue: IInvalidCodeLine[] = [];
|
||||||
|
|
||||||
|
public withReturnValue(lines: readonly IInvalidCodeLine[]) {
|
||||||
|
this.returnValue = [...lines];
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
|
this.receivedLines.push(...[lines]);
|
||||||
|
return this.returnValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
tests/unit/shared/Stubs/CodeValidatorStub.ts
Normal file
34
tests/unit/shared/Stubs/CodeValidatorStub.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { ICodeValidationRule } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||||
|
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { Type } from '../Type';
|
||||||
|
|
||||||
|
export class CodeValidatorStub implements ICodeValidator {
|
||||||
|
public callHistory = new Array<{
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
}>();
|
||||||
|
|
||||||
|
public throwIfInvalid(
|
||||||
|
code: string,
|
||||||
|
rules: readonly ICodeValidationRule[],
|
||||||
|
): void {
|
||||||
|
this.callHistory.push({
|
||||||
|
code,
|
||||||
|
rules: Array.from(rules),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertHistory(expected: {
|
||||||
|
validatedCodes: readonly string[],
|
||||||
|
rules: readonly Type<ICodeValidationRule>[],
|
||||||
|
}) {
|
||||||
|
expect(this.callHistory).to.have.lengthOf(expected.validatedCodes.length);
|
||||||
|
const actualValidatedCodes = this.callHistory.map((args) => args.code);
|
||||||
|
expect(actualValidatedCodes.sort()).deep.equal([...expected.validatedCodes].sort());
|
||||||
|
for (const call of this.callHistory) {
|
||||||
|
const actualRules = call.rules.map((rule) => rule.constructor);
|
||||||
|
expect(actualRules.sort()).to.deep.equal([...expected.rules].sort());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
|
|||||||
givenArgs: FunctionCallArgumentCollectionStub,
|
givenArgs: FunctionCallArgumentCollectionStub,
|
||||||
) {
|
) {
|
||||||
return this
|
return this
|
||||||
.setup({ givenCode: func.body.code.do, givenArgs, result: func.body.code.do })
|
.setup({ givenCode: func.body.code.execute, givenArgs, result: func.body.code.execute })
|
||||||
.setup({ givenCode: func.body.code.revert, givenArgs, result: func.body.code.revert });
|
.setup({ givenCode: func.body.code.revert, givenArgs, result: func.body.code.revert });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { IFunctionCode } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { IFunctionCode } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
|
|
||||||
export class FunctionCodeStub implements IFunctionCode {
|
export class FunctionCodeStub implements IFunctionCode {
|
||||||
public do = 'do code (function-code-stub)';
|
public execute = 'execute code (function-code-stub)';
|
||||||
|
|
||||||
public revert? = 'revert code (function-code-stub)';
|
public revert? = 'revert code (function-code-stub)';
|
||||||
|
|
||||||
public withDo(code: string) {
|
public withExecute(code: string) {
|
||||||
this.do = code;
|
this.execute = code;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
|
|
||||||
export class LanguageSyntaxStub implements ILanguageSyntax {
|
export class LanguageSyntaxStub implements ILanguageSyntax {
|
||||||
public commentDelimiters = [];
|
public commentDelimiters = [];
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class SharedFunctionStub implements ISharedFunction {
|
|||||||
return {
|
return {
|
||||||
type: this.bodyType,
|
type: this.bodyType,
|
||||||
code: this.bodyType === FunctionBodyType.Code ? {
|
code: this.bodyType === FunctionBodyType.Code ? {
|
||||||
do: this.code,
|
execute: this.code,
|
||||||
revert: this.revertCode,
|
revert: this.revertCode,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
calls: this.bodyType === FunctionBodyType.Calls ? this.calls : undefined,
|
calls: this.bodyType === FunctionBodyType.Calls ? this.calls : undefined,
|
||||||
|
|||||||
@@ -2,9 +2,15 @@ import type { FunctionData } from '@/application/collections/';
|
|||||||
import { sequenceEqual } from '@/application/Common/Array';
|
import { sequenceEqual } from '@/application/Common/Array';
|
||||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
||||||
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
|
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
|
||||||
|
|
||||||
export class SharedFunctionsParserStub implements ISharedFunctionsParser {
|
export class SharedFunctionsParserStub implements ISharedFunctionsParser {
|
||||||
|
public callHistory = new Array<{
|
||||||
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
}>();
|
||||||
|
|
||||||
private setupResults = new Array<{
|
private setupResults = new Array<{
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
result: ISharedFunctionCollection,
|
result: ISharedFunctionCollection,
|
||||||
@@ -14,7 +20,14 @@ export class SharedFunctionsParserStub implements ISharedFunctionsParser {
|
|||||||
this.setupResults.push({ functions, result });
|
this.setupResults.push({ functions, result });
|
||||||
}
|
}
|
||||||
|
|
||||||
public parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
public parseFunctions(
|
||||||
|
functions: readonly FunctionData[],
|
||||||
|
syntax: ILanguageSyntax,
|
||||||
|
): ISharedFunctionCollection {
|
||||||
|
this.callHistory.push({
|
||||||
|
functions: Array.from(functions),
|
||||||
|
syntax,
|
||||||
|
});
|
||||||
const result = this.findResult(functions);
|
const result = this.findResult(functions);
|
||||||
return result || new SharedFunctionCollectionStub();
|
return result || new SharedFunctionCollectionStub();
|
||||||
}
|
}
|
||||||
|
|||||||
24
tests/unit/shared/TestCases/SingletonTests.ts
Normal file
24
tests/unit/shared/TestCases/SingletonTests.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { Type } from '../Type';
|
||||||
|
|
||||||
|
interface ISingletonTestData<T> {
|
||||||
|
getter: () => T;
|
||||||
|
expectedType: Type<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function itIsSingleton<T>(test: ISingletonTestData<T>): void {
|
||||||
|
it('gets the expected type', () => {
|
||||||
|
// act
|
||||||
|
const instance = test.getter();
|
||||||
|
// assert
|
||||||
|
expect(instance).to.be.instanceOf(test.expectedType);
|
||||||
|
});
|
||||||
|
it('multiple calls get the same instance', () => {
|
||||||
|
// act
|
||||||
|
const instance1 = test.getter();
|
||||||
|
const instance2 = test.getter();
|
||||||
|
// assert
|
||||||
|
expect(instance1).to.equal(instance2);
|
||||||
|
});
|
||||||
|
}
|
||||||
5
tests/unit/shared/Type.ts
Normal file
5
tests/unit/shared/Type.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
export type Type<T, TArgs extends unknown[] = never> = Function & {
|
||||||
|
prototype: T,
|
||||||
|
apply: (this: unknown, args: TArgs) => void
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user