add support for shared functions #41

This commit is contained in:
undergroundwires
2020-09-17 21:46:20 +01:00
parent 1a9db31c77
commit 8ce06facbd
27 changed files with 1533 additions and 354 deletions

View File

@@ -5,22 +5,41 @@ import 'mocha';
import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
describe('ApplicationParser', () => {
describe('parseApplication', () => {
it('can parse current application file', () => {
expect(() => parseApplication(applicationFile)).to.not.throw();
// act
const act = () => parseApplication(applicationFile);
// assert
expect(act).to.not.throw();
});
it('throws when undefined', () => {
expect(() => parseApplication(undefined)).to.throw('application is null or undefined');
// arrange
const expectedError = 'application is null or undefined';
// act
const act = () => parseApplication(undefined);
// assert
expect(act).to.throw(expectedError);
});
it('throws when undefined actions', () => {
const sut: ApplicationYaml = { actions: undefined };
expect(() => parseApplication(sut)).to.throw('application does not define any action');
// arrange
const sut: ApplicationYaml = { actions: undefined, functions: undefined };
const expectedError = 'application does not define any action';
// act
const act = () => parseApplication(sut);
// assert
expect(act).to.throw(expectedError);
});
it('throws when has no actions', () => {
const sut: ApplicationYaml = { actions: [] };
expect(() => parseApplication(sut)).to.throw('application does not define any action');
// arrange
const sut: ApplicationYaml = { actions: [], functions: undefined };
const expectedError = 'application does not define any action';
// act
const act = () => parseApplication(sut);
// assert
expect(act).to.throw(expectedError);
});
describe('information', () => {
it('returns expected repository version', () => {
@@ -28,7 +47,7 @@ describe('ApplicationParser', () => {
const expected = 'expected-version';
const env = getProcessEnvironmentStub();
env.VUE_APP_VERSION = expected;
const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
// act
const actual = parseApplication(sut, env).info.version;
// assert
@@ -39,7 +58,7 @@ describe('ApplicationParser', () => {
const expected = 'https://expected-repository.url';
const env = getProcessEnvironmentStub();
env.VUE_APP_REPOSITORY_URL = expected;
const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
// act
const actual = parseApplication(sut, env).info.repositoryUrl;
// assert
@@ -50,7 +69,7 @@ describe('ApplicationParser', () => {
const expected = 'expected-app-name';
const env = getProcessEnvironmentStub();
env.VUE_APP_NAME = expected;
const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
// act
const actual = parseApplication(sut, env).info.name;
// assert
@@ -61,7 +80,7 @@ describe('ApplicationParser', () => {
const expected = 'https://expected.sexy';
const env = getProcessEnvironmentStub();
env.VUE_APP_HOMEPAGE_URL = expected;
const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
// act
const actual = parseApplication(sut, env).info.homepage;
// assert
@@ -71,8 +90,9 @@ describe('ApplicationParser', () => {
it('parses actions', () => {
// arrange
const actions = [ getTestCategory('test1'), getTestCategory('test2') ];
const expected = [ parseCategory(actions[0]), parseCategory(actions[1]) ];
const sut: ApplicationYaml = { actions };
const compiler = new ScriptCompilerStub();
const expected = [ parseCategory(actions[0], compiler), parseCategory(actions[1], compiler) ];
const sut: ApplicationYaml = { actions, functions: undefined };
// act
const actual = parseApplication(sut).actions;
// assert
@@ -103,6 +123,7 @@ function getTestScript(scriptName: string, level: RecommendationLevel = Recommen
code: 'script code',
revertCode: 'revert code',
recommend: RecommendationLevel[level].toLowerCase(),
call: undefined,
};
}

View File

@@ -4,88 +4,150 @@ import { parseCategory } from '@/application/Parser/CategoryParser';
import { YamlCategory, CategoryOrScript, YamlScript } from 'js-yaml-loader!./application.yaml';
import { parseScript } from '@/application/Parser/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { YamlScriptStub } from '../../stubs/YamlScriptStub';
describe('CategoryParser', () => {
describe('parseCategory', () => {
it('throws when undefined', () => {
expect(() => parseCategory(undefined)).to.throw('category is null or undefined');
describe('invalid category', () => {
it('throws when undefined', () => {
// arrange
const expectedMessage = 'category is null or undefined';
const category = undefined;
const compiler = new ScriptCompilerStub();
// act
const act = () => parseCategory(category, compiler);
// assert
expect(act).to.throw(expectedMessage);
});
it('throws when children are empty', () => {
// arrange
const expectedMessage = 'category has no children';
const category: YamlCategory = {
category: 'test',
children: [],
};
const compiler = new ScriptCompilerStub();
// act
const act = () => parseCategory(category, compiler);
// assert
expect(act).to.throw(expectedMessage);
});
it('throws when children are undefined', () => {
// arrange
const expectedMessage = 'category has no children';
const category: YamlCategory = {
category: 'test',
children: undefined,
};
const compiler = new ScriptCompilerStub();
// act
const act = () => parseCategory(category, compiler);
// assert
expect(act).to.throw(expectedMessage);
});
it('throws when name is empty or undefined', () => {
// arrange
const expectedMessage = 'category has no name';
const invalidNames = ['', undefined];
invalidNames.forEach((invalidName) => {
const category: YamlCategory = {
category: invalidName,
children: getTestChildren(),
};
const compiler = new ScriptCompilerStub();
// act
const act = () => parseCategory(category, compiler);
// assert
expect(act).to.throw(expectedMessage);
});
});
});
it('throws when children is empty', () => {
const category: YamlCategory = {
category: 'test',
children: [],
};
expect(() => parseCategory(category)).to.throw('category has no children');
it('throws when compiler is undefined', () => {
// arrange
const expectedError = 'undefined compiler';
const compiler = undefined;
const category = getValidCategory();
// act
const act = () => parseCategory(category, compiler);
// assert
expect(act).to.throw(expectedError);
});
it('throws when children is undefined', () => {
const category: YamlCategory = {
category: 'test',
children: undefined,
};
expect(() => parseCategory(category)).to.throw('category has no children');
});
it('throws when name is empty', () => {
const category: YamlCategory = {
category: '',
children: getTestChildren(),
};
expect(() => parseCategory(category)).to.throw('category has no name');
});
it('throws when name is undefined', () => {
const category: YamlCategory = {
category: undefined,
children: getTestChildren(),
};
expect(() => parseCategory(category)).to.throw('category has no name');
});
it('returns expected docs', () => {
// arrange
const url = 'https://privacy.sexy';
const expected = parseDocUrls({ docs: url });
const compiler = new ScriptCompilerStub();
const category: YamlCategory = {
category: 'category name',
children: getTestChildren(),
docs: url,
};
// act
const actual = parseCategory(category).documentationUrls;
const actual = parseCategory(category, compiler).documentationUrls;
// assert
expect(actual).to.deep.equal(expected);
});
it('returns expected scripts', () => {
// arrange
const script = getTestScript();
const expected = [ parseScript(script) ];
const category: YamlCategory = {
category: 'category name',
children: [ script ],
};
// act
const actual = parseCategory(category).scripts;
// assert
expect(actual).to.deep.equal(expected);
describe('parses expected subscript', () => {
it('single script with code', () => {
// arrange
const script = YamlScriptStub.createWithCode();
const compiler = new ScriptCompilerStub();
const expected = [ parseScript(script, compiler) ];
const category: YamlCategory = {
category: 'category name',
children: [ script ],
};
// act
const actual = parseCategory(category, compiler).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
it('single script with function call', () => {
// arrange
const script = YamlScriptStub.createWithCall();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script);
const expected = [ parseScript(script, compiler) ];
const category: YamlCategory = {
category: 'category name',
children: [ script ],
};
// act
const actual = parseCategory(category, compiler).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
it('multiple scripts with function call and code', () => {
// arrange
const callableScript = YamlScriptStub.createWithCall();
const scripts = [ callableScript, YamlScriptStub.createWithCode() ];
const compiler = new ScriptCompilerStub()
.withCompileAbility(callableScript);
const expected = scripts.map((script) => parseScript(script, compiler));
const category: YamlCategory = {
category: 'category name',
children: scripts,
};
// act
const actual = parseCategory(category, compiler).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
});
it('returns expected subcategories', () => {
// arrange
const expected: YamlCategory[] = [ {
category: 'test category',
children: [ getTestScript() ],
children: [ YamlScriptStub.createWithCode() ],
}];
const category: YamlCategory = {
category: 'category name',
children: expected,
};
const compiler = new ScriptCompilerStub();
// act
const actual = parseCategory(category).subCategories;
const actual = parseCategory(category, compiler).subCategories;
// assert
expect(actual).to.have.lengthOf(1);
expect(actual[0].name).to.equal(expected[0].category);
@@ -94,17 +156,16 @@ describe('CategoryParser', () => {
});
});
function getTestChildren(): ReadonlyArray<CategoryOrScript> {
return [
getTestScript(),
];
}
function getTestScript(): YamlScript {
function getValidCategory(): YamlCategory {
return {
name: 'script name',
code: 'script code',
revertCode: 'revert code',
recommend: RecommendationLevel[RecommendationLevel.Standard],
category: 'category name',
children: getTestChildren(),
docs: undefined,
};
}
function getTestChildren(): ReadonlyArray<CategoryOrScript> {
return [
YamlScriptStub.createWithCode(),
];
}

View File

@@ -0,0 +1,363 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptCompiler } from '@/application/Parser/Compiler/ScriptCompiler';
import { YamlScriptStub } from '../../../stubs/YamlScriptStub';
import { YamlFunction, YamlScript, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
import { IScriptCode } from '@/domain/IScriptCode';
import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
describe('ScriptCompiler', () => {
describe('ctor', () => {
it('throws when functions have same names', () => {
// arrange
const expectedError = `duplicate function name: "same-func-name"`;
const functions: YamlFunction[] = [ {
name: 'same-func-name',
code: 'non-empty-code',
}, {
name: 'same-func-name',
code: 'non-empty-code-2',
}];
// act
const act = () => new ScriptCompiler(functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws when function parameters have same names', () => {
// arrange
const func: YamlFunction = {
name: 'function-name',
code: 'non-empty-code',
parameters: [ 'duplicate', 'duplicate' ],
};
const expectedError = `"${func.name}": duplicate parameter name: "duplicate"`;
// act
const act = () => new ScriptCompiler([func]);
// assert
expect(act).to.throw(expectedError);
});
describe('throws when when function have duplicate code', () => {
it('code', () => {
// arrange
const expectedError = `duplicate "code" in functions: "duplicate-code"`;
const functions: YamlFunction[] = [ {
name: 'func-1',
code: 'duplicate-code',
}, {
name: 'func-2',
code: 'duplicate-code',
}];
// act
const act = () => new ScriptCompiler(functions);
// assert
expect(act).to.throw(expectedError);
});
it('revertCode', () => {
// arrange
const expectedError = `duplicate "revertCode" in functions: "duplicate-revert-code"`;
const functions: YamlFunction[] = [ {
name: 'func-1',
code: 'code-1',
revertCode: 'duplicate-revert-code',
}, {
name: 'func-2',
code: 'code-2',
revertCode: 'duplicate-revert-code',
}];
// act
const act = () => new ScriptCompiler(functions);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('canCompile', () => {
it('returns true if "call" is defined', () => {
// arrange
const sut = new ScriptCompiler([]);
const script = YamlScriptStub.createWithCall();
// act
const actual = sut.canCompile(script);
// assert
expect(actual).to.equal(true);
});
it('returns false if "call" is undefined', () => {
// arrange
const sut = new ScriptCompiler([]);
const script = YamlScriptStub.createWithCode();
// act
const actual = sut.canCompile(script);
// assert
expect(actual).to.equal(false);
});
});
describe('compile', () => {
describe('invalid state', () => {
it('throws if functions are empty', () => {
// arrange
const expectedError = 'cannot compile without shared functions';
const functions = [];
const sut = new ScriptCompiler(functions);
const script = YamlScriptStub.createWithCall();
// act
const act = () => sut.compile(script);
// 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 invalidValues = [undefined, 'string', 33];
const sut = new ScriptCompiler(createFunctions());
invalidValues.forEach((invalidValue) => {
const script = YamlScriptStub.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 ScriptCompiler(createFunctions());
const nonExistingFunctionName = 'non-existing-func';
const expectedError = `called function is not defined "${nonExistingFunctionName}"`;
const call: ScriptFunctionCall = { function: nonExistingFunctionName };
const script = YamlScriptStub.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 ScriptCompiler(createFunctions(existingFunctionName));
const call: ScriptFunctionCall = [
{ function: existingFunctionName },
undefined,
];
const script = YamlScriptStub.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 ScriptCompiler(createFunctions(existingFunctionName));
const call: FunctionCall[] = [
{ function: existingFunctionName },
{ function: undefined }];
const script = YamlScriptStub.createWithCall(call);
const expectedError = `empty function name called in script "${script.name}"`;
// act
const act = () => sut.compile(script);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('builds code as expected', () => {
it('builds single call as expected', () => {
// arrange
const functionName = 'testSharedFunction';
const expected: IScriptCode = {
execute: 'expected-code',
revert: 'expected-revert-code',
};
const func: YamlFunction = {
name: functionName,
parameters: [],
code: expected.execute,
revertCode: expected.revert,
};
const sut = new ScriptCompiler([func]);
const call: FunctionCall = { function: functionName };
const script = YamlScriptStub.createWithCall(call);
// act
const actual = sut.compile(script);
// assert
expect(actual).to.deep.equal(expected);
});
it('builds call sequence as expected', () => {
// arrange
const firstFunction: YamlFunction = {
name: 'first-function-name',
parameters: [],
code: 'first-function-code',
revertCode: 'first-function-revert-code',
};
const secondFunction: YamlFunction = {
name: 'second-function-name',
parameters: [],
code: 'second-function-code',
revertCode: 'second-function-revert-code',
};
const expected: IScriptCode = {
execute: 'first-function-code\nsecond-function-code',
revert: 'first-function-revert-code\nsecond-function-revert-code',
};
const sut = new ScriptCompiler([firstFunction, secondFunction]);
const call: FunctionCall[] = [
{ function: firstFunction.name },
{ function: secondFunction.name },
];
const script = YamlScriptStub.createWithCall(call);
// act
const actual = sut.compile(script);
// assert
expect(actual).to.deep.equal(expected);
});
});
describe('parameter substitution', () => {
describe('substitutes by ignoring whitespaces inside mustaches', () => {
// arrange
const mustacheVariations = [
'Hello {{ $test }}!',
'Hello {{$test }}!',
'Hello {{ $test}}!',
'Hello {{$test}}!'];
mustacheVariations.forEach((variation) => {
it(variation, () => {
// arrange
const env = new TestEnvironment({
code: variation,
parameters: {
test: 'world',
},
});
const expected = env.expect('Hello world!');
// act
const actual = env.sut.compile(env.script);
// assert
expect(actual).to.deep.equal(expected);
});
});
});
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 same parameter repeated', () => {
// arrange
const env = new TestEnvironment({
code: '{{ $parameter }} {{ $parameter }}!',
parameters: {
parameter: 'Hodor',
},
});
const expected = env.expect('Hodor Hodor!');
// act
const actual = env.sut.compile(env.script);
// assert
expect(actual).to.deep.equal(expected);
});
});
it('throws when parameters is 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);
// assert
expect(act).to.throw(expectedError);
});
it('throws on unknown expressions', () => {
// arrange
const env = new TestEnvironment({
code: '{{ each }}',
parameters: {
parameter: undefined,
},
});
const expectedError = 'unknown expression: "each"';
// act
const act = () => env.sut.compile(env.script);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
interface ITestCase {
code: string;
parameters?: FunctionCallParameters;
}
class TestEnvironment {
public readonly sut: IScriptCompiler;
public readonly script: YamlScript;
constructor(testCase: ITestCase) {
const functionName = 'testFunction';
const func: YamlFunction = {
name: functionName,
parameters: testCase.parameters ? Object.keys(testCase.parameters) : undefined,
code: this.getCode(testCase.code, 'execute'),
revertCode: this.getCode(testCase.code, 'revert'),
};
this.sut = new ScriptCompiler([func]);
const call: FunctionCall = {
function: functionName,
parameters: testCase.parameters,
};
this.script = YamlScriptStub.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})`;
}
}
function createFunctions(...names: string[]): YamlFunction[] {
if (!names || names.length === 0) {
names = ['test-function'];
}
return names.map((functionName) => {
const func: YamlFunction = {
name: functionName,
parameters: [],
code: `REM test-code (${functionName})`,
revertCode: `REM test-revert-code (${functionName})`,
};
return func;
});
}

View File

@@ -1,58 +1,93 @@
import { YamlScript } from 'js-yaml-loader!./application.yaml';
import 'mocha';
import { expect } from 'chai';
import { parseScript } from '@/application/Parser/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptCode } from '@/domain/ScriptCode';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { YamlScriptStub } from '../../stubs/YamlScriptStub';
describe('ScriptParser', () => {
describe('parseScript', () => {
it('parses name as expected', () => {
// arrange
const script = getValidScript();
script.name = 'expected-name';
const expected = 'test-expected-name';
const script = YamlScriptStub.createWithCode()
.withName(expected);
const compiler = new ScriptCompilerStub();
// act
const actual = parseScript(script);
const actual = parseScript(script, compiler);
// assert
expect(actual.name).to.equal(script.name);
});
it('parses code as expected', () => {
// arrange
const script = getValidScript();
script.code = 'expected-code';
// act
const actual = parseScript(script);
// assert
expect(actual.code).to.equal(script.code);
});
it('parses revertCode as expected', () => {
// arrange
const script = getValidScript();
script.code = 'expected-code';
// act
const actual = parseScript(script);
// assert
expect(actual.revertCode).to.equal(script.revertCode);
expect(actual.name).to.equal(expected);
});
it('parses docs as expected', () => {
// arrange
const script = getValidScript();
script.docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
const docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
const script = YamlScriptStub.createWithCode()
.withDocs(docs);
const compiler = new ScriptCompilerStub();
const expected = parseDocUrls(script);
// act
const actual = parseScript(script);
const actual = parseScript(script, compiler);
// assert
expect(actual.documentationUrls).to.deep.equal(expected);
});
describe('invalid script', () => {
it('throws when script is undefined', () => {
// arrange
const expectedError = 'undefined script';
const compiler = new ScriptCompilerStub();
const script = undefined;
// act
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw(expectedError);
});
it('throws when both function call and code are defined', () => {
// arrange
const expectedError = 'cannot define both "call" and "code"';
const compiler = new ScriptCompilerStub();
const script = YamlScriptStub
.createWithCall()
.withCode('code');
// act
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw(expectedError);
});
it('throws when both function call and revertCode are defined', () => {
// arrange
const expectedError = 'cannot define "revertCode" if "call" is defined';
const compiler = new ScriptCompilerStub();
const script = YamlScriptStub
.createWithCall()
.withRevertCode('revert-code');
// act
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw(expectedError);
});
it('throws when neither call or revertCode are defined', () => {
// arrange
const expectedError = 'must define either "call" or "code"';
const compiler = new ScriptCompilerStub();
const script = YamlScriptStub.createWithoutCallOrCodes();
// act
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw(expectedError);
});
});
describe('level', () => {
it('accepts undefined level', () => {
const undefinedLevels: string[] = [ '', undefined ];
undefinedLevels.forEach((undefinedLevel) => {
// arrange
const script = getValidScript();
const compiler = new ScriptCompilerStub();
const script = YamlScriptStub.createWithCode();
script.recommend = undefinedLevel;
// act
const actual = parseScript(script);
const actual = parseScript(script, compiler);
// assert
expect(actual.level).to.equal(undefined);
});
@@ -60,10 +95,11 @@ describe('ScriptParser', () => {
it('throws on unknown level', () => {
// arrange
const unknownLevel = 'boi';
const script = getValidScript();
const compiler = new ScriptCompilerStub();
const script = YamlScriptStub.createWithCode();
script.recommend = unknownLevel;
// act
const act = () => parseScript(script);
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw(`unknown level: "${unknownLevel}"`);
});
@@ -71,10 +107,11 @@ describe('ScriptParser', () => {
const nonStringTypes: any[] = [ 5, true ];
nonStringTypes.forEach((nonStringType) => {
// arrange
const script = getValidScript();
const script = YamlScriptStub.createWithCode();
const compiler = new ScriptCompilerStub();
script.recommend = nonStringType;
// act
const act = () => parseScript(script);
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw(`level must be a string but it was ${typeof nonStringType}`);
});
@@ -84,10 +121,11 @@ describe('ScriptParser', () => {
it(levelText, () => {
// arrange
const expectedLevel = RecommendationLevel[levelText];
const script = getValidScript();
const script = YamlScriptStub.createWithCode();
const compiler = new ScriptCompilerStub();
script.recommend = levelText;
// act
const actual = parseScript(script);
const actual = parseScript(script, compiler);
// assert
expect(actual.level).to.equal(expectedLevel);
});
@@ -95,24 +133,66 @@ describe('ScriptParser', () => {
});
it('parses level case insensitive', () => {
// arrange
const script = getValidScript();
const script = YamlScriptStub.createWithCode();
const compiler = new ScriptCompilerStub();
const expected = RecommendationLevel.Standard;
script.recommend = RecommendationLevel[expected].toUpperCase();
// act
const actual = parseScript(script);
const actual = parseScript(script, compiler);
// assert
expect(actual.level).to.equal(expected);
});
});
describe('code', () => {
it('parses code as expected', () => {
// arrange
const expected = 'expected-code';
const script = YamlScriptStub
.createWithCode()
.withCode(expected);
const compiler = new ScriptCompilerStub();
// act
const parsed = parseScript(script, compiler);
// assert
const actual = parsed.code.execute;
expect(actual).to.equal(expected);
});
it('parses revertCode as expected', () => {
// arrange
const expected = 'expected-revert-code';
const script = YamlScriptStub
.createWithCode()
.withRevertCode(expected);
const compiler = new ScriptCompilerStub();
// act
const parsed = parseScript(script, compiler);
// assert
const actual = parsed.code.revert;
expect(actual).to.equal(expected);
});
describe('compiler', () => {
it('throws when compiler is not defined', () => {
// arrange
const script = YamlScriptStub.createWithCode();
const compiler = undefined;
// act
const act = () => parseScript(script, compiler);
// assert
expect(act).to.throw('undefined compiler');
});
it('gets code from compiler', () => {
// arrange
const expected = new ScriptCode('test-script', 'code', 'revert-code');
const script = YamlScriptStub.createWithCode();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script, expected);
// act
const parsed = parseScript(script, compiler);
// assert
const actual = parsed.code;
expect(actual).to.equal(expected);
});
});
});
});
});
function getValidScript(): YamlScript {
return {
name: 'valid-name',
code: 'valid-code',
revertCode: 'expected revert code',
docs: ['hello.com'],
recommend: RecommendationLevel[RecommendationLevel.Standard].toLowerCase(),
};
}

View File

@@ -18,10 +18,11 @@ describe('UserFilter', () => {
// assert
expect(isCalled).to.be.equal(true);
});
it('currentFilter is undefined', () => {
it('sets currentFilter to undefined', () => {
// arrange
const sut = new UserFilter(new ApplicationStub());
// act
sut.setFilter('non-important');
sut.removeFilter();
// assert
expect(sut.currentFilter).to.be.equal(undefined);
@@ -51,68 +52,69 @@ describe('UserFilter', () => {
expect(actual.hasAnyMatches()).be.equal(false);
expect(actual.query).to.equal(nonMatchingFilter);
});
describe('signals when script matches', () => {
it('code matches', () => {
// arrange
const code = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withCode(code);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
describe('signals when matches', () => {
describe('signals when script matches', () => {
it('code matches', () => {
// arrange
const code = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withCode(code);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('revertCode matches', () => {
// arrange
const revertCode = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withRevertCode(revertCode);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('name matches', () => {
// arrange
const name = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withName(name);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
});
it('revertCode matches', () => {
// arrange
const revertCode = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withRevertCode(revertCode);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('name matches', () => {
// arrange
const name = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withName(name);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('signals when category matches', () => {
// arrange
const categoryName = 'HELLO world';

View File

@@ -1,76 +1,149 @@
import 'mocha';
import { expect } from 'chai';
import { Script } from '@/domain/Script';
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
import { RecommendationLevel, RecommendationLevels } from '@/domain/RecommendationLevel';
import { ScriptCode } from '@/domain/ScriptCode';
import { IScriptCode } from '@/domain/IScriptCode';
describe('Script', () => {
describe('ctor', () => {
describe('code', () => {
it('cannot construct with duplicate lines', () => {
const code = 'duplicate\nduplicate\ntest\nduplicate';
expect(() => createWithCode(code)).to.throw();
});
it('cannot construct with empty lines', () => {
const code = 'duplicate\n\n\ntest\nduplicate';
expect(() => createWithCode(code)).to.throw();
});
describe('scriptCode', () => {
it('sets as expected', () => {
const expected = 'expected-revert';
const sut = createWithCode(expected);
expect(sut.code).to.equal(expected);
// arrange
const name = 'test-script';
const expected = new ScriptCode(name, 'expected-execute', 'expected-revert');
const sut = new ScriptBuilder()
.withCode(expected)
.build();
// act
const actual = sut.code;
// assert
expect(actual).to.deep.equal(expected);
});
});
describe('revertCode', () => {
it('cannot construct with duplicate lines', () => {
const code = 'duplicate\nduplicate\ntest\nduplicate';
expect(() => createWithCode('REM', code)).to.throw();
});
it('cannot construct with empty lines', () => {
const code = 'duplicate\n\n\ntest\nduplicate';
expect(() => createWithCode('REM', code)).to.throw();
});
it('cannot construct with when same as code', () => {
const code = 'REM';
expect(() => createWithCode(code, code)).to.throw();
});
it('sets as expected', () => {
const expected = 'expected-revert';
const sut = createWithCode('abc', expected);
expect(sut.revertCode).to.equal(expected);
it('throws if undefined', () => {
// arrange
const name = 'script-name';
const expectedError = `undefined code (script: ${name})`;
const code: IScriptCode = undefined;
// act
const construct = () => new ScriptBuilder()
.withName(name)
.withCode(code)
.build();
// assert
expect(construct).to.throw(expectedError);
});
});
describe('canRevert', () => {
it('returns false without revert code', () => {
const sut = createWithCode('code');
expect(sut.canRevert()).to.equal(false);
// arrange
const sut = new ScriptBuilder()
.withCodes('code')
.build();
// act
const actual = sut.canRevert();
// assert
expect(actual).to.equal(false);
});
it('returns true with revert code', () => {
const sut = createWithCode('code', 'non empty revert code');
expect(sut.canRevert()).to.equal(true);
// arrange
const sut = new ScriptBuilder()
.withCodes('code', 'non empty revert code')
.build();
// act
const actual = sut.canRevert();
// assert
expect(actual).to.equal(true);
});
});
describe('level', () => {
it('cannot construct with invalid wrong value', () => {
expect(() => createWithLevel(55)).to.throw('invalid level');
// arrange
const invalidValue: RecommendationLevel = 55;
const expectedError = 'invalid level';
// act
const construct = () => new ScriptBuilder()
.withRecommendationLevel(invalidValue)
.build();
// assert
expect(construct).to.throw(expectedError);
});
it('sets undefined as expected', () => {
const sut = createWithLevel(undefined);
expect(sut.level).to.equal(undefined);
// arrange
const expected = undefined;
// act
const sut = new ScriptBuilder()
.withRecommendationLevel(expected)
.build();
// assert
expect(sut.level).to.equal(expected);
});
it('sets as expected', () => {
for (const expected of RecommendationLevelNames) {
const sut = createWithLevel(RecommendationLevel[expected]);
const actual = RecommendationLevel[sut.level];
// arrange
for (const expected of RecommendationLevels) {
// act
const sut = new ScriptBuilder()
.withRecommendationLevel(expected)
.build();
// assert
const actual = sut.level;
expect(actual).to.equal(expected);
}
});
});
describe('documentationUrls', () => {
it('sets as expected', () => {
// arrange
const expected = [ 'doc1', 'doc2 '];
// act
const sut = new ScriptBuilder()
.withDocumentationUrls(expected)
.build();
const actual = sut.documentationUrls;
// assert
expect(actual).to.equal(expected);
});
});
});
});
function createWithCode(code: string, revertCode?: string): Script {
return new Script('name', code, revertCode, [], RecommendationLevel.Standard);
}
function createWithLevel(level: RecommendationLevel): Script {
return new Script('name', 'code', 'revertCode', [], level);
class ScriptBuilder {
private name = 'test-script';
private code: IScriptCode = new ScriptCode(this.name, 'code', 'revert-code');
private level = RecommendationLevel.Standard;
private documentationUrls: readonly string[] = undefined;
public withCodes(code: string, revertCode = ''): ScriptBuilder {
this.code = new ScriptCode(this.name, code, revertCode);
return this;
}
public withCode(code: IScriptCode): ScriptBuilder {
this.code = code;
return this;
}
public withName(name: string): ScriptBuilder {
this.name = name;
return this;
}
public withRecommendationLevel(level: RecommendationLevel): ScriptBuilder {
this.level = level;
return this;
}
public withDocumentationUrls(urls: readonly string[]): ScriptBuilder {
this.documentationUrls = urls;
return this;
}
public build(): Script {
return new Script(
this.name,
this.code,
this.documentationUrls,
this.level,
);
}
}

View File

@@ -0,0 +1,93 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptCode } from '@/domain/ScriptCode';
describe('ScriptCode', () => {
describe('scriptName', () => {
it('throws if undefined', () => {
// arrange
const expectedError = 'name is undefined';
const name = undefined;
// act
const act = () => new ScriptCode(name, 'non-empty-code', '');
// assert
expect(act).to.throw(expectedError);
});
});
describe('code', () => {
it('cannot construct with duplicate lines', () => {
// arrange
const code = 'duplicate\nduplicate\ntest\nduplicate';
// act
const act = () => createSut(code);
// assert
expect(act).to.throw();
});
it('cannot construct with empty lines', () => {
// arrange
const code = 'line1\n\n\nline2';
// act
const act = () => createSut(code);
// assert
expect(act).to.throw();
});
it('cannot construct with empty or undefined values', () => {
// arrange
const name = 'test-code';
const errorMessage = `code of ${name} is empty or undefined`;
const invalidValues = [ '', undefined ];
invalidValues.forEach((invalidValue) => {
// act
const act = () => new ScriptCode(name, invalidValue, '');
// assert
expect(act).to.throw(errorMessage);
});
});
it('sets as expected', () => {
// arrange
const expected = 'expected-revert';
// act
const sut = createSut(expected);
// assert
expect(sut.execute).to.equal(expected);
});
});
describe('revert', () => {
it('cannot construct with duplicate lines', () => {
// arrange
const code = 'duplicate\nduplicate\ntest\nduplicate';
// act
const act = () => createSut('REM', code);
// assert
expect(act).to.throw();
});
it('cannot construct with empty lines', () => {
// arrange
const code = 'line1\n\n\nline2';
// act
const act = () => createSut('REM', code);
// assert
expect(act).to.throw();
});
it('cannot construct with when same as code', () => {
// arrange
const code = 'REM';
// act
const act = () => createSut(code, code);
// assert
expect(act).to.throw();
});
it('sets as expected', () => {
// arrange
const expected = 'expected-revert';
// act
const sut = createSut('abc', expected);
// assert
expect(sut.revert).to.equal(expected);
});
});
});
function createSut(code: string, revert = ''): ScriptCode {
return new ScriptCode('test-code', code, revert);
}

View File

@@ -3,11 +3,11 @@ import { expect } from 'chai';
import { getScriptNodeId, getScriptId, getCategoryNodeId, getCategoryId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
import { CategoryStub } from '../../../stubs/CategoryStub';
import { ScriptStub } from '../../../stubs/ScriptStub';
import { parseSingleCategory, parseAllCategories } from '../../../../../src/presentation/Scripts/ScriptsTree/ScriptNodeParser';
import { parseSingleCategory, parseAllCategories } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
import { ApplicationStub } from '../../../stubs/ApplicationStub';
import { INode, NodeType } from '../../../../../src/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
import { IScript } from '../../../../../src/domain/IScript';
import { ICategory } from '../../../../../src/domain/ICategory';
import { INode, NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
describe('ScriptNodeParser', () => {
it('can convert script id and back', () => {
@@ -80,7 +80,7 @@ describe('ScriptNodeParser', () => {
function isReversible(category: ICategory): boolean {
if (category.scripts) {
return category.scripts.every((s) => s.revertCode);
return category.scripts.every((s) => s.canRevert());
}
return category.subCategories.every((c) => isReversible(c));
}
@@ -100,8 +100,8 @@ function expectSameCategory(node: INode, category: ICategory): void {
}
function getErrorMessage(field: string) {
return `Unexpected node field: ${field}.\n` +
`\nActual node:\n${JSON.stringify(node, null, 2)}` +
`\nExpected category:\n${JSON.stringify(category, null, 2)}`;
`\nActual node:\n${print(node)}` +
`\nExpected category:\n${print(category)}`;
}
}
@@ -110,11 +110,15 @@ function expectSameScript(node: INode, script: IScript): void {
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
expect(node.text).to.equal(script.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(!!script.revertCode, getErrorMessage('revertCode'));
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
expect(node.children).to.equal(undefined);
function getErrorMessage(field: string) {
return `Unexpected node field: ${field}.` +
`\nActual node:\n${JSON.stringify(node, null, 2)}\n` +
`\nExpected script:\n${JSON.stringify(script, null, 2)}`;
`\nActual node:\n${print(node)}\n` +
`\nExpected script:\n${print(script)}`;
}
}
function print(object: any) {
return JSON.stringify(object, null, 2);
}

View File

@@ -4,7 +4,7 @@ import { ScriptReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTre
import { SelectedScriptStub } from '../../../../../../stubs/SelectedScriptStub';
import { getScriptNodeId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
import { ScriptStub } from '../../../../../../stubs/ScriptStub';
import { UserSelection } from '../../../../../../../../src/application/State/Selection/UserSelection';
import { UserSelection } from '@/application/State/Selection/UserSelection';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { ApplicationStub } from '../../../../../../stubs/ApplicationStub';
import { CategoryStub } from '../../../../../../stubs/CategoryStub';

View File

@@ -0,0 +1,18 @@
import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
import { IScriptCode } from '@/domain/IScriptCode';
import { YamlScript } from 'js-yaml-loader!./application.yaml';
export class ScriptCompilerStub implements IScriptCompiler {
public compilables = new Map<YamlScript, IScriptCode>();
public canCompile(script: YamlScript): boolean {
return this.compilables.has(script);
}
public compile(script: YamlScript): IScriptCode {
return this.compilables.get(script);
}
public withCompileAbility(script: YamlScript, result?: IScriptCode): ScriptCompilerStub {
this.compilables.set(script, result ||
{ execute: `compiled code of ${script.name}`, revert: `compiled revert code of ${script.name}` });
return this;
}
}

View File

@@ -4,8 +4,10 @@ import { RecommendationLevel } from '@/domain/RecommendationLevel';
export class ScriptStub extends BaseEntity<string> implements IScript {
public name = `name${this.id}`;
public code = `REM code${this.id}`;
public revertCode = `REM revertCode${this.id}`;
public code = {
execute: `REM execute-code (${this.id})`,
revert: `REM revert-code (${this.id})`,
};
public readonly documentationUrls = new Array<string>();
public level = RecommendationLevel.Standard;
@@ -14,7 +16,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
}
public canRevert(): boolean {
return Boolean(this.revertCode);
return Boolean(this.code.revert);
}
public withLevel(value: RecommendationLevel): ScriptStub {
@@ -23,7 +25,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
}
public withCode(value: string): ScriptStub {
this.code = value;
this.code.execute = value;
return this;
}
@@ -33,7 +35,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
}
public withRevertCode(revertCode: string): ScriptStub {
this.revertCode = revertCode;
this.code.revert = revertCode;
return this;
}
}

View File

@@ -0,0 +1,61 @@
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptFunctionCall, YamlScript } from 'js-yaml-loader!./application.yaml';
export class YamlScriptStub implements YamlScript {
public static createWithCode(): YamlScriptStub {
return new YamlScriptStub()
.withCode('stub-code')
.withRevertCode('stub-revert-code');
}
public static createWithCall(call?: ScriptFunctionCall): YamlScriptStub {
let instance = new YamlScriptStub();
if (call) {
instance = instance.withCall(call);
} else {
instance = instance.withMockCall();
}
return instance;
}
public static createWithoutCallOrCodes(): YamlScriptStub {
return new YamlScriptStub();
}
public name = 'valid-name';
public code = undefined;
public revertCode = undefined;
public call = undefined;
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
public docs = ['hello.com'];
private constructor() { }
public withName(name: string): YamlScriptStub {
this.name = name;
return this;
}
public withDocs(docs: string[]): YamlScriptStub {
this.docs = docs;
return this;
}
public withCode(code: string): YamlScriptStub {
this.code = code;
return this;
}
public withRevertCode(revertCode: string): YamlScriptStub {
this.revertCode = revertCode;
return this;
}
public withMockCall(): YamlScriptStub {
this.call = { function: 'func', parameters: [] };
return this;
}
public withCall(call: ScriptFunctionCall): YamlScriptStub {
this.call = call;
return this;
}
}