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

@@ -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 { 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('createEnumParser', () => {
@@ -78,7 +80,7 @@ describe('Enum', () => {
// act
const actual = getEnumNames(TestEnum);
// assert
expect(expected.sort()).to.deep.equal(actual.sort());
expect(scrambledEqual(expected, actual));
});
});
describe('getEnumValues', () => {
@@ -89,7 +91,19 @@ describe('Enum', () => {
// act
const actual = getEnumValues(TestEnum);
// 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 { ApplicationStub } from '../../stubs/ApplicationStub';
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
import { EnumRangeTestRunner } from '../Common/EnumRangeTestRunner';
describe('ApplicationContext', () => {
describe('changeContext', () => {
@@ -180,40 +181,15 @@ describe('ApplicationContext', () => {
expect(actual).to.deep.equal(expected);
});
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
for (const testCase of testCases) {
it(testCase.name, () => {
const act = () =>
new ObservableApplicationContextFactory()
.withInitialOs(testCase.os)
.construct();
// assert
expect(act).to.throw(testCase.expectedError);
});
}
const act = (os: OperatingSystem) => new ObservableApplicationContextFactory()
.withInitialOs(os)
.construct();
// assert
new EnumRangeTestRunner(act)
.testOutOfRangeThrows()
.testUndefinedValueThrows()
.testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
});
});
describe('app', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import 'mocha';
import { expect } from 'chai';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { EnumRangeTestRunner } from '../application/Common/EnumRangeTestRunner';
describe('ProjectInformation', () => {
it('sets name as expected', () => {
@@ -115,14 +116,16 @@ describe('ProjectInformation', () => {
// assert
expect(actual).to.equal(expected);
});
it('throws when OS is unknown', () => {
describe('throws when os is invalid', () => {
// arrange
const sut = new ProjectInformation('name', 'version', 'repositoryUrl', 'homepage');
const os = OperatingSystem.Unknown;
// act
const act = () => sut.getDownloadUrl(os);
const act = (os: OperatingSystem) => sut.getDownloadUrl(os);
// 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 'mocha';
import { expect } from 'chai';
import { runCodeAsync } from '@/infrastructure/CodeRunner';
import { CodeRunner } from '@/infrastructure/CodeRunner';
describe('CodeRunner', () => {
describe('runCodeAsync', () => {
@@ -127,7 +127,8 @@ class TestContext {
private env = mockEnvironment(OperatingSystem.Windows);
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) {
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 { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { FunctionData } from 'js-yaml-loader!*';
@@ -27,15 +28,3 @@ export class FunctionCompilerStub implements IFunctionCompiler {
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();
}
}