refactor extra code, duplicates, complexity

- refactor array equality check and add tests
- remove OperatingSystem.Unknown causing extra logic, return undefined instead
- refactor enum validation to share same logic
- refactor scripting language factories to share same logic
- refactor too many args in runCodeAsync
- refactor ScriptCode constructor to reduce complexity
- fix writing useless write to member object since another property write always override it
This commit is contained in:
undergroundwires
2021-04-11 14:37:02 +01:00
parent 3e9c99f5f8
commit 00d8e551db
37 changed files with 512 additions and 233 deletions

View File

@@ -3,6 +3,14 @@
- It's mainly responsible for - It's mainly responsible for
- creating and event based [application state](#application-state) - creating and event based [application state](#application-state)
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data) - [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
- Consumed by [presentation layer](./presentation.md)
## Structure
- [`/src/` **`application/`**](./../src/application/): Contains all application related code.
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
- [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
- `..`: other classes are categorized using folders-by-feature structure
## Application state ## Application state

View File

@@ -0,0 +1,21 @@
// Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) {
return array.slice().sort();
}
}
// Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) {
return false;
}
return array1.every((val, index) => val === array2[index]);
}

View File

@@ -1,6 +1,6 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
type EnumType = number | string; export type EnumType = number | string;
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue }; export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> { export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
@@ -41,3 +41,14 @@ export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
return getEnumNames(enumVariable) return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[]; .map((level) => enumVariable[level]) as TEnumValue[];
} }
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>) {
if (value === undefined) {
throw new Error('undefined enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}
}

View File

@@ -0,0 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
export interface IScriptingLanguageFactory<T> {
create(language: ScriptingLanguage): T;
}

View File

@@ -0,0 +1,31 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum';
type Getter<T> = () => T;
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('undefined getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
}

View File

@@ -5,6 +5,7 @@ import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
@@ -22,7 +23,7 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem) { initialContext: OperatingSystem) {
validateApp(app); validateApp(app);
validateOs(initialContext); assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app); this.states = initializeStates(app);
this.changeContext(initialContext); this.changeContext(initialContext);
} }
@@ -50,18 +51,6 @@ function validateApp(app: IApplication) {
} }
} }
function validateOs(os: OperatingSystem) {
if (os === undefined) {
throw new Error('undefined os');
}
if (os === OperatingSystem.Unknown) {
throw new Error('unknown os');
}
if (!(os in OperatingSystem)) {
throw new Error(`os "${os}" is out of range`);
}
}
function initializeStates(app: IApplication): StateMachine { function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>(); const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) { for (const collection of app.collections) {

View File

@@ -1,15 +1,14 @@
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
import { BatchBuilder } from './Languages/BatchBuilder'; import { BatchBuilder } from './Languages/BatchBuilder';
import { ShellBuilder } from './Languages/ShellBuilder'; import { ShellBuilder } from './Languages/ShellBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
export class CodeBuilderFactory implements ICodeBuilderFactory { export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
public create(language: ScriptingLanguage): ICodeBuilder { constructor() {
switch (language) { super();
case ScriptingLanguage.shellscript: return new ShellBuilder(); this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
case ScriptingLanguage.batchfile: return new BatchBuilder(); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
} }
} }

View File

@@ -1,6 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ICodeBuilderFactory { export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
create(language: ScriptingLanguage): ICodeBuilder;
} }

View File

@@ -4,17 +4,17 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector { export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors; private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem { public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) { if (!userAgent) {
return OperatingSystem.Unknown; return undefined;
} }
for (const detector of this.detectors) { for (const detector of this.detectors) {
const os = detector.detect(userAgent); const os = detector.detect(userAgent);
if (os !== OperatingSystem.Unknown) { if (os !== undefined) {
return os; return os;
} }
} }
return OperatingSystem.Unknown; return undefined;
} }
} }

View File

@@ -29,10 +29,10 @@ export class DetectorBuilder {
throw new Error('User agent is null or undefined'); throw new Error('User agent is null or undefined');
} }
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) { if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return OperatingSystem.Unknown; return undefined;
} }
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) { if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return OperatingSystem.Unknown; return undefined;
} }
return this.os; return this.os;
} }

View File

@@ -1,5 +1,5 @@
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector { export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem; detect(userAgent: string): OperatingSystem | undefined;
} }

View File

@@ -44,7 +44,7 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
return variables.process.platform; return variables.process.platform;
} }
function getDesktopOsType(processPlatform: string): OperatingSystem { function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
// https://nodejs.org/api/process.html#process_process_platform // https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') { if (processPlatform === 'darwin') {
return OperatingSystem.macOS; return OperatingSystem.macOS;
@@ -53,7 +53,7 @@ function getDesktopOsType(processPlatform: string): OperatingSystem {
} else if (processPlatform === 'linux') { } else if (processPlatform === 'linux') {
return OperatingSystem.Linux; return OperatingSystem.Linux;
} }
return OperatingSystem.Unknown; return undefined;
} }
function isDesktop(variables: IEnvironmentVariables): boolean { function isDesktop(variables: IEnvironmentVariables): boolean {

View File

@@ -1,9 +1,10 @@
import { ISharedFunction } from './ISharedFunction'; import { ISharedFunction } from './ISharedFunction';
export class SharedFunction implements ISharedFunction { export class SharedFunction implements ISharedFunction {
public readonly parameters: readonly string[];
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly parameters: readonly string[], parameters: readonly string[],
public readonly code: string, public readonly code: string,
public readonly revertCode: string, public readonly revertCode: string,
) { ) {

View File

@@ -1,6 +1,5 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ISyntaxFactory { export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
create(language: ScriptingLanguage): ILanguageSyntax;
} }

View File

@@ -1,15 +1,14 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ISyntaxFactory } from './ISyntaxFactory'; import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { BatchFileSyntax } from './BatchFileSyntax'; import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory';
export class SyntaxFactory implements ISyntaxFactory { export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
public create(language: ScriptingLanguage): ILanguageSyntax { constructor() {
switch (language) { super();
case ScriptingLanguage.batchfile: return new BatchFileSyntax(); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
case ScriptingLanguage.shellscript: return new ShellScriptSyntax(); this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
} }
} }

View File

@@ -1,4 +1,4 @@
import { getEnumNames, getEnumValues } from '@/application/Common/Enum'; import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum';
import { IEntity } from '../infrastructure/Entity/IEntity'; import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
@@ -21,7 +21,7 @@ export class CategoryCollection implements ICategoryCollection {
throw new Error('undefined scripting definition'); throw new Error('undefined scripting definition');
} }
this.queryable = makeQueryable(actions); this.queryable = makeQueryable(actions);
ensureValidOs(os); assertInRange(os, OperatingSystem);
ensureValid(this.queryable); ensureValid(this.queryable);
ensureNoDuplicates(this.queryable.allCategories); ensureNoDuplicates(this.queryable.allCategories);
ensureNoDuplicates(this.queryable.allScripts); ensureNoDuplicates(this.queryable.allScripts);
@@ -54,18 +54,6 @@ export class CategoryCollection implements ICategoryCollection {
} }
} }
function ensureValidOs(os: OperatingSystem): void {
if (os === undefined) {
throw new Error('undefined os');
}
if (os === OperatingSystem.Unknown) {
throw new Error('unknown os');
}
if (!(os in OperatingSystem)) {
throw new Error(`os "${os}" is out of range`);
}
}
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) { function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
const totalOccurrencesById = new Map<TKey, number>(); const totalOccurrencesById = new Map<TKey, number>();
for (const entity of entities) { for (const entity of entities) {

View File

@@ -10,5 +10,4 @@ export enum OperatingSystem {
Android, Android,
iOS, iOS,
WindowsPhone, WindowsPhone,
Unknown,
} }

View File

@@ -1,5 +1,6 @@
import { IProjectInformation } from './IProjectInformation'; import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
import { assertInRange } from '@/application/Common/Enum';
export class ProjectInformation implements IProjectInformation { export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string; public readonly repositoryWebUrl: string;
@@ -42,6 +43,7 @@ function getWebUrl(gitUrl: string) {
} }
function getFileName(os: OperatingSystem, version: string): string { function getFileName(os: OperatingSystem, version: string): string {
assertInRange(os, OperatingSystem);
switch (os) { switch (os) {
case OperatingSystem.Linux: case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`; return `privacy.sexy-${version}.AppImage`;
@@ -50,6 +52,6 @@ function getFileName(os: OperatingSystem, version: string): string {
case OperatingSystem.Windows: case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`; return `privacy.sexy-Setup-${version}.exe`;
default: default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`); throw new RangeError(`Unsupported os: ${OperatingSystem[os]}`);
} }
} }

View File

@@ -7,16 +7,7 @@ export class ScriptCode implements IScriptCode {
syntax: ILanguageSyntax) { syntax: ILanguageSyntax) {
if (!syntax) { throw new Error('undefined syntax'); } if (!syntax) { throw new Error('undefined syntax'); }
validateCode(execute, syntax); validateCode(execute, syntax);
if (revert) { validateRevertCode(revert, execute, syntax);
try {
validateCode(revert, syntax);
if (execute === revert) {
throw new Error(`Code itself and its reverting code cannot be the same`);
}
} catch (err) {
throw Error(`(revert): ${err.message}`);
}
}
} }
} }
@@ -25,6 +16,20 @@ export interface ILanguageSyntax {
readonly commonCodeParts: string[]; readonly commonCodeParts: string[];
} }
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
if (!revertCode) {
return;
}
try {
validateCode(revertCode, syntax);
if (execute === revertCode) {
throw new Error(`Code itself and its reverting code cannot be the same`);
}
} catch (err) {
throw Error(`(revert): ${err.message}`);
}
}
function validateCode(code: string, syntax: ILanguageSyntax): void { function validateCode(code: string, syntax: ILanguageSyntax): void {
if (!code || code.length === 0) { if (!code || code.length === 0) {
throw new Error(`code is empty or undefined`); throw new Error(`code is empty or undefined`);

View File

@@ -5,16 +5,20 @@ import fs from 'fs';
import child_process from 'child_process'; import child_process from 'child_process';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export async function runCodeAsync( export class CodeRunner {
code: string, folderName: string, fileExtension: string, constructor(
node = getNodeJs(), environment = Environment.CurrentEnvironment): Promise<void> { private readonly node = getNodeJs(),
const dir = node.path.join(node.os.tmpdir(), folderName); private readonly environment = Environment.CurrentEnvironment) {
await node.fs.promises.mkdir(dir, {recursive: true}); }
const filePath = node.path.join(dir, `run.${fileExtension}`); public async runCodeAsync(code: string, folderName: string, fileExtension: string): Promise<void> {
await node.fs.promises.writeFile(filePath, code); const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await node.fs.promises.chmod(filePath, '755'); await this.node.fs.promises.mkdir(dir, {recursive: true});
const command = getExecuteCommand(filePath, environment); const filePath = this.node.path.join(dir, `run.${fileExtension}`);
node.child_process.exec(command); await this.node.fs.promises.writeFile(filePath, code);
await this.node.fs.promises.chmod(filePath, '755');
const command = getExecuteCommand(filePath, this.environment);
this.node.child_process.exec(command);
}
} }
function getExecuteCommand(scriptPath: string, environment: Environment): string { function getExecuteCommand(scriptPath: string, environment: Environment): string {

View File

@@ -37,7 +37,7 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { runCodeAsync } from '@/infrastructure/CodeRunner'; import { CodeRunner } from '@/infrastructure/CodeRunner';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
@Component({ @Component({
@@ -116,11 +116,12 @@ function buildFileName(scripting: IScriptingDefinition) {
} }
async function executeCodeAsync(context: IApplicationContext) { async function executeCodeAsync(context: IApplicationContext) {
await runCodeAsync( const runner = new CodeRunner();
/*code*/ context.state.code.current, await runner.runCodeAsync(
/*appName*/ context.app.info.name, /*code*/ context.state.code.current,
/*fileExtension*/ context.state.collection.scripting.fileExtension, /*appName*/ context.app.info.name,
); /*fileExtension*/ context.state.collection.scripting.fileExtension,
);
} }
</script> </script>

View File

@@ -24,7 +24,7 @@ import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component @Component
export default class TheOsChanger extends StatefulVue { export default class TheOsChanger extends StatefulVue {
public allOses: Array<{ name: string, os: OperatingSystem }> = []; public allOses: Array<{ name: string, os: OperatingSystem }> = [];
public currentOs: OperatingSystem = OperatingSystem.Unknown; public currentOs?: OperatingSystem = undefined;
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getAppAsync();

View File

@@ -0,0 +1,69 @@
interface IComparerTestCase<T> {
readonly name: string;
readonly first: readonly T[];
readonly second: readonly T[];
readonly expected: boolean;
}
export class ComparerTestScenario {
private readonly testCases: Array<IComparerTestCase<number>> = [];
public addEmptyArrays(expectedResult: boolean) {
return this.addTestCase({
name: 'empty array',
first: [ ],
second: [ ],
expected: expectedResult,
}, true);
}
public addSameItemsWithSameOrder(expectedResult: boolean) {
return this.addTestCase({
name: 'same items with same order',
first: [ 1, 2, 3 ],
second: [ 1, 2, 3 ],
expected: expectedResult,
}, true);
}
public addSameItemsWithDifferentOrder(expectedResult: boolean) {
return this.addTestCase({
name: 'same items with different order',
first: [ 1, 2, 3 ],
second: [ 2, 3, 1 ],
expected: expectedResult,
}, true);
}
public addDifferentItemsWithSameLength(expectedResult: boolean) {
return this.addTestCase({
name: 'different items with same length',
first: [ 1, 2, 3 ],
second: [ 4, 5, 6 ],
expected: expectedResult,
}, true);
}
public addDifferentItemsWithDifferentLength(expectedResult: boolean) {
return this.addTestCase({
name: 'different items with different length',
first: [ 1, 2 ],
second: [ 3, 4, 5 ],
expected: expectedResult,
}, true);
}
public forEachCase(handler: (testCase: IComparerTestCase<number>) => void) {
for (const testCase of this.testCases) {
handler(testCase);
}
}
private addTestCase(testCase: IComparerTestCase<number>, addReversed: boolean) {
this.testCases.push(testCase);
if (addReversed) {
this.testCases.push({
name: `${testCase.name} (reversed)`,
first: testCase.second,
second: testCase.first,
expected: testCase.expected,
});
}
return this;
}
}

View File

@@ -0,0 +1,68 @@
import 'mocha';
import { expect } from 'chai';
import { scrambledEqual } from '@/application/Common/Array';
import { sequenceEqual } from '@/application/Common/Array';
import { ComparerTestScenario } from './Array.ComparerTestScenario';
describe('Array', () => {
describe('scrambledEqual', () => {
describe('throws if arguments are undefined', () => {
it('first argument is undefined', () => {
const expectedError = 'undefined first array';
const act = () => scrambledEqual(undefined, []);
expect(act).to.throw(expectedError);
});
it('second arguments is undefined', () => {
const expectedError = 'undefined second array';
const act = () => scrambledEqual([], undefined);
expect(act).to.throw(expectedError);
});
});
describe('returns as expected', () => {
// arrange
const scenario = new ComparerTestScenario()
.addSameItemsWithSameOrder(true)
.addSameItemsWithDifferentOrder(true)
.addDifferentItemsWithSameLength(false)
.addDifferentItemsWithDifferentLength(false);
// act
scenario.forEachCase((testCase) => {
it(testCase.name, () => {
const actual = scrambledEqual(testCase.first, testCase.second);
// assert
expect(actual).to.equal(testCase.expected);
});
});
});
});
describe('sequenceEqual', () => {
describe('throws if arguments are undefined', () => {
it('first argument is undefined', () => {
const expectedError = 'undefined first array';
const act = () => sequenceEqual(undefined, []);
expect(act).to.throw(expectedError);
});
it('second arguments is undefined', () => {
const expectedError = 'undefined second array';
const act = () => sequenceEqual([], undefined);
expect(act).to.throw(expectedError);
});
});
describe('returns as expected', () => {
// arrange
const scenario = new ComparerTestScenario()
.addSameItemsWithSameOrder(true)
.addSameItemsWithDifferentOrder(true)
.addDifferentItemsWithSameLength(false)
.addDifferentItemsWithDifferentLength(false);
// act
scenario.forEachCase((testCase) => {
it(testCase.name, () => {
const actual = scrambledEqual(testCase.first, testCase.second);
// assert
expect(actual).to.equal(testCase.expected);
});
});
});
});
});

View File

@@ -1,6 +1,8 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { getEnumNames, getEnumValues, createEnumParser } from '@/application/Common/Enum'; import { getEnumNames, getEnumValues, createEnumParser, assertInRange } from '@/application/Common/Enum';
import { EnumRangeTestRunner } from './EnumRangeTestRunner';
import { scrambledEqual } from '@/application/Common/Array';
describe('Enum', () => { describe('Enum', () => {
describe('createEnumParser', () => { describe('createEnumParser', () => {
@@ -78,7 +80,7 @@ describe('Enum', () => {
// act // act
const actual = getEnumNames(TestEnum); const actual = getEnumNames(TestEnum);
// assert // assert
expect(expected.sort()).to.deep.equal(actual.sort()); expect(scrambledEqual(expected, actual));
}); });
}); });
describe('getEnumValues', () => { describe('getEnumValues', () => {
@@ -89,7 +91,19 @@ describe('Enum', () => {
// act // act
const actual = getEnumValues(TestEnum); const actual = getEnumValues(TestEnum);
// assert // assert
expect(expected.sort()).to.deep.equal(actual.sort()); expect(scrambledEqual(expected, actual));
}); });
}); });
describe('assertInRange', () => {
// arrange
enum TestEnum { Red, Green, Blue }
const validValue = TestEnum.Red;
// act
const act = (value: TestEnum) => assertInRange(value, TestEnum);
// assert
new EnumRangeTestRunner(act)
.testOutOfRangeThrows()
.testUndefinedValueThrows()
.testValidValueDoesNotThrow(validValue);
});
}); });

View File

@@ -0,0 +1,54 @@
import 'mocha';
import { expect } from 'chai';
import { EnumType } from '@/application/Common/Enum';
export class EnumRangeTestRunner<TEnumValue extends EnumType> {
constructor(private readonly runner: (value: TEnumValue) => any) {
}
public testOutOfRangeThrows() {
it('throws when value is out of range', () => {
// arrange
const value = Number.MAX_SAFE_INTEGER as TEnumValue;
const expectedError = `enum value "${value}" is out of range`;
// act
const act = () => this.runner(value);
// assert
expect(act).to.throw(expectedError);
});
return this;
}
public testUndefinedValueThrows() {
it('throws when value is undefined', () => {
// arrange
const value = undefined;
const expectedError = 'undefined enum value';
// act
const act = () => this.runner(value);
// assert
expect(act).to.throw(expectedError);
});
return this;
}
public testInvalidValueThrows(invalidValue: TEnumValue, expectedError: string) {
it(`throws ${expectedError}`, () => {
// arrange
const value = invalidValue;
// act
const act = () => this.runner(value);
// assert
expect(act).to.throw(expectedError);
});
return this;
}
public testValidValueDoesNotThrow(validValue: TEnumValue) {
it('throws when value is undefined', () => {
// arrange
const value = validValue;
// act
const act = () => this.runner(value);
// assert
expect(act).to.not.throw();
});
return this;
}
}

View File

@@ -0,0 +1,60 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { ScriptingLanguageFactoryTestRunner } from './ScriptingLanguageFactoryTestRunner';
import { EnumRangeTestRunner } from '../EnumRangeTestRunner';
class ScriptingLanguageConcrete extends ScriptingLanguageFactory<number> {
public registerGetter(language: ScriptingLanguage, getter: () => number) {
super.registerGetter(language, getter);
}
}
describe('ScriptingLanguageFactory', () => {
describe('registerGetter', () => {
describe('validates language', () => {
// arrange
const validValue = ScriptingLanguage.batchfile;
const getter = () => undefined;
const sut = new ScriptingLanguageConcrete();
// act
const act = (language: ScriptingLanguage) => sut.registerGetter(language, getter);
// assert
new EnumRangeTestRunner(act)
.testOutOfRangeThrows()
.testUndefinedValueThrows()
.testValidValueDoesNotThrow(validValue);
});
it('throw when getter is undefined', () => {
// arrange
const expectedError = `undefined getter`;
const language = ScriptingLanguage.batchfile;
const getter = undefined;
const sut = new ScriptingLanguageConcrete();
// act
const act = () => sut.registerGetter(language, getter);
// assert
expect(act).to.throw(expectedError);
});
it('throw when language is already registered', () => {
// arrange
const language = ScriptingLanguage.batchfile;
const expectedError = `${ScriptingLanguage[language]} is already registered`;
const getter = () => undefined;
const sut = new ScriptingLanguageConcrete();
// act
sut.registerGetter(language, getter);
const reRegister = () => sut.registerGetter(language, getter);
// assert
expect(reRegister).to.throw(expectedError);
});
});
describe('create', () => {
const sut = new ScriptingLanguageConcrete();
sut.registerGetter(ScriptingLanguage.batchfile, () => undefined);
const runner = new ScriptingLanguageFactoryTestRunner();
runner.testCreateMethod(sut);
});
});

View File

@@ -0,0 +1,48 @@
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { expect } from 'chai';
import { EnumRangeTestRunner } from '../EnumRangeTestRunner';
export class ScriptingLanguageFactoryTestRunner<T> {
private expectedTypes = new Map<ScriptingLanguage, T>();
public expect(language: ScriptingLanguage, resultType: T) {
this.expectedTypes.set(language, resultType);
return this;
}
public testCreateMethod(sut: IScriptingLanguageFactory<T>) {
if (!sut) { throw new Error('undefined sut'); }
testLanguageValidation(sut);
testExpectedInstanceTypes(sut, this.expectedTypes);
}
}
function testExpectedInstanceTypes<T>(
sut: IScriptingLanguageFactory<T>,
expectedTypes: Map<ScriptingLanguage, T>) {
describe('create returns expected instances', () => {
// arrange
for (const language of Array.from(expectedTypes.keys())) {
it(ScriptingLanguage[language], () => {
// act
const expected = expectedTypes.get(language);
const result = sut.create(language);
// assert
expect(result).to.be.instanceOf(expected, `Actual was: ${result.constructor.name}`);
});
}
});
}
function testLanguageValidation<T>(sut: IScriptingLanguageFactory<T>) {
describe('validates language', () => {
// arrange
const validValue = ScriptingLanguage.batchfile;
// act
const act = (value: ScriptingLanguage) => sut.create(value);
// assert
new EnumRangeTestRunner(act)
.testOutOfRangeThrows()
.testUndefinedValueThrows()
.testValidValueDoesNotThrow(validValue);
});
}

View File

@@ -7,6 +7,7 @@ import { IApplicationContext, IApplicationContextChangedEvent } from '@/applicat
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { ApplicationStub } from '../../stubs/ApplicationStub'; import { ApplicationStub } from '../../stubs/ApplicationStub';
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
import { EnumRangeTestRunner } from '../Common/EnumRangeTestRunner';
describe('ApplicationContext', () => { describe('ApplicationContext', () => {
describe('changeContext', () => { describe('changeContext', () => {
@@ -180,40 +181,15 @@ describe('ApplicationContext', () => {
expect(actual).to.deep.equal(expected); expect(actual).to.deep.equal(expected);
}); });
describe('throws when OS is invalid', () => { describe('throws when OS is invalid', () => {
// arrange
const testCases = [
{
name: 'out of range',
expectedError: 'os "9999" is out of range',
os: 9999,
},
{
name: 'undefined',
expectedError: 'undefined os',
os: undefined,
},
{
name: 'unknown',
expectedError: 'unknown os',
os: OperatingSystem.Unknown,
},
{
name: 'does not exist in application',
expectedError: 'os "Android" is not defined in application',
os: OperatingSystem.Android,
},
];
// act // act
for (const testCase of testCases) { const act = (os: OperatingSystem) => new ObservableApplicationContextFactory()
it(testCase.name, () => { .withInitialOs(os)
const act = () => .construct();
new ObservableApplicationContextFactory() // assert
.withInitialOs(testCase.os) new EnumRangeTestRunner(act)
.construct(); .testOutOfRangeThrows()
// assert .testUndefinedValueThrows()
expect(act).to.throw(testCase.expectedError); .testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
});
}
}); });
}); });
describe('app', () => { describe('app', () => {

View File

@@ -1,36 +1,14 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder'; import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder';
import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder'; import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder';
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
import { ScriptingLanguageFactoryTestRunner } from '../../../../Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner';
describe('CodeBuilderFactory', () => { describe('CodeBuilderFactory', () => {
describe('create', () => { const sut = new CodeBuilderFactory();
describe('creates expected type', () => { const runner = new ScriptingLanguageFactoryTestRunner()
// arrange .expect(ScriptingLanguage.shellscript, ShellBuilder)
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [ .expect(ScriptingLanguage.batchfile, BatchBuilder);
{ language: ScriptingLanguage.shellscript, expected: ShellBuilder}, runner.testCreateMethod(sut);
{ 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

@@ -4,13 +4,14 @@ import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOs
import { BrowserOsTestCases } from './BrowserOsTestCases'; import { BrowserOsTestCases } from './BrowserOsTestCases';
describe('BrowserOsDetector', () => { describe('BrowserOsDetector', () => {
it('unkown when user agent is undefined', () => { it('returns undefined when user agent is undefined', () => {
// arrange // arrange
const expected = undefined;
const sut = new BrowserOsDetector(); const sut = new BrowserOsDetector();
// act // act
const actual = sut.detect(undefined); const actual = sut.detect(undefined);
// assert // assert
expect(actual).to.equal(OperatingSystem.Unknown); expect(actual).to.equal(expected);
}); });
it('detects as expected', () => { it('detects as expected', () => {
for (const testCase of BrowserOsTestCases) { for (const testCase of BrowserOsTestCases) {

View File

@@ -9,7 +9,7 @@ interface IDesktopTestCase {
export const DesktopOsTestCases: ReadonlyArray<IDesktopTestCase> = [ export const DesktopOsTestCases: ReadonlyArray<IDesktopTestCase> = [
{ {
processPlatform: 'aix', processPlatform: 'aix',
expectedOs: OperatingSystem.Unknown, expectedOs: undefined,
}, },
{ {
processPlatform: 'darwin', processPlatform: 'darwin',
@@ -17,7 +17,7 @@ export const DesktopOsTestCases: ReadonlyArray<IDesktopTestCase> = [
}, },
{ {
processPlatform: 'freebsd', processPlatform: 'freebsd',
expectedOs: OperatingSystem.Unknown, expectedOs: undefined,
}, },
{ {
processPlatform: 'linux', processPlatform: 'linux',
@@ -25,11 +25,11 @@ export const DesktopOsTestCases: ReadonlyArray<IDesktopTestCase> = [
}, },
{ {
processPlatform: 'openbsd', processPlatform: 'openbsd',
expectedOs: OperatingSystem.Unknown, expectedOs: undefined,
}, },
{ {
processPlatform: 'sunos', processPlatform: 'sunos',
expectedOs: OperatingSystem.Unknown, expectedOs: undefined,
}, },
{ {
processPlatform: 'win32', processPlatform: 'win32',

View File

@@ -1,38 +1,14 @@
import 'mocha'; import 'mocha';
import { expect } from 'chai';
import { SyntaxFactory } from '@/application/Parser/Script/Syntax/SyntaxFactory'; import { SyntaxFactory } from '@/application/Parser/Script/Syntax/SyntaxFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax'; import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax'; import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
import { ScriptingLanguageFactoryTestRunner } from '../../../Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner';
describe('SyntaxFactory', () => { describe('SyntaxFactory', () => {
describe('getSyntax', () => { const sut = new SyntaxFactory();
describe('creates expected type', () => { const runner = new ScriptingLanguageFactoryTestRunner()
describe('shellscript returns ShellBuilder', () => { .expect(ScriptingLanguage.shellscript, ShellScriptSyntax)
// arrange .expect(ScriptingLanguage.batchfile, BatchFileSyntax);
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [ runner.testCreateMethod(sut);
{ 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

@@ -9,6 +9,7 @@ import { getEnumValues } from '@/application/Common/Enum';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
import { ScriptStub } from '../stubs/ScriptStub'; import { ScriptStub } from '../stubs/ScriptStub';
import { CategoryStub } from '../stubs/CategoryStub'; import { CategoryStub } from '../stubs/CategoryStub';
import { EnumRangeTestRunner } from '../application/Common/EnumRangeTestRunner';
describe('CategoryCollection', () => { describe('CategoryCollection', () => {
describe('getScriptsByLevel', () => { describe('getScriptsByLevel', () => {
@@ -186,35 +187,15 @@ describe('CategoryCollection', () => {
// assert // assert
expect(sut.os).to.deep.equal(expected); expect(sut.os).to.deep.equal(expected);
}); });
it('cannot construct with unknown os', () => { describe('throws when invalid', () => {
// arrange
const os = OperatingSystem.Unknown;
// act // act
const construct = () => new CategoryCollectionBuilder() const act = (os: OperatingSystem) => new CategoryCollectionBuilder()
.withOs(os) .withOs(os)
.construct(); .construct();
// assert // assert
expect(construct).to.throw('unknown os'); new EnumRangeTestRunner(act)
}); .testOutOfRangeThrows()
it('cannot construct with undefined os', () => { .testUndefinedValueThrows();
// arrange
const os = undefined;
// act
const construct = () => new CategoryCollectionBuilder()
.withOs(os)
.construct();
// assert
expect(construct).to.throw('undefined os');
});
it('cannot construct with OS not in range', () => {
// arrange
const os: OperatingSystem = 666;
// act
const construct = () => new CategoryCollectionBuilder()
.withOs(os)
.construct();
// assert
expect(construct).to.throw(`os "${os}" is out of range`);
}); });
}); });
describe('scriptingDefinition', () => { describe('scriptingDefinition', () => {

View File

@@ -2,6 +2,7 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { ProjectInformation } from '@/domain/ProjectInformation'; import { ProjectInformation } from '@/domain/ProjectInformation';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { EnumRangeTestRunner } from '../application/Common/EnumRangeTestRunner';
describe('ProjectInformation', () => { describe('ProjectInformation', () => {
it('sets name as expected', () => { it('sets name as expected', () => {
@@ -115,14 +116,16 @@ describe('ProjectInformation', () => {
// assert // assert
expect(actual).to.equal(expected); expect(actual).to.equal(expected);
}); });
it('throws when OS is unknown', () => { describe('throws when os is invalid', () => {
// arrange // arrange
const sut = new ProjectInformation('name', 'version', 'repositoryUrl', 'homepage'); const sut = new ProjectInformation('name', 'version', 'repositoryUrl', 'homepage');
const os = OperatingSystem.Unknown;
// act // act
const act = () => sut.getDownloadUrl(os); const act = (os: OperatingSystem) => sut.getDownloadUrl(os);
// assert // assert
expect(act).to.throw(`Unsupported os: ${OperatingSystem[os]}`); new EnumRangeTestRunner(act)
.testOutOfRangeThrows()
.testUndefinedValueThrows()
.testInvalidValueThrows(OperatingSystem.KaiOS, `Unsupported os: ${OperatingSystem[OperatingSystem.KaiOS]}`);
}); });
}); });
}); });

View File

@@ -2,7 +2,7 @@ import { EnvironmentStub } from './../stubs/EnvironmentStub';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { runCodeAsync } from '@/infrastructure/CodeRunner'; import { CodeRunner } from '@/infrastructure/CodeRunner';
describe('CodeRunner', () => { describe('CodeRunner', () => {
describe('runCodeAsync', () => { describe('runCodeAsync', () => {
@@ -127,7 +127,8 @@ class TestContext {
private env = mockEnvironment(OperatingSystem.Windows); private env = mockEnvironment(OperatingSystem.Windows);
public async runCodeAsync(): Promise<void> { public async runCodeAsync(): Promise<void> {
await runCodeAsync(this.code, this.folderName, this.fileExtension, this.mocks, this.env); const runner = new CodeRunner(this.mocks, this.env);
await runner.runCodeAsync(this.code, this.folderName, this.fileExtension);
} }
public withOs(os: OperatingSystem) { public withOs(os: OperatingSystem) {
this.env = mockEnvironment(os); this.env = mockEnvironment(os);

View File

@@ -1,3 +1,4 @@
import { sequenceEqual } from '@/application/Common/Array';
import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler'; import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { FunctionData } from 'js-yaml-loader!*'; import { FunctionData } from 'js-yaml-loader!*';
@@ -27,15 +28,3 @@ export class FunctionCompilerStub implements IFunctionCompiler {
return undefined; return undefined;
} }
} }
function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (array1.length !== array2.length) {
return false;
}
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sortedArray1.every((val, index) => val === sortedArray2[index]);
function sort(array: readonly T[]) {
return array.slice().sort();
}
}