]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
2628f1f69a42899de41031afac3256fbd7d7a52d
[ceph-ci.git] /
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';
5
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';
10
11 import {
12   configureTestBed,
13   FixtureHelper,
14   FormHelper,
15   i18nProviders
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';
23
24 describe('ErasureCodeProfileFormModalComponent', () => {
25   let component: ErasureCodeProfileFormModalComponent;
26   let ecpService: ErasureCodeProfileService;
27   let fixture: ComponentFixture<ErasureCodeProfileFormModalComponent>;
28   let formHelper: FormHelper;
29   let fixtureHelper: FixtureHelper;
30   let data: {};
31
32   // Object contains mock functions
33   const mock = {
34     node: (
35       name: string,
36       id: number,
37       type: string,
38       type_id: number,
39       children?: number[],
40       device_class?: string
41     ): CrushNode => {
42       return { name, type, type_id, id, children, device_class };
43     }
44   };
45
46   configureTestBed({
47     imports: [
48       HttpClientTestingModule,
49       RouterTestingModule,
50       ToastrModule.forRoot(),
51       PoolModule,
52       NgBootstrapFormValidationModule.forRoot()
53     ],
54     providers: [ErasureCodeProfileService, BsModalRef, i18nProviders]
55   });
56
57   beforeEach(() => {
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);
63     data = {
64       plugins: ['isa', 'jerasure', 'shec', 'lrc'],
65       names: ['ecp1', 'ecp2'],
66       /**
67        * Create the following test crush map:
68        * > default
69        * --> ssd-host
70        * ----> 3x osd with ssd
71        * --> mix-host
72        * ----> hdd-rack
73        * ------> 2x osd-rack with hdd
74        * ----> ssd-rack
75        * ------> 2x osd-rack with ssd
76        */
77       nodes: [
78         // Root node
79         mock.node('default', -1, 'root', 11, [-2, -3]),
80         // SSD host
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]),
87         // HDD rack
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'),
94         // SSD rack
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')
101       ]
102     };
103     spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
104     fixture.detectChanges();
105   });
106
107   it('should create', () => {
108     expect(component).toBeTruthy();
109   });
110
111   it('calls listing to get ecps on ngInit', () => {
112     expect(ecpService.getInfo).toHaveBeenCalled();
113     expect(component.names.length).toBe(2);
114   });
115
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();
121     });
122
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');
128     });
129
130     it('sets k to min error', () => {
131       formHelper.expectErrorChange('k', 1, 'min');
132     });
133
134     it('sets m to min error', () => {
135       formHelper.expectErrorChange('m', 0, 'min');
136     });
137
138     it(`should show all default form controls`, () => {
139       const showDefaults = (plugin: string) => {
140         formHelper.setValue('plugin', plugin);
141         fixtureHelper.expectIdElementsVisible(
142           [
143             'name',
144             'plugin',
145             'k',
146             'm',
147             'crushFailureDomain',
148             'crushRoot',
149             'crushDeviceClass',
150             'directory'
151           ],
152           true
153         );
154       };
155       showDefaults('jerasure');
156       showDefaults('shec');
157       showDefaults('lrc');
158       showDefaults('isa');
159     });
160
161     describe(`for 'jerasure' plugin (default)`, () => {
162       it(`requires 'm' and 'k'`, () => {
163         formHelper.expectErrorChange('k', null, 'required');
164         formHelper.expectErrorChange('m', null, 'required');
165       });
166
167       it(`should show 'packetSize' and 'technique'`, () => {
168         fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
169       });
170
171       it(`should not show any other plugin specific form control`, () => {
172         fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false);
173       });
174
175       it('should not allow "k" to be changed more than possible', () => {
176         formHelper.expectErrorChange('k', 10, 'max');
177       });
178
179       it('should not allow "m" to be changed more than possible', () => {
180         formHelper.expectErrorChange('m', 10, 'max');
181       });
182     });
183
184     describe(`for 'isa' plugin`, () => {
185       beforeEach(() => {
186         formHelper.setValue('plugin', 'isa');
187       });
188
189       it(`does require 'm' and 'k'`, () => {
190         formHelper.expectErrorChange('k', null, 'required');
191         formHelper.expectErrorChange('m', null, 'required');
192       });
193
194       it(`should show 'technique'`, () => {
195         fixtureHelper.expectIdElementsVisible(['technique'], true);
196         expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy();
197       });
198
199       it(`should not show any other plugin specific form control`, () => {
200         fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false);
201       });
202
203       it('should not allow "k" to be changed more than possible', () => {
204         formHelper.expectErrorChange('k', 10, 'max');
205       });
206
207       it('should not allow "m" to be changed more than possible', () => {
208         formHelper.expectErrorChange('m', 10, 'max');
209       });
210     });
211
212     describe(`for 'lrc' plugin`, () => {
213       beforeEach(() => {
214         formHelper.setValue('plugin', 'lrc');
215         formHelper.expectValid('k');
216         formHelper.expectValid('l');
217         formHelper.expectValid('m');
218       });
219
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');
224       });
225
226       it(`should show 'l' and 'crushLocality'`, () => {
227         fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
228       });
229
230       it(`should not show any other plugin specific form control`, () => {
231         fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false);
232       });
233
234       it('should not allow "k" to be changed more than possible', () => {
235         formHelper.expectErrorChange('k', 10, 'max');
236       });
237
238       it('should not allow "m" to be changed more than possible', () => {
239         formHelper.expectErrorChange('m', 10, 'max');
240       });
241
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');
244       });
245
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');
250       });
251
252       describe('lrc calculation', () => {
253         const expectCorrectCalculation = (
254           k: number,
255           m: number,
256           l: number,
257           failedControl: string[] = []
258         ) => {
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');
265             } else {
266               formHelper.expectValid(name);
267             }
268           });
269         };
270
271         const tests = {
272           kFails: [
273             [2, 1, 1],
274             [2, 2, 1],
275             [3, 1, 1],
276             [3, 2, 1],
277             [3, 1, 2],
278             [3, 3, 1],
279             [3, 3, 3],
280             [4, 1, 1],
281             [4, 2, 1],
282             [4, 2, 2],
283             [4, 3, 1],
284             [4, 4, 1]
285           ],
286           lFails: [
287             [2, 1, 2],
288             [3, 2, 2],
289             [3, 1, 3],
290             [3, 2, 3],
291             [4, 1, 2],
292             [4, 3, 2],
293             [4, 3, 3],
294             [4, 1, 3],
295             [4, 4, 3],
296             [4, 1, 4],
297             [4, 2, 4],
298             [4, 3, 4]
299           ],
300           success: [
301             [2, 2, 2],
302             [2, 2, 4],
303             [3, 3, 2],
304             [3, 3, 6],
305             [4, 2, 3],
306             [4, 2, 6],
307             [4, 4, 2],
308             [4, 4, 8],
309             [4, 4, 4]
310           ]
311         };
312
313         it('tests all cases where k fails', () => {
314           tests.kFails.forEach((testCase) => {
315             expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
316           });
317         });
318
319         it('tests all cases where l fails', () => {
320           tests.lFails.forEach((testCase) => {
321             expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
322           });
323         });
324
325         it('tests all cases where everything is valid', () => {
326           tests.success.forEach((testCase) => {
327             expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
328           });
329         });
330       });
331     });
332
333     describe(`for 'shec' plugin`, () => {
334       beforeEach(() => {
335         formHelper.setValue('plugin', 'shec');
336         formHelper.expectValid('c');
337         formHelper.expectValid('m');
338         formHelper.expectValid('k');
339       });
340
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');
345       });
346
347       it(`should show 'c'`, () => {
348         fixtureHelper.expectIdElementsVisible(['c'], true);
349       });
350
351       it(`should not show any other plugin specific form control`, () => {
352         fixtureHelper.expectIdElementsVisible(
353           ['l', 'crushLocality', 'packetSize', 'technique'],
354           false
355         );
356       });
357
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');
361       });
362
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');
366       });
367
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');
372
373         formHelper.expectValidChange('m', 1);
374         formHelper.expectError('c', 'cGreaterM');
375         formHelper.expectValid('k');
376       });
377     });
378   });
379
380   describe('submission', () => {
381     let ecp: ErasureCodeProfile;
382     let submittedEcp: ErasureCodeProfile;
383
384     const testCreation = () => {
385       fixture.detectChanges();
386       component.onSubmit();
387       expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
388     };
389
390     const ecpChange = (attribute: string, value: string | number) => {
391       ecp[attribute] = value;
392       submittedEcp[attribute] = value;
393     };
394
395     beforeEach(() => {
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';
402
403       const taskWrapper = TestBed.get(TaskWrapperService);
404       spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
405       spyOn(ecpService, 'create').and.stub();
406     });
407
408     describe(`'jerasure' usage`, () => {
409       beforeEach(() => {
410         submittedEcp['plugin'] = 'jerasure';
411         ecpChange('name', 'jerasureProfile');
412         submittedEcp.k = 4;
413         submittedEcp.m = 2;
414       });
415
416       it('should be able to create a profile with only required fields', () => {
417         formHelper.setMultipleValues(ecp, true);
418         testCreation();
419       });
420
421       it(`does not create with missing 'k' or invalid form`, () => {
422         ecpChange('k', 0);
423         formHelper.setMultipleValues(ecp, true);
424         component.onSubmit();
425         expect(ecpService.create).not.toHaveBeenCalled();
426       });
427
428       it('should be able to create a profile with m, k, name, directory and packetSize', () => {
429         ecpChange('m', 3);
430         ecpChange('directory', '/different/ecp/path');
431         formHelper.setMultipleValues(ecp, true);
432         formHelper.setValue('packetSize', 8192, true);
433         ecpChange('packetsize', 8192);
434         testCreation();
435       });
436
437       it('should not send the profile with unsupported fields', () => {
438         formHelper.setMultipleValues(ecp, true);
439         formHelper.setValue('crushLocality', 'osd', true);
440         testCreation();
441       });
442     });
443
444     describe(`'isa' usage`, () => {
445       beforeEach(() => {
446         ecpChange('name', 'isaProfile');
447         ecpChange('plugin', 'isa');
448         submittedEcp.k = 7;
449         submittedEcp.m = 3;
450         delete submittedEcp.packetsize;
451       });
452
453       it('should be able to create a profile with only plugin and name', () => {
454         formHelper.setMultipleValues(ecp, true);
455         testCreation();
456       });
457
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';
464         testCreation();
465       });
466
467       it('should not send the profile with unsupported fields', () => {
468         formHelper.setMultipleValues(ecp, true);
469         formHelper.setValue('packetSize', 'osd', true);
470         testCreation();
471       });
472     });
473
474     describe(`'lrc' usage`, () => {
475       beforeEach(() => {
476         ecpChange('name', 'lrcProfile');
477         ecpChange('plugin', 'lrc');
478         submittedEcp.k = 4;
479         submittedEcp.m = 2;
480         submittedEcp.l = 3;
481         delete submittedEcp.packetsize;
482         delete submittedEcp.technique;
483       });
484
485       it('should be able to create a profile with only required fields', () => {
486         formHelper.setMultipleValues(ecp, true);
487         testCreation();
488       });
489
490       it('should send profile with all required fields and crush root and locality', () => {
491         ecpChange('l', '6');
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';
497         testCreation();
498       });
499
500       it('should not send the profile with unsupported fields', () => {
501         formHelper.setMultipleValues(ecp, true);
502         formHelper.setValue('c', 4, true);
503         testCreation();
504       });
505     });
506
507     describe(`'shec' usage`, () => {
508       beforeEach(() => {
509         ecpChange('name', 'shecProfile');
510         ecpChange('plugin', 'shec');
511         submittedEcp.k = 4;
512         submittedEcp.m = 3;
513         submittedEcp.c = 2;
514         delete submittedEcp.packetsize;
515         delete submittedEcp.technique;
516       });
517
518       it('should be able to create a profile with only plugin and name', () => {
519         formHelper.setMultipleValues(ecp, true);
520         testCreation();
521       });
522
523       it('should send profile with plugin, name, c and crush device class only', () => {
524         ecpChange('c', '3');
525         formHelper.setMultipleValues(ecp, true);
526         formHelper.setValue('crushDeviceClass', 'ssd', true);
527         submittedEcp['crush-device-class'] = 'ssd';
528         testCreation();
529       });
530
531       it('should not send the profile with unsupported fields', () => {
532         formHelper.setMultipleValues(ecp, true);
533         formHelper.setValue('l', 8, true);
534         testCreation();
535       });
536     });
537   });
538 });