Implemented unit tests for <customresponse>
This commit is contained in:
184
common/lib/capa/capa/tests/response_xml_factory.py
Normal file
184
common/lib/capa/capa/tests/response_xml_factory.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from lxml import etree
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
class ResponseXMLFactory(object):
|
||||
""" Abstract base class for capa response XML factories.
|
||||
Subclasses override create_response_element and
|
||||
create_input_element to produce XML of particular response types"""
|
||||
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Subclasses override to return an etree element
|
||||
representing the capa response XML
|
||||
(e.g. <numericalresponse>).
|
||||
|
||||
The tree should NOT contain any input elements
|
||||
(such as <textline />) as these will be added later."""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def create_input_element(self, **kwargs):
|
||||
""" Subclasses override this to return an etree element
|
||||
representing the capa input XML (such as <textline />)"""
|
||||
return None
|
||||
|
||||
def build_xml(self, **kwargs):
|
||||
""" Construct an XML string for a capa response
|
||||
based on **kwargs.
|
||||
|
||||
**kwargs is a dictionary that will be passed
|
||||
to create_response_element() and create_input_element().
|
||||
See the subclasses below for other keyword arguments
|
||||
you can specify.
|
||||
|
||||
For all response types, **kwargs can contain:
|
||||
|
||||
'question_text': The text of the question to display,
|
||||
wrapped in <p> tags.
|
||||
|
||||
'explanation_text': The detailed explanation that will
|
||||
be shown if the user answers incorrectly.
|
||||
|
||||
'script': The embedded Python script (a string)
|
||||
|
||||
'num_inputs': The number of input elements
|
||||
to create [DEFAULT: 1]
|
||||
|
||||
Returns a string representation of the XML tree.
|
||||
"""
|
||||
|
||||
# Retrieve keyward arguments
|
||||
question_text = kwargs.get('question_text', '')
|
||||
explanation_text = kwargs.get('explanation_text')
|
||||
script = kwargs.get('script', None)
|
||||
num_inputs = kwargs.get('num_inputs', 1)
|
||||
|
||||
# The root is <problem>
|
||||
root = etree.Element("problem")
|
||||
|
||||
# Add a script if there is one
|
||||
if script:
|
||||
script_element = etree.SubElement(root, "script")
|
||||
script_element.set("type", "loncapa/python")
|
||||
script_element.text = str(script)
|
||||
|
||||
# The problem has a child <p> with question text
|
||||
question = etree.SubElement(root, "p")
|
||||
question.text = question_text
|
||||
|
||||
# Add the response
|
||||
response_element = self.create_response_element(**kwargs)
|
||||
root.append(response_element)
|
||||
|
||||
# Add input elements
|
||||
for i in range(0, int(num_inputs)):
|
||||
input_element = self.create_input_element(**kwargs)
|
||||
response_element.append(input_element)
|
||||
|
||||
# The problem has an explanation of the solution
|
||||
explanation = etree.SubElement(root, "solution")
|
||||
explanation_div = etree.SubElement(explanation, "div")
|
||||
explanation_div.set("class", "detailed-solution")
|
||||
explanation_div.text = explanation_text
|
||||
|
||||
return etree.tostring(root)
|
||||
|
||||
@staticmethod
|
||||
def textline_input_xml(**kwargs):
|
||||
""" Create a <textline/> XML element
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
'math_display': If True, then includes a MathJax display of user input
|
||||
|
||||
'size': An integer representing the width of the text line
|
||||
"""
|
||||
math_display = kwargs.get('math_display', False)
|
||||
size = kwargs.get('size', None)
|
||||
|
||||
input_element = etree.Element('textline')
|
||||
|
||||
if math_display:
|
||||
input_element.set('math', '1')
|
||||
|
||||
if size:
|
||||
input_element.set('size', str(size))
|
||||
|
||||
return input_element
|
||||
|
||||
class NumericalResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <numericalresponse> XML trees """
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create a <numericalresponse> XML element.
|
||||
Uses **kwarg keys:
|
||||
|
||||
'answer': The correct answer (e.g. "5")
|
||||
|
||||
'tolerance': The tolerance within which a response
|
||||
is considered correct. Can be a decimal (e.g. "0.01")
|
||||
or percentage (e.g. "2%")
|
||||
"""
|
||||
|
||||
answer = kwargs.get('answer', None)
|
||||
tolerance = kwargs.get('tolerance', None)
|
||||
|
||||
response_element = etree.Element('numericalresponse')
|
||||
|
||||
if answer:
|
||||
response_element.set('answer', str(answer))
|
||||
|
||||
if tolerance:
|
||||
responseparam_element = etree.SubElement(response_element, 'responseparam')
|
||||
responseparam_element.set('type', 'tolerance')
|
||||
responseparam_element.set('default', str(tolerance))
|
||||
|
||||
return response_element
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
return ResponseXMLFactory.textline_input_xml(**kwargs)
|
||||
|
||||
class CustomResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <customresponse> XML trees """
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create a <customresponse> XML element.
|
||||
|
||||
Use **kwargs:
|
||||
|
||||
'cfn': the Python code to run. Can be inline code,
|
||||
or the name of a function defined in earlier <script> tags.
|
||||
|
||||
Should have the form: cfn(expect, ans)
|
||||
where expect is a value (see below)
|
||||
and ans is a list of values.
|
||||
|
||||
'expect': The value passed as the first argument to the function cfn
|
||||
|
||||
'answer': Inline script that calculates the answer
|
||||
"""
|
||||
|
||||
# Retrieve **kwargs
|
||||
cfn = kwargs.get('cfn', None)
|
||||
expect = kwargs.get('expect', None)
|
||||
answer = kwargs.get('answer', None)
|
||||
|
||||
# Create the response element
|
||||
response_element = etree.Element("customresponse")
|
||||
|
||||
if cfn:
|
||||
response_element.set('cfn', str(cfn))
|
||||
|
||||
if expect:
|
||||
response_element.set('expect', str(expect))
|
||||
|
||||
if answer:
|
||||
answer_element = etree.SubElement(response_element, "answer")
|
||||
answer_element.text = str(answer)
|
||||
|
||||
return response_element
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
return ResponseXMLFactory.textline_input_xml(**kwargs)
|
||||
@@ -493,3 +493,119 @@ class NumericalResponseTest(unittest.TestCase):
|
||||
for input_str in incorrect_answers:
|
||||
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
|
||||
self.assertEqual(result, 'incorrect')
|
||||
|
||||
from response_xml_factory import CustomResponseXMLFactory
|
||||
class CustomResponseTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.xml_factory = CustomResponseXMLFactory()
|
||||
|
||||
def test_inline_code(self):
|
||||
|
||||
# For inline code, we directly modify global context variables
|
||||
# 'answers' is a list of answers provided to us
|
||||
# 'correct' is a list we fill in with True/False
|
||||
# 'expect' is given to us (if provided in the XML)
|
||||
inline_script = """correct[0] = 'correct' if (answers['1_2_1'] == expect) else 'incorrect'"""
|
||||
|
||||
xml = self.xml_factory.build_xml(answer=inline_script, expect="42")
|
||||
|
||||
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
|
||||
|
||||
# Correct answer
|
||||
input_dict = {'1_2_1': '42'}
|
||||
result = problem.grade_answers(input_dict).get_correctness('1_2_1')
|
||||
self.assertEqual(result, 'correct')
|
||||
|
||||
# Incorrect answer
|
||||
input_dict = {'1_2_1': '0'}
|
||||
result = problem.grade_answers(input_dict).get_correctness('1_2_1')
|
||||
self.assertEqual(result, 'incorrect')
|
||||
|
||||
def test_inline_message(self):
|
||||
|
||||
# Inline code can update the global messages list
|
||||
# to pass messages to the CorrectMap for a particular input
|
||||
inline_script = """messages[0] = "Test Message" """
|
||||
|
||||
xml = self.xml_factory.build_xml(answer=inline_script)
|
||||
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
|
||||
|
||||
input_dict = {'1_2_1': '0'}
|
||||
msg = problem.grade_answers(input_dict).get_msg('1_2_1')
|
||||
self.assertEqual(msg, "Test Message")
|
||||
|
||||
def test_function_code(self):
|
||||
|
||||
# For function code, we pass in three arguments:
|
||||
#
|
||||
# 'expect' is the expect attribute of the <customresponse>
|
||||
#
|
||||
# 'answer_given' is the answer the student gave (if there is just one input)
|
||||
# or an ordered list of answers (if there are multiple inputs)
|
||||
#
|
||||
# 'student_answers' is a dictionary of answers by input ID
|
||||
#
|
||||
#
|
||||
# The function should return a dict of the form
|
||||
# { 'ok': BOOL, 'msg': STRING }
|
||||
#
|
||||
script = """def check_func(expect, answer_given, student_answers):
|
||||
return {'ok': answer_given == expect, 'msg': 'Message text'}"""
|
||||
|
||||
xml = self.xml_factory.build_xml(script=script, cfn="check_func", expect="42")
|
||||
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
|
||||
|
||||
# Correct answer
|
||||
input_dict = {'1_2_1': '42'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
correctness = correct_map.get_correctness('1_2_1')
|
||||
msg = correct_map.get_msg('1_2_1')
|
||||
|
||||
self.assertEqual(correctness, 'correct')
|
||||
self.assertEqual(msg, "Message text\n")
|
||||
|
||||
# Incorrect answer
|
||||
input_dict = {'1_2_1': '0'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
correctness = correct_map.get_correctness('1_2_1')
|
||||
msg = correct_map.get_msg('1_2_1')
|
||||
|
||||
self.assertEqual(correctness, 'incorrect')
|
||||
self.assertEqual(msg, "Message text\n")
|
||||
|
||||
def test_multiple_inputs(self):
|
||||
# When given multiple inputs, the 'answer_given' argument
|
||||
# to the check_func() is a list of inputs
|
||||
# The sample script below marks the problem as correct
|
||||
# if and only if it receives answer_given=[1,2,3]
|
||||
# (or string values ['1','2','3'])
|
||||
script = """def check_func(expect, answer_given, student_answers):
|
||||
check1 = (int(answer_given[0]) == 1)
|
||||
check2 = (int(answer_given[1]) == 2)
|
||||
check3 = (int(answer_given[2]) == 3)
|
||||
return {'ok': (check1 and check2 and check3), 'msg': 'Message text'}"""
|
||||
|
||||
xml = self.xml_factory.build_xml(script=script,
|
||||
cfn="check_func", num_inputs=3)
|
||||
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
|
||||
|
||||
# Grade the inputs (one input incorrect)
|
||||
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
# Everything marked incorrect
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertEqual(correct_map.get_correctness('1_2_2'), 'incorrect')
|
||||
self.assertEqual(correct_map.get_correctness('1_2_3'), 'incorrect')
|
||||
|
||||
# Grade the inputs (everything correct)
|
||||
input_dict = {'1_2_1': '1', '1_2_2': '2', '1_2_3': '3' }
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
# Everything marked incorrect
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
|
||||
self.assertEqual(correct_map.get_correctness('1_2_2'), 'correct')
|
||||
self.assertEqual(correct_map.get_correctness('1_2_3'), 'correct')
|
||||
|
||||
Reference in New Issue
Block a user