1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { RouterTestingModule } from '@angular/router/testing';
5 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
6 import { ToastrModule } from 'ngx-toastr';
7 import { of } from 'rxjs';
9 import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
10 import { CrushNode } from '~/app/shared/models/crush-node';
11 import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
12 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
13 import { configureTestBed, FixtureHelper, FormHelper, Mocks } from '~/testing/unit-test-helper';
14 import { PoolModule } from '../pool.module';
15 import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form-modal.component';
17 describe('ErasureCodeProfileFormModalComponent', () => {
18 let component: ErasureCodeProfileFormModalComponent;
19 let ecpService: ErasureCodeProfileService;
20 let fixture: ComponentFixture<ErasureCodeProfileFormModalComponent>;
21 let formHelper: FormHelper;
22 let fixtureHelper: FixtureHelper;
23 let data: { plugins: string[]; names: string[]; nodes: CrushNode[] };
25 const expectTechnique = (current: string) =>
26 expect(component.form.getValue('technique')).toBe(current);
28 const expectTechniques = (techniques: string[], current: string) => {
29 expect(component.techniques).toEqual(techniques);
30 expectTechnique(current);
33 const expectRequiredControls = (controlNames: string[]) => {
34 controlNames.forEach((name) => {
35 const value = component.form.getValue(name);
36 formHelper.expectValid(name);
37 formHelper.expectErrorChange(name, null, 'required');
38 // This way other fields won't fail through getting invalid.
39 formHelper.expectValidChange(name, value);
41 fixtureHelper.expectIdElementsVisible(controlNames, true);
45 imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), PoolModule],
46 providers: [ErasureCodeProfileService, NgbActiveModal]
50 fixture = TestBed.createComponent(ErasureCodeProfileFormModalComponent);
51 fixtureHelper = new FixtureHelper(fixture);
52 component = fixture.componentInstance;
53 formHelper = new FormHelper(component.form);
54 ecpService = TestBed.inject(ErasureCodeProfileService);
56 plugins: ['isa', 'jerasure', 'shec', 'lrc'],
57 names: ['ecp1', 'ecp2'],
59 * Create the following test crush map:
62 * ----> 3x osd with ssd
65 * ------> 5x osd-rack with hdd
67 * ------> 5x osd-rack with ssd
71 Mocks.getCrushNode('default', -1, 'root', 11, [-2, -3]),
73 Mocks.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]),
74 Mocks.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'),
75 Mocks.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'),
76 Mocks.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'),
77 // SSD and HDD mixed devices host
78 Mocks.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]),
80 Mocks.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]),
81 Mocks.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
82 Mocks.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
83 Mocks.getCrushNode('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'),
84 Mocks.getCrushNode('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'),
85 Mocks.getCrushNode('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'),
87 Mocks.getCrushNode('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]),
88 Mocks.getCrushNode('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'),
89 Mocks.getCrushNode('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'),
90 Mocks.getCrushNode('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'),
91 Mocks.getCrushNode('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'),
92 Mocks.getCrushNode('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd')
95 spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
96 fixture.detectChanges();
99 it('should create', () => {
100 expect(component).toBeTruthy();
103 it('calls listing to get ecps on ngInit', () => {
104 expect(ecpService.getInfo).toHaveBeenCalled();
105 expect(component.names.length).toBe(2);
108 describe('form validation', () => {
109 it(`isn't valid if name is not set`, () => {
110 expect(component.form.invalid).toBeTruthy();
111 formHelper.setValue('name', 'someProfileName');
112 expect(component.form.valid).toBeTruthy();
115 it('sets name invalid', () => {
116 component.names = ['awesomeProfileName'];
117 formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
118 formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
119 formHelper.expectErrorChange('name', null, 'required');
122 it('sets k to min error', () => {
123 formHelper.expectErrorChange('k', 1, 'min');
126 it('sets m to min error', () => {
127 formHelper.expectErrorChange('m', 0, 'min');
130 it(`should show all default form controls`, () => {
131 const showDefaults = (plugin: string) => {
132 formHelper.setValue('plugin', plugin);
133 fixtureHelper.expectIdElementsVisible(
139 'crushFailureDomain',
147 showDefaults('jerasure');
148 showDefaults('shec');
153 it('should change technique to default if not available in other plugin', () => {
154 expectTechnique('reed_sol_van');
155 formHelper.setValue('technique', 'blaum_roth');
156 expectTechnique('blaum_roth');
157 formHelper.setValue('plugin', 'isa');
158 expectTechnique('reed_sol_van');
159 formHelper.setValue('plugin', 'clay');
160 formHelper.expectValidChange('scalar_mds', 'shec');
161 expectTechnique('single');
164 describe(`for 'jerasure' plugin (default)`, () => {
165 it(`requires 'm' and 'k'`, () => {
166 expectRequiredControls(['k', 'm']);
169 it(`should show 'packetSize' and 'technique'`, () => {
170 fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
173 it('should show available techniques', () => {
188 it(`should not show any other plugin specific form control`, () => {
189 fixtureHelper.expectIdElementsVisible(
190 ['c', 'l', 'crushLocality', 'd', 'scalar_mds'],
195 it('should not allow "k" to be changed more than possible', () => {
196 formHelper.expectErrorChange('k', 10, 'max');
199 it('should not allow "m" to be changed more than possible', () => {
200 formHelper.expectErrorChange('m', 10, 'max');
204 describe(`for 'isa' plugin`, () => {
206 formHelper.setValue('plugin', 'isa');
209 it(`does require 'm' and 'k'`, () => {
210 expectRequiredControls(['k', 'm']);
213 it(`should show 'technique'`, () => {
214 fixtureHelper.expectIdElementsVisible(['technique'], true);
217 it('should show available techniques', () => {
218 expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
221 it(`should not show any other plugin specific form control`, () => {
222 fixtureHelper.expectIdElementsVisible(
223 ['c', 'l', 'crushLocality', 'packetSize', 'd', 'scalar_mds'],
228 it('should not allow "k" to be changed more than possible', () => {
229 formHelper.expectErrorChange('k', 10, 'max');
232 it('should not allow "m" to be changed more than possible', () => {
233 formHelper.expectErrorChange('m', 10, 'max');
237 describe(`for 'lrc' plugin`, () => {
239 formHelper.setValue('plugin', 'lrc');
240 formHelper.expectValid('k');
241 formHelper.expectValid('l');
242 formHelper.expectValid('m');
245 it(`requires 'm', 'l' and 'k'`, () => {
246 expectRequiredControls(['k', 'm', 'l']);
249 it(`should show 'l' and 'crushLocality'`, () => {
250 fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
253 it(`should not show any other plugin specific form control`, () => {
254 fixtureHelper.expectIdElementsVisible(
255 ['c', 'packetSize', 'technique', 'd', 'scalar_mds'],
260 it('should not allow "k" to be changed more than possible', () => {
261 formHelper.expectErrorChange('k', 10, 'max');
264 it('should not allow "m" to be changed more than possible', () => {
265 formHelper.expectErrorChange('m', 10, 'max');
268 it('should not allow "l" to be changed so that (k+m) is not a multiple of "l"', () => {
269 formHelper.expectErrorChange('l', 4, 'unequal');
272 it('should update validity of k and l on m change', () => {
273 formHelper.expectValidChange('m', 3);
274 formHelper.expectError('k', 'unequal');
275 formHelper.expectError('l', 'unequal');
278 describe('lrc calculation', () => {
279 const expectCorrectCalculation = (
283 failedControl: string[] = []
285 formHelper.setValue('k', k);
286 formHelper.setValue('m', m);
287 formHelper.setValue('l', l);
288 ['k', 'l'].forEach((name) => {
289 if (failedControl.includes(name)) {
290 formHelper.expectError(name, 'unequal');
292 formHelper.expectValid(name);
339 it('tests all cases where k fails', () => {
340 tests.kFails.forEach((testCase) => {
341 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
345 it('tests all cases where l fails', () => {
346 tests.lFails.forEach((testCase) => {
347 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
351 it('tests all cases where everything is valid', () => {
352 tests.success.forEach((testCase) => {
353 expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
359 describe(`for 'shec' plugin`, () => {
361 formHelper.setValue('plugin', 'shec');
362 formHelper.expectValid('c');
363 formHelper.expectValid('m');
364 formHelper.expectValid('k');
367 it(`does require 'm', 'c' and 'k'`, () => {
368 expectRequiredControls(['k', 'm', 'c']);
371 it(`should not show any other plugin specific form control`, () => {
372 fixtureHelper.expectIdElementsVisible(
373 ['l', 'crushLocality', 'packetSize', 'technique', 'd', 'scalar_mds'],
378 it('should make sure that k has to be equal or greater than m', () => {
379 formHelper.expectValidChange('k', 3);
380 formHelper.expectErrorChange('k', 2, 'kLowerM');
383 it('should make sure that c has to be equal or less than m', () => {
384 formHelper.expectValidChange('c', 3);
385 formHelper.expectErrorChange('c', 4, 'cGreaterM');
388 it('should update validity of k and c on m change', () => {
389 formHelper.expectValidChange('m', 5);
390 formHelper.expectError('k', 'kLowerM');
391 formHelper.expectValid('c');
393 formHelper.expectValidChange('m', 1);
394 formHelper.expectError('c', 'cGreaterM');
395 formHelper.expectValid('k');
399 describe(`for 'clay' plugin`, () => {
401 formHelper.setValue('plugin', 'clay');
402 // Through this change d has a valid range from 4 to 7
403 formHelper.expectValidChange('k', 3);
404 formHelper.expectValidChange('m', 5);
407 it(`does require 'm', 'c', 'd', 'scalar_mds' and 'k'`, () => {
408 fixtureHelper.clickElement('#d-calc-btn');
409 expectRequiredControls(['k', 'm', 'd', 'scalar_mds']);
412 it(`should not show any other plugin specific form control`, () => {
413 fixtureHelper.expectIdElementsVisible(['l', 'crushLocality', 'packetSize', 'c'], false);
416 it('should show default values for d and scalar_mds', () => {
417 expect(component.form.getValue('d')).toBe(7); // (k+m-1)
418 expect(component.form.getValue('scalar_mds')).toBe('jerasure');
421 it('should auto change d if auto calculation is enabled (default)', () => {
422 formHelper.expectValidChange('k', 4);
423 expect(component.form.getValue('d')).toBe(8);
426 it('should have specific techniques for scalar_mds jerasure', () => {
428 ['reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig', 'cauchy_good', 'liber8tion'],
433 it('should have specific techniques for scalar_mds isa', () => {
434 formHelper.expectValidChange('scalar_mds', 'isa');
435 expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
438 it('should have specific techniques for scalar_mds shec', () => {
439 formHelper.expectValidChange('scalar_mds', 'shec');
440 expectTechniques(['single', 'multiple'], 'single');
443 describe('Validity of d', () => {
445 // Don't automatically change d - the only way to get d invalid
446 fixtureHelper.clickElement('#d-calc-btn');
449 it('should not automatically change d if k or m have been changed', () => {
450 formHelper.expectValidChange('m', 4);
451 formHelper.expectValidChange('k', 5);
452 expect(component.form.getValue('d')).toBe(7);
455 it('should trigger dMin through change of d', () => {
456 formHelper.expectErrorChange('d', 3, 'dMin');
459 it('should trigger dMax through change of d', () => {
460 formHelper.expectErrorChange('d', 8, 'dMax');
463 it('should trigger dMin through change of k and m', () => {
464 formHelper.expectValidChange('m', 2);
465 formHelper.expectValidChange('k', 7);
466 formHelper.expectError('d', 'dMin');
469 it('should trigger dMax through change of m', () => {
470 formHelper.expectValidChange('m', 3);
471 formHelper.expectError('d', 'dMax');
474 it('should remove dMax through change of k', () => {
475 formHelper.expectValidChange('m', 3);
476 formHelper.expectError('d', 'dMax');
477 formHelper.expectValidChange('k', 5);
478 formHelper.expectValid('d');
484 describe('submission', () => {
485 let ecp: ErasureCodeProfile;
486 let submittedEcp: ErasureCodeProfile;
488 const testCreation = () => {
489 fixture.detectChanges();
490 component.onSubmit();
491 expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
494 const ecpChange = (attribute: string, value: string | number) => {
495 ecp[attribute] = value;
496 submittedEcp[attribute] = value;
500 ecp = new ErasureCodeProfile();
501 submittedEcp = new ErasureCodeProfile();
502 submittedEcp['crush-root'] = 'default';
503 submittedEcp['crush-failure-domain'] = 'osd-rack';
504 submittedEcp['packetsize'] = 2048;
505 submittedEcp['technique'] = 'reed_sol_van';
507 const taskWrapper = TestBed.inject(TaskWrapperService);
508 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
509 spyOn(ecpService, 'create').and.stub();
512 describe(`'jerasure' usage`, () => {
514 submittedEcp['plugin'] = 'jerasure';
515 ecpChange('name', 'jerasureProfile');
520 it('should be able to create a profile with only required fields', () => {
521 formHelper.setMultipleValues(ecp, true);
525 it(`does not create with missing 'k' or invalid form`, () => {
527 formHelper.setMultipleValues(ecp, true);
528 component.onSubmit();
529 expect(ecpService.create).not.toHaveBeenCalled();
532 it('should be able to create a profile with m, k, name, directory and packetSize', () => {
534 ecpChange('directory', '/different/ecp/path');
535 formHelper.setMultipleValues(ecp, true);
536 formHelper.setValue('packetSize', 8192, true);
537 ecpChange('packetsize', 8192);
541 it('should not send the profile with unsupported fields', () => {
542 formHelper.setMultipleValues(ecp, true);
543 formHelper.setValue('crushLocality', 'osd', true);
548 describe(`'isa' usage`, () => {
550 ecpChange('name', 'isaProfile');
551 ecpChange('plugin', 'isa');
554 delete submittedEcp.packetsize;
557 it('should be able to create a profile with only plugin and name', () => {
558 formHelper.setMultipleValues(ecp, true);
562 it('should send profile with plugin, name, failure domain and technique only', () => {
563 ecpChange('technique', 'cauchy');
564 formHelper.setMultipleValues(ecp, true);
565 formHelper.setValue('crushFailureDomain', 'osd', true);
566 submittedEcp['crush-failure-domain'] = 'osd';
567 submittedEcp['crush-device-class'] = 'ssd';
571 it('should not send the profile with unsupported fields', () => {
572 formHelper.setMultipleValues(ecp, true);
573 formHelper.setValue('packetSize', 'osd', true);
578 describe(`'lrc' usage`, () => {
580 ecpChange('name', 'lrcProfile');
581 ecpChange('plugin', 'lrc');
585 delete submittedEcp.packetsize;
586 delete submittedEcp.technique;
589 it('should be able to create a profile with only required fields', () => {
590 formHelper.setMultipleValues(ecp, true);
594 it('should send profile with all required fields and crush root and locality', () => {
596 formHelper.setMultipleValues(ecp, true);
597 formHelper.setValue('crushRoot', component.buckets[2], true);
598 submittedEcp['crush-root'] = 'mix-host';
599 formHelper.setValue('crushLocality', 'osd-rack', true);
600 submittedEcp['crush-locality'] = 'osd-rack';
604 it('should not send the profile with unsupported fields', () => {
605 formHelper.setMultipleValues(ecp, true);
606 formHelper.setValue('c', 4, true);
611 describe(`'shec' usage`, () => {
613 ecpChange('name', 'shecProfile');
614 ecpChange('plugin', 'shec');
618 delete submittedEcp.packetsize;
619 delete submittedEcp.technique;
622 it('should be able to create a profile with only plugin and name', () => {
623 formHelper.setMultipleValues(ecp, true);
627 it('should send profile with plugin, name, c and crush device class only', () => {
629 formHelper.setMultipleValues(ecp, true);
630 formHelper.setValue('crushDeviceClass', 'ssd', true);
631 submittedEcp['crush-device-class'] = 'ssd';
635 it('should not send the profile with unsupported fields', () => {
636 formHelper.setMultipleValues(ecp, true);
637 formHelper.setValue('l', 8, true);
642 describe(`'clay' usage`, () => {
644 ecpChange('name', 'clayProfile');
645 ecpChange('plugin', 'clay');
646 // Setting expectations
650 submittedEcp.scalar_mds = 'jerasure';
651 delete submittedEcp.packetsize;
654 it('should be able to create a profile with only plugin and name', () => {
655 formHelper.setMultipleValues(ecp, true);
659 it('should send profile with a changed d', () => {
660 formHelper.setMultipleValues(ecp, true);
666 it('should send profile with a changed k which automatically changes d', () => {
668 formHelper.setMultipleValues(ecp, true);
673 it('should send profile with a changed sclara_mds', () => {
674 ecpChange('scalar_mds', 'shec');
675 formHelper.setMultipleValues(ecp, true);
676 submittedEcp.scalar_mds = 'shec';
677 submittedEcp.technique = 'single';
681 it('should not send the profile with unsupported fields', () => {
682 formHelper.setMultipleValues(ecp, true);
683 formHelper.setValue('l', 8, true);