From 309c63872fc39afdee3ade4c33c08a5263daa423 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Mon, 18 Jun 2018 19:28:40 +0200 Subject: [PATCH] =?UTF-8?q?:white=5Fcheck=5Fmark:=20ajoute=20les=20tests?= =?UTF-8?q?=20de=20generateQuestion=20(et=20corrige=20le=20moteur=20pour?= =?UTF-8?q?=20qu'ils=20passent=20=C3=A0=20nouveau)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/engine/traverse.js | 59 +++-- test/generateQuestions.test.js | 428 +++++++++++++++++++++++++++++++++ 2 files changed, 470 insertions(+), 17 deletions(-) diff --git a/source/engine/traverse.js b/source/engine/traverse.js index 7487bdedf..0493a25ff 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -135,24 +135,49 @@ let fillVariableNode = (rules, rule, filter) => parseResult => { let variable = findRuleByDottedName(parsedRules, dottedName), variableHasFormula = variable.formule != null, variableHasCond = variable['applicable si'] != null || variable['non applicable si'] != null, - situationValue = evaluateVariable(situation, dottedName, variable), - needsEvaluation = (variableHasCond || variableHasFormula) && situationValue == null, - parsedRule = needsEvaluation - ? evaluateNode(cache, situation, parsedRules, variable) - : variable, + situationValue = evaluateVariable(situation, dottedName, variable), + needsEvaluation = situationValue == null && (variableHasCond || variableHasFormula), // evaluateVariable renvoit la valeur déduite de la situation courante renseignée par l'utilisateur - explanation = parsedRule, - nodeValue = - situationValue != null - ? situationValue // cette variable a été directement renseignée - : variableHasCond || variableHasFormula - ? parsedRule.nodeValue // la valeur du calcul fait foi - : null, // elle restera donc nulle - missingVariables = nodeValue != null ? {} : - { - ...(needsEvaluation ? parsedRule.missingVariables : {}), - ...(!situationValue ? { [dottedName]: 1 } : {}), - } + explanation = needsEvaluation + ? evaluateNode(cache, situation, parsedRules, variable) + : variable + + let nodeValue; + let missingVariables; + + // SITUATION 1 : La variable est directement renseignée + if (situationValue != null) { + nodeValue = situationValue; + missingVariables = {}; + } + // SITUATION 2 : La variable est calculée + if (situationValue == null && variableHasFormula) { + nodeValue = explanation.nodeValue; + missingVariables = explanation.missingVariables; + } + // SITUATION 3 : La variable est une question sans condition dont la valeur n'a pas été renseignée + if(situationValue == null && !variableHasFormula && !variableHasCond) { + nodeValue = null + missingVariables = { [dottedName]: 1} + } + // SITUATION 4 : La variable est une question avec conditions + if(situationValue == null && !variableHasFormula && variableHasCond) { + // SITUATION 4.1 : La condition est connue et vrai + if (explanation.isApplicable) { + nodeValue = explanation.nodeValue, + missingVariables = { [dottedName]: 1} + } + // SITUATION 4.2 : La condition est connue et fausse + if (explanation.isApplicable === false) { + nodeValue = explanation.nodeValue + missingVariables = {} + } + // SITUATION 4.3 : La condition n'est pas connue + if (explanation.isApplicable == null) { + nodeValue = null + missingVariables = explanation.missingVariables + } + } cache[cacheName] = rewriteNode( node, diff --git a/test/generateQuestions.test.js b/test/generateQuestions.test.js index e69de29bb..64e433be4 100644 --- a/test/generateQuestions.test.js +++ b/test/generateQuestions.test.js @@ -0,0 +1,428 @@ +import { expect } from 'chai' +import { + collectMissingVariables, + getNextSteps +} from '../source/engine/generateQuestions' +import { enrichRule, rules as realRules } from '../source/engine/rules' +import { analyse, parseAll } from '../source/engine/traverse' + +let stateSelector = () => null + +describe('collectMissingVariables', function() { + it('should identify missing variables', function() { + let rawRules = [ + { + nom: 'startHere', + formule: 2, + 'non applicable si': 'sum . evt . ko', + espace: 'sum' + }, + { + nom: 'evt', + espace: 'sum', + formule: { 'une possibilité': ['ko'] }, + titre: 'Truc', + question: '?' + }, + { nom: 'ko', espace: 'sum . evt' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('sum . evt . ko') + }) + + it('should identify missing variables mentioned in expressions', function() { + let rawRules = [ + { + nom: 'startHere', + formule: 2, + 'non applicable si': 'evt . nyet > evt . nope', + espace: 'sum' + }, + { nom: 'nope', espace: 'sum . evt' }, + { nom: 'nyet', espace: 'sum . evt' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('sum . evt . nyet') + expect(result).to.include('sum . evt . nope') + }) + + it('should ignore missing variables in the formula if not applicable', function() { + let rawRules = [ + { + nom: 'startHere', + formule: 'trois', + 'non applicable si': '3 > 2', + espace: 'sum' + }, + { nom: 'trois', espace: 'sum' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + }) + + it('should not report missing variables when "one of these" short-circuits', function() { + let rawRules = [ + { + nom: 'startHere', + formule: 'trois', + 'non applicable si': { + 'une de ces conditions': ['3 > 2', 'trois'] + }, + espace: 'sum' + }, + { nom: 'trois', espace: 'sum' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + }) + + it('should report "une possibilité" as a missing variable even though it has a formula', function() { + let rawRules = [ + { nom: 'startHere', formule: 'trois', espace: 'top' }, + { + nom: 'trois', + formule: { 'une possibilité': ['ko'] }, + espace: 'top' + } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('top . trois') + }) + + it('should not report missing variables when "une possibilité" is inapplicable', function() { + let rawRules = [ + { nom: 'startHere', formule: 'trois', espace: 'top' }, + { + nom: 'trois', + formule: { 'une possibilité': ['ko'] }, + 'non applicable si': 1, + espace: 'top' + } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + null + }) + + it('should not report missing variables when "une possibilité" was answered', function() { + let mySelector = name => ({ 'top . trois': 'ko' }[name]) + + let rawRules = [ + { nom: 'startHere', formule: 'trois', espace: 'top' }, + { + nom: 'trois', + formule: { 'une possibilité': ['ko'] }, + espace: 'top' + } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(mySelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + }) + + it('should report missing variables in switch statements', function() { + let rawRules = [ + { + nom: 'startHere', + formule: { + 'aiguillage numérique': { + '11 > dix': '1000%', + '3 > dix': '1100%', + '1 > dix': '1200%' + } + }, + espace: 'top' + }, + { nom: 'dix', espace: 'top' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('top . dix') + }) + + it('should report missing variables in variations', function() { + let rawRules = [ + { + nom: 'startHere', + formule: { somme: ['variations'] }, + espace: 'top' + }, + { + nom: 'variations', + espace: 'top', + formule: { + barème: { + assiette: 2008, + variations: [ + { + si: 'dix', + 'multiplicateur des tranches': 'deux', + tranches: [ + { 'en-dessous de': 1, taux: 0.1 }, + { de: 1, à: 2, taux: 'trois' }, + { 'au-dessus de': 2, taux: 10 } + ] + }, + { + si: '3 > 4', + 'multiplicateur des tranches': 'quatre', + tranches: [ + { 'en-dessous de': 1, taux: 0.1 }, + { de: 1, à: 2, taux: 1.8 }, + { 'au-dessus de': 2, taux: 10 } + ] + } + ] + } + } + }, + { nom: 'dix', espace: 'top' }, + { nom: 'deux', espace: 'top' }, + { nom: 'trois', espace: 'top' }, + { nom: 'quatre', espace: 'top' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('top . dix') + expect(result).to.include('top . deux') + expect(result).not.to.include('top . quatre') + // TODO + // expect(result).to.include('top . trois') + }) + + it('should not report missing variables in irrelevant variations', function() { + let rawRules = [ + { + nom: 'startHere', + formule: { somme: ['variations'] }, + espace: 'top' + }, + { + nom: 'variations', + espace: 'top', + formule: { + barème: { + assiette: 2008, + 'multiplicateur des tranches': 1000, + variations: [ + { + si: 'dix', + tranches: [ + { 'en-dessous de': 1, taux: 0.1 }, + { de: 1, à: 2, taux: 'deux' }, + { 'au-dessus de': 2, taux: 10 } + ] + }, + { + si: '3 > 2', + tranches: [ + { 'en-dessous de': 1, taux: 0.1 }, + { de: 1, à: 2, taux: 1.8 }, + { 'au-dessus de': 2, taux: 10 } + ] + } + ] + } + } + }, + { nom: 'dix', espace: 'top' }, + { nom: 'deux', espace: 'top' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + }) + + it('should not report missing variables in switch for consequences of false conditions', function() { + let rawRules = [ + { + nom: 'startHere', + formule: { + 'aiguillage numérique': { + '8 > 10': '1000%', + '1 > 2': 'dix' + } + }, + espace: 'top' + }, + { nom: 'dix', espace: 'top' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + }) + + it('should report missing variables in consequence when its condition is unresolved', function() { + let rawRules = [ + { + nom: 'startHere', + formule: { + 'aiguillage numérique': { + '10 > 11': '1000%', + '3 > dix': { + douze: '560%', + '1 > 2': '75015%' + } + } + }, + espace: 'top' + }, + { nom: 'douze', espace: 'top' }, + { nom: 'dix', espace: 'top' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('top . dix') + expect(result).to.include('top . douze') + }) + + it('should not report missing variables when a switch short-circuits', function() { + let rawRules = [ + { + nom: 'startHere', + formule: { + 'aiguillage numérique': { + '11 > 10': '1000%', + '3 > dix': '1100%', + '1 > dix': '1200%' + } + }, + espace: 'top' + }, + { nom: 'dix', espace: 'top' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'startHere')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.be.empty + }) +}) + +describe('nextSteps', function() { + it('should generate questions', function() { + let rawRules = [ + { nom: 'sum', formule: { somme: [2, 'deux'] }, espace: 'top' }, + { + nom: 'deux', + formule: 2, + 'non applicable si': "top . sum . evt = 'ko'", + espace: 'top' + }, + { + nom: 'evt', + espace: 'top . sum', + formule: { 'une possibilité': ['ko'] }, + titre: 'Truc', + question: '?' + }, + { nom: 'ko', espace: 'top . sum . evt' } + ], + rules = parseAll(rawRules.map(enrichRule)), + analysis = analyse(rules, 'sum')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result).to.have.lengthOf(1) + expect(result[0]).to.equal('top . sum . evt') + }) + + it('should generate questions from the real rules, experimental version', function() { + let stateSelector = name => + ({ + 'contrat salarié . type de contrat': 'CDI', + 'entreprise . effectif': '50' + }[name]) + + let rules = parseAll(realRules.map(enrichRule)), + analysis = analyse(rules, 'salaire')(stateSelector), + result = collectMissingVariables(analysis.targets) + + expect(result[0]).to.equal('contrat salarié . temps partiel') + }) + + it('should ask "motif CDD" if "CDD" applies', function() { + let stateSelector = name => + ({ + 'contrat salarié . type de contrat': 'CDD', + 'contrat salarié . salaire . brut de base': '2300' + }[name]) + + let rules = parseAll(realRules.map(enrichRule)), + analysis = analyse(rules, 'contrat salarié . salaire . net')( + stateSelector + ), + result = collectMissingVariables(analysis.targets) + + expect(result).to.include('contrat salarié . CDD . motif') + }) +}) + +describe('getNextSteps', function() { + it('should give priority to questions that advance most targets', function() { + let missingVariablesByTarget = { + chargé: { + effectif: 34.01, + cadre: 30 + }, + net: { + cadre: 10.1 + }, + aides: { + effectif: 32.0, + cadre: 10 + } + } + + let result = getNextSteps(missingVariablesByTarget) + + expect(result[0]).to.equal('cadre') + }) + + it('should give priority to questions by total weight when advancing the same target count', function() { + let missingVariablesByTarget = { + chargé: { + effectif: 24.01, + cadre: 30 + }, + net: { + effectif: 24.01, + cadre: 10.1 + }, + aides: {} + } + + let result = getNextSteps(missingVariablesByTarget) + + expect(result[0]).to.equal('effectif') + }) +})