import { Spec, TestCase } from '@yourcause/test-decorators'; import { DescribeAngularService } from '@yourcause/test-decorators/angular'; import { expect } from 'chai'; import { FormulaBuilderService } from './formula-builder.service'; import { FormulaEvaluationType, FormulaStep, FormulaStepValueType, RootFormula } from './formula-builder.typing'; interface RecordToEvaluate { value: number; someOtherValue?: string; nested: { value: number; nested: { value: number; }; }; } @DescribeAngularService(FormulaBuilderService, { }) export class FormulaBuilderServiceSpec implements Spec { record: RecordToEvaluate = { value: 1, someOtherValue: '', nested: { value: 3, nested: { value: 5 } } }; @TestCase('should be able to do basic addition') testShouldDoBasicAddition (service: FormulaBuilderService) { // do 3 + 3 const step: FormulaStep = { type: FormulaEvaluationType.Add, values: [ { type: FormulaStepValueType.Fixed, value: 3 }, { type: FormulaStepValueType.Fixed, value: 3 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(6); } @TestCase('should be able to do basic subtraction') testShouldDoBasicSubtraction (service: FormulaBuilderService) { // do 3 - 3 const step: FormulaStep = { type: FormulaEvaluationType.Subtract, values: [ { type: FormulaStepValueType.Fixed, value: 3 }, { type: FormulaStepValueType.Fixed, value: 3 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(0); } @TestCase('should be able to do basic multiplication') testShouldDoBasicMultiplication (service: FormulaBuilderService) { // do 3 * 3 const step: FormulaStep = { type: FormulaEvaluationType.Multiply, values: [ { type: FormulaStepValueType.Fixed, value: 3 }, { type: FormulaStepValueType.Fixed, value: 3 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(9); } @TestCase('should be able to do basic division') testShouldDoBasicDivision (service: FormulaBuilderService) { // do 3 / 3 const step: FormulaStep = { type: FormulaEvaluationType.Divide, values: [ { type: FormulaStepValueType.Fixed, value: 3 }, { type: FormulaStepValueType.Fixed, value: 3 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(1); } @TestCase('should be able to do basic average') testShouldDoBasicAverage (service: FormulaBuilderService) { // do 3 / 3 const step: FormulaStep = { type: FormulaEvaluationType.Average, values: [ { type: FormulaStepValueType.Fixed, value: 2 }, { type: FormulaStepValueType.Fixed, value: 3 }, { type: FormulaStepValueType.Fixed, value: 4 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(3); } @TestCase('should be able to do complex average') testShouldDoComplexAverage (service: FormulaBuilderService) { // do ((4 * 2) + (2 * 3) + (1 * 1)) -> 15 / 3 const step: FormulaStep = { type: FormulaEvaluationType.Average, values: [ { type: FormulaStepValueType.NestedStep, value: { type: FormulaEvaluationType.Multiply, values: [{ type: FormulaStepValueType.Fixed, value: 4 }, { type: FormulaStepValueType.Fixed, value: 2 }] } }, { type: FormulaStepValueType.NestedStep, value: { type: FormulaEvaluationType.Multiply, values: [{ type: FormulaStepValueType.Fixed, value: 2 }, { type: FormulaStepValueType.Fixed, value: 3 }] } }, { type: FormulaStepValueType.NestedStep, value: { type: FormulaEvaluationType.Multiply, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.Fixed, value: 1 }] } } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(5); } @TestCase('should be able to do deep math') testShouldDoDeepMath (service: FormulaBuilderService) { // do (1 + 5) / 2 const step: FormulaStep = { type: FormulaEvaluationType.Divide, values: [ { type: FormulaStepValueType.NestedStep, value: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.Fixed, value: 5 }] } }, { type: FormulaStepValueType.Fixed, value: 2 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(3); } @TestCase('should be able to do deep math based on parent values') testShouldDoDeepMathBasedOnParentValues (service: FormulaBuilderService) { // do (1 + 5) / 2 const step: FormulaStep = { type: FormulaEvaluationType.Divide, values: [ { type: FormulaStepValueType.NestedStep, value: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.ParentValue, value: 'value' // 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.nested.value' // 5 }] } }, { type: FormulaStepValueType.Fixed, value: 2 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const result = service.getCurrentValueFromState(state, 'value'); expect(result).to.equal(3); } @TestCase('should be able to re-run logic') testShouldRerunLogic (service: FormulaBuilderService) { // do (1 + 5) / 2 const step: FormulaStep = { type: FormulaEvaluationType.Divide, values: [ { type: FormulaStepValueType.NestedStep, value: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.ParentValue, value: 'value' // 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.nested.value' // 5 }] } }, { type: FormulaStepValueType.Fixed, value: 2 } ] }; const state = service.startRootFormulas([{ step, property: 'value' }], this.record); const value = service.getCurrentValueFromState(state, 'value'); expect(value).to.equal(3); this.record.nested.nested.value = 7; const newState = service.rerunFormulas('nested.nested.value', state, this.record); const newValue = service.getCurrentValueFromState(newState, 'value'); expect(newValue).to.equal(4); } @TestCase('should detect complex infinite loops') testShouldDetectComplexInfiniteLoops (service: FormulaBuilderService) { // rule1 dependends on the value of rule2, which depends on the value of rule3, which depends on the value of rule1 (infinite loop) const rule1: RootFormula = { property: 'value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.value' }] } }; const rule2: RootFormula = { property: 'nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.nested.value' }] } }; const rule3: RootFormula = { property: 'nested.nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'value' }] } }; const loops = service.detectInfiniteLoops(rule1, [rule2, rule3]); // value -> nested.value -> nested.nested.value -> value expect(loops).to.deep.equal([['value', 'nested.value', 'nested.nested.value', 'value']]); } @TestCase('should make it through related infinite loops') testShouldGoThroughRelatedInfiniteLoops (service: FormulaBuilderService) { /** * rule1 references rule2 which references rule3 which references rule2 * * This is an infinite loop, but not by something this component did * * Despite this, the method should still return this */ const rule1: RootFormula = { property: 'value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.value' }] } }; const rule2: RootFormula = { property: 'nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.nested.value' }] } }; const rule3: RootFormula = { property: 'nested.nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.value' }] } }; const loops = service.detectInfiniteLoops(rule1, [rule2, rule3]); // even though this formula doesn't cause it, the method should still detect it // value -> nested.value -> nested.nested.value -> nested.value expect(loops).to.deep.equal([['value', 'nested.value', 'nested.nested.value', 'nested.value']]); } @TestCase('should detect simple infinite loops') testShouldDetectSimpleInfiniteLoops (service: FormulaBuilderService) { // rule1 dependends on the value of rule2, which depends on the value of rule1 (direct infinite loop) const rule1: RootFormula = { property: 'value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.value' }] } }; const rule2: RootFormula = { property: 'nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'value' }] } }; const loops = service.detectInfiniteLoops(rule1, [rule2]); // shows dependency trees // value -> nested.value -> value expect(loops).to.deep.equal([['value', 'nested.value', 'value']]); } @TestCase('should not incorrectly identify an infinite loop') testShouldNotIncorrectlyIdentifyLoop (service: FormulaBuilderService) { // rule1 dependends on the value of rule2, which depends on the value of rule3, which depends on two fixed values (not infinite) const rule1: RootFormula = { property: 'value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.value' }] } }; const rule2: RootFormula = { property: 'nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.ParentValue, value: 'nested.nested.value' }] } }; const rule3: RootFormula = { property: 'nested.nested.value', step: { type: FormulaEvaluationType.Add, values: [{ type: FormulaStepValueType.Fixed, value: 1 }, { type: FormulaStepValueType.Fixed, value: 2 }] } }; const infiniteLoops = service.detectInfiniteLoops(rule1, [rule2, rule3]); expect(infiniteLoops).to.deep.equal([]); } }