# -*- coding: utf-8 -*- """ Tests of extended hints """ import unittest import pytest from ddt import data, ddt, unpack from xmodule.capa.tests.helpers import load_fixture, new_loncapa_problem # With the use of ddt, some of the data expected_string cases below are naturally long stretches # of text text without whitespace. I think it's best to leave such lines intact # in the test code. Therefore: # pylint: disable=line-too-long # For out many ddt data cases, prefer a compact form of { .. } class HintTest(unittest.TestCase): """Base class for tests of extended hinting functionality.""" def correctness(self, problem_id, choice): """Grades the problem and returns the 'correctness' string from cmap.""" student_answers = {problem_id: choice} cmap = self.problem.grade_answers(answers=student_answers) # pylint: disable=no-member return cmap[problem_id]["correctness"] def get_hint(self, problem_id, choice): """Grades the problem and returns its hint from cmap or the empty string.""" student_answers = {problem_id: choice} cmap = self.problem.grade_answers(answers=student_answers) # pylint: disable=no-member adict = cmap.cmap.get(problem_id) if adict: return adict["msg"] else: return "" # It is a little surprising how much more complicated TextInput is than all the other cases. @ddt class TextInputHintsTest(HintTest): """ Test Text Input Hints Test """ xml = load_fixture("extended_hints_text_input.xml") problem = new_loncapa_problem(xml) def test_tracking_log(self): """Test that the tracking log comes out right.""" self.problem.capa_block.reset_mock() self.get_hint("1_3_1", "Blue") self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_2", "trigger_type": "single", "hint_label": "Correct:", "correctness": True, "student_answer": ["Blue"], "question_type": "stringresponse", "hints": [{"text": "The red light is scattered by water molecules leaving only blue light."}], }, ) @data( { "problem_id": "1_2_1", "choice": "GermanyΩ", "expected_string": '
Answer
Incorrect:
I do not think so.Ω
', }, { "problem_id": "1_2_1", "choice": "franceΩ", "expected_string": '
Answer
Correct:
Viva la France!Ω
', }, { "problem_id": "1_2_1", "choice": "FranceΩ", "expected_string": '
Answer
Correct:
Viva la France!Ω
', }, {"problem_id": "1_2_1", "choice": "Mexico", "expected_string": ""}, { "problem_id": "1_2_1", "choice": "USAΩ", "expected_string": '
Answer
Correct:
Less well known, but yes, there is a Paris, Texas.Ω
', }, { "problem_id": "1_2_1", "choice": "usaΩ", "expected_string": '
Answer
Correct:
Less well known, but yes, there is a Paris, Texas.Ω
', }, {"problem_id": "1_2_1", "choice": "uSAxΩ", "expected_string": ""}, { "problem_id": "1_2_1", "choice": "NICKLANDΩ", "expected_string": '
Answer
Incorrect:
The country name does not end in LANDΩ
', }, { "problem_id": "1_3_1", "choice": "Blue", "expected_string": '
Answer
Correct:
The red light is scattered by water molecules leaving only blue light.
', }, {"problem_id": "1_3_1", "choice": "blue", "expected_string": ""}, {"problem_id": "1_3_1", "choice": "b", "expected_string": ""}, ) @unpack def test_text_input_hints(self, problem_id, choice, expected_string): hint = self.get_hint(problem_id, choice) assert hint == expected_string @ddt class TextInputExtendedHintsCaseInsensitive(HintTest): """Test Text Input Extended hints Case Insensitive""" xml = load_fixture("extended_hints_text_input.xml") problem = new_loncapa_problem(xml) @data( {"problem_id": "1_5_1", "choice": "abc", "expected_string": ""}, # wrong answer yielding no hint { "problem_id": "1_5_1", "choice": "A", "expected_string": '
Answer
Woo Hoo
hint1
', }, { "problem_id": "1_5_1", "choice": "a", "expected_string": '
Answer
Woo Hoo
hint1
', }, { "problem_id": "1_5_1", "choice": "B", "expected_string": '
Answer
hint2
', }, { "problem_id": "1_5_1", "choice": "b", "expected_string": '
Answer
hint2
', }, { "problem_id": "1_5_1", "choice": "C", "expected_string": '
Answer
hint4
', }, { "problem_id": "1_5_1", "choice": "c", "expected_string": '
Answer
hint4
', }, # regexp cases { "problem_id": "1_5_1", "choice": "FGGG", "expected_string": '
Answer
hint6
', }, { "problem_id": "1_5_1", "choice": "fgG", "expected_string": '
Answer
hint6
', }, ) @unpack def test_text_input_hints(self, problem_id, choice, expected_string): hint = self.get_hint(problem_id, choice) assert hint == expected_string @ddt class TextInputExtendedHintsCaseSensitive(HintTest): """Sometimes the semantics can be encoded in the class name.""" xml = load_fixture("extended_hints_text_input.xml") problem = new_loncapa_problem(xml) @data( {"problem_id": "1_6_1", "choice": "abc", "expected_string": ""}, { "problem_id": "1_6_1", "choice": "A", "expected_string": '
Answer
Correct:
hint1
', }, {"problem_id": "1_6_1", "choice": "a", "expected_string": ""}, { "problem_id": "1_6_1", "choice": "B", "expected_string": '
Answer
Correct:
hint2
', }, {"problem_id": "1_6_1", "choice": "b", "expected_string": ""}, { "problem_id": "1_6_1", "choice": "C", "expected_string": '
Answer
Incorrect:
hint4
', }, {"problem_id": "1_6_1", "choice": "c", "expected_string": ""}, # regexp cases { "problem_id": "1_6_1", "choice": "FGG", "expected_string": '
Answer
Incorrect:
hint6
', }, {"problem_id": "1_6_1", "choice": "fgG", "expected_string": ""}, ) @unpack def test_text_input_hints(self, problem_id, choice, expected_string): message_text = self.get_hint(problem_id, choice) assert message_text == expected_string @ddt class TextInputExtendedHintsCompatible(HintTest): """ Compatibility test with mixed old and new style additional_answer tags. """ xml = load_fixture("extended_hints_text_input.xml") problem = new_loncapa_problem(xml) @data( { "problem_id": "1_7_1", "choice": "A", "correct": "correct", "expected_string": '
Answer
Correct:
hint1
', }, {"problem_id": "1_7_1", "choice": "B", "correct": "correct", "expected_string": ""}, { "problem_id": "1_7_1", "choice": "C", "correct": "correct", "expected_string": '
Answer
Correct:
hint2
', }, {"problem_id": "1_7_1", "choice": "D", "correct": "incorrect", "expected_string": ""}, # check going through conversion with difficult chars {"problem_id": "1_7_1", "choice": """<&"'>""", "correct": "correct", "expected_string": ""}, ) @unpack def test_text_input_hints(self, problem_id, choice, correct, expected_string): message_text = self.get_hint(problem_id, choice) assert message_text == expected_string assert self.correctness(problem_id, choice) == correct @ddt class TextInputExtendedHintsRegex(HintTest): """ Extended hints where the answer is regex mode. """ xml = load_fixture("extended_hints_text_input.xml") problem = new_loncapa_problem(xml) @data( {"problem_id": "1_8_1", "choice": "ABwrong", "correct": "incorrect", "expected_string": ""}, { "problem_id": "1_8_1", "choice": "ABC", "correct": "correct", "expected_string": '
Answer
Correct:
hint1
', }, { "problem_id": "1_8_1", "choice": "ABBBBC", "correct": "correct", "expected_string": '
Answer
Correct:
hint1
', }, { "problem_id": "1_8_1", "choice": "aBc", "correct": "correct", "expected_string": '
Answer
Correct:
hint1
', }, { "problem_id": "1_8_1", "choice": "BBBB", "correct": "correct", "expected_string": '
Answer
Correct:
hint2
', }, { "problem_id": "1_8_1", "choice": "bbb", "correct": "correct", "expected_string": '
Answer
Correct:
hint2
', }, { "problem_id": "1_8_1", "choice": "C", "correct": "incorrect", "expected_string": '
Answer
Incorrect:
hint4
', }, { "problem_id": "1_8_1", "choice": "c", "correct": "incorrect", "expected_string": '
Answer
Incorrect:
hint4
', }, { "problem_id": "1_8_1", "choice": "D", "correct": "incorrect", "expected_string": '
Answer
Incorrect:
hint6
', }, { "problem_id": "1_8_1", "choice": "d", "correct": "incorrect", "expected_string": '
Answer
Incorrect:
hint6
', }, ) @unpack def test_text_input_hints(self, problem_id, choice, correct, expected_string): message_text = self.get_hint(problem_id, choice) assert message_text == expected_string assert self.correctness(problem_id, choice) == correct @ddt class NumericInputHintsTest(HintTest): """ This class consists of a suite of test cases to be run on the numeric input problem represented by the XML below. """ xml = load_fixture("extended_hints_numeric_input.xml") problem = new_loncapa_problem(xml) # this problem is properly constructed def test_tracking_log(self): self.get_hint("1_2_1", "1.141") self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_1", "trigger_type": "single", "hint_label": "Nice", "correctness": True, "student_answer": ["1.141"], "question_type": "numericalresponse", "hints": [{"text": "The square root of two turns up in the strangest places."}], }, ) @data( { "problem_id": "1_2_1", "choice": "1.141", "expected_string": '
Answer
Nice
The square root of two turns up in the strangest places.
', }, # additional answer { "problem_id": "1_2_1", "choice": "10", "expected_string": '
Answer
Correct:
This is an additional hint.
', }, { "problem_id": "1_3_1", "choice": "4", "expected_string": '
Answer
Correct:
Pretty easy, uh?.
', }, # should get hint, when correct via numeric-tolerance { "problem_id": "1_2_1", "choice": "1.15", "expected_string": '
Answer
Nice
The square root of two turns up in the strangest places.
', }, # when they answer wrong, nothing {"problem_id": "1_2_1", "choice": "2", "expected_string": ""}, ) @unpack def test_numeric_input_hints(self, problem_id, choice, expected_string): hint = self.get_hint(problem_id, choice) assert hint == expected_string @ddt class CheckboxHintsTest(HintTest): """ This class consists of a suite of test cases to be run on the checkbox problem represented by the XML below. """ xml = load_fixture("extended_hints_checkbox.xml") problem = new_loncapa_problem(xml) # this problem is properly constructed @data( { "problem_id": "1_2_1", "choice": ["choice_0"], "expected_string": '
Answer
Incorrect:
You are right that apple is a fruit.
You are right that mushrooms are not fruit
Remember that grape is also a fruit.
What is a camero anyway?
', }, { "problem_id": "1_2_1", "choice": ["choice_1"], "expected_string": '
Answer
Incorrect:
Remember that apple is also a fruit.
Mushroom is a fungus, not a fruit.
Remember that grape is also a fruit.
What is a camero anyway?
', }, { "problem_id": "1_2_1", "choice": ["choice_2"], "expected_string": '
Answer
Incorrect:
Remember that apple is also a fruit.
You are right that mushrooms are not fruit
You are right that grape is a fruit
What is a camero anyway?
', }, { "problem_id": "1_2_1", "choice": ["choice_3"], "expected_string": '
Answer
Incorrect:
Remember that apple is also a fruit.
You are right that mushrooms are not fruit
Remember that grape is also a fruit.
What is a camero anyway?
', }, { "problem_id": "1_2_1", "choice": ["choice_4"], "expected_string": '
Answer
Incorrect:
Remember that apple is also a fruit.
You are right that mushrooms are not fruit
Remember that grape is also a fruit.
I do not know what a Camero is but it is not a fruit.
', }, { "problem_id": "1_2_1", "choice": ["choice_0", "choice_1"], # compound "expected_string": '
Answer
Almost right
You are right that apple is a fruit, but there is one you are missing. Also, mushroom is not a fruit.
', }, { "problem_id": "1_2_1", "choice": ["choice_1", "choice_2"], # compound "expected_string": '
Answer
Incorrect:
You are right that grape is a fruit, but there is one you are missing. Also, mushroom is not a fruit.
', }, { "problem_id": "1_2_1", "choice": ["choice_0", "choice_2"], "expected_string": '
Answer
Correct:
You are right that apple is a fruit.
You are right that mushrooms are not fruit
You are right that grape is a fruit
What is a camero anyway?
', }, { "problem_id": "1_3_1", "choice": ["choice_0"], "expected_string": '
Answer
Incorrect:
No, sorry, a banana is a fruit.
You are right that mushrooms are not vegatbles
Brussel sprout is the only vegetable in this list.
', }, { "problem_id": "1_3_1", "choice": ["choice_1"], "expected_string": '
Answer
Incorrect:
poor banana.
You are right that mushrooms are not vegatbles
Brussel sprout is the only vegetable in this list.
', }, { "problem_id": "1_3_1", "choice": ["choice_2"], "expected_string": '
Answer
Incorrect:
poor banana.
Mushroom is a fungus, not a vegetable.
Brussel sprout is the only vegetable in this list.
', }, { "problem_id": "1_3_1", "choice": ["choice_3"], "expected_string": '
Answer
Correct:
poor banana.
You are right that mushrooms are not vegatbles
Brussel sprouts are vegetables.
', }, { "problem_id": "1_3_1", "choice": ["choice_0", "choice_1"], # compound "expected_string": '
Answer
Very funny
Making a banana split?
', }, { "problem_id": "1_3_1", "choice": ["choice_1", "choice_2"], "expected_string": '
Answer
Incorrect:
poor banana.
Mushroom is a fungus, not a vegetable.
Brussel sprout is the only vegetable in this list.
', }, { "problem_id": "1_3_1", "choice": ["choice_0", "choice_2"], "expected_string": '
Answer
Incorrect:
No, sorry, a banana is a fruit.
Mushroom is a fungus, not a vegetable.
Brussel sprout is the only vegetable in this list.
', }, # check for interaction between compoundhint and correct/incorrect { "problem_id": "1_4_1", "choice": ["choice_0", "choice_1"], # compound "expected_string": '
Answer
Incorrect:
AB
', }, { "problem_id": "1_4_1", "choice": ["choice_0", "choice_2"], # compound "expected_string": '
Answer
Correct:
AC
', }, # check for labeling where multiple child hints have labels # These are some tricky cases { "problem_id": "1_5_1", "choice": ["choice_0", "choice_1"], "expected_string": '
Answer
AA
aa
', }, { "problem_id": "1_5_1", "choice": ["choice_0"], "expected_string": '
Answer
Incorrect:
aa
bb
', }, {"problem_id": "1_5_1", "choice": ["choice_1"], "expected_string": ""}, { "problem_id": "1_5_1", "choice": [], "expected_string": '
Answer
BB
bb
', }, { "problem_id": "1_6_1", "choice": ["choice_0"], "expected_string": '
Answer
aa
', }, { "problem_id": "1_6_1", "choice": ["choice_0", "choice_1"], "expected_string": '
Answer
compoundo
', }, # The user selects *nothing*, but can still get "unselected" feedback { "problem_id": "1_7_1", "choice": [], "expected_string": '
Answer
Incorrect:
bb
', }, # 100% not match of sel/unsel feedback {"problem_id": "1_7_1", "choice": ["choice_1"], "expected_string": ""}, # Here we have the correct combination, and that makes feedback too { "problem_id": "1_7_1", "choice": ["choice_0"], "expected_string": '
Answer
Correct:
aa
bb
', }, ) @unpack def test_checkbox_hints(self, problem_id, choice, expected_string): self.maxDiff = None # pylint: disable=invalid-name hint = self.get_hint(problem_id, choice) assert hint == expected_string class CheckboxHintsTestTracking(HintTest): """ Test the rather complicated tracking log output for checkbox cases. """ xml = """

question

Apple A true A false Banana Cronut C true A C Compound
""" problem = new_loncapa_problem(xml) def test_tracking_log(self): """Test checkbox tracking log - by far the most complicated case""" # A -> 1 hint self.get_hint("1_2_1", ["choice_0"]) self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "hint_label": "Incorrect:", "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_1", "choice_all": ["choice_0", "choice_1", "choice_2"], "correctness": False, "trigger_type": "single", "student_answer": ["choice_0"], "hints": [{"text": "A true", "trigger": [{"choice": "choice_0", "selected": True}]}], "question_type": "choiceresponse", }, ) # B C -> 2 hints self.problem.capa_block.runtime.publish.reset_mock() self.get_hint("1_2_1", ["choice_1", "choice_2"]) self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "hint_label": "Incorrect:", "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_1", "choice_all": ["choice_0", "choice_1", "choice_2"], "correctness": False, "trigger_type": "single", "student_answer": ["choice_1", "choice_2"], "hints": [ {"text": "A false", "trigger": [{"choice": "choice_0", "selected": False}]}, {"text": "C true", "trigger": [{"choice": "choice_2", "selected": True}]}, ], "question_type": "choiceresponse", }, ) # A C -> 1 Compound hint self.problem.capa_block.runtime.publish.reset_mock() self.get_hint("1_2_1", ["choice_0", "choice_2"]) self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "hint_label": "Correct:", "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_1", "choice_all": ["choice_0", "choice_1", "choice_2"], "correctness": True, "trigger_type": "compound", "student_answer": ["choice_0", "choice_2"], "hints": [ { "text": "A C Compound", "trigger": [{"choice": "choice_0", "selected": True}, {"choice": "choice_2", "selected": True}], } ], "question_type": "choiceresponse", }, ) @ddt class MultpleChoiceHintsTest(HintTest): """ This class consists of a suite of test cases to be run on the multiple choice problem represented by the XML below. """ xml = load_fixture("extended_hints_multiple_choice.xml") problem = new_loncapa_problem(xml) def test_tracking_log(self): """Test that the tracking log comes out right.""" self.problem.capa_block.reset_mock() self.get_hint("1_3_1", "choice_2") self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_2", "trigger_type": "single", "student_answer": ["choice_2"], "correctness": False, "question_type": "multiplechoiceresponse", "hint_label": "OOPS", "hints": [{"text": "Apple is a fruit."}], }, ) @data( { "problem_id": "1_2_1", "choice": "choice_0", "expected_string": '
Answer
Mushroom is a fungus, not a fruit.
', }, {"problem_id": "1_2_1", "choice": "choice_1", "expected_string": ""}, { "problem_id": "1_3_1", "choice": "choice_1", "expected_string": '
Answer
Correct:
Potato is a root vegetable.
', }, { "problem_id": "1_2_1", "choice": "choice_2", "expected_string": '
Answer
OUTSTANDING
Apple is indeed a fruit.
', }, { "problem_id": "1_3_1", "choice": "choice_2", "expected_string": '
Answer
OOPS
Apple is a fruit.
', }, {"problem_id": "1_3_1", "choice": "choice_9", "expected_string": ""}, ) @unpack def test_multiplechoice_hints(self, problem_id, choice, expected_string): hint = self.get_hint(problem_id, choice) assert hint == expected_string @ddt class MultpleChoiceHintsWithHtmlTest(HintTest): """ This class consists of a suite of test cases to be run on the multiple choice problem represented by the XML below. """ xml = load_fixture("extended_hints_multiple_choice_with_html.xml") problem = new_loncapa_problem(xml) def test_tracking_log(self): """Test that the tracking log comes out right.""" self.problem.capa_block.reset_mock() self.get_hint("1_2_1", "choice_0") self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_1", "trigger_type": "single", "student_answer": ["choice_0"], "correctness": False, "question_type": "multiplechoiceresponse", "hint_label": "Incorrect:", "hints": [{"text": 'Mushroom is a fungus, not a fruit.'}], }, ) @data( { "problem_id": "1_2_1", "choice": "choice_0", "expected_string": '
Answer
Incorrect:
Mushroom is a fungus, not a fruit.
', }, { "problem_id": "1_2_1", "choice": "choice_1", "expected_string": '
Answer
Incorrect:
Potato is not a fruit.
', }, { "problem_id": "1_2_1", "choice": "choice_2", "expected_string": '
Answer
Correct:
Apple is a fruit.
', }, ) @unpack def test_multiplechoice_hints(self, problem_id, choice, expected_string): hint = self.get_hint(problem_id, choice) assert hint == expected_string @ddt class DropdownHintsTest(HintTest): """ This class consists of a suite of test cases to be run on the drop down problem represented by the XML below. """ xml = load_fixture("extended_hints_dropdown.xml") problem = new_loncapa_problem(xml) def test_tracking_log(self): """Test that the tracking log comes out right.""" self.problem.capa_block.reset_mock() self.get_hint("1_3_1", "FACES") self.problem.capa_block.runtime.publish.assert_called_with( self.problem.capa_block, "edx.problem.hint.feedback_displayed", { "module_id": "i4x://Foo/bar/mock/abc", "problem_part_id": "1_2", "trigger_type": "single", "student_answer": ["FACES"], "correctness": True, "question_type": "optionresponse", "hint_label": "Correct:", "hints": [{"text": "With lots of makeup, doncha know?"}], }, ) @data( { "problem_id": "1_2_1", "choice": "Multiple Choice", "expected_string": '
Answer
Good Job
Yes, multiple choice is the right answer.
', }, { "problem_id": "1_2_1", "choice": "Text Input", "expected_string": '
Answer
Incorrect:
No, text input problems do not present options.
', }, { "problem_id": "1_2_1", "choice": "Numerical Input", "expected_string": '
Answer
Incorrect:
No, numerical input problems do not present options.
', }, { "problem_id": "1_3_1", "choice": "FACES", "expected_string": '
Answer
Correct:
With lots of makeup, doncha know?
', }, { "problem_id": "1_3_1", "choice": "dogs", "expected_string": '
Answer
NOPE
Not dogs, not cats, not toads
', }, {"problem_id": "1_3_1", "choice": "wrongo", "expected_string": ""}, # Regression case where feedback includes answer substring { "problem_id": "1_4_1", "choice": "AAA", "expected_string": '
Answer
Incorrect:
AAABBB1
', }, { "problem_id": "1_4_1", "choice": "BBB", "expected_string": '
Answer
Correct:
AAABBB2
', }, {"problem_id": "1_4_1", "choice": "not going to match", "expected_string": ""}, ) @unpack def test_dropdown_hints(self, problem_id, choice, expected_string): hint = self.get_hint(problem_id, choice) assert hint == expected_string class ErrorConditionsTest(HintTest): """ Erroneous xml should raise exception. """ def test_error_conditions_illegal_element(self): xml_with_errors = load_fixture("extended_hints_with_errors.xml") with pytest.raises(Exception): new_loncapa_problem(xml_with_errors) # this problem is improperly constructed