Commit f62dad2f by Will Daly Committed by Ned Batchelder

Added symbolic response tests

parent e61a6fe7
...@@ -46,7 +46,7 @@ import sys ...@@ -46,7 +46,7 @@ import sys
import pyparsing import pyparsing
from .registry import TagRegistry from .registry import TagRegistry
from capa.chem import chemcalc from chem import chemcalc
import xqueue_interface import xqueue_interface
from datetime import datetime from datetime import datetime
......
...@@ -29,7 +29,7 @@ from collections import namedtuple ...@@ -29,7 +29,7 @@ from collections import namedtuple
from shapely.geometry import Point, MultiPoint from shapely.geometry import Point, MultiPoint
# specific library imports # specific library imports
from .calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from .correctmap import CorrectMap from .correctmap import CorrectMap
from datetime import datetime from datetime import datetime
from .util import * from .util import *
...@@ -1043,7 +1043,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1043,7 +1043,7 @@ class CustomResponse(LoncapaResponse):
messages = self.context['messages'] messages = self.context['messages']
correct_map = CorrectMap() 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) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
...@@ -1195,12 +1195,24 @@ class SymbolicResponse(CustomResponse): ...@@ -1195,12 +1195,24 @@ class SymbolicResponse(CustomResponse):
""" """
response_tag = 'symbolicresponse' 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): def execute_check_function(self, idset, submission):
from symmath import symmath_check from symmath import symmath_check
fn = self.code
try: 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( ret = symmath_check(
self.expect, answer_given, self.expect, answer_given,
dynamath=self.context.get('dynamath'), dynamath=self.context.get('dynamath'),
......
...@@ -734,3 +734,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): ...@@ -734,3 +734,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
option_element.text = description option_element.text = description
return input_element 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 ...@@ -10,6 +10,7 @@ import random
import unittest import unittest
import textwrap import textwrap
import mock import mock
import textwrap
from . import test_system from . import test_system
...@@ -184,44 +185,85 @@ class ImageResponseTest(ResponseTest): ...@@ -184,44 +185,85 @@ class ImageResponseTest(ResponseTest):
self.assert_answer_format(problem) self.assert_answer_format(problem)
class SymbolicResponseTest(unittest.TestCase): class SymbolicResponseTest(ResponseTest):
from response_xml_factory import SymbolicResponseXMLFactory from response_xml_factory import SymbolicResponseXMLFactory
xml_factory_class = SymbolicResponseXMLFactory xml_factory_class = SymbolicResponseXMLFactory
def test_symbolic_response_grade(self): def test_grade_single_input(self):
symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml" problem = self.build_problem(math_display=True,
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system) expect="2*x+3*y")
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
'1_2_1_dynamath': ''' # 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"> <math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true"> <mstyle displaystyle="true">
<mrow> <mrow>
<mi>cos</mi> <mi>cos</mi>
<mrow> <mrow><mo>(</mo><mi>&#x3B8;</mi><mo>)</mo></mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow> </mrow>
<mo>&#x22C5;</mo> <mo>&#x22C5;</mo>
<mrow> <mrow>
<mo>[</mo> <mo>[</mo>
<mtable> <mtable>
<mtr> <mtr>
<mtd> <mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr> </mtr>
<mtr> <mtr>
<mtd> <mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr> </mtr>
</mtable> </mtable>
<mo>]</mo> <mo>]</mo>
...@@ -232,9 +274,7 @@ class SymbolicResponseTest(unittest.TestCase): ...@@ -232,9 +274,7 @@ class SymbolicResponseTest(unittest.TestCase):
<mrow> <mrow>
<mi>sin</mi> <mi>sin</mi>
<mrow> <mrow>
<mo>(</mo> <mo>(</mo><mi>&#x3B8;</mi><mo>)</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow> </mrow>
</mrow> </mrow>
<mo>&#x22C5;</mo> <mo>&#x22C5;</mo>
...@@ -242,49 +282,54 @@ class SymbolicResponseTest(unittest.TestCase): ...@@ -242,49 +282,54 @@ class SymbolicResponseTest(unittest.TestCase):
<mo>[</mo> <mo>[</mo>
<mtable> <mtable>
<mtr> <mtr>
<mtd> <mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr> </mtr>
<mtr> <mtr>
<mtd> <mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr> </mtr>
</mtable> </mtable>
<mo>]</mo> <mo>]</mo>
</mrow> </mrow>
</mstyle> </mstyle>
</math> </math>
''', """),
} 'correct')
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')
# Incorrect answer
with mock.patch.object(requests, 'post') as mock_post: 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 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): 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