diff --git a/.gitignore b/.gitignore index 32a7edc50..fdb959406 100644 --- a/.gitignore +++ b/.gitignore @@ -45,5 +45,8 @@ bower_componenets/ # Files produced by runestone builds. runestone/build_info runestone/*/test/build_info +**/sphinx_settings.json +# IDEs .vscode/ +**/sphinx-enki-info.txt diff --git a/runestone/assess/assessbase.py b/runestone/assess/assessbase.py index 42d30496b..dbbbe60aa 100644 --- a/runestone/assess/assessbase.py +++ b/runestone/assess/assessbase.py @@ -15,7 +15,7 @@ # __author__ = 'bmiller' -from runestone.common.runestonedirective import RunestoneDirective, RunestoneNode +from runestone.common.runestonedirective import RunestoneDirective _base_js_escapes = ( ('\\', r'\u005C'), @@ -71,17 +71,16 @@ def getNumber(self): def run(self): - self.options['qnumber'] = self.getNumber() self.options['divid'] = self.arguments[0] - if self.content[0][:2] == '..': # first line is a directive - self.content[0] = self.options['qnumber'] + ': \n\n' + self.content[0] - else: - self.content[0] = self.options['qnumber'] + ': ' + self.content[0] - if self.content: + if self.content[0][:2] == '..': # first line is a directive + self.content[0] = self.options['qnumber'] + ': \n\n' + self.content[0] + else: + self.content[0] = self.options['qnumber'] + ': ' + self.content[0] + if 'iscode' in self.options: self.options['bodytext'] = '
' + "\n".join(self.content) + '
' else: diff --git a/runestone/assess/js/mchoice.js b/runestone/assess/js/mchoice.js index d3ed6f598..f27d53150 100644 --- a/runestone/assess/js/mchoice.js +++ b/runestone/assess/js/mchoice.js @@ -194,7 +194,7 @@ MultipleChoice.prototype.renderMCFormOpts = function () { label.appendChild(input); label.appendChild(labelspan); //$(label).attr("for", optid); - $(labelspan).html(this.answerList[k].content); + $(labelspan).html(String.fromCharCode(65 + j) + '. ' + this.answerList[k].content); // create the object to store in optionArray var optObj = { @@ -369,7 +369,7 @@ MultipleChoice.prototype.getSubmittedOpts = function () { if (buttonObjs[i].checked) { given = buttonObjs[i].value; this.givenArray.push(given); - this.feedbackString += given + ": " + this.feedbackList[i] + "
"; + this.feedbackString += '
  • ' + this.feedbackList[i] + "
  • "; this.givenlog += given + ","; this.singlefeedback = this.feedbackList[i]; } @@ -416,12 +416,12 @@ MultipleChoice.prototype.renderMCMAFeedBack = function () { var feedbackText = this.feedbackString; if (numCorrect === numNeeded && numNeeded === numGiven) { - $(this.feedBackDiv).html("Correct!
    " + feedbackText); + $(this.feedBackDiv).html('Correct.
      ' + feedbackText + ""); $(this.feedBackDiv).attr("class", "alert alert-success"); } else { $(this.feedBackDiv).html("Incorrect. " + "You gave " + numGiven + " " + answerStr + " and got " + numCorrect + " correct of " + - numNeeded + " needed.
      " + feedbackText); + numNeeded + ' needed.
        ' + feedbackText + ""); $(this.feedBackDiv).attr("class", "alert alert-danger"); } }; @@ -459,13 +459,13 @@ MultipleChoice.prototype.logMCMFsubmission = function () { MultipleChoice.prototype.renderMCMFFeedback = function (correct, feedbackText) { if (correct) { - $(this.feedBackDiv).html("Correct! " + feedbackText); + $(this.feedBackDiv).html(feedbackText); $(this.feedBackDiv).attr("class", "alert alert-success"); } else { if (feedbackText == null) { feedbackText = ""; } - $(this.feedBackDiv).html("Incorrect. " + feedbackText); + $(this.feedBackDiv).html(feedbackText); $(this.feedBackDiv).attr("class", "alert alert-danger"); } }; diff --git a/runestone/assess/multiplechoice.py b/runestone/assess/multiplechoice.py index 0aaaefad7..d862d1b80 100644 --- a/runestone/assess/multiplechoice.py +++ b/runestone/assess/multiplechoice.py @@ -17,7 +17,8 @@ from docutils import nodes from docutils.parsers.rst import directives -from .assessbase import * +from .assessbase import Assessment +from runestone.common.runestonedirective import RunestoneNode, get_node_line from runestone.server.componentdb import addQuestionToDB, addHTMLToDB @@ -61,7 +62,7 @@ def depart_mc_node(self,node): if 'answer_' in k: x,label = k.split('_') node.mc_options['alabel'] = label - node.mc_options['atext'] = "(" +k[-1].upper() + ") " + node.mc_options[k] + node.mc_options['atext'] = node.mc_options[k] currFeedback = "feedback_" + label node.mc_options['feedtext'] = node.mc_options.get(currFeedback,"") #node.mc_options[currFeedback] if label in node.mc_options['correct']: @@ -94,8 +95,8 @@ class MChoice(Assessment): The syntax for a multiple-choice question is: .. mchoice:: uniqueid - :multiple_answers: boolean [optional]. Implied if ``:correct:`` contains a list. - :random: boolean [optional] + :multiple_answers: [optional]. Implied if ``:correct:`` contains a list. + :random: [optional] The following arguments supply answers and feedback. See below for an alternative method of specification. @@ -120,7 +121,7 @@ class MChoice(Assessment): .. - - +Text for answer A. The leading ``+`` indicates this answer is correct. Prefix all correct answers with a ``+``. + - CText for answer A. The leading ``C`` indicates this answer is correct. Prefix all correct answers with a ``C``. Your text may be multiple paragraphs, including `images `_ and any other `inline `_ or block markup. For example: :math:`\sqrt(2)/2`. As earlier, if your feedback contains an unordered list, end it with a comment. @@ -134,10 +135,10 @@ class MChoice(Assessment): This may also span multiple paragraphs and include any markup. However, there can be only one item in this unordered list. - - \+Text for answer B. This answer is incorrect, instead showing how to display a ``+`` at the beginning of an answer without marking it as a correct answer. + - \CText for answer B. This answer is incorrect, instead showing how to display a ``C`` at the beginning of an answer without marking it as a correct answer. - Feedback for answer B. - - Text for answer C. This answer is also incorrect. Note that the empty line between a sublist and a list may be omitted. + - C ``Text`` for answer C. This answer is correct. Note that the empty line between a sublist and a list may be omitted. Placing a space after the ``C`` allows the following text to be treated as an `inline literal `_. - Feedback for answer C. However, the empty line is required between a list and a sublist. @@ -149,7 +150,7 @@ class MChoice(Assessment): optional_arguments = 1 final_argument_whitespace = True has_content = True - option_spec = RunestoneDirective.option_spec.copy() + option_spec = Assessment.option_spec.copy() option_spec.update({'answer_a':directives.unchanged, 'answer_b':directives.unchanged, 'answer_c':directives.unchanged, @@ -223,7 +224,7 @@ def run(self): # ...and so on... # # See if the last item is a list. If so, and questions/answers weren't specified as options, assume it contains questions and answers. - answers_bullet_list = mcNode[-1] + answers_bullet_list = mcNode[-1] if len(mcNode) else None if isinstance(answers_bullet_list, nodes.bullet_list) and ('answer_a' not in self.options and ('correct' not in self.options)): # Accumulate the correct answers. correct_answers = [] @@ -232,7 +233,7 @@ def run(self): for answer_list_item in answers_bullet_list: assert isinstance(answer_list_item, nodes.list_item) - # Look for a correct answer: An initial ``+``. In this case, the expected structure is: + # Look for a correct answer: An initial ``C``. In this case, the expected structure is: # # .. code-block:: # :number-lines: @@ -244,19 +245,20 @@ def run(self): possible_paragraph = answer_list_item[0] if isinstance(possible_paragraph, nodes.paragraph): possible_Text = possible_paragraph[0] - if isinstance(possible_Text, nodes.Text) and possible_Text.rawsource.startswith('+'): + if isinstance(possible_Text, nodes.Text) and possible_Text.rawsource.strip().startswith('C'): # This is a correct answer. # # Remove the +. While a simple statement like ``possible_Text.rawsource = possible_Text.rawsource[1:]`` might seem the right approach, it doesn't work: the ``__new__`` method for Text nodes does something weird. - possible_paragraph[0] = nodes.Text(possible_Text.rawsource[1:]) + possible_paragraph[0] = nodes.Text(possible_Text.rawsource.replace('C', '', 1)) # Record this in the list of correct answers. correct_answers.append(chr(answer_list_item.parent.index(answer_list_item) + ord('a'))) # Look for the feedback for this answer -- the last child of this answer list item. feedback_bullet_list = answer_list_item[-1] - assert isinstance(feedback_bullet_list, nodes.bullet_list) - # It should have just one item (the feedback itself). - assert len(feedback_bullet_list) == 1 + if ((not isinstance(feedback_bullet_list, nodes.bullet_list) or + # It should have just one item (the feedback itself). + (len(feedback_bullet_list) != 1))): + raise self.error('On line {}, a single-item list must be nested under each answer.'.format(get_node_line(feedback_bullet_list))) # Change the feedback list item (which is currently a generic list_item) to our special node class (a FeedbackListItem). feedback_list_item = feedback_bullet_list[0] @@ -274,6 +276,9 @@ def run(self): # Store the correct answers. self.options['correct'] = ','.join(correct_answers) + # Check that a correct answer was provided. + if not self.options.get('correct'): + raise self.error('No correct answer specified.') return [mcNode] @@ -314,14 +319,13 @@ def visit_answer_list_item(self, node): label = chr(node.parent.index(node) + ord('a')) # Update dict for formatting the HTML. mcNode.mc_options['alabel'] = label - mcNode.mc_options['letter'] = label.upper() if label in mcNode.mc_options['correct']: mcNode.mc_options['is_correct'] = 'data-correct' else: mcNode.mc_options['is_correct'] = '' # Format the HTML. - self.body.append('
      1. (%(letter)s) ' % mcNode.mc_options) + self.body.append('
      2. ' % mcNode.mc_options) # Although the feedback for an answer is given as a sublist, the HTML is just a list. So, let the feedback list item close this list. def depart_answer_list_item(self, node): diff --git a/runestone/assess/test/_sources/index.rst b/runestone/assess/test/_sources/index.rst index 5b02847b0..ad454f89c 100644 --- a/runestone/assess/test/_sources/index.rst +++ b/runestone/assess/test/_sources/index.rst @@ -2,21 +2,10 @@ Testing: Multiple Choice and Multiple Answer Questions ====================================================== -.. Here is were you specify the content and order of your new book. - -.. Each section heading (e.g. "SECTION 1: A Random Section") will be - a heading in the table of contents. Source files that should be - generated and included in that section should be placed on individual - lines, with one line separating the first source filename and the - :maxdepth: line. - -.. Sources can also be included from subfolders of this directory. - (e.g. "DataStructures/queues.rst"). - - Multiple Answer ---------------- - +=============== +Old style +--------- .. mchoice:: question1 :multiple_answers: :correct: a, c @@ -31,6 +20,97 @@ Multiple Answer Which colors might be found in a rainbow (check all)? +New style +--------- +.. mchoice:: question1_new + + Which colors might be found in a rainbow (check all)? + + - Cred + + - Red it is. + + - brown + + - Not brown. + + - Cblue + + - Blue it is. + + - gray + + - Not gray. + + +Test error handling +^^^^^^^^^^^^^^^^^^^ +.. mchoice:: error1_no_content + +.. mchoice:: error2 + + No list is provided. + +.. mchoice:: error3 + + A list with missing sublists. + + - COne + + - Yes. + + - Two + + +.. mchoice:: error4 + + A list with extra sublists. + + - COne + + - Yes. + - OK. + + - Two + + - No. + +.. This just produces a confused question. The auto-numbering in the base classes prepends ``Q-x`` to ``- COne``, which means it's no longer a list. There's no easy way to detect this, without rewriting the way question numbers are prepended. + + .. mchoice:: error5_only_list_is_provided + + - COne + + - Yes. + + - Two + + - No. + +.. mchoice:: error6 + + A list with something else instead of sublists. + + - COne + + Not a sublist. + + - Two + + - No + + +.. mchoice:: error7 + + No correct answers. + + - One + + - No. + + - Two + + - Nope. Multiple Choice --------------- diff --git a/runestone/assess/test/test_assess.py b/runestone/assess/test/test_assess.py index d22d4de31..4fd2fac33 100644 --- a/runestone/assess/test/test_assess.py +++ b/runestone/assess/test/test_assess.py @@ -4,9 +4,42 @@ __author__ = 'yasinovskyy' +from unittest import TestCase from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -setUpModule, tearDownModule = module_fixture_maker(__file__) +mf, setUpModule, tearDownModule = module_fixture_maker(__file__, True) + +# Look for errors producted by invalid questions. +class MultipleChoiceQuestion_Error_Tests(TestCase): + def test_1(self): + # Check for the following directive-level errors. + directive_level_errors = ( + # Produced my mchoice id: error1_no_content, + (48, 'No correct answer specified'), + # error2, + (50, 'No correct answer specified.'), + # error7, + (103, 'No correct answer specified.'), + ) + for error_line, error_string in directive_level_errors: + # The rst_prolog in conf.py confuses line numbers. Adjust for it. + self.assertIn(':{}: WARNING: {}'.format(error_line + 4, error_string), mf.build_stderr_data) + + # Check for the following error inside the directive. + inside_directive_lines = ( + # Produced my mchoice id error3, + 62, + # error4, + 71, + # error6 + 96, + ) + for error_line in inside_directive_lines: + # The rst_prolog in conf.py confuses line numbers. Adjust for it. + self.assertIn(': WARNING: On line {}, a single-item list must be nested under each answer.'.format(error_line + 4), mf.build_stderr_data) + + # Make sure we saw all errors. + self.assertEqual(len(directive_level_errors) + len(inside_directive_lines), mf.build_stderr_data.count('WARNING')) class MultipleChoiceQuestion_Tests(RunestoneTestCase): def test_ma1(self): @@ -23,7 +56,8 @@ def test_ma1(self): self.assertIn("alert-danger", cnamestr) - def test_ma2(self): + # Testing time in dominated by browser startup/shutdown. So, simply run all tests in a single browser instance to speed things up. On failures, uncomment test functions to diagnose. + #def test_ma2(self): '''Multiple Answer: Correct answer(s) selected''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question1") @@ -40,7 +74,7 @@ def test_ma2(self): self.assertIn("alert-success", cnamestr) - def test_ma3(self): + #def test_ma3(self): '''Multiple Answer: Incorrect answer(s) selected''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question1") @@ -57,7 +91,7 @@ def test_ma3(self): self.assertIn("alert-danger", cnamestr) - def test_ma4(self): + #def test_ma4(self): '''Multiple Answer: All options clicked one by one''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question1") @@ -75,6 +109,7 @@ def test_ma4(self): self.assertIn("alert-danger", cnamestr) + # If commented out, produces a failure on the first assertFalse below. ??? def test_ma5(self): '''Multiple Answer: Correct answer(s) selected and unselected''' self.driver.get(self.host + "/index.html") @@ -87,7 +122,7 @@ def test_ma5(self): self.assertFalse(cbs.is_selected()) - def test_mc1(self): + #def test_mc1(self): '''Multiple Choice: Nothing selected''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question2") @@ -101,7 +136,7 @@ def test_mc1(self): self.assertIn("alert-danger", cnamestr) - def test_mc2(self): + #def test_mc2(self): '''Multiple Choice: Correct answer selected''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question2") @@ -117,7 +152,7 @@ def test_mc2(self): self.assertIn("alert-success", cnamestr) - def test_mc3(self): + #def test_mc3(self): '''Multiple Choice: Incorrect answer selected''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question2") @@ -133,7 +168,7 @@ def test_mc3(self): self.assertIn("alert-danger", cnamestr) - def test_mc4(self): + #def test_mc4(self): '''Multiple Choice: All options clicked one by one''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question2") diff --git a/runestone/common/__init__.py b/runestone/common/__init__.py index 719e0058e..386b71600 100644 --- a/runestone/common/__init__.py +++ b/runestone/common/__init__.py @@ -1 +1 @@ -from .runestonedirective import RunestoneDirective, RunestoneNode +from .runestonedirective import RunestoneDirective, RunestoneNode, get_node_line diff --git a/runestone/common/runestonedirective.py b/runestone/common/runestonedirective.py index cf50c6d90..4c1984e4a 100644 --- a/runestone/common/runestonedirective.py +++ b/runestone/common/runestonedirective.py @@ -51,3 +51,11 @@ def __init__(self, *args, **kwargs): self.options['basecourse'] = self.basecourse self.options['chapter'] = self.chapter self.options['subchapter'] = self.subchapter + +# Some nodes have a line number of None. Look through their children to find the node's line number. +def get_node_line(node): + while not node.line: + node = node[0] + return node.line + + diff --git a/runestone/fitb/fitb.py b/runestone/fitb/fitb.py index a99ffe873..88b91a1fd 100644 --- a/runestone/fitb/fitb.py +++ b/runestone/fitb/fitb.py @@ -20,8 +20,9 @@ from numbers import Number from docutils import nodes from docutils.parsers.rst import directives +from sphinx.util import logging from runestone.server.componentdb import addQuestionToDB, addHTMLToDB -from runestone.common import RunestoneDirective, RunestoneNode +from runestone.common import RunestoneDirective, RunestoneNode, get_node_line def setup(app): @@ -68,7 +69,9 @@ def depart_fitb_node(self, node): # Warn if there are fewer feedback items than blanks. if len(node.feedbackArray) < blankCount: - print('Warning at {} line {}: there'' not enough feedback for the number of blanks supplied.'.format(node.source, node.line)) + # Taken from the example in the `logging API `_. + logger = logging.getLogger(__name__) + logger.warning('Not enough feedback for the number of blanks supplied.', location=node) # Generate the HTML. node.fitb_options['json'] = json.dumps(node.feedbackArray) @@ -82,7 +85,6 @@ def depart_fitb_node(self, node): self.body.remove(node.delimiter) - class FillInTheBlank(RunestoneDirective): """ .. fillintheblank:: some_unique_id_here @@ -197,12 +199,12 @@ def run(self): self.assert_has_content() feedback_bullet_list = fitbNode.pop() if not isinstance(feedback_bullet_list, nodes.bullet_list): - self.error('The last item in a fill-in-the-blank question must be a bulleted list.') + raise self.error('On line {}, the last item in a fill-in-the-blank question must be a bulleted list.'.format(get_node_line(feedback_bullet_list))) for feedback_list_item in feedback_bullet_list.children: assert isinstance(feedback_list_item, nodes.list_item) feedback_field_list = feedback_list_item[0] if len(feedback_list_item) != 1 or not isinstance(feedback_field_list, nodes.field_list): - self.error('Each list item in a fill-in-the-blank problems must contain only one item, a field list.') + raise self.error('On line {}, each list item in a fill-in-the-blank problems must contain only one item, a field list.'.format(get_node_line(feedback_list_item))) blankArray = [] for feedback_field in feedback_field_list: assert isinstance(feedback_field, nodes.field) diff --git a/runestone/fitb/js/fitb.js b/runestone/fitb/js/fitb.js index 61bbf7076..23ef5469a 100644 --- a/runestone/fitb/js/fitb.js +++ b/runestone/fitb/js/fitb.js @@ -196,6 +196,7 @@ FITB.prototype.evaluateAnswers = function () { // If this blank is empty, provide no feedback for it. if (given === "") { this.isCorrectArray.push(""); + this.displayFeed.push('No answer provided.'); } else { // Look through all feedback for this blank. The last element in the array always matches. var fbl = this.feedbackArray[i]; @@ -251,7 +252,6 @@ FITB.prototype.isCompletelyBlank = function () { FITB.prototype.renderFITBFeedback = function () { if (this.correct) { - $(this.feedBackDiv).html("Correct.
        "); $(this.feedBackDiv).attr("class", "alert alert-success"); for (var j = 0; j < this.blankArray.length; j++) { $(this.blankArray[j]).removeClass("input-validation-error"); @@ -260,7 +260,6 @@ FITB.prototype.renderFITBFeedback = function () { if (this.displayFeed === null) { this.displayFeed = ""; } - $(this.feedBackDiv).html("Incorrect.
        "); for (var j = 0; j < this.blankArray.length; j++) { if (!this.isCorrectArray[j]) { $(this.blankArray[j]).addClass("input-validation-error"); @@ -270,10 +269,16 @@ FITB.prototype.renderFITBFeedback = function () { } $(this.feedBackDiv).attr("class", "alert alert-danger"); } + var feedback_html = '
          '; for (var i = 0; i < this.displayFeed.length; i++) { - this.feedBackDiv.innerHTML += this.displayFeed[i]; - this.feedBackDiv.appendChild(document.createElement("br")); + feedback_html += '
        • ' + this.displayFeed[i] + '
        • '; } + feedback_html += '
        '; + // Remove the list if it's just one element. + if (this.displayFeed.length == 1) { + feedback_html = feedback_html.slice('
        • '.length, -('
        '.length)) + } + this.feedBackDiv.innerHTML = feedback_html; }; /*================================== diff --git a/runestone/fitb/test/_sources/index.rst b/runestone/fitb/test/_sources/index.rst index 1a9f31bdb..de9926a38 100644 --- a/runestone/fitb/test/_sources/index.rst +++ b/runestone/fitb/test/_sources/index.rst @@ -2,11 +2,8 @@ This Is A New Project ===================== - - Fill in the Blank ----------------- - .. fillintheblank:: fill1412 Fill in the blanks to make the following sentence: "The red car drove away." @@ -14,6 +11,26 @@ Fill in the Blank The |blank| car drove |blank|. - :red: Correct. - :x: Try 'red'. - - :car: Correct. - :x: Try 'away'. + :x: Incorrect. Try 'red'. + - :away: Correct. + :x: Incorrect. Try 'away'. + +Error testing +------------- +.. fillintheblank:: error1_no_content + +.. fillintheblank:: error2 + + No feedback provided. + +.. fillintheblank:: error3 + + List contents aren't field lists. + + - I'm not a field list. + +.. fillintheblank:: error4 + + Not enough feedback. |blank| |blank| + + - :Feedback: For blank 1. diff --git a/runestone/fitb/test/test_fitb.py b/runestone/fitb/test/test_fitb.py index 754c20af1..b324a332d 100644 --- a/runestone/fitb/test/test_fitb.py +++ b/runestone/fitb/test/test_fitb.py @@ -1,16 +1,41 @@ +from unittest import TestCase from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -setUpModule, tearDownModule = module_fixture_maker(__file__) +mf, setUpModule, tearDownModule = module_fixture_maker(__file__, True) +# Look for errors producted by invalid questions. +class FITB_Error_Tests(TestCase): + def test_1(self): + # Check for the following directive-level errors. + directive_level_errors = ( + # Produced my mchoice id error1_no_content, + (20, 'Content block expected for the "fillintheblank" directive; none found.'), + (32, 'Not enough feedback for the number of blanks supplied.'), + ) + for error_line, error_string in directive_level_errors: + # The rst_prolog in conf.py confuses line numbers. Adjust for it. + self.assertIn(':{}: WARNING: {}'.format(error_line + 4, error_string), mf.build_stderr_data) + + # Check for the following error inside the directive. + inside_directive_errors = ( + # error2, + (24, 'the last item in a fill-in-the-blank question must be a bulleted list.'), + # error3, + (30, 'each list item in a fill-in-the-blank problems must contain only one item, a field list.'), + ) + for error_line, error_string in inside_directive_errors: + # The rst_prolog in conf.py confuses line numbers. Adjust for it. + self.assertIn(': WARNING: On line {}, {}'.format(error_line + 4, error_string), mf.build_stderr_data) + + # Make sure we saw all errors. + self.assertEqual(len(directive_level_errors) + len(inside_directive_errors), mf.build_stderr_data.count('WARNING')) + class FITBtests(RunestoneTestCase): ## Helpers ## ======= - def setUp(self): - super(FITBtests, self).setUp() - self.driver.get(self.host + "/index.html") # Access page - - # Return the DIV containing a FITB question. + # Load the web page, then return the DIV containing a FITB question. def find_fitb(self): + self.driver.get(self.host + "/index.html") self.fitb = self.driver.find_element_by_id("fill1412") return self.fitb @@ -37,15 +62,15 @@ def test_fitb(self): self.find_blank(0).send_keys("red") self.click_checkme() feedback = self.find_feedback() - self.assertIsNotNone(feedback.text) + self.assertIn('Correct', feedback.text) - # No answers yet -- Incorrect feedback + # No answers yet -- no answer provided feedback. def test_fitb2(self): self.find_fitb() self.click_checkme() feedback = self.find_feedback() self.assertIsNotNone(feedback.text) - self.assertIn("Incorrect",feedback.text) + self.assertIn("No answer provided.", feedback.text) # Both correct answers diff --git a/runestone/unittest_base.py b/runestone/unittest_base.py index eec4c03aa..1010886ff 100644 --- a/runestone/unittest_base.py +++ b/runestone/unittest_base.py @@ -8,7 +8,7 @@ PORT = '8081' # Define `module modules fixtures `_ to build the test Runestone project, run the server, then shut it down when the tests complete. -class ModuleFixture(object): +class ModuleFixture(unittest.TestCase): def __init__(self, # The path to the Python module in which the test resides. This provides a simple way to determine the path in which to run runestone build/serve. module_path): @@ -20,8 +20,11 @@ def setUpModule(self): # Change to this directory for running Runestone. self.old_cwd = os.getcwd() os.chdir(self.base_path) - # Compile the docs. - subprocess.check_call(['runestone', 'build', '--all']) + # Compile the docs. Save the stdout and stderr for examination. + p = subprocess.Popen(['runestone', 'build', '--all'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + self.build_stdout_data, self.build_stderr_data = p.communicate() + print(self.build_stdout_data + self.build_stderr_data) + self.assertFalse(p.returncode) # Run the server. Simply calling ``runestone serve`` fails, since the process killed isn't the actual server, but probably a setuptools-created launcher. self.runestone_server = subprocess.Popen(['python', '-m', 'runestone', 'serve', '--port', PORT]) @@ -38,9 +41,12 @@ def tearDownModule(self): # # from unittest_base import module_fixture_maker # setUpModule, tearDownModule = module_fixture_maker(__file__) -def module_fixture_maker(module_path): +def module_fixture_maker(module_path, return_mf=False): mf = ModuleFixture(module_path) - return mf.setUpModule, mf.tearDownModule + if return_mf: + return mf, mf.setUpModule, mf.tearDownModule + else: + return mf.setUpModule, mf.tearDownModule # Provide a base test case which sets up the `Selenium `_ driver. class RunestoneTestCase(unittest.TestCase):