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';
14 } from '~/testing/unit-test-helper';
15 import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
16 import { CrushNode } from '~/app/shared/models/crush-node';
17 import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
18 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
19 import { PoolModule } from '../pool.module';
20 import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form-modal.component';
22 describe('ErasureCodeProfileFormModalComponent', () => {
23 let component: ErasureCodeProfileFormModalComponent;
24 let ecpService: ErasureCodeProfileService;
25 let fixture: ComponentFixture<ErasureCodeProfileFormModalComponent>;
26 let formHelper: FormHelper;
27 let fixtureHelper: FixtureHelper;
28 let data: { plugins: string[]; names: string[]; nodes: CrushNode[] };
30 const expectTechnique = (current: string) =>
31 expect(component.form.getValue('technique')).toBe(current);
33 const expectTechniques = (techniques: string[], current: string) => {
34 expect(component.techniques).toEqual(techniques);
35 expectTechnique(current);
38 const expectRequiredControls = (controlNames: string[]) => {
39 controlNames.forEach((name) => {
40 const value = component.form.getValue(name);
41 formHelper.expectValid(name);
42 formHelper.expectErrorChange(name, null, 'required');
43 // This way other fields won't fail through getting invalid.
44 formHelper.expectValidChange(name, value);
46 fixtureHelper.expectIdElementsVisible(controlNames, true);
50 imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), PoolModule],
51 providers: [ErasureCodeProfileService, NgbActiveModal]
55 fixture = TestBed.createComponent(ErasureCodeProfileFormModalComponent);
56 fixtureHelper = new FixtureHelper(fixture);
57 component = fixture.componentInstance;
58 formHelper = new FormHelper(component.form);
59 ecpService = TestBed.inject(ErasureCodeProfileService);
61 plugins: ['isa', 'jerasure', 'shec', 'lrc'],
62 names: ['ecp1', 'ecp2'],
64 * Create the following test crush map:
67 * ----> 3x osd with ssd
70 * ------> 5x osd-rack with hdd
72 * ------> 5x osd-rack with ssd
76 Mocks.getCrushNode('default', -1, 'root', 11, [-2, -3]),
78 Mocks.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]),
79 Mocks.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'),
80 Mocks.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'),
81 Mocks.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'),
82 // SSD and HDD mixed devices host
83 Mocks.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]),
85 Mocks.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]),
86 Mocks.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
87 Mocks.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
88 Mocks.getCrushNode('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'),
89 Mocks.getCrushNode('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'),
90 Mocks.getCrushNode('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'),
92 Mocks.getCrushNode('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]),
93 Mocks.getCrushNode('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'),
94 Mocks.getCrushNode('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'),
95 Mocks.getCrushNode('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'),
96 Mocks.getCrushNode('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'),
97 Mocks.getCrushNode('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd')
100 spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
101 fixture.detectChanges();
104 it('should create', () => {
105 expect(component).toBeTruthy();
108 it('calls listing to get ecps on ngInit', () => {
109 expect(ecpService.getInfo).toHaveBeenCalled();
110 expect(component.names.length).toBe(2);
113 describe('form validation', () => {
114 it(`isn't valid if name is not set`, () => {
115 expect(component.form.invalid).toBeTruthy();
116 formHelper.setValue('name', 'someProfileName');
117 expect(component.form.valid).toBeTruthy();
120 it('sets name invalid', () => {
121 component.names = ['awesomeProfileName'];
122 formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
123 formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
124 formHelper.expectErrorChange('name', null, 'required');
127 it('sets k to min error', () => {
128 formHelper.expectErrorChange('k', 1, 'min');
131 it('sets m to min error', () => {
132 formHelper.expectErrorChange('m', 0, 'min');
135 it(`should show all default form controls`, () => {
136 const showDefaults = (plugin: string) => {
137 formHelper.setValue('plugin', plugin);
138 fixtureHelper.expectIdElementsVisible(
144 'crushFailureDomain',
152 showDefaults('jerasure');
153 showDefaults('shec');
158 it('should change technique to default if not available in other plugin', () => {
159 expectTechnique('reed_sol_van');
160 formHelper.setValue('technique', 'blaum_roth');
161 expectTechnique('blaum_roth');
162 formHelper.setValue('plugin', 'isa');
163 expectTechnique('reed_sol_van');
164 formHelper.setValue('plugin', 'clay');
165 formHelper.expectValidChange('scalar_mds', 'shec');
166 expectTechnique('single');
169 describe(`for 'jerasure' plugin (default)`, () => {
170 it(`requires 'm' and 'k'`, () => {
171 expectRequiredControls(['k', 'm']);
174 it(`should show 'packetSize' and 'technique'`, () => {
175 fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
178 it('should show available techniques', () => {
193 it(`should not show any other plugin specific form control`, () => {
194 fixtureHelper.expectIdElementsVisible(
195 ['c', 'l', 'crushLocality', 'd', 'scalar_mds'],
200 it('should not allow "k" to be changed more than possible', () => {
201 formHelper.expectErrorChange('k', 10, 'max');
204 it('should not allow "m" to be changed more than possible', () => {
205 formHelper.expectErrorChange('m', 10, 'max');
209 describe(`for 'isa' plugin`, () => {
211 formHelper.setValue('plugin', 'isa');
214 it(`does require 'm' and 'k'`, () => {
215 expectRequiredControls(['k', 'm']);
218 it(`should show 'technique'`, () => {
219 fixtureHelper.expectIdElementsVisible(['technique'], true);
222 it('should show available techniques', () => {
223 expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
226 it(`should not show any other plugin specific form control`, () => {
227 fixtureHelper.expectIdElementsVisible(
228 ['c', 'l', 'crushLocality', 'packetSize', 'd', 'scalar_mds'],
233 it('should not allow "k" to be changed more than possible', () => {
234 formHelper.expectErrorChange('k', 10, 'max');
237 it('should not allow "m" to be changed more than possible', () => {
238 formHelper.expectErrorChange('m', 10, 'max');
242 describe(`for 'lrc' plugin`, () => {
244 formHelper.setValue('plugin', 'lrc');
245 formHelper.expectValid('k');
246 formHelper.expectValid('l');
247 formHelper.expectValid('m');
250 it(`requires 'm', 'l' and 'k'`, () => {
251 expectRequiredControls(['k', 'm', 'l']);
254 it(`should show 'l' and 'crushLocality'`, () => {
255 fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
258 it(`should not show any other plugin specific form control`, () => {
259 fixtureHelper.expectIdElementsVisible(
260 ['c', 'packetSize', 'technique', 'd', 'scalar_mds'],
265 it('should not allow "k" to be changed more than possible', () => {
266 formHelper.expectErrorChange('k', 10, 'max');
269 it('should not allow "m" to be changed more than possible', () => {
270 formHelper.expectErrorChange('m', 10, 'max');
273 it('should not allow "l" to be changed so that (k+m) is not a multiple of "l"', () => {
274 formHelper.expectErrorChange('l', 4, 'unequal');
277 it('should update validity of k and l on m change', () => {
278 formHelper.expectValidChange('m', 3);
279 formHelper.expectError('k', 'unequal');
280 formHelper.expectError('l', 'unequal');
283 describe('lrc calculation', () => {
284 const expectCorrectCalculation = (
288 failedControl: string[] = []
290 formHelper.setValue('k', k);
291 formHelper.setValue('m', m);
292 formHelper.setValue('l', l);
293 ['k', 'l'].forEach((name) => {
294 if (failedControl.includes(name)) {
295 formHelper.expectError(name, 'unequal');
297 formHelper.expectValid(name);
344 it('tests all cases where k fails', () => {
345 tests.kFails.forEach((testCase) => {
346 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
350 it('tests all cases where l fails', () => {
351 tests.lFails.forEach((testCase) => {
352 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
356 it('tests all cases where everything is valid', () => {
357 tests.success.forEach((testCase) => {
358 expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
364 describe(`for 'shec' plugin`, () => {
366 formHelper.setValue('plugin', 'shec');
367 formHelper.expectValid('c');
368 formHelper.expectValid('m');
369 formHelper.expectValid('k');
372 it(`does require 'm', 'c' and 'k'`, () => {
373 expectRequiredControls(['k', 'm', 'c']);
376 it(`should not show any other plugin specific form control`, () => {
377 fixtureHelper.expectIdElementsVisible(
378 ['l', 'crushLocality', 'packetSize', 'technique', 'd', 'scalar_mds'],
383 it('should make sure that k has to be equal or greater than m', () => {
384 formHelper.expectValidChange('k', 3);
385 formHelper.expectErrorChange('k', 2, 'kLowerM');
388 it('should make sure that c has to be equal or less than m', () => {
389 formHelper.expectValidChange('c', 3);
390 formHelper.expectErrorChange('c', 4, 'cGreaterM');
393 it('should update validity of k and c on m change', () => {
394 formHelper.expectValidChange('m', 5);
395 formHelper.expectError('k', 'kLowerM');
396 formHelper.expectValid('c');
398 formHelper.expectValidChange('m', 1);
399 formHelper.expectError('c', 'cGreaterM');
400 formHelper.expectValid('k');
404 describe(`for 'clay' plugin`, () => {
406 formHelper.setValue('plugin', 'clay');
407 // Through this change d has a valid range from 4 to 7
408 formHelper.expectValidChange('k', 3);
409 formHelper.expectValidChange('m', 5);
412 it(`does require 'm', 'c', 'd', 'scalar_mds' and 'k'`, () => {
413 fixtureHelper.clickElement('#d-calc-btn');
414 expectRequiredControls(['k', 'm', 'd', 'scalar_mds']);
417 it(`should not show any other plugin specific form control`, () => {
418 fixtureHelper.expectIdElementsVisible(['l', 'crushLocality', 'packetSize', 'c'], false);
421 it('should show default values for d and scalar_mds', () => {
422 expect(component.form.getValue('d')).toBe(7); // (k+m-1)
423 expect(component.form.getValue('scalar_mds')).toBe('jerasure');
426 it('should auto change d if auto calculation is enabled (default)', () => {
427 formHelper.expectValidChange('k', 4);
428 expect(component.form.getValue('d')).toBe(8);
431 it('should have specific techniques for scalar_mds jerasure', () => {
433 ['reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig', 'cauchy_good', 'liber8tion'],
438 it('should have specific techniques for scalar_mds isa', () => {
439 formHelper.expectValidChange('scalar_mds', 'isa');
440 expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
443 it('should have specific techniques for scalar_mds shec', () => {
444 formHelper.expectValidChange('scalar_mds', 'shec');
445 expectTechniques(['single', 'multiple'], 'single');
448 describe('Validity of d', () => {
450 // Don't automatically change d - the only way to get d invalid
451 fixtureHelper.clickElement('#d-calc-btn');
454 it('should not automatically change d if k or m have been changed', () => {
455 formHelper.expectValidChange('m', 4);
456 formHelper.expectValidChange('k', 5);
457 expect(component.form.getValue('d')).toBe(7);
460 it('should trigger dMin through change of d', () => {
461 formHelper.expectErrorChange('d', 3, 'dMin');
464 it('should trigger dMax through change of d', () => {
465 formHelper.expectErrorChange('d', 8, 'dMax');
468 it('should trigger dMin through change of k and m', () => {
469 formHelper.expectValidChange('m', 2);
470 formHelper.expectValidChange('k', 7);
471 formHelper.expectError('d', 'dMin');
474 it('should trigger dMax through change of m', () => {
475 formHelper.expectValidChange('m', 3);
476 formHelper.expectError('d', 'dMax');
479 it('should remove dMax through change of k', () => {
480 formHelper.expectValidChange('m', 3);
481 formHelper.expectError('d', 'dMax');
482 formHelper.expectValidChange('k', 5);
483 formHelper.expectValid('d');
489 describe('submission', () => {
490 let ecp: ErasureCodeProfile;
491 let submittedEcp: ErasureCodeProfile;
493 const testCreation = () => {
494 fixture.detectChanges();
495 component.onSubmit();
496 expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
499 const ecpChange = (attribute: string, value: string | number) => {
500 ecp[attribute] = value;
501 submittedEcp[attribute] = value;
505 ecp = new ErasureCodeProfile();
506 submittedEcp = new ErasureCodeProfile();
507 submittedEcp['crush-root'] = 'default';
508 submittedEcp['crush-failure-domain'] = 'osd-rack';
509 submittedEcp['packetsize'] = 2048;
510 submittedEcp['technique'] = 'reed_sol_van';
512 const taskWrapper = TestBed.inject(TaskWrapperService);
513 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
514 spyOn(ecpService, 'create').and.stub();
517 describe(`'jerasure' usage`, () => {
519 submittedEcp['plugin'] = 'jerasure';
520 ecpChange('name', 'jerasureProfile');
525 it('should be able to create a profile with only required fields', () => {
526 formHelper.setMultipleValues(ecp, true);
530 it(`does not create with missing 'k' or invalid form`, () => {
532 formHelper.setMultipleValues(ecp, true);
533 component.onSubmit();
534 expect(ecpService.create).not.toHaveBeenCalled();
537 it('should be able to create a profile with m, k, name, directory and packetSize', () => {
539 ecpChange('directory', '/different/ecp/path');
540 formHelper.setMultipleValues(ecp, true);
541 formHelper.setValue('packetSize', 8192, true);
542 ecpChange('packetsize', 8192);
546 it('should not send the profile with unsupported fields', () => {
547 formHelper.setMultipleValues(ecp, true);
548 formHelper.setValue('crushLocality', 'osd', true);
553 describe(`'isa' usage`, () => {
555 ecpChange('name', 'isaProfile');
556 ecpChange('plugin', 'isa');
559 delete submittedEcp.packetsize;
562 it('should be able to create a profile with only plugin and name', () => {
563 formHelper.setMultipleValues(ecp, true);
567 it('should send profile with plugin, name, failure domain and technique only', () => {
568 ecpChange('technique', 'cauchy');
569 formHelper.setMultipleValues(ecp, true);
570 formHelper.setValue('crushFailureDomain', 'osd', true);
571 submittedEcp['crush-failure-domain'] = 'osd';
572 submittedEcp['crush-device-class'] = 'ssd';
576 it('should not send the profile with unsupported fields', () => {
577 formHelper.setMultipleValues(ecp, true);
578 formHelper.setValue('packetSize', 'osd', true);
583 describe(`'lrc' usage`, () => {
585 ecpChange('name', 'lrcProfile');
586 ecpChange('plugin', 'lrc');
590 delete submittedEcp.packetsize;
591 delete submittedEcp.technique;
594 it('should be able to create a profile with only required fields', () => {
595 formHelper.setMultipleValues(ecp, true);
599 it('should send profile with all required fields and crush root and locality', () => {
601 formHelper.setMultipleValues(ecp, true);
602 formHelper.setValue('crushRoot', component.buckets[2], true);
603 submittedEcp['crush-root'] = 'mix-host';
604 formHelper.setValue('crushLocality', 'osd-rack', true);
605 submittedEcp['crush-locality'] = 'osd-rack';
609 it('should not send the profile with unsupported fields', () => {
610 formHelper.setMultipleValues(ecp, true);
611 formHelper.setValue('c', 4, true);
616 describe(`'shec' usage`, () => {
618 ecpChange('name', 'shecProfile');
619 ecpChange('plugin', 'shec');
623 delete submittedEcp.packetsize;
624 delete submittedEcp.technique;
627 it('should be able to create a profile with only plugin and name', () => {
628 formHelper.setMultipleValues(ecp, true);
632 it('should send profile with plugin, name, c and crush device class only', () => {
634 formHelper.setMultipleValues(ecp, true);
635 formHelper.setValue('crushDeviceClass', 'ssd', true);
636 submittedEcp['crush-device-class'] = 'ssd';
640 it('should not send the profile with unsupported fields', () => {
641 formHelper.setMultipleValues(ecp, true);
642 formHelper.setValue('l', 8, true);
647 describe(`'clay' usage`, () => {
649 ecpChange('name', 'clayProfile');
650 ecpChange('plugin', 'clay');
651 // Setting expectations
655 submittedEcp.scalar_mds = 'jerasure';
656 delete submittedEcp.packetsize;
659 it('should be able to create a profile with only plugin and name', () => {
660 formHelper.setMultipleValues(ecp, true);
664 it('should send profile with a changed d', () => {
665 formHelper.setMultipleValues(ecp, true);
671 it('should send profile with a changed k which automatically changes d', () => {
673 formHelper.setMultipleValues(ecp, true);
678 it('should send profile with a changed sclara_mds', () => {
679 ecpChange('scalar_mds', 'shec');
680 formHelper.setMultipleValues(ecp, true);
681 submittedEcp.scalar_mds = 'shec';
682 submittedEcp.technique = 'single';
686 it('should not send the profile with unsupported fields', () => {
687 formHelper.setMultipleValues(ecp, true);
688 formHelper.setValue('l', 8, true);