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';
16 } from '../../../../testing/unit-test-helper';
17 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
18 import { CrushNode } from '../../../shared/models/crush-node';
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;
32 // Object contains mock functions
42 return { name, type, type_id, id, children, device_class };
48 HttpClientTestingModule,
50 ToastrModule.forRoot(),
52 NgBootstrapFormValidationModule.forRoot()
54 providers: [ErasureCodeProfileService, BsModalRef, i18nProviders]
58 fixture = TestBed.createComponent(ErasureCodeProfileFormModalComponent);
59 fixtureHelper = new FixtureHelper(fixture);
60 component = fixture.componentInstance;
61 formHelper = new FormHelper(component.form);
62 ecpService = TestBed.get(ErasureCodeProfileService);
64 plugins: ['isa', 'jerasure', 'shec', 'lrc'],
65 names: ['ecp1', 'ecp2'],
67 * Create the following test crush map:
70 * ----> 3x osd with ssd
73 * ------> 2x osd-rack with hdd
75 * ------> 2x osd-rack with ssd
79 mock.node('default', -1, 'root', 11, [-2, -3]),
81 mock.node('ssd-host', -2, 'host', 1, [1, 0, 2]),
82 mock.node('osd.0', 0, 'osd', 0, undefined, 'ssd'),
83 mock.node('osd.1', 1, 'osd', 0, undefined, 'ssd'),
84 mock.node('osd.2', 2, 'osd', 0, undefined, 'ssd'),
85 // SSD and HDD mixed devices host
86 mock.node('mix-host', -3, 'host', 1, [-4, -5]),
88 mock.node('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]),
89 mock.node('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
90 mock.node('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
91 mock.node('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'),
92 mock.node('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'),
93 mock.node('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'),
95 mock.node('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]),
96 mock.node('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'),
97 mock.node('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'),
98 mock.node('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'),
99 mock.node('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'),
100 mock.node('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd')
103 spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
104 fixture.detectChanges();
107 it('should create', () => {
108 expect(component).toBeTruthy();
111 it('calls listing to get ecps on ngInit', () => {
112 expect(ecpService.getInfo).toHaveBeenCalled();
113 expect(component.names.length).toBe(2);
116 describe('form validation', () => {
117 it(`isn't valid if name is not set`, () => {
118 expect(component.form.invalid).toBeTruthy();
119 formHelper.setValue('name', 'someProfileName');
120 expect(component.form.valid).toBeTruthy();
123 it('sets name invalid', () => {
124 component.names = ['awesomeProfileName'];
125 formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
126 formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
127 formHelper.expectErrorChange('name', null, 'required');
130 it('sets k to min error', () => {
131 formHelper.expectErrorChange('k', 1, 'min');
134 it('sets m to min error', () => {
135 formHelper.expectErrorChange('m', 0, 'min');
138 it(`should show all default form controls`, () => {
139 const showDefaults = (plugin: string) => {
140 formHelper.setValue('plugin', plugin);
141 fixtureHelper.expectIdElementsVisible(
147 'crushFailureDomain',
155 showDefaults('jerasure');
156 showDefaults('shec');
161 describe(`for 'jerasure' plugin (default)`, () => {
162 it(`requires 'm' and 'k'`, () => {
163 formHelper.expectErrorChange('k', null, 'required');
164 formHelper.expectErrorChange('m', null, 'required');
167 it(`should show 'packetSize' and 'technique'`, () => {
168 fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
171 it(`should not show any other plugin specific form control`, () => {
172 fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false);
175 it('should not allow "k" to be changed more than possible', () => {
176 formHelper.expectErrorChange('k', 10, 'max');
179 it('should not allow "m" to be changed more than possible', () => {
180 formHelper.expectErrorChange('m', 10, 'max');
184 describe(`for 'isa' plugin`, () => {
186 formHelper.setValue('plugin', 'isa');
189 it(`does require 'm' and 'k'`, () => {
190 formHelper.expectErrorChange('k', null, 'required');
191 formHelper.expectErrorChange('m', null, 'required');
194 it(`should show 'technique'`, () => {
195 fixtureHelper.expectIdElementsVisible(['technique'], true);
196 expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy();
199 it(`should not show any other plugin specific form control`, () => {
200 fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false);
203 it('should not allow "k" to be changed more than possible', () => {
204 formHelper.expectErrorChange('k', 10, 'max');
207 it('should not allow "m" to be changed more than possible', () => {
208 formHelper.expectErrorChange('m', 10, 'max');
212 describe(`for 'lrc' plugin`, () => {
214 formHelper.setValue('plugin', 'lrc');
215 formHelper.expectValid('k');
216 formHelper.expectValid('l');
217 formHelper.expectValid('m');
220 it(`requires 'm', 'l' and 'k'`, () => {
221 formHelper.expectErrorChange('k', null, 'required');
222 formHelper.expectErrorChange('m', null, 'required');
223 formHelper.expectErrorChange('l', null, 'required');
226 it(`should show 'l' and 'crushLocality'`, () => {
227 fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
230 it(`should not show any other plugin specific form control`, () => {
231 fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false);
234 it('should not allow "k" to be changed more than possible', () => {
235 formHelper.expectErrorChange('k', 10, 'max');
238 it('should not allow "m" to be changed more than possible', () => {
239 formHelper.expectErrorChange('m', 10, 'max');
242 it('should not allow "l" to be changed so that (k+m) is not a multiple of "l"', () => {
243 formHelper.expectErrorChange('l', 4, 'unequal');
246 it('should update validity of k and l on m change', () => {
247 formHelper.expectValidChange('m', 3);
248 formHelper.expectError('k', 'unequal');
249 formHelper.expectError('l', 'unequal');
252 describe('lrc calculation', () => {
253 const expectCorrectCalculation = (
257 failedControl: string[] = []
259 formHelper.setValue('k', k);
260 formHelper.setValue('m', m);
261 formHelper.setValue('l', l);
262 ['k', 'l'].forEach((name) => {
263 if (failedControl.includes(name)) {
264 formHelper.expectError(name, 'unequal');
266 formHelper.expectValid(name);
313 it('tests all cases where k fails', () => {
314 tests.kFails.forEach((testCase) => {
315 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
319 it('tests all cases where l fails', () => {
320 tests.lFails.forEach((testCase) => {
321 expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
325 it('tests all cases where everything is valid', () => {
326 tests.success.forEach((testCase) => {
327 expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
333 describe(`for 'shec' plugin`, () => {
335 formHelper.setValue('plugin', 'shec');
336 formHelper.expectValid('c');
337 formHelper.expectValid('m');
338 formHelper.expectValid('k');
341 it(`does require 'm', 'c' and 'k'`, () => {
342 formHelper.expectErrorChange('k', null, 'required');
343 formHelper.expectErrorChange('m', null, 'required');
344 formHelper.expectErrorChange('c', null, 'required');
347 it(`should show 'c'`, () => {
348 fixtureHelper.expectIdElementsVisible(['c'], true);
351 it(`should not show any other plugin specific form control`, () => {
352 fixtureHelper.expectIdElementsVisible(
353 ['l', 'crushLocality', 'packetSize', 'technique'],
358 it('should make sure that k has to be equal or greater than m', () => {
359 formHelper.expectValidChange('k', 3);
360 formHelper.expectErrorChange('k', 2, 'kLowerM');
363 it('should make sure that c has to be equal or less than m', () => {
364 formHelper.expectValidChange('c', 3);
365 formHelper.expectErrorChange('c', 4, 'cGreaterM');
368 it('should update validity of k and c on m change', () => {
369 formHelper.expectValidChange('m', 5);
370 formHelper.expectError('k', 'kLowerM');
371 formHelper.expectValid('c');
373 formHelper.expectValidChange('m', 1);
374 formHelper.expectError('c', 'cGreaterM');
375 formHelper.expectValid('k');
380 describe('submission', () => {
381 let ecp: ErasureCodeProfile;
382 let submittedEcp: ErasureCodeProfile;
384 const testCreation = () => {
385 fixture.detectChanges();
386 component.onSubmit();
387 expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
390 const ecpChange = (attribute: string, value: string | number) => {
391 ecp[attribute] = value;
392 submittedEcp[attribute] = value;
396 ecp = new ErasureCodeProfile();
397 submittedEcp = new ErasureCodeProfile();
398 submittedEcp['crush-root'] = 'default';
399 submittedEcp['crush-failure-domain'] = 'osd-rack';
400 submittedEcp['packetsize'] = 2048;
401 submittedEcp['technique'] = 'reed_sol_van';
403 const taskWrapper = TestBed.get(TaskWrapperService);
404 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
405 spyOn(ecpService, 'create').and.stub();
408 describe(`'jerasure' usage`, () => {
410 submittedEcp['plugin'] = 'jerasure';
411 ecpChange('name', 'jerasureProfile');
416 it('should be able to create a profile with only required fields', () => {
417 formHelper.setMultipleValues(ecp, true);
421 it(`does not create with missing 'k' or invalid form`, () => {
423 formHelper.setMultipleValues(ecp, true);
424 component.onSubmit();
425 expect(ecpService.create).not.toHaveBeenCalled();
428 it('should be able to create a profile with m, k, name, directory and packetSize', () => {
430 ecpChange('directory', '/different/ecp/path');
431 formHelper.setMultipleValues(ecp, true);
432 formHelper.setValue('packetSize', 8192, true);
433 ecpChange('packetsize', 8192);
437 it('should not send the profile with unsupported fields', () => {
438 formHelper.setMultipleValues(ecp, true);
439 formHelper.setValue('crushLocality', 'osd', true);
444 describe(`'isa' usage`, () => {
446 ecpChange('name', 'isaProfile');
447 ecpChange('plugin', 'isa');
450 delete submittedEcp.packetsize;
453 it('should be able to create a profile with only plugin and name', () => {
454 formHelper.setMultipleValues(ecp, true);
458 it('should send profile with plugin, name, failure domain and technique only', () => {
459 ecpChange('technique', 'cauchy');
460 formHelper.setMultipleValues(ecp, true);
461 formHelper.setValue('crushFailureDomain', 'osd', true);
462 submittedEcp['crush-failure-domain'] = 'osd';
463 submittedEcp['crush-device-class'] = 'ssd';
467 it('should not send the profile with unsupported fields', () => {
468 formHelper.setMultipleValues(ecp, true);
469 formHelper.setValue('packetSize', 'osd', true);
474 describe(`'lrc' usage`, () => {
476 ecpChange('name', 'lrcProfile');
477 ecpChange('plugin', 'lrc');
481 delete submittedEcp.packetsize;
482 delete submittedEcp.technique;
485 it('should be able to create a profile with only required fields', () => {
486 formHelper.setMultipleValues(ecp, true);
490 it('should send profile with all required fields and crush root and locality', () => {
492 formHelper.setMultipleValues(ecp, true);
493 formHelper.setValue('crushRoot', component.buckets[2], true);
494 submittedEcp['crush-root'] = 'mix-host';
495 formHelper.setValue('crushLocality', 'osd-rack', true);
496 submittedEcp['crush-locality'] = 'osd-rack';
500 it('should not send the profile with unsupported fields', () => {
501 formHelper.setMultipleValues(ecp, true);
502 formHelper.setValue('c', 4, true);
507 describe(`'shec' usage`, () => {
509 ecpChange('name', 'shecProfile');
510 ecpChange('plugin', 'shec');
514 delete submittedEcp.packetsize;
515 delete submittedEcp.technique;
518 it('should be able to create a profile with only plugin and name', () => {
519 formHelper.setMultipleValues(ecp, true);
523 it('should send profile with plugin, name, c and crush device class only', () => {
525 formHelper.setMultipleValues(ecp, true);
526 formHelper.setValue('crushDeviceClass', 'ssd', true);
527 submittedEcp['crush-device-class'] = 'ssd';
531 it('should not send the profile with unsupported fields', () => {
532 formHelper.setMultipleValues(ecp, true);
533 formHelper.setValue('l', 8, true);