# -*- 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