From cf40097b794febebe53b5f32d1fdcef8e03a52e8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Stephan=20M=C3=BCller?= Date: Wed, 6 May 2020 10:08:41 +0200 Subject: [PATCH] mgr/dashboard: Provide shareable mocks in unit test helper MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Provides a view first mocks: * Crush map (static and dynamic) * Crush node * Crush rule * Crush rule config Fixes: https://tracker.ceph.com/issues/44620 Signed-off-by: Stephan Müller --- .../crush-rule-form-modal.component.spec.ts | 52 +----- ...-code-profile-form-modal.component.spec.ts | 58 +++--- .../crush.node.selection.class.spec.ts | 73 +------- .../frontend/src/testing/unit-test-helper.ts | 167 ++++++++++++++++++ 4 files changed, 202 insertions(+), 148 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts index 003ec29bef7b2..ca2f744ff9bd8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts @@ -11,7 +11,8 @@ import { configureTestBed, FixtureHelper, FormHelper, - i18nProviders + i18nProviders, + Mocks } from '../../../../testing/unit-test-helper'; import { CrushRuleService } from '../../../shared/api/crush-rule.service'; import { CrushNode } from '../../../shared/models/crush-node'; @@ -28,31 +29,6 @@ describe('CrushRuleFormComponent', () => { let fixtureHelper: FixtureHelper; let data: { names: string[]; nodes: CrushNode[] }; - // Object contains mock functions - const mock = { - node: ( - name: string, - id: number, - type: string, - type_id: number, - children?: number[], - device_class?: string - ): CrushNode => { - return { name, type, type_id, id, children, device_class }; - }, - rule: ( - name: string, - root: string, - failure_domain: string, - device_class?: string - ): CrushRuleConfig => ({ - name, - root, - failure_domain, - device_class - }) - }; - // Object contains functions to get something const get = { nodeByName: (name: string): CrushNode => data.nodes.find((node) => node.name === name), @@ -125,25 +101,7 @@ describe('CrushRuleFormComponent', () => { * ----> ssd-rack * ------> 2x osd-rack with ssd */ - nodes: [ - // Root node - mock.node('default', -1, 'root', 11, [-2, -3]), - // SSD host - mock.node('ssd-host', -2, 'host', 1, [1, 0, 2]), - mock.node('osd.0', 0, 'osd', 0, undefined, 'ssd'), - mock.node('osd.1', 1, 'osd', 0, undefined, 'ssd'), - mock.node('osd.2', 2, 'osd', 0, undefined, 'ssd'), - // SSD and HDD mixed devices host - mock.node('mix-host', -3, 'host', 1, [-4, -5]), - // HDD rack - mock.node('hdd-rack', -4, 'rack', 3, [3, 4]), - mock.node('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'), - mock.node('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'), - // SSD rack - mock.node('ssd-rack', -5, 'rack', 3, [5, 6]), - mock.node('osd2.0', 5, 'osd-rack', 0, undefined, 'ssd'), - mock.node('osd2.1', 6, 'osd-rack', 0, undefined, 'ssd') - ] + nodes: Mocks.getCrushMap() }; spyOn(crushRuleService, 'getInfo').and.callFake(() => of(data)); fixture.detectChanges(); @@ -254,12 +212,12 @@ describe('CrushRuleFormComponent', () => { }); it('creates a rule with only required fields', () => { - assert.creation(mock.rule('default-rule', 'default', 'osd-rack')); + assert.creation(Mocks.getCrushRuleConfig('default-rule', 'default', 'osd-rack')); }); it('creates a rule with all fields', () => { assert.valuesOnRootChange('ssd-host', 'osd', 'ssd'); - assert.creation(mock.rule('ssd-host-rule', 'ssd-host', 'osd', 'ssd')); + assert.creation(Mocks.getCrushRuleConfig('ssd-host-rule', 'ssd-host', 'osd', 'ssd')); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts index 2628f1f69a428..d404bc63d2a31 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts @@ -12,10 +12,10 @@ import { configureTestBed, FixtureHelper, FormHelper, - i18nProviders + i18nProviders, + Mocks } from '../../../../testing/unit-test-helper'; import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service'; -import { CrushNode } from '../../../shared/models/crush-node'; import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; import { PoolModule } from '../pool.module'; @@ -29,20 +29,6 @@ describe('ErasureCodeProfileFormModalComponent', () => { let fixtureHelper: FixtureHelper; let data: {}; - // Object contains mock functions - const mock = { - node: ( - name: string, - id: number, - type: string, - type_id: number, - children?: number[], - device_class?: string - ): CrushNode => { - return { name, type, type_id, id, children, device_class }; - } - }; - configureTestBed({ imports: [ HttpClientTestingModule, @@ -70,34 +56,34 @@ describe('ErasureCodeProfileFormModalComponent', () => { * ----> 3x osd with ssd * --> mix-host * ----> hdd-rack - * ------> 2x osd-rack with hdd + * ------> 5x osd-rack with hdd * ----> ssd-rack - * ------> 2x osd-rack with ssd + * ------> 5x osd-rack with ssd */ nodes: [ // Root node - mock.node('default', -1, 'root', 11, [-2, -3]), + Mocks.getCrushNode('default', -1, 'root', 11, [-2, -3]), // SSD host - mock.node('ssd-host', -2, 'host', 1, [1, 0, 2]), - mock.node('osd.0', 0, 'osd', 0, undefined, 'ssd'), - mock.node('osd.1', 1, 'osd', 0, undefined, 'ssd'), - mock.node('osd.2', 2, 'osd', 0, undefined, 'ssd'), + Mocks.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]), + Mocks.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'), + Mocks.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'), + Mocks.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'), // SSD and HDD mixed devices host - mock.node('mix-host', -3, 'host', 1, [-4, -5]), + Mocks.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]), // HDD rack - mock.node('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]), - mock.node('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'), - mock.node('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'), - mock.node('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'), - mock.node('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'), - mock.node('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'), + Mocks.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]), + Mocks.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'), + Mocks.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'), + Mocks.getCrushNode('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'), + Mocks.getCrushNode('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'), + Mocks.getCrushNode('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'), // SSD rack - mock.node('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]), - mock.node('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'), - mock.node('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'), - mock.node('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'), - mock.node('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'), - mock.node('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd') + Mocks.getCrushNode('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]), + Mocks.getCrushNode('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'), + Mocks.getCrushNode('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'), + Mocks.getCrushNode('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'), + Mocks.getCrushNode('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'), + Mocks.getCrushNode('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd') ] }; spyOn(ecpService, 'getInfo').and.callFake(() => of(data)); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts index f9a675b48bf48..59a26c5bd3601 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts @@ -1,79 +1,22 @@ import { FormControl } from '@angular/forms'; -import { configureTestBed } from '../../../testing/unit-test-helper'; +import { configureTestBed, Mocks } from '../../../testing/unit-test-helper'; import { CrushNode } from '../models/crush-node'; -import { CrushRuleConfig } from '../models/crush-rule'; import { CrushNodeSelectionClass } from './crush.node.selection.class'; describe('CrushNodeSelectionService', () => { - let service: CrushNodeSelectionClass; + const nodes = Mocks.getCrushNodes() + let service: CrushNodeSelectionClass; let controls: { root: FormControl; failure: FormControl; device: FormControl; }; - // Object contains mock functions - const mock = { - node: ( - name: string, - id: number, - type: string, - type_id: number, - children?: number[], - device_class?: string - ): CrushNode => { - return { name, type, type_id, id, children, device_class }; - }, - rule: ( - name: string, - root: string, - failure_domain: string, - device_class?: string - ): CrushRuleConfig => ({ - name, - root, - failure_domain, - device_class - }), - nodes: [] as CrushNode[] - }; - - /** - * Create the following test crush map: - * > default - * --> ssd-host - * ----> 3x osd with ssd - * --> mix-host - * ----> hdd-rack - * ------> 2x osd-rack with hdd - * ----> ssd-rack - * ------> 2x osd-rack with ssd - */ - mock.nodes = [ - // Root node - mock.node('default', -1, 'root', 11, [-2, -3]), - // SSD host - mock.node('ssd-host', -2, 'host', 1, [1, 0, 2]), - mock.node('osd.0', 0, 'osd', 0, undefined, 'ssd'), - mock.node('osd.1', 1, 'osd', 0, undefined, 'ssd'), - mock.node('osd.2', 2, 'osd', 0, undefined, 'ssd'), - // SSD and HDD mixed devices host - mock.node('mix-host', -3, 'host', 1, [-4, -5]), - // HDD rack - mock.node('hdd-rack', -4, 'rack', 3, [3, 4]), - mock.node('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'), - mock.node('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'), - // SSD rack - mock.node('ssd-rack', -5, 'rack', 3, [5, 6]), - mock.node('osd2.0', 5, 'osd-rack', 0, undefined, 'ssd'), - mock.node('osd2.1', 6, 'osd-rack', 0, undefined, 'ssd') - ]; - // Object contains functions to get something const get = { - nodeByName: (name: string): CrushNode => mock.nodes.find((node) => node.name === name), + nodeByName: (name: string): CrushNode => nodes.find((node) => node.name === name), nodesByNames: (names: string[]): CrushNode[] => names.map(get.nodeByName) }; @@ -117,12 +60,12 @@ describe('CrushNodeSelectionService', () => { // Normally this should be extended by the class using it service = new CrushNodeSelectionClass(); // Therefore to get it working correctly use "this" instead of "service" - service.initCrushNodeSelection(mock.nodes, controls.root, controls.failure, controls.device); + service.initCrushNodeSelection(nodes, controls.root, controls.failure, controls.device); }); it('should be created', () => { expect(service).toBeTruthy(); - expect(mock.nodes.length).toBe(12); + expect(nodes.length).toBe(12); }); describe('lists', () => { @@ -134,7 +77,7 @@ describe('CrushNodeSelectionService', () => { }); it('has the following lists after init', () => { - assert.failureDomains(mock.nodes, ['host', 'osd', 'osd-rack', 'rack']); // Not root as root only exist once + assert.failureDomains(nodes, ['host', 'osd', 'osd-rack', 'rack']); // Not root as root only exist once expect(service.devices).toEqual(['hdd', 'ssd']); }); @@ -148,7 +91,7 @@ describe('CrushNodeSelectionService', () => { controls.root.setValue(get.nodeByName('mix-host')); expect(service.devices).toEqual(['hdd', 'ssd']); assert.failureDomains( - get.nodesByNames(['hdd-rack', 'ssd-rack', 'osd2.0', 'osd2.1', 'osd2.0', 'osd2.1']), + get.nodesByNames(['hdd-rack', 'ssd-rack', 'osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']), ['osd-rack', 'rack'] ); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts b/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts index fa8a326bbbe7d..81047f9063778 100644 --- a/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts +++ b/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts @@ -12,6 +12,8 @@ import { Icons } from '../app/shared/enum/icons.enum'; import { CdFormGroup } from '../app/shared/forms/cd-form-group'; import { CdTableAction } from '../app/shared/models/cd-table-action'; import { CdTableSelection } from '../app/shared/models/cd-table-selection'; +import { CrushNode } from '../app/shared/models/crush-node'; +import { CrushRule, CrushRuleConfig } from '../app/shared/models/crush-rule'; import { Permission } from '../app/shared/models/permissions'; import { AlertmanagerAlert, @@ -375,3 +377,168 @@ export class IscsiHelper { formHelper.expectErrorChange(fieldName, 'thisPasswordIsWayTooBig', 'pattern'); } } + +export class Mocks { + static getCrushNode( + name: string, + id: number, + type: string, + type_id: number, + children?: number[], + device_class?: string + ): CrushNode { + return { name, type, type_id, id, children, device_class }; + } + + /** + * Create the following test crush map: + * > default + * --> ssd-host + * ----> 3x osd with ssd + * --> mix-host + * ----> hdd-rack + * ------> 2x osd-rack with hdd + * ----> ssd-rack + * ------> 2x osd-rack with ssd + */ + static getCrushMap(): CrushNode[] { + return [ + // Root node + this.getCrushNode('default', -1, 'root', 11, [-2, -3]), + // SSD host + this.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]), + this.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'), + this.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'), + this.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'), + // SSD and HDD mixed devices host + this.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]), + // HDD rack + this.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4]), + this.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'), + this.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'), + // SSD rack + this.getCrushNode('ssd-rack', -5, 'rack', 3, [5, 6]), + this.getCrushNode('osd3.0', 5, 'osd-rack', 0, undefined, 'ssd'), + this.getCrushNode('osd3.1', 6, 'osd-rack', 0, undefined, 'ssd') + ]; + } + + /** + * Generates an simple crush map with multiple hosts that have OSDs with either ssd or hdd OSDs. + * Hosts with zero or even numbers at the end have SSD OSDs the other hosts have hdd OSDs. + * + * Host names follow the following naming convention: + * host.$index + * $index represents a number count started at 0 (like an index within an array) (same for OSDs) + * + * OSD names follow the following naming convention: + * osd.$hostIndex.$osdIndex + * + * The following crush map will be generated with the set defaults: + * > default + * --> host.0 (has only ssd OSDs) + * ----> osd.0.0 + * ----> osd.0.1 + * ----> osd.0.2 + * ----> osd.0.3 + * --> host.1 (has only hdd OSDs) + * ----> osd.1.0 + * ----> osd.1.1 + * ----> osd.1.2 + * ----> osd.1.3 + */ + static generateSimpleCrushMap(hosts: number = 2, osds: number = 4): CrushNode[] { + const nodes = []; + const createOsdLeafs = (hostSuffix: number): number[] => { + let osdId = 0; + const osdIds = []; + const osdsInUse = hostSuffix * osds; + for (let o = 0; o < osds; o++) { + osdIds.push(osdId); + nodes.push( + this.getCrushNode( + `osd.${hostSuffix}.${osdId}`, + osdId + osdsInUse, + 'osd', + 0, + undefined, + hostSuffix % 2 === 0 ? 'ssd' : 'hdd' + ) + ); + osdId++; + } + return osdIds; + }; + const createHostBuckets = (): number[] => { + let hostId = -2; + const hostIds = []; + for (let h = 0; h < hosts; h++) { + const hostSuffix = hostId * -1 - 2; + hostIds.push(hostId); + nodes.push( + this.getCrushNode(`host.${hostSuffix}`, hostId, 'host', 1, createOsdLeafs(hostSuffix)) + ); + hostId--; + } + return hostIds; + }; + nodes.push(this.getCrushNode('default', -1, 'root', 11, createHostBuckets())); + return nodes; + } + + static getCrushRuleConfig( + name: string, + root: string, + failure_domain: string, + device_class?: string + ): CrushRuleConfig { + return { + name, + root, + failure_domain, + device_class + }; + } + + static getCrushRule({ + id = 0, + name = 'somePoolName', + min = 1, + max = 10, + type = 'replicated', + failureDomain = 'osd', + itemName = 'default' // This string also sets the device type - "default~ssd" <- ssd usage only + }: { + max?: number; + min?: number; + id?: number; + name?: string; + type?: string; + failureDomain?: string; + itemName?: string; + }): CrushRule { + const typeNumber = type === 'erasure' ? 3 : 1; + const rule = new CrushRule(); + rule.max_size = max; + rule.min_size = min; + rule.rule_id = id; + rule.ruleset = typeNumber; + rule.rule_name = name; + rule.steps = [ + { + item_name: itemName, + item: -1, + op: 'take' + }, + { + num: 0, + type: failureDomain, + op: 'choose_firstn' + }, + { + op: 'emit' + } + ]; + return rule; + } +} -- 2.47.3