1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { By } from '@angular/platform-browser';
4 import { RouterTestingModule } from '@angular/router/testing';
6 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
7 import { BsModalRef } from 'ngx-bootstrap/modal';
8 import { ToastrModule } from 'ngx-toastr';
9 import { of } from 'rxjs';
17 } from '../../../../testing/unit-test-helper';
18 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
19 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
20 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
21 import { PoolModule } from '../pool.module';
22 import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form-modal.component';
24 describe('ErasureCodeProfileFormModalComponent', () => {
25 let component: ErasureCodeProfileFormModalComponent;
26 let ecpService: ErasureCodeProfileService;
27 let fixture: ComponentFixture<ErasureCodeProfileFormModalComponent>;
28 let formHelper: FormHelper;
29 let fixtureHelper: FixtureHelper;
34 HttpClientTestingModule,
36 ToastrModule.forRoot(),
38 NgBootstrapFormValidationModule.forRoot()
40 providers: [ErasureCodeProfileService, BsModalRef, i18nProviders]
44 fixture = TestBed.createComponent(ErasureCodeProfileFormModalComponent);
45 fixtureHelper = new FixtureHelper(fixture);
46 component = fixture.componentInstance;
47 formHelper = new FormHelper(component.form);
48 ecpService = TestBed.inject(ErasureCodeProfileService);
50 plugins: ['isa', 'jerasure', 'shec', 'lrc'],
51 names: ['ecp1', 'ecp2'],
53 * Create the following test crush map:
56 * ----> 3x osd with ssd
59 * ------> 5x osd-rack with hdd
61 * ------> 5x osd-rack with ssd
65 Mocks.getCrushNode('default', -1, 'root', 11, [-2, -3]),
67 Mocks.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]),
68 Mocks.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'),
69 Mocks.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'),
70 Mocks.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'),
71 // SSD and HDD mixed devices host
72 Mocks.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]),
74 Mocks.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]),
75 Mocks.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
76 Mocks.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
77 Mocks.getCrushNode('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'),
78 Mocks.getCrushNode('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'),
79 Mocks.getCrushNode('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'),
81 Mocks.getCrushNode('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]),
82 Mocks.getCrushNode('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'),
83 Mocks.getCrushNode('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'),
84 Mocks.getCrushNode('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'),
85 Mocks.getCrushNode('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'),
86 Mocks.getCrushNode('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd')
89 spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
90 fixture.detectChanges();
93 it('should create', () => {
94 expect(component).toBeTruthy();
97 it('calls listing to get ecps on ngInit', () => {
98 expect(ecpService.getInfo).toHaveBeenCalled();
99 expect(component.names.length).toBe(2);
102 describe('form validation', () => {
103 it(`isn't valid if name is not set`, () => {
104 expect(component.form.invalid).toBeTruthy();
105 formHelper.setValue('name', 'someProfileName');
106 expect(component.form.valid).toBeTruthy();
109 it('sets name invalid', () => {
110 component.names = ['awesomeProfileName'];
111 formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
112 formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
113 formHelper.expectErrorChange('name', null, 'required');
116 it('sets k to min error', () => {
117 formHelper.expectErrorChange('k', 1, 'min');
120 it('sets m to min error', () => {
121 formHelper.expectErrorChange('m', 0, 'min');
124 it(`should show all default form controls`, () => {
125 const showDefaults = (plugin: string) => {
126 formHelper.setValue('plugin', plugin);
127 fixtureHelper.expectIdElementsVisible(
133 'crushFailureDomain',
141 showDefaults('jerasure');
142 showDefaults('shec');
147 describe(`for 'jerasure' plugin (default)`, () => {
148 it(`requires 'm' and 'k'`, () => {
149 formHelper.expectErrorChange('k', null, 'required');
150 formHelper.expectErrorChange('m', null, 'required');
153 it(`should show 'packetSize' and 'technique'`, () => {
154 fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
157 it(`should not show any other plugin specific form control`, () => {
158 fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false);
161 it('should not allow "k" to be changed more than possible', () => {
162 formHelper.expectErrorChange('k', 10, 'max');
165 it('should not allow "m" to be changed more than possible', () => {
166 formHelper.expectErrorChange('m', 10, 'max');
170 describe(`for 'isa' plugin`, () => {
172 formHelper.setValue('plugin', 'isa');
175 it(`does require 'm' and 'k'`, () => {
176 formHelper.expectErrorChange('k', null, 'required');
177 formHelper.expectErrorChange('m', null, 'required');
180 it(`should show 'technique'`, () => {
181 fixtureHelper.expectIdElementsVisible(['technique'], true);
182 expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy();
185 it(`should not show any other plugin specific form control`, () => {
186 fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false);
189 it('should not allow "k" to be changed more than possible', () => {
190 formHelper.expectErrorChange('k', 10, 'max');
193 it('should not allow "m" to be changed more than possible', () => {
194 formHelper.expectErrorChange('m', 10, 'max');
198 describe(`for 'lrc' plugin`, () => {
200 formHelper.setValue('plugin', 'lrc');
201 formHelper.expectValid('k');
202 formHelper.expectValid('l');
203 formHelper.expectValid('m');
206 it(`requires 'm', 'l' and 'k'`, () => {
207 formHelper.expectErrorChange('k', null, 'required');
208 formHelper.expectErrorChange('m', null, 'required');
209 formHelper.expectErrorChange('l', null, 'required');
212 it(`should show 'l' and 'crushLocality'`, () => {
213 fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
216 it(`should not show any other plugin specific form control`, () => {
217 fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false);
220 it('should not allow "k" to be changed more than possible', () => {
221 formHelper.expectErrorChange('k', 10, 'max');
224 it('should not allow "m" to be changed more than possible', () => {
225 formHelper.expectErrorChange('m', 10, 'max');
228 it('should not allow "l" to be changed so that (k+m) is not a multiple of "l"', () => {
229 formHelper.expectErrorChange('l', 4, 'unequal');
232 it('should update validity of k and l on m change', () => {
233 formHelper.expectValidChange('m', 3);
234 formHelper.expectError('k', 'unequal');
235 formHelper.expectError('l', 'unequal');
238 describe('lrc calculation', () => {
239 const expectCorrectCalculation = (
243 failedControl: string[] = []
245 formHelper.setValue('k', k);
246 formHelper.setValue('m', m);
247 formHelper.setValue('l', l);
248 ['k', 'l'].forEach((name) => {
249 if (failedControl.includes(name)) {
250 formHelper.expectError(name, 'unequal');
252 formHelper.expectValid(name);
299 it('tests all cases where k fails', () => {
300 tests.kFails.forEach((testCase) => {
301 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
305 it('tests all cases where l fails', () => {
306 tests.lFails.forEach((testCase) => {
307 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
311 it('tests all cases where everything is valid', () => {
312 tests.success.forEach((testCase) => {
313 expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
319 describe(`for 'shec' plugin`, () => {
321 formHelper.setValue('plugin', 'shec');
322 formHelper.expectValid('c');
323 formHelper.expectValid('m');
324 formHelper.expectValid('k');
327 it(`does require 'm', 'c' and 'k'`, () => {
328 formHelper.expectErrorChange('k', null, 'required');
329 formHelper.expectErrorChange('m', null, 'required');
330 formHelper.expectErrorChange('c', null, 'required');
333 it(`should show 'c'`, () => {
334 fixtureHelper.expectIdElementsVisible(['c'], true);
337 it(`should not show any other plugin specific form control`, () => {
338 fixtureHelper.expectIdElementsVisible(
339 ['l', 'crushLocality', 'packetSize', 'technique'],
344 it('should make sure that k has to be equal or greater than m', () => {
345 formHelper.expectValidChange('k', 3);
346 formHelper.expectErrorChange('k', 2, 'kLowerM');
349 it('should make sure that c has to be equal or less than m', () => {
350 formHelper.expectValidChange('c', 3);
351 formHelper.expectErrorChange('c', 4, 'cGreaterM');
354 it('should update validity of k and c on m change', () => {
355 formHelper.expectValidChange('m', 5);
356 formHelper.expectError('k', 'kLowerM');
357 formHelper.expectValid('c');
359 formHelper.expectValidChange('m', 1);
360 formHelper.expectError('c', 'cGreaterM');
361 formHelper.expectValid('k');
366 describe('submission', () => {
367 let ecp: ErasureCodeProfile;
368 let submittedEcp: ErasureCodeProfile;
370 const testCreation = () => {
371 fixture.detectChanges();
372 component.onSubmit();
373 expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
376 const ecpChange = (attribute: string, value: string | number) => {
377 ecp[attribute] = value;
378 submittedEcp[attribute] = value;
382 ecp = new ErasureCodeProfile();
383 submittedEcp = new ErasureCodeProfile();
384 submittedEcp['crush-root'] = 'default';
385 submittedEcp['crush-failure-domain'] = 'osd-rack';
386 submittedEcp['packetsize'] = 2048;
387 submittedEcp['technique'] = 'reed_sol_van';
389 const taskWrapper = TestBed.inject(TaskWrapperService);
390 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
391 spyOn(ecpService, 'create').and.stub();
394 describe(`'jerasure' usage`, () => {
396 submittedEcp['plugin'] = 'jerasure';
397 ecpChange('name', 'jerasureProfile');
402 it('should be able to create a profile with only required fields', () => {
403 formHelper.setMultipleValues(ecp, true);
407 it(`does not create with missing 'k' or invalid form`, () => {
409 formHelper.setMultipleValues(ecp, true);
410 component.onSubmit();
411 expect(ecpService.create).not.toHaveBeenCalled();
414 it('should be able to create a profile with m, k, name, directory and packetSize', () => {
416 ecpChange('directory', '/different/ecp/path');
417 formHelper.setMultipleValues(ecp, true);
418 formHelper.setValue('packetSize', 8192, true);
419 ecpChange('packetsize', 8192);
423 it('should not send the profile with unsupported fields', () => {
424 formHelper.setMultipleValues(ecp, true);
425 formHelper.setValue('crushLocality', 'osd', true);
430 describe(`'isa' usage`, () => {
432 ecpChange('name', 'isaProfile');
433 ecpChange('plugin', 'isa');
436 delete submittedEcp.packetsize;
439 it('should be able to create a profile with only plugin and name', () => {
440 formHelper.setMultipleValues(ecp, true);
444 it('should send profile with plugin, name, failure domain and technique only', () => {
445 ecpChange('technique', 'cauchy');
446 formHelper.setMultipleValues(ecp, true);
447 formHelper.setValue('crushFailureDomain', 'osd', true);
448 submittedEcp['crush-failure-domain'] = 'osd';
449 submittedEcp['crush-device-class'] = 'ssd';
453 it('should not send the profile with unsupported fields', () => {
454 formHelper.setMultipleValues(ecp, true);
455 formHelper.setValue('packetSize', 'osd', true);
460 describe(`'lrc' usage`, () => {
462 ecpChange('name', 'lrcProfile');
463 ecpChange('plugin', 'lrc');
467 delete submittedEcp.packetsize;
468 delete submittedEcp.technique;
471 it('should be able to create a profile with only required fields', () => {
472 formHelper.setMultipleValues(ecp, true);
476 it('should send profile with all required fields and crush root and locality', () => {
478 formHelper.setMultipleValues(ecp, true);
479 formHelper.setValue('crushRoot', component.buckets[2], true);
480 submittedEcp['crush-root'] = 'mix-host';
481 formHelper.setValue('crushLocality', 'osd-rack', true);
482 submittedEcp['crush-locality'] = 'osd-rack';
486 it('should not send the profile with unsupported fields', () => {
487 formHelper.setMultipleValues(ecp, true);
488 formHelper.setValue('c', 4, true);
493 describe(`'shec' usage`, () => {
495 ecpChange('name', 'shecProfile');
496 ecpChange('plugin', 'shec');
500 delete submittedEcp.packetsize;
501 delete submittedEcp.technique;
504 it('should be able to create a profile with only plugin and name', () => {
505 formHelper.setMultipleValues(ecp, true);
509 it('should send profile with plugin, name, c and crush device class only', () => {
511 formHelper.setMultipleValues(ecp, true);
512 formHelper.setValue('crushDeviceClass', 'ssd', true);
513 submittedEcp['crush-device-class'] = 'ssd';
517 it('should not send the profile with unsupported fields', () => {
518 formHelper.setMultipleValues(ecp, true);
519 formHelper.setValue('l', 8, true);