allow functions to call other functions #53
This commit is contained in:
@@ -101,11 +101,15 @@
|
||||
### `Function`
|
||||
|
||||
- Functions allow re-usable code throughout the defined scripts.
|
||||
- Functions are templates compiled by privacy.sexy and uses special expressions.
|
||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||
- Functions are templates compiled by privacy.sexy and uses special [expressions](#expressions).
|
||||
- Functions can call other functions by defining `call` property instead of `code`
|
||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||
|
||||
#### Parameter substitution
|
||||
#### Expressions
|
||||
|
||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||
|
||||
##### Parameter substitution
|
||||
|
||||
A simple function example
|
||||
|
||||
@@ -125,6 +129,22 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
||||
argument: World
|
||||
```
|
||||
|
||||
A function can call other functions such as:
|
||||
|
||||
```yaml
|
||||
-
|
||||
function: CallerFunction
|
||||
parameters: [ 'value' ]
|
||||
call:
|
||||
function: EchoArgument
|
||||
parameters:
|
||||
argument: {{ $value }}
|
||||
-
|
||||
function: EchoArgument
|
||||
parameters: [ 'argument' ]
|
||||
code: Hello {{ $argument }} !
|
||||
```
|
||||
|
||||
#### `Function` syntax
|
||||
|
||||
- `name`: *`string`* (**required**)
|
||||
@@ -135,15 +155,20 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
||||
- `parameters`: `[` *`string`* `, ... ]`
|
||||
- Name of the parameters that the function has.
|
||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
||||
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
||||
- Parameter names must be defined to be used in [expressions](#expressions)
|
||||
- ❗ Parameter names must be unique
|
||||
`code`: *`string`* (**required**)
|
||||
`code`: *`string`* (**required** if `call` is undefined)
|
||||
- Batch file commands that will be executed
|
||||
- 💡 If defined, best practice to also define `revertCode`
|
||||
- ❗ If not defined `call` must be defined
|
||||
- `revertCode`: *`string`*
|
||||
- Code that'll undo the change done by `code` property.
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- The parameter values that are sent can use [expressions](#expressions)
|
||||
- ❗ If not defined `code` must be defined
|
||||
|
||||
### `ScriptingDefinition`
|
||||
|
||||
|
||||
29
docs/tests.md
Normal file
29
docs/tests.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Unit tests
|
||||
|
||||
- Unit tests are defined in [`./tests`](./../tests)
|
||||
- They follow same folder structure as [`./src`](./../src)
|
||||
|
||||
## Naming
|
||||
|
||||
- Each test suite first describe the system under test
|
||||
- E.g. tests for class `Application` is categorized under `Application`
|
||||
- Tests for specific methods are categorized under method name (if applicable)
|
||||
- E.g. test for `run()` is categorized under `run`
|
||||
|
||||
## Act, arrange, assert
|
||||
|
||||
- Tests use act, arrange and assert (AAA) pattern when applicable
|
||||
- **Arrange**
|
||||
- Should set up the test case
|
||||
- Starts with comment line `// arrange`
|
||||
- **Act**
|
||||
- Should cover the main thing to be tested
|
||||
- Starts with comment line `// act`
|
||||
- **Assert**
|
||||
- Should elicit some sort of response
|
||||
- Starts with comment line `// assert`
|
||||
|
||||
## Stubs
|
||||
|
||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
||||
- They implement dummy behavior to be functional
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
|
||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
public static readonly instance: IExpressionsCompiler = new ExpressionsCompiler();
|
||||
protected constructor() { }
|
||||
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
return intermediateCode.compile();
|
||||
}
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: ParameterValueDictionary): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
ensureValuesProvided(parameterNames, parameters);
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
}
|
||||
return intermediateCode;
|
||||
}
|
||||
|
||||
function ensureValuesProvided(names: string[], nameValues: ParameterValueDictionary) {
|
||||
nameValues = nameValues || {};
|
||||
const notProvidedNames = names.filter((name) => !Boolean(nameValues[name]));
|
||||
if (notProvidedNames.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedNames)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface ParameterValueDictionary { [parameterName: string]: string; }
|
||||
|
||||
export interface IExpressionsCompiler {
|
||||
compileExpressions(code: string, parameters?: ParameterValueDictionary): string;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { FunctionData, InstructionHolder } from 'js-yaml-loader!*';
|
||||
import { SharedFunction } from './SharedFunction';
|
||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
import { IFunctionCompiler } from './IFunctionCompiler';
|
||||
import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler';
|
||||
|
||||
export class FunctionCompiler implements IFunctionCompiler {
|
||||
public static readonly instance: IFunctionCompiler = new FunctionCompiler();
|
||||
protected constructor(
|
||||
private readonly functionCallCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance) {
|
||||
}
|
||||
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
return collection;
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
functions
|
||||
.filter((func) => hasCode(func))
|
||||
.forEach((func) => {
|
||||
const shared = new SharedFunction(func.name, func.parameters, func.code, func.revertCode);
|
||||
collection.addFunction(shared);
|
||||
});
|
||||
functions
|
||||
.filter((func) => hasCall(func))
|
||||
.forEach((func) => {
|
||||
const code = this.functionCallCompiler.compileCall(func.call, collection);
|
||||
const shared = new SharedFunction(func.name, func.parameters, code.code, code.revertCode);
|
||||
collection.addFunction(shared);
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
function hasCode(data: FunctionData): boolean {
|
||||
return Boolean(data.code);
|
||||
}
|
||||
|
||||
function hasCall(data: FunctionData): boolean {
|
||||
return Boolean(data.call);
|
||||
}
|
||||
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicatesInParameterNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
||||
// Ensure functions do not define both call and code
|
||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||
if (withBothCallAndCode.length) {
|
||||
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
|
||||
}
|
||||
// Ensure functions have either code or call
|
||||
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
|
||||
if (hasEitherCodeOrCall.length) {
|
||||
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
||||
}
|
||||
}
|
||||
function printNames(holders: readonly InstructionHolder[]) {
|
||||
return printList(holders.map((holder) => holder.name));
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
}
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||
const functionsWithParameters = functions
|
||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||
for (const func of functionsWithParameters) {
|
||||
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||
if (duplicateParameterNames.length) {
|
||||
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions
|
||||
.map((func) => func.code)
|
||||
.filter((code) => code),
|
||||
);
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDuplicates(texts: readonly string[]): string[] {
|
||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
export interface IFunctionCompiler {
|
||||
compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
readonly parameters?: readonly string[];
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export class SharedFunction implements ISharedFunction {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: readonly string[],
|
||||
public readonly code: string,
|
||||
public readonly revertCode: string,
|
||||
) {
|
||||
if (!name) { throw new Error('undefined function name'); }
|
||||
if (!code) { throw new Error(`undefined function ("${name}") code`); }
|
||||
this.parameters = parameters || [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||
|
||||
public addFunction(func: ISharedFunction): void {
|
||||
if (!func) { throw new Error('undefined function'); }
|
||||
if (this.functionsByName.has(func.name)) {
|
||||
throw new Error(`function with name ${func.name} already exists`);
|
||||
}
|
||||
this.functionsByName.set(func.name, func);
|
||||
}
|
||||
|
||||
public getFunctionByName(name: string): ISharedFunction {
|
||||
if (!name) { throw Error('undefined function name'); }
|
||||
const func = this.functionsByName.get(name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { FunctionCallData, FunctionCallParametersData, FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { IExpressionsCompiler } from '../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = ExpressionsCompiler.instance) { }
|
||||
public compileCall(
|
||||
call: ScriptFunctionCallData,
|
||||
functions: ISharedFunctionCollection): ICompiledCode {
|
||||
if (!functions) { throw new Error('undefined functions'); }
|
||||
if (!call) { throw new Error('undefined call'); }
|
||||
const compiledCodes = new Array<ICompiledCode>();
|
||||
const calls = getCallSequence(call);
|
||||
calls.forEach((currentCall, currentCallIndex) => {
|
||||
ensureValidCall(currentCall);
|
||||
const commonFunction = functions.getFunctionByName(currentCall.function);
|
||||
ensureExpectedParameters(commonFunction, currentCall);
|
||||
let functionCode = compileCode(commonFunction, currentCall.parameters, this.expressionsCompiler);
|
||||
if (currentCallIndex !== calls.length - 1) {
|
||||
functionCode = appendLine(functionCode);
|
||||
}
|
||||
compiledCodes.push(functionCode);
|
||||
});
|
||||
const compiledCode = merge(compiledCodes);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
||||
if (!func.parameters && !call.parameters) {
|
||||
return;
|
||||
}
|
||||
const unexpectedParameters = Object.keys(call.parameters || {})
|
||||
.filter((callParam) => !func.parameters.includes(callParam));
|
||||
if (unexpectedParameters.length) {
|
||||
throw new Error(
|
||||
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||
return {
|
||||
code: codes.map((code) => code.code).join(''),
|
||||
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||
};
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
func: FunctionData,
|
||||
parameters: FunctionCallParametersData,
|
||||
compiler: IExpressionsCompiler): ICompiledCode {
|
||||
return {
|
||||
code: compiler.compileExpressions(func.code, parameters),
|
||||
revertCode: compiler.compileExpressions(func.revertCode, parameters),
|
||||
};
|
||||
}
|
||||
|
||||
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||
if (typeof call !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
}
|
||||
if (call instanceof Array) {
|
||||
return call as FunctionCallData[];
|
||||
}
|
||||
return [ call as FunctionCallData ];
|
||||
}
|
||||
|
||||
function ensureValidCall(call: FunctionCallData) {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call`);
|
||||
}
|
||||
if (!call.function) {
|
||||
throw new Error(`empty function name called`);
|
||||
}
|
||||
}
|
||||
|
||||
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||
return {
|
||||
code: appendLineIfNotEmpty(code.code),
|
||||
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
|
||||
|
||||
export interface IFunctionCallCompiler {
|
||||
compileCall(
|
||||
call: ScriptFunctionCallData,
|
||||
functions: ISharedFunctionCollection): ICompiledCode;
|
||||
}
|
||||
@@ -1,184 +1,42 @@
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
interface ICompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
}
|
||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCallCompiler } from './FunctionCall/FunctionCallCompiler';
|
||||
import { IFunctionCompiler } from './Function/IFunctionCompiler';
|
||||
import { FunctionCompiler } from './Function/FunctionCompiler';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
constructor(
|
||||
private readonly functions: readonly FunctionData[] | undefined,
|
||||
private syntax: ILanguageSyntax) {
|
||||
ensureValidFunctions(functions);
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
private readonly syntax: ILanguageSyntax,
|
||||
functionCompiler: IFunctionCompiler = FunctionCompiler.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
this.functions = functionCompiler.compileFunctions(functions);
|
||||
}
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
if (!script.call) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
this.ensureCompilable(script.call);
|
||||
const compiledCodes = new Array<ICompiledCode>();
|
||||
const calls = getCallSequence(script.call);
|
||||
calls.forEach((currentCall, currentCallIndex) => {
|
||||
ensureValidCall(currentCall, script.name);
|
||||
const commonFunction = this.getFunctionByName(currentCall.function);
|
||||
ensureExpectedParameters(commonFunction, currentCall);
|
||||
let functionCode = compileCode(commonFunction, currentCall.parameters);
|
||||
if (currentCallIndex !== calls.length - 1) {
|
||||
functionCode = appendLine(functionCode);
|
||||
}
|
||||
compiledCodes.push(functionCode);
|
||||
});
|
||||
const scriptCode = merge(compiledCodes);
|
||||
return new ScriptCode(scriptCode.code, scriptCode.revertCode, script.name, this.syntax);
|
||||
}
|
||||
|
||||
private getFunctionByName(name: string): FunctionData {
|
||||
const func = this.functions.find((f) => f.name === name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
private ensureCompilable(call: ScriptFunctionCallData) {
|
||||
if (!this.functions || this.functions.length === 0) {
|
||||
throw new Error('cannot compile without shared functions');
|
||||
}
|
||||
if (typeof call !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
try {
|
||||
const compiledCode = this.callCompiler.compileCall(script.call, this.functions);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
this.syntax);
|
||||
} catch (error) {
|
||||
throw Error(`Script "${script.name}" ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
||||
if (!func.parameters && !call.parameters) {
|
||||
return;
|
||||
}
|
||||
const unexpectedParameters = Object.keys(call.parameters || {})
|
||||
.filter((callParam) => !func.parameters.includes(callParam));
|
||||
if (unexpectedParameters.length) {
|
||||
throw new Error(
|
||||
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDuplicates(texts: readonly string[]): string[] {
|
||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
}
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||
const functionsWithParameters = functions
|
||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||
for (const func of functionsWithParameters) {
|
||||
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||
if (duplicateParameterNames.length) {
|
||||
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
if (!functions || functions.length === 0) {
|
||||
return;
|
||||
}
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicatesInParameterNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
}
|
||||
|
||||
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||
return {
|
||||
code: appendLineIfNotEmpty(code.code),
|
||||
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||
};
|
||||
}
|
||||
|
||||
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||
return {
|
||||
code: codes.map((code) => code.code).join(''),
|
||||
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||
};
|
||||
}
|
||||
|
||||
function compileCode(func: FunctionData, parameters: FunctionCallParametersData): ICompiledCode {
|
||||
return {
|
||||
code: compileExpressions(func.code, parameters),
|
||||
revertCode: compileExpressions(func.revertCode, parameters),
|
||||
};
|
||||
}
|
||||
|
||||
function compileExpressions(code: string, parameters: FunctionCallParametersData): string {
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
return intermediateCode.compile();
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: FunctionCallParametersData): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
if (parameterNames.length && !parameters) {
|
||||
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
||||
}
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
if (!parameterValue) {
|
||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||
}
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
}
|
||||
return intermediateCode;
|
||||
}
|
||||
|
||||
function ensureValidCall(call: FunctionCallData, scriptName: string) {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call in script "${scriptName}"`);
|
||||
}
|
||||
if (!call.function) {
|
||||
throw new Error(`empty function name called in script "${scriptName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||
if (call instanceof Array) {
|
||||
return call as FunctionCallData[];
|
||||
}
|
||||
return [ call as FunctionCallData ];
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function parseCode(script: ScriptData, context: ICategoryCollectionParseContext)
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
return new ScriptCode(script.code, script.revertCode, script.name, context.syntax);
|
||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
||||
}
|
||||
|
||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { generateIlCode } from './Script/Compiler/ILCode';
|
||||
import { generateIlCode } from './Script/Compiler/Expressions/ILCode';
|
||||
|
||||
export function parseScriptingDefinition(
|
||||
definition: ScriptingDefinitionData,
|
||||
|
||||
31
src/application/collections/collection.yaml.d.ts
vendored
31
src/application/collections/collection.yaml.d.ts
vendored
@@ -18,30 +18,33 @@ declare module 'js-yaml-loader!*' {
|
||||
readonly docs?: DocumentationUrlsData;
|
||||
}
|
||||
|
||||
export interface FunctionData {
|
||||
name: string;
|
||||
code: string;
|
||||
revertCode?: string;
|
||||
parameters?: readonly string[];
|
||||
export interface InstructionHolder {
|
||||
readonly name: string;
|
||||
|
||||
readonly code?: string;
|
||||
readonly revertCode?: string;
|
||||
|
||||
readonly call?: ScriptFunctionCallData;
|
||||
}
|
||||
|
||||
export interface FunctionData extends InstructionHolder {
|
||||
readonly parameters?: readonly string[];
|
||||
}
|
||||
|
||||
export interface FunctionCallParametersData {
|
||||
[index: string]: string;
|
||||
readonly [index: string]: string;
|
||||
}
|
||||
|
||||
export interface FunctionCallData {
|
||||
function: string;
|
||||
parameters?: FunctionCallParametersData;
|
||||
readonly function: string;
|
||||
readonly parameters?: FunctionCallParametersData;
|
||||
}
|
||||
|
||||
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||
|
||||
export interface ScriptData extends DocumentableData {
|
||||
name: string;
|
||||
code?: string;
|
||||
revertCode?: string;
|
||||
call: ScriptFunctionCallData;
|
||||
recommend?: string;
|
||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||
readonly name: string;
|
||||
readonly recommend?: string;
|
||||
}
|
||||
|
||||
export interface ScriptingDefinitionData {
|
||||
|
||||
@@ -2719,8 +2719,19 @@ actions:
|
||||
-
|
||||
name: Disable NetBios for all interfaces
|
||||
docs: https://10dsecurity.com/saying-goodbye-netbios/
|
||||
code: Powershell -Command "$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'; Get-ChildItem $key | foreach { Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose}"
|
||||
revertCode: Powershell -Command "$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'; Get-ChildItem $key | foreach { Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose}"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code:
|
||||
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
|
||||
Get-ChildItem $key | foreach {
|
||||
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose
|
||||
}
|
||||
revertCode:
|
||||
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
|
||||
Get-ChildItem $key | foreach {
|
||||
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose
|
||||
}
|
||||
-
|
||||
category: Remove bloatware
|
||||
children:
|
||||
@@ -4168,23 +4179,27 @@ functions:
|
||||
-
|
||||
name: UninstallStoreApp
|
||||
parameters: [ packageName ]
|
||||
code: PowerShell -Command "Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code: Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage
|
||||
revertCode:
|
||||
PowerShell -ExecutionPolicy Unrestricted -Command "
|
||||
$package = Get-AppxPackage -AllUsers '{{ $packageName }}';
|
||||
if (!$package) {
|
||||
Write-Error \"Cannot reinstall '{{ $packageName }}'\" -ErrorAction Stop
|
||||
}
|
||||
$manifest = $package.InstallLocation + '\AppxManifest.xml';
|
||||
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\" "
|
||||
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\"
|
||||
-
|
||||
name: UninstallSystemApp
|
||||
parameters: [ packageName ]
|
||||
# It simply renames files
|
||||
# Because system apps are non removable (check: (Get-AppxPackage -AllUsers 'Windows.CBSPreview').NonRemovable)
|
||||
# Otherwise they throw 0x80070032 when trying to uninstall them
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code:
|
||||
PowerShell -Command "
|
||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||
if (!$package) {
|
||||
Write-Host 'Not installed';
|
||||
@@ -4202,9 +4217,8 @@ functions:
|
||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||
}
|
||||
};"
|
||||
}
|
||||
revertCode:
|
||||
PowerShell -Command "
|
||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||
if (!$package) {
|
||||
Write-Error 'App could not be found' -ErrorAction Stop;
|
||||
@@ -4220,12 +4234,17 @@ functions:
|
||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||
}
|
||||
};"
|
||||
}
|
||||
-
|
||||
name: UninstallCapability
|
||||
parameters: [ capabilityName ]
|
||||
code: PowerShell -Command "Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online"
|
||||
revertCode: PowerShell -Command "$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'; Add-WindowsCapability -Name \"$capability.Name\" -Online"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code: Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online
|
||||
revertCode:
|
||||
$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*';
|
||||
Add-WindowsCapability -Name \"$capability.Name\" -Online
|
||||
-
|
||||
name: RenameSystemFile
|
||||
parameters: [ filePath ]
|
||||
@@ -4250,15 +4269,21 @@ functions:
|
||||
-
|
||||
name: SetVsCodeSetting
|
||||
parameters: [ setting, powerShellValue ]
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code:
|
||||
Powershell -Command "
|
||||
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
||||
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json;
|
||||
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force;
|
||||
$json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
$json | ConvertTo-Json | Set-Content $jsonfile;
|
||||
revertCode:
|
||||
Powershell -Command "
|
||||
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
||||
$json = Get-Content $jsonfile | ConvertFrom-Json;
|
||||
$json.PSObject.Properties.Remove('{{ $setting }}');
|
||||
$json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
$json | ConvertTo-Json | Set-Content $jsonfile;
|
||||
-
|
||||
name: RunPowerShell
|
||||
parameters: [ code, revertCode ]
|
||||
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code }}"
|
||||
revertCode: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $revertCode }}"
|
||||
|
||||
@@ -4,16 +4,17 @@ export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string,
|
||||
scriptName: string,
|
||||
syntax: ILanguageSyntax) {
|
||||
if (!scriptName) { throw new Error('script name is undefined'); }
|
||||
if (!syntax) { throw new Error('syntax is undefined'); }
|
||||
validateCode(scriptName, execute, syntax);
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
validateCode(execute, syntax);
|
||||
if (revert) {
|
||||
scriptName = `${scriptName} (revert)`;
|
||||
validateCode(scriptName, revert, syntax);
|
||||
try {
|
||||
validateCode(revert, syntax);
|
||||
if (execute === revert) {
|
||||
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
||||
throw new Error(`Code itself and its reverting code cannot be the same`);
|
||||
}
|
||||
} catch (err) {
|
||||
throw Error(`(revert): ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,21 +25,21 @@ export interface ILanguageSyntax {
|
||||
readonly commonCodeParts: string[];
|
||||
}
|
||||
|
||||
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error(`code of ${name} is empty or undefined`);
|
||||
throw new Error(`code is empty or undefined`);
|
||||
}
|
||||
ensureNoEmptyLines(name, code);
|
||||
ensureCodeHasUniqueLines(name, code, syntax);
|
||||
ensureNoEmptyLines(code);
|
||||
ensureCodeHasUniqueLines(code, syntax);
|
||||
}
|
||||
|
||||
function ensureNoEmptyLines(name: string, code: string): void {
|
||||
function ensureNoEmptyLines(code: string): void {
|
||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||
throw Error(`script has empty lines "${name}"`);
|
||||
throw Error(`script has empty lines`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
||||
const lines = code.split('\n')
|
||||
.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||
if (lines.length === 0) {
|
||||
@@ -46,7 +47,7 @@ function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageS
|
||||
}
|
||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||
if (duplicateLines.length !== 0) {
|
||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||
throw Error(`Duplicates detected in script :\n ${duplicateLines.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('CategoryCollectionParser', () => {
|
||||
const scriptName = 'script-name';
|
||||
const script = ScriptDataStub.createWithCall({ function: functionName })
|
||||
.withName(scriptName);
|
||||
const func = new FunctionDataStub()
|
||||
const func = FunctionDataStub.createWithCode()
|
||||
.withName(functionName)
|
||||
.withCode(expectedCode);
|
||||
const category = new CategoryDataStub()
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('CategoryCollectionParseContext', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined scripting';
|
||||
const scripting = undefined;
|
||||
const functionsData = [ new FunctionDataStub() ];
|
||||
const functionsData = [ FunctionDataStub.createWithCode() ];
|
||||
// act
|
||||
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
|
||||
// assert
|
||||
@@ -39,7 +39,7 @@ describe('CategoryCollectionParseContext', () => {
|
||||
describe('compiler', () => {
|
||||
it('constructed as expected', () => {
|
||||
// arrange
|
||||
const functionsData = [ new FunctionDataStub() ];
|
||||
const functionsData = [ FunctionDataStub.createWithCode() ];
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
const expected = new ScriptCompiler(functionsData, syntax);
|
||||
const language = ScriptingLanguage.shellscript;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
|
||||
describe('ExpressionsCompiler', () => {
|
||||
describe('parameter substitution', () => {
|
||||
describe('substitutes as expected', () => {
|
||||
// arrange
|
||||
const testCases = [ {
|
||||
name: 'with different parameters',
|
||||
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
||||
parameters: {
|
||||
firstParameter: 'llo',
|
||||
secondParameter: 'world',
|
||||
},
|
||||
expected: 'Hello world!',
|
||||
}, {
|
||||
name: 'with single parameter',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
parameter: 'Hodor',
|
||||
},
|
||||
expected: 'Hodor!',
|
||||
|
||||
}];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const sut = new MockableExpressionsCompiler();
|
||||
// act
|
||||
const actual = sut.compileExpressions(testCase.code, testCase.parameters);
|
||||
// assert
|
||||
expect(actual).to.equal(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('throws when expected value is not provided', () => {
|
||||
// arrange
|
||||
const noParameterTestCases = [
|
||||
{
|
||||
name: 'empty parameters',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'undefined parameters',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: undefined,
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'unnecessary parameter provided',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
unnecessaryParameter: 'unnecessaryValue',
|
||||
},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'undefined value',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
parameter: undefined,
|
||||
},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'multiple values are not',
|
||||
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
||||
parameters: {},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
|
||||
},
|
||||
{
|
||||
name: 'some values are provided',
|
||||
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
||||
parameters: {
|
||||
parameter2: 'value',
|
||||
},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
|
||||
},
|
||||
];
|
||||
for (const testCase of noParameterTestCases) {
|
||||
it(testCase.name, () => {
|
||||
const sut = new MockableExpressionsCompiler();
|
||||
// act
|
||||
const act = () => sut.compileExpressions(testCase.code, testCase.parameters);
|
||||
// assert
|
||||
expect(act).to.throw(testCase.expectedError);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class MockableExpressionsCompiler extends ExpressionsCompiler {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { generateIlCode } from '@/application/Parser/Script/Compiler/ILCode';
|
||||
import { generateIlCode } from '@/application/Parser/Script/Compiler/Expressions/ILCode';
|
||||
|
||||
describe('ILCode', () => {
|
||||
describe('getUniqueParameterNames', () => {
|
||||
@@ -0,0 +1,192 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCompiler } from '@/application/Parser/Script/Compiler/Function/FunctionCompiler';
|
||||
import { FunctionCallCompilerStub } from '../../../../../stubs/FunctionCallCompilerStub';
|
||||
import { FunctionDataStub } from '../../../../../stubs/FunctionDataStub';
|
||||
|
||||
describe('FunctionsCompiler', () => {
|
||||
describe('compileFunctions', () => {
|
||||
describe('validates functions', () => {
|
||||
it('throws if one of the functions is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = `some functions are undefined`;
|
||||
const functions = [ FunctionDataStub.createWithCode(), undefined ];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when functions have same names', () => {
|
||||
// arrange
|
||||
const name = 'same-func-name';
|
||||
const expectedError = `duplicate function name: "${name}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithCode().withName(name),
|
||||
FunctionDataStub.createWithCode().withName(name),
|
||||
];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when function parameters have same names', () => {
|
||||
// arrange
|
||||
const parameterName = 'duplicate-parameter';
|
||||
const func = FunctionDataStub.createWithCall()
|
||||
.withParameters(parameterName, parameterName);
|
||||
const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions([ func ]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws when when function have duplicate code', () => {
|
||||
it('code', () => {
|
||||
// arrange
|
||||
const code = 'duplicate-code';
|
||||
const expectedError = `duplicate "code" in functions: "${code}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
|
||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
|
||||
];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('revertCode', () => {
|
||||
// arrange
|
||||
const revertCode = 'duplicate-revert-code';
|
||||
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
||||
FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
||||
];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('both code and call are defined', () => {
|
||||
// arrange
|
||||
const functionName = 'invalid-function';
|
||||
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
|
||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(functionName)
|
||||
.withCode('code')
|
||||
.withMockCall();
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions([ invalidFunction ]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('neither code and call is defined', () => {
|
||||
// arrange
|
||||
const functionName = 'invalid-function';
|
||||
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(functionName);
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions([ invalidFunction ]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('returns empty with empty functions', () => {
|
||||
// arrange
|
||||
const emptyValues = [ [], undefined ];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
for (const emptyFunctions of emptyValues) {
|
||||
// act
|
||||
const actual = sut.compileFunctions(emptyFunctions);
|
||||
// assert
|
||||
expect(actual).to.not.equal(undefined);
|
||||
}
|
||||
});
|
||||
it('parses single function with code as expected', () => {
|
||||
// arrange
|
||||
const name = 'function-name';
|
||||
const expected = FunctionDataStub
|
||||
.createWithoutCallOrCodes()
|
||||
.withName(name)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code')
|
||||
.withParameters('expected-parameter-1', 'expected-parameter-2');
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const collection = sut.compileFunctions([ expected ]);
|
||||
// expect
|
||||
const actual = collection.getFunctionByName(name);
|
||||
expectEqualFunctions(expected, actual);
|
||||
});
|
||||
it('parses function with call as expected', () => {
|
||||
// arrange
|
||||
const calleeName = 'callee-function';
|
||||
const caller = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('caller-function')
|
||||
.withCall({ function: calleeName });
|
||||
const callee = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(calleeName)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code');
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const collection = sut.compileFunctions([ caller, callee ]);
|
||||
// expect
|
||||
const actual = collection.getFunctionByName(caller.name);
|
||||
expectEqualFunctionCode(callee, actual);
|
||||
});
|
||||
it('parses multiple functions with call as expected', () => {
|
||||
// arrange
|
||||
const calleeName = 'callee-function';
|
||||
const caller1 = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('caller-function')
|
||||
.withCall({ function: calleeName });
|
||||
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('caller-function-2')
|
||||
.withCall({ function: calleeName });
|
||||
const callee = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(calleeName)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code');
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const collection = sut.compileFunctions([ caller1, caller2, callee ]);
|
||||
// expect
|
||||
const compiledCaller1 = collection.getFunctionByName(caller1.name);
|
||||
const compiledCaller2 = collection.getFunctionByName(caller2.name);
|
||||
expectEqualFunctionCode(callee, compiledCaller1);
|
||||
expectEqualFunctionCode(callee, compiledCaller2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectEqualFunctions(expected: FunctionData, actual: ISharedFunction) {
|
||||
expect(actual.name).to.equal(expected.name);
|
||||
expect(actual.parameters).to.deep.equal(expected.parameters);
|
||||
expectEqualFunctionCode(expected, actual);
|
||||
}
|
||||
|
||||
function expectEqualFunctionCode(expected: FunctionData, actual: ISharedFunction) {
|
||||
expect(actual.code).to.equal(expected.code);
|
||||
expect(actual.revertCode).to.equal(expected.revertCode);
|
||||
}
|
||||
|
||||
class MockableFunctionCompiler extends FunctionCompiler {
|
||||
constructor(functionCallCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub()) {
|
||||
super(functionCallCompiler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SharedFunction } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
|
||||
|
||||
describe('SharedFunction', () => {
|
||||
describe('name', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-function-name';
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withName(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.name).equal(expected);
|
||||
});
|
||||
it('throws if empty or undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function name';
|
||||
const invalidValues = [ undefined, '' ];
|
||||
for (const invalidValue of invalidValues) {
|
||||
// act
|
||||
const act = () => new SharedFunctionBuilder()
|
||||
.withName(invalidValue)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('parameters', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = [ 'expected-parameter' ];
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withParameters(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.parameters).to.deep.equal(expected);
|
||||
});
|
||||
it('returns empty array if undefined', () => {
|
||||
// arrange
|
||||
const expected = [ ];
|
||||
const value = undefined;
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withParameters(value)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.parameters).to.not.equal(undefined);
|
||||
expect(sut.parameters).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('code', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-code';
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withCode(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.code).equal(expected);
|
||||
});
|
||||
it('throws if empty or undefined', () => {
|
||||
// arrange
|
||||
const functionName = 'expected-function-name';
|
||||
const expectedError = `undefined function ("${functionName}") code`;
|
||||
const invalidValues = [ undefined, '' ];
|
||||
for (const invalidValue of invalidValues) {
|
||||
// act
|
||||
const act = () => new SharedFunctionBuilder()
|
||||
.withName(functionName)
|
||||
.withCode(invalidValue)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('revertCode', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const testData = [ 'expected-revert-code', undefined, '' ];
|
||||
for (const data of testData) {
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withRevertCode(data)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.revertCode).equal(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class SharedFunctionBuilder {
|
||||
private name = 'name';
|
||||
private parameters: readonly string[] = [ 'parameter' ];
|
||||
private code = 'code';
|
||||
private revertCode = 'revert-code';
|
||||
|
||||
public build(): SharedFunction {
|
||||
return new SharedFunction(
|
||||
this.name,
|
||||
this.parameters,
|
||||
this.code,
|
||||
this.revertCode,
|
||||
);
|
||||
}
|
||||
public withName(name: string) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
public withParameters(parameters: readonly string[]) {
|
||||
this.parameters = parameters;
|
||||
return this;
|
||||
}
|
||||
public withCode(code: string) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
public withRevertCode(revertCode: string) {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/SharedFunctionCollection';
|
||||
import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
|
||||
|
||||
describe('SharedFunctionCollection', () => {
|
||||
describe('addFunction', () => {
|
||||
it('throws if function is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function';
|
||||
const func = undefined;
|
||||
const sut = new SharedFunctionCollection();
|
||||
// act
|
||||
const act = () => sut.addFunction(func);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if function with same name already exists', () => {
|
||||
// arrange
|
||||
const functionName = 'duplicate-function';
|
||||
const expectedError = `function with name ${functionName} already exists`;
|
||||
const func = new SharedFunctionStub()
|
||||
.withName('duplicate-function');
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(func);
|
||||
// act
|
||||
const act = () => sut.addFunction(func);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
|
||||
});
|
||||
});
|
||||
describe('getFunctionByName', () => {
|
||||
it('throws if name is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function name';
|
||||
const invalidValues = [ undefined, '' ];
|
||||
const sut = new SharedFunctionCollection();
|
||||
for (const invalidValue of invalidValues) {
|
||||
const name = invalidValue;
|
||||
// act
|
||||
const act = () => sut.getFunctionByName(name);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
it('throws if function does not exist', () => {
|
||||
// arrange
|
||||
const name = 'unique-name';
|
||||
const expectedError = `called function is not defined "${name}"`;
|
||||
const func = new SharedFunctionStub()
|
||||
.withName('unexpected-name');
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(func);
|
||||
// act
|
||||
const act = () => sut.getFunctionByName(name);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns existing function', () => {
|
||||
// arrange
|
||||
const name = 'expected-function-name';
|
||||
const expected = new SharedFunctionStub()
|
||||
.withName(name);
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(new SharedFunctionStub().withName('another-function-name'));
|
||||
sut.addFunction(expected);
|
||||
// act
|
||||
const actual = sut.getFunctionByName(name);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { FunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!*';
|
||||
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompilerStub } from '../../../../../stubs/ExpressionsCompilerStub';
|
||||
import { SharedFunctionCollectionStub } from '../../../../../stubs/SharedFunctionCollectionStub';
|
||||
import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
|
||||
|
||||
describe('FunctionCallCompiler', () => {
|
||||
describe('compileCall', () => {
|
||||
describe('call', () => {
|
||||
it('throws with undefined call', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined call';
|
||||
const call = undefined;
|
||||
const functions = new SharedFunctionCollectionStub();
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
// act
|
||||
const act = () => sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if call is not an object', () => {
|
||||
// arrange
|
||||
const expectedError = 'called function(s) must be an object';
|
||||
const invalidCalls: readonly any[] = ['string', 33];
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
const functions = new SharedFunctionCollectionStub();
|
||||
invalidCalls.forEach((invalidCall) => {
|
||||
// act
|
||||
const act = () => sut.compileCall(invalidCall, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws if call sequence has undefined call', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function call';
|
||||
const call: FunctionCallData[] = [
|
||||
{ function: 'function-name' },
|
||||
undefined,
|
||||
];
|
||||
const functions = new SharedFunctionCollectionStub();
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
// act
|
||||
const act = () => sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if call sequence has undefined function name', () => {
|
||||
// arrange
|
||||
const expectedError = 'empty function name called';
|
||||
const call: FunctionCallData[] = [
|
||||
{ function: 'function-name' },
|
||||
{ function: undefined },
|
||||
];
|
||||
const functions = new SharedFunctionCollectionStub();
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
// act
|
||||
const act = () => sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if call parameters does not match function parameters', () => {
|
||||
// arrange
|
||||
const unexpectedCallParameterName = 'unexpected-parameter-name';
|
||||
const func = new SharedFunctionStub()
|
||||
.withName('test-function-name')
|
||||
.withParameters('another-parameter');
|
||||
const expectedError = `function "${func.name}" has unexpected parameter(s) provided: "${unexpectedCallParameterName}"`;
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
const params: FunctionCallParametersData = {
|
||||
[`${unexpectedCallParameterName}`]: 'unexpected-parameter-value',
|
||||
};
|
||||
const call: FunctionCallData = { function: func.name, parameters: params };
|
||||
const functions = new SharedFunctionCollectionStub().withFunction(func);
|
||||
// act
|
||||
const act = () => sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('functions', () => {
|
||||
it('throws with undefined functions', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined functions';
|
||||
const call: FunctionCallData = { function: 'function-call' };
|
||||
const functions = undefined;
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
// act
|
||||
const act = () => sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if function does not exist', () => {
|
||||
// arrange
|
||||
const expectedError = 'function does not exist';
|
||||
const call: FunctionCallData = { function: 'function-call' };
|
||||
const functions: ISharedFunctionCollection = {
|
||||
getFunctionByName: () => { throw new Error(expectedError); },
|
||||
};
|
||||
const sut = new MockableFunctionCallCompiler();
|
||||
// act
|
||||
const act = () => sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('builds code as expected', () => {
|
||||
describe('builds single call as expected', () => {
|
||||
// arrange
|
||||
const parametersTestCases = [
|
||||
{
|
||||
name: 'undefined parameters',
|
||||
parameters: undefined,
|
||||
parameterValues: undefined,
|
||||
},
|
||||
{
|
||||
name: 'empty parameters',
|
||||
parameters: [],
|
||||
parameterValues: { },
|
||||
},
|
||||
{
|
||||
name: 'non-empty parameters',
|
||||
parameters: [ 'param1', 'param2' ],
|
||||
parameterValues: { param1: 'value1', param2: 'value2' },
|
||||
},
|
||||
];
|
||||
for (const testCase of parametersTestCases) {
|
||||
it(testCase.name, () => {
|
||||
const expectedExecute = `expected-execute`;
|
||||
const expectedRevert = `expected-revert`;
|
||||
const func = new SharedFunctionStub().withParameters(...testCase.parameters);
|
||||
const functions = new SharedFunctionCollectionStub().withFunction(func);
|
||||
const call: FunctionCallData = { function: func.name, parameters: testCase.parameterValues };
|
||||
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||
.setup(func.code, testCase.parameterValues, expectedExecute)
|
||||
.setup(func.revertCode, testCase.parameterValues, expectedRevert);
|
||||
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
||||
// act
|
||||
const actual = sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(actual.code).to.equal(expectedExecute);
|
||||
expect(actual.revertCode).to.equal(expectedRevert);
|
||||
});
|
||||
}
|
||||
});
|
||||
it('builds call sequence as expected', () => {
|
||||
// arrange
|
||||
const firstFunction = new SharedFunctionStub()
|
||||
.withName('first-function-name')
|
||||
.withCode('first-function-code')
|
||||
.withRevertCode('first-function-revert-code');
|
||||
const secondFunction = new SharedFunctionStub()
|
||||
.withName('second-function-name')
|
||||
.withParameters('testParameter')
|
||||
.withCode('second-function-code')
|
||||
.withRevertCode('second-function-revert-code');
|
||||
const secondCallArguments = { testParameter: 'testValue' };
|
||||
const call: FunctionCallData[] = [
|
||||
{ function: firstFunction.name },
|
||||
{ function: secondFunction.name, parameters: secondCallArguments },
|
||||
];
|
||||
const expressionsCompilerMock = new ExpressionsCompilerStub()
|
||||
.setup(firstFunction.code, {}, firstFunction.code)
|
||||
.setup(firstFunction.revertCode, {}, firstFunction.revertCode)
|
||||
.setup(secondFunction.code, secondCallArguments, secondFunction.code)
|
||||
.setup(secondFunction.revertCode, secondCallArguments, secondFunction.revertCode);
|
||||
const expectedExecute = `${firstFunction.code}\n${secondFunction.code}`;
|
||||
const expectedRevert = `${firstFunction.revertCode}\n${secondFunction.revertCode}`;
|
||||
const functions = new SharedFunctionCollectionStub()
|
||||
.withFunction(firstFunction)
|
||||
.withFunction(secondFunction);
|
||||
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
|
||||
// act
|
||||
const actual = sut.compileCall(call, functions);
|
||||
// assert
|
||||
expect(actual.code).to.equal(expectedExecute);
|
||||
expect(actual.revertCode).to.equal(expectedRevert);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class MockableFunctionCallCompiler extends FunctionCallCompiler {
|
||||
constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) {
|
||||
super(expressionsCompiler);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||
import { FunctionData, ScriptData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
|
||||
import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler';
|
||||
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||
import { ICompiledCode } from '@/application/Parser/Script/Compiler/FunctionCall/ICompiledCode';
|
||||
import { LanguageSyntaxStub } from '../../../../stubs/LanguageSyntaxStub';
|
||||
import { ScriptDataStub } from '../../../../stubs/ScriptDataStub';
|
||||
import { FunctionDataStub } from '../../../../stubs/FunctionDataStub';
|
||||
import { FunctionCallCompilerStub } from '../../../../stubs/FunctionCallCompilerStub';
|
||||
import { FunctionCompilerStub } from '../../../../stubs/FunctionCompilerStub';
|
||||
import { SharedFunctionCollectionStub } from '../../../../stubs/SharedFunctionCollectionStub';
|
||||
|
||||
describe('ScriptCompiler', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -22,88 +26,20 @@ describe('ScriptCompiler', () => {
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if one of the functions is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = `some functions are undefined`;
|
||||
const functions = [ new FunctionDataStub(), undefined ];
|
||||
// act
|
||||
const act = () => new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when functions have same names', () => {
|
||||
// arrange
|
||||
const name = 'same-func-name';
|
||||
const expectedError = `duplicate function name: "${name}"`;
|
||||
const functions = [
|
||||
new FunctionDataStub().withName(name),
|
||||
new FunctionDataStub().withName(name),
|
||||
];
|
||||
// act
|
||||
const act = () => new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when function parameters have same names', () => {
|
||||
// arrange
|
||||
const parameterName = 'duplicate-parameter';
|
||||
const func = new FunctionDataStub()
|
||||
.withParameters(parameterName, parameterName);
|
||||
const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
|
||||
// act
|
||||
const act = () => new ScriptCompilerBuilder()
|
||||
.withFunctions(func)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws when when function have duplicate code', () => {
|
||||
it('code', () => {
|
||||
// arrange
|
||||
const code = 'duplicate-code';
|
||||
const expectedError = `duplicate "code" in functions: "${code}"`;
|
||||
const functions = [
|
||||
new FunctionDataStub().withName('func-1').withCode(code),
|
||||
new FunctionDataStub().withName('func-2').withCode(code),
|
||||
];
|
||||
// act
|
||||
const act = () => new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('revertCode', () => {
|
||||
// arrange
|
||||
const revertCode = 'duplicate-revert-code';
|
||||
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
||||
const functions = [
|
||||
new FunctionDataStub().withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
||||
new FunctionDataStub().withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
||||
];
|
||||
// act
|
||||
const act = () => new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('can construct with empty functions', () => {
|
||||
// arrange
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions();
|
||||
// act
|
||||
const act = () => builder.build();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
describe('canCompile', () => {
|
||||
it('throws if script is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined script';
|
||||
const argument = undefined;
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
// act
|
||||
const act = () => builder.canCompile(argument);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns true if "call" is defined', () => {
|
||||
// arrange
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
@@ -128,274 +64,97 @@ describe('ScriptCompiler', () => {
|
||||
});
|
||||
});
|
||||
describe('compile', () => {
|
||||
describe('invalid state', () => {
|
||||
it('throws if functions are empty', () => {
|
||||
it('throws if script is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'cannot compile without shared functions';
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
const expectedError = 'undefined script';
|
||||
const argument = undefined;
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
// act
|
||||
const act = () => builder.compile(argument);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns code as expected', () => {
|
||||
// arrange
|
||||
const expected: ICompiledCode = {
|
||||
code: 'expected-code',
|
||||
revertCode: 'expected-revert-code',
|
||||
};
|
||||
const script = ScriptDataStub.createWithCall();
|
||||
const functions = [ FunctionDataStub.createWithCode().withName('existing-func') ];
|
||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||
const compilerMock = new FunctionCompilerStub();
|
||||
compilerMock.setup(functions, compiledFunctions);
|
||||
const callCompilerMock = new FunctionCallCompilerStub();
|
||||
callCompilerMock.setup(script.call, compiledFunctions, expected);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(...functions)
|
||||
.withFunctionCompiler(compilerMock)
|
||||
.withFunctionCallCompiler(callCompilerMock)
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
const code = sut.compile(script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
expect(code.execute).to.equal(expected.code);
|
||||
expect(code.revert).to.equal(expected.revertCode);
|
||||
});
|
||||
it('throws if call is not an object', () => {
|
||||
it('creates with expected syntax', () => {
|
||||
// arrange
|
||||
const expectedError = 'called function(s) must be an object';
|
||||
const invalidValues = [undefined, 'string', 33];
|
||||
let isUsed = false;
|
||||
const syntax: ILanguageSyntax = {
|
||||
get commentDelimiters() {
|
||||
isUsed = true;
|
||||
return [];
|
||||
},
|
||||
get commonCodeParts() {
|
||||
isUsed = true;
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.build();
|
||||
invalidValues.forEach((invalidValue) => {
|
||||
const script = ScriptDataStub.createWithoutCallOrCodes() // because call ctor overwrites "undefined"
|
||||
.withCall(invalidValue as any);
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('invalid function reference', () => {
|
||||
it('throws if function does not exist', () => {
|
||||
// arrange
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.build();
|
||||
const nonExistingFunctionName = 'non-existing-func';
|
||||
const expectedError = `called function is not defined "${nonExistingFunctionName}"`;
|
||||
const call: ScriptFunctionCallData = { function: nonExistingFunctionName };
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if function is undefined', () => {
|
||||
// arrange
|
||||
const existingFunctionName = 'existing-func';
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctionNames(existingFunctionName)
|
||||
.build();
|
||||
const call: ScriptFunctionCallData = [
|
||||
{ function: existingFunctionName },
|
||||
undefined,
|
||||
];
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
const expectedError = `undefined function call in script "${script.name}"`;
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if function name is not given', () => {
|
||||
// arrange
|
||||
const existingFunctionName = 'existing-func';
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctionNames(existingFunctionName)
|
||||
.build();
|
||||
const call: FunctionCallData[] = [
|
||||
{ function: existingFunctionName },
|
||||
{ function: undefined }];
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
const expectedError = `empty function name called in script "${script.name}"`;
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws if provided parameters does not match given ones', () => {
|
||||
// arrange
|
||||
const unexpectedParameterName = 'unexpected-parameter-name';
|
||||
const functionName = 'test-function-name';
|
||||
const expectedError = `function "${functionName}" has unexpected parameter(s) provided: "${unexpectedParameterName}"`;
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(
|
||||
new FunctionDataStub()
|
||||
.withName(functionName)
|
||||
.withParameters('another-parameter'))
|
||||
.build();
|
||||
const params: FunctionCallParametersData = {};
|
||||
params[unexpectedParameterName] = 'unexpected-parameter-value';
|
||||
const call: ScriptFunctionCallData = { function: functionName, parameters: params };
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('builds code as expected', () => {
|
||||
it('creates code with expected syntax', () => { // test through script validation logic
|
||||
// act
|
||||
const commentDelimiter = 'should not throw';
|
||||
const syntax = new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter);
|
||||
const func = new FunctionDataStub()
|
||||
.withCode(`${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(func)
|
||||
.withSyntax(syntax)
|
||||
.build();
|
||||
const call: FunctionCallData = { function: func.name };
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
const scriptData = ScriptDataStub.createWithCall();
|
||||
// act
|
||||
const act = () => sut.compile(script);
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
expect(isUsed).to.equal(true);
|
||||
});
|
||||
it('builds single call as expected', () => {
|
||||
it('rethrows error from ScriptCode with script name', () => {
|
||||
// arrange
|
||||
const functionName = 'testSharedFunction';
|
||||
const expectedExecute = `expected-execute`;
|
||||
const expectedRevert = `expected-revert`;
|
||||
const func = new FunctionDataStub()
|
||||
.withName(functionName)
|
||||
.withCode(expectedExecute)
|
||||
.withRevertCode(expectedRevert);
|
||||
const scriptName = 'scriptName'; // // arrange
|
||||
const innerError = 'innerError';
|
||||
const expectedError = `Script "${scriptName}" ${innerError}`;
|
||||
const callCompiler: IFunctionCallCompiler = {
|
||||
compileCall: () => { throw new Error(innerError); },
|
||||
};
|
||||
const scriptData = ScriptDataStub.createWithCall()
|
||||
.withName(scriptName);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(func)
|
||||
.withSomeFunctions()
|
||||
.withFunctionCallCompiler(callCompiler)
|
||||
.build();
|
||||
const call: FunctionCallData = { function: functionName };
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
// act
|
||||
const actual = sut.compile(script);
|
||||
// assert
|
||||
expect(actual.execute).to.equal(expectedExecute);
|
||||
expect(actual.revert).to.equal(expectedRevert);
|
||||
});
|
||||
it('builds call sequence as expected', () => {
|
||||
// arrange
|
||||
const firstFunction = new FunctionDataStub()
|
||||
.withName('first-function-name')
|
||||
.withCode('first-function-code')
|
||||
.withRevertCode('first-function-revert-code');
|
||||
const secondFunction = new FunctionDataStub()
|
||||
.withName('second-function-name')
|
||||
.withCode('second-function-code')
|
||||
.withRevertCode('second-function-revert-code');
|
||||
const expectedExecute = `${firstFunction.code}\n${secondFunction.code}`;
|
||||
const expectedRevert = `${firstFunction.revertCode}\n${secondFunction.revertCode}`;
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(firstFunction, secondFunction)
|
||||
.build();
|
||||
const call: FunctionCallData[] = [
|
||||
{ function: firstFunction.name },
|
||||
{ function: secondFunction.name },
|
||||
];
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
// act
|
||||
const actual = sut.compile(script);
|
||||
// assert
|
||||
expect(actual.execute).to.equal(expectedExecute);
|
||||
expect(actual.revert).to.equal(expectedRevert);
|
||||
});
|
||||
});
|
||||
describe('parameter substitution', () => {
|
||||
describe('substitutes as expected', () => {
|
||||
it('with different parameters', () => {
|
||||
// arrange
|
||||
const env = new TestEnvironment({
|
||||
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
||||
parameters: {
|
||||
firstParameter: 'llo',
|
||||
secondParameter: 'world',
|
||||
},
|
||||
});
|
||||
const expected = env.expect('Hello world!');
|
||||
// act
|
||||
const actual = env.sut.compile(env.script);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('with single parameter', () => {
|
||||
// arrange
|
||||
const env = new TestEnvironment({
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
parameter: 'Hodor',
|
||||
},
|
||||
});
|
||||
const expected = env.expect('Hodor!');
|
||||
// act
|
||||
const actual = env.sut.compile(env.script);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
it('throws when parameters are undefined', () => {
|
||||
// arrange
|
||||
const env = new TestEnvironment({
|
||||
code: '{{ $parameter }} {{ $parameter }}!',
|
||||
});
|
||||
const expectedError = 'no parameters defined, expected: "parameter"';
|
||||
// act
|
||||
const act = () => env.sut.compile(env.script);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when parameter value is not provided', () => {
|
||||
// arrange
|
||||
const env = new TestEnvironment({
|
||||
code: '{{ $parameter }} {{ $parameter }}!',
|
||||
parameters: {
|
||||
parameter: undefined,
|
||||
},
|
||||
});
|
||||
const expectedError = 'parameter value is not provided for "parameter" in function call';
|
||||
// act
|
||||
const act = () => env.sut.compile(env.script);
|
||||
const act = () => sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
interface ITestCase {
|
||||
code: string;
|
||||
parameters?: FunctionCallParametersData;
|
||||
}
|
||||
class TestEnvironment {
|
||||
public readonly sut: IScriptCompiler;
|
||||
public readonly script: ScriptData;
|
||||
constructor(testCase: ITestCase) {
|
||||
const functionName = 'testFunction';
|
||||
const parameters = testCase.parameters ? Object.keys(testCase.parameters) : [];
|
||||
const func = new FunctionDataStub()
|
||||
.withName(functionName)
|
||||
.withParameters(...parameters)
|
||||
.withCode(this.getCode(testCase.code, 'execute'))
|
||||
.withRevertCode(this.getCode(testCase.code, 'revert'));
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
this.sut = new ScriptCompiler([func], syntax);
|
||||
const call: FunctionCallData = {
|
||||
function: functionName,
|
||||
parameters: testCase.parameters,
|
||||
};
|
||||
this.script = ScriptDataStub.createWithCall(call);
|
||||
}
|
||||
public expect(code: string): IScriptCode {
|
||||
return {
|
||||
execute: this.getCode(code, 'execute'),
|
||||
revert: this.getCode(code, 'revert'),
|
||||
};
|
||||
}
|
||||
private getCode(text: string, type: 'execute' | 'revert'): string {
|
||||
return `${text} (${type})`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
class ScriptCompilerBuilder {
|
||||
private static createFunctions(...names: string[]): FunctionData[] {
|
||||
return names.map((functionName) => {
|
||||
return new FunctionDataStub().withName(functionName);
|
||||
return FunctionDataStub.createWithCode().withName(functionName);
|
||||
});
|
||||
}
|
||||
private functions: FunctionData[];
|
||||
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
private functionCompiler: IFunctionCompiler = new FunctionCompilerStub();
|
||||
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
|
||||
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
|
||||
this.functions = functions;
|
||||
return this;
|
||||
@@ -416,10 +175,18 @@ class ScriptCompilerBuilder {
|
||||
this.syntax = syntax;
|
||||
return this;
|
||||
}
|
||||
public withFunctionCompiler(functionCompiler: IFunctionCompiler): ScriptCompilerBuilder {
|
||||
this.functionCompiler = functionCompiler;
|
||||
return this;
|
||||
}
|
||||
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
|
||||
this.callCompiler = callCompiler;
|
||||
return this;
|
||||
}
|
||||
public build(): ScriptCompiler {
|
||||
if (!this.functions) {
|
||||
throw new Error('Function behavior not defined');
|
||||
}
|
||||
return new ScriptCompiler(this.functions, this.syntax);
|
||||
return new ScriptCompiler(this.functions, this.syntax, this.functionCompiler, this.callCompiler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('ScriptParser', () => {
|
||||
});
|
||||
});
|
||||
describe('code', () => {
|
||||
it('parses code as expected', () => {
|
||||
it('parses "execute" as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-code';
|
||||
const script = ScriptDataStub
|
||||
@@ -125,7 +125,7 @@ describe('ScriptParser', () => {
|
||||
const actual = parsed.code.execute;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
it('parses revertCode as expected', () => {
|
||||
it('parses "revert" as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-revert-code';
|
||||
const script = ScriptDataStub
|
||||
|
||||
@@ -6,23 +6,9 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { LanguageSyntaxStub } from '../stubs/LanguageSyntaxStub';
|
||||
|
||||
describe('ScriptCode', () => {
|
||||
describe('scriptName', () => {
|
||||
it('throws if undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'name is undefined';
|
||||
const name = undefined;
|
||||
// act
|
||||
const act = () => new ScriptCodeBuilder()
|
||||
.withName(name)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('code', () => {
|
||||
describe('throws with invalid code', () => {
|
||||
// arrange
|
||||
const scriptName = 'test-script';
|
||||
const testCases = [
|
||||
{
|
||||
name: 'throws when "execute" and "revert" are same',
|
||||
@@ -30,7 +16,7 @@ describe('ScriptCode', () => {
|
||||
execute: 'same',
|
||||
revert: 'same',
|
||||
},
|
||||
expectedError: `${scriptName} (revert): Code itself and its reverting code cannot be the same`,
|
||||
expectedError: `(revert): Code itself and its reverting code cannot be the same`,
|
||||
},
|
||||
{
|
||||
name: 'cannot construct with undefined "execute"',
|
||||
@@ -38,7 +24,7 @@ describe('ScriptCode', () => {
|
||||
execute: undefined,
|
||||
revert: 'code',
|
||||
},
|
||||
expectedError: `code of ${scriptName} is empty or undefined`,
|
||||
expectedError: `code is empty or undefined`,
|
||||
},
|
||||
{
|
||||
name: 'cannot construct with empty "execute"',
|
||||
@@ -46,14 +32,13 @@ describe('ScriptCode', () => {
|
||||
execute: '',
|
||||
revert: 'code',
|
||||
},
|
||||
expectedError: `code of ${scriptName} is empty or undefined`,
|
||||
expectedError: `code is empty or undefined`,
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
// act
|
||||
const act = () => new ScriptCodeBuilder()
|
||||
.withName(scriptName)
|
||||
.withExecute( testCase.code.execute)
|
||||
.withRevert(testCase.code.revert)
|
||||
.build();
|
||||
@@ -64,39 +49,35 @@ describe('ScriptCode', () => {
|
||||
});
|
||||
describe('throws with invalid code in both "execute" or "revert"', () => {
|
||||
// arrange
|
||||
const scriptName = 'script-name';
|
||||
const testCases = [
|
||||
{
|
||||
testName: 'cannot construct with duplicate lines',
|
||||
code: 'duplicate\nduplicate\ntest\nduplicate',
|
||||
expectedMessage: 'Duplicates detected in script "$scriptName":\n duplicate\nduplicate',
|
||||
expectedMessage: 'Duplicates detected in script :\n duplicate\nduplicate',
|
||||
},
|
||||
{
|
||||
testName: 'cannot construct with empty lines',
|
||||
code: 'line1\n\n\nline2',
|
||||
expectedMessage: 'script has empty lines "$scriptName"',
|
||||
expectedMessage: 'script has empty lines',
|
||||
},
|
||||
];
|
||||
// act
|
||||
const actions = [];
|
||||
for (const testCase of testCases) {
|
||||
const substituteScriptName = (name: string) => testCase.expectedMessage.replace('$scriptName', name);
|
||||
actions.push(...[
|
||||
{
|
||||
act: () => new ScriptCodeBuilder()
|
||||
.withName(scriptName)
|
||||
.withExecute(testCase.code)
|
||||
.build(),
|
||||
testName: `execute: ${testCase.testName}`,
|
||||
expectedMessage: substituteScriptName(scriptName),
|
||||
expectedMessage: testCase.expectedMessage,
|
||||
},
|
||||
{
|
||||
act: () => new ScriptCodeBuilder()
|
||||
.withName(scriptName)
|
||||
.withRevert(testCase.code)
|
||||
.build(),
|
||||
testName: `revert: ${testCase.testName}`,
|
||||
expectedMessage: substituteScriptName(`${scriptName} (revert)`),
|
||||
expectedMessage: `(revert): ${testCase.expectedMessage}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -168,18 +149,26 @@ describe('ScriptCode', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('syntax', () => {
|
||||
it('throws if undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined syntax';
|
||||
const syntax = undefined;
|
||||
// act
|
||||
const act = () => new ScriptCodeBuilder()
|
||||
.withSyntax(syntax)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ScriptCodeBuilder {
|
||||
public execute = 'default-execute-code';
|
||||
public revert = '';
|
||||
public scriptName = 'default-script-name';
|
||||
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
|
||||
public withName(name: string) {
|
||||
this.scriptName = name;
|
||||
return this;
|
||||
}
|
||||
public withExecute(execute: string) {
|
||||
this.execute = execute;
|
||||
return this;
|
||||
@@ -197,7 +186,6 @@ class ScriptCodeBuilder {
|
||||
return new ScriptCode(
|
||||
this.execute,
|
||||
this.revert,
|
||||
this.scriptName,
|
||||
this.syntax);
|
||||
}
|
||||
}
|
||||
|
||||
28
tests/unit/stubs/ExpressionsCompilerStub.ts
Normal file
28
tests/unit/stubs/ExpressionsCompilerStub.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IExpressionsCompiler, ParameterValueDictionary } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
|
||||
interface Scenario { code: string; parameters: ParameterValueDictionary; result: string; }
|
||||
|
||||
export class ExpressionsCompilerStub implements IExpressionsCompiler {
|
||||
private readonly scenarios = new Array<Scenario>();
|
||||
public setup(code: string, parameters: ParameterValueDictionary, result: string) {
|
||||
this.scenarios.push({ code, parameters, result });
|
||||
return this;
|
||||
}
|
||||
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||
const scenario = this.scenarios.find((s) => s.code === code && deepEqual(s.parameters, parameters));
|
||||
if (scenario) {
|
||||
return scenario.result;
|
||||
}
|
||||
return `[ExpressionsCompilerStub] code: "${code}"` +
|
||||
`| parameters: ${Object.keys(parameters || {}).map((p) => p + '=' + parameters[p]).join(',')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function deepEqual(dict1: ParameterValueDictionary, dict2: ParameterValueDictionary) {
|
||||
const dict1Keys = Object.keys(dict1 || {});
|
||||
const dict2Keys = Object.keys(dict2 || {});
|
||||
if (dict1Keys.length !== dict2Keys.length) {
|
||||
return false;
|
||||
}
|
||||
return dict1Keys.every((key) => dict2.hasOwnProperty(key) && dict2[key] === dict1[key]);
|
||||
}
|
||||
26
tests/unit/stubs/FunctionCallCompilerStub.ts
Normal file
26
tests/unit/stubs/FunctionCallCompilerStub.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { ICompiledCode } from '@/application/Parser/Script/Compiler/FunctionCall/ICompiledCode';
|
||||
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCallData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||
|
||||
interface Scenario { call: ScriptFunctionCallData; functions: ISharedFunctionCollection; result: ICompiledCode; }
|
||||
|
||||
export class FunctionCallCompilerStub implements IFunctionCallCompiler {
|
||||
public scenarios = new Array<Scenario>();
|
||||
public setup(call: ScriptFunctionCallData, functions: ISharedFunctionCollection, result: ICompiledCode) {
|
||||
this.scenarios.push({ call, functions, result });
|
||||
}
|
||||
public compileCall(
|
||||
call: ScriptFunctionCallData,
|
||||
functions: ISharedFunctionCollection): ICompiledCode {
|
||||
const predefined = this.scenarios.find((s) => s.call === call && s.functions === functions);
|
||||
if (predefined) {
|
||||
return predefined.result;
|
||||
}
|
||||
const callee = functions.getFunctionByName((call as FunctionCallData).function);
|
||||
return {
|
||||
code: callee.code,
|
||||
revertCode: callee.revertCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
41
tests/unit/stubs/FunctionCompilerStub.ts
Normal file
41
tests/unit/stubs/FunctionCompilerStub.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
|
||||
|
||||
export class FunctionCompilerStub implements IFunctionCompiler {
|
||||
private setupResults = new Array<{
|
||||
functions: readonly FunctionData[],
|
||||
result: ISharedFunctionCollection,
|
||||
}>();
|
||||
|
||||
public setup(functions: readonly FunctionData[], result: ISharedFunctionCollection) {
|
||||
this.setupResults.push( { functions, result });
|
||||
}
|
||||
|
||||
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||
const result = this.findResult(functions);
|
||||
return result || new SharedFunctionCollectionStub();
|
||||
}
|
||||
|
||||
private findResult(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||
for (const result of this.setupResults) {
|
||||
if (sequenceEqual(result.functions, functions)) {
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
if (array1.length !== array2.length) {
|
||||
return false;
|
||||
}
|
||||
const sortedArray1 = sort(array1);
|
||||
const sortedArray2 = sort(array2);
|
||||
return sortedArray1.every((val, index) => val === sortedArray2[index]);
|
||||
function sort(array: readonly T[]) {
|
||||
return array.slice().sort();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,31 @@
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||
|
||||
export class FunctionDataStub implements FunctionData {
|
||||
public static createWithCode() {
|
||||
return new FunctionDataStub()
|
||||
.withCode('stub-code')
|
||||
.withRevertCode('stub-revert-code');
|
||||
}
|
||||
public static createWithCall(call?: ScriptFunctionCallData) {
|
||||
let instance = new FunctionDataStub();
|
||||
if (call) {
|
||||
instance = instance.withCall(call);
|
||||
} else {
|
||||
instance = instance.withMockCall();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
public static createWithoutCallOrCodes() {
|
||||
return new FunctionDataStub();
|
||||
}
|
||||
|
||||
public name = 'function data stub';
|
||||
public code = 'function data stub code';
|
||||
public revertCode = 'function data stub revertCode';
|
||||
public code: string;
|
||||
public revertCode: string;
|
||||
public parameters?: readonly string[];
|
||||
public call?: ScriptFunctionCallData;
|
||||
|
||||
private constructor() { }
|
||||
|
||||
public withName(name: string) {
|
||||
this.name = name;
|
||||
@@ -22,4 +43,12 @@ export class FunctionDataStub implements FunctionData {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
public withCall(call: ScriptFunctionCallData) {
|
||||
this.call = call;
|
||||
return this;
|
||||
}
|
||||
public withMockCall() {
|
||||
this.call = { function: 'func' };
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export class ScriptDataStub implements ScriptData {
|
||||
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
||||
public docs = ['hello.com'];
|
||||
|
||||
private constructor() { }
|
||||
|
||||
public withName(name: string): ScriptDataStub {
|
||||
this.name = name;
|
||||
return this;
|
||||
|
||||
21
tests/unit/stubs/SharedFunctionCollectionStub.ts
Normal file
21
tests/unit/stubs/SharedFunctionCollectionStub.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
|
||||
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
|
||||
private readonly functions = new Map<string, ISharedFunction>();
|
||||
public withFunction(func: ISharedFunction) {
|
||||
this.functions.set(func.name, func);
|
||||
return this;
|
||||
}
|
||||
public getFunctionByName(name: string): ISharedFunction {
|
||||
if (this.functions.has(name)) {
|
||||
return this.functions.get(name);
|
||||
}
|
||||
return {
|
||||
name,
|
||||
parameters: [],
|
||||
code: 'code by SharedFunctionCollectionStub',
|
||||
revertCode: 'revert-code by SharedFunctionCollectionStub',
|
||||
};
|
||||
}
|
||||
}
|
||||
27
tests/unit/stubs/SharedFunctionStub.ts
Normal file
27
tests/unit/stubs/SharedFunctionStub.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
|
||||
export class SharedFunctionStub implements ISharedFunction {
|
||||
public name = 'shared-function-stub-name';
|
||||
public parameters?: readonly string[] = [
|
||||
'shared-function-stub-parameter',
|
||||
];
|
||||
public code = 'shared-function-stub-code';
|
||||
public revertCode = 'shared-function-stub-revert-code';
|
||||
|
||||
public withName(name: string) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
public withCode(code: string) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
public withRevertCode(revertCode: string) {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
public withParameters(...params: string[]) {
|
||||
this.parameters = params;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user