]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
2628f1f69a42899de41031afac3256fbd7d7a52d
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / erasure-code-profile-form / erasure-code-profile-form-modal.component.spec.ts
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 });