]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
6ce5e83bf09961948018fc3632b581050a3ea640
[ceph.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   Mocks
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';
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   configureTestBed({
33     imports: [
34       HttpClientTestingModule,
35       RouterTestingModule,
36       ToastrModule.forRoot(),
37       PoolModule,
38       NgBootstrapFormValidationModule.forRoot()
39     ],
40     providers: [ErasureCodeProfileService, BsModalRef, i18nProviders]
41   });
42
43   beforeEach(() => {
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);
49     data = {
50       plugins: ['isa', 'jerasure', 'shec', 'lrc'],
51       names: ['ecp1', 'ecp2'],
52       /**
53        * Create the following test crush map:
54        * > default
55        * --> ssd-host
56        * ----> 3x osd with ssd
57        * --> mix-host
58        * ----> hdd-rack
59        * ------> 5x osd-rack with hdd
60        * ----> ssd-rack
61        * ------> 5x osd-rack with ssd
62        */
63       nodes: [
64         // Root node
65         Mocks.getCrushNode('default', -1, 'root', 11, [-2, -3]),
66         // SSD host
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]),
73         // HDD rack
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'),
80         // SSD rack
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')
87       ]
88     };
89     spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
90     fixture.detectChanges();
91   });
92
93   it('should create', () => {
94     expect(component).toBeTruthy();
95   });
96
97   it('calls listing to get ecps on ngInit', () => {
98     expect(ecpService.getInfo).toHaveBeenCalled();
99     expect(component.names.length).toBe(2);
100   });
101
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();
107     });
108
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');
114     });
115
116     it('sets k to min error', () => {
117       formHelper.expectErrorChange('k', 1, 'min');
118     });
119
120     it('sets m to min error', () => {
121       formHelper.expectErrorChange('m', 0, 'min');
122     });
123
124     it(`should show all default form controls`, () => {
125       const showDefaults = (plugin: string) => {
126         formHelper.setValue('plugin', plugin);
127         fixtureHelper.expectIdElementsVisible(
128           [
129             'name',
130             'plugin',
131             'k',
132             'm',
133             'crushFailureDomain',
134             'crushRoot',
135             'crushDeviceClass',
136             'directory'
137           ],
138           true
139         );
140       };
141       showDefaults('jerasure');
142       showDefaults('shec');
143       showDefaults('lrc');
144       showDefaults('isa');
145     });
146
147     describe(`for 'jerasure' plugin (default)`, () => {
148       it(`requires 'm' and 'k'`, () => {
149         formHelper.expectErrorChange('k', null, 'required');
150         formHelper.expectErrorChange('m', null, 'required');
151       });
152
153       it(`should show 'packetSize' and 'technique'`, () => {
154         fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
155       });
156
157       it(`should not show any other plugin specific form control`, () => {
158         fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false);
159       });
160
161       it('should not allow "k" to be changed more than possible', () => {
162         formHelper.expectErrorChange('k', 10, 'max');
163       });
164
165       it('should not allow "m" to be changed more than possible', () => {
166         formHelper.expectErrorChange('m', 10, 'max');
167       });
168     });
169
170     describe(`for 'isa' plugin`, () => {
171       beforeEach(() => {
172         formHelper.setValue('plugin', 'isa');
173       });
174
175       it(`does require 'm' and 'k'`, () => {
176         formHelper.expectErrorChange('k', null, 'required');
177         formHelper.expectErrorChange('m', null, 'required');
178       });
179
180       it(`should show 'technique'`, () => {
181         fixtureHelper.expectIdElementsVisible(['technique'], true);
182         expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy();
183       });
184
185       it(`should not show any other plugin specific form control`, () => {
186         fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false);
187       });
188
189       it('should not allow "k" to be changed more than possible', () => {
190         formHelper.expectErrorChange('k', 10, 'max');
191       });
192
193       it('should not allow "m" to be changed more than possible', () => {
194         formHelper.expectErrorChange('m', 10, 'max');
195       });
196     });
197
198     describe(`for 'lrc' plugin`, () => {
199       beforeEach(() => {
200         formHelper.setValue('plugin', 'lrc');
201         formHelper.expectValid('k');
202         formHelper.expectValid('l');
203         formHelper.expectValid('m');
204       });
205
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');
210       });
211
212       it(`should show 'l' and 'crushLocality'`, () => {
213         fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
214       });
215
216       it(`should not show any other plugin specific form control`, () => {
217         fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false);
218       });
219
220       it('should not allow "k" to be changed more than possible', () => {
221         formHelper.expectErrorChange('k', 10, 'max');
222       });
223
224       it('should not allow "m" to be changed more than possible', () => {
225         formHelper.expectErrorChange('m', 10, 'max');
226       });
227
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');
230       });
231
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');
236       });
237
238       describe('lrc calculation', () => {
239         const expectCorrectCalculation = (
240           k: number,
241           m: number,
242           l: number,
243           failedControl: string[] = []
244         ) => {
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');
251             } else {
252               formHelper.expectValid(name);
253             }
254           });
255         };
256
257         const tests = {
258           kFails: [
259             [2, 1, 1],
260             [2, 2, 1],
261             [3, 1, 1],
262             [3, 2, 1],
263             [3, 1, 2],
264             [3, 3, 1],
265             [3, 3, 3],
266             [4, 1, 1],
267             [4, 2, 1],
268             [4, 2, 2],
269             [4, 3, 1],
270             [4, 4, 1]
271           ],
272           lFails: [
273             [2, 1, 2],
274             [3, 2, 2],
275             [3, 1, 3],
276             [3, 2, 3],
277             [4, 1, 2],
278             [4, 3, 2],
279             [4, 3, 3],
280             [4, 1, 3],
281             [4, 4, 3],
282             [4, 1, 4],
283             [4, 2, 4],
284             [4, 3, 4]
285           ],
286           success: [
287             [2, 2, 2],
288             [2, 2, 4],
289             [3, 3, 2],
290             [3, 3, 6],
291             [4, 2, 3],
292             [4, 2, 6],
293             [4, 4, 2],
294             [4, 4, 8],
295             [4, 4, 4]
296           ]
297         };
298
299         it('tests all cases where k fails', () => {
300           tests.kFails.forEach((testCase) => {
301             expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
302           });
303         });
304
305         it('tests all cases where l fails', () => {
306           tests.lFails.forEach((testCase) => {
307             expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
308           });
309         });
310
311         it('tests all cases where everything is valid', () => {
312           tests.success.forEach((testCase) => {
313             expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
314           });
315         });
316       });
317     });
318
319     describe(`for 'shec' plugin`, () => {
320       beforeEach(() => {
321         formHelper.setValue('plugin', 'shec');
322         formHelper.expectValid('c');
323         formHelper.expectValid('m');
324         formHelper.expectValid('k');
325       });
326
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');
331       });
332
333       it(`should show 'c'`, () => {
334         fixtureHelper.expectIdElementsVisible(['c'], true);
335       });
336
337       it(`should not show any other plugin specific form control`, () => {
338         fixtureHelper.expectIdElementsVisible(
339           ['l', 'crushLocality', 'packetSize', 'technique'],
340           false
341         );
342       });
343
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');
347       });
348
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');
352       });
353
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');
358
359         formHelper.expectValidChange('m', 1);
360         formHelper.expectError('c', 'cGreaterM');
361         formHelper.expectValid('k');
362       });
363     });
364   });
365
366   describe('submission', () => {
367     let ecp: ErasureCodeProfile;
368     let submittedEcp: ErasureCodeProfile;
369
370     const testCreation = () => {
371       fixture.detectChanges();
372       component.onSubmit();
373       expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
374     };
375
376     const ecpChange = (attribute: string, value: string | number) => {
377       ecp[attribute] = value;
378       submittedEcp[attribute] = value;
379     };
380
381     beforeEach(() => {
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';
388
389       const taskWrapper = TestBed.inject(TaskWrapperService);
390       spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
391       spyOn(ecpService, 'create').and.stub();
392     });
393
394     describe(`'jerasure' usage`, () => {
395       beforeEach(() => {
396         submittedEcp['plugin'] = 'jerasure';
397         ecpChange('name', 'jerasureProfile');
398         submittedEcp.k = 4;
399         submittedEcp.m = 2;
400       });
401
402       it('should be able to create a profile with only required fields', () => {
403         formHelper.setMultipleValues(ecp, true);
404         testCreation();
405       });
406
407       it(`does not create with missing 'k' or invalid form`, () => {
408         ecpChange('k', 0);
409         formHelper.setMultipleValues(ecp, true);
410         component.onSubmit();
411         expect(ecpService.create).not.toHaveBeenCalled();
412       });
413
414       it('should be able to create a profile with m, k, name, directory and packetSize', () => {
415         ecpChange('m', 3);
416         ecpChange('directory', '/different/ecp/path');
417         formHelper.setMultipleValues(ecp, true);
418         formHelper.setValue('packetSize', 8192, true);
419         ecpChange('packetsize', 8192);
420         testCreation();
421       });
422
423       it('should not send the profile with unsupported fields', () => {
424         formHelper.setMultipleValues(ecp, true);
425         formHelper.setValue('crushLocality', 'osd', true);
426         testCreation();
427       });
428     });
429
430     describe(`'isa' usage`, () => {
431       beforeEach(() => {
432         ecpChange('name', 'isaProfile');
433         ecpChange('plugin', 'isa');
434         submittedEcp.k = 7;
435         submittedEcp.m = 3;
436         delete submittedEcp.packetsize;
437       });
438
439       it('should be able to create a profile with only plugin and name', () => {
440         formHelper.setMultipleValues(ecp, true);
441         testCreation();
442       });
443
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';
450         testCreation();
451       });
452
453       it('should not send the profile with unsupported fields', () => {
454         formHelper.setMultipleValues(ecp, true);
455         formHelper.setValue('packetSize', 'osd', true);
456         testCreation();
457       });
458     });
459
460     describe(`'lrc' usage`, () => {
461       beforeEach(() => {
462         ecpChange('name', 'lrcProfile');
463         ecpChange('plugin', 'lrc');
464         submittedEcp.k = 4;
465         submittedEcp.m = 2;
466         submittedEcp.l = 3;
467         delete submittedEcp.packetsize;
468         delete submittedEcp.technique;
469       });
470
471       it('should be able to create a profile with only required fields', () => {
472         formHelper.setMultipleValues(ecp, true);
473         testCreation();
474       });
475
476       it('should send profile with all required fields and crush root and locality', () => {
477         ecpChange('l', '6');
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';
483         testCreation();
484       });
485
486       it('should not send the profile with unsupported fields', () => {
487         formHelper.setMultipleValues(ecp, true);
488         formHelper.setValue('c', 4, true);
489         testCreation();
490       });
491     });
492
493     describe(`'shec' usage`, () => {
494       beforeEach(() => {
495         ecpChange('name', 'shecProfile');
496         ecpChange('plugin', 'shec');
497         submittedEcp.k = 4;
498         submittedEcp.m = 3;
499         submittedEcp.c = 2;
500         delete submittedEcp.packetsize;
501         delete submittedEcp.technique;
502       });
503
504       it('should be able to create a profile with only plugin and name', () => {
505         formHelper.setMultipleValues(ecp, true);
506         testCreation();
507       });
508
509       it('should send profile with plugin, name, c and crush device class only', () => {
510         ecpChange('c', '3');
511         formHelper.setMultipleValues(ecp, true);
512         formHelper.setValue('crushDeviceClass', 'ssd', true);
513         submittedEcp['crush-device-class'] = 'ssd';
514         testCreation();
515       });
516
517       it('should not send the profile with unsupported fields', () => {
518         formHelper.setMultipleValues(ecp, true);
519         formHelper.setValue('l', 8, true);
520         testCreation();
521       });
522     });
523   });
524 });