Commit f62dad2f by Will Daly Committed by Ned Batchelder

Added symbolic response tests

parent e61a6fe7
......@@ -46,7 +46,7 @@ import sys
import pyparsing
from .registry import TagRegistry
from capa.chem import chemcalc
from chem import chemcalc
import xqueue_interface
from datetime import datetime
......
......@@ -29,7 +29,7 @@ from collections import namedtuple
from shapely.geometry import Point, MultiPoint
# specific library imports
from .calc import evaluator, UndefinedVariable
from calc import evaluator, UndefinedVariable
from .correctmap import CorrectMap
from datetime import datetime
from .util import *
......@@ -1043,7 +1043,7 @@ class CustomResponse(LoncapaResponse):
messages = self.context['messages']
correct_map = CorrectMap()
overall_message = self.clean_message_html(self.context['overall_message']))
overall_message = self.clean_message_html(self.context['overall_message'])
correct_map.set_overall_message(overall_message)
for k in range(len(idset)):
......@@ -1195,12 +1195,24 @@ class SymbolicResponse(CustomResponse):
"""
response_tag = 'symbolicresponse'
max_inputfields = 1
def setup_response(self):
# Symbolic response always uses symmath_check()
# If the XML did not specify this, then set it now
# Otherwise, we get an error from the superclass
self.xml.set('cfn', 'symmath_check')
# Let CustomResponse do its setup
super(SymbolicResponse, self).setup_response()
def execute_check_function(self, idset, submission):
from symmath import symmath_check
fn = self.code
try:
answer_given = submission[0] if (len(idset) == 1) else submission
# Since we have limited max_inputfields to 1,
# we can assume that there is only one submission
answer_given = submission[0]
ret = symmath_check(
self.expect, answer_given,
dynamath=self.context.get('dynamath'),
......
......@@ -734,3 +734,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
option_element.text = description
return input_element
class SymbolicResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <symbolicresponse> xml """
def create_response_element(self, **kwargs):
""" Build the <symbolicresponse> XML element.
Uses **kwargs:
*expect*: The correct answer (a sympy string)
*options*: list of option strings to pass to symmath_check
(e.g. 'matrix', 'qbit', 'imaginary', 'numerical')"""
# Retrieve **kwargs
expect = kwargs.get('expect', '')
options = kwargs.get('options', [])
# Symmath check expects a string of options
options_str = ",".join(options)
# Construct the <symbolicresponse> element
response_element = etree.Element('symbolicresponse')
if expect:
response_element.set('expect', str(expect))
if options_str:
response_element.set('options', str(options_str))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
......@@ -10,6 +10,7 @@ import random
import unittest
import textwrap
import mock
import textwrap
from . import test_system
......@@ -184,44 +185,85 @@ class ImageResponseTest(ResponseTest):
self.assert_answer_format(problem)
class SymbolicResponseTest(unittest.TestCase):
class SymbolicResponseTest(ResponseTest):
from response_xml_factory import SymbolicResponseXMLFactory
xml_factory_class = SymbolicResponseXMLFactory
def test_symbolic_response_grade(self):
symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml"
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
'1_2_1_dynamath': '''
def test_grade_single_input(self):
problem = self.build_problem(math_display=True,
expect="2*x+3*y")
# Correct answers
correct_inputs = [
('2x+3y', textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>2</mn><mo>*</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
</mstyle></math>""")),
('x+x+3y', textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mi>x</mi><mo>+</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
</mstyle></math>""")),
]
for (input_str, input_mathml) in correct_inputs:
self._assert_symbolic_grade(problem, input_str, input_mathml, 'correct')
# Incorrect answers
incorrect_inputs = [
('0', ''),
('4x+3y', textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>4</mn><mo>*</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
</mstyle></math>""")),
]
for (input_str, input_mathml) in incorrect_inputs:
self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect')
def test_complex_number_grade(self):
problem = self.build_problem(math_display=True,
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
options=["matrix", "imaginary"])
# For LaTeX-style inputs, symmath_check() will try to contact
# a server to convert the input to MathML.
# We mock out the server, simulating the response that it would give
# for this input.
import requests
dirpath = os.path.dirname(__file__)
correct_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_correct.html")).read().decode('utf8')
wrong_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_wrong.html")).read().decode('utf8')
# Correct answer
with mock.patch.object(requests, 'post') as mock_post:
# Simulate what the LaTeX-to-MathML server would
# send for the correct response input
mock_post.return_value.text = correct_snuggletex_response
self._assert_symbolic_grade(problem,
"cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]",
textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>cos</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
<mrow><mo>(</mo><mi>&#x3B8;</mi><mo>)</mo></mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
<mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
</mtr>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
<mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
</mtr>
</mtable>
<mo>]</mo>
......@@ -232,9 +274,7 @@ class SymbolicResponseTest(unittest.TestCase):
<mrow>
<mi>sin</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
<mo>(</mo><mi>&#x3B8;</mi><mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
......@@ -242,49 +282,54 @@ class SymbolicResponseTest(unittest.TestCase):
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
<mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
</mtr>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
<mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</mstyle>
</math>
''',
}
wrong_answers = {'1_2_1': '2',
'1_2_1_dynamath': '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>2</mn>
</mstyle>
</math>''',
}
import requests
d = os.path.dirname(__file__)
correct_snuggletex_response = open(os.path.join(d, "test_files/snuggletex_correct.html")).read().decode('utf8')
wrong_snuggletex_response = open(os.path.join(d, "test_files/snuggletex_wrong.html")).read().decode('utf8')
"""),
'correct')
# Incorrect answer
with mock.patch.object(requests, 'post') as mock_post:
mock_post.return_value.text = correct_snuggletex_response
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
with mock.patch.object(requests, 'post') as mock_post:
# Simulate what the LaTeX-to-MathML server would
# send for the incorrect response input
mock_post.return_value.text = wrong_snuggletex_response
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
self._assert_symbolic_grade(problem, "2",
textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true"><mn>2</mn></mstyle>
</math>
"""),
'incorrect')
def test_multiple_inputs_exception(self):
# Should not allow multiple inputs, since we specify
# only one "expect" value
with self.assertRaises(Exception):
problem = self.build_problem(math_display=True,
expect="2*x+3*y",
num_inputs=3)
def _assert_symbolic_grade(self, problem,
student_input,
dynamath_input,
expected_correctness):
input_dict = {'1_2_1': str(student_input),
'1_2_1_dynamath': str(dynamath_input) }
correct_map = problem.grade_answers(input_dict)
self.assertEqual(correct_map.get_correctness('1_2_1'),
expected_correctness)
class OptionResponseTest(ResponseTest):
......
from .calc import evaluator, UndefinedVariable
from calc import evaluator
#-----------------------------------------------------------------------------
#
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment