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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
21
src/application/Common/Array.ts
Normal file
21
src/application/Common/Array.ts
Normal 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]);
|
||||||
|
}
|
||||||
@@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface IScriptingLanguageFactory<T> {
|
||||||
|
create(language: ScriptingLanguage): T;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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]}"`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]}"`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ export enum OperatingSystem {
|
|||||||
Android,
|
Android,
|
||||||
iOS,
|
iOS,
|
||||||
WindowsPhone,
|
WindowsPhone,
|
||||||
Unknown,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
69
tests/unit/application/Common/Array.ComparerTestScenario.ts
Normal file
69
tests/unit/application/Common/Array.ComparerTestScenario.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
tests/unit/application/Common/Array.spec.ts
Normal file
68
tests/unit/application/Common/Array.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
54
tests/unit/application/Common/EnumRangeTestRunner.ts
Normal file
54
tests/unit/application/Common/EnumRangeTestRunner.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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]}"`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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]}"`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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]}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user