add initial macOS support #40

This commit is contained in:
undergroundwires
2021-01-13 16:31:20 +01:00
parent 2428de23ee
commit 8a8b7319d5
99 changed files with 2663 additions and 1135 deletions

View File

@@ -22,7 +22,7 @@ describe('ApplicationContext', () => {
.construct();
sut.changeContext(OperatingSystem.macOS);
// assert
expect(sut.collection).to.equal(expectedCollection);
expect(sut.state.collection).to.equal(expectedCollection);
});
it('currentOs is changed as expected', () => {
// arrange
@@ -35,9 +35,9 @@ describe('ApplicationContext', () => {
.construct();
sut.changeContext(expectedOs);
// assert
expect(sut.currentOs).to.equal(expectedOs);
expect(sut.state.os).to.equal(expectedOs);
});
it('state is changed as expected', () => {
it('new state is empty', () => {
// arrange
const testContext = new ObservableApplicationContextFactory()
.withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS);
@@ -45,6 +45,7 @@ describe('ApplicationContext', () => {
const sut = testContext
.withInitialOs(OperatingSystem.Windows)
.construct();
sut.state.filter.setFilter('filtered');
sut.changeContext(OperatingSystem.macOS);
// assert
expectEmptyState(sut.state);
@@ -82,12 +83,13 @@ describe('ApplicationContext', () => {
const sut = testContext
.withInitialOs(OperatingSystem.Windows)
.construct();
const oldState = sut.state;
sut.changeContext(nextOs);
// assert
expect(testContext.firedEvents.length).to.equal(1);
expect(testContext.firedEvents[0].newCollection).to.equal(expectedCollection);
expect(testContext.firedEvents[0].newState).to.equal(sut.state);
expect(testContext.firedEvents[0].newOs).to.equal(nextOs);
expect(testContext.firedEvents[0].newState.collection).to.equal(expectedCollection);
expect(testContext.firedEvents[0].oldState).to.equal(oldState);
});
it('is not fired when initial os is changed to same one', () => {
// arrange
@@ -148,7 +150,7 @@ describe('ApplicationContext', () => {
.withInitialOs(os)
.construct();
// assert
const actual = sut.collection;
const actual = sut.state.collection;
expect(actual).to.deep.equal(expected);
});
});
@@ -174,7 +176,7 @@ describe('ApplicationContext', () => {
.withInitialOs(expected)
.construct();
// assert
const actual = sut.currentOs;
const actual = sut.state.os;
expect(actual).to.deep.equal(expected);
});
describe('throws when OS is invalid', () => {

View File

@@ -17,7 +17,7 @@ describe('ApplicationContextProvider', () => {
// act
const context = buildContext(parserMock);
// assert
// TODO: expect(expected).to.equal(context.app);
expect(expected).to.equal(context.app);
});
describe('sets initial OS as expected', () => {
it('returns currentOs if it is supported', () => {
@@ -28,7 +28,8 @@ describe('ApplicationContextProvider', () => {
// act
const context = buildContext(parser, environment);
// assert
expect(expected).to.equal(context.currentOs);
const actual = context.state.os;
expect(expected).to.equal(actual);
});
it('fallbacks to other os if OS in environment is not supported', () => {
// arrange
@@ -39,11 +40,25 @@ describe('ApplicationContextProvider', () => {
// act
const context = buildContext(parser, environment);
// assert
const actual = context.currentOs;
const actual = context.state.os;
expect(expected).to.equal(actual);
});
it('fallbacks to most supported os if current os is not supported', () => {
// TODO: After more than single collection can be parsed
// arrange
const expectedOs = OperatingSystem.Android;
const allCollections = [
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
];
const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
const app = new ApplicationStub().withCollections(...allCollections);
const parser: ApplicationParserType = () => app;
// act
const context = buildContext(parser, environment);
// assert
const actual = context.state.os;
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
});
});
});

View File

@@ -3,6 +3,7 @@ import { expect } from 'chai';
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode';
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IScript } from '@/domain/IScript';
import { ScriptStub } from '../../../stubs/ScriptStub';
import { CategoryStub } from '../../../stubs/CategoryStub';
@@ -21,7 +22,8 @@ describe('CategoryCollectionState', () => {
});
it('reacts to selection changes as expected', () => {
// arrange
const collection = new CategoryCollectionStub().withAction(new CategoryStub(0).withScriptIds('scriptId'));
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScriptIds('scriptId'));
const selectionStub = new UserSelection(collection, []);
const expectedCodeGenerator = new ApplicationCode(selectionStub, collection.scripting);
selectionStub.selectAll();
@@ -34,6 +36,19 @@ describe('CategoryCollectionState', () => {
expect(actualCode).to.equal(expectedCode);
});
});
describe('os', () => {
it('same as its collection', () => {
// arrange
const expected = OperatingSystem.macOS;
const collection = new CategoryCollectionStub()
.withOs(expected);
// act
const sut = new CategoryCollectionState(collection);
// assert
const actual = sut.os;
expect(expected).to.equal(actual);
});
});
describe('selection', () => {
it('initialized with no selection', () => {
// arrange
@@ -70,7 +85,8 @@ describe('CategoryCollectionState', () => {
it('can match a script from current collection', () => {
// arrange
const scriptNameFilter = 'scriptName';
const expectedScript = new ScriptStub('scriptId').withName(scriptNameFilter);
const expectedScript = new ScriptStub('scriptId')
.withName(scriptNameFilter);
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScript(expectedScript));
const sut = new CategoryCollectionState(collection);

View File

@@ -3,10 +3,23 @@ import { expect } from 'chai';
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
describe('CodeBuilder', () => {
class CodeBuilderConcrete extends CodeBuilder {
private commentDelimiter = '//';
public withCommentDelimiter(delimiter: string): CodeBuilderConcrete {
this.commentDelimiter = delimiter;
return this;
}
protected getCommentDelimiter(): string {
return this.commentDelimiter;
}
protected writeStandardOut(text: string): string {
return text;
}
}
describe('appendLine', () => {
it('when empty appends empty line', () => {
// arrange
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
// act
sut.appendLine().appendLine().appendLine();
// assert
@@ -14,7 +27,7 @@ describe('CodeBuilder', () => {
});
it('when not empty append string in new line', () => {
// arrange
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
const expected = 'str';
// act
sut.appendLine()
@@ -27,7 +40,7 @@ describe('CodeBuilder', () => {
});
it('appendFunction', () => {
// arrange
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
const functionName = 'function';
const code = 'code';
// act
@@ -39,11 +52,13 @@ describe('CodeBuilder', () => {
});
it('appendTrailingHyphensCommentLine', () => {
// arrange
const sut = new CodeBuilder();
const totalHypens = 5;
const expected = `:: ${'-'.repeat(totalHypens)}`;
const commentDelimiter = '//';
const sut = new CodeBuilderConcrete()
.withCommentDelimiter(commentDelimiter);
const totalHyphens = 5;
const expected = `${commentDelimiter} ${'-'.repeat(totalHyphens)}`;
// act
sut.appendTrailingHyphensCommentLine(totalHypens);
sut.appendTrailingHyphensCommentLine(totalHyphens);
// assert
const result = sut.toString();
const lines = getLines(result);
@@ -51,38 +66,45 @@ describe('CodeBuilder', () => {
});
it('appendCommentLine', () => {
// arrange
const sut = new CodeBuilder();
const commentDelimiter = '//';
const sut = new CodeBuilderConcrete()
.withCommentDelimiter(commentDelimiter);
const comment = 'comment';
const expected = ':: comment';
const expected = `${commentDelimiter} comment`;
// act
sut.appendCommentLine(comment);
const result = sut
.appendCommentLine(comment)
.toString();
// assert
const result = sut.toString();
const lines = getLines(result);
expect(lines[0]).to.equal(expected);
});
it('appendCommentLineWithHyphensAround', () => {
// arrange
const sut = new CodeBuilder();
const commentDelimiter = '//';
const sut = new CodeBuilderConcrete()
.withCommentDelimiter(commentDelimiter);
const sectionName = 'section';
const totalHypens = sectionName.length + 3 * 2;
const expected = ':: ---section---';
sut.appendCommentLineWithHyphensAround(sectionName, totalHypens);
const totalHyphens = sectionName.length + 3 * 2;
const expected = `${commentDelimiter} ---section---`;
// act
const result = sut
.appendCommentLineWithHyphensAround(sectionName, totalHyphens)
.toString();
// assert
const result = sut.toString();
const lines = getLines(result);
expect(lines[1]).to.equal(expected);
});
describe('currentLine', () => {
it('no lines returns zero', () => {
// arrange & act
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
// assert
expect(sut.currentLine).to.equal(0);
});
it('single line returns one', () => {
// arrange
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
// act
sut.appendLine();
// assert
@@ -90,15 +112,17 @@ describe('CodeBuilder', () => {
});
it('multiple lines returns as expected', () => {
// arrange
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
// act
sut.appendLine('1').appendCommentLine('2').appendLine();
sut.appendLine('1')
.appendCommentLine('2')
.appendLine();
// assert
expect(sut.currentLine).to.equal(3);
});
it('multiple lines in code', () => {
// arrange
const sut = new CodeBuilder();
const sut = new CodeBuilderConcrete();
// act
sut.appendLine('hello\ncode-here\nwith-3-lines');
// assert

View File

@@ -0,0 +1,36 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder';
import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder';
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
describe('CodeBuilderFactory', () => {
describe('create', () => {
describe('creates expected type', () => {
// arrange
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
{ language: ScriptingLanguage.shellscript, expected: ShellBuilder},
{ language: ScriptingLanguage.batchfile, expected: BatchBuilder},
];
for (const testCase of testCases) {
it(ScriptingLanguage[testCase.language], () => {
// act
const sut = new CodeBuilderFactory();
const result = sut.create(testCase.language);
// assert
expect(result).to.be.instanceOf(testCase.expected,
`Actual was: ${result.constructor.name}`);
});
}
});
it('throws on unknown scripting language', () => {
// arrange
const sut = new CodeBuilderFactory();
// act
const act = () => sut.create(3131313131);
// assert
expect(act).to.throw(`unknown language: "${ScriptingLanguage[3131313131]}"`);
});
});
});

View File

@@ -0,0 +1,37 @@
import 'mocha';
import { expect } from 'chai';
import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder';
describe('BatchBuilder', () => {
class BatchBuilderRevealer extends BatchBuilder {
public getCommentDelimiter(): string {
return super.getCommentDelimiter();
}
public writeStandardOut(text: string): string {
return super.writeStandardOut(text);
}
}
describe('getCommentDelimiter', () => {
it('returns expected', () => {
// arrange
const expected = '::';
const sut = new BatchBuilderRevealer();
// act
const actual = sut.getCommentDelimiter();
// assert
expect(expected).to.equal(actual);
});
});
describe('writeStandardOut', () => {
it('prepends expected', () => {
// arrange
const text = 'test';
const expected = `echo ${text}`;
const sut = new BatchBuilderRevealer();
// act
const actual = sut.writeStandardOut(text);
// assert
expect(expected).to.equal(actual);
});
});
});

View File

@@ -0,0 +1,37 @@
import 'mocha';
import { expect } from 'chai';
import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder';
describe('ShellBuilder', () => {
class ShellBuilderRevealer extends ShellBuilder {
public getCommentDelimiter(): string {
return super.getCommentDelimiter();
}
public writeStandardOut(text: string): string {
return super.writeStandardOut(text);
}
}
describe('getCommentDelimiter', () => {
it('returns expected', () => {
// arrange
const expected = '#';
const sut = new ShellBuilderRevealer();
// act
const actual = sut.getCommentDelimiter();
// assert
expect(expected).to.equal(actual);
});
});
describe('writeStandardOut', () => {
it('prepends expected', () => {
// arrange
const text = 'test';
const expected = `echo '${text}'`;
const sut = new ShellBuilderRevealer();
// act
const actual = sut.writeStandardOut(text);
// assert
expect(expected).to.equal(actual);
});
});
});

View File

@@ -2,7 +2,8 @@ import 'mocha';
import { expect } from 'chai';
import { UserScriptGenerator } from '@/application/Context/State/Code/Generation/UserScriptGenerator';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
import { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation/ICodeBuilderFactory';
import { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder';
import { ScriptStub } from '../../../../../stubs/ScriptStub';
import { ScriptingDefinitionStub } from '../../../../../stubs/ScriptingDefinitionStub';
@@ -28,14 +29,15 @@ describe('UserScriptGenerator', () => {
});
it('is not prepended if empty', () => {
// arrange
const sut = new UserScriptGenerator();
const codeBuilderStub = new CodeBuilderStub();
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
const script = new ScriptStub('id')
.withCode('code\nmulti-lined')
.toSelectedScript();
const definition = new ScriptingDefinitionStub()
.withStartCode(undefined)
.withEndCode(undefined);
const expectedStart = new CodeBuilder()
const expectedStart = codeBuilderStub
.appendFunction(script.script.name, script.script.code.execute)
.toString();
// act
@@ -64,15 +66,16 @@ describe('UserScriptGenerator', () => {
});
it('is not appended if empty', () => {
// arrange
const sut = new UserScriptGenerator();
const codeBuilderStub = new CodeBuilderStub();
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
const script = new ScriptStub('id')
.withCode('code\nmulti-lined')
.toSelectedScript();
const definition = new ScriptingDefinitionStub()
.withEndCode(undefined);
const expectedEnd = new CodeBuilder()
const expectedEnd = codeBuilderStub
.appendFunction(script.script.name, script.script.code.execute)
.toString();
const definition = new ScriptingDefinitionStub()
.withEndCode(undefined);
// act
const code = sut.buildCode([script], definition);
// assert
@@ -199,3 +202,36 @@ describe('UserScriptGenerator', () => {
});
});
});
function mockCodeBuilderFactory(mock: ICodeBuilder): ICodeBuilderFactory {
return {
create: () => mock,
};
}
class CodeBuilderStub implements ICodeBuilder {
public currentLine = 0;
private text = '';
public appendLine(code?: string): ICodeBuilder {
this.text += this.text ? `${code}\n` : code;
this.currentLine++;
return this;
}
public appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder {
return this.appendLine(`trailing-hyphens-${totalRepeatHyphens}`);
}
public appendCommentLine(commentLine?: string): ICodeBuilder {
return this.appendLine(`Comment | ${commentLine}`);
}
public appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder {
return this.appendLine(`hyphens-around-${totalRepeatHyphens} | Section name: ${sectionName} | hyphens-around-${totalRepeatHyphens}`);
}
public appendFunction(name: string, code: string): ICodeBuilder {
return this
.appendLine(`Function | Name: ${name}`)
.appendLine(`Function | Code: ${code}`);
}
public toString(): string {
return this.text;
}
}

View File

@@ -3,11 +3,13 @@ import { expect } from 'chai';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
import { CollectionData } from 'js-yaml-loader!@/*';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getEnumValues } from '@/application/Common/Enum';
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
import { getProcessEnvironmentStub } from '../../stubs/ProcessEnvironmentStub';
import { CollectionDataStub } from '../../stubs/CollectionDataStub';
@@ -24,15 +26,18 @@ describe('ApplicationParser', () => {
it('returns result from the parser', () => {
// arrange
const os = OperatingSystem.macOS;
const data = new CollectionDataStub();
const expected = new CategoryCollectionStub()
.withOs(os);
const parser = new CategoryCollectionParserSpy()
.setResult(expected)
.setUpReturnValue(data, expected)
.mockParser();
const env = getProcessEnvironmentStub();
const collections = [ data ];
// act
const context = parseApplication(parser);
const app = parseApplication(parser, env, collections);
// assert
const actual = context.getCollection(os);
const actual = app.getCollection(os);
expect(expected).to.equal(actual);
});
});
@@ -44,10 +49,10 @@ describe('ApplicationParser', () => {
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
// act
const context = parseApplication(parserMock, env);
const app = parseApplication(parserMock, env);
// assert
expect(expected).to.deep.equal(context.info);
expect(expected).to.deep.equal(parserSpy.lastArguments.info);
expect(expected).to.deep.equal(app.info);
expect(parserSpy.arguments.map((arg) => arg.info).every((info) => info === expected));
});
it('defaults to process.env', () => {
// arrange
@@ -56,54 +61,110 @@ describe('ApplicationParser', () => {
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
// act
const context = parseApplication(parserMock);
const app = parseApplication(parserMock);
// assert
expect(expected).to.deep.equal(context.info);
expect(expected).to.deep.equal(parserSpy.lastArguments.info);
expect(expected).to.deep.equal(app.info);
expect(parserSpy.arguments.map((arg) => arg.info).every((info) => info === expected));
});
});
describe('collectionData', () => {
it('parsed with expected data', () => {
describe('collectionsData', () => {
describe('set as expected', () => {
// arrange
const expected = new CollectionDataStub();
const env = getProcessEnvironmentStub();
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
const testCases = [
{
name: 'single collection',
input: [ new CollectionDataStub() ],
output: [ new CategoryCollectionStub().withOs(OperatingSystem.macOS) ],
},
{
name: 'multiple collections',
input: [
new CollectionDataStub().withOs('windows'),
new CollectionDataStub().withOs('macos'),
],
output: [
new CategoryCollectionStub().withOs(OperatingSystem.macOS),
new CategoryCollectionStub().withOs(OperatingSystem.Windows),
],
},
];
// act
parseApplication(parserMock, env, expected);
// assert
expect(expected).to.equal(parserSpy.lastArguments.file);
for (const testCase of testCases) {
it(testCase.name, () => {
const env = getProcessEnvironmentStub();
let parserSpy = new CategoryCollectionParserSpy();
for (let i = 0; i < testCase.input.length; i++) {
parserSpy = parserSpy.setUpReturnValue(testCase.input[i], testCase.output[i]);
}
const parserMock = parserSpy.mockParser();
// act
const app = parseApplication(parserMock, env, testCase.input);
// assert
expect(app.collections).to.deep.equal(testCase.output);
});
}
});
it('defaults to windows data', () => {
it('defaults to expected data', () => {
// arrange
const expected = WindowsData;
const expected = [ WindowsData, MacOsData ];
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
// act
parseApplication(parserMock);
// assert
expect(expected).to.equal(parserSpy.lastArguments.file);
const actual = parserSpy.arguments.map((args) => args.data);
expect(actual).to.deep.equal(expected);
});
describe('throws when data is invalid', () => {
// arrange
const testCases = [
{
expectedError: 'no collection provided',
data: [],
},
{
expectedError: 'undefined collection provided',
data: [ new CollectionDataStub(), undefined ],
},
];
for (const testCase of testCases) {
it(testCase.expectedError, () => {
const parserMock = new CategoryCollectionParserSpy().mockParser();
const env = getProcessEnvironmentStub();
// act
const act = () => parseApplication(parserMock, env, testCase.data);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
});
});
});
class CategoryCollectionParserSpy {
public lastArguments: {
file: CollectionData;
info: ProjectInformation;
} = { file: undefined, info: undefined };
private result: ICategoryCollection = new CategoryCollectionStub();
public arguments = new Array<{
data: CollectionData,
info: ProjectInformation,
}>();
public setResult(collection: ICategoryCollection): CategoryCollectionParserSpy {
this.result = collection;
private returnValues = new Map<CollectionData, ICategoryCollection>();
public setUpReturnValue(data: CollectionData, collection: ICategoryCollection): CategoryCollectionParserSpy {
this.returnValues.set(data, collection);
return this;
}
public mockParser(): CategoryCollectionParserType {
return (file: CollectionData, info: IProjectInformation) => {
this.lastArguments.file = file;
this.lastArguments.info = info;
return this.result;
return (data: CollectionData, info: IProjectInformation) => {
this.arguments.push({ data, info });
if (this.returnValues.has(data)) {
return this.returnValues.get(data);
} else {
// Get next OS with a unique OS so mock does not result in invalid app (with duplicate OS collections)
const currentRun = this.arguments.length - 1;
const nextOs = getEnumValues(OperatingSystem)[currentRun];
return new CategoryCollectionStub().withOs(nextOs);
}
};
}
}

View File

@@ -8,8 +8,8 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
import { mockEnumParser } from '../../stubs/EnumParserStub';
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => {
@@ -48,8 +48,8 @@ describe('CategoryCollectionParser', () => {
it('parses actions', () => {
// arrange
const actions = [ getCategoryStub('test1'), getCategoryStub('test2') ];
const compiler = new ScriptCompilerStub();
const expected = [ parseCategory(actions[0], compiler), parseCategory(actions[1], compiler) ];
const context = new CategoryCollectionParseContextStub();
const expected = [ parseCategory(actions[0], context), parseCategory(actions[1], context) ];
const collection = new CollectionDataStub()
.withActions(actions);
const info = new ProjectInformationStub();

View File

@@ -2,10 +2,12 @@ import 'mocha';
import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser';
import { CategoryData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
import { parseScript } from '@/application/Parser/ScriptParser';
import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
import { LanguageSyntaxStub } from '../../stubs/LanguageSyntaxStub';
describe('CategoryParser', () => {
describe('parseCategory', () => {
@@ -14,9 +16,9 @@ describe('CategoryParser', () => {
// arrange
const expectedMessage = 'category is null or undefined';
const category = undefined;
const compiler = new ScriptCompilerStub();
const context = new CategoryCollectionParseContextStub();
// act
const act = () => parseCategory(category, compiler);
const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
@@ -28,9 +30,9 @@ describe('CategoryParser', () => {
category: categoryName,
children: [],
};
const compiler = new ScriptCompilerStub();
const context = new CategoryCollectionParseContextStub();
// act
const act = () => parseCategory(category, compiler);
const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
@@ -42,9 +44,9 @@ describe('CategoryParser', () => {
category: categoryName,
children: undefined,
};
const compiler = new ScriptCompilerStub();
const context = new CategoryCollectionParseContextStub();
// act
const act = () => parseCategory(category, compiler);
const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
@@ -57,21 +59,21 @@ describe('CategoryParser', () => {
category: invalidName,
children: getTestChildren(),
};
const compiler = new ScriptCompilerStub();
const context = new CategoryCollectionParseContextStub();
// act
const act = () => parseCategory(category, compiler);
const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
});
});
it('throws when compiler is undefined', () => {
it('throws when context is undefined', () => {
// arrange
const expectedError = 'undefined compiler';
const compiler = undefined;
const expectedError = 'undefined context';
const context = undefined;
const category = getValidCategory();
// act
const act = () => parseCategory(category, compiler);
const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedError);
});
@@ -79,14 +81,14 @@ describe('CategoryParser', () => {
// arrange
const url = 'https://privacy.sexy';
const expected = parseDocUrls({ docs: url });
const compiler = new ScriptCompilerStub();
const category: CategoryData = {
category: 'category name',
children: getTestChildren(),
docs: url,
};
const context = new CategoryCollectionParseContextStub();
// act
const actual = parseCategory(category, compiler).documentationUrls;
const actual = parseCategory(category, context).documentationUrls;
// assert
expect(actual).to.deep.equal(expected);
});
@@ -94,14 +96,14 @@ describe('CategoryParser', () => {
it('single script with code', () => {
// arrange
const script = ScriptDataStub.createWithCode();
const compiler = new ScriptCompilerStub();
const expected = [ parseScript(script, compiler) ];
const context = new CategoryCollectionParseContextStub();
const expected = [ parseScript(script, context) ];
const category: CategoryData = {
category: 'category name',
children: [ script ],
};
// act
const actual = parseCategory(category, compiler).scripts;
const actual = parseCategory(category, context).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
@@ -110,13 +112,15 @@ describe('CategoryParser', () => {
const script = ScriptDataStub.createWithCall();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script);
const expected = [ parseScript(script, compiler) ];
const context = new CategoryCollectionParseContextStub()
.withCompiler(compiler);
const expected = [ parseScript(script, context) ];
const category: CategoryData = {
category: 'category name',
children: [ script ],
};
// act
const actual = parseCategory(category, compiler).scripts;
const actual = parseCategory(category, context).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
@@ -124,18 +128,44 @@ describe('CategoryParser', () => {
// arrange
const callableScript = ScriptDataStub.createWithCall();
const scripts = [ callableScript, ScriptDataStub.createWithCode() ];
const compiler = new ScriptCompilerStub()
.withCompileAbility(callableScript);
const expected = scripts.map((script) => parseScript(script, compiler));
const category: CategoryData = {
category: 'category name',
children: scripts,
};
const compiler = new ScriptCompilerStub()
.withCompileAbility(callableScript);
const context = new CategoryCollectionParseContextStub()
.withCompiler(compiler);
const expected = scripts.map((script) => parseScript(script, context));
// act
const actual = parseCategory(category, compiler).scripts;
const actual = parseCategory(category, context).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
it('script is created with right context', () => { // test through script validation logic
// arrange
const commentDelimiter = 'should not throw';
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
const parseContext = new CategoryCollectionParseContextStub()
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
const category: CategoryData = {
category: 'category name',
children: [
{
category: 'sub-category',
children: [
ScriptDataStub
.createWithoutCallOrCodes()
.withCode(duplicatedCode),
],
},
],
};
// act
const act = () => parseCategory(category, parseContext).scripts;
// assert
expect(act).to.not.throw();
});
});
it('returns expected subcategories', () => {
// arrange
@@ -147,9 +177,9 @@ describe('CategoryParser', () => {
category: 'category name',
children: expected,
};
const compiler = new ScriptCompilerStub();
const context = new CategoryCollectionParseContextStub();
// act
const actual = parseCategory(category, compiler).subCategories;
const actual = parseCategory(category, context).subCategories;
// assert
expect(actual).to.have.lengthOf(1);
expect(actual[0].name).to.equal(expected[0].category);

View File

@@ -1,325 +0,0 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptCompiler } from '@/application/Parser/Compiler/ScriptCompiler';
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
import { FunctionData, ScriptData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
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: FunctionData[] = [ {
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: FunctionData = {
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: FunctionData[] = [ {
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: FunctionData[] = [ {
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 = ScriptDataStub.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 = ScriptDataStub.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 = ScriptDataStub.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 = 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 ScriptCompiler(createFunctions());
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 ScriptCompiler(createFunctions(existingFunctionName));
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 ScriptCompiler(createFunctions(existingFunctionName));
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);
});
});
});
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: FunctionData = {
name: functionName,
parameters: [],
code: expected.execute,
revertCode: expected.revert,
};
const sut = new ScriptCompiler([func]);
const call: FunctionCallData = { function: functionName };
const script = ScriptDataStub.createWithCall(call);
// act
const actual = sut.compile(script);
// assert
expect(actual).to.deep.equal(expected);
});
it('builds call sequence as expected', () => {
// arrange
const firstFunction: FunctionData = {
name: 'first-function-name',
parameters: [],
code: 'first-function-code',
revertCode: 'first-function-revert-code',
};
const secondFunction: FunctionData = {
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: FunctionCallData[] = [
{ function: firstFunction.name },
{ function: secondFunction.name },
];
const script = ScriptDataStub.createWithCall(call);
// act
const actual = sut.compile(script);
// assert
expect(actual).to.deep.equal(expected);
});
});
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 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);
});
});
});
});
interface ITestCase {
code: string;
parameters?: FunctionCallParametersData;
}
class TestEnvironment {
public readonly sut: IScriptCompiler;
public readonly script: ScriptData;
constructor(testCase: ITestCase) {
const functionName = 'testFunction';
const func: FunctionData = {
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: 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})`;
}
}
function createFunctions(...names: string[]): FunctionData[] {
if (!names || names.length === 0) {
names = ['test-function'];
}
return names.map((functionName) => {
const func: FunctionData = {
name: functionName,
parameters: [],
code: `REM test-code (${functionName})`,
revertCode: `REM test-revert-code (${functionName})`,
};
return func;
});
}

View File

@@ -0,0 +1,82 @@
import 'mocha';
import { expect } from 'chai';
import { ISyntaxFactory } from '@/application/Parser/Script/Syntax/ISyntaxFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
import { CategoryCollectionParseContext } from '@/application/Parser/Script/CategoryCollectionParseContext';
import { ScriptingDefinitionStub } from '../../../stubs/ScriptingDefinitionStub';
import { FunctionDataStub } from '../../../stubs/FunctionDataStub';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
import { FunctionData } from 'js-yaml-loader!*';
describe('CategoryCollectionParseContext', () => {
describe('ctor', () => {
describe('functionsData', () => {
it('can create with empty values', () => {
// arrange
const testData: FunctionData[][] = [ undefined, [] ];
const scripting = new ScriptingDefinitionStub();
for (const functionsData of testData) {
// act
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
// assert
expect(act).to.not.throw();
}
});
});
it('scripting', () => {
// arrange
const expectedError = 'undefined scripting';
const scripting = undefined;
const functionsData = [ new FunctionDataStub() ];
// act
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
// assert
expect(act).to.throw(expectedError);
});
});
describe('compiler', () => {
it('constructed as expected', () => {
// arrange
const functionsData = [ new FunctionDataStub() ];
const syntax = new LanguageSyntaxStub();
const expected = new ScriptCompiler(functionsData, syntax);
const language = ScriptingLanguage.shellscript;
const factoryMock = mockFactory(language, syntax);
const definition = new ScriptingDefinitionStub()
.withLanguage(language);
// act
const sut = new CategoryCollectionParseContext(functionsData, definition, factoryMock);
const actual = sut.compiler;
// assert
expect(actual).to.deep.equal(expected);
});
});
describe('syntax', () => {
it('set from syntax factory', () => {
// arrange
const language = ScriptingLanguage.shellscript;
const expected = new LanguageSyntaxStub();
const factoryMock = mockFactory(language, expected);
const definition = new ScriptingDefinitionStub()
.withLanguage(language);
// act
const sut = new CategoryCollectionParseContext([], definition, factoryMock);
const actual = sut.syntax;
// assert
expect(actual).to.equal(expected);
});
});
});
function mockFactory(expectedLanguage: ScriptingLanguage, result: ILanguageSyntax): ISyntaxFactory {
return {
create: (language: ScriptingLanguage) => {
if (language !== expectedLanguage) {
throw new Error('unexpected language');
}
return result;
},
};
}

View File

@@ -1,6 +1,6 @@
import 'mocha';
import { expect } from 'chai';
import { generateIlCode } from '@/application/Parser/Compiler/ILCode';
import { generateIlCode } from '@/application/Parser/Script/Compiler/ILCode';
describe('ILCode', () => {
describe('getUniqueParameterNames', () => {

View File

@@ -0,0 +1,405 @@
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 { ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
import { LanguageSyntaxStub } from '../../../../stubs/LanguageSyntaxStub';
import { ScriptDataStub } from '../../../../stubs/ScriptDataStub';
import { FunctionDataStub } from '../../../../stubs/FunctionDataStub';
describe('ScriptCompiler', () => {
describe('ctor', () => {
it('throws if syntax is undefined', () => {
// arrange
const expectedError = `undefined syntax`;
// act
const act = () => new ScriptCompilerBuilder()
.withSomeFunctions()
.withSyntax(undefined)
.build();
// 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('returns true if "call" is defined', () => {
// arrange
const sut = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
const script = ScriptDataStub.createWithCall();
// act
const actual = sut.canCompile(script);
// assert
expect(actual).to.equal(true);
});
it('returns false if "call" is undefined', () => {
// arrange
const sut = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
const script = ScriptDataStub.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 sut = new ScriptCompilerBuilder()
.withEmptyFunctions()
.build();
const script = ScriptDataStub.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 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);
});
});
});
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);
// act
const act = () => sut.compile(script);
// assert
expect(act).to.not.throw();
});
it('builds single call as expected', () => {
// arrange
const functionName = 'testSharedFunction';
const expectedExecute = `expected-execute`;
const expectedRevert = `expected-revert`;
const func = new FunctionDataStub()
.withName(functionName)
.withCode(expectedExecute)
.withRevertCode(expectedRevert);
const sut = new ScriptCompilerBuilder()
.withFunctions(func)
.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 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);
});
});
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);
});
}
private functions: FunctionData[];
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
this.functions = functions;
return this;
}
public withSomeFunctions(): ScriptCompilerBuilder {
this.functions = ScriptCompilerBuilder.createFunctions('test-function');
return this;
}
public withFunctionNames(...functionNames: string[]): ScriptCompilerBuilder {
this.functions = ScriptCompilerBuilder.createFunctions(...functionNames);
return this;
}
public withEmptyFunctions(): ScriptCompilerBuilder {
this.functions = [];
return this;
}
public withSyntax(syntax: ILanguageSyntax): ScriptCompilerBuilder {
this.syntax = syntax;
return this;
}
public build(): ScriptCompiler {
if (!this.functions) {
throw new Error('Function behavior not defined');
}
return new ScriptCompiler(this.functions, this.syntax);
}
}

View File

@@ -1,12 +1,15 @@
import 'mocha';
import { expect } from 'chai';
import { parseScript } from '@/application/Parser/ScriptParser';
import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptCode } from '@/domain/ScriptCode';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
import { mockEnumParser } from '../../stubs/EnumParserStub';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
import { mockEnumParser } from '../../../stubs/EnumParserStub';
import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
describe('ScriptParser', () => {
describe('parseScript', () => {
@@ -15,9 +18,9 @@ describe('ScriptParser', () => {
const expected = 'test-expected-name';
const script = ScriptDataStub.createWithCode()
.withName(expected);
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
// act
const actual = parseScript(script, compiler);
const actual = parseScript(script, parseContext);
// assert
expect(actual.name).to.equal(expected);
});
@@ -26,10 +29,10 @@ describe('ScriptParser', () => {
const docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
const script = ScriptDataStub.createWithCode()
.withDocs(docs);
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
const expected = parseDocUrls(script);
// act
const actual = parseScript(script, compiler);
const actual = parseScript(script, parseContext);
// assert
expect(actual.documentationUrls).to.deep.equal(expected);
});
@@ -37,44 +40,44 @@ describe('ScriptParser', () => {
it('throws when script is undefined', () => {
// arrange
const expectedError = 'undefined script';
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
const script = undefined;
// act
const act = () => parseScript(script, compiler);
const act = () => parseScript(script, parseContext);
// 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 parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub
.createWithCall()
.withCode('code');
// act
const act = () => parseScript(script, compiler);
const act = () => parseScript(script, parseContext);
// 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 parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub
.createWithCall()
.withRevertCode('revert-code');
// act
const act = () => parseScript(script, compiler);
const act = () => parseScript(script, parseContext);
// 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 parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub.createWithoutCallOrCodes();
// act
const act = () => parseScript(script, compiler);
const act = () => parseScript(script, parseContext);
// assert
expect(act).to.throw(expectedError);
});
@@ -84,11 +87,11 @@ describe('ScriptParser', () => {
const undefinedLevels: string[] = [ '', undefined ];
undefinedLevels.forEach((undefinedLevel) => {
// arrange
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub.createWithCode()
.withRecommend(undefinedLevel);
// act
const actual = parseScript(script, compiler);
const actual = parseScript(script, parseContext);
// assert
expect(actual.level).to.equal(undefined);
});
@@ -100,10 +103,10 @@ describe('ScriptParser', () => {
const levelText = 'standard';
const script = ScriptDataStub.createWithCode()
.withRecommend(levelText);
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
// act
const actual = parseScript(script, compiler, parserMock);
const actual = parseScript(script, parseContext, parserMock);
// assert
expect(actual.level).to.equal(expectedLevel);
});
@@ -115,9 +118,9 @@ describe('ScriptParser', () => {
const script = ScriptDataStub
.createWithCode()
.withCode(expected);
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
// act
const parsed = parseScript(script, compiler);
const parsed = parseScript(script, parseContext);
// assert
const actual = parsed.code.execute;
expect(actual).to.equal(expected);
@@ -128,36 +131,55 @@ describe('ScriptParser', () => {
const script = ScriptDataStub
.createWithCode()
.withRevertCode(expected);
const compiler = new ScriptCompilerStub();
const parseContext = new CategoryCollectionParseContextStub();
// act
const parsed = parseScript(script, compiler);
const parsed = parseScript(script, parseContext);
// assert
const actual = parsed.code.revert;
expect(actual).to.equal(expected);
});
describe('compiler', () => {
it('throws when compiler is not defined', () => {
it('throws when context is not defined', () => {
// arrange
const expectedMessage = 'undefined context';
const script = ScriptDataStub.createWithCode();
const compiler = undefined;
const context: ICategoryCollectionParseContext = undefined;
// act
const act = () => parseScript(script, compiler);
const act = () => parseScript(script, context);
// assert
expect(act).to.throw('undefined compiler');
expect(act).to.throw(expectedMessage);
});
it('gets code from compiler', () => {
// arrange
const expected = new ScriptCode('test-script', 'code', 'revert-code');
const expected = new ScriptCodeStub();
const script = ScriptDataStub.createWithCode();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script, expected);
const parseContext = new CategoryCollectionParseContextStub()
.withCompiler(compiler);
// act
const parsed = parseScript(script, compiler);
const parsed = parseScript(script, parseContext);
// assert
const actual = parsed.code;
expect(actual).to.equal(expected);
});
});
describe('syntax', () => {
it('set from the context', () => { // test through script validation logic
// arrange
const commentDelimiter = 'should not throw';
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
const parseContext = new CategoryCollectionParseContextStub()
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
const script = ScriptDataStub
.createWithoutCallOrCodes()
.withCode(duplicatedCode);
// act
const act = () => parseScript(script, parseContext);
// assert
expect(act).to.not.throw();
});
});
});
});
});

View File

@@ -0,0 +1,33 @@
import 'mocha';
import { expect } from 'chai';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
function getSystemsUnderTest(): ILanguageSyntax[] {
return [ new BatchFileSyntax(), new ShellScriptSyntax() ];
}
describe('ConcreteSyntaxes', () => {
describe('commentDelimiters', () => {
for (const sut of getSystemsUnderTest()) {
it(`${sut.constructor.name} returns defined value`, () => {
// act
const value = sut.commentDelimiters;
// assert
expect(value);
});
}
});
describe('commonCodeParts', () => {
for (const sut of getSystemsUnderTest()) {
it(`${sut.constructor.name} returns defined value`, () => {
// act
const value = sut.commonCodeParts;
// assert
expect(value);
});
}
});
});

View File

@@ -0,0 +1,38 @@
import 'mocha';
import { expect } from 'chai';
import { SyntaxFactory } from '@/application/Parser/Script/Syntax/SyntaxFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
describe('SyntaxFactory', () => {
describe('getSyntax', () => {
describe('creates expected type', () => {
it('shellscript returns ShellBuilder', () => {
// arrange
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
{ language: ScriptingLanguage.shellscript, expected: ShellScriptSyntax},
{ language: ScriptingLanguage.batchfile, expected: BatchFileSyntax},
];
for (const testCase of testCases) {
it(ScriptingLanguage[testCase.language], () => {
// act
const sut = new SyntaxFactory();
const result = sut.create(testCase.language);
// assert
expect(result).to.be.instanceOf(testCase.expected,
`Actual was: ${result.constructor.name}`);
});
}
});
});
it('throws on unknown scripting language', () => {
// arrange
const sut = new SyntaxFactory();
// act
const act = () => sut.create(3131313131);
// assert
expect(act).to.throw(`unknown language: "${ScriptingLanguage[3131313131]}"`);
});
});
});

View File

@@ -34,7 +34,8 @@ describe('ScriptingDefinitionParser', () => {
const expectedName = 'language';
const info = new ProjectInformationStub();
const definition = new ScriptingDefinitionBuilder()
.withLanguage(languageText).construct();
.withLanguage(languageText)
.construct();
const parserMock = mockEnumParser(expectedName, languageText, expectedLanguage);
// act
const actual = parseScriptingDefinition(definition, info, new Date(), parserMock);

View File

@@ -1,6 +1,5 @@
import 'mocha';
import { expect } from 'chai';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { ICategory } from '@/domain/ICategory';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';

View File

@@ -3,16 +3,15 @@ import 'mocha';
import { expect } from 'chai';
import { Script } from '@/domain/Script';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptCode } from '@/domain/ScriptCode';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCodeStub } from '../stubs/ScriptCodeStub';
describe('Script', () => {
describe('ctor', () => {
describe('scriptCode', () => {
it('sets as expected', () => {
// arrange
const name = 'test-script';
const expected = new ScriptCode(name, 'expected-execute', 'expected-revert');
const expected = new ScriptCodeStub();
const sut = new ScriptBuilder()
.withCode(expected)
.build();
@@ -110,12 +109,14 @@ describe('Script', () => {
class ScriptBuilder {
private name = 'test-script';
private code: IScriptCode = new ScriptCode(this.name, 'code', 'revert-code');
private code: IScriptCode = new ScriptCodeStub();
private level = RecommendationLevel.Standard;
private documentationUrls: readonly string[] = undefined;
public withCodes(code: string, revertCode = ''): ScriptBuilder {
this.code = new ScriptCode(this.name, code, revertCode);
this.code = new ScriptCodeStub()
.withExecute(code)
.withRevert(revertCode);
return this;
}

View File

@@ -2,6 +2,8 @@ import 'mocha';
import { expect } from 'chai';
import { ScriptCode } from '@/domain/ScriptCode';
import { IScriptCode } from '@/domain/IScriptCode';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { LanguageSyntaxStub } from '../stubs/LanguageSyntaxStub';
describe('ScriptCode', () => {
describe('scriptName', () => {
@@ -10,7 +12,9 @@ describe('ScriptCode', () => {
const expectedError = 'name is undefined';
const name = undefined;
// act
const act = () => new ScriptCode(name, 'non-empty-code', '');
const act = () => new ScriptCodeBuilder()
.withName(name)
.build();
// assert
expect(act).to.throw(expectedError);
});
@@ -48,7 +52,11 @@ describe('ScriptCode', () => {
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const act = () => new ScriptCode(scriptName, testCase.code.execute, testCase.code.revert);
const act = () => new ScriptCodeBuilder()
.withName(scriptName)
.withExecute( testCase.code.execute)
.withRevert(testCase.code.revert)
.build();
// assert
expect(act).to.throw(testCase.expectedError);
});
@@ -72,15 +80,21 @@ describe('ScriptCode', () => {
// act
const actions = [];
for (const testCase of testCases) {
const substituteScriptName = (name) => testCase.expectedMessage.replace('$scriptName', name);
const substituteScriptName = (name: string) => testCase.expectedMessage.replace('$scriptName', name);
actions.push(...[
{
act: () => new ScriptCode(scriptName, testCase.code, undefined),
act: () => new ScriptCodeBuilder()
.withName(scriptName)
.withExecute(testCase.code)
.build(),
testName: `execute: ${testCase.testName}`,
expectedMessage: substituteScriptName(scriptName),
},
{
act: () => new ScriptCode(scriptName, 'valid code', testCase.code),
act: () => new ScriptCodeBuilder()
.withName(scriptName)
.withRevert(testCase.code)
.build(),
testName: `revert: ${testCase.testName}`,
expectedMessage: substituteScriptName(`${scriptName} (revert)`),
},
@@ -96,26 +110,29 @@ describe('ScriptCode', () => {
});
describe('sets as expected with valid "execute" or "revert"', () => {
// arrange
const syntax = new LanguageSyntaxStub()
.withCommonCodeParts(')', 'else', '(')
.withCommentDelimiters('#', '//');
const testCases = [
{
testName: 'code is a valid string',
code: 'valid code',
},
{
testName: 'code consists of frequent code parts',
code: ') else (',
testName: 'code consists of common code parts',
code: syntax.commonCodeParts.join(' '),
},
{
testName: 'code is a frequent code part',
code: ')',
testName: 'code is a common code part',
code: syntax.commonCodeParts[0],
},
{
testName: 'code with duplicated comment lines (::)',
code: ':: comment\n:: comment',
testName: `code with duplicated comment lines (${syntax.commentDelimiters[0]})`,
code: `${syntax.commentDelimiters[0]} comment\n${syntax.commentDelimiters[0]} comment`,
},
{
testName: 'code with duplicated comment lines (REM)',
code: 'REM comment\nREM comment',
testName: `code with duplicated comment lines (${syntax.commentDelimiters[1]})`,
code: `${syntax.commentDelimiters[1]} comment\n${syntax.commentDelimiters[1]} comment`,
},
];
// act
@@ -124,12 +141,20 @@ describe('ScriptCode', () => {
actions.push(...[
{
testName: `execute: ${testCase.testName}`,
act: () => createSut(testCase.code),
act: () =>
new ScriptCodeBuilder()
.withSyntax(syntax)
.withExecute(testCase.code)
.build(),
expect: (sut: IScriptCode) => sut.execute === testCase.code,
},
{
testName: `revert: ${testCase.testName}`,
act: () => createSut('different code', testCase.code),
act: () =>
new ScriptCodeBuilder()
.withSyntax(syntax)
.withRevert(testCase.code)
.build(),
expect: (sut: IScriptCode) => sut.revert === testCase.code,
},
]);
@@ -145,6 +170,34 @@ describe('ScriptCode', () => {
});
});
function createSut(code: string, revert = ''): ScriptCode {
return new ScriptCode('test-code', code, revert);
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;
}
public withRevert(revert: string) {
this.revert = revert;
return this;
}
public withSyntax(syntax: ILanguageSyntax) {
this.syntax = syntax;
return this;
}
public build(): ScriptCode {
return new ScriptCode(
this.execute,
this.revert,
this.scriptName,
this.syntax);
}
}

View File

@@ -3,7 +3,6 @@ import { expect } from 'chai';
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { getEnumValues } from '@/application/Common/Enum';
import { OperatingSystem } from '@/domain/OperatingSystem';
describe('ScriptingDefinition', () => {
describe('language', () => {
@@ -38,7 +37,7 @@ describe('ScriptingDefinition', () => {
// arrange
const testCases = new Map<ScriptingLanguage, string>([
[ScriptingLanguage.batchfile, 'bat'],
[ScriptingLanguage.bash, 'sh'],
[ScriptingLanguage.shellscript, 'sh'],
]);
Array.from(testCases.entries()).forEach((test) => {
const language = test[0];
@@ -108,7 +107,7 @@ describe('ScriptingDefinition', () => {
});
class ScriptingDefinitionBuilder {
private language = ScriptingLanguage.bash;
private language = ScriptingLanguage.shellscript;
private startCode = 'REM start-code';
private endCode = 'REM end-code';

View File

@@ -35,10 +35,10 @@ describe('AsyncLazy', () => {
expect(results).to.deep.equal([1, 1, 1, 1, 1]);
});
it('when running long-running task paralelly', async () => {
const sleep = (time: number) => new Promise(((resolve) => setTimeout(resolve, time)));
it('when running long-running task in parallel', async () => {
const sleepAsync = (time: number) => new Promise(((resolve) => setTimeout(resolve, time)));
const sut = new AsyncLazy(async () => {
await sleep(100);
await sleepAsync(100);
totalExecuted++;
return Promise.resolve(totalExecuted);
});

View File

@@ -1,81 +1,87 @@
import { ISignal } from '@/infrastructure/Events/ISignal';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
import { Signal } from '@/infrastructure/Events/Signal';
import { expect } from 'chai';
import { EventHandler } from '@/infrastructure/Events/ISignal';
describe('Signal', () => {
class ReceiverMock {
public onRecieveCalls = new Array<number>();
public onReceive(arg: number): void { this.onRecieveCalls.push(arg); }
class ObserverMock {
public readonly onReceiveCalls = new Array<number>();
public readonly callbacks = new Array<EventHandler<number>>();
public readonly subscription: IEventSubscription;
constructor(subject: ISignal<number>) {
this.callbacks.push((arg) => this.onReceiveCalls.push(arg));
this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg)));
}
}
let signal: Signal<number>;
beforeEach(() => signal = new Signal());
describe('single reciever', () => {
let receiver: ReceiverMock;
describe('single observer', () => {
// arrange
let observer: ObserverMock;
beforeEach(() => {
receiver = new ReceiverMock();
signal.on((arg) => receiver.onReceive(arg));
observer = new ObserverMock(signal);
});
it('notify() executes the callback', () => {
// act
signal.notify(5);
expect(receiver.onRecieveCalls).to.have.length(1);
// assert
expect(observer.onReceiveCalls).to.have.length(1);
});
it('notify() executes the callback with the payload', () => {
const expected = 5;
// act
signal.notify(expected);
expect(receiver.onRecieveCalls).to.deep.equal([expected]);
// assert
expect(observer.onReceiveCalls).to.deep.equal([expected]);
});
it('notify() does not call callback when unsubscribed', () => {
// act
observer.subscription.unsubscribe();
signal.notify(5);
// assert
expect(observer.onReceiveCalls).to.have.lengthOf(0);
});
});
describe('multiple recievers', () => {
let receivers: ReceiverMock[];
describe('multiple observers', () => {
// arrange
let observers: ObserverMock[];
beforeEach(() => {
receivers = [
new ReceiverMock(), new ReceiverMock(),
new ReceiverMock(), new ReceiverMock()];
function subscribeReceiver(receiver: ReceiverMock) {
signal.on((arg) => receiver.onReceive(arg));
}
for (const receiver of receivers) {
subscribeReceiver(receiver);
}
observers = [
new ObserverMock(signal), new ObserverMock(signal),
new ObserverMock(signal), new ObserverMock(signal),
];
});
it('notify() should execute all callbacks', () => {
// act
signal.notify(5);
receivers.forEach((receiver) => {
expect(receiver.onRecieveCalls).to.have.length(1);
// assert
observers.forEach((observer) => {
expect(observer.onReceiveCalls).to.have.length(1);
});
});
it('notify() should execute all callbacks with payload', () => {
const expected = 5;
// act
signal.notify(expected);
receivers.forEach((receiver) => {
expect(receiver.onRecieveCalls).to.deep.equal([expected]);
// assert
observers.forEach((observer) => {
expect(observer.onReceiveCalls).to.deep.equal([expected]);
});
});
it('notify() executes in FIFO order', () => {
// arrange
const expectedSequence = [0, 1, 2, 3];
const actualSequence = new Array<number>();
for (let i = 0; i < receivers.length; i++) {
receivers[i].onReceive = ((arg) => {
actualSequence.push(i);
});
for (let i = 0; i < observers.length; i++) {
observers[i].callbacks.push(() => actualSequence.push(i));
}
// act
signal.notify(5);
// assert
expect(actualSequence).to.deep.equal(expectedSequence);
});
});
});

View File

@@ -0,0 +1,19 @@
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { ScriptCompilerStub } from './ScriptCompilerStub';
import { LanguageSyntaxStub } from './LanguageSyntaxStub';
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
import { ILanguageSyntax } from '@/domain/ScriptCode';
export class CategoryCollectionParseContextStub implements ICategoryCollectionParseContext {
public compiler: IScriptCompiler = new ScriptCompilerStub();
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
public withCompiler(compiler: IScriptCompiler) {
this.compiler = compiler;
return this;
}
public withSyntax(syntax: ILanguageSyntax) {
this.syntax = syntax;
return this;
}
}

View File

@@ -30,6 +30,11 @@ export class CategoryCollectionStub implements ICategoryCollection {
this.initialScript = script;
return this;
}
public withTotalScripts(totalScripts: number) {
this.totalScripts = totalScripts;
return this;
}
public findCategory(categoryId: number): ICategory {
return this.getAllCategories().find(
(category) => category.id === categoryId);

View File

@@ -0,0 +1,25 @@
import { FunctionData } from 'js-yaml-loader!*';
export class FunctionDataStub implements FunctionData {
public name = 'function data stub';
public code = 'function data stub code';
public revertCode = 'function data stub revertCode';
public parameters?: readonly string[];
public withName(name: string) {
this.name = name;
return this;
}
public withParameters(...parameters: string[]) {
this.parameters = parameters;
return this;
}
public withCode(code: string) {
this.code = code;
return this;
}
public withRevertCode(revertCode: string) {
this.revertCode = revertCode;
return this;
}
}

View File

@@ -0,0 +1,15 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
export class LanguageSyntaxStub implements ILanguageSyntax {
public commentDelimiters = [];
public commonCodeParts = [];
public withCommentDelimiters(...delimiters: string[]) {
this.commentDelimiters = delimiters;
return this;
}
public withCommonCodeParts(...codeParts: string[]) {
this.commonCodeParts = codeParts;
return this;
}
}

View File

@@ -0,0 +1,15 @@
import { IScriptCode } from '@/domain/IScriptCode';
export class ScriptCodeStub implements IScriptCode {
public execute = 'default execute code';
public revert = 'default revert code';
public withExecute(code: string) {
this.execute = code;
return this;
}
public withRevert(revert: string) {
this.revert = revert;
return this;
}
}

View File

@@ -1,4 +1,4 @@
import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptData } from 'js-yaml-loader!@/*';

View File

@@ -27,39 +27,30 @@ 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;
}
public withDocs(docs: string[]): ScriptDataStub {
this.docs = docs;
return this;
}
public withCode(code: string): ScriptDataStub {
this.code = code;
return this;
}
public withRevertCode(revertCode: string): ScriptDataStub {
this.revertCode = revertCode;
return this;
}
public withMockCall(): ScriptDataStub {
this.call = { function: 'func', parameters: [] };
return this;
}
public withCall(call: ScriptFunctionCallData): ScriptDataStub {
this.call = call;
return this;
}
public withRecommend(recommend: string): ScriptDataStub {
this.recommend = recommend;
return this;

View File

@@ -15,4 +15,8 @@ export class ScriptingDefinitionStub implements IScriptingDefinition {
this.endCode = endCode;
return this;
}
public withLanguage(language: ScriptingLanguage): ScriptingDefinitionStub {
this.language = language;
return this;
}
}