MDL-32323 convert question tests
authorPetr Skoda <commits@skodak.org>
Sun, 8 Apr 2012 22:24:57 +0000 (00:24 +0200)
committerPetr Skoda <commits@skodak.org>
Tue, 10 Apr 2012 13:27:11 +0000 (15:27 +0200)
1/ type/match/tests/walkthrough_test.php - tests are failing randomly, looks like some weird randomisation is going on there - see TODOs

2/ type/multianswer/tests/upgradelibnewqe_test.php contains invalid expected value - see TODO

92 files changed:
phpunit.xml.dist
question/behaviour/adaptive/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/adaptivenopenalty/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/deferredcbm/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/deferredfeedback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/immediatecbm/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/immediatefeedback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/informationitem/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/interactive/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/interactivecountback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/manualgraded/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/missing/tests/missingbehaviour_test.php [new file with mode: 0644]
question/engine/tests/datalib_test.php [new file with mode: 0644]
question/engine/tests/helpers.php [new file with mode: 0644]
question/engine/tests/questionattempt_test.php [new file with mode: 0644]
question/engine/tests/questionattemptiterator_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstep_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstepiterator_test.php [new file with mode: 0644]
question/engine/tests/questionbank_test.php [new file with mode: 0644]
question/engine/tests/questioncbm_test.php [new file with mode: 0644]
question/engine/tests/questionengine_test.php [new file with mode: 0644]
question/engine/tests/questionstate_test.php [new file with mode: 0644]
question/engine/tests/questionusagebyactivity_test.php [new file with mode: 0644]
question/engine/tests/questionutils_test.php [new file with mode: 0644]
question/engine/tests/unitofwork_test.php [new file with mode: 0644]
question/engine/upgrade/tests/helper.php [new file with mode: 0644]
question/format/gift/tests/fixtures/questions.gift.txt [new file with mode: 0644]
question/format/gift/tests/giftformat_test.php [new file with mode: 0644]
question/format/xml/tests/xmlformat_test.php [new file with mode: 0644]
question/tests/importexport_test.php [new file with mode: 0644]
question/type/calculated/tests/helper.php [new file with mode: 0644]
question/type/calculated/tests/question_test.php [new file with mode: 0644]
question/type/calculated/tests/questiontype_test.php [new file with mode: 0644]
question/type/calculated/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculated/tests/variablesubstituter_test.php [new file with mode: 0644]
question/type/calculated/tests/walkthrough_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/helper.php [new file with mode: 0644]
question/type/calculatedmulti/tests/question_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/walkthrough_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/helper.php [new file with mode: 0644]
question/type/calculatedsimple/tests/question_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/walkthrough_test.php [new file with mode: 0644]
question/type/description/tests/helper.php [new file with mode: 0644]
question/type/description/tests/questiontype_test.php [new file with mode: 0644]
question/type/description/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/description/tests/walkthrough_test.php [new file with mode: 0644]
question/type/edit_question_form.php
question/type/essay/questiontype.php
question/type/essay/tests/question_test.php [new file with mode: 0644]
question/type/essay/tests/questiontype_test.php [new file with mode: 0644]
question/type/essay/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/match/tests/question_test.php [new file with mode: 0644]
question/type/match/tests/questiontype_test.php [new file with mode: 0644]
question/type/match/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/match/tests/walkthrough_test.php [new file with mode: 0644]
question/type/missingtype/tests/missingtype_test.php [new file with mode: 0644]
question/type/multianswer/tests/helper.php [new file with mode: 0644]
question/type/multianswer/tests/question_test.php [new file with mode: 0644]
question/type/multianswer/tests/questiontype_test.php [new file with mode: 0644]
question/type/multianswer/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/multianswer/tests/walkthrough_test.php [new file with mode: 0644]
question/type/multichoice/questiontype.php
question/type/multichoice/tests/question_test.php [new file with mode: 0644]
question/type/multichoice/tests/questiontype_test.php [new file with mode: 0644]
question/type/multichoice/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/multichoice/tests/walkthrough_test.php [new file with mode: 0644]
question/type/numerical/question.php
question/type/numerical/questiontype.php
question/type/numerical/tests/answer_test.php [new file with mode: 0644]
question/type/numerical/tests/answerprocessor_test.php [new file with mode: 0644]
question/type/numerical/tests/form_test.php [new file with mode: 0644]
question/type/numerical/tests/helper.php [new file with mode: 0644]
question/type/numerical/tests/question_test.php [new file with mode: 0644]
question/type/numerical/tests/questiontype_test.php [new file with mode: 0644]
question/type/numerical/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/numerical/tests/walkthrough_test.php [new file with mode: 0644]
question/type/random/tests/questiontype_test.php [new file with mode: 0644]
question/type/random/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/shortanswer/tests/helper.php [new file with mode: 0644]
question/type/shortanswer/tests/question_test.php [new file with mode: 0644]
question/type/shortanswer/tests/questiontype_test.php [new file with mode: 0644]
question/type/shortanswer/tests/tupgradelibnewqe_test.php [new file with mode: 0644]
question/type/tests/questionbase_test.php [new file with mode: 0644]
question/type/tests/questiontype_test.php [new file with mode: 0644]
question/type/truefalse/questiontype.php
question/type/truefalse/tests/helper.php [new file with mode: 0644]
question/type/truefalse/tests/question_test.php [new file with mode: 0644]
question/type/truefalse/tests/questiontype_test.php [new file with mode: 0644]
question/type/truefalse/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/truefalse/tests/walkthrough_test.php [new file with mode: 0644]

index b847fdd..f0838ee 100644 (file)
         <testsuite name="core_course">
             <directory suffix="_test.php">course/tests</directory>
         </testsuite>
+        <testsuite name="core_question">
+            <directory suffix="_test.php">question/engine/tests</directory>
+            <directory suffix="_test.php">question/tests</directory>
+            <directory suffix="_test.php">question/type/tests</directory>
+        </testsuite>
 
         <!--Plugin suites: use admin/tool/phpunit/cli/util.php to build phpunit.xml from phpunit.xml.dist with up-to-date list of plugins in current install-->
 <!--@plugin_suites_start@-->
diff --git a/question/behaviour/adaptive/tests/walkthrough_test.php b/question/behaviour/adaptive/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..b78e470
--- /dev/null
@@ -0,0 +1,747 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * This file contains tests that walks a question through the adaptive
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage adaptive
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the adaptive behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptive_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    protected function get_contains_penalty_info_expectation($penalty) {
+        $penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive',
+                                  format_float($penalty, $this->displayoptions->markdp));
+        return new question_pattern_expectation('/'.preg_quote($penaltyinfo).'/');
+    }
+
+    protected function get_does_not_contain_penalty_info_expectation() {
+        $penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive', 'XXXXX');
+        $penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo)).'/';
+        return new question_no_pattern_expectation($penaltypattern);
+    }
+
+    protected function get_contains_total_penalty_expectation($penalty) {
+        $penaltyinfo = get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive',
+                                  format_float($penalty, $this->displayoptions->markdp));
+        return new question_pattern_expectation('/'.preg_quote($penaltyinfo).'/');
+    }
+
+    protected function get_does_not_contain_total_penalty_expectation() {
+        $penaltyinfo = get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive', 'XXXXX');
+        $penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo)).'/';
+        return new question_no_pattern_expectation($penaltypattern);
+    }
+
+    protected function get_contains_disregarded_info_expectation() {
+        $penaltyinfo = get_string('disregardedwithoutpenalty', 'qbehaviour_adaptive');
+        return new question_pattern_expectation('/'.preg_quote($penaltyinfo).'/');
+    }
+
+    protected function get_does_not_contain_disregarded_info_expectation() {
+        $penaltyinfo = get_string('disregardedwithoutpenalty', 'qbehaviour_adaptive');
+        return new question_no_pattern_expectation('/'.preg_quote($penaltyinfo).'/');
+    }
+
+    public function test_adaptive_multichoice() {
+
+        // Create a multiple choice, single response question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $mc->penalty = 0.3333333;
+        $this->start_attempt_at_question($mc, 'adaptive', 3);
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process a submit.
+        $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 2) % 3, true, false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(1.00),
+                $this->get_does_not_contain_total_penalty_expectation());
+        $this->assertRegExp('/B|C/',
+                $this->quba->get_response_summary($this->slot));
+
+        // Process a change of answer to the right one, but not sumbitted.
+        $this->process_submission(array('answer' => $rightindex));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_mc_radio_expectation($rightindex, true, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false));
+        $this->assertRegExp('/B|C/',
+                $this->quba->get_response_summary($this->slot));
+
+        // Now submit the right answer.
+        $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(3 * (1 - $mc->penalty));
+        $this->check_current_output(
+                $this->get_contains_mark_summary(3 * (1 - $mc->penalty)),
+                $this->get_contains_mc_radio_expectation($rightindex, true, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation());
+        $this->assertEquals('A',
+                $this->quba->get_response_summary($this->slot));
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3 * (1 - $mc->penalty));
+        $this->check_current_output(
+                $this->get_contains_mark_summary(3 * (1 - $mc->penalty)),
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, false, false),
+                $this->get_contains_correct_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 1);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Now change the correct answer to the question, and regrade.
+        $mc->answers[13]->fraction = -0.33333333;
+        $mc->answers[14]->fraction = 1; // We don't know which "wrong" index we chose above!
+        $mc->answers[15]->fraction = 1; // Therefore, treat answers B and C with the same score.
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_partcorrect_expectation());
+
+        $autogradedstep = $this->get_step($this->get_step_count() - 2);
+        $this->assertEquals($autogradedstep->get_fraction(), 1, '', 0.0000001);
+    }
+
+    public function test_adaptive_multichoice2() {
+
+        // Create a multiple choice, multiple response question.
+        $mc = test_question_maker::make_a_multichoice_multi_question();
+        $mc->penalty = 0.3333333;
+        $mc->shuffleanswers = 0;
+        $this->start_attempt_at_question($mc, 'adaptive', 2);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process a submit.
+        $this->process_submission(array('choice0' => 1, 'choice2' => 1, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(2);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(2),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation());
+
+        // Save the same correct answer again. Should not do anything.
+        $numsteps = $this->get_step_count();
+        $this->process_submission(array('choice0' => 1, 'choice2' => 1));
+
+        // Verify.
+        $this->check_step_count($numsteps);
+        $this->check_current_mark(2);
+        $this->check_current_state(question_state::$complete);
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_step_count($numsteps + 1);
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(2);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(2),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_correct_expectation());
+    }
+
+    public function test_adaptive_shortanswer_partially_right() {
+
+        // Create a short answer question
+        $sa = test_question_maker::make_question('shortanswer');
+        $this->start_attempt_at_question($sa, 'adaptive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit a partially correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'toad'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0.8);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.8),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_partcorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(0.33),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit an incorrect answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0.8);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.8),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(0.33),
+                $this->get_contains_total_penalty_expectation(0.67),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit a correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(0.8);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.8),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(0.8);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.8),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_adaptive_shortanswer_wrong_right_wrong() {
+
+        // Create a short answer question
+        $sa = test_question_maker::make_question('shortanswer');
+        $this->start_attempt_at_question($sa, 'adaptive', 6);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit a wrong answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(2.00),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit the same wrong answer again. Nothing should change.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(2.00),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit a correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(4.00);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(4.00),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit another incorrect answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(4.00);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(4.00),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(4.00);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(4.00),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_adaptive_shortanswer_invalid_after_complete() {
+
+        // Create a short answer question
+        $sa = test_question_maker::make_question('shortanswer');
+        $this->start_attempt_at_question($sa, 'adaptive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit a wrong answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(0.33),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit a correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(0.66666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.67),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit an empty answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(0.66666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.67),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_contains_validation_error_expectation());
+
+        // Submit another wrong answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(0.66666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.67),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(0.66666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.67),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_adaptive_shortanswer_zero_penalty() {
+
+        // Create a short answer question
+        $sa = test_question_maker::make_question('shortanswer');
+        // Disable penalties for this question
+        $sa->penalty = 0;
+        $this->start_attempt_at_question($sa, 'adaptive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit a wrong answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit a correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(1.0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1.0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1.0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1.0),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_adaptive_shortanswer_try_to_submit_blank() {
+
+        // Create a short answer question with correct answer true.
+        $sa = test_question_maker::make_question('shortanswer');
+        $this->start_attempt_at_question($sa, 'adaptive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit with blank answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_contains_disregarded_info_expectation());
+        $this->assertNull($this->quba->get_response_summary($this->slot));
+
+        // Now get it wrong.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'toad'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0.8);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.8),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_partcorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(0.33),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit blank again.
+        $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(0.8);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.8),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_contains_validation_error_expectation());
+    }
+
+    public function test_adaptive_numerical() {
+
+        // Create a numerical question
+        $sa = test_question_maker::make_question('numerical', 'pi');
+        $this->start_attempt_at_question($sa, 'adaptive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit the correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit an incorrect answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => '-5'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_adaptive_numerical_invalid() {
+
+        // Create a numerical question
+        $numq = test_question_maker::make_question('numerical', 'pi');
+        $numq->penalty = 0.1;
+        $this->start_attempt_at_question($numq, 'adaptive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit a non-numerical answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'Pi'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(1),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_contains_disregarded_info_expectation());
+
+        // Submit an incorrect answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => '-5'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_penalty_info_expectation(0.1),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_does_not_contain_disregarded_info_expectation());
+
+        // Submit another non-numerical answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'Pi*2'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_contains_disregarded_info_expectation());
+
+        // Submit the correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(0.9);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.9),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_does_not_contain_disregarded_info_expectation());
+
+        // Submit another non-numerical answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'Pi/3'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(0.9);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.9),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_penalty_info_expectation(),
+                $this->get_does_not_contain_total_penalty_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_contains_disregarded_info_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(0.9);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.9),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_does_not_contain_disregarded_info_expectation());
+    }
+}
diff --git a/question/behaviour/adaptivenopenalty/tests/walkthrough_test.php b/question/behaviour/adaptivenopenalty/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..546a5b5
--- /dev/null
@@ -0,0 +1,292 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the adaptive (no penalties)k
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage adaptivenopenalty
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the adaptive (no penalties) behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptivenopenalty_walkthrough_test extends qbehaviour_walkthrough_test_base {
+
+    protected function get_does_not_contain_gradingdetails_expectation() {
+        return new question_no_pattern_expectation('/class="gradingdetails"/');
+    }
+
+    public function test_multichoice() {
+
+        // Create a multiple choice, single response question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $mc->penalty = 0.3333333;
+        $this->start_attempt_at_question($mc, 'adaptivenopenalty', 3);
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process a submit.
+        $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 2) % 3, true, false),
+                $this->get_contains_incorrect_expectation());
+        $this->assertRegExp('/B|C/',
+                $this->quba->get_response_summary($this->slot));
+
+        // Process a change of answer to the right one, but not sumbitted.
+        $this->process_submission(array('answer' => $rightindex));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_mc_radio_expectation($rightindex, true, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false));
+        $this->assertRegExp('/B|C/',
+                $this->quba->get_response_summary($this->slot));
+
+        // Now submit the right answer.
+        $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(3);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(3),
+                $this->get_contains_mc_radio_expectation($rightindex, true, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false),
+                $this->get_contains_correct_expectation());
+        $this->assertEquals('A',
+                $this->quba->get_response_summary($this->slot));
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(3),
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, false, false),
+                $this->get_contains_correct_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 1);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Now change the correct answer to the question, and regrade.
+        $mc->answers[13]->fraction = -0.33333333;
+        $mc->answers[14]->fraction = 1; // We don't know which "wrong" index we chose above!
+        $mc->answers[15]->fraction = 1; // Therefore, treat answers B and C with the same score.
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_partcorrect_expectation());
+
+        $autogradedstep = $this->get_step($this->get_step_count() - 3);
+        $this->assertEquals($autogradedstep->get_fraction(), 1, '', 0.0000001);
+    }
+
+    public function test_multichoice2() {
+
+        // Create a multiple choice, multiple response question.
+        $mc = test_question_maker::make_a_multichoice_multi_question();
+        $mc->penalty = 0.3333333;
+        $mc->shuffleanswers = 0;
+        $this->start_attempt_at_question($mc, 'adaptivenopenalty', 2);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process a submit.
+        $this->process_submission(array('choice0' => 1, 'choice2' => 1, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(2);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(2),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation());
+
+        // Save the same correct answer again. Should no do anything.
+        $numsteps = $this->get_step_count();
+        $this->process_submission(array('choice0' => 1, 'choice2' => 1));
+
+        // Verify.
+        $this->check_step_count($numsteps);
+        $this->check_current_state(question_state::$complete);
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_step_count($numsteps + 1);
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(2);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(2),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_correct_expectation());
+    }
+
+    public function test_numerical_invalid() {
+
+        // Create a numerical question
+        $numq = test_question_maker::make_question('numerical', 'pi');
+        $numq->penalty = 0.1;
+        $this->start_attempt_at_question($numq, 'adaptivenopenalty');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit a non-numerical answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'Pi'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(1),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit an incorrect answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => '-5'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit another non-numerical answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'Pi*2'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_does_not_contain_gradingdetails_expectation());
+
+        // Submit the correct answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(1.0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1.0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Submit another non-numerical answer.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'Pi/3'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(1.0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1.0),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_does_not_contain_gradingdetails_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(1.0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1.0),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+}
diff --git a/question/behaviour/deferredcbm/tests/walkthrough_test.php b/question/behaviour/deferredcbm/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..852b068
--- /dev/null
@@ -0,0 +1,273 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the deferred feedback
+ * with certainty base marking behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage deferredcbm
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the deferred feedback with certainty base marking behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredcbm_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_deferred_cbm_truefalse_high_certainty() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($tf),
+                $this->get_contains_tf_true_radio_expectation(true, false),
+                $this->get_contains_tf_false_radio_expectation(true, false),
+                $this->get_contains_cbm_radio_expectation(1, true, false),
+                $this->get_contains_cbm_radio_expectation(2, true, false),
+                $this->get_contains_cbm_radio_expectation(3, true, false),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process the data extracted for this question.
+        $this->process_submission(array('answer' => 1, '-certainty' => 3));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_tf_true_radio_expectation(true, true),
+                $this->get_contains_cbm_radio_expectation(3, true, true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process the same data again, check it does not create a new step.
+        $numsteps = $this->get_step_count();
+        $this->process_submission(array('answer' => 1, '-certainty' => 3));
+        $this->check_step_count($numsteps);
+
+        // Process different data, check it creates a new step.
+        $this->process_submission(array('answer' => 1, '-certainty' => 1));
+        $this->check_step_count($numsteps + 1);
+        $this->check_current_state(question_state::$complete);
+
+        // Change back, check it creates a new step.
+        $this->process_submission(array('answer' => 1, '-certainty' => 3));
+        $this->check_step_count($numsteps + 2);
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(2);
+        $this->check_current_output(
+                $this->get_contains_tf_true_radio_expectation(false, true),
+                $this->get_contains_cbm_radio_expectation(3, false, true),
+                $this->get_contains_correct_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 1);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(new question_pattern_expectation('/' .
+                preg_quote('Not good enough!') . '/'));
+
+        // Now change the correct answer to the question, and regrade.
+        $tf->rightanswer = false;
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $autogradedstep = $this->get_step($this->get_step_count() - 2);
+        $this->assertEquals($autogradedstep->get_fraction(), -2, '', 0.0000001);
+    }
+
+    public function test_deferred_cbm_truefalse_low_certainty() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, true, false),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit ansewer with low certainty.
+        $this->process_submission(array('answer' => 1, '-certainty' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, true, true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(0.6666667);
+        $this->check_current_output($this->get_contains_correct_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, false, true));
+        $this->assertEquals(get_string('true', 'qtype_truefalse') . ' [' .
+                question_cbm::get_string(1) . ']',
+                $this->quba->get_response_summary($this->slot));
+    }
+
+    public function test_deferred_cbm_truefalse_default_certainty() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, true, false),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit ansewer with low certainty and finish the attempt.
+        $this->process_submission(array('answer' => 1));
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $qa = $this->quba->get_question_attempt($this->slot);
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(0.6666667);
+        $this->check_current_output($this->get_contains_correct_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, false, false),
+                new question_pattern_expectation('/' . preg_quote(
+                        get_string('assumingcertainty', 'qbehaviour_deferredcbm',
+                        question_cbm::get_string(
+                            $qa->get_last_behaviour_var('_assumedcertainty')))) . '/'));
+        $this->assertEquals(get_string('true', 'qtype_truefalse'),
+                $this->quba->get_response_summary($this->slot));
+    }
+
+    public function test_deferredcbm_resume_multichoice_single() {
+
+        // Create a multiple-choice question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+
+        // Attempt it getting it wrong.
+        $this->start_attempt_at_question($mc, 'deferredcbm', 3);
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+        $this->process_submission(array('answer' => $wrongindex, '-certainty' => 2));
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(-3.3333333);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_cbm_radio_expectation(2, false, true),
+                $this->get_contains_incorrect_expectation());
+        $this->assertEquals('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
+                $this->quba->get_right_answer_summary($this->slot));
+        $this->assertRegExp('/' . preg_quote($mc->questiontext) . '/',
+                $this->quba->get_question_summary($this->slot));
+        $this->assertRegExp('/(B|C) \[' . preg_quote(question_cbm::get_string(2)) . '\]/',
+                $this->quba->get_response_summary($this->slot));
+
+        // Save the old attempt.
+        $oldqa = $this->quba->get_question_attempt($this->slot);
+
+        // Reinitialise.
+        $this->setUp();
+        $this->quba->set_preferred_behaviour('deferredcbm');
+        $this->slot = $this->quba->add_question($mc, 3);
+        $this->quba->start_question_based_on($this->slot, $oldqa);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_cbm_radio_expectation(2, true, true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_correctness_expectation());
+        $this->assertEquals('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
+                $this->quba->get_right_answer_summary($this->slot));
+        $this->assertRegExp('/' . preg_quote($mc->questiontext) . '/',
+                $this->quba->get_question_summary($this->slot));
+        $this->assertNull($this->quba->get_response_summary($this->slot));
+
+        // Now get it right.
+        $this->process_submission(array('answer' => $rightindex, '-certainty' => 3));
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_cbm_radio_expectation(3, false, true),
+                $this->get_contains_correct_expectation());
+        $this->assertRegExp('/(A) \[' . preg_quote(question_cbm::get_string(3)) . '\]/',
+                $this->quba->get_response_summary($this->slot));
+    }
+
+    public function test_deferred_cbm_truefalse_no_certainty_feedback_when_not_answered() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, true, false),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish without answering.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gaveup);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_no_pattern_expectation('/class=\"im-feedback/'));
+    }
+}
diff --git a/question/behaviour/deferredfeedback/tests/walkthrough_test.php b/question/behaviour/deferredfeedback/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..80faf0c
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the deferred feedback
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage deferredfeedback
+ * @copyright  2009 The Open University
+ * @license  http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the deferred feedback behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredfeedback_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_deferredfeedback_feedback_truefalse() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredfeedback', 2);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_question_text_expectation($tf),
+                $this->get_does_not_contain_feedback_expectation());
+        $this->assertEquals(get_string('true', 'qtype_truefalse'),
+                $this->quba->get_right_answer_summary($this->slot));
+        $this->assertRegExp('/' . preg_quote($tf->questiontext) . '/',
+                $this->quba->get_question_summary($this->slot));
+        $this->assertNull($this->quba->get_response_summary($this->slot));
+
+        // Process a true answer and check the expected result.
+        $this->process_submission(array('answer' => 1));
+
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_tf_true_radio_expectation(true, true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process the same data again, check it does not create a new step.
+        $numsteps = $this->get_step_count();
+        $this->process_submission(array('answer' => 1));
+        $this->check_step_count($numsteps);
+
+        // Process different data, check it creates a new step.
+        $this->process_submission(array('answer' => 0));
+        $this->check_step_count($numsteps + 1);
+        $this->check_current_state(question_state::$complete);
+
+        // Change back, check it creates a new step.
+        $this->process_submission(array('answer' => 1));
+        $this->check_step_count($numsteps + 2);
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(2);
+        $this->check_current_output($this->get_contains_correct_expectation(),
+                $this->get_contains_tf_true_radio_expectation(false, true),
+                new question_pattern_expectation('/class="r0 correct"/'));
+        $this->assertEquals(get_string('true', 'qtype_truefalse'),
+                $this->quba->get_response_summary($this->slot));
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 1);
+
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Now change the correct answer to the question, and regrade.
+        $tf->rightanswer = false;
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+
+        $autogradedstep = $this->get_step($this->get_step_count() - 2);
+        $this->assertEquals($autogradedstep->get_fraction(), 0, '', 0.0000001);
+    }
+
+    public function test_deferredfeedback_feedback_multichoice_single() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
+
+        // Start a deferred feedback attempt and add the question to it.
+        $rightindex = $this->get_mc_right_answer_index($mc);
+
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process the data extracted for this question.
+        $this->process_submission(array('answer' => $rightindex));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, true, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_correct_expectation());
+
+        // Now change the correct answer to the question, and regrade.
+        $mc->answers[13]->fraction = -0.33333333;
+        $mc->answers[14]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(-1);
+        $this->check_current_output(
+                $this->get_contains_incorrect_expectation());
+    }
+
+    public function test_deferredfeedback_resume_multichoice_single() {
+
+        // Create a multiple-choice question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+
+        // Attempt it getting it wrong.
+        $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+        $this->process_submission(array('answer' => $wrongindex));
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(-1);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_incorrect_expectation());
+
+        // Save the old attempt.
+        $oldqa = $this->quba->get_question_attempt($this->slot);
+
+        // Reinitialise.
+        $this->setUp();
+        $this->quba->set_preferred_behaviour('deferredfeedback');
+        $this->slot = $this->quba->add_question($mc, 3);
+        $this->quba->start_question_based_on($this->slot, $oldqa);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_correctness_expectation());
+
+        // Now get it right.
+        $this->process_submission(array('answer' => $rightindex));
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_correct_expectation());
+    }
+}
diff --git a/question/behaviour/immediatecbm/tests/walkthrough_test.php b/question/behaviour/immediatecbm/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..b0f3d9a
--- /dev/null
@@ -0,0 +1,292 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the immediate cbm
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage immediatecbm
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the immediate cbm behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatecbm_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_immediatecbm_feedback_multichoice_right() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'immediatecbm');
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+        $this->assertEquals('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
+                $this->quba->get_right_answer_summary($this->slot));
+        $this->assertRegExp('/' . preg_quote($mc->questiontext) . '/',
+                $this->quba->get_question_summary($this->slot));
+        $this->assertNull($this->quba->get_response_summary($this->slot));
+
+        // Save the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex, '-certainty' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit the right answer.
+        $this->process_submission(
+                array('answer' => $rightindex, '-certainty' => 2, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(2/3);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_correct_expectation());
+        $this->assertEquals('A [' . question_cbm::get_string(2) . ']',
+                $this->quba->get_response_summary($this->slot));
+
+        $numsteps = $this->get_step_count();
+
+        // Finish the attempt - should not need to add a new state.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->assertEquals($numsteps, $this->get_step_count());
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(2/3);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_correct_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 0.5);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_partcorrect_expectation(),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Now change the correct answer to the question, and regrade.
+        $mc->answers[13]->fraction = -0.33333333;
+        $mc->answers[15]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_partcorrect_expectation());
+
+        $autogradedstep = $this->get_step($this->get_step_count() - 2);
+        $this->assertEquals($autogradedstep->get_fraction(), -10/9, '', 0.0000001);
+    }
+
+    public function test_immediatecbm_feedback_multichoice_try_to_submit_blank() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'immediatecbm');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit nothing.
+        $this->process_submission(array('-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_validation_error_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gaveup);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation(0, false, false),
+                $this->get_contains_mc_radio_expectation(1, false, false),
+                $this->get_contains_mc_radio_expectation(2, false, false));
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 0.5);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_partcorrect_expectation(),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+    }
+
+    public function test_immediatecbm_feedback_shortanswer_try_to_submit_no_certainty() {
+
+        // Create a short answer question with correct answer true.
+        $sa = test_question_maker::make_question('shortanswer');
+        $this->start_attempt_at_question($sa, 'immediatecbm');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit with certainty missing.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_validation_error_expectation());
+
+        // Now get it right.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog', '-certainty' => 3));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_immediatecbm_feedback_multichoice_wrong_on_finish() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'immediatecbm');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Save the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex, '-certainty' => 3));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(-3);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_incorrect_expectation());
+    }
+
+    public function test_immediatecbm_cbm_truefalse_no_certainty_feedback_when_not_answered() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_contains_cbm_radio_expectation(1, true, false),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish without answering.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gaveup);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_no_pattern_expectation('/class=\"im-feedback/'));
+    }
+}
diff --git a/question/behaviour/immediatefeedback/tests/walkthrough_test.php b/question/behaviour/immediatefeedback/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..ebea45e
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the immediate feedback
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage immediatefeedback
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the immediate feedback behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatefeedback_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_immediatefeedback_feedback_multichoice_right() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'immediatefeedback');
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Save the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit the right answer.
+        $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_correct_expectation());
+        $this->assertEquals('A',
+                $this->quba->get_response_summary($this->slot));
+
+        $numsteps = $this->get_step_count();
+
+        // Now try to save again - as if the user clicked next in the quiz.
+        $this->process_submission(array('answer' => $rightindex));
+
+        // Verify.
+        $this->assertEquals($numsteps, $this->get_step_count());
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_correct_expectation());
+
+        // Finish the attempt - should not need to add a new state.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->assertEquals($numsteps, $this->get_step_count());
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_correct_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 0.5);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_partcorrect_expectation(),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Now change the correct answer to the question, and regrade.
+        $mc->answers[13]->fraction = -0.33333333;
+        $mc->answers[15]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_partcorrect_expectation());
+
+        $autogradedstep = $this->get_step($this->get_step_count() - 2);
+        $this->assertEquals($autogradedstep->get_fraction(), -0.3333333, '', 0.0000001);
+    }
+
+    public function test_immediatefeedback_feedback_multichoice_try_to_submit_blank() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'immediatefeedback');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Submit nothing.
+        $this->process_submission(array('-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_contains_validation_error_expectation());
+        $this->assertNull($this->quba->get_response_summary($this->slot));
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gaveup);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation(0, false, false),
+                $this->get_contains_mc_radio_expectation(1, false, false),
+                $this->get_contains_mc_radio_expectation(2, false, false));
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 0.5);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_partcorrect_expectation(),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+    }
+
+    public function test_immediatefeedback_feedback_multichoice_wrong_on_finish() {
+
+        // Create a true-false question with correct answer true.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $this->start_attempt_at_question($mc, 'immediatefeedback');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation());
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Save the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(-0.3333333);
+        $this->check_current_output(
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_incorrect_expectation());
+        $this->assertRegExp('/B|C/',
+                $this->quba->get_response_summary($this->slot));
+    }
+}
diff --git a/question/behaviour/informationitem/tests/walkthrough_test.php b/question/behaviour/informationitem/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..d2b4744
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the information item
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage informationitem
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the information item behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_informationitem_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_informationitem_feedback_description() {
+
+        // Create a true-false question with correct answer true.
+        $description = test_question_maker::make_question('description');
+        $this->start_attempt_at_question($description, 'deferredfeedback');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_question_text_expectation($description),
+                new question_contains_tag_with_attributes('input', array('type' => 'hidden',
+                'name' => $this->quba->get_field_prefix($this->slot) . '-seen', 'value' => 1)),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process a submission indicating this question has been seen.
+        $this->process_submission(array('-seen' => 1));
+
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_does_not_contain_correctness_expectation(),
+                new question_no_pattern_expectation(
+                '/type=\"hidden\"[^>]*name=\"[^"]*seen\"|name=\"[^"]*seen\"[^>]*type=\"hidden\"/'),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$finished);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($description),
+                $this->get_contains_general_feedback_expectation($description));
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', null);
+
+        $this->check_current_state(question_state::$manfinished);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Check that trying to process a manual comment with a grade causes an exception.
+        $this->setExpectedException('moodle_exception');
+        $this->manual_grade('Not good enough!', 1);
+    }
+}
diff --git a/question/behaviour/interactive/tests/walkthrough_test.php b/question/behaviour/interactive/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..4af3490
--- /dev/null
@@ -0,0 +1,490 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the interactive
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage interactive
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the interactive behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactive_walkthrough_test extends qbehaviour_walkthrough_test_base {
+
+    public function test_interactive_feedback_multichoice_right() {
+
+        // Create a multichoice single question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $mc->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, false),
+            new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+        );
+        $this->start_attempt_at_question($mc, 'interactive');
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(3),
+                $this->get_no_hint_visible_expectation());
+
+        // Save the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(3),
+                $this->get_no_hint_visible_expectation());
+
+        // Submit the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_try_again_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+                $this->get_contains_hint_expectation('This is the first hint'));
+
+        // Check that, if we review in this state, the try again button is disabled.
+        $displayoptions = new question_display_options();
+        $displayoptions->readonly = true;
+        $html = $this->quba->render_question($this->slot, $displayoptions);
+        $this->assert($this->get_contains_try_again_button_expectation(false), $html);
+
+        // Do try again.
+        $this->process_submission(array('-tryagain' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(2),
+                $this->get_no_hint_visible_expectation());
+
+        // Submit the right answer.
+        $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(0.6666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.6666667),
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_correct_expectation(),
+                $this->get_no_hint_visible_expectation());
+
+        // Finish the attempt - should not need to add a new state.
+        $numsteps = $this->get_step_count();
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->assertEquals($numsteps, $this->get_step_count());
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(0.6666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.6666667),
+                $this->get_contains_mc_radio_expectation($rightindex, false, true),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+                $this->get_contains_correct_expectation(),
+                $this->get_no_hint_visible_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 0.5);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.5),
+                $this->get_contains_partcorrect_expectation(),
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Check regrading does not mess anything up.
+        $this->quba->regrade_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(0.5);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.5),
+                $this->get_contains_partcorrect_expectation());
+
+        $autogradedstep = $this->get_step($this->get_step_count() - 2);
+        $this->assertEquals($autogradedstep->get_fraction(), 0.6666667, '', 0.0000001);
+    }
+
+    public function test_interactive_finish_when_try_again_showing() {
+
+        // Create a multichoice single question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $mc->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, false),
+        );
+        $this->start_attempt_at_question($mc, 'interactive');
+
+        $rightindex = $this->get_mc_right_answer_index($mc);
+        $wrongindex = ($rightindex + 1) % 3;
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_radio_expectation(0, true, false),
+                $this->get_contains_mc_radio_expectation(1, true, false),
+                $this->get_contains_mc_radio_expectation(2, true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(2),
+                $this->get_no_hint_visible_expectation(),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('selectone', 'qtype_multichoice'), '/') . '/'));
+
+        // Submit the wrong answer.
+        $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_try_again_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+                $this->get_contains_hint_expectation('This is the first hint'));
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_no_hint_visible_expectation());
+    }
+
+    public function test_interactive_shortanswer_try_to_submit_blank() {
+
+        // Create a short answer question.
+        $sa = test_question_maker::make_question('shortanswer');
+        $sa->hints = array(
+            new question_hint(0, 'This is the first hint.', FORMAT_HTML),
+            new question_hint(0, 'This is the second hint.', FORMAT_HTML),
+        );
+        $this->start_attempt_at_question($sa, 'interactive');
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_does_not_contain_try_again_button_expectation(),
+                $this->get_no_hint_visible_expectation());
+
+        // Submit blank.
+        $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_does_not_contain_try_again_button_expectation(),
+                $this->get_no_hint_visible_expectation());
+
+        // Now get it wrong.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'newt'));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_contains_try_again_button_expectation(true),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+                $this->get_contains_hint_expectation('This is the first hint'));
+        $this->assertEquals('newt',
+                $this->quba->get_response_summary($this->slot));
+
+        // Try again.
+        $this->process_submission(array('-tryagain' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_does_not_contain_try_again_button_expectation(),
+                $this->get_no_hint_visible_expectation());
+
+        // Now submit blank again.
+        $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_contains_validation_error_expectation(),
+                $this->get_does_not_contain_try_again_button_expectation(),
+                $this->get_no_hint_visible_expectation());
+
+        // Now get it right.
+        $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(0.6666667);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0.6666667),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_correct_expectation(),
+                $this->get_does_not_contain_validation_error_expectation(),
+                $this->get_no_hint_visible_expectation());
+        $this->assertEquals('frog',
+                $this->quba->get_response_summary($this->slot));
+    }
+
+    public function test_interactive_feedback_multichoice_multiple_reset() {
+
+        // Create a multichoice multiple question.
+        $mc = test_question_maker::make_a_multichoice_multi_question();
+        $mc->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+            new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+        );
+        $this->start_attempt_at_question($mc, 'interactive', 2);
+
+        $right = array_keys($mc->get_correct_response());
+        $wrong = array_diff(array('choice0', 'choice1', 'choice2', 'choice3'), $right);
+        $wrong = array_values(array_diff(
+                array('choice0', 'choice1', 'choice2', 'choice3'), $right));
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_question_text_expectation($mc),
+                $this->get_contains_mc_checkbox_expectation('choice0', true, false),
+                $this->get_contains_mc_checkbox_expectation('choice1', true, false),
+                $this->get_contains_mc_checkbox_expectation('choice2', true, false),
+                $this->get_contains_mc_checkbox_expectation('choice3', true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_num_parts_correct(),
+                $this->get_tries_remaining_expectation(3),
+                $this->get_no_hint_visible_expectation(),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('selectmulti', 'qtype_multichoice'), '/') . '/'));
+
+        // Submit an answer with one right, and one wrong.
+        $this->process_submission(array($right[0] => 1, $wrong[0] => 1, '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_mc_checkbox_expectation($right[0], false, true),
+                $this->get_contains_mc_checkbox_expectation($right[1], false, false),
+                $this->get_contains_mc_checkbox_expectation($wrong[0], false, true),
+                $this->get_contains_mc_checkbox_expectation($wrong[1], false, false),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_try_again_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+                $this->get_contains_hint_expectation('This is the first hint'),
+                $this->get_contains_num_parts_correct(1),
+                $this->get_contains_standard_incorrect_combined_feedback_expectation(),
+                $this->get_contains_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . $right[0], '1'),
+                $this->get_does_not_contain_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . $right[1]),
+                $this->get_contains_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . $wrong[0], '0'),
+                $this->get_does_not_contain_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . $wrong[1]));
+
+        // Do try again.
+        $this->process_submission(array($right[0] => 1, '-tryagain' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_contains_mc_checkbox_expectation($right[0], true, true),
+                $this->get_contains_mc_checkbox_expectation($right[1], true, false),
+                $this->get_contains_mc_checkbox_expectation($wrong[0], true, false),
+                $this->get_contains_mc_checkbox_expectation($wrong[1], true, false),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(2),
+                $this->get_no_hint_visible_expectation());
+    }
+
+    public function test_interactive_regrade_changing_num_tries_leaving_open() {
+        // Create a multichoice multiple question.
+        $q = test_question_maker::make_question('shortanswer');
+        $q->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+            new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+        );
+        $this->start_attempt_at_question($q, 'interactive', 3);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_tries_remaining_expectation(3));
+
+        // Submit the right answer.
+        $this->process_submission(array('answer' => 'frog', '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+
+        // Now change the quiestion so that answer is only partially right, and regrade.
+        $q->answers[13]->fraction = 0.6666667;
+        $q->answers[14]->fraction = 1;
+
+        $this->quba->regrade_all_questions(false);
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+    }
+
+    public function test_interactive_regrade_changing_num_tries_finished() {
+        // Create a multichoice multiple question.
+        $q = test_question_maker::make_question('shortanswer');
+        $q->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+            new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+        );
+        $this->start_attempt_at_question($q, 'interactive', 3);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_tries_remaining_expectation(3));
+
+        // Submit the right answer.
+        $this->process_submission(array('answer' => 'frog', '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+
+        // Now change the quiestion so that answer is only partially right, and regrade.
+        $q->answers[13]->fraction = 0.6666667;
+        $q->answers[14]->fraction = 1;
+
+        $this->quba->regrade_all_questions(true);
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedpartial);
+        // TODO I don't think 1 is the right fraction here. However, it is what
+        // you get attempting a question like this without regrading being involved,
+        // and I am currently interested in testing regrading here.
+        $this->check_current_mark(1);
+    }
+}
diff --git a/question/behaviour/interactivecountback/tests/walkthrough_test.php b/question/behaviour/interactivecountback/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..844ec22
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the interactive with
+ * countback behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage interactivecountback
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the interactive with countback behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactivecountback_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_interactive_feedback_match_reset() {
+
+        // Create a matching question.
+        $m = test_question_maker::make_a_matching_question();
+        $m->shufflestems = false;
+        $m->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+            new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+        );
+        $this->start_attempt_at_question($m, 'interactive', 12);
+
+        $choiceorder = $m->get_choice_order();
+        $orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
+        $choices = array(0 => get_string('choose') . '...');
+        foreach ($choiceorder as $key => $choice) {
+            $choices[$key] = $m->choices[$choice];
+        }
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->assertEquals('interactivecountback',
+                $this->quba->get_question_attempt($this->slot)->get_behaviour_name());
+        $this->check_current_output(
+                $this->get_contains_select_expectation('sub0', $choices, null, true),
+                $this->get_contains_select_expectation('sub1', $choices, null, true),
+                $this->get_contains_select_expectation('sub2', $choices, null, true),
+                $this->get_contains_select_expectation('sub3', $choices, null, true),
+                $this->get_contains_question_text_expectation($m),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(3),
+                $this->get_does_not_contain_num_parts_correct(),
+                $this->get_no_hint_visible_expectation());
+
+        // Submit an answer with two right, and two wrong.
+        $this->process_submission(array('sub0' => $orderforchoice[1],
+                'sub1' => $orderforchoice[1], 'sub2' => $orderforchoice[1],
+                'sub3' => $orderforchoice[1], '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], false),
+                $this->get_contains_select_expectation('sub1', $choices, $orderforchoice[1], false),
+                $this->get_contains_select_expectation('sub2', $choices, $orderforchoice[1], false),
+                $this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], false),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_contains_try_again_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                new question_pattern_expectation('/' .
+                        preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+                $this->get_contains_hint_expectation('This is the first hint'),
+                $this->get_contains_num_parts_correct(2),
+                $this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
+                $this->get_contains_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . 'sub0', $orderforchoice[1]),
+                $this->get_contains_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . 'sub1', '0'),
+                $this->get_contains_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . 'sub2', '0'),
+                $this->get_contains_hidden_expectation(
+                        $this->quba->get_field_prefix($this->slot) . 'sub3', $orderforchoice[1]));
+
+        // Check that extract responses will return the reset data.
+        $prefix = $this->quba->get_field_prefix($this->slot);
+        $this->assertEquals(array('sub0' => 1),
+                $this->quba->extract_responses($this->slot, array($prefix . 'sub0' => 1)));
+
+        // Do try again.
+        $this->process_submission(array('sub0' => $orderforchoice[1],
+                'sub3' => $orderforchoice[1], '-tryagain' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], true),
+                $this->get_contains_select_expectation('sub1', $choices, null, true),
+                $this->get_contains_select_expectation('sub2', $choices, null, true),
+                $this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], true),
+                $this->get_contains_submit_button_expectation(true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_tries_remaining_expectation(2),
+                $this->get_no_hint_visible_expectation());
+
+        // Submit the right answer.
+        $this->process_submission(array('sub0' => $orderforchoice[1],
+                'sub1' => $orderforchoice[2], 'sub2' => $orderforchoice[2],
+                'sub3' => $orderforchoice[1], '-submit' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(10);
+        $this->check_current_output(
+                $this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], false),
+                $this->get_contains_select_expectation('sub1', $choices, $orderforchoice[2], false),
+                $this->get_contains_select_expectation('sub2', $choices, $orderforchoice[2], false),
+                $this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], false),
+                $this->get_contains_submit_button_expectation(false),
+                $this->get_does_not_contain_try_again_button_expectation(),
+                $this->get_contains_correct_expectation(),
+                $this->get_contains_standard_correct_combined_feedback_expectation(),
+                new question_no_pattern_expectation('/class="control\b[^"]*\bpartiallycorrect"/'));
+    }
+}
diff --git a/question/behaviour/manualgraded/tests/walkthrough_test.php b/question/behaviour/manualgraded/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..32af729
--- /dev/null
@@ -0,0 +1,270 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests that walks a question through the manual graded
+ * behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage manualgraded
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the manual graded behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_test_base {
+    public function test_manual_graded_essay() {
+
+        // Create an essay question.
+        $essay = test_question_maker::make_an_essay_question();
+        $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+        // Check the right model is being used.
+        $this->assertEquals('manualgraded', $this->quba->get_question_attempt(
+                $this->slot)->get_behaviour_name());
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_question_text_expectation($essay),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Simulate some data submitted by the student.
+        $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_contains_tag_with_attribute('textarea', 'name',
+                $this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process the same data again, check it does not create a new step.
+        $numsteps = $this->get_step_count();
+        $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+        $this->check_step_count($numsteps);
+
+        // Process different data, check it creates a new step.
+        $this->process_submission(array('answer' => ''));
+        $this->check_step_count($numsteps + 1);
+        $this->check_current_state(question_state::$todo);
+
+        // Change back, check it creates a new step.
+        $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+        $this->check_step_count($numsteps + 2);
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->assertEquals('This is my wonderful essay!',
+                $this->quba->get_response_summary($this->slot));
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 10);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrright);
+        $this->check_current_mark(10);
+        $this->check_current_output(
+                new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+
+        // Now change the max mark for the question and regrade.
+        $this->quba->regrade_question($this->slot, true, 1);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrright);
+        $this->check_current_mark(1);
+    }
+
+    public function test_manual_graded_truefalse() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'manualgraded', 2);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_question_text_expectation($tf),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Process a true answer and check the expected result.
+        $this->process_submission(array('answer' => 1));
+
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_tf_true_radio_expectation(true, true),
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_does_not_contain_correctness_expectation(),
+                $this->get_does_not_contain_specific_feedback_expectation());
+
+        // Process a manual comment.
+        $this->manual_grade('Not good enough!', 1);
+
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+            $this->get_does_not_contain_correctness_expectation(),
+            $this->get_does_not_contain_specific_feedback_expectation(),
+            new question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
+    }
+
+    public function test_manual_graded_ignore_repeat_sumbission() {
+        // Create an essay question.
+        $essay = test_question_maker::make_an_essay_question();
+        $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+        // Check the right model is being used.
+        $this->assertEquals('manualgraded', $this->quba->get_question_attempt(
+                $this->slot)->get_behaviour_name());
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+
+        // Simulate some data submitted by the student.
+        $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->assertEquals('This is my wonderful essay!',
+                $this->quba->get_response_summary($this->slot));
+
+        // Process a blank manual comment. Ensure it does not change the state.
+        $numsteps = $this->get_step_count();
+        $this->manual_grade('', '');
+        $this->check_step_count($numsteps);
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+
+        // Process a comment, but with the mark blank. Should be recorded, but
+        // not change the mark.
+        $this->manual_grade('I am not sure what grade to award.', '');
+        $this->check_step_count($numsteps + 1);
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_pattern_expectation('/' .
+                        preg_quote('I am not sure what grade to award.') . '/'));
+
+        // Now grade it.
+        $this->manual_grade('Pretty good!', '9.00000');
+        $this->check_step_count($numsteps + 2);
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(9);
+        $this->check_current_output(
+                new question_pattern_expectation('/' . preg_quote('Pretty good!') . '/'));
+
+        // Process the same data again, and make sure it does not add a step.
+        $this->manual_grade('Pretty good!', '9.00000');
+        $this->check_step_count($numsteps + 2);
+        $this->check_current_state(question_state::$mangrpartial);
+        $this->check_current_mark(9);
+
+        // Now set the mark back to blank.
+        $this->manual_grade('Actually, I am not sure any more.', '');
+        $this->check_step_count($numsteps + 3);
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_pattern_expectation('/' .
+                        preg_quote('Actually, I am not sure any more.') . '/'));
+
+        $qa = $this->quba->get_question_attempt($this->slot);
+        $this->assertEquals('Commented: Actually, I am not sure any more.',
+                $qa->summarise_action($qa->get_last_step()));
+    }
+
+    public function test_manual_graded_essay_can_grade_0() {
+
+        // Create an essay question.
+        $essay = test_question_maker::make_an_essay_question();
+        $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+        // Check the right model is being used.
+        $this->assertEquals('manualgraded', $this->quba->get_question_attempt(
+                $this->slot)->get_behaviour_name());
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_question_text_expectation($essay),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Simulate some data submitted by the student.
+        $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_contains_tag_with_attribute('textarea', 'name',
+                $this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->assertEquals('This is my wonderful essay!',
+                $this->quba->get_response_summary($this->slot));
+
+        // Process a blank comment and a grade of 0.
+        $this->manual_grade('', 0);
+
+        // Verify.
+        $this->check_current_state(question_state::$mangrwrong);
+        $this->check_current_mark(0);
+    }
+}
diff --git a/question/behaviour/missing/tests/missingbehaviour_test.php b/question/behaviour/missing/tests/missingbehaviour_test.php
new file mode 100644 (file)
index 0000000..6c43c67
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the 'missing' behaviour.
+ *
+ * @package    qbehaviour
+ * @subpackage missing
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
+require_once(dirname(__FILE__) . '/../behaviour.php');
+
+
+/**
+ * Unit tests for the 'missing' behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_missing_test extends advanced_testcase {
+    public function test_missing_cannot_start() {
+        $qa = new question_attempt(test_question_maker::make_question('truefalse', 'true'), 0);
+        $behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
+        $this->setExpectedException('moodle_exception');
+        $behaviour->init_first_step(new question_attempt_step(array()), 1);
+    }
+
+    public function test_missing_cannot_process() {
+        $qa = new question_attempt(test_question_maker::make_question('truefalse', 'true'), 0);
+        $behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
+        $this->setExpectedException('moodle_exception');
+        $behaviour->process_action(new question_attempt_pending_step(array()));
+    }
+
+    public function test_missing_cannot_get_min_grade() {
+        $qa = new question_attempt(test_question_maker::make_question('truefalse', 'true'), 0);
+        $behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
+        $this->setExpectedException('moodle_exception');
+        $behaviour->get_min_fraction();
+    }
+
+    public function test_render_missing() {
+        $records = new question_test_recordset(array(
+            array('questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                   'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                            'questionsummary', 'rightanswer', 'responsesummary',
+                    'timemodified', 'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                       'timecreated', 'userid', 'name', 'value'),
+            array(1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 0, '', '', '',
+                    1256233790, 1, 0, 'todo',     null, 1256233700, 1,   '_order', '1,2,3'),
+            array(1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 0, '', '', '',
+                    1256233790, 2, 1, 'complete', 0.50, 1256233705, 1,  '-submit',  '1'),
+            array(1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 0, '', '', '',
+                    1256233790, 2, 1, 'complete', 0.50, 1256233705, 1,  'choice0',  '1'),
+        ));
+
+        $question = test_question_maker::make_question('truefalse', 'true');
+        $question->id = -1;
+
+        question_bank::start_unit_test();
+        question_bank::load_test_question_data($question);
+        $qa = question_attempt::load_from_records($records, 1,
+                new question_usage_null_observer(), 'deferredfeedback');
+        question_bank::end_unit_test();
+
+        $this->assertEquals(2, $qa->get_num_steps());
+
+        $step = $qa->get_step(0);
+        $this->assertEquals(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233700, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('_order' => '1,2,3'), $step->get_all_data());
+
+        $step = $qa->get_step(1);
+        $this->assertEquals(question_state::$complete, $step->get_state());
+        $this->assertEquals(0.5, $step->get_fraction());
+        $this->assertEquals(1256233705, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('-submit' => '1', 'choice0' => '1'), $step->get_all_data());
+
+        $output = $qa->render(new question_display_options(), '1');
+        $this->assertRegExp('/' . preg_quote($qa->get_question()->questiontext) . '/', $output);
+        $this->assertRegExp('/' . preg_quote(
+                get_string('questionusedunknownmodel', 'qbehaviour_missing')) . '/', $output);
+        $this->assertTag(array('tag'=>'div', 'attributes'=>array('class'=>'warning')), $output);
+    }
+}
diff --git a/question/engine/tests/datalib_test.php b/question/engine/tests/datalib_test.php
new file mode 100644 (file)
index 0000000..79ffb4e
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for some of the code in ../datalib.php.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+
+
+/**
+ * Unit tests for some of the code in ../datalib.php.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qubaid_condition_test extends advanced_testcase {
+
+    protected function normalize_sql($sql, $params) {
+        $newparams = array();
+        preg_match_all('/(?<!:):([a-z][a-z0-9_]*)/', $sql, $named_matches);
+        foreach($named_matches[1] as $param) {
+            if (array_key_exists($param, $params)) {
+                $newparams[] = $params[$param];
+            }
+        }
+        $newsql = preg_replace('/(?<!:):[a-z][a-z0-9_]*/', '?', $sql);
+        return array($newsql, $newparams);
+    }
+
+    protected function check_typical_question_attempts_query(
+            qubaid_condition $qubaids, $expectedsql, $expectedparams) {
+        $sql = "SELECT qa.id, qa.maxmark
+            FROM {$qubaids->from_question_attempts('qa')}
+            WHERE {$qubaids->where()} AND qa.slot = :slot";
+        $params = $qubaids->from_where_params();
+        $params['slot'] = 1;
+
+        // NOTE: parameter names may change thanks to $DB->inorequaluniqueindex, normal comparison is very wrong!!
+        list($sql, $params) = $this->normalize_sql($sql, $params);
+        list($expectedsql, $expectedparams) = $this->normalize_sql($expectedsql, $expectedparams);
+
+        $this->assertEquals($expectedsql, $sql);
+        $this->assertEquals($expectedparams, $params);
+    }
+
+    protected function check_typical_in_query(qubaid_condition $qubaids,
+            $expectedsql, $expectedparams) {
+        $sql = "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid {$qubaids->usage_id_in()}";
+
+        // NOTE: parameter names may change thanks to $DB->inorequaluniqueindex, normal comparison is very wrong!!
+        list($sql, $params) = $this->normalize_sql($sql, $qubaids->usage_id_in_params());
+        list($expectedsql, $expectedparams) = $this->normalize_sql($expectedsql, $expectedparams);
+
+        $this->assertEquals($expectedsql, $sql);
+        $this->assertEquals($expectedparams, $params);
+    }
+
+    public function test_qubaid_list_one_join() {
+        $qubaids = new qubaid_list(array(1));
+        $this->check_typical_question_attempts_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid = :qubaid1 AND qa.slot = :slot",
+            array('qubaid1' => 1, 'slot' => 1));
+    }
+
+    public function test_qubaid_list_several_join() {
+        $qubaids = new qubaid_list(array(1, 3, 7));
+        $this->check_typical_question_attempts_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid IN (:qubaid2,:qubaid3,:qubaid4) AND qa.slot = :slot",
+            array('qubaid2' => 1, 'qubaid3' => 3, 'qubaid4' => 7, 'slot' => 1));
+    }
+
+    public function test_qubaid_join() {
+        $qubaids = new qubaid_join("{other_table} ot", 'ot.usageid', 'ot.id = 1');
+
+        $this->check_typical_question_attempts_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {other_table} ot
+                JOIN {question_attempts} qa ON qa.questionusageid = ot.usageid
+            WHERE ot.id = 1 AND qa.slot = :slot", array('slot' => 1));
+    }
+
+    public function test_qubaid_join_no_where_join() {
+        $qubaids = new qubaid_join("{other_table} ot", 'ot.usageid');
+
+        $this->check_typical_question_attempts_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {other_table} ot
+                JOIN {question_attempts} qa ON qa.questionusageid = ot.usageid
+            WHERE 1 = 1 AND qa.slot = :slot", array('slot' => 1));
+    }
+
+    public function test_qubaid_list_one_in() {
+        global $CFG;
+        $qubaids = new qubaid_list(array(1));
+        $this->check_typical_in_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid = :qubaid5", array('qubaid5' => 1));
+    }
+
+    public function test_qubaid_list_several_in() {
+        global $CFG;
+        $qubaids = new qubaid_list(array(1, 2, 3));
+        $this->check_typical_in_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid IN (:qubaid6,:qubaid7,:qubaid8)",
+                array('qubaid6' => 1, 'qubaid7' => 2, 'qubaid8' => 3));
+    }
+
+    public function test_qubaid_join_in() {
+        global $CFG;
+        $qubaids = new qubaid_join("{other_table} ot", 'ot.usageid', 'ot.id = 1');
+
+        $this->check_typical_in_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid IN (SELECT ot.usageid FROM {other_table} ot WHERE ot.id = 1)",
+                array());
+    }
+
+    public function test_qubaid_join_no_where_in() {
+        global $CFG;
+        $qubaids = new qubaid_join("{other_table} ot", 'ot.usageid');
+
+        $this->check_typical_in_query($qubaids,
+                "SELECT qa.id, qa.maxmark
+            FROM {question_attempts} qa
+            WHERE qa.questionusageid IN (SELECT ot.usageid FROM {other_table} ot WHERE 1 = 1)",
+                array());
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/helpers.php b/question/engine/tests/helpers.php
new file mode 100644 (file)
index 0000000..14425d6
--- /dev/null
@@ -0,0 +1,1016 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains helper classes for testing the question engine.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+
+
+/**
+ * Makes some protected methods of question_attempt public to facilitate testing.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_question_attempt extends question_attempt {
+    public function add_step(question_attempt_step $step) {
+        parent::add_step($step);
+    }
+    public function set_min_fraction($fraction) {
+        $this->minfraction = $fraction;
+    }
+    public function set_behaviour(question_behaviour $behaviour) {
+        $this->behaviour = $behaviour;
+    }
+}
+
+
+/**
+ * Base class for question type test helpers.
+ *
+ * @copyright  2011 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_test_helper {
+    /**
+     * @return array of example question names that can be passed as the $which
+     * argument of {@link test_question_maker::make_question} when $qtype is
+     * this question type.
+     */
+    abstract public function get_test_questions();
+}
+
+
+/**
+ * This class creates questions of various types, which can then be used when
+ * testing.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_question_maker {
+    const STANDARD_OVERALL_CORRECT_FEEDBACK = 'Well done!';
+    const STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK =
+        'Parts, but only parts, of your response are correct.';
+    const STANDARD_OVERALL_INCORRECT_FEEDBACK = 'That is not right at all.';
+
+    /** @var array qtype => qtype test helper class. */
+    protected static $testhelpers = array();
+
+    /**
+     * Just make a question_attempt at a question. Useful for unit tests that
+     * need to pass a $qa to methods that call format_text. Probably not safe
+     * to use for anything beyond that.
+     * @param question_definition $question a question.
+     * @param number $maxmark the max mark to set.
+     * @return question_attempt the question attempt.
+     */
+    public static function get_a_qa($question, $maxmark = 3) {
+        return new question_attempt($question, 13, null, $maxmark);
+    }
+
+    /**
+     * Initialise the common fields of a question of any type.
+     */
+    public static function initialise_a_question($q) {
+        global $USER;
+
+        $q->id = 0;
+        $q->category = 0;
+        $q->parent = 0;
+        $q->questiontextformat = FORMAT_HTML;
+        $q->generalfeedbackformat = FORMAT_HTML;
+        $q->defaultmark = 1;
+        $q->penalty = 0.3333333;
+        $q->length = 1;
+        $q->stamp = make_unique_id_code();
+        $q->version = make_unique_id_code();
+        $q->hidden = 0;
+        $q->timecreated = time();
+        $q->timemodified = time();
+        $q->createdby = $USER->id;
+        $q->modifiedby = $USER->id;
+    }
+
+    public static function initialise_question_data($qdata) {
+        global $USER;
+
+        $qdata->id = 0;
+        $qdata->category = 0;
+        $qdata->contextid = 0;
+        $qdata->parent = 0;
+        $qdata->questiontextformat = FORMAT_HTML;
+        $qdata->generalfeedbackformat = FORMAT_HTML;
+        $qdata->defaultmark = 1;
+        $qdata->penalty = 0.3333333;
+        $qdata->length = 1;
+        $qdata->stamp = make_unique_id_code();
+        $qdata->version = make_unique_id_code();
+        $qdata->hidden = 0;
+        $qdata->timecreated = time();
+        $qdata->timemodified = time();
+        $qdata->createdby = $USER->id;
+        $qdata->modifiedby = $USER->id;
+        $qdata->hints = array();
+    }
+
+    public static function initialise_question_form_data($qdata) {
+        $formdata = new stdClass();
+        $formdata->id = 0;
+        $formdata->category = '0,0';
+        $formdata->usecurrentcat = 1;
+        $formdata->categorymoveto = '0,0';
+        $formdata->tags = array();
+        $formdata->penalty = 0.3333333;
+        $formdata->questiontextformat = FORMAT_HTML;
+        $formdata->generalfeedbackformat = FORMAT_HTML;
+    }
+
+    /**
+     * Get the test helper class for a particular question type.
+     * @param $qtype the question type name, e.g. 'multichoice'.
+     * @return question_test_helper the test helper class.
+     */
+    public static function get_test_helper($qtype) {
+        global $CFG;
+
+        if (array_key_exists($qtype, self::$testhelpers)) {
+            return self::$testhelpers[$qtype];
+        }
+
+        $file = get_plugin_directory('qtype', $qtype) . '/tests/helper.php';
+        if (!is_readable($file)) {
+            throw new coding_exception('Question type ' . $qtype .
+                ' does not have test helper code.');
+        }
+        include_once($file);
+
+        $class = 'qtype_' . $qtype . '_test_helper';
+        if (!class_exists($class)) {
+            throw new coding_exception('Class ' . $class . ' is not defined in ' . $file);
+        }
+
+        self::$testhelpers[$qtype] = new $class();
+        return self::$testhelpers[$qtype];
+    }
+
+    /**
+     * Call a method on a qtype_{$qtype}_test_helper class and return the result.
+     *
+     * @param string $methodtemplate e.g. 'make_{qtype}_question_{which}';
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @param unknown_type $which
+     */
+    protected static function call_question_helper_method($methodtemplate, $qtype, $which = null) {
+        $helper = self::get_test_helper($qtype);
+
+        $available = $helper->get_test_questions();
+
+        if (is_null($which)) {
+            $which = reset($available);
+        } else if (!in_array($which, $available)) {
+            throw new coding_exception('Example question ' . $which . ' of type ' .
+                $qtype . ' does not exist.');
+        }
+
+        $method = str_replace(array('{qtype}', '{which}'),
+            array($qtype,    $which), $methodtemplate);
+
+        if (!method_exists($helper, $method)) {
+            throw new coding_exception('Method ' . $method . ' does not exist on the' .
+                $qtype . ' question type test helper class.');
+        }
+
+        return $helper->$method();
+    }
+
+    /**
+     * Question types can provide a number of test question defintions.
+     * They do this by creating a qtype_{$qtype}_test_helper class that extends
+     * question_test_helper. The get_test_questions method returns the list of
+     * test questions available for this question type.
+     *
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @return question_definition the requested question object.
+     */
+    public static function make_question($qtype, $which = null) {
+        return self::call_question_helper_method('make_{qtype}_question_{which}',
+            $qtype, $which);
+    }
+
+    /**
+     * Like {@link make_question()} but returns the datastructure from
+     * get_question_options instead of the question_definition object.
+     *
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @return stdClass the requested question object.
+     */
+    public static function get_question_data($qtype, $which = null) {
+        return self::call_question_helper_method('get_{qtype}_question_data_{which}',
+            $qtype, $which);
+    }
+
+    /**
+     * Like {@link make_question()} but returns the data what would be saved from
+     * the question editing form instead of the question_definition object.
+     *
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @return stdClass the requested question object.
+     */
+    public static function get_question_form_data($qtype, $which = null) {
+        return self::call_question_helper_method('get_{qtype}_question_form_data_{which}',
+            $qtype, $which);
+    }
+
+    /**
+     * Makes a multichoice question with choices 'A', 'B' and 'C' shuffled. 'A'
+     * is correct, defaultmark 1.
+     * @return qtype_multichoice_single_question
+     */
+    public static function make_a_multichoice_single_question() {
+        question_bank::load_question_definition_classes('multichoice');
+        $mc = new qtype_multichoice_single_question();
+        self::initialise_a_question($mc);
+        $mc->name = 'Multi-choice question, single response';
+        $mc->questiontext = 'The answer is A.';
+        $mc->generalfeedback = 'You should have selected A.';
+        $mc->qtype = question_bank::get_qtype('multichoice');
+
+        $mc->shuffleanswers = 1;
+        $mc->answernumbering = 'abc';
+
+        $mc->answers = array(
+            13 => new question_answer(13, 'A', 1, 'A is right', FORMAT_HTML),
+            14 => new question_answer(14, 'B', -0.3333333, 'B is wrong', FORMAT_HTML),
+            15 => new question_answer(15, 'C', -0.3333333, 'C is wrong', FORMAT_HTML),
+        );
+
+        return $mc;
+    }
+
+    /**
+     * Makes a multichoice question with choices 'A', 'B', 'C' and 'D' shuffled.
+     * 'A' and 'C' is correct, defaultmark 1.
+     * @return qtype_multichoice_multi_question
+     */
+    public static function make_a_multichoice_multi_question() {
+        question_bank::load_question_definition_classes('multichoice');
+        $mc = new qtype_multichoice_multi_question();
+        self::initialise_a_question($mc);
+        $mc->name = 'Multi-choice question, multiple response';
+        $mc->questiontext = 'The answer is A and C.';
+        $mc->generalfeedback = 'You should have selected A and C.';
+        $mc->qtype = question_bank::get_qtype('multichoice');
+
+        $mc->shuffleanswers = 1;
+        $mc->answernumbering = 'abc';
+
+        self::set_standard_combined_feedback_fields($mc);
+
+        $mc->answers = array(
+            13 => new question_answer(13, 'A', 0.5, 'A is part of the right answer', FORMAT_HTML),
+            14 => new question_answer(14, 'B', -1, 'B is wrong', FORMAT_HTML),
+            15 => new question_answer(15, 'C', 0.5, 'C is part of the right answer', FORMAT_HTML),
+            16 => new question_answer(16, 'D', -1, 'D is wrong', FORMAT_HTML),
+        );
+
+        return $mc;
+    }
+
+    /**
+     * Makes a matching question to classify 'Dog', 'Frog', 'Toad' and 'Cat' as
+     * 'Mammal', 'Amphibian' or 'Insect'.
+     * defaultmark 1. Stems are shuffled by default.
+     * @return qtype_match_question
+     */
+    public static function make_a_matching_question() {
+        question_bank::load_question_definition_classes('match');
+        $match = new qtype_match_question();
+        self::initialise_a_question($match);
+        $match->name = 'Matching question';
+        $match->questiontext = 'Classify the animals.';
+        $match->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.';
+        $match->qtype = question_bank::get_qtype('match');
+
+        $match->shufflestems = 1;
+
+        self::set_standard_combined_feedback_fields($match);
+
+        // Using unset to get 1-based arrays.
+        $match->stems = array('', 'Dog', 'Frog', 'Toad', 'Cat');
+        $match->stemformat = array('', FORMAT_HTML, FORMAT_HTML, FORMAT_HTML, FORMAT_HTML);
+        $match->choices = array('', 'Mammal', 'Amphibian', 'Insect');
+        $match->right = array('', 1, 2, 2, 1);
+        unset($match->stems[0]);
+        unset($match->stemformat[0]);
+        unset($match->choices[0]);
+        unset($match->right[0]);
+
+        return $match;
+    }
+
+    /**
+     * Makes a truefalse question with correct ansewer true, defaultmark 1.
+     * @return qtype_essay_question
+     */
+    public static function make_an_essay_question() {
+        question_bank::load_question_definition_classes('essay');
+        $essay = new qtype_essay_question();
+        self::initialise_a_question($essay);
+        $essay->name = 'Essay question';
+        $essay->questiontext = 'Write an essay.';
+        $essay->generalfeedback = 'I hope you wrote an interesting essay.';
+        $essay->penalty = 0;
+        $essay->qtype = question_bank::get_qtype('essay');
+
+        $essay->responseformat = 'editor';
+        $essay->responsefieldlines = 15;
+        $essay->attachments = 0;
+        $essay->graderinfo = '';
+        $essay->graderinfoformat = FORMAT_MOODLE;
+
+        return $essay;
+    }
+
+    /**
+     * Add some standard overall feedback to a question. You need to use these
+     * specific feedback strings for the corresponding contains_..._feedback
+     * methods in {@link qbehaviour_walkthrough_test_base} to works.
+     * @param question_definition $q the question to add the feedback to.
+     */
+    public static function set_standard_combined_feedback_fields($q) {
+        $q->correctfeedback = self::STANDARD_OVERALL_CORRECT_FEEDBACK;
+        $q->correctfeedbackformat = FORMAT_HTML;
+        $q->partiallycorrectfeedback = self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK;
+        $q->partiallycorrectfeedbackformat = FORMAT_HTML;
+        $q->shownumcorrect = true;
+        $q->incorrectfeedback = self::STANDARD_OVERALL_INCORRECT_FEEDBACK;
+        $q->incorrectfeedbackformat = FORMAT_HTML;
+    }
+}
+
+
+/**
+ * Helper for tests that need to simulate records loaded from the database.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class testing_db_record_builder {
+    public static function build_db_records(array $table) {
+        $columns = array_shift($table);
+        $records = array();
+        foreach ($table as $row) {
+            if (count($row) != count($columns)) {
+                throw new coding_exception("Row contains the wrong number of fields.");
+            }
+            $rec = new stdClass();
+            foreach ($columns as $i => $name) {
+                $rec->$name = $row[$i];
+            }
+            $records[] = $rec;
+        }
+        return $records;
+    }
+}
+
+
+/**
+ * Helper base class for tests that need to simulate records loaded from the
+ * database.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_loading_method_test_base extends advanced_testcase {
+    public function build_db_records(array $table) {
+        return testing_db_record_builder::build_db_records($table);
+    }
+    public function test_must_have_methods() {
+        // each test case must have at least one method..
+    }
+}
+
+
+class question_testcase extends advanced_testcase {
+
+    public function test_must_have_methods() {
+        // each test case must have at least one method..
+    }
+
+    public function assert($expectation, $compare, $message = '') {
+        $message = (isset($expectation->message) and $expectation->message !== '') ? $expectation->message : $message;
+
+        if (get_class($expectation) === 'question_pattern_expectation') {
+            $this->assertRegExp($expectation->pattern, $compare, $message);
+            return;
+
+        } else if (get_class($expectation) === 'question_no_pattern_expectation') {
+            $this->assertNotRegExp($expectation->pattern, $compare, $message);
+            return;
+
+        } else if (get_class($expectation) === 'question_contains_tag_with_attributes') {
+            $this->assertTag(array('tag'=>$expectation->tag, 'attributes'=>$expectation->expectedvalues), $compare, $message);
+            foreach ($expectation->forbiddenvalues as $k=>$v) {
+                $attr = $expectation->expectedvalues;
+                $attr[$k] = $v;
+                $this->assertNotTag(array('tag'=>$expectation->tag, 'attributes'=>$attr), $compare, $message);
+            }
+            return;
+
+        } else if (get_class($expectation) === 'question_contains_tag_with_attribute') {
+            $attr = array($expectation->attribute=>$expectation->value);
+            $this->assertTag(array('tag'=>$expectation->tag, 'attributes'=>$attr), $compare, $message);
+            return;
+
+        } else if (get_class($expectation) === 'question_does_not_contain_tag_with_attributes') {
+            $this->assertNotTag(array('tag'=>$expectation->tag, 'attributes'=>$expectation->attributes), $compare, $message);
+            return;
+
+        } else if (get_class($expectation) === 'question_contains_select_expectation') {
+            $tag = array('tag'=>'select', 'attributes'=>array('name'=>$expectation->name),
+                'children'=>array('count'=>count($expectation->choices)));
+            if (!$expectation->enabled) {
+                $tag['attributes']['disabled'] = 'disabled';
+            }
+            foreach(array_keys($expectation->choices) as $value) {
+                if ($expectation->selected === $value) {
+                    $tag['child'] = array('tag'=>'option', 'attributes'=>array('value'=>$value, 'selected'=>'selected'));
+                } else {
+                    $tag['child'] = array('tag'=>'option', 'attributes'=>array('value'=>$value));
+                }
+                $this->assertTag($tag, $compare, $message);
+            }
+            return;
+
+        } else if (get_class($expectation) === 'question_check_specified_fields_expectation') {
+            $expect = (array)$expectation->expect;
+            $compare = (array)$compare;
+            foreach ($expect as $k=>$v) {
+                if (!array_key_exists($k, $compare)) {
+                    if (!$message) {
+                        $message = "Property $k does not exist";
+                    }
+                    $this->fail($message);
+                }
+                if ($v != $compare[$k]) {
+                    if (!$message) {
+                        $message = "Property $k is different";
+                    }
+                    $this->fail($message);
+                }
+            }
+            $this->assertTrue(true);
+            return;
+
+        } else if (get_class($expectation) === 'question_contains_tag_with_contents') {
+            $this->assertTag(array('tag'=>$expectation->tag, 'content'=>$expectation->content), $compare, $message);
+            return;
+        }
+
+        throw new coding_exception('Unknown expectiontion:'.get_class($expectation));
+    }
+}
+
+
+class question_contains_tag_with_contents {
+    public $tag;
+    public $content;
+    public $message;
+
+    public function __construct($tag, $content, $message = '') {
+        $this->tag = $tag;
+        $this->content = $content;
+        $this->message = $message;
+    }
+
+}
+
+class question_check_specified_fields_expectation {
+    public $expect;
+    public $message;
+
+    function __construct($expected, $message = '') {
+        $this->expect = $expected;
+        $this->message = $message;
+    }
+}
+
+
+class question_contains_select_expectation {
+    public $name;
+    public $choices;
+    public $selected;
+    public $enabled;
+    public $message;
+
+    public function __construct($name, $choices, $selected = null, $enabled = null, $message = '') {
+        $this->name = $name;
+        $this->choices = $choices;
+        $this->selected = $selected;
+        $this->enabled = $enabled;
+        $this->message = $message;
+    }
+}
+
+
+class question_does_not_contain_tag_with_attributes {
+    public $tag;
+    public $attributes;
+    public $message;
+
+    public function __construct($tag, $attributes, $message = '') {
+        $this->tag = $tag;
+        $this->attributes = $attributes;
+        $this->message = $message;
+    }
+}
+
+
+class question_contains_tag_with_attribute {
+    public $tag;
+    public $attribute;
+    public $value;
+    public $message;
+
+    public function __construct($tag, $attribute, $value, $message = '') {
+        $this->tag = $tag;
+        $this->attribute = $attribute;
+        $this->value = $value;
+        $this->message = $message;
+    }
+}
+
+
+class question_contains_tag_with_attributes {
+    public $tag;
+    public $expectedvalues = array();
+    public $forbiddenvalues = array();
+    public $message;
+
+    public function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '') {
+        $this->tag = $tag;
+        $this->expectedvalues = $expectedvalues;
+        $this->forbiddenvalues = $forbiddenvalues;
+        $this->message = $message;
+    }
+}
+
+
+class question_pattern_expectation {
+    public $pattern;
+    public $message;
+
+    public function __construct($pattern, $message = '') {
+        $this->pattern = $pattern;
+        $this->message = $message;
+    }
+}
+
+
+class question_no_pattern_expectation {
+    public $pattern;
+    public $message;
+
+    public function __construct($pattern, $message = '') {
+        $this->pattern = $pattern;
+        $this->message = $message;
+    }
+}
+
+
+/**
+ * Helper base class for tests that walk a question through a sequents of
+ * interactions under the control of a particular behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_walkthrough_test_base extends question_testcase {
+    /** @var question_display_options */
+    protected $displayoptions;
+    /** @var question_usage_by_activity */
+    protected $quba;
+    /** @var unknown_type integer */
+    protected $slot;
+
+    protected function setUp() {
+        parent::setUp();
+        $this->resetAfterTest(true);
+
+        $this->displayoptions = new question_display_options();
+        $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
+            get_context_instance(CONTEXT_SYSTEM));
+    }
+
+    protected function tearDown() {
+        $this->displayoptions = null;
+        $this->quba = null;
+        parent::tearDown();
+    }
+
+    protected function start_attempt_at_question($question, $preferredbehaviour,
+                                                 $maxmark = null, $variant = 1) {
+        $this->quba->set_preferred_behaviour($preferredbehaviour);
+        $this->slot = $this->quba->add_question($question, $maxmark);
+        $this->quba->start_question($this->slot, $variant);
+    }
+    protected function process_submission($data) {
+        $this->quba->process_action($this->slot, $data);
+    }
+
+    protected function manual_grade($comment, $mark) {
+        $this->quba->manual_grade($this->slot, $comment, $mark);
+    }
+
+    protected function check_current_state($state) {
+        $this->assertEquals($this->quba->get_question_state($this->slot), $state,
+            'Questions is in the wrong state: %s.');
+    }
+
+    protected function check_current_mark($mark) {
+        if (is_null($mark)) {
+            $this->assertNull($this->quba->get_question_mark($this->slot));
+        } else {
+            if ($mark == 0) {
+                // PHP will think a null mark and a mark of 0 are equal,
+                // so explicity check not null in this case.
+                $this->assertNotNull($this->quba->get_question_mark($this->slot));
+            }
+            $this->assertEquals($mark, $this->quba->get_question_mark($this->slot),
+                'Expected mark and actual mark differ: %s.', 0.000001);
+        }
+    }
+
+    /**
+     * @param $condition one or more Expectations. (users varargs).
+     */
+    protected function check_current_output() {
+        $html = $this->quba->render_question($this->slot, $this->displayoptions);
+        foreach (func_get_args() as $condition) {
+            $this->assert($condition, $html);
+        }
+    }
+
+    protected function get_question_attempt() {
+        return $this->quba->get_question_attempt($this->slot);
+    }
+
+    protected function get_step_count() {
+        return $this->get_question_attempt()->get_num_steps();
+    }
+
+    protected function check_step_count($expectednumsteps) {
+        $this->assertEquals($expectednumsteps, $this->get_step_count());
+    }
+
+    protected function get_step($stepnum) {
+        return $this->get_question_attempt()->get_step($stepnum);
+    }
+
+    protected function get_contains_question_text_expectation($question) {
+        return new question_pattern_expectation('/' . preg_quote($question->questiontext) . '/');
+    }
+
+    protected function get_contains_general_feedback_expectation($question) {
+        return new question_pattern_expectation('/' . preg_quote($question->generalfeedback) . '/');
+    }
+
+    protected function get_does_not_contain_correctness_expectation() {
+        return new question_no_pattern_expectation('/class=\"correctness/');
+    }
+
+    protected function get_contains_correct_expectation() {
+        return new question_pattern_expectation('/' . preg_quote(get_string('correct', 'question')) . '/');
+    }
+
+    protected function get_contains_partcorrect_expectation() {
+        return new question_pattern_expectation('/' .
+            preg_quote(get_string('partiallycorrect', 'question')) . '/');
+    }
+
+    protected function get_contains_incorrect_expectation() {
+        return new question_pattern_expectation('/' . preg_quote(get_string('incorrect', 'question')) . '/');
+    }
+
+    protected function get_contains_standard_correct_combined_feedback_expectation() {
+        return new question_pattern_expectation('/' .
+            preg_quote(test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK) . '/');
+    }
+
+    protected function get_contains_standard_partiallycorrect_combined_feedback_expectation() {
+        return new question_pattern_expectation('/' .
+            preg_quote(test_question_maker::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK) . '/');
+    }
+
+    protected function get_contains_standard_incorrect_combined_feedback_expectation() {
+        return new question_pattern_expectation('/' .
+            preg_quote(test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK) . '/');
+    }
+
+    protected function get_does_not_contain_feedback_expectation() {
+        return new question_no_pattern_expectation('/class="feedback"/');
+    }
+
+    protected function get_does_not_contain_num_parts_correct() {
+        return new question_no_pattern_expectation('/class="numpartscorrect"/');
+    }
+
+    protected function get_contains_num_parts_correct($num) {
+        $a = new stdClass();
+        $a->num = $num;
+        return new question_pattern_expectation('/<div class="numpartscorrect">' .
+            preg_quote(get_string('yougotnright', 'question', $a)) . '/');
+    }
+
+    protected function get_does_not_contain_specific_feedback_expectation() {
+        return new question_no_pattern_expectation('/class="specificfeedback"/');
+    }
+
+    protected function get_contains_validation_error_expectation() {
+        return new question_contains_tag_with_attribute('div', 'class', 'validationerror');
+    }
+
+    protected function get_does_not_contain_validation_error_expectation() {
+        return new question_no_pattern_expectation('/class="validationerror"/');
+    }
+
+    protected function get_contains_mark_summary($mark) {
+        $a = new stdClass();
+        $a->mark = format_float($mark, $this->displayoptions->markdp);
+        $a->max = format_float($this->quba->get_question_max_mark($this->slot),
+            $this->displayoptions->markdp);
+        return new question_pattern_expectation('/' .
+            preg_quote(get_string('markoutofmax', 'question', $a)) . '/');
+    }
+
+    protected function get_contains_marked_out_of_summary() {
+        $max = format_float($this->quba->get_question_max_mark($this->slot),
+            $this->displayoptions->markdp);
+        return new question_pattern_expectation('/' .
+            preg_quote(get_string('markedoutofmax', 'question', $max)) . '/');
+    }
+
+    protected function get_does_not_contain_mark_summary() {
+        return new question_no_pattern_expectation('/<div class="grade">/');
+    }
+
+    protected function get_contains_checkbox_expectation($baseattr, $enabled, $checked) {
+        $expectedattributes = $baseattr;
+        $forbiddenattributes = array();
+        $expectedattributes['type'] = 'checkbox';
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        if ($checked === true) {
+            $expectedattributes['checked'] = 'checked';
+        } else if ($checked === false) {
+            $forbiddenattributes['checked'] = 'checked';
+        }
+        return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_contains_mc_checkbox_expectation($index, $enabled = null,
+                                                            $checked = null) {
+        return $this->get_contains_checkbox_expectation(array(
+            'name' => $this->quba->get_field_prefix($this->slot) . $index,
+            'value' => 1,
+        ), $enabled, $checked);
+    }
+
+    protected function get_contains_radio_expectation($baseattr, $enabled, $checked) {
+        $expectedattributes = $baseattr;
+        $forbiddenattributes = array();
+        $expectedattributes['type'] = 'radio';
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        if ($checked === true) {
+            $expectedattributes['checked'] = 'checked';
+        } else if ($checked === false) {
+            $forbiddenattributes['checked'] = 'checked';
+        }
+        return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_contains_mc_radio_expectation($index, $enabled = null, $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+            'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
+            'value' => $index,
+        ), $enabled, $checked);
+    }
+
+    protected function get_contains_hidden_expectation($name, $value = null) {
+        $expectedattributes = array('type' => 'hidden', 'name' => s($name));
+        if (!is_null($value)) {
+            $expectedattributes['value'] = s($value);
+        }
+        return new question_contains_tag_with_attributes('input', $expectedattributes);
+    }
+
+    protected function get_does_not_contain_hidden_expectation($name, $value = null) {
+        $expectedattributes = array('type' => 'hidden', 'name' => s($name));
+        if (!is_null($value)) {
+            $expectedattributes['value'] = s($value);
+        }
+        return new question_does_not_contain_tag_with_attributes('input', $expectedattributes);
+    }
+
+    protected function get_contains_tf_true_radio_expectation($enabled = null, $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+            'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
+            'value' => 1,
+        ), $enabled, $checked);
+    }
+
+    protected function get_contains_tf_false_radio_expectation($enabled = null, $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+            'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
+            'value' => 0,
+        ), $enabled, $checked);
+    }
+
+    protected function get_contains_cbm_radio_expectation($certainty, $enabled = null,
+                                                          $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+            'name' => $this->quba->get_field_prefix($this->slot) . '-certainty',
+            'value' => $certainty,
+        ), $enabled, $checked);
+    }
+
+    protected function get_contains_button_expectation($name, $value = null, $enabled = null) {
+        $expectedattributes = array(
+            'type' => 'submit',
+            'name' => $name,
+        );
+        $forbiddenattributes = array();
+        if (!is_null($value)) {
+            $expectedattributes['value'] = $value;
+        }
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_contains_submit_button_expectation($enabled = null) {
+        return $this->get_contains_button_expectation(
+            $this->quba->get_field_prefix($this->slot) . '-submit', null, $enabled);
+    }
+
+    protected function get_tries_remaining_expectation($n) {
+        return new question_pattern_expectation('/' .
+            preg_quote(get_string('triesremaining', 'qbehaviour_interactive', $n)) . '/');
+    }
+
+    protected function get_invalid_answer_expectation() {
+        return new question_pattern_expectation('/' .
+            preg_quote(get_string('invalidanswer', 'question')) . '/');
+    }
+
+    protected function get_contains_try_again_button_expectation($enabled = null) {
+        $expectedattributes = array(
+            'type' => 'submit',
+            'name' => $this->quba->get_field_prefix($this->slot) . '-tryagain',
+        );
+        $forbiddenattributes = array();
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_does_not_contain_try_again_button_expectation() {
+        return new question_no_pattern_expectation('/name="' .
+            $this->quba->get_field_prefix($this->slot) . '-tryagain"/');
+    }
+
+    protected function get_contains_select_expectation($name, $choices,
+                                                       $selected = null, $enabled = null) {
+        $fullname = $this->quba->get_field_prefix($this->slot) . $name;
+        return new question_contains_select_expectation($fullname, $choices, $selected, $enabled);
+    }
+
+    protected function get_mc_right_answer_index($mc) {
+        $order = $mc->get_order($this->get_question_attempt());
+        foreach ($order as $i => $ansid) {
+            if ($mc->answers[$ansid]->fraction == 1) {
+                return $i;
+            }
+        }
+        $this->fail('This multiple choice question does not seem to have a right answer!');
+    }
+
+    protected function get_no_hint_visible_expectation() {
+        return new question_no_pattern_expectation('/class="hint"/');
+    }
+
+    protected function get_contains_hint_expectation($hinttext) {
+        // Does not currently verify hint text.
+        return new question_contains_tag_with_attribute('div', 'class', 'hint');
+    }
+}
+
+/**
+ * Simple class that implements the {@link moodle_recordset} API based on an
+ * array of test data.
+ *
+ *  See the {@link question_attempt_step_db_test} class in
+ *  question/engine/tests/testquestionattemptstep.php for an example of how
+ *  this is used.
+ *
+ * @copyright  2011 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_test_recordset extends moodle_recordset {
+    protected $records;
+
+    /**
+     * Constructor
+     * @param $table as for {@link testing_db_record_builder::build_db_records()}
+     *      but does not need a unique first column.
+     */
+    public function __construct(array $table) {
+        $columns = array_shift($table);
+        $this->records = array();
+        foreach ($table as $row) {
+            if (count($row) != count($columns)) {
+                throw new coding_exception("Row contains the wrong number of fields.");
+            }
+            $rec = array();
+            foreach ($columns as $i => $name) {
+                $rec[$name] = $row[$i];
+            }
+            $this->records[] = $rec;
+        }
+        reset($this->records);
+    }
+
+    public function __destruct() {
+        $this->close();
+    }
+
+    public function current() {
+        return (object) current($this->records);
+    }
+
+    public function key() {
+        if (is_null(key($this->records))) {
+            return false;
+        }
+        $current = current($this->records);
+        return reset($current);
+    }
+
+    public function next() {
+        next($this->records);
+    }
+
+    public function valid() {
+        return !is_null(key($this->records));
+    }
+
+    public function close() {
+        $this->records = null;
+    }
+}
diff --git a/question/engine/tests/questionattempt_test.php b/question/engine/tests/questionattempt_test.php
new file mode 100644 (file)
index 0000000..3ee6779
--- /dev/null
@@ -0,0 +1,373 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_attempt class.
+ *
+ * Action methods like start, process_action and finish are assumed to be
+ * tested by walkthrough tests in the various behaviours.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Unit tests for the {@link question_attempt} class.
+ *
+ * These are the tests that don't require any steps.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_test extends advanced_testcase {
+    private $question;
+    private $usageid;
+    private $qa;
+
+    protected function setUp() {
+        $this->question = test_question_maker::make_question('description');
+        $this->question->defaultmark = 3;
+        $this->usageid = 13;
+        $this->qa = new question_attempt($this->question, $this->usageid);
+    }
+
+    protected function tearDown() {
+        $this->question = null;
+        $this->useageid = null;
+        $this->qa = null;
+    }
+
+    public function test_constructor_sets_maxmark() {
+        $qa = new question_attempt($this->question, $this->usageid);
+        $this->assertSame($this->question, $qa->get_question());
+        $this->assertEquals(3, $qa->get_max_mark());
+    }
+
+    public function test_maxmark_beats_default_mark() {
+        $qa = new question_attempt($this->question, $this->usageid, null, 2);
+        $this->assertEquals(2, $qa->get_max_mark());
+    }
+
+    public function test_get_set_slot() {
+        $this->qa->set_slot(7);
+        $this->assertEquals(7, $this->qa->get_slot());
+    }
+
+    public function test_fagged_initially_false() {
+        $this->assertEquals(false, $this->qa->is_flagged());
+    }
+
+    public function test_set_is_flagged() {
+        $this->qa->set_flagged(true);
+        $this->assertEquals(true, $this->qa->is_flagged());
+    }
+
+    public function test_get_qt_field_name() {
+        $name = $this->qa->get_qt_field_name('test');
+        $this->assertRegExp('/^' . preg_quote($this->qa->get_field_prefix()) . '/', $name);
+        $this->assertRegExp('/_test$/', $name);
+    }
+
+    public function test_get_behaviour_field_name() {
+        $name = $this->qa->get_behaviour_field_name('test');
+        $this->assertRegExp('/^' . preg_quote($this->qa->get_field_prefix()) . '/', $name);
+        $this->assertRegExp('/_-test$/', $name);
+    }
+
+    public function test_get_field_prefix() {
+        $this->qa->set_slot(7);
+        $name = $this->qa->get_field_prefix();
+        $this->assertRegExp('/' . preg_quote($this->usageid) . '/', $name);
+        $this->assertRegExp('/' . preg_quote($this->qa->get_slot()) . '/', $name);
+    }
+
+    public function test_get_submitted_var_not_present_var_returns_null() {
+        $this->assertNull($this->qa->get_submitted_var(
+                'reallyunlikelyvariablename', PARAM_BOOL));
+    }
+
+    public function test_get_submitted_var_param_mark_not_present() {
+        $this->assertNull($this->qa->get_submitted_var(
+                'name', question_attempt::PARAM_MARK, array()));
+    }
+
+    public function test_get_submitted_var_param_mark_blank() {
+        $this->assertSame('', $this->qa->get_submitted_var(
+                'name', question_attempt::PARAM_MARK, array('name' => '')));
+    }
+
+    public function test_get_submitted_var_param_mark_number() {
+        $this->assertSame(123.0, $this->qa->get_submitted_var(
+                'name', question_attempt::PARAM_MARK, array('name' => '123')));
+    }
+
+    public function test_get_submitted_var_param_mark_number_uk_decimal() {
+        $this->assertSame(123.45, $this->qa->get_submitted_var(
+                'name', question_attempt::PARAM_MARK, array('name' => '123.45')));
+    }
+
+    public function test_get_submitted_var_param_mark_number_eu_decimal() {
+        $this->assertSame(123.45, $this->qa->get_submitted_var(
+                'name', question_attempt::PARAM_MARK, array('name' => '123,45')));
+    }
+
+    public function test_get_submitted_var_param_mark_invalid() {
+        $this->assertSame(0.0, $this->qa->get_submitted_var(
+                'name', question_attempt::PARAM_MARK, array('name' => 'frog')));
+    }
+}
+
+
+/**
+ * These tests use a standard fixture of a {@link question_attempt} with three steps.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_with_steps_test extends advanced_testcase {
+    private $question;
+    private $qa;
+
+    protected function setUp() {
+        $this->question = test_question_maker::make_question('description');
+        $this->qa = new testable_question_attempt($this->question, 0, null, 2);
+        for ($i = 0; $i < 3; $i++) {
+            $step = new question_attempt_step(array('i' => $i));
+            $this->qa->add_step($step);
+        }
+    }
+
+    protected function tearDown() {
+        $this->qa = null;
+    }
+
+    public function test_get_step_before_start() {
+        $this->setExpectedException('moodle_exception');
+        $step = $this->qa->get_step(-1);
+    }
+
+    public function test_get_step_at_start() {
+        $step = $this->qa->get_step(0);
+        $this->assertEquals(0, $step->get_qt_var('i'));
+    }
+
+    public function test_get_step_at_end() {
+        $step = $this->qa->get_step(2);
+        $this->assertEquals(2, $step->get_qt_var('i'));
+    }
+
+    public function test_get_step_past_end() {
+        $this->setExpectedException('moodle_exception');
+        $step = $this->qa->get_step(3);
+    }
+
+    public function test_get_num_steps() {
+        $this->assertEquals(3, $this->qa->get_num_steps());
+    }
+
+    public function test_get_last_step() {
+        $step = $this->qa->get_last_step();
+        $this->assertEquals(2, $step->get_qt_var('i'));
+    }
+
+    public function test_get_last_qt_var_there1() {
+        $this->assertEquals(2, $this->qa->get_last_qt_var('i'));
+    }
+
+    public function test_get_last_qt_var_there2() {
+        $this->qa->get_step(0)->set_qt_var('_x', 'a value');
+        $this->assertEquals('a value', $this->qa->get_last_qt_var('_x'));
+    }
+
+    public function test_get_last_qt_var_missing() {
+        $this->assertNull($this->qa->get_last_qt_var('notthere'));
+    }
+
+    public function test_get_last_qt_var_missing_default() {
+        $this->assertEquals('default', $this->qa->get_last_qt_var('notthere', 'default'));
+    }
+
+    public function test_get_last_behaviour_var_missing() {
+        $this->assertNull($this->qa->get_last_qt_var('notthere'));
+    }
+
+    public function test_get_last_behaviour_var_there() {
+        $this->qa->get_step(1)->set_behaviour_var('_x', 'a value');
+        $this->assertEquals('a value', '' . $this->qa->get_last_behaviour_var('_x'));
+    }
+
+    public function test_get_state_gets_state_of_last() {
+        $this->qa->get_step(2)->set_state(question_state::$gradedright);
+        $this->qa->get_step(1)->set_state(question_state::$gradedwrong);
+        $this->assertEquals(question_state::$gradedright, $this->qa->get_state());
+    }
+
+    public function test_get_mark_gets_mark_of_last() {
+        $this->assertEquals(2, $this->qa->get_max_mark());
+        $this->qa->get_step(2)->set_fraction(0.5);
+        $this->qa->get_step(1)->set_fraction(0.1);
+        $this->assertEquals(1, $this->qa->get_mark());
+    }
+
+    public function test_get_fraction_gets_fraction_of_last() {
+        $this->qa->get_step(2)->set_fraction(0.5);
+        $this->qa->get_step(1)->set_fraction(0.1);
+        $this->assertEquals(0.5, $this->qa->get_fraction());
+    }
+
+    public function test_get_fraction_returns_null_if_none() {
+        $this->assertNull($this->qa->get_fraction());
+    }
+
+    public function test_format_mark() {
+        $this->qa->get_step(2)->set_fraction(0.5);
+        $this->assertEquals('1.00', $this->qa->format_mark(2));
+    }
+
+    public function test_format_max_mark() {
+        $this->assertEquals('2.0000000', $this->qa->format_max_mark(7));
+    }
+
+    public function test_get_min_fraction() {
+        $this->qa->set_min_fraction(-1);
+        $this->assertEquals(-1, $this->qa->get_min_fraction(0));
+    }
+
+    public function test_cannot_get_min_fraction_before_start() {
+        $qa = new question_attempt($this->question, 0);
+        $this->setExpectedException('moodle_exception');
+        $qa->get_min_fraction();
+    }
+}
+
+
+/**
+ * Unit tests for loading data into the {@link question_attempt} class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_db_test extends data_loading_method_test_base {
+    public function test_load() {
+        $records = new question_test_recordset(array(
+            array('questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                   'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                                                                       'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
+                                                                                                               'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                                                                                                                'timecreated', 'userid', 'name', 'value'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo',              null, 1256233700, 1,       null, null),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete',          null, 1256233705, 1,   'answer',  '1'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1, '', '', '', 1256233790, 3, 2, 'complete',          null, 1256233710, 1,   'answer',  '0'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 4, 3, 'complete',          null, 1256233715, 1,   'answer',  '1'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 4, 'gradedright',  1.0000000, 1256233720, 1,  '-finish',  '1'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1, '-comment', 'Not good enough!'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1,    '-mark',  '1'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1, '-maxmark',  '2'),
+        ));
+
+        $question = test_question_maker::make_question('truefalse', 'true');
+        $question->id = -1;
+
+        question_bank::start_unit_test();
+        question_bank::load_test_question_data($question);
+        $qa = question_attempt::load_from_records($records, 1, new question_usage_null_observer(), 'deferredfeedback');
+        question_bank::end_unit_test();
+
+        $this->assertEquals($question->questiontext, $qa->get_question()->questiontext);
+
+        $this->assertEquals(6, $qa->get_num_steps());
+
+        $step = $qa->get_step(0);
+        $this->assertEquals(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233700, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array(), $step->get_all_data());
+
+        $step = $qa->get_step(1);
+        $this->assertEquals(question_state::$complete, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233705, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('answer' => '1'), $step->get_all_data());
+
+        $step = $qa->get_step(2);
+        $this->assertEquals(question_state::$complete, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233710, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('answer' => '0'), $step->get_all_data());
+
+        $step = $qa->get_step(3);
+        $this->assertEquals(question_state::$complete, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233715, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('answer' => '1'), $step->get_all_data());
+
+        $step = $qa->get_step(4);
+        $this->assertEquals(question_state::$gradedright, $step->get_state());
+        $this->assertEquals(1, $step->get_fraction());
+        $this->assertEquals(1256233720, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('-finish' => '1'), $step->get_all_data());
+
+        $step = $qa->get_step(5);
+        $this->assertEquals(question_state::$mangrpartial, $step->get_state());
+        $this->assertEquals(0.5, $step->get_fraction());
+        $this->assertEquals(1256233790, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('-comment' => 'Not good enough!', '-mark' => '1', '-maxmark' => '2'),
+                $step->get_all_data());
+    }
+
+    public function test_load_missing_question() {
+        $records = new question_test_recordset(array(
+            array('questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                   'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                                                                       'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
+                                                                                                               'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                                                                                                                'timecreated', 'userid', 'name', 'value'),
+            array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo',              null, 1256233700, 1,       null, null),
+        ));
+
+        question_bank::start_unit_test();
+        $qa = question_attempt::load_from_records($records, 1, new question_usage_null_observer(), 'deferredfeedback');
+        question_bank::end_unit_test();
+
+        $missingq = question_bank::get_qtype('missingtype')->make_deleted_instance(-1, 2);
+        $this->assertEquals($missingq, $qa->get_question());
+
+        $this->assertEquals(1, $qa->get_num_steps());
+
+        $step = $qa->get_step(0);
+        $this->assertEquals(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233700, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array(), $step->get_all_data());
+    }
+}
diff --git a/question/engine/tests/questionattemptiterator_test.php b/question/engine/tests/questionattemptiterator_test.php
new file mode 100644 (file)
index 0000000..4ca8ed3
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_attempt_iterator class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * This file contains tests for the {@link question_attempt_iterator} class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_iterator_test extends advanced_testcase {
+    private $quba;
+    private $qas = array();
+    private $iterator;
+
+    protected function setUp() {
+        $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+        $this->quba->set_preferred_behaviour('deferredfeedback');
+
+        $slot = $this->quba->add_question(test_question_maker::make_question('description'));
+        $this->qas[$slot] = $this->quba->get_question_attempt($slot);
+
+        $slot = $this->quba->add_question(test_question_maker::make_question('description'));
+        $this->qas[$slot] = $this->quba->get_question_attempt($slot);
+
+        $this->iterator = $this->quba->get_attempt_iterator();
+    }
+
+    protected function tearDown() {
+        $this->quba = null;
+        $this->iterator = null;
+    }
+
+    public function test_foreach_loop() {
+        $i = 1;
+        foreach ($this->iterator as $key => $qa) {
+            $this->assertEquals($i, $key);
+            $this->assertSame($this->qas[$i], $qa);
+            $i++;
+        }
+        $this->assertEquals(3, $i);
+    }
+
+    public function test_offsetExists_before_start() {
+        $this->assertFalse(isset($this->iterator[0]));
+    }
+
+    public function test_offsetExists_at_start() {
+        $this->assertTrue(isset($this->iterator[1]));
+    }
+
+    public function test_offsetExists_at_endt() {
+        $this->assertTrue(isset($this->iterator[2]));
+    }
+
+    public function test_offsetExists_past_end() {
+        $this->assertFalse(isset($this->iterator[3]));
+    }
+
+    public function test_offsetGet_before_start() {
+        $this->setExpectedException('moodle_exception');
+        $step = $this->iterator[0];
+    }
+
+    public function test_offsetGet_at_start() {
+        $this->assertSame($this->qas[1], $this->iterator[1]);
+    }
+
+    public function test_offsetGet_at_end() {
+        $this->assertSame($this->qas[2], $this->iterator[2]);
+    }
+
+    public function test_offsetGet_past_end() {
+        $this->setExpectedException('moodle_exception');
+        $step = $this->iterator[3];
+    }
+
+    public function test_cannot_set() {
+        $this->setExpectedException('moodle_exception');
+        $this->iterator[0] = null;
+    }
+
+    public function test_cannot_unset() {
+        $this->setExpectedException('moodle_exception');
+        unset($this->iterator[2]);
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/questionattemptstep_test.php b/question/engine/tests/questionattemptstep_test.php
new file mode 100644 (file)
index 0000000..6ac1b67
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_attempt_step class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Unit tests for the {@link question_attempt_step} class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_step_test extends advanced_testcase {
+    public function test_initial_state_unprocessed() {
+        $step = new question_attempt_step();
+        $this->assertEquals(question_state::$unprocessed, $step->get_state());
+    }
+
+    public function test_get_set_state() {
+        $step = new question_attempt_step();
+        $step->set_state(question_state::$gradedright);
+        $this->assertEquals(question_state::$gradedright, $step->get_state());
+    }
+
+    public function test_initial_fraction_null() {
+        $step = new question_attempt_step();
+        $this->assertNull($step->get_fraction());
+    }
+
+    public function test_get_set_fraction() {
+        $step = new question_attempt_step();
+        $step->set_fraction(0.5);
+        $this->assertEquals(0.5, $step->get_fraction());
+    }
+
+    public function test_has_var() {
+        $step = new question_attempt_step(array('x' => 1, '-y' => 'frog'));
+        $this->assertTrue($step->has_qt_var('x'));
+        $this->assertTrue($step->has_behaviour_var('y'));
+        $this->assertFalse($step->has_qt_var('y'));
+        $this->assertFalse($step->has_behaviour_var('x'));
+    }
+
+    public function test_get_var() {
+        $step = new question_attempt_step(array('x' => 1, '-y' => 'frog'));
+        $this->assertEquals('1', $step->get_qt_var('x'));
+        $this->assertEquals('frog', $step->get_behaviour_var('y'));
+        $this->assertNull($step->get_qt_var('y'));
+    }
+
+    public function test_set_var() {
+        $step = new question_attempt_step();
+        $step->set_qt_var('_x', 1);
+        $step->set_behaviour_var('_x', 2);
+        $this->assertEquals('1', $step->get_qt_var('_x'));
+        $this->assertEquals('2', $step->get_behaviour_var('_x'));
+    }
+
+    public function test_cannot_set_qt_var_without_underscore() {
+        $step = new question_attempt_step();
+        $this->setExpectedException('moodle_exception');
+        $step->set_qt_var('x', 1);
+    }
+
+    public function test_cannot_set_behaviour_var_without_underscore() {
+        $step = new question_attempt_step();
+        $this->setExpectedException('moodle_exception');
+        $step->set_behaviour_var('x', 1);
+    }
+
+    public function test_get_data() {
+        $step = new question_attempt_step(array('x' => 1, '-y' => 'frog', ':flagged' => 1));
+        $this->assertEquals(array('x' => '1'), $step->get_qt_data());
+        $this->assertEquals(array('y' => 'frog'), $step->get_behaviour_data());
+        $this->assertEquals(array('x' => 1, '-y' => 'frog', ':flagged' => 1), $step->get_all_data());
+    }
+
+    public function test_get_submitted_data() {
+        $step = new question_attempt_step(array('x' => 1, '-y' => 'frog'));
+        $step->set_qt_var('_x', 1);
+        $step->set_behaviour_var('_x', 2);
+        $this->assertEquals(array('x' => 1, '-y' => 'frog'), $step->get_submitted_data());
+    }
+
+    public function test_constructor_default_params() {
+        global $USER;
+        $step = new question_attempt_step();
+        $this->assertEquals(time(), $step->get_timecreated(), '', 5);
+        $this->assertEquals($USER->id, $step->get_user_id());
+        $this->assertEquals(array(), $step->get_qt_data());
+        $this->assertEquals(array(), $step->get_behaviour_data());
+
+    }
+
+    public function test_constructor_given_params() {
+        global $USER;
+        $step = new question_attempt_step(array(), 123, 5);
+        $this->assertEquals(123, $step->get_timecreated());
+        $this->assertEquals(5, $step->get_user_id());
+        $this->assertEquals(array(), $step->get_qt_data());
+        $this->assertEquals(array(), $step->get_behaviour_data());
+
+    }
+}
+
+
+/**
+ * Unit tests for the loading data into the {@link question_attempt_step} class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_step_db_test extends data_loading_method_test_base {
+    public function test_load_with_data() {
+        $records = new question_test_recordset(array(
+            array('attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'),
+            array(             1,                   1,                0,  'todo',       null,    1256228502,       13,   null,    null),
+            array(             2,                   1,                1,  'complete',   null,    1256228505,       13,    'x',     'a'),
+            array(             2,                   1,                1,  'complete',   null,    1256228505,       13,   '_y',    '_b'),
+            array(             2,                   1,                1,  'complete',   null,    1256228505,       13,   '-z',    '!c'),
+            array(             2,                   1,                1,  'complete',   null,    1256228505,       13, '-_t',    '!_d'),
+            array(             3,                   1,                2,  'gradedright', 1.0,    1256228515,       13, '-finish',  '1'),
+        ));
+
+        $step = question_attempt_step::load_from_records($records, 2);
+        $this->assertEquals(question_state::$complete, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256228505, $step->get_timecreated());
+        $this->assertEquals(13, $step->get_user_id());
+        $this->assertEquals(array('x' => 'a', '_y' => '_b', '-z' => '!c', '-_t' => '!_d'), $step->get_all_data());
+    }
+
+    public function test_load_without_data() {
+        $records = new question_test_recordset(array(
+            array('attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'),
+            array(             2,                   1,                1,  'complete',   null,    1256228505,       13,   null,    null),
+        ));
+
+        $step = question_attempt_step::load_from_records($records, 2);
+        $this->assertEquals(question_state::$complete, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256228505, $step->get_timecreated());
+        $this->assertEquals(13, $step->get_user_id());
+        $this->assertEquals(array(), $step->get_all_data());
+    }
+
+    public function test_load_dont_be_too_greedy() {
+        $records = new question_test_recordset(array(
+            array('attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'),
+            array(             1,                   1,                0,  'todo',       null,    1256228502,       13,    'x',     'right'),
+            array(             2,                   2,                0,  'complete',   null,    1256228505,       13,    'x',     'wrong'),
+        ));
+
+        $step = question_attempt_step::load_from_records($records, 1);
+        $this->assertEquals(array('x' => 'right'), $step->get_all_data());
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/questionattemptstepiterator_test.php b/question/engine/tests/questionattemptstepiterator_test.php
new file mode 100644 (file)
index 0000000..a0101cf
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_attempt_step_iterator class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Unit tests for the {@link question_attempt_step_iterator} class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_step_iterator_test extends advanced_testcase {
+    private $qa;
+    private $iterator;
+
+    protected function setUp() {
+        $question = test_question_maker::make_question('description');
+        $this->qa = new testable_question_attempt($question, 0);
+        for ($i = 0; $i < 3; $i++) {
+            $step = new question_attempt_step(array('i' => $i));
+            $this->qa->add_step($step);
+        }
+        $this->iterator = $this->qa->get_step_iterator();
+    }
+
+    protected function tearDown() {
+        $this->qa = null;
+        $this->iterator = null;
+    }
+
+    public function test_foreach_loop() {
+        $i = 0;
+        foreach ($this->iterator as $key => $step) {
+            $this->assertEquals($i, $key);
+            $this->assertEquals($i, $step->get_qt_var('i'));
+            $i++;
+        }
+    }
+
+    public function test_foreach_loop_add_step_during() {
+        $i = 0;
+        foreach ($this->iterator as $key => $step) {
+            $this->assertEquals($i, $key);
+            $this->assertEquals($i, $step->get_qt_var('i'));
+            $i++;
+            if ($i == 2) {
+                $step = new question_attempt_step(array('i' => 3));
+                $this->qa->add_step($step);
+            }
+        }
+        $this->assertEquals(4, $i);
+    }
+
+    public function test_reverse_foreach_loop() {
+        $i = 2;
+        foreach ($this->qa->get_reverse_step_iterator() as $key => $step) {
+            $this->assertEquals($i, $key);
+            $this->assertEquals($i, $step->get_qt_var('i'));
+            $i--;
+        }
+    }
+
+    public function test_offsetExists_before_start() {
+        $this->assertFalse(isset($this->iterator[-1]));
+    }
+
+    public function test_offsetExists_at_start() {
+        $this->assertTrue(isset($this->iterator[0]));
+    }
+
+    public function test_offsetExists_at_endt() {
+        $this->assertTrue(isset($this->iterator[2]));
+    }
+
+    public function test_offsetExists_past_end() {
+        $this->assertFalse(isset($this->iterator[3]));
+    }
+
+    public function test_offsetGet_before_start() {
+        $this->setExpectedException('moodle_exception');
+        $step = $this->iterator[-1];
+    }
+
+    public function test_offsetGet_at_start() {
+        $step = $this->iterator[0];
+        $this->assertEquals(0, $step->get_qt_var('i'));
+    }
+
+    public function test_offsetGet_at_end() {
+        $step = $this->iterator[2];
+        $this->assertEquals(2, $step->get_qt_var('i'));
+    }
+
+    public function test_offsetGet_past_end() {
+        $this->setExpectedException('moodle_exception');
+        $step = $this->iterator[3];
+    }
+
+    public function test_cannot_set() {
+        $this->setExpectedException('moodle_exception');
+        $this->iterator[0] = null;
+    }
+
+    public function test_cannot_unset() {
+        $this->setExpectedException('moodle_exception');
+        unset($this->iterator[2]);
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/questionbank_test.php b/question/engine/tests/questionbank_test.php
new file mode 100644 (file)
index 0000000..9c6275e
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_bank class.
+ *
+ * @package    moodlecore
+ * @subpackage questionbank
+ * @copyright  2011 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+
+
+/**
+ *Unit tests for the {@link question_bank} class.
+ *
+ * @copyright  2011 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_bank_test extends advanced_testcase {
+
+    public function test_sort_qtype_array() {
+        $config = new stdClass();
+        $config->multichoice_sortorder = '1';
+        $config->calculated_sortorder = '2';
+        $qtypes = array(
+            'frog' => 'toad',
+            'calculated' => 'newt',
+            'multichoice' => 'eft',
+        );
+        $this->assertEquals(question_bank::sort_qtype_array($qtypes, $config), array(
+            'multichoice' => 'eft',
+            'calculated' => 'newt',
+            'frog' => 'toad',
+        ));
+    }
+
+    public function test_fraction_options() {
+        $fractions = question_bank::fraction_options();
+        $this->assertSame(get_string('none'), reset($fractions));
+        $this->assertSame('0.0', key($fractions));
+        $this->assertSame('5%', end($fractions));
+        $this->assertSame('0.05', key($fractions));
+        array_shift($fractions);
+        array_pop($fractions);
+        array_pop($fractions);
+        $this->assertSame('100%', reset($fractions));
+        $this->assertSame('1.0', key($fractions));
+        $this->assertSame('11.11111%', end($fractions));
+        $this->assertSame('0.1111111', key($fractions));
+    }
+
+    public function test_fraction_options_full() {
+        $fractions = question_bank::fraction_options_full();
+        $this->assertSame(get_string('none'), reset($fractions));
+        $this->assertSame('0.0', key($fractions));
+        $this->assertSame('-100%', end($fractions));
+        $this->assertSame('-1.0', key($fractions));
+        array_shift($fractions);
+        array_pop($fractions);
+        array_pop($fractions);
+        $this->assertSame('100%', reset($fractions));
+        $this->assertSame('1.0', key($fractions));
+        $this->assertSame('-83.33333%', end($fractions));
+        $this->assertSame('-0.8333333', key($fractions));
+    }
+}
diff --git a/question/engine/tests/questioncbm_test.php b/question/engine/tests/questioncbm_test.php
new file mode 100644 (file)
index 0000000..1757fd8
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_cbm class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+
+
+/**
+ * Unit tests for the question_cbm class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_cbm_test extends advanced_testcase {
+    public function test_adjust_fraction() {
+        $this->assertEquals(0, question_cbm::adjust_fraction(0, question_cbm::LOW), '', 0.0000001);
+        $this->assertEquals(-2/3, question_cbm::adjust_fraction(0, question_cbm::MED), '', 0.0000001);
+        $this->assertEquals(-2, question_cbm::adjust_fraction(0, question_cbm::HIGH), '', 0.0000001);
+        $this->assertEquals(1/3, question_cbm::adjust_fraction(1, question_cbm::LOW), '', 0.0000001);
+        $this->assertEquals(2/3, question_cbm::adjust_fraction(1, question_cbm::MED), '', 0.0000001);
+        $this->assertEquals(1, question_cbm::adjust_fraction(1, question_cbm::HIGH), '', 0.0000001);
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/questionengine_test.php b/question/engine/tests/questionengine_test.php
new file mode 100644 (file)
index 0000000..1a5505d
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_engine class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+
+
+/**
+ *Unit tests for the question_engine class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_engine_test extends advanced_testcase {
+
+    public function test_load_behaviour_class() {
+        // Exercise SUT
+        question_engine::load_behaviour_class('deferredfeedback');
+        // Verify
+        $this->assertTrue(class_exists('qbehaviour_deferredfeedback'));
+    }
+
+    public function test_load_behaviour_class_missing() {
+        // Set expectation.
+        $this->setExpectedException('moodle_exception');
+        // Exercise SUT
+        question_engine::load_behaviour_class('nonexistantbehaviour');
+    }
+
+    public function test_get_behaviour_unused_display_options() {
+        $this->assertEquals(array(), question_engine::get_behaviour_unused_display_options('interactive'));
+        $this->assertEquals(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
+                question_engine::get_behaviour_unused_display_options('deferredfeedback'));
+        $this->assertEquals(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
+                question_engine::get_behaviour_unused_display_options('deferredcbm'));
+        $this->assertEquals(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
+                question_engine::get_behaviour_unused_display_options('manualgraded'));
+    }
+
+    public function test_sort_behaviours() {
+        $in = array('b1' => 'Behave 1', 'b2' => 'Behave 2', 'b3' => 'Behave 3', 'b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6');
+
+        $out = array('b1' => 'Behave 1', 'b2' => 'Behave 2', 'b3' => 'Behave 3', 'b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6');
+        $this->assertSame($out, question_engine::sort_behaviours($in, '', '', ''));
+
+        $this->assertSame($out, question_engine::sort_behaviours($in, '', 'b4', 'b4'));
+
+        $out = array('b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6');
+        $this->assertSame($out, question_engine::sort_behaviours($in, '', 'b1,b2,b3,b4', 'b4'));
+
+        $out = array('b6' => 'Behave 6', 'b1' => 'Behave 1', 'b4' => 'Behave 4');
+        $this->assertSame($out, question_engine::sort_behaviours($in, 'b6,b1,b4', 'b2,b3,b4,b5', 'b4'));
+
+        $out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
+        $this->assertSame($out, question_engine::sort_behaviours($in, 'b6,b5,b4', 'b1,b2,b3', 'b4'));
+
+        $out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
+        $this->assertSame($out, question_engine::sort_behaviours($in, 'b1,b6,b5', 'b1,b2,b3,b4', 'b4'));
+
+        $out = array('b2' => 'Behave 2', 'b4' => 'Behave 4', 'b6' => 'Behave 6');
+        $this->assertSame($out, question_engine::sort_behaviours($in, 'b2,b4,b6', 'b1,b3,b5', 'b2'));
+
+        // Ignore unknown input in the order argument.
+        $this->assertSame($in, question_engine::sort_behaviours($in, 'unknown', '', ''));
+
+        // Ignore unknown input in the disabled argument.
+        $this->assertSame($in, question_engine::sort_behaviours($in, '', 'unknown', ''));
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/questionstate_test.php b/question/engine/tests/questionstate_test.php
new file mode 100644 (file)
index 0000000..b1077fc
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_state class and subclasses.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once($CFG->libdir . '/questionlib.php');
+
+
+/**
+ * Unit tests for the {@link question_state} class and subclasses.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_state_test extends advanced_testcase {
+    public function test_is_active() {
+        $this->assertFalse(question_state::$notstarted->is_active());
+        $this->assertFalse(question_state::$unprocessed->is_active());
+        $this->assertTrue(question_state::$todo->is_active());
+        $this->assertTrue(question_state::$invalid->is_active());
+        $this->assertTrue(question_state::$complete->is_active());
+        $this->assertFalse(question_state::$needsgrading->is_active());
+        $this->assertFalse(question_state::$finished->is_active());
+        $this->assertFalse(question_state::$gaveup->is_active());
+        $this->assertFalse(question_state::$gradedwrong->is_active());
+        $this->assertFalse(question_state::$gradedpartial->is_active());
+        $this->assertFalse(question_state::$gradedright->is_active());
+        $this->assertFalse(question_state::$manfinished->is_active());
+        $this->assertFalse(question_state::$mangaveup->is_active());
+        $this->assertFalse(question_state::$mangrwrong->is_active());
+        $this->assertFalse(question_state::$mangrpartial->is_active());
+        $this->assertFalse(question_state::$mangrright->is_active());
+    }
+
+    public function test_is_finished() {
+        $this->assertFalse(question_state::$notstarted->is_finished());
+        $this->assertFalse(question_state::$unprocessed->is_finished());
+        $this->assertFalse(question_state::$todo->is_finished());
+        $this->assertFalse(question_state::$invalid->is_finished());
+        $this->assertFalse(question_state::$complete->is_finished());
+        $this->assertTrue(question_state::$needsgrading->is_finished());
+        $this->assertTrue(question_state::$finished->is_finished());
+        $this->assertTrue(question_state::$gaveup->is_finished());
+        $this->assertTrue(question_state::$gradedwrong->is_finished());
+        $this->assertTrue(question_state::$gradedpartial->is_finished());
+        $this->assertTrue(question_state::$gradedright->is_finished());
+        $this->assertTrue(question_state::$manfinished->is_finished());
+        $this->assertTrue(question_state::$mangaveup->is_finished());
+        $this->assertTrue(question_state::$mangrwrong->is_finished());
+        $this->assertTrue(question_state::$mangrpartial->is_finished());
+        $this->assertTrue(question_state::$mangrright->is_finished());
+    }
+
+    public function test_is_graded() {
+        $this->assertFalse(question_state::$notstarted->is_graded());
+        $this->assertFalse(question_state::$unprocessed->is_graded());
+        $this->assertFalse(question_state::$todo->is_graded());
+        $this->assertFalse(question_state::$invalid->is_graded());
+        $this->assertFalse(question_state::$complete->is_graded());
+        $this->assertFalse(question_state::$needsgrading->is_graded());
+        $this->assertFalse(question_state::$finished->is_graded());
+        $this->assertFalse(question_state::$gaveup->is_graded());
+        $this->assertTrue(question_state::$gradedwrong->is_graded());
+        $this->assertTrue(question_state::$gradedpartial->is_graded());
+        $this->assertTrue(question_state::$gradedright->is_graded());
+        $this->assertFalse(question_state::$manfinished->is_graded());
+        $this->assertFalse(question_state::$mangaveup->is_graded());
+        $this->assertTrue(question_state::$mangrwrong->is_graded());
+        $this->assertTrue(question_state::$mangrpartial->is_graded());
+        $this->assertTrue(question_state::$mangrright->is_graded());
+    }
+
+    public function test_is_commented() {
+        $this->assertFalse(question_state::$notstarted->is_commented());
+        $this->assertFalse(question_state::$unprocessed->is_commented());
+        $this->assertFalse(question_state::$todo->is_commented());
+        $this->assertFalse(question_state::$invalid->is_commented());
+        $this->assertFalse(question_state::$complete->is_commented());
+        $this->assertFalse(question_state::$needsgrading->is_commented());
+        $this->assertFalse(question_state::$finished->is_commented());
+        $this->assertFalse(question_state::$gaveup->is_commented());
+        $this->assertFalse(question_state::$gradedwrong->is_commented());
+        $this->assertFalse(question_state::$gradedpartial->is_commented());
+        $this->assertFalse(question_state::$gradedright->is_commented());
+        $this->assertTrue(question_state::$manfinished->is_commented());
+        $this->assertTrue(question_state::$mangaveup->is_commented());
+        $this->assertTrue(question_state::$mangrwrong->is_commented());
+        $this->assertTrue(question_state::$mangrpartial->is_commented());
+        $this->assertTrue(question_state::$mangrright->is_commented());
+    }
+
+    public function test_graded_state_for_fraction() {
+        $this->assertEquals(question_state::$gradedwrong, question_state::graded_state_for_fraction(-1));
+        $this->assertEquals(question_state::$gradedwrong, question_state::graded_state_for_fraction(0));
+        $this->assertEquals(question_state::$gradedpartial, question_state::graded_state_for_fraction(0.000001));
+        $this->assertEquals(question_state::$gradedpartial, question_state::graded_state_for_fraction(0.999999));
+        $this->assertEquals(question_state::$gradedright, question_state::graded_state_for_fraction(1));
+    }
+
+    public function test_manually_graded_state_for_other_state() {
+        $this->assertEquals(question_state::$manfinished,
+                question_state::$finished->corresponding_commented_state(null));
+        $this->assertEquals(question_state::$mangaveup,
+                question_state::$gaveup->corresponding_commented_state(null));
+        $this->assertEquals(question_state::$manfinished,
+                question_state::$manfinished->corresponding_commented_state(null));
+        $this->assertEquals(question_state::$mangaveup,
+                question_state::$mangaveup->corresponding_commented_state(null));
+        $this->assertEquals(question_state::$needsgrading,
+                question_state::$mangrright->corresponding_commented_state(null));
+        $this->assertEquals(question_state::$needsgrading,
+                question_state::$mangrright->corresponding_commented_state(null));
+
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$gaveup->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$needsgrading->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$gradedwrong->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$gradedpartial->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$gradedright->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$mangrright->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$mangrpartial->corresponding_commented_state(0));
+        $this->assertEquals(question_state::$mangrwrong,
+                question_state::$mangrright->corresponding_commented_state(0));
+
+        $this->assertEquals(question_state::$mangrpartial,
+                question_state::$gradedpartial->corresponding_commented_state(0.5));
+
+        $this->assertEquals(question_state::$mangrright,
+                question_state::$gradedpartial->corresponding_commented_state(1));
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/questionusagebyactivity_test.php b/question/engine/tests/questionusagebyactivity_test.php
new file mode 100644 (file)
index 0000000..73ff8b3
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_usage_by_activity class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Unit tests for the question_usage_by_activity class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_usage_by_activity_test extends advanced_testcase {
+
+    public function test_set_get_preferred_model() {
+        // Set up
+        $quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+
+        // Exercise SUT and verify.
+        $quba->set_preferred_behaviour('deferredfeedback');
+        $this->assertEquals('deferredfeedback', $quba->get_preferred_behaviour());
+    }
+
+    public function test_set_get_id() {
+        // Set up
+        $quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+
+        // Exercise SUT and verify
+        $quba->set_id_from_database(123);
+        $this->assertEquals(123, $quba->get_id());
+    }
+
+    public function test_fake_id() {
+        // Set up
+        $quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+
+        // Exercise SUT and verify
+        $this->assertNotEmpty($quba->get_id());
+    }
+
+    public function test_create_usage_and_add_question() {
+        // Exercise SUT
+        $context = get_context_instance(CONTEXT_SYSTEM);
+        $quba = question_engine::make_questions_usage_by_activity('unit_test', $context);
+        $quba->set_preferred_behaviour('deferredfeedback');
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $slot = $quba->add_question($tf);
+
+        // Verify.
+        $this->assertEquals($slot, 1);
+        $this->assertEquals('unit_test', $quba->get_owning_component());
+        $this->assertSame($context, $quba->get_owning_context());
+        $this->assertEquals($quba->question_count(), 1);
+        $this->assertEquals($quba->get_question_state($slot), question_state::$notstarted);
+    }
+
+    public function test_get_question() {
+        // Set up.
+        $quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+        $quba->set_preferred_behaviour('deferredfeedback');
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $slot = $quba->add_question($tf);
+
+        // Exercise SUT and verify.
+        $this->assertSame($tf, $quba->get_question($slot));
+
+        $this->setExpectedException('moodle_exception');
+        $quba->get_question($slot + 1);
+    }
+
+    public function test_extract_responses() {
+        // Start a deferred feedback attempt with CBM and add the question to it.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+        $quba->set_preferred_behaviour('deferredcbm');
+        $slot = $quba->add_question($tf);
+        $quba->start_all_questions();
+
+        // Prepare data to be submitted
+        $prefix = $quba->get_field_prefix($slot);
+        $answername = $prefix . 'answer';
+        $certaintyname = $prefix . '-certainty';
+        $getdata = array(
+            $answername => 1,
+            $certaintyname => 3,
+            'irrelevant' => 'should be ignored',
+        );
+
+        // Exercise SUT
+        $submitteddata = $quba->extract_responses($slot, $getdata);
+
+        // Verify.
+        $this->assertEquals(array('answer' => 1, '-certainty' => 3), $submitteddata);
+    }
+
+    public function test_access_out_of_sequence_throws_exception() {
+        // Start a deferred feedback attempt with CBM and add the question to it.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+        $quba->set_preferred_behaviour('deferredcbm');
+        $slot = $quba->add_question($tf);
+        $quba->start_all_questions();
+
+        // Prepare data to be submitted
+        $prefix = $quba->get_field_prefix($slot);
+        $answername = $prefix . 'answer';
+        $certaintyname = $prefix . '-certainty';
+        $postdata = array(
+            $answername => 1,
+            $certaintyname => 3,
+            $prefix . ':sequencecheck' => 1,
+            'irrelevant' => 'should be ignored',
+        );
+
+        // Exercise SUT - no exception yet.
+        $quba->process_all_actions($slot, $postdata);
+
+        $postdata = array(
+            $answername => 1,
+            $certaintyname => 3,
+            $prefix . ':sequencecheck' => 3,
+            'irrelevant' => 'should be ignored',
+        );
+
+        // Exercise SUT - now it should fail.
+        $this->setExpectedException('question_out_of_sequence_exception');
+        $quba->process_all_actions($slot, $postdata);
+    }
+}
+
+/**
+ * Unit tests for loading data into the {@link question_usage_by_activity} class.
+ *
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_usage_db_test extends data_loading_method_test_base {
+    public function test_load() {
+        $records = new question_test_recordset(array(
+        array('qubaid', 'contextid', 'component', 'preferredbehaviour',
+                                               'questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                                              'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                                                                                             'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
+                                                                                                                                     'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                                                                                                                                     'timecreated', 'userid', 'name', 'value'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo',             null, 1256233700, 1,       null, null),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233705, 1,   'answer',  '1'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 2, 'gradedright', 1.0000000, 1256233720, 1,  '-finish',  '1'),
+        ));
+
+        $question = test_question_maker::make_question('truefalse', 'true');
+        $question->id = -1;
+
+        question_bank::start_unit_test();
+        question_bank::load_test_question_data($question);
+        $quba = question_usage_by_activity::load_from_records($records, 1);
+        question_bank::end_unit_test();
+
+        $this->assertEquals('unit_test', $quba->get_owning_component());
+        $this->assertEquals(1, $quba->get_id());
+        $this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
+        $this->assertEquals('interactive', $quba->get_preferred_behaviour());
+
+        $qa = $quba->get_question_attempt(1);
+
+        $this->assertEquals($question->questiontext, $qa->get_question()->questiontext);
+
+        $this->assertEquals(3, $qa->get_num_steps());
+
+        $step = $qa->get_step(0);
+        $this->assertEquals(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233700, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array(), $step->get_all_data());
+
+        $step = $qa->get_step(1);
+        $this->assertEquals(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEquals(1256233705, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('answer' => '1'), $step->get_all_data());
+
+        $step = $qa->get_step(2);
+        $this->assertEquals(question_state::$gradedright, $step->get_state());
+        $this->assertEquals(1, $step->get_fraction());
+        $this->assertEquals(1256233720, $step->get_timecreated());
+        $this->assertEquals(1, $step->get_user_id());
+        $this->assertEquals(array('-finish' => '1'), $step->get_all_data());
+    }
+}
diff --git a/question/engine/tests/questionutils_test.php b/question/engine/tests/questionutils_test.php
new file mode 100644 (file)
index 0000000..e51d4db
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the {@link question_utils} class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2010 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+
+
+/**
+ * Unit tests for the {@link question_utils} class.
+ *
+ * @copyright  2010 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_utils_test extends advanced_testcase {
+    public function test_arrays_have_same_keys_and_values() {
+        $this->assertTrue(question_utils::arrays_have_same_keys_and_values(
+                array(),
+                array()));
+        $this->assertTrue(question_utils::arrays_have_same_keys_and_values(
+                array('key' => 1),
+                array('key' => '1')));
+        $this->assertFalse(question_utils::arrays_have_same_keys_and_values(
+                array(),
+                array('key' => 1)));
+        $this->assertFalse(question_utils::arrays_have_same_keys_and_values(
+                array('key' => 2),
+                array('key' => 1)));
+        $this->assertFalse(question_utils::arrays_have_same_keys_and_values(
+                array('key' => 1),
+                array('otherkey' => 1)));
+        $this->assertFalse(question_utils::arrays_have_same_keys_and_values(
+                array('sub0' => '2', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1'),
+                array('sub0' => '1', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1')));
+    }
+
+    public function test_arrays_same_at_key() {
+        $this->assertTrue(question_utils::arrays_same_at_key(
+                array(),
+                array(),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key(
+                array(),
+                array('key' => 1),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key(
+                array('key' => 1),
+                array(),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key(
+                array('key' => 1),
+                array('key' => 1),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key(
+                array('key' => 1),
+                array('key' => 2),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key(
+                array('key' => 1),
+                array('key' => '1'),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key(
+                array('key' => 0),
+                array('key' => ''),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key(
+                array(),
+                array('key' => ''),
+                'key'));
+    }
+
+    public function test_arrays_same_at_key_missing_is_blank() {
+        $this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
+                array(),
+                array(),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
+                array(),
+                array('key' => 1),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
+                array('key' => 1),
+                array(),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
+                array('key' => 1),
+                array('key' => 1),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
+                array('key' => 1),
+                array('key' => 2),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
+                array('key' => 1),
+                array('key' => '1'),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
+                array('key' => '0'),
+                array('key' => ''),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
+                array(),
+                array('key' => ''),
+                'key'));
+    }
+
+    public function test_arrays_same_at_key_integer() {
+        $this->assertTrue(question_utils::arrays_same_at_key_integer(
+                array(),
+                array(),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_integer(
+                array(),
+                array('key' => 1),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_integer(
+                array('key' => 1),
+                array(),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_integer(
+                array('key' => 1),
+                array('key' => 1),
+                'key'));
+        $this->assertFalse(question_utils::arrays_same_at_key_integer(
+                array('key' => 1),
+                array('key' => 2),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_integer(
+                array('key' => 1),
+                array('key' => '1'),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_integer(
+                array('key' => '0'),
+                array('key' => ''),
+                'key'));
+        $this->assertTrue(question_utils::arrays_same_at_key_integer(
+                array(),
+                array('key' => 0),
+                'key'));
+    }
+
+    public function test_int_to_roman() {
+        $this->assertSame('i', question_utils::int_to_roman(1));
+        $this->assertSame('iv', question_utils::int_to_roman(4));
+        $this->assertSame('v', question_utils::int_to_roman(5));
+        $this->assertSame('vi', question_utils::int_to_roman(6));
+        $this->assertSame('ix', question_utils::int_to_roman(9));
+        $this->assertSame('xi', question_utils::int_to_roman(11));
+        $this->assertSame('xlviii', question_utils::int_to_roman(48));
+        $this->assertSame('lxxxvii', question_utils::int_to_roman(87));
+        $this->assertSame('c', question_utils::int_to_roman(100));
+        $this->assertSame('mccxxxiv', question_utils::int_to_roman(1234));
+        $this->assertSame('mmmcmxcix', question_utils::int_to_roman(3999));
+    }
+
+    public function test_int_to_roman_too_small() {
+        $this->setExpectedException('moodle_exception');
+        question_utils::int_to_roman(0);
+    }
+
+    public function test_int_to_roman_too_big() {
+        $this->setExpectedException('moodle_exception');
+        question_utils::int_to_roman(4000);
+    }
+
+    public function test_int_to_roman_not_int() {
+        $this->setExpectedException('moodle_exception');
+        question_utils::int_to_roman(1.5);
+    }
+}
\ No newline at end of file
diff --git a/question/engine/tests/unitofwork_test.php b/question/engine/tests/unitofwork_test.php
new file mode 100644 (file)
index 0000000..d44e9b9
--- /dev/null
@@ -0,0 +1,302 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the question_engine_unit_of_work class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Test subclass to allow access to some protected data so that the correct
+ * behaviour can be verified.
+ *
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_question_engine_unit_of_work extends question_engine_unit_of_work {
+    public function get_modified() {
+        return $this->modified;
+    }
+
+    public function get_attempts_added() {
+        return $this->attemptsadded;
+    }
+
+    public function get_attempts_modified() {
+        return $this->attemptsmodified;
+    }
+
+    public function get_steps_added() {
+        return $this->stepsadded;
+    }
+
+    public function get_steps_modified() {
+        return $this->stepsmodified;
+    }
+
+    public function get_steps_deleted() {
+        return $this->stepsdeleted;
+    }
+}
+
+
+/**
+ * Unit tests for the {@link question_engine_unit_of_work} class.
+ *
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_engine_unit_of_work_test extends data_loading_method_test_base {
+    /** @var question_usage_by_activity the test question usage. */
+    protected $quba;
+
+    /** @var int the slot number of the one qa in the test usage.*/
+    protected $slot;
+
+    /** @var testable_question_engine_unit_of_work the unit of work we are testing. */
+    protected $observer;
+
+    protected function setUp() {
+        // Create a usage in an initial state, with one shortanswer question added,
+        // and attempted in interactive mode submitted responses 'toad' then 'frog'.
+        // Then set it to use a new unit of work for any subsequent changes.
+        // Create a short answer question.
+        $question = test_question_maker::make_question('shortanswer');
+        $question->hints = array(
+            new question_hint(0, 'This is the first hint.', FORMAT_HTML),
+            new question_hint(0, 'This is the second hint.', FORMAT_HTML),
+        );
+        $question->id = -1;
+        question_bank::start_unit_test();
+        question_bank::load_test_question_data($question);
+
+        $this->setup_initial_test_state($this->get_test_data());
+     }
+
+    public function testDown() {
+        question_bank::end_unit_test();
+    }
+
+    protected function setup_initial_test_state($testdata) {
+        $records = new question_test_recordset($testdata);
+
+        $this->quba = question_usage_by_activity::load_from_records($records, 1);
+
+        $this->slot = 1;
+        $this->observer = new testable_question_engine_unit_of_work($this->quba);
+        $this->quba->set_observer($this->observer);
+    }
+
+    protected function get_test_data() {
+        return array(
+        array('qubaid', 'contextid', 'component', 'preferredbehaviour',
+                                                'questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                                               'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                                                                                              'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
+                                                                                                                                     'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                                                                                                                                     'timecreated', 'userid', 'name', 'value'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo',             null, 1256233700, 1, '-_triesleft', 3),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233720, 1, 'answer',     'toad'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233720, 1, '-submit',     1),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233720, 1, '-_triesleft', 1),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 3, 2, 'todo',             null, 1256233740, 1, '-tryagain',   1),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright',      null, 1256233790, 1, 'answer',     'frog'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 1.0000000, 1256233790, 1, '-finish',     1),
+        );
+    }
+
+    public function test_initial_state() {
+        $this->assertFalse($this->observer->get_modified());
+        $this->assertEquals(0, count($this->observer->get_attempts_added()));
+        $this->assertEquals(0, count($this->observer->get_attempts_modified()));
+        $this->assertEquals(0, count($this->observer->get_steps_added()));
+        $this->assertEquals(0, count($this->observer->get_steps_modified()));
+        $this->assertEquals(0, count($this->observer->get_steps_deleted()));
+    }
+
+    public function test_update_usage() {
+
+        $this->quba->set_preferred_behaviour('deferredfeedback');
+
+        $this->assertTrue($this->observer->get_modified());
+    }
+
+    public function test_add_question() {
+
+        $slot = $this->quba->add_question(test_question_maker::make_question('truefalse'));
+
+        $newattempts = $this->observer->get_attempts_added();
+        $this->assertEquals(1, count($newattempts));
+        $this->assertTrue($this->quba->get_question_attempt($slot) === reset($newattempts));
+        $this->assertSame($slot, key($newattempts));
+    }
+
+    public function test_add_and_start_question() {
+
+        $slot = $this->quba->add_question(test_question_maker::make_question('truefalse'));
+                $this->quba->start_question($slot);
+
+        // The point here is that, although we have added a step, it is not listed
+        // separately becuase it is part of a newly added attempt, and all steps
+        // for a newly added attempt are automatically added to the DB, so it does
+        // not need to be tracked separately.
+        $newattempts = $this->observer->get_attempts_added();
+        $this->assertEquals(1, count($newattempts));
+        $this->assertTrue($this->quba->get_question_attempt($slot) === reset($newattempts));
+        $this->assertSame($slot, key($newattempts));
+        $this->assertEquals(0, count($this->observer->get_steps_added()));
+    }
+
+    public function test_process_action() {
+
+        $this->quba->manual_grade($this->slot, 'Acutally, that is not quite right', 0.5);
+
+        // Here, however, were we are adding a step to an existing qa, we do need to track that.
+        $this->assertEquals(0, count($this->observer->get_attempts_added()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEquals(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertTrue($this->quba->get_question_attempt($this->slot) === $updatedattempt);
+        $this->assertSame($this->slot, key($updatedattempts));
+
+        $newsteps = $this->observer->get_steps_added();
+        $this->assertEquals(1, count($newsteps));
+
+        list($newstep, $qaid, $seq) = reset($newsteps);
+        $this->assertSame($this->quba->get_question_attempt($this->slot)->get_last_step(), $newstep);
+    }
+
+    public function test_regrade_same_steps() {
+
+        // Change the question in a minor way and regrade.
+        if (!isset($this->quba->get_question($this->slot)->answer)) {
+            $this->quba->get_question($this->slot)->answer = array();
+        }
+        if (!isset($this->quba->get_question($this->slot)->answer[14])) {
+            $this->quba->get_question($this->slot)->answer[14] = new stdClass();
+        }
+        $this->quba->get_question($this->slot)->answer[14]->fraction = 0.5;
+        $this->quba->regrade_all_questions();
+
+        // Here, the qa, and all the steps, should be marked as updated.
+        // Here, however, were we are adding a step to an existing qa, we do need to track that.
+        $this->assertEquals(0, count($this->observer->get_attempts_added()));
+        $this->assertEquals(0, count($this->observer->get_steps_added()));
+        $this->assertEquals(0, count($this->observer->get_steps_deleted()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEquals(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertTrue($this->quba->get_question_attempt($this->slot) === $updatedattempt);
+
+        $updatedsteps = $this->observer->get_steps_modified();
+        $this->assertEquals($updatedattempt->get_num_steps(), count($updatedsteps));
+
+        foreach ($updatedattempt->get_step_iterator() as $seq => $step) {
+            $this->assertSame(array($step, $updatedattempt->get_database_id(), $seq),
+                    $updatedsteps[$seq]);
+        }
+    }
+
+    public function test_regrade_losing_steps() {
+
+        // Change the question so that 'toad' is also right, and regrade. This
+        // will mean that the try again, and second try states are no longer
+        // needed, so they should be dropped.
+        $this->quba->get_question($this->slot)->answers[14]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        $this->assertEquals(0, count($this->observer->get_attempts_added()));
+        $this->assertEquals(0, count($this->observer->get_steps_added()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEquals(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertTrue($this->quba->get_question_attempt($this->slot) === $updatedattempt);
+
+        $updatedsteps = $this->observer->get_steps_modified();
+        $this->assertEquals($updatedattempt->get_num_steps(), count($updatedsteps));
+
+        foreach ($updatedattempt->get_step_iterator() as $seq => $step) {
+            $this->assertSame(array($step, $updatedattempt->get_database_id(), $seq),
+                    $updatedsteps[$seq]);
+        }
+
+        $deletedsteps = $this->observer->get_steps_deleted();
+        $this->assertEquals(2, count($deletedsteps));
+
+        $firstdeletedstep = reset($deletedsteps);
+        $this->assertEquals(array('-tryagain' => 1), $firstdeletedstep->get_all_data());
+
+        $seconddeletedstep = end($deletedsteps);
+        $this->assertEquals(array('answer' => 'frog', '-finish' => 1),
+                $seconddeletedstep->get_all_data());
+    }
+
+    public function test_tricky_regrade() {
+
+        // The tricky thing here is that we take a half-complete question-attempt,
+        // and then as one transaction, we submit some more responses, and then
+        // change the question attempt as in test_regrade_losing_steps, and regrade
+        // before the steps are even written to the database the first time.
+        $somedata = $this->get_test_data();
+        $somedata = array_slice($somedata, 0, 5);
+        $this->setup_initial_test_state($somedata);
+
+        $this->quba->process_action($this->slot, array('-tryagain' => 1));
+        $this->quba->process_action($this->slot, array('answer' => 'frog', '-submit' => 1));
+        $this->quba->finish_all_questions();
+
+        $this->quba->get_question($this->slot)->answers[14]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        $this->assertEquals(0, count($this->observer->get_attempts_added()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEquals(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertTrue($this->quba->get_question_attempt($this->slot) === $updatedattempt);
+
+        $this->assertEquals(0, count($this->observer->get_steps_added()));
+
+        $updatedsteps = $this->observer->get_steps_modified();
+        $this->assertEquals($updatedattempt->get_num_steps(), count($updatedsteps));
+
+        foreach ($updatedattempt->get_step_iterator() as $seq => $step) {
+            $this->assertSame(array($step, $updatedattempt->get_database_id(), $seq),
+                    $updatedsteps[$seq]);
+        }
+
+        $this->assertEquals(0, count($this->observer->get_steps_deleted()));
+    }
+}
diff --git a/question/engine/upgrade/tests/helper.php b/question/engine/upgrade/tests/helper.php
new file mode 100644 (file)
index 0000000..3ba39f7
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains test helper code for testing the upgrade to the new
+ * question engine. The acutal tests are organised by question type in files
+ * like question/type/truefalse/db/simpletest/testupgradelibnewqe.php.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../upgradelib.php');
+
+
+/**
+ * Subclass of question_engine_attempt_upgrader to help with testing.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_question_engine_attempt_upgrader extends question_engine_attempt_upgrader {
+    public function prevent_timeout() {
+    }
+
+    public function __construct($loader, $logger) {
+        $this->questionloader = $loader;
+        $this->logger = $logger;
+    }
+}
+
+
+/**
+ * Subclass of question_engine_upgrade_question_loader for unit testing.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_question_engine_upgrade_question_loader extends question_engine_upgrade_question_loader {
+    public function put_question_in_cache($question) {
+        $this->cache[$question->id] = $question;
+    }
+
+    public function load_question($questionid, $quizid) {
+        global $CFG;
+
+        if (isset($this->cache[$questionid])) {
+            return $this->cache[$questionid];
+        }
+
+        return null;
+    }
+
+    public function put_dataset_in_cache($questionid, $selecteditem, $dataset) {
+        $this->datasetcache[$questionid][$selecteditem] = $dataset;
+    }
+
+    public function load_dataset($questionid, $selecteditem) {
+        global $DB;
+
+        if (isset($this->datasetcache[$questionid][$selecteditem])) {
+            return $this->datasetcache[$questionid][$selecteditem];
+        }
+
+        throw new coding_exception('Test dataset not loaded.');
+    }
+}
+
+
+/**
+ * Base class for tests that thest the upgrade of one particular attempt and
+ * one question.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_attempt_upgrader_test_base extends advanced_testcase {
+    protected $updater;
+    protected $loader;
+
+    protected function setUp() {
+        parent::setUp();
+        $logger = new dummy_question_engine_assumption_logger();
+        $this->loader = new test_question_engine_upgrade_question_loader($logger);
+        $this->updater = new test_question_engine_attempt_upgrader($this->loader, $logger);
+    }
+
+    protected function tearDown() {
+        $this->updater = null;
+        parent::tearDown();
+    }
+
+    public function test_must_have_methods() {
+        // each test case must have at least one method..
+    }
+
+    /**
+     * Clear text, bringing independence of html2text results
+     *
+     * Some tests performing text comparisons of converted text are too much
+     * dependent of the behavior of the html2text library. This function is
+     * aimed to reduce such dependencies that should not affect the results
+     * of these question attempt upgrade tests.
+     */
+    protected function clear_html2text_dependencies($qa) {
+        // Cleaning all whitespace should be enough to ignore any html2text dependency
+        if (property_exists($qa, 'responsesummary')) {
+            $qa->responsesummary = preg_replace('/\s/', '', $qa->responsesummary);
+        }
+        if (property_exists($qa, 'questionsummary')) {
+            $qa->questionsummary = preg_replace('/\s/', '', $qa->questionsummary);
+        }
+    }
+
+    /**
+     * Compare two qas, ignoring inessential differences.
+     * @param object $expectedqa the expected qa.
+     * @param object $qa the actual qa.
+     */
+    protected function compare_qas($expectedqa, $qa) {
+        $this->clear_html2text_dependencies($expectedqa);
+        $this->clear_html2text_dependencies($qa);
+
+        $this->assertEquals($expectedqa, $qa);
+
+        // uncomment following to get better diff on failure
+        //foreach (get_object_vars($expectedqa) as $k=>$v) {
+        //    $this->assertEquals($v, $qa->$k, '', 0, 10, false);
+        //}
+    }
+}
diff --git a/question/format/gift/tests/fixtures/questions.gift.txt b/question/format/gift/tests/fixtures/questions.gift.txt
new file mode 100644 (file)
index 0000000..e7ad9a6
--- /dev/null
@@ -0,0 +1,48 @@
+// essay
+::Q8:: How are you? {}
+
+// question: 2  name: Moodle activities
+::Moodle activities::[html]Match the <b>activity</b> to the description.{
+    =[html]An activity supporting asynchronous discussions. -> Forum
+    =[moodle]A teacher asks a question and specifies a choice of multiple responses. -> Choice
+    =[plain]A bank of record entries which participants can add to. -> Database
+    =[markdown]A collection of web pages that anyone can add to or edit. -> Wiki
+    = -> Chat
+}
+
+// multiple choice with specified feedback for right and wrong answers
+::Q2:: What's between orange and green in the spectrum?
+{
+    =yellow # right; good!
+    ~red # [html]wrong, it's yellow
+    ~[plain]blue # wrong, it's yellow
+}
+
+// multiple choice, multiple response with specified feedback for right and wrong answers
+::colours:: What's between orange and green in the spectrum?
+{
+    ~%50%yellow # right; good!
+    ~%-100%red # [html]wrong
+    ~%50%off-beige # right; good!
+    ~%-100%[plain]blue # wrong
+}
+
+// math range question
+::Q5:: What is a number from 1 to 5? {#3:2~#Completely wrong}";
+
+// question: 666  name: Shortanswer
+::Shortanswer::Which is the best animal?{
+    =Frog#Good!
+    =%50%Cat#What is it with Moodlers and cats?
+    =%0%*#Completely wrong
+}
+
+// true/false
+::Q1:: 42 is the Absolute Answer to everything.{
+FALSE#42 is the Ultimate Answer.#You gave the right answer.}";
+
+// name 0-11
+::2-08 TSL::TSL is blablabla.{T}
+
+// name 0-11
+::2-08 TSL::TSL is blablabla.{TRUE}
diff --git a/question/format/gift/tests/giftformat_test.php b/question/format/gift/tests/giftformat_test.php
new file mode 100644 (file)
index 0000000..73a8a6e
--- /dev/null
@@ -0,0 +1,888 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the Moodle GIFT format.
+ *
+ * @package    qformat
+ * @subpackage gift
+ * @copyright  2010 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/format/gift/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the GIFT import/export format.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_gift_test extends question_testcase {
+    public function assert_same_gift($expectedtext, $text) {
+        $this->assertEquals(str_replace("\r\n", "\n", $expectedtext),
+                str_replace("\r\n", "\n", $text));
+    }
+
+    public function test_import_essay() {
+        $gift = '
+// essay
+::Q8:: How are you? {}';
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Q8',
+            'questiontext' => 'How are you?',
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'essay',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'responseformat' => 'editor',
+            'responsefieldlines' => 15,
+            'attachments' => 0,
+            'graderinfo' => array(
+                'text' => '',
+                'format' => FORMAT_HTML,
+                'files' => array()),
+        );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_export_essay() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Q8',
+            'questiontext' => 'How are you?',
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'qtype' => 'essay',
+            'options' => (object) array(
+                'responseformat' => 'editor',
+                'responsefieldlines' => 15,
+                'attachments' => 0,
+                'graderinfo' => '',
+                'graderinfoformat' => FORMAT_HTML,
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Q8
+::Q8::How are you?{}
+
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+
+    public function test_import_match() {
+        $gift = '
+// question: 2  name: Moodle activities
+::Moodle activities::[html]Match the <b>activity</b> to the description.{
+    =[html]An activity supporting asynchronous discussions. -> Forum
+    =[moodle]A teacher asks a question and specifies a choice of multiple responses. -> Choice
+    =[plain]A bank of record entries which participants can add to. -> Database
+    =[markdown]A collection of web pages that anyone can add to or edit. -> Wiki
+    = -> Chat
+}';
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Moodle activities',
+            'questiontext' => 'Match the <b>activity</b> to the description.',
+            'questiontextformat' => FORMAT_HTML,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_HTML,
+            'qtype' => 'match',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'shuffleanswers' => '1',
+            'correctfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            ),
+            'partiallycorrectfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            ),
+            'incorrectfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            ),
+            'subquestions' => array(
+                0 => array(
+                    'text' => 'An activity supporting asynchronous discussions.',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => 'A teacher asks a question and specifies a choice of multiple responses.',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => 'A bank of record entries which participants can add to.',
+                    'format' => FORMAT_PLAIN,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => 'A collection of web pages that anyone can add to or edit.',
+                    'format' => FORMAT_MARKDOWN,
+                    'files' => array(),
+                ),
+                4 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+            ),
+            'subanswers' => array(
+                0 => 'Forum',
+                1 => 'Choice',
+                2 => 'Database',
+                3 => 'Wiki',
+                4 => 'Chat',
+            ),
+        );
+
+        // Repeated test for better failure messages.
+        $this->assertEquals($expectedq->subquestions, $q->subquestions);
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_export_match() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Moodle activities',
+            'questiontext' => 'Match the <b>activity</b> to the description.',
+            'questiontextformat' => FORMAT_HTML,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_HTML,
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'qtype' => 'match',
+            'options' => (object) array(
+                'id' => 123,
+                'question' => 666,
+                'shuffleanswers' => 1,
+                'subquestions' => array(
+                    42 => (object) array(
+                        'id' => 1234,
+                        'code' => 12341234,
+                        'question' => 666,
+                        'questiontext' => '<div class="frog">An activity supporting asynchronous discussions.</div>',
+                        'questiontextformat' => FORMAT_HTML,
+                        'answertext' => 'Forum',
+                    ),
+                    43 => (object) array(
+                        'id' => 1234,
+                        'code' => 12341234,
+                        'question' => 666,
+                        'questiontext' => 'A teacher asks a question and specifies a choice of multiple responses.',
+                        'questiontextformat' => FORMAT_MOODLE,
+                        'answertext' => 'Choice',
+                    ),
+                    44 => (object) array(
+                        'id' => 1234,
+                        'code' => 12341234,
+                        'question' => 666,
+                        'questiontext' => 'A bank of record entries which participants can add to.',
+                        'questiontextformat' => FORMAT_PLAIN,
+                        'answertext' => 'Database',
+                    ),
+                    45 => (object) array(
+                        'id' => 1234,
+                        'code' => 12341234,
+                        'question' => 666,
+                        'questiontext' => 'A collection of web pages that anyone can add to or edit.',
+                        'questiontextformat' => FORMAT_MARKDOWN,
+                        'answertext' => 'Wiki',
+                    ),
+                    46 => (object) array(
+                        'id' => 1234,
+                        'code' => 12341234,
+                        'question' => 666,
+                        'questiontext' => '',
+                        'questiontextformat' => FORMAT_MARKDOWN,
+                        'answertext' => 'Chat',
+                    ),
+                ),
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Moodle activities
+::Moodle activities::[html]Match the <b>activity</b> to the description.{
+\t=<div class\\=\"frog\">An activity supporting asynchronous discussions.</div> -> Forum
+\t=[moodle]A teacher asks a question and specifies a choice of multiple responses. -> Choice
+\t=[plain]A bank of record entries which participants can add to. -> Database
+\t=[markdown]A collection of web pages that anyone can add to or edit. -> Wiki
+\t= -> Chat
+}
+
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+
+    public function test_import_multichoice() {
+        $gift = "
+// multiple choice with specified feedback for right and wrong answers
+::Q2:: What's between orange and green in the spectrum?
+{
+    =yellow # right; good!
+    ~red # [html]wrong, it's yellow
+    ~[plain]blue # wrong, it's yellow
+}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Q2',
+            'questiontext' => "What's between orange and green in the spectrum?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'multichoice',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'single' => 1,
+            'shuffleanswers' => '1',
+            'answernumbering' => 'abc',
+            'correctfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'partiallycorrectfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'incorrectfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'answer' => array(
+                0 => array(
+                    'text' => 'yellow',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => 'red',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => 'blue',
+                    'format' => FORMAT_PLAIN,
+                    'files' => array(),
+                ),
+            ),
+            'fraction' => array(1, 0, 0),
+            'feedback' => array(
+                0 => array(
+                    'text' => 'right; good!',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => "wrong, it's yellow",
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => "wrong, it's yellow",
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+            ),
+        );
+
+        // Repeated test for better failure messages.
+        $this->assertEquals($expectedq->answer, $q->answer);
+        $this->assertEquals($expectedq->feedback, $q->feedback);
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_multichoice_multi() {
+        $gift = "
+// multiple choice, multiple response with specified feedback for right and wrong answers
+::colours:: What's between orange and green in the spectrum?
+{
+    ~%50%yellow # right; good!
+    ~%-100%red # [html]wrong
+    ~%50%off-beige # right; good!
+    ~%-100%[plain]blue # wrong
+}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'colours',
+            'questiontext' => "What's between orange and green in the spectrum?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'multichoice',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'single' => 0,
+            'shuffleanswers' => '1',
+            'answernumbering' => 'abc',
+            'correctfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'partiallycorrectfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'incorrectfeedback' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'answer' => array(
+                0 => array(
+                    'text' => 'yellow',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => 'red',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => 'off-beige',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => 'blue',
+                    'format' => FORMAT_PLAIN,
+                    'files' => array(),
+                ),
+            ),
+            'fraction' => array(0.5, -1, 0.5, -1),
+            'feedback' => array(
+                0 => array(
+                    'text' => 'right; good!',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => "wrong",
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => "right; good!",
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => "wrong",
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+            ),
+        );
+
+        // Repeated test for better failure messages.
+        $this->assertEquals($expectedq->answer, $q->answer);
+        $this->assertEquals($expectedq->feedback, $q->feedback);
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_export_multichoice() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Q8',
+            'questiontext' => "What's between orange and green in the spectrum?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'qtype' => 'multichoice',
+            'options' => (object) array(
+                'single' => 1,
+                'shuffleanswers' => '1',
+                'answernumbering' => 'abc',
+                'correctfeedback' => '',
+                'correctfeedbackformat' => FORMAT_MOODLE,
+                'partiallycorrectfeedback' => '',
+                'partiallycorrectfeedbackformat' => FORMAT_MOODLE,
+                'incorrectfeedback' => '',
+                'incorrectfeedbackformat' => FORMAT_MOODLE,
+                'answers' => array(
+                    123 => (object) array(
+                        'id' => 123,
+                        'answer' => 'yellow',
+                        'answerformat' => FORMAT_MOODLE,
+                        'fraction' => 1,
+                        'feedback' => 'right; good!',
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    124 => (object) array(
+                        'id' => 124,
+                        'answer' => 'red',
+                        'answerformat' => FORMAT_MOODLE,
+                        'fraction' => 0,
+                        'feedback' => "wrong, it's yellow",
+                        'feedbackformat' => FORMAT_HTML,
+                    ),
+                    125 => (object) array(
+                        'id' => 125,
+                        'answer' => 'blue',
+                        'answerformat' => FORMAT_PLAIN,
+                        'fraction' => 0,
+                        'feedback' => "wrong, it's yellow",
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                ),
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Q8
+::Q8::What's between orange and green in the spectrum?{
+\t=yellow#right; good!
+\t~red#[html]wrong, it's yellow
+\t~[plain]blue#wrong, it's yellow
+}
+
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+
+    public function test_import_numerical() {
+        $gift = "
+// math range question
+::Q5:: What is a number from 1 to 5? {#3:2~#Completely wrong}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Q5',
+            'questiontext' => "What is a number from 1 to 5?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'numerical',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'answer' => array(
+                '3',
+                '*',
+            ),
+            'fraction' => array(1, 0),
+            'feedback' => array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => "Completely wrong",
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+            ),
+            'tolerance' => array(2, 0),
+        );
+
+        // Repeated test for better failure messages.
+        $this->assertEquals($expectedq->answer, $q->answer);
+        $this->assertEquals($expectedq->fraction, $q->fraction);
+        $this->assertEquals($expectedq->feedback, $q->feedback);
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_export_numerical() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Q5',
+            'questiontext' => "What is a number from 1 to 5?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'defaultmark' => 1,
+            'penalty' => 1,
+            'length' => 1,
+            'qtype' => 'numerical',
+            'options' => (object) array(
+                'id' => 123,
+                'question' => 666,
+                'showunits' => 0,
+                'unitsleft' => 0,
+                'showunits' => 2,
+                'unitgradingtype' => 0,
+                'unitpenalty' => 0,
+                'answers' => array(
+                    1 => (object) array(
+                        'id' => 123,
+                        'answer' => '3',
+                        'answerformat' => 0,
+                        'fraction' => 1,
+                        'tolerance' => 2,
+                        'feedback' => '',
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    2 => (object) array(
+                        'id' => 124,
+                        'answer' => '*',
+                        'answerformat' => 0,
+                        'fraction' => 0,
+                        'tolerance' => 0,
+                        'feedback' => "Completely wrong",
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                ),
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Q5
+::Q5::What is a number from 1 to 5?{#
+\t=%100%3:2#
+\t~#Completely wrong
+}
+
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+
+    public function test_import_shortanswer() {
+        $gift = "
+// question: 666  name: Shortanswer
+::Shortanswer::Which is the best animal?{
+    =Frog#Good!
+    =%50%Cat#What is it with Moodlers and cats?
+    =%0%*#Completely wrong
+}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Shortanswer',
+            'questiontext' => "Which is the best animal?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'shortanswer',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'answer' => array(
+                'Frog',
+                'Cat',
+                '*',
+            ),
+            'fraction' => array(1, 0.5, 0),
+            'feedback' => array(
+                0 => array(
+                    'text' => 'Good!',
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => "What is it with Moodlers and cats?",
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => "Completely wrong",
+                    'format' => FORMAT_MOODLE,
+                    'files' => array(),
+                ),
+            ),
+        );
+
+        // Repeated test for better failure messages.
+        $this->assertEquals($expectedq->answer, $q->answer);
+        $this->assertEquals($expectedq->fraction, $q->fraction);
+        $this->assertEquals($expectedq->feedback, $q->feedback);
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_export_shortanswer() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Shortanswer',
+            'questiontext' => "Which is the best animal?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'defaultmark' => 1,
+            'penalty' => 1,
+            'length' => 1,
+            'qtype' => 'shortanswer',
+            'options' => (object) array(
+                'id' => 123,
+                'question' => 666,
+                'usecase' => 1,
+                'answers' => array(
+                    1 => (object) array(
+                        'id' => 1,
+                        'answer' => 'Frog',
+                        'answerformat' => 0,
+                        'fraction' => 1,
+                        'feedback' => 'Good!',
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    2 => (object) array(
+                        'id' => 2,
+                        'answer' => 'Cat',
+                        'answerformat' => 0,
+                        'fraction' => 0.5,
+                        'feedback' => "What is it with Moodlers and cats?",
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    3 => (object) array(
+                        'id' => 3,
+                        'answer' => '*',
+                        'answerformat' => 0,
+                        'fraction' => 0,
+                        'feedback' => "Completely wrong",
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                ),
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Shortanswer
+::Shortanswer::Which is the best animal?{
+\t=%100%Frog#Good!
+\t=%50%Cat#What is it with Moodlers and cats?
+\t=%0%*#Completely wrong
+}
+
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+
+    public function test_import_truefalse() {
+        $gift = "
+// true/false
+::Q1:: 42 is the Absolute Answer to everything.{
+FALSE#42 is the Ultimate Answer.#You gave the right answer.}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Q1',
+            'questiontext' => "42 is the Absolute Answer to everything.",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'truefalse',
+            'defaultmark' => 1,
+            'penalty' => 1,
+            'length' => 1,
+            'correctanswer' => 0,
+            'feedbacktrue' => array(
+                'text' => '42 is the Ultimate Answer.',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'feedbackfalse' => array(
+                'text' => 'You gave the right answer.',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+        );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_truefalse_true_answer1() {
+        $gift = "// name 0-11
+::2-08 TSL::TSL is blablabla.{T}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => '2-08 TSL',
+            'questiontext' => "TSL is blablabla.",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'truefalse',
+            'defaultmark' => 1,
+            'penalty' => 1,
+            'length' => 1,
+            'correctanswer' => 1,
+            'feedbacktrue' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'feedbackfalse' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+        );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_truefalse_true_answer2() {
+        $gift = "// name 0-11
+::2-08 TSL::TSL is blablabla.{TRUE}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => '2-08 TSL',
+            'questiontext' => "TSL is blablabla.",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'qtype' => 'truefalse',
+            'defaultmark' => 1,
+            'penalty' => 1,
+            'length' => 1,
+            'correctanswer' => 1,
+            'feedbacktrue' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+            'feedbackfalse' => array(
+                'text' => '',
+                'format' => FORMAT_MOODLE,
+                'files' => array(),
+            ),
+        );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_export_truefalse() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Q1',
+            'questiontext' => "42 is the Absolute Answer to everything.",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'defaultmark' => 1,
+            'penalty' => 1,
+            'length' => 1,
+            'qtype' => 'truefalse',
+            'options' => (object) array(
+                'id' => 123,
+                'question' => 666,
+                'trueanswer' => 1,
+                'falseanswer' => 2,
+                'answers' => array(
+                    1 => (object) array(
+                        'id' => 123,
+                        'answer' => 'True',
+                        'answerformat' => 0,
+                        'fraction' => 1,
+                        'feedback' => 'You gave the right answer.',
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    2 => (object) array(
+                        'id' => 124,
+                        'answer' => 'False',
+                        'answerformat' => 0,
+                        'fraction' => 0,
+                        'feedback' => "42 is the Ultimate Answer.",
+                        'feedbackformat' => FORMAT_HTML,
+                    ),
+                ),
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Q1
+::Q1::42 is the Absolute Answer to everything.{TRUE#[html]42 is the Ultimate Answer.#You gave the right answer.}
+
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+}
diff --git a/question/format/xml/tests/xmlformat_test.php b/question/format/xml/tests/xmlformat_test.php
new file mode 100644 (file)
index 0000000..fd5eb55
--- /dev/null
@@ -0,0 +1,1409 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the Moodle XML format.
+ *
+ * @package    qformat
+ * @subpackage xml
+ * @copyright  2010 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format/xml/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the matching question definition class.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_xml_test extends question_testcase {
+    public function assert_same_xml($expectedxml, $xml) {
+        $this->assertEquals(str_replace("\r\n", "\n", $expectedxml),
+                str_replace("\r\n", "\n", $xml));
+    }
+
+    public function make_test_question() {
+        global $USER;
+        $q = new stdClass();
+        $q->id = 0;
+        $q->contextid = 0;
+        $q->category = 0;
+        $q->parent = 0;
+        $q->questiontextformat = FORMAT_HTML;
+        $q->generalfeedbackformat = FORMAT_HTML;
+        $q->defaultmark = 1;
+        $q->penalty = 0.3333333;
+        $q->length = 1;
+        $q->stamp = make_unique_id_code();
+        $q->version = make_unique_id_code();
+        $q->hidden = 0;
+        $q->timecreated = time();
+        $q->timemodified = time();
+        $q->createdby = $USER->id;
+        $q->modifiedby = $USER->id;
+        return $q;
+    }
+
+    /**
+     * The data the XML import format sends to save_question is not exactly
+     * the same as the data returned from the editing form, so this method
+     * makes necessary changes to the return value of
+     * test_question_maker::get_question_form_data so that the tests can work.
+     * @param object $expectedq as returned by get_question_form_data.
+     * @return object one more likely to match the return value of import_...().
+     */
+    public function remove_irrelevant_form_data_fields($expectedq) {
+        return $this->itemid_to_files($expectedq);
+    }
+
+    /**
+     * Becuase XML import uses a files array instead of an itemid integer to
+     * handle saving files with a question, we need to covert the output of
+     * test_question_maker::get_question_form_data to match. This method recursively
+     * replaces all array elements with key itemid with an array entry with
+     * key files and value an empty array.
+     *
+     * @param mixed $var any data structure.
+     * @return mixed an equivalent structure with the relacements made.
+     */
+    protected function itemid_to_files($var) {
+        if (is_object($var)) {
+            $newvar = new stdClass();
+            foreach(get_object_vars($var) as $field => $value) {
+                $newvar->$field = $this->itemid_to_files($value);
+            }
+
+        } else if (is_array($var)) {
+            $newvar = array();
+            foreach ($var as $index => $value) {
+                if ($index === 'itemid') {
+                    $newvar['files'] = array();
+                } else {
+                    $newvar[$index] = $this->itemid_to_files($value);
+                }
+            }
+
+        } else {
+            $newvar = $var;
+        }
+
+        return $newvar;
+    }
+
+    public function test_write_hint_basic() {
+        $q = $this->make_test_question();
+        $q->name = 'Short answer question';
+        $q->questiontext = 'Name an amphibian: __________';
+        $q->generalfeedback = 'Generalfeedback: frog or toad would have been OK.';
+        if (!isset($q->options)) {
+            $q->options = new stdClass();
+        }
+        $q->options->usecase = false;
+        $q->options->answers = array(
+            13 => new question_answer(13, 'frog', 1.0, 'Frog is a very good answer.', FORMAT_HTML),
+            14 => new question_answer(14, 'toad', 0.8, 'Toad is an OK good answer.', FORMAT_HTML),
+            15 => new question_answer(15, '*', 0.0, 'That is a bad answer.', FORMAT_HTML),
+        );
+        $q->qtype = 'shortanswer';
+        $q->hints = array(
+            new question_hint(0, 'This is the first hint.', FORMAT_MOODLE),
+        );
+
+        $exporter = new qformat_xml();
+        $xml = $exporter->writequestion($q);
+
+        $this->assertRegExp('|<hint format=\"moodle_auto_format\">\s*<text>\s*' .
+                'This is the first hint\.\s*</text>\s*</hint>|', $xml);
+        $this->assertNotRegExp('|<shownumcorrect/>|', $xml);
+        $this->assertNotRegExp('|<clearwrong/>|', $xml);
+        $this->assertNotRegExp('|<options>|', $xml);
+    }
+
+    public function test_write_hint_with_parts() {
+        $q = $this->make_test_question();
+        $q->name = 'Matching question';
+        $q->questiontext = 'Classify the animals.';
+        $q->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.';
+        $q->qtype = 'match';
+
+        if (!isset($q->options)) {
+            $q->options = new stdClass();
+        }
+        $q->options->shuffleanswers = 1;
+        $q->options->correctfeedback = '';
+        $q->options->correctfeedbackformat = FORMAT_HTML;
+        $q->options->partiallycorrectfeedback = '';
+        $q->options->partiallycorrectfeedbackformat = FORMAT_HTML;
+        $q->options->incorrectfeedback = '';
+        $q->options->incorrectfeedbackformat = FORMAT_HTML;
+
+        $q->options->subquestions = array();
+        $q->hints = array(
+            new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, true),
+            new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, false),
+        );
+
+        $exporter = new qformat_xml();
+        $xml = $exporter->writequestion($q);
+
+        $this->assertRegExp(
+                '|<hint format=\"html\">\s*<text>\s*This is the first hint\.\s*</text>|', $xml);
+        $this->assertRegExp(
+                '|<hint format=\"html\">\s*<text>\s*This is the second hint\.\s*</text>|', $xml);
+        list($ignored, $hint1, $hint2) = explode('<hint', $xml);
+        $this->assertNotRegExp('|<shownumcorrect/>|', $hint1);
+        $this->assertRegExp('|<clearwrong/>|', $hint1);
+        $this->assertRegExp('|<shownumcorrect/>|', $hint2);
+        $this->assertNotRegExp('|<clearwrong/>|', $hint2);
+        $this->assertNotRegExp('|<options>|', $xml);
+    }
+
+    public function test_import_hints_no_parts() {
+        $xml = <<<END
+<question>
+    <hint>
+        <text>This is the first hint</text>
+        <clearwrong/>
+    </hint>
+    <hint>
+        <text>This is the second hint</text>
+        <shownumcorrect/>
+    </hint>
+</question>
+END;
+
+        $questionxml = xmlize($xml);
+        $qo = new stdClass();
+
+        $importer = new qformat_xml();
+        $importer->import_hints($qo, $questionxml['question'], false, false, 'html');
+
+        $this->assertEquals(array(
+                array('text' => 'This is the first hint',
+                        'format' => FORMAT_HTML, 'files' => array()),
+                array('text' => 'This is the second hint',
+                        'format' => FORMAT_HTML, 'files' => array()),
+                ), $qo->hint);
+        $this->assertFalse(isset($qo->hintclearwrong));
+        $this->assertFalse(isset($qo->hintshownumcorrect));
+    }
+
+    public function test_import_hints_with_parts() {
+        $xml = <<<END
+<question>
+    <hint>
+        <text>This is the first hint</text>
+        <clearwrong/>
+    </hint>
+    <hint>
+        <text>This is the second hint</text>
+        <shownumcorrect/>
+    </hint>
+</question>
+END;
+
+        $questionxml = xmlize($xml);
+        $qo = new stdClass();
+
+        $importer = new qformat_xml();
+        $importer->import_hints($qo, $questionxml['question'], true, true, 'html');
+
+        $this->assertEquals(array(
+                array('text' => 'This is the first hint',
+                        'format' => FORMAT_HTML, 'files' => array()),
+                array('text' => 'This is the second hint',
+                        'format' => FORMAT_HTML, 'files' => array()),
+                ), $qo->hint);
+        $this->assertEquals(array(1, 0), $qo->hintclearwrong);
+        $this->assertEquals(array(0, 1), $qo->hintshownumcorrect);
+    }
+
+    public function test_import_no_hints_no_error() {
+        $xml = <<<END
+<question>
+</question>
+END;
+
+        $questionxml = xmlize($xml);
+        $qo = new stdClass();
+
+        $importer = new qformat_xml();
+        $importer->import_hints($qo, $questionxml['question'], 'html');
+
+        $this->assertFalse(isset($qo->hint));
+    }
+
+    public function test_import_description() {
+        $xml = '  <question type="description">
+ &nbs