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('- (%(letter)s) ' % mcNode.mc_options)
+ self.body.append('
- ' % 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))
+ }
+ 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):