From e1325dfb99b73f4eae41d53055cde23828ec0834 Mon Sep 17 00:00:00 2001 From: Christian Lawson-Perfect Date: Wed, 24 Jul 2024 08:10:19 +0100 Subject: [PATCH] add unit tests for built-in constants --- runtime/scripts/jme-variables.js | 4 ++- tests/jme-runtime.js | 29 +++++++++++++++++-- tests/jme/jme-tests.mjs | 2 ++ tests/numbas-runtime.js | 48 ++++++++++++++++++++++++-------- tests/parts/part-tests.mjs | 33 ++++++++++++++++++++-- 5 files changed, 99 insertions(+), 17 deletions(-) diff --git a/runtime/scripts/jme-variables.js b/runtime/scripts/jme-variables.js index 088cbb1e2..4664786c0 100644 --- a/runtime/scripts/jme-variables.js +++ b/runtime/scripts/jme-variables.js @@ -432,7 +432,9 @@ jme.variables = /** @lends Numbas.jme.variables */ { value = scope.evaluate(value+''); } names.forEach(function(name) { - if(enabled===undefined ? !(def.enabled === undefined || def.enabled) : !(enabled[name]===undefined || enabled[name])) { + var def_enabled = def.enabled === undefined || def.enabled; + var q_enabled = enabled !== undefined && (enabled[name] || (enabled[name]===undefined && def_enabled)); + if(!(enabled===undefined ? def_enabled : q_enabled)) { scope.deleteConstant(name); return; } diff --git a/tests/jme-runtime.js b/tests/jme-runtime.js index 7a4fd6077..e10164f17 100644 --- a/tests/jme-runtime.js +++ b/tests/jme-runtime.js @@ -422,6 +422,22 @@ Copyright 2011-14 Newcastle University Numbas.queueScript('util',['base', 'math', 'parsel'],function() { /** @namespace Numbas.util */ var util = Numbas.util = /** @lends Numbas.util */ { + /** Run the given function when the document is ready. + * + * @param {Function} fn + */ + document_ready: function(fn) { + if(document.readyState == 'complete') { + setTimeout(fn, 1); + } else { + document.addEventListener('readystatechange', function(e) { + if(document.readyState == 'complete') { + setTimeout(fn, 1); + } + }); + } + }, + /** Derive type B from A (class inheritance, really) * * B's prototype supercedes A's. @@ -8456,6 +8472,7 @@ var math = Numbas.math; * @typedef Numbas.jme.constant_definition * @property {TeX} tex - A TeX rendering of the constant * @property {Numbas.jme.token} value - The JME value of the constant. + * @property {boolean} enabled - Is the constant enabled? True by default. */ @@ -14099,7 +14116,8 @@ var builtin_constants = Numbas.jme.builtin_constants = [ {name: 'pi', value: new TNum(Math.PI), tex: '\\pi'}, {name: 'i', value: new TNum(math.complex(0,1)), tex: 'i'}, {name: 'infinity,infty', value: new TNum(Infinity), tex: '\\infty'}, - {name: 'NaN', value: new TNum(NaN), tex: '\\texttt{NaN}'} + {name: 'NaN', value: new TNum(NaN), tex: '\\texttt{NaN}'}, + {name: 'j', value: new TNum(math.complex(0,1)), tex: 'j', enabled: false}, ]; Numbas.jme.variables.makeConstants(Numbas.jme.builtin_constants, builtinScope); @@ -19813,9 +19831,10 @@ jme.variables = /** @lends Numbas.jme.variables */ { * * @param {Array.} definitions * @param {Numbas.jme.Scope} scope + * @param {Object.} enabled - For each constant name, is it enabled? If not given, then the `enabled` value in the definition is used. * @returns {Array.} - The names of constants added to the scope. */ - makeConstants: function(definitions,scope) { + makeConstants: function(definitions, scope, enabled) { var defined_names = []; definitions.forEach(function(def) { var names = def.name.split(/\s*,\s*/); @@ -19824,6 +19843,12 @@ jme.variables = /** @lends Numbas.jme.variables */ { value = scope.evaluate(value+''); } names.forEach(function(name) { + var def_enabled = def.enabled === undefined || def.enabled; + var q_enabled = enabled !== undefined && (enabled[name] || (enabled[name]===undefined && def_enabled)); + if(!(enabled===undefined ? def_enabled : q_enabled)) { + scope.deleteConstant(name); + return; + } defined_names.push(jme.normaliseName(name,scope)); scope.setConstant(name,{value:value, tex:def.tex}); }); diff --git a/tests/jme/jme-tests.mjs b/tests/jme/jme-tests.mjs index 142ff7668..1c4ca2263 100644 --- a/tests/jme/jme-tests.mjs +++ b/tests/jme/jme-tests.mjs @@ -1574,6 +1574,8 @@ Numbas.queueScript('jme_tests',['qunit','jme','jme-rules','jme-display','jme-cal assert.equal(Numbas.jme.builtinScope.evaluate('e').value, Math.E, 'e is the base of the natural logarithm in the built-in scope'); deepCloseEqual(assert,Numbas.jme.builtinScope.evaluate('i').value, Numbas.math.complex(0,1), 'i is sqrt(-1) in the built-in scope'); + assert.notOk(Numbas.jme.builtinScope.getConstant('j'), 'j is not a defined constant in the built-in scope'); + var s = new Numbas.jme.Scope([Numbas.jme.builtinScope]); s.deleteConstant('pi'); assert.notOk(s.getConstant('pi'),'pi is not a constant after deleting'); diff --git a/tests/numbas-runtime.js b/tests/numbas-runtime.js index 7eaf7f37a..2120daf3b 100644 --- a/tests/numbas-runtime.js +++ b/tests/numbas-runtime.js @@ -423,6 +423,22 @@ Copyright 2011-14 Newcastle University Numbas.queueScript('util',['base', 'math', 'parsel'],function() { /** @namespace Numbas.util */ var util = Numbas.util = /** @lends Numbas.util */ { + /** Run the given function when the document is ready. + * + * @param {Function} fn + */ + document_ready: function(fn) { + if(document.readyState == 'complete') { + setTimeout(fn, 1); + } else { + document.addEventListener('readystatechange', function(e) { + if(document.readyState == 'complete') { + setTimeout(fn, 1); + } + }); + } + }, + /** Derive type B from A (class inheritance, really) * * B's prototype supercedes A's. @@ -8047,6 +8063,7 @@ var math = Numbas.math; * @typedef Numbas.jme.constant_definition * @property {TeX} tex - A TeX rendering of the constant * @property {Numbas.jme.token} value - The JME value of the constant. + * @property {boolean} enabled - Is the constant enabled? True by default. */ @@ -13690,7 +13707,8 @@ var builtin_constants = Numbas.jme.builtin_constants = [ {name: 'pi', value: new TNum(Math.PI), tex: '\\pi'}, {name: 'i', value: new TNum(math.complex(0,1)), tex: 'i'}, {name: 'infinity,infty', value: new TNum(Infinity), tex: '\\infty'}, - {name: 'NaN', value: new TNum(NaN), tex: '\\texttt{NaN}'} + {name: 'NaN', value: new TNum(NaN), tex: '\\texttt{NaN}'}, + {name: 'j', value: new TNum(math.complex(0,1)), tex: 'j', enabled: false}, ]; Numbas.jme.variables.makeConstants(Numbas.jme.builtin_constants, builtinScope); @@ -19404,9 +19422,10 @@ jme.variables = /** @lends Numbas.jme.variables */ { * * @param {Array.} definitions * @param {Numbas.jme.Scope} scope + * @param {Object.} enabled - For each constant name, is it enabled? If not given, then the `enabled` value in the definition is used. * @returns {Array.} - The names of constants added to the scope. */ - makeConstants: function(definitions,scope) { + makeConstants: function(definitions, scope, enabled) { var defined_names = []; definitions.forEach(function(def) { var names = def.name.split(/\s*,\s*/); @@ -19415,6 +19434,12 @@ jme.variables = /** @lends Numbas.jme.variables */ { value = scope.evaluate(value+''); } names.forEach(function(name) { + var def_enabled = def.enabled === undefined || def.enabled; + var q_enabled = enabled !== undefined && (enabled[name] || (enabled[name]===undefined && def_enabled)); + if(!(enabled===undefined ? def_enabled : q_enabled)) { + scope.deleteConstant(name); + return; + } defined_names.push(jme.normaliseName(name,scope)); scope.setConstant(name,{value:value, tex:def.tex}); }); @@ -23288,16 +23313,14 @@ Question.prototype = /** @lends Numbas.Question.prototype */ } }); q.signals.on(['preambleRun', 'constantsLoaded'], function() { - var defined_constants = Numbas.jme.variables.makeConstants(q.constantsTodo.custom,q.scope); + var enabled_constants = {}; q.constantsTodo.builtin.forEach(function(c) { - if(!c.enable) { - c.name.split(',').forEach(function(name) { - if(defined_constants.indexOf(jme.normaliseName(name,q.scope))==-1) { - q.scope.deleteConstant(name); - } - }); - } + c.name.split(',').forEach(function(name) { + enabled_constants[name] = c.enable; + }); }); + Numbas.jme.variables.makeConstants(Numbas.jme.builtin_constants, q.scope, enabled_constants); + var defined_constants = Numbas.jme.variables.makeConstants(q.constantsTodo.custom,q.scope); q.signals.trigger('constantsMade'); }); q.signals.on(['preambleRun', 'functionsLoaded'], function() { @@ -26928,7 +26951,7 @@ Copyright 2011-14 Newcastle University // 'base' gives the third-party libraries on which Numbas depends Numbas.queueScript('base',['jquery','localisation','seedrandom','knockout','sarissa'],function() { }); -Numbas.queueScript('start-exam',['base','exam','settings'],function() { +Numbas.queueScript('start-exam',['base','util', 'exam','settings'],function() { for(var name in Numbas.custom_part_types) { Numbas.partConstructors[name] = Numbas.parts.CustomPart; }; @@ -26957,7 +26980,7 @@ Numbas.queueScript('start-exam',['base','exam','settings'],function() { * @function */ var init = Numbas.init = function() { - $(document).ready(function() { + Numbas.util.document_ready(function() { for(var x in Numbas.extensions) { Numbas.activateExtension(x); } @@ -26980,6 +27003,7 @@ Numbas.queueScript('start-exam',['base','exam','settings'],function() { exam.entry = entry; switch(entry) { + case '': case 'ab-initio': job(exam.init,exam); exam.signals.on('ready', function() { diff --git a/tests/parts/part-tests.mjs b/tests/parts/part-tests.mjs index 88a8f52c0..cce7add37 100644 --- a/tests/parts/part-tests.mjs +++ b/tests/parts/part-tests.mjs @@ -1094,6 +1094,35 @@ Numbas.queueScript('part_tests',['qunit','json','jme','localisation','parts/numb } ); + question_test( + 'Built-in constants: with j', + { + builtin_constants: { + j: true, + e: false + } + }, + async function(assert, q) { + assert.ok(q.scope.getConstant('j'), 'j is a defined constant, turned on by the question'); + assert.ok(q.scope.getConstant('pi'), 'pi is a defined constant, turned on by default'); + assert.notOk(q.scope.getConstant('e'), 'e is not a defined constant, turned off by the question'); + } + ); + + question_test( + 'Built-in constants: no j', + { + builtin_constants: { + e: false + } + }, + async function(assert, q) { + assert.notOk(q.scope.getConstant('j'), 'j is not a defined constant, turned off by default'); + assert.ok(q.scope.getConstant('pi'), 'pi is a defined constant, turned on by default'); + assert.notOk(q.scope.getConstant('e'), 'e is not a defined constant, turned off by the question'); + } + ); + question_test( "A big question", {"name":"Working on standalone part instances","tags":[],"metadata":{"description":"

Check that the MarkingScript reimplementations of the marking algorithms work properly.

","licence":"None specified"},"statement":"

Parts a to f use the standard marking algorithms.

","advice":"","rulesets":{},"extensions":[],"variables":{"m":{"name":"m","group":"Ungrouped variables","definition":"id(2)","description":"","templateType":"anything"}},"variablesTest":{"condition":"","maxRuns":100},"ungrouped_variables":["m"],"variable_groups":[],"functions":{},"preamble":{"js":"","css":""},"parts":[{"type":"numberentry","marks":1,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[],"variableReplacementStrategy":"originalfirst","customMarkingAlgorithm":"","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

Write a number between 1 and 2

","minValue":"1","maxValue":"2","correctAnswerFraction":false,"allowFractions":false,"mustBeReduced":false,"mustBeReducedPC":0,"precisionType":"dp","precision":"2","precisionPartialCredit":0,"precisionMessage":"You have not given your answer to the correct precision.","strictPrecision":false,"showPrecisionHint":true,"notationStyles":["plain","en","si-en"],"correctAnswerStyle":"plain"},{"type":"matrix","marks":1,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[],"variableReplacementStrategy":"originalfirst","customMarkingAlgorithm":"","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

Write a $2 \\times 2$ identity matrix.

","correctAnswer":"id(2)","correctAnswerFractions":false,"numRows":"2","numColumns":"2","allowResize":true,"tolerance":0,"markPerCell":true,"allowFractions":false,"precisionType":"dp","precision":0,"precisionPartialCredit":"40","precisionMessage":"You have not given your answer to the correct precision.","strictPrecision":true},{"type":"jme","marks":1,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[],"variableReplacementStrategy":"originalfirst","customMarkingAlgorithm":"","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

Write $x$

","answer":"x","showPreview":true,"checkingType":"absdiff","checkingAccuracy":0.001,"failureRate":1,"vsetRangePoints":5,"vsetRange":[0,1],"checkVariableNames":true,"expectedVariableNames":["x"],"notallowed":{"strings":["("],"showStrings":false,"partialCredit":0,"message":"

No brackets!

"}},{"type":"patternmatch","marks":1,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[],"variableReplacementStrategy":"originalfirst","customMarkingAlgorithm":"","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

Write \"a+\"

","answer":"a+","displayAnswer":"","caseSensitive":true,"partialCredit":"30","matchMode":"exact"},{"type":"1_n_2","marks":0,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[],"variableReplacementStrategy":"originalfirst","customMarkingAlgorithm":"","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

Choose choice 1

","minMarks":0,"maxMarks":0,"shuffleChoices":false,"displayType":"radiogroup","displayColumns":0,"choices":["Choice 1","Choice 2","Choice 3"],"matrix":["1",0,"-1"],"distractors":["Choice 1 is good","Choice 2 is not great","Choice 3 is bad"]},{"type":"numberentry","marks":1,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[{"variable":"m","part":"p1","must_go_first":false}],"variableReplacementStrategy":"alwaysreplace","customMarkingAlgorithm":"","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

What's the determinant of the matrix in part b?

","minValue":"det(m)","maxValue":"det(m)","correctAnswerFraction":false,"allowFractions":false,"mustBeReduced":false,"mustBeReducedPC":0,"notationStyles":["plain","en","si-en"],"correctAnswerStyle":"plain"},{"type":"numberentry","marks":1,"showCorrectAnswer":true,"showFeedbackIcon":true,"scripts":{},"variableReplacements":[],"variableReplacementStrategy":"originalfirst","customMarkingAlgorithm":"q:\n apply_marking_script(\"numberentry\",studentAnswer,settings+[\"minvalue\":4,\"maxvalue\":5],1)\n\nr:\n apply_marking_script(\"numberentry\",studentAnswer,settings+[\"minvalue\":3,\"maxvalue\":4],1)\n\nmark:\n feedback(\"number between 4 and 5\");\n concat_feedback(q[\"mark\"][\"feedback\"],marks/2);\n feedback(\"number between 3 and 4\");\n concat_feedback(r[\"mark\"][\"feedback\"],marks/2)","extendBaseMarkingAlgorithm":true,"unitTests":[],"prompt":"

Write a number between 4 and 5, and between 3 and 4.

","minValue":"1","maxValue":"2","correctAnswerFraction":false,"allowFractions":false,"mustBeReduced":false,"mustBeReducedPC":0,"notationStyles":["plain","en","si-en"],"correctAnswerStyle":"plain"}]}, @@ -2635,7 +2664,7 @@ mark: variables: { 'a': { name: 'a', - definition: 'random(2..5)*j' + definition: 'random(2..5)*imj' }, 'b': { name: 'b', @@ -2643,7 +2672,7 @@ mark: } }, constants: [ - {name: 'j', tex: 'j', value: 'sqrt(-1)'} + {name: 'imj', tex: 'j', value: 'sqrt(-1)'} ] } ]