first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
defined('MOODLE_INTERNAL') || die();
/**
* Coverage information for the question_engine.
*
* @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
return new class extends phpunit_coverage_info {
/** @var array The list of files relative to the plugin root to include in coverage generation. */
protected $includelistfiles = [
'bank.php',
'datalib.php',
'lib.php',
'phpunit.xml',
'questionattempt.php',
'questionattemptstep.php',
'questionusage.php',
'renderer.php',
];
};
@@ -0,0 +1,382 @@
<?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/>.
namespace core_question;
use quiz_statistics\tests\statistics_helper;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use qubaid_list;
use question_bank;
use question_engine;
use question_engine_data_mapper;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the parts of {@link question_engine_data_mapper} related to reporting.
*
* @package core_question
* @category test
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class datalib_reporting_queries_test extends \qbehaviour_walkthrough_test_base {
/** @var question_engine_data_mapper */
protected $dm;
/** @var qtype_shortanswer_question */
protected $sa;
/** @var qtype_essay_question */
protected $essay;
/** @var array */
protected $usageids = array();
/** @var qubaid_condition */
protected $bothusages;
/** @var array */
protected $allslots = array();
/**
* Test the various methods that load data for reporting.
*
* Since these methods need an expensive set-up, and then only do read-only
* operations on the data, we use a single method to do the set-up, which
* calls diffents methods to test each query.
*/
public function test_reporting_queries(): void {
// We create two usages, each with two questions, a short-answer marked
// out of 5, and and essay marked out of 10.
//
// In the first usage, the student answers the short-answer
// question correctly, and enters something in the essay.
//
// In the second useage, the student answers the short-answer question
// wrongly, and leaves the essay blank.
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$this->sa = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
$this->essay = $generator->create_question('essay', null,
array('category' => $cat->id));
$this->usageids = array();
// Create the first usage.
$q = question_bank::load_question($this->sa->id);
$this->start_attempt_at_question($q, 'interactive', 5);
$this->allslots[] = $this->slot;
$this->process_submission(array('answer' => 'cat'));
$this->process_submission(array('answer' => 'frog', '-submit' => 1));
$q = question_bank::load_question($this->essay->id);
$this->start_attempt_at_question($q, 'interactive', 10);
$this->allslots[] = $this->slot;
$this->process_submission(array('answer' => '<p>The cat sat on the mat.</p>', 'answerformat' => FORMAT_HTML));
$this->finish();
$this->save_quba();
$this->usageids[] = $this->quba->get_id();
// Create the second usage.
$this->quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
$q = question_bank::load_question($this->sa->id);
$this->start_attempt_at_question($q, 'interactive', 5);
$this->process_submission(array('answer' => 'fish'));
$q = question_bank::load_question($this->essay->id);
$this->start_attempt_at_question($q, 'interactive', 10);
$this->finish();
$this->save_quba();
$this->usageids[] = $this->quba->get_id();
// Set up some things the tests will need.
$this->dm = new question_engine_data_mapper();
$this->bothusages = new qubaid_list($this->usageids);
// Now test the various queries.
$this->dotest_load_questions_usages_latest_steps($this->allslots);
$this->dotest_load_questions_usages_latest_steps(null);
$this->dotest_load_questions_usages_question_state_summary($this->allslots);
$this->dotest_load_questions_usages_question_state_summary(null);
$this->dotest_load_questions_usages_where_question_in_state();
$this->dotest_load_average_marks($this->allslots);
$this->dotest_load_average_marks(null);
$this->dotest_sum_usage_marks_subquery();
$this->dotest_question_attempt_latest_state_view();
}
/**
* This test is executed by {@link test_reporting_queries()}.
*
* @param array|null $slots list of slots to use in the call.
*/
protected function dotest_load_questions_usages_latest_steps($slots) {
$rawstates = $this->dm->load_questions_usages_latest_steps($this->bothusages, $slots,
'qa.id AS questionattemptid, qa.questionusageid, qa.slot, ' .
'qa.questionid, qa.maxmark, qas.sequencenumber, qas.state');
$states = array();
foreach ($rawstates as $state) {
$states[$state->questionusageid][$state->slot] = $state;
unset($state->questionattemptid);
unset($state->questionusageid);
unset($state->slot);
}
$state = $states[$this->usageids[0]][$this->allslots[0]];
$this->assertEquals((object) array(
'questionid' => $this->sa->id,
'maxmark' => 5.0,
'sequencenumber' => 2,
'state' => (string) question_state::$gradedright,
), $state);
$state = $states[$this->usageids[0]][$this->allslots[1]];
$this->assertEquals((object) array(
'questionid' => $this->essay->id,
'maxmark' => 10.0,
'sequencenumber' => 2,
'state' => (string) question_state::$needsgrading,
), $state);
$state = $states[$this->usageids[1]][$this->allslots[0]];
$this->assertEquals((object) array(
'questionid' => $this->sa->id,
'maxmark' => 5.0,
'sequencenumber' => 2,
'state' => (string) question_state::$gradedwrong,
), $state);
$state = $states[$this->usageids[1]][$this->allslots[1]];
$this->assertEquals((object) array(
'questionid' => $this->essay->id,
'maxmark' => 10.0,
'sequencenumber' => 1,
'state' => (string) question_state::$gaveup,
), $state);
}
/**
* This test is executed by {@link test_reporting_queries()}.
*
* @param array|null $slots list of slots to use in the call.
*/
protected function dotest_load_questions_usages_question_state_summary($slots) {
$summary = $this->dm->load_questions_usages_question_state_summary(
$this->bothusages, $slots);
$this->assertEquals($summary[$this->allslots[0] . ',' . $this->sa->id],
(object) array(
'slot' => $this->allslots[0],
'questionid' => $this->sa->id,
'name' => $this->sa->name,
'inprogress' => 0,
'needsgrading' => 0,
'autograded' => 2,
'manuallygraded' => 0,
'all' => 2,
));
$this->assertEquals($summary[$this->allslots[1] . ',' . $this->essay->id],
(object) array(
'slot' => $this->allslots[1],
'questionid' => $this->essay->id,
'name' => $this->essay->name,
'inprogress' => 0,
'needsgrading' => 1,
'autograded' => 1,
'manuallygraded' => 0,
'all' => 2,
));
}
/**
* This test is executed by {@link test_reporting_queries()}.
*/
protected function dotest_load_questions_usages_where_question_in_state() {
$this->assertEquals(
array(array($this->usageids[0], $this->usageids[1]), 2),
$this->dm->load_questions_usages_where_question_in_state($this->bothusages,
'all', $this->allslots[1], null, 'questionusageid'));
$this->assertEquals(
array(array($this->usageids[0], $this->usageids[1]), 2),
$this->dm->load_questions_usages_where_question_in_state($this->bothusages,
'autograded', $this->allslots[0], null, 'questionusageid'));
$this->assertEquals(
array(array($this->usageids[0]), 1),
$this->dm->load_questions_usages_where_question_in_state($this->bothusages,
'needsgrading', $this->allslots[1], null, 'questionusageid'));
}
/**
* This test is executed by {@link test_reporting_queries()}.
*
* @param array|null $slots list of slots to use in the call.
*/
protected function dotest_load_average_marks($slots) {
$averages = $this->dm->load_average_marks($this->bothusages, $slots);
$this->assertEquals(array(
$this->allslots[0] => (object) array(
'slot' => $this->allslots[0],
'averagefraction' => 0.5,
'numaveraged' => 2,
),
$this->allslots[1] => (object) array(
'slot' => $this->allslots[1],
'averagefraction' => 0,
'numaveraged' => 1,
),
), $averages);
}
/**
* This test is executed by {@link test_reporting_queries()}.
*/
protected function dotest_sum_usage_marks_subquery() {
global $DB;
$totals = $DB->get_records_sql_menu("SELECT qu.id, ({$this->dm->sum_usage_marks_subquery('qu.id')}) AS totalmark
FROM {question_usages} qu
WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})");
$this->assertNull($totals[$this->usageids[0]]); // Since a question requires grading.
$this->assertNotNull($totals[$this->usageids[1]]); // Grrr! PHP null == 0 makes this hard.
$this->assertEquals(0, $totals[$this->usageids[1]]);
}
/**
* This test is executed by {@link test_reporting_queries()}.
*/
protected function dotest_question_attempt_latest_state_view() {
global $DB;
list($inlineview, $viewparams) = $this->dm->question_attempt_latest_state_view(
'lateststate', $this->bothusages);
$rawstates = $DB->get_records_sql("
SELECT lateststate.questionattemptid,
qu.id AS questionusageid,
lateststate.slot,
lateststate.questionid,
lateststate.maxmark,
lateststate.sequencenumber,
lateststate.state
FROM {question_usages} qu
LEFT JOIN $inlineview ON lateststate.questionusageid = qu.id
WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})", $viewparams);
$states = array();
foreach ($rawstates as $state) {
$states[$state->questionusageid][$state->slot] = $state;
unset($state->questionattemptid);
unset($state->questionusageid);
unset($state->slot);
}
$state = $states[$this->usageids[0]][$this->allslots[0]];
$this->assertEquals((object) array(
'questionid' => $this->sa->id,
'maxmark' => 5.0,
'sequencenumber' => 2,
'state' => (string) question_state::$gradedright,
), $state);
$state = $states[$this->usageids[0]][$this->allslots[1]];
$this->assertEquals((object) array(
'questionid' => $this->essay->id,
'maxmark' => 10.0,
'sequencenumber' => 2,
'state' => (string) question_state::$needsgrading,
), $state);
$state = $states[$this->usageids[1]][$this->allslots[0]];
$this->assertEquals((object) array(
'questionid' => $this->sa->id,
'maxmark' => 5.0,
'sequencenumber' => 2,
'state' => (string) question_state::$gradedwrong,
), $state);
$state = $states[$this->usageids[1]][$this->allslots[1]];
$this->assertEquals((object) array(
'questionid' => $this->essay->id,
'maxmark' => 10.0,
'sequencenumber' => 1,
'state' => (string) question_state::$gaveup,
), $state);
}
/**
* Test that a Quiz with only description questions wont break \quiz_statistics\task\recalculate.
*
* @covers \quiz_statistics\task\recalculate::execute
*/
public function test_quiz_with_description_questions_recalculate_statistics(): void {
$this->resetAfterTest();
// Create course with quiz module.
$course = $this->getDataGenerator()->create_course();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$layout = '1';
$quiz = $quizgenerator->create_instance([
'course' => $course->id,
'grade' => 0.0, 'sumgrades' => 1,
'layout' => $layout
]);
// Add question of type description to quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
// Create attempt.
$user = $this->getDataGenerator()->create_user();
$quizobj = quiz_settings::create($quiz->id, $user->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$timenow = time();
$attempt = quiz_create_attempt($quizobj, 1, null, $timenow, false, $user->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Submit attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions($timenow, false);
$attemptobj->process_finish($timenow, false);
// Calculate the statistics.
$this->expectOutputRegex('~.*Calculations completed.*~');
statistics_helper::run_pending_recalculation_tasks();
}
}
+308
View File
@@ -0,0 +1,308 @@
<?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/>.
namespace core_question;
use qubaid_join;
use qubaid_list;
use question_bank;
use question_engine;
use question_engine_data_mapper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for parts of {@link question_engine_data_mapper}.
*
* Note that many of the methods used when attempting questions, like
* load_questions_usage_by_activity, insert_question_*, delete_steps are
* tested elsewhere, e.g. by {@link question_usage_autosave_test}. We do not
* re-test them here.
*
* @package core_question
* @category test
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \question_engine_data_mapper
*/
class datalib_test extends \qbehaviour_walkthrough_test_base {
/**
* We create two usages, each with two questions, a short-answer marked
* out of 5, and and essay marked out of 10. We just start these attempts.
*
* Then we change the max mark for the short-answer question in one of the
* usages to 20, using a qubaid_list, and verify.
*
* Then we change the max mark for the essay question in the other
* usage to 2, using a qubaid_join, and verify.
*/
public function test_set_max_mark_in_attempts(): void {
// Set up some things the tests will need.
$this->resetAfterTest();
$dm = new question_engine_data_mapper();
// Create the questions.
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$sa = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
$essay = $generator->create_question('essay', null,
array('category' => $cat->id));
// Create the first usage.
$q = question_bank::load_question($sa->id);
$this->start_attempt_at_question($q, 'interactive', 5);
$q = question_bank::load_question($essay->id);
$this->start_attempt_at_question($q, 'interactive', 10);
$this->finish();
$this->save_quba();
$usage1id = $this->quba->get_id();
// Create the second usage.
$this->quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
$q = question_bank::load_question($sa->id);
$this->start_attempt_at_question($q, 'interactive', 5);
$this->process_submission(array('answer' => 'fish'));
$q = question_bank::load_question($essay->id);
$this->start_attempt_at_question($q, 'interactive', 10);
$this->finish();
$this->save_quba();
$usage2id = $this->quba->get_id();
// Test set_max_mark_in_attempts with a qubaid_list.
$usagestoupdate = new qubaid_list(array($usage1id));
$dm->set_max_mark_in_attempts($usagestoupdate, 1, 20.0);
$quba1 = question_engine::load_questions_usage_by_activity($usage1id);
$quba2 = question_engine::load_questions_usage_by_activity($usage2id);
$this->assertEquals(20, $quba1->get_question_max_mark(1));
$this->assertEquals(10, $quba1->get_question_max_mark(2));
$this->assertEquals( 5, $quba2->get_question_max_mark(1));
$this->assertEquals(10, $quba2->get_question_max_mark(2));
// Test set_max_mark_in_attempts with a qubaid_join.
$usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id',
'qu.id = :usageid', array('usageid' => $usage2id));
$dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0);
$quba1 = question_engine::load_questions_usage_by_activity($usage1id);
$quba2 = question_engine::load_questions_usage_by_activity($usage2id);
$this->assertEquals(20, $quba1->get_question_max_mark(1));
$this->assertEquals(10, $quba1->get_question_max_mark(2));
$this->assertEquals( 5, $quba2->get_question_max_mark(1));
$this->assertEquals( 2, $quba2->get_question_max_mark(2));
// Test the nothing to do case.
$usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id',
'qu.id = :usageid', array('usageid' => -1));
$dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0);
$quba1 = question_engine::load_questions_usage_by_activity($usage1id);
$quba2 = question_engine::load_questions_usage_by_activity($usage2id);
$this->assertEquals(20, $quba1->get_question_max_mark(1));
$this->assertEquals(10, $quba1->get_question_max_mark(2));
$this->assertEquals( 5, $quba2->get_question_max_mark(1));
$this->assertEquals( 2, $quba2->get_question_max_mark(2));
}
public function test_load_used_variants(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
$questiondata2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
$questiondata3 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
$quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
$quba->set_preferred_behaviour('deferredfeedback');
$question1 = question_bank::load_question($questiondata1->id);
$question3 = question_bank::load_question($questiondata3->id);
$quba->add_question($question1);
$quba->add_question($question1);
$quba->add_question($question3);
$quba->start_all_questions();
question_engine::save_questions_usage_by_activity($quba);
$this->assertEquals(array(
$questiondata1->id => array(1 => 2),
$questiondata2->id => array(),
$questiondata3->id => array(1 => 1),
), question_engine::load_used_variants(
array($questiondata1->id, $questiondata2->id, $questiondata3->id),
new qubaid_list(array($quba->get_id()))));
}
public function test_repeated_usage_saving_new_usage(): void {
global $DB;
$this->resetAfterTest();
$initialqurows = $DB->count_records('question_usages');
$initialqarows = $DB->count_records('question_attempts');
$initialqasrows = $DB->count_records('question_attempt_steps');
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
$quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
$quba->set_preferred_behaviour('deferredfeedback');
$quba->add_question(question_bank::load_question($questiondata1->id));
$quba->start_all_questions();
question_engine::save_questions_usage_by_activity($quba);
// Check one usage, question_attempts and step added.
$firstid = $quba->get_id();
$this->assertEquals(1, $DB->count_records('question_usages') - $initialqurows);
$this->assertEquals(1, $DB->count_records('question_attempts') - $initialqarows);
$this->assertEquals(1, $DB->count_records('question_attempt_steps') - $initialqasrows);
$quba->finish_all_questions();
question_engine::save_questions_usage_by_activity($quba);
// Check usage id not changed.
$this->assertEquals($firstid, $quba->get_id());
// Check still one usage, question_attempts, but now two steps.
$this->assertEquals(1, $DB->count_records('question_usages') - $initialqurows);
$this->assertEquals(1, $DB->count_records('question_attempts') - $initialqarows);
$this->assertEquals(2, $DB->count_records('question_attempt_steps') - $initialqasrows);
}
public function test_repeated_usage_saving_existing_usage(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
$initquba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
$initquba->set_preferred_behaviour('deferredfeedback');
$slot = $initquba->add_question(question_bank::load_question($questiondata1->id));
$initquba->start_all_questions();
question_engine::save_questions_usage_by_activity($initquba);
$quba = question_engine::load_questions_usage_by_activity($initquba->get_id());
$initialqurows = $DB->count_records('question_usages');
$initialqarows = $DB->count_records('question_attempts');
$initialqasrows = $DB->count_records('question_attempt_steps');
$quba->process_all_actions(time(), $quba->prepare_simulated_post_data(
[$slot => ['answer' => 'Frog']]));
question_engine::save_questions_usage_by_activity($quba);
// Check one usage, question_attempts and step added.
$this->assertEquals(0, $DB->count_records('question_usages') - $initialqurows);
$this->assertEquals(0, $DB->count_records('question_attempts') - $initialqarows);
$this->assertEquals(1, $DB->count_records('question_attempt_steps') - $initialqasrows);
$quba->finish_all_questions();
question_engine::save_questions_usage_by_activity($quba);
// Check still one usage, question_attempts, but now two steps.
$this->assertEquals(0, $DB->count_records('question_usages') - $initialqurows);
$this->assertEquals(0, $DB->count_records('question_attempts') - $initialqarows);
$this->assertEquals(2, $DB->count_records('question_attempt_steps') - $initialqasrows);
}
/**
* Test that database operations on an empty usage work without errors.
*/
public function test_save_and_load_an_empty_usage(): void {
$this->resetAfterTest();
// Create a new usage.
$quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
$quba->set_preferred_behaviour('deferredfeedback');
// Save it.
question_engine::save_questions_usage_by_activity($quba);
// Reload it.
$reloadedquba = question_engine::load_questions_usage_by_activity($quba->get_id());
$this->assertCount(0, $quba->get_slots());
// Delete it.
question_engine::delete_questions_usage_by_activity($quba->get_id());
}
public function test_cannot_save_a_step_with_a_missing_state(): void {
global $DB;
$this->resetAfterTest();
// Create a question.
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$questiondata = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
// Create a usage.
$quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
$quba->set_preferred_behaviour('deferredfeedback');
$slot = $quba->add_question(question_bank::load_question($questiondata->id));
$quba->start_all_questions();
// Add a step with a bad state.
$newstep = new \question_attempt_step();
$newstep->set_state(null);
$addstepmethod = new \ReflectionMethod('question_attempt', 'add_step');
$addstepmethod->invoke($quba->get_question_attempt($slot), $newstep);
// Verify that trying to save this throws an exception.
$this->expectException(\dml_write_exception::class);
question_engine::save_questions_usage_by_activity($quba);
}
/**
* Test cases for {@see test_get_file_area_name()}.
*
* @return array test cases
*/
public function get_file_area_name_cases(): array {
return [
'simple variable' => ['response_attachments', 'response_attachments'],
'behaviour variable' => ['response_5:answer', 'response_5answer'],
'variable with special character' => ['response_5:answer', 'response_5answer'],
'multiple underscores in different places' => ['response_weird____variable__name', 'response_weird_variable_name'],
];
}
/**
* Test get_file_area_name.
*
* @covers \question_file_saver::clean_file_area_name
* @dataProvider get_file_area_name_cases
*
* @param string $uncleanedfilearea
* @param string $expectedfilearea
*/
public function test_clean_file_area_name(string $uncleanedfilearea, string $expectedfilearea): void {
$this->assertEquals($expectedfilearea, \question_file_saver::clean_file_area_name($uncleanedfilearea));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,158 @@
<?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/>.
namespace core_question;
use qubaid_condition;
use qubaid_join;
use qubaid_list;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
/**
* Unit tests for qubaid_condition and subclasses.
*
* @package core_question
* @category test
* @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(): void {
$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(): void {
$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(): void {
$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(): void {
$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(): void {
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(): void {
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(): void {
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(): void {
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());
}
}
@@ -0,0 +1,97 @@
<?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/>.
namespace core_question;
/**
* Unit tests for {@see \question_display_options}.
*
* @coversDefaultClass \question_display_options
* @package core_question
* @category test
* @copyright 2023 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_display_options_test extends \advanced_testcase {
/**
* Data provider for {@see self::test_has_question_identifier()}
*
* @return array[]
*/
public function has_question_identifier_provider(): array {
return [
'Empty string' => ['', false],
'Empty space' => [' ', false],
'Null' => [null, false],
'Non-empty string' => ["Hello!", true],
];
}
/**
* Tests for {@see \question_display_options::has_question_identifier}
*
* @covers ::has_question_identifier
* @dataProvider has_question_identifier_provider
* @param string|null $identifier The question identifier
* @param bool $expected The expected return value
* @return void
*/
public function test_has_question_identifier(?string $identifier, bool $expected): void {
$options = new \question_display_options();
$options->questionidentifier = $identifier;
$this->assertEquals($expected, $options->has_question_identifier());
}
/**
* Data provider for {@see self::test_add_question_identifier_to_label()
*
* @return array[]
*/
public function add_question_identifier_to_label_provider(): array {
return [
'Empty string identifier' => ['Hello', '', false, false, "Hello"],
'Null identifier' => ['Hello', null, false, false, "Hello"],
'With identifier' => ['Hello', 'World', false, false, "Hello World"],
'With identifier, sr-only' => ['Hello', 'World', true, false, 'Hello <span class="sr-only">World</span>'],
'With identifier, prepend' => ['Hello', 'World', false, true, "World Hello"],
];
}
/**
* Tests for {@see \question_display_options::add_question_identifier_to_label()}
*
* @covers ::add_question_identifier_to_label
* @dataProvider add_question_identifier_to_label_provider
* @param string $label The label string.
* @param string|null $identifier The question identifier.
* @param bool $sronly Whether to render the question identifier in a sr-only container
* @param bool $addbefore Whether to render the question identifier before the label.
* @param string $expected The expected return value.
* @return void
*/
public function test_add_question_identifier_to_label(
string $label,
?string $identifier,
bool $sronly,
bool $addbefore,
string $expected
): void {
$options = new \question_display_options();
$options->questionidentifier = $identifier;
$this->assertEquals($expected, $options->add_question_identifier_to_label($label, $sronly, $addbefore));
}
}
@@ -0,0 +1,339 @@
<?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
*/
namespace core_question;
use advanced_testcase;
use moodle_exception;
use question_engine;
/**
* 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
* @coversDefaultClass \question_engine
*/
class question_engine_test extends advanced_testcase {
/**
* Load required libraries.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/question/engine/lib.php");
}
/**
* Tests for load_behaviour_class.
*
* @covers \question_engine::load_behaviour_class
*/
public function test_load_behaviour_class(): void {
// Exercise SUT.
question_engine::load_behaviour_class('deferredfeedback');
// Verify.
$this->assertTrue(class_exists('qbehaviour_deferredfeedback'));
}
/**
* Tests for load_behaviour_class when a class is missing.
*
* @covers \question_engine::load_behaviour_class
*/
public function test_load_behaviour_class_missing(): void {
// Exercise SUT.
$this->expectException(moodle_exception::class);
question_engine::load_behaviour_class('nonexistantbehaviour');
}
/**
* Test the get_behaviour_unused_display_options with various options.
*
* @covers \question_engine::get_behaviour_unused_display_options
* @dataProvider get_behaviour_unused_display_options_provider
* @param string $behaviour
* @param array $expected
*/
public function test_get_behaviour_unused_display_options(string $behaviour, array $expected): void {
$this->assertEquals($expected, question_engine::get_behaviour_unused_display_options($behaviour));
}
/**
* Data provider for get_behaviour_unused_display_options.
*
* @return array
*/
public function get_behaviour_unused_display_options_provider(): array {
return [
'interactive' => [
'interactive',
[],
],
'deferredfeedback' => [
'deferredfeedback',
['correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'],
],
'deferredcbm' => [
'deferredcbm',
['correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'],
],
'manualgraded' => [
'manualgraded',
['correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'],
],
];
}
/**
* Tests for can_questions_finish_during_the_attempt.
*
* @covers \question_engine::can_questions_finish_during_the_attempt
* @dataProvider can_questions_finish_during_the_attempt_provider
* @param string $behaviour
* @param bool $expected
*/
public function test_can_questions_finish_during_the_attempt(string $behaviour, bool $expected): void {
$this->assertEquals($expected, question_engine::can_questions_finish_during_the_attempt($behaviour));
}
/**
* Data provider for can_questions_finish_during_the_attempt_provider.
*
* @return array
*/
public function can_questions_finish_during_the_attempt_provider(): array {
return [
['deferredfeedback', false],
['interactive', true],
];
}
/**
* Tests for sort_behaviours
*
* @covers \question_engine::sort_behaviours
* @dataProvider sort_behaviours_provider
* @param array $input The params passed to sort_behaviours
* @param array $expected
*/
public function test_sort_behaviours(array $input, array $expected): void {
$this->assertSame($expected, question_engine::sort_behaviours(...$input));
}
/**
* Data provider for sort_behaviours.
*
* @return array
*/
public function sort_behaviours_provider(): array {
$in = [
'b1' => 'Behave 1',
'b2' => 'Behave 2',
'b3' => 'Behave 3',
'b4' => 'Behave 4',
'b5' => 'Behave 5',
'b6' => 'Behave 6',
];
return [
[
[$in, '', '', ''],
$in,
],
[
[$in, '', 'b4', 'b4'],
$in,
],
[
[$in, '', 'b1,b2,b3,b4', 'b4'],
['b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6'],
],
[
[$in, 'b6,b1,b4', 'b2,b3,b4,b5', 'b4'],
['b6' => 'Behave 6', 'b1' => 'Behave 1', 'b4' => 'Behave 4'],
],
[
[$in, 'b6,b5,b4', 'b1,b2,b3', 'b4'],
['b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4'],
],
[
[$in, 'b1,b6,b5', 'b1,b2,b3,b4', 'b4'],
['b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4'],
],
[
[$in, 'b2,b4,b6', 'b1,b3,b5', 'b2'],
['b2' => 'Behave 2', 'b4' => 'Behave 4', 'b6' => 'Behave 6'],
],
// Ignore unknown input in the order argument.
[
[$in, 'unknown', '', ''],
$in,
],
// Ignore unknown input in the disabled argument.
[
[$in, '', 'unknown', ''],
$in,
],
];
}
/**
* Tests for is_manual_grade_in_range.
*
* @dataProvider is_manual_grade_in_range_provider
* @covers \question_engine::is_manual_grade_in_range
* @param array $post The values to add to $_POST
* @param array $params The params to pass to is_manual_grade_in_range
* @param bool $expected
*/
public function test_is_manual_grade_in_range(array $post, array $params, bool $expected): void {
$_POST[] = $post;
$this->assertEquals($expected, question_engine::is_manual_grade_in_range(...$params));
}
/**
* Data provider for is_manual_grade_in_range tests.
*
* @return array
*/
public function is_manual_grade_in_range_provider(): array {
return [
'In range' => [
'post' => [
'q1:2_-mark' => 0.5,
'q1:2_-maxmark' => 1.0,
'q1:2_:minfraction' => 0,
'q1:2_:maxfraction' => 1,
],
'range' => [1, 2],
'expected' => true,
],
'Bottom end' => [
'post' => [
'q1:2_-mark' => -1.0,
'q1:2_-maxmark' => 2.0,
'q1:2_:minfraction' => -0.5,
'q1:2_:maxfraction' => 1,
],
'range' => [1, 2],
'expected' => true,
],
'Too low' => [
'post' => [
'q1:2_-mark' => -1.1,
'q1:2_-maxmark' => 2.0,
'q1:2_:minfraction' => -0.5,
'q1:2_:maxfraction' => 1,
],
'range' => [1, 2],
'expected' => true,
],
'Top end' => [
'post' => [
'q1:2_-mark' => 3.0,
'q1:2_-maxmark' => 1.0,
'q1:2_:minfraction' => -6.0,
'q1:2_:maxfraction' => 3.0,
],
'range' => [1, 2],
'expected' => true,
],
'Too high' => [
'post' => [
'q1:2_-mark' => 3.1,
'q1:2_-maxmark' => 1.0,
'q1:2_:minfraction' => -6.0,
'q1:2_:maxfraction' => 3.0,
],
'range' => [1, 2],
'expected' => true,
],
];
}
/**
* Tests for is_manual_grade_in_range.
*
* @covers \question_engine::is_manual_grade_in_range
*/
public function test_is_manual_grade_in_range_ungraded(): void {
$this->assertTrue(question_engine::is_manual_grade_in_range(1, 2));
}
/**
* Ensure that the number renderer performs as expected.
*
* @covers \core_question_renderer::number
* @dataProvider render_question_number_provider
* @param mixed $value
* @param string $expected
*/
public function test_render_question_number($value, string $expected): void {
global $PAGE;
$renderer = new \core_question_renderer($PAGE, 'core_question');
$rc = new \ReflectionClass($renderer);
$rcm = $rc->getMethod('number');
$this->assertEquals($expected, $rcm->invoke($renderer, $value));
}
/**
* Data provider for test_render_question_number.
*
* @return array
*/
public function render_question_number_provider(): array {
return [
'Test with number is i character' => [
'i',
'<h3 class="no">Information</h3>',
],
'Test with number is empty string' => [
'',
'',
],
'Test with null' => [
null,
'',
],
'Test with number is 0' => [
0,
'<h3 class="no">Question <span class="qno">0</span></h3>',
],
'Test with number is numeric' => [
1,
'<h3 class="no">Question <span class="qno">1</span></h3>',
],
'Test with number is string' => [
'1 of 2',
'<h3 class="no">Question <span class="qno">1 of 2</span></h3>',
],
];
}
}
@@ -0,0 +1,123 @@
<?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/>.
namespace core_question;
use advanced_testcase;
use context_system;
use core_question\local\bank\question_version_status;
use core_question_generator;
/**
* Unit tests for the {@see question_reference_manager} class.
*
* @package core_question
* @category test
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_question\question_reference_manager
*/
class question_reference_manager_test extends advanced_testcase {
public function test_questions_with_references(): void {
global $DB;
$this->resetAfterTest();
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$systemcontext = context_system::instance();
// Create three questions, each with three versions.
// In each case, the third version is draft.
$cat = $questiongenerator->create_question_category();
$q1v1 = $questiongenerator->create_question('truefalse', null, ['name' => 'Q1V1', 'category' => $cat->id]);
$q1v2 = $questiongenerator->update_question($q1v1, null, ['name' => 'Q1V2']);
$q1v3 = $questiongenerator->update_question($q1v2, null,
['name' => 'Q1V3', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
$q2v1 = $questiongenerator->create_question('truefalse', null, ['name' => 'Q2V1', 'category' => $cat->id]);
$q2v2 = $questiongenerator->update_question($q2v1, null, ['name' => 'Q2V2']);
$q2v3 = $questiongenerator->update_question($q2v2, null,
['name' => 'Q2V3', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
$q3v1 = $questiongenerator->create_question('truefalse', null, ['name' => 'Q3V1', 'category' => $cat->id]);
$q3v2 = $questiongenerator->update_question($q3v1, null, ['name' => 'Q3V2']);
$q3v3 = $questiongenerator->update_question($q3v2, null,
['name' => 'Q3V3', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
// Create specific references to Q2V1 and Q2V3.
$DB->insert_record('question_references', ['usingcontextid' => $systemcontext->id,
'component' => 'core_question', 'questionarea' => 'test', 'itemid' => 0,
'questionbankentryid' => $q2v1->questionbankentryid, 'version' => 1]);
$DB->insert_record('question_references', ['usingcontextid' => $systemcontext->id,
'component' => 'core_question', 'questionarea' => 'test', 'itemid' => 1,
'questionbankentryid' => $q2v1->questionbankentryid, 'version' => 3]);
// Create an always-latest reference to Q3.
$DB->insert_record('question_references', ['usingcontextid' => $systemcontext->id,
'component' => 'core_question', 'questionarea' => 'test', 'itemid' => 2,
'questionbankentryid' => $q3v1->questionbankentryid, 'version' => null]);
// Verify which versions of Q1 are used.
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q1v1->id]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q1v2->id]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q1v3->id]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q1v1->id, $q1v2->id, $q1v3->id]));
// Verify which versions of Q2 are used.
$this->assertEqualsCanonicalizing([$q2v1->id],
question_reference_manager::questions_with_references([$q2v1->id]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q2v2->id]));
$this->assertEqualsCanonicalizing([$q2v3->id],
question_reference_manager::questions_with_references([$q2v3->id]));
$this->assertEqualsCanonicalizing([$q2v1->id, $q2v3->id],
question_reference_manager::questions_with_references([$q2v1->id, $q2v2->id, $q2v3->id]));
// Verify which versions of Q1 are used.
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q3v1->id]));
$this->assertEqualsCanonicalizing([$q3v2->id],
question_reference_manager::questions_with_references([$q3v2->id]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([$q3v3->id]));
$this->assertEqualsCanonicalizing([$q3v2->id],
question_reference_manager::questions_with_references([$q3v1->id, $q3v2->id, $q3v3->id]));
// Do some combined queries.
$this->assertEqualsCanonicalizing([$q2v1->id, $q2v3->id, $q3v2->id],
question_reference_manager::questions_with_references([
$q1v1->id, $q1v2->id, $q1v3->id,
$q2v1->id, $q2v2->id, $q2v3->id,
$q3v1->id, $q3v2->id, $q3v3->id]));
$this->assertEqualsCanonicalizing([$q2v1->id, $q2v3->id, $q3v2->id],
question_reference_manager::questions_with_references([$q2v1->id, $q2v3->id, $q3v2->id]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([
$q1v1->id, $q1v2->id, $q1v3->id,
$q2v2->id,
$q3v1->id, $q3v3->id]));
// Test some edge cases.
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([]));
$this->assertEqualsCanonicalizing([],
question_reference_manager::questions_with_references([-1]));
}
}
@@ -0,0 +1,256 @@
<?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/>.
namespace core_question;
use question_attempt;
use question_bank;
use question_engine;
use question_state;
use question_test_recordset;
use question_usage_null_observer;
use testable_question_engine_unit_of_work;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for loading data into the {@link question_attempt} class.
*
* Action methods like start, process_action and finish are assumed to be
* tested by walkthrough tests in the various behaviours.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattempt_db_test extends \data_loading_method_test_base {
public function test_load(): void {
$records = new question_test_recordset(array(
array('questionattemptid', 'contextid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', '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, 1.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete', null, 1256233705, 1, 'answer', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 1, '', '', '', 1256233790, 3, 2, 'complete', null, 1256233710, 1, 'answer', '0'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 4, 3, 'complete', null, 1256233715, 1, 'answer', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 4, 'gradedright', 1.0000000, 1256233720, 1, '-finish', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.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, 1.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1, '-mark', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.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(false)->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(): void {
$records = new question_test_recordset(array(
array('questionattemptid', 'contextid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', '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, 1.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(false));
$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());
}
public function test_load_with_autosaved_data(): void {
$records = new question_test_recordset(array(
array('questionattemptid', 'contextid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', '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, 1.0000000, 0, '', '', '', 1256233790, 4, -3, 'complete', null, 1256233715, 1, 'answer', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete', null, 1256233705, 1, 'answer', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 1, '', '', '', 1256233790, 3, 2, 'complete', null, 1256233710, 1, 'answer', '0'),
));
$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(false)->questiontext);
$this->assertEquals(4, $qa->get_num_steps());
$this->assertTrue($qa->has_autosaved_step());
$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());
}
public function test_load_with_unnecessary_autosaved_data(): void {
// The point here is that the somehow (probably due to two things
// happening concurrently, we have autosaved data in the database that
// has already been superceded by real data, so it should be ignored.
// There is also a second lot of redundant data to delete.
$records = new question_test_recordset(array(
array('questionattemptid', 'contextid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', '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, 1.0000000, 0, '', '', '', 1256233790, 5, -2, 'complete', null, 1256233715, 1, 'answer', '0'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 4, -1, 'complete', null, 1256233715, 1, 'answer', '0'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete', null, 1256233705, 1, 'answer', '1'),
array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0000000, 0.0000000, 1.0000000, 1, '', '', '', 1256233790, 3, 2, 'complete', null, 1256233710, 1, 'answer', '0'),
));
$question = \test_question_maker::make_question('truefalse', 'true');
$question->id = -1;
question_bank::start_unit_test();
question_bank::load_test_question_data($question);
$observer = new testable_question_engine_unit_of_work(
question_engine::make_questions_usage_by_activity('unit_test', \context_system::instance()));
$qa = question_attempt::load_from_records($records, 1, $observer, 'deferredfeedback');
question_bank::end_unit_test();
$this->assertEquals($question->questiontext, $qa->get_question(false)->questiontext);
$this->assertEquals(3, $qa->get_num_steps());
$this->assertFalse($qa->has_autosaved_step());
$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());
$this->assertEquals(2, count($observer->get_steps_deleted()));
}
}
@@ -0,0 +1,114 @@
<?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/>.
namespace core_question;
use question_attempt;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the {@link question_attempt} class.
*
* Action methods like start, process_action and finish are assumed to be
* tested by walkthrough tests in the various behaviours.
*
* These are the tests that don't require any steps.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattempt_test extends \advanced_testcase {
/** @var question_definition a question that can be used in the tests. */
private $question;
/** @var int fake question_usage id used in some tests. */
private $usageid;
/** @var question_attempt a question attempt that can be used in the tests. */
private $qa;
protected function setUp(): void {
$this->question = \test_question_maker::make_question('description');
$this->question->defaultmark = 3;
$this->usageid = 13;
$this->qa = new question_attempt($this->question, $this->usageid);
}
public function test_constructor_sets_maxmark(): void {
$qa = new question_attempt($this->question, $this->usageid);
$this->assertSame($this->question, $qa->get_question(false));
$this->assertEquals(3, $qa->get_max_mark());
}
public function test_maxmark_beats_default_mark(): void {
$qa = new question_attempt($this->question, $this->usageid, null, 2);
$this->assertEquals(2, $qa->get_max_mark());
}
public function test_get_set_slot(): void {
$this->qa->set_slot(7);
$this->assertEquals(7, $this->qa->get_slot());
}
public function test_fagged_initially_false(): void {
$this->assertEquals(false, $this->qa->is_flagged());
}
public function test_set_is_flagged(): void {
$this->qa->set_flagged(true);
$this->assertEquals(true, $this->qa->is_flagged());
}
public function test_get_qt_field_name(): void {
$name = $this->qa->get_qt_field_name('test');
$this->assertMatchesRegularExpression('/^' . preg_quote($this->qa->get_field_prefix(), '/') . '/', $name);
$this->assertMatchesRegularExpression('/_test$/', $name);
}
public function test_get_behaviour_field_name(): void {
$name = $this->qa->get_behaviour_field_name('test');
$this->assertMatchesRegularExpression('/^' . preg_quote($this->qa->get_field_prefix(), '/') . '/', $name);
$this->assertMatchesRegularExpression('/_-test$/', $name);
}
public function test_get_field_prefix(): void {
$this->qa->set_slot(7);
$name = $this->qa->get_field_prefix();
$this->assertMatchesRegularExpression('/' . preg_quote($this->usageid, '/') . '/', $name);
$this->assertMatchesRegularExpression('/' . preg_quote($this->qa->get_slot(), '/') . '/', $name);
}
public function test_get_submitted_var_not_present_var_returns_null(): void {
$this->assertNull($this->qa->get_submitted_var(
'reallyunlikelyvariablename', PARAM_BOOL));
}
public function test_get_all_submitted_qt_vars(): void {
$this->qa->set_usage_id('MDOgzdhS4W');
$this->qa->set_slot(1);
$this->assertEquals(array('omval_response1' => 1, 'omval_response2' => 666, 'omact_gen_14' => 'Check'),
$this->qa->get_all_submitted_qt_vars(array(
'qMDOgzdhS4W:1_omval_response1' => 1,
'qMDOgzdhS4W:1_omval_response2' => 666,
'qMDOgzdhS4W:1_omact_gen_14' => 'Check',
)));
}
}
@@ -0,0 +1,208 @@
<?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/>.
namespace core_question;
use question_attempt;
use question_attempt_step;
use question_state;
use testable_question_attempt;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* These tests use a standard fixture of a {@link question_attempt} with three steps.
*
* Action methods like start, process_action and finish are assumed to be
* tested by walkthrough tests in the various behaviours.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattempt_with_steps_test extends \advanced_testcase {
private $question;
private $qa;
protected function setUp(): void {
$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(): void {
$this->qa = null;
}
public function test_get_step_before_start(): void {
$this->expectException(\moodle_exception::class);
$step = $this->qa->get_step(-1);
}
public function test_get_step_at_start(): void {
$step = $this->qa->get_step(0);
$this->assertEquals(0, $step->get_qt_var('i'));
}
public function test_get_step_at_end(): void {
$step = $this->qa->get_step(2);
$this->assertEquals(2, $step->get_qt_var('i'));
}
public function test_get_step_past_end(): void {
$this->expectException(\moodle_exception::class);
$step = $this->qa->get_step(3);
}
public function test_get_num_steps(): void {
$this->assertEquals(3, $this->qa->get_num_steps());
}
public function test_get_last_step(): void {
$step = $this->qa->get_last_step();
$this->assertEquals(2, $step->get_qt_var('i'));
}
public function test_get_last_qt_var_there1(): void {
$this->assertEquals(2, $this->qa->get_last_qt_var('i'));
}
public function test_get_last_qt_var_there2(): void {
$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(): void {
$this->assertNull($this->qa->get_last_qt_var('notthere'));
}
public function test_get_last_qt_var_missing_default(): void {
$this->assertEquals('default', $this->qa->get_last_qt_var('notthere', 'default'));
}
public function test_get_last_behaviour_var_missing(): void {
$this->assertNull($this->qa->get_last_qt_var('notthere'));
}
public function test_get_last_behaviour_var_there(): void {
$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(): void {
$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(): void {
$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(): void {
$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(): void {
$this->assertNull($this->qa->get_fraction());
}
public function test_format_mark(): void {
$this->qa->get_step(2)->set_fraction(0.5);
$this->assertEquals('1.00', $this->qa->format_mark(2));
}
public function test_format_max_mark(): void {
$this->assertEquals('2.0000000', $this->qa->format_max_mark(7));
}
public function test_get_min_fraction(): void {
$this->qa->set_min_fraction(-1);
$this->assertEquals(-1, $this->qa->get_min_fraction());
}
public function test_cannot_get_min_fraction_before_start(): void {
$qa = new question_attempt($this->question, 0);
$this->expectException('moodle_exception');
$qa->get_min_fraction();
}
public function test_get_max_fraction(): void {
$this->qa->set_max_fraction(2);
$this->assertEquals(2, $this->qa->get_max_fraction());
}
public function test_cannot_get_max_fraction_before_start(): void {
$qa = new question_attempt($this->question, 0);
$this->expectException('moodle_exception');
$qa->get_max_fraction();
}
/**
* Test cases for {@see test_validate_manual_mark()}.
*
* @return array test cases
*/
public function validate_manual_mark_cases(): array {
// Recall, the DB schema stores question grade information to 7 decimal places.
return [
[0, 1, 2, null, ''],
[0, 1, 2, '', ''],
[0, 1, 2, '0', ''],
[0, 1, 2, '0.0', ''],
[0, 1, 2, '2,0', ''],
[0, 1, 2, 'frog', get_string('manualgradeinvalidformat', 'question')],
[0, 1, 2, '2.1', get_string('manualgradeoutofrange', 'question')],
[0, 1, 2, '-0,01', get_string('manualgradeoutofrange', 'question')],
[-0.3333333, 1, 0.75, '0.75', ''],
[-0.3333333, 1, 0.75, '0.7500001', get_string('manualgradeoutofrange', 'question')],
[-0.3333333, 1, 0.75, '-0.25', ''],
[-0.3333333, 1, 0.75, '-0.2500001', get_string('manualgradeoutofrange', 'question')],
];
}
/**
* Test validate_manual_mark.
*
* @dataProvider validate_manual_mark_cases
*
* @param float $minfraction minimum fraction for the question being attempted.
* @param float $maxfraction maximum fraction for the question being attempted.
* @param float $maxmark marks for the question attempt.
* @param string|null $currentmark submitted mark.
* @param string $expectederror expected error, if any.
*/
public function test_validate_manual_mark(float $minfraction, float $maxfraction,
float $maxmark, ?string $currentmark, string $expectederror): void {
$this->qa->set_min_fraction($minfraction);
$this->qa->set_max_fraction($maxfraction);
$this->qa->set_max_mark($maxmark);
$this->assertSame($expectederror, $this->qa->validate_manual_mark($currentmark));
}
}
@@ -0,0 +1,112 @@
<?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/>.
namespace core_question;
use question_engine;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* This file contains tests for the {@link question_attempt_iterator} class.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattemptiterator_test extends \advanced_testcase {
private $quba;
private $qas = array();
private $iterator;
protected function setUp(): void {
$this->quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
$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(): void {
$this->quba = null;
$this->iterator = null;
}
public function test_foreach_loop(): void {
$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(): void {
$this->assertFalse(isset($this->iterator[0]));
}
public function test_offsetExists_at_start(): void {
$this->assertTrue(isset($this->iterator[1]));
}
public function test_offsetExists_at_endt(): void {
$this->assertTrue(isset($this->iterator[2]));
}
public function test_offsetExists_past_end(): void {
$this->assertFalse(isset($this->iterator[3]));
}
public function test_offsetGet_before_start(): void {
$this->expectException(\moodle_exception::class);
$step = $this->iterator[0];
}
public function test_offsetGet_at_start(): void {
$this->assertSame($this->qas[1], $this->iterator[1]);
}
public function test_offsetGet_at_end(): void {
$this->assertSame($this->qas[2], $this->iterator[2]);
}
public function test_offsetGet_past_end(): void {
$this->expectException(\moodle_exception::class);
$step = $this->iterator[3];
}
public function test_cannot_set(): void {
$this->expectException(\moodle_exception::class);
$this->iterator[0] = null;
}
public function test_cannot_unset(): void {
$this->expectException(\moodle_exception::class);
unset($this->iterator[2]);
}
}
@@ -0,0 +1,81 @@
<?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/>.
namespace core_question;
use question_attempt_step;
use question_state;
use question_test_recordset;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the loading data into the {@link question_attempt_step} class.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattemptstep_db_test extends \data_loading_method_test_base {
public function test_load_with_data(): void {
$records = new question_test_recordset(array(
array('attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value', 'qtype', 'contextid'),
array( 1, 1, 0, 'todo', null, 1256228502, 13, null, null, 'description', 1),
array( 2, 1, 1, 'complete', null, 1256228505, 13, 'x', 'a', 'description', 1),
array( 2, 1, 1, 'complete', null, 1256228505, 13, '_y', '_b', 'description', 1),
array( 2, 1, 1, 'complete', null, 1256228505, 13, '-z', '!c', 'description', 1),
array( 2, 1, 1, 'complete', null, 1256228505, 13, '-_t', '!_d', 'description', 1),
array( 3, 1, 2, 'gradedright', 1.0, 1256228515, 13, '-finish', '1', 'description', 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(): void {
$records = new question_test_recordset(array(
array('attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value', 'contextid'),
array( 2, 1, 1, 'complete', null, 1256228505, 13, null, null, 1),
));
$step = question_attempt_step::load_from_records($records, 2, 'description');
$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(): void {
$records = new question_test_recordset(array(
array('attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value', 'contextid'),
array( 1, 1, 0, 'todo', null, 1256228502, 13, 'x', 'right', 1),
array( 2, 2, 0, 'complete', null, 1256228505, 13, 'x', 'wrong', 1),
));
$step = question_attempt_step::load_from_records($records, 1, 'description');
$this->assertEquals(array('x' => 'right'), $step->get_all_data());
}
}
@@ -0,0 +1,173 @@
<?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/>.
namespace core_question;
use question_attempt_step;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the {@link question_attempt_step} class.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattemptstep_test extends \advanced_testcase {
public function test_initial_state_unprocessed(): void {
$step = new question_attempt_step();
$this->assertEquals(question_state::$unprocessed, $step->get_state());
}
public function test_get_set_state(): void {
$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(): void {
$step = new question_attempt_step();
$this->assertNull($step->get_fraction());
}
public function test_get_set_fraction(): void {
$step = new question_attempt_step();
$step->set_fraction(0.5);
$this->assertEquals(0.5, $step->get_fraction());
}
public function test_has_var(): void {
$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(): void {
$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(): void {
$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(): void {
$step = new question_attempt_step();
$this->expectException('moodle_exception');
$step->set_qt_var('x', 1);
}
public function test_cannot_set_behaviour_var_without_underscore(): void {
$step = new question_attempt_step();
$this->expectException('moodle_exception');
$step->set_behaviour_var('x', 1);
}
public function test_get_data(): void {
$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(): void {
$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(): void {
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(): void {
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());
}
/**
* Test get_user function.
*/
public function test_get_user(): void {
$this->resetAfterTest(true);
$student = $this->getDataGenerator()->create_user();
$step = new question_attempt_step(array(), 123, $student->id);
$step->add_full_user_object($student);
$this->assertEquals($student, $step->get_user());
}
/**
* Test get_user_fullname function.
*/
public function test_get_user_fullname(): void {
$this->resetAfterTest(true);
$student = $this->getDataGenerator()->create_user();
$step = new question_attempt_step(array(), 123, $student->id);
$step->add_full_user_object($student);
$this->assertEquals(fullname($student), $step->get_user_fullname());
}
/**
* Test add_full_user_object function.
*/
public function test_add_full_user_object(): void {
$this->resetAfterTest(true);
$student1 = $this->getDataGenerator()->create_user();
$student2 = $this->getDataGenerator()->create_user();
$step = new question_attempt_step(array(), 123, $student1->id);
// Add full user with the valid user.
$step->add_full_user_object($student1);
$this->assertEquals($student1, $step->get_user());
// Throw exception with the invalid user.
$this->expectException('coding_exception');
$this->expectExceptionMessage('Wrong user passed to add_full_user_object');
$step->add_full_user_object($student2);
}
}
@@ -0,0 +1,132 @@
<?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/>.
namespace core_question;
use question_attempt_step;
use testable_question_attempt;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the {@link question_attempt_step_iterator} class.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionattemptstepiterator_test extends \advanced_testcase {
private $qa;
private $iterator;
protected function setUp(): void {
$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(): void {
$this->qa = null;
$this->iterator = null;
}
public function test_foreach_loop(): void {
$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(): void {
$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(): void {
$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(): void {
$this->assertFalse(isset($this->iterator[-1]));
}
public function test_offsetExists_at_start(): void {
$this->assertTrue(isset($this->iterator[0]));
}
public function test_offsetExists_at_endt(): void {
$this->assertTrue(isset($this->iterator[2]));
}
public function test_offsetExists_past_end(): void {
$this->assertFalse(isset($this->iterator[3]));
}
public function test_offsetGet_before_start(): void {
$this->expectException(\moodle_exception::class);
$step = $this->iterator[-1];
}
public function test_offsetGet_at_start(): void {
$step = $this->iterator[0];
$this->assertEquals(0, $step->get_qt_var('i'));
}
public function test_offsetGet_at_end(): void {
$step = $this->iterator[2];
$this->assertEquals(2, $step->get_qt_var('i'));
}
public function test_offsetGet_past_end(): void {
$this->expectException(\moodle_exception::class);
$step = $this->iterator[3];
}
public function test_cannot_set(): void {
$this->expectException(\moodle_exception::class);
$this->iterator[0] = null;
}
public function test_cannot_unset(): void {
$this->expectException(\moodle_exception::class);
unset($this->iterator[2]);
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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/>.
namespace core_question;
use core_question\local\bank\question_version_status;
use qubaid_list;
use question_bank;
use question_engine;
use question_finder;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
/**
* Unit tests for the {@see question_bank} class.
*
* @package core_question
* @category test
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionbank_test extends \advanced_testcase {
public function test_sort_qtype_array(): void {
$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(): void {
$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(): void {
$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));
}
public function test_load_many_for_cache(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$q1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
$qs = question_finder::get_instance()->load_many_for_cache([$q1->id]);
$this->assertArrayHasKey($q1->id, $qs);
}
public function test_load_many_for_cache_missing_id(): void {
// Try to load a non-existent question.
$this->expectException(\dml_missing_record_exception::class);
question_finder::get_instance()->load_many_for_cache([-1]);
}
/**
* Test get_questions_from_categories.
*
* @covers \question_finder::get_questions_from_categories
*
* @return void
*/
public function test_get_questions_from_categories(): void {
$this->resetAfterTest();
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create three questions in a question bank category, each with three versions.
// The first question has all three versions in status ready.
$cat = $questiongenerator->create_question_category();
$q1v1 = $questiongenerator->create_question('truefalse', null, ['name' => 'Q1V1', 'category' => $cat->id]);
$q1v2 = $questiongenerator->update_question($q1v1, null, ['name' => 'Q1V2']);
$q1v3 = $questiongenerator->update_question($q1v2, null, ['name' => 'Q1V3']);
// The second question has the first version in status draft, the second version in status ready,
// and third version in status draft.
$q2v1 = $questiongenerator->create_question('numerical', null, ['name' => 'Q2V2', 'category' => $cat->id,
'status' => question_version_status::QUESTION_STATUS_DRAFT, ]);
$q2v2 = $questiongenerator->update_question($q2v1, null, ['name' => 'Q2V2',
'status' => question_version_status::QUESTION_STATUS_READY, ]);
$q2v3 = $questiongenerator->update_question($q2v2, null,
['name' => 'Q2V3', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
// The third question has all three version in status draft.
$q3v1 = $questiongenerator->create_question('shortanswer', null, ['name' => 'Q3V1', 'category' => $cat->id,
'status' => question_version_status::QUESTION_STATUS_DRAFT, ]);
$q3v2 = $questiongenerator->update_question($q3v1, null, ['name' => 'Q3V2',
'status' => question_version_status::QUESTION_STATUS_DRAFT, ]);
$q3v3 = $questiongenerator->update_question($q3v2, null, ['name' => 'Q3V3',
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
// Test the returned array of questions in that category is the desired one with version three of the first
// question, version two of the second question, and the third question omitted completely since there are
// only draft versions.
$this->assertEquals([$q1v3->id => $q1v3->id, $q2v2->id => $q2v2->id],
question_bank::get_finder()->get_questions_from_categories([$cat->id], ""));
}
}
@@ -0,0 +1,168 @@
<?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/>.
namespace core_question;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once($CFG->libdir . '/questionlib.php');
/**
* Unit tests for the {@link question_state} class and subclasses.
*
* @package core_question
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \question_state
*/
class questionstate_test extends \advanced_testcase {
public function test_is_active(): void {
$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(): void {
$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(): void {
$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(): void {
$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(): void {
$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(): void {
$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));
}
public function test_get(): void {
$this->assertEquals(question_state::$todo, question_state::get('todo'));
}
public function test_get_bad_data(): void {
question_state::get('');
$this->assertDebuggingCalled('Attempt to create a state from an empty string. ' .
'This is probably a sign of bad data in your database. See MDL-80127.');
}
}
@@ -0,0 +1,751 @@
<?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/>.
namespace core_question;
use question_bank;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the autosave parts of the {@link question_usage} class.
*
* @package core_question
* @category test
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionusage_autosave_test extends \qbehaviour_walkthrough_test_base {
public function test_autosave_then_display(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->delete_quba();
}
public function test_autosave_then_autosave_different_data(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process a second autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'third response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'third response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->delete_quba();
}
public function test_autosave_then_autosave_same_data(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$stepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
// Process a second autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Try to check it is really the same step
$newstepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
$this->assertEquals($stepid, $newstepid);
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->delete_quba();
}
public function test_autosave_then_autosave_original_data(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process a second autosave saving the original response.
// This should remove the autosave step.
$this->load_quba();
$this->process_autosave(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->delete_quba();
}
public function test_autosave_then_real_save(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Now save for real a third response.
$this->process_submission(array('answer' => 'third response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'third response');
$this->check_output_contains_hidden_input(':sequencecheck', 3);
}
public function test_autosave_then_real_save_same(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Now save for real of the same response.
$this->process_submission(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 3);
}
public function test_autosave_then_submit(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Now submit a third response.
$this->process_submission(array('answer' => 'third response'));
$this->quba->finish_all_questions();
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(0);
$this->check_step_count(4);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'third response', false);
$this->check_output_contains_hidden_input(':sequencecheck', 4);
}
public function test_autosave_and_save_concurrently(): void {
// This test simulates the following scenario:
// 1. Student looking at a page of the quiz, and edits a field then waits.
// 2. Autosave starts.
// 3. Student immediately clicks Next, which submits the current page.
// In this situation, the real submit should beat the autosave, even
// thought they happen concurrently. We simulate this by opening a
// second db connections.
global $DB;
// Open second connection
$cfg = $DB->export_dbconfig();
if (!isset($cfg->dboptions)) {
$cfg->dboptions = array();
}
$DB2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
$DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
// Since we need to commit our transactions in a given order, close the
// standard unit test transaction.
$this->preventResetByRollback();
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->save_quba();
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Start to process an autosave on $DB.
$transaction = $DB->start_delegated_transaction();
$this->load_quba($DB);
$this->process_autosave(array('answer' => 'autosaved response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba($DB); // Don't commit the transaction yet.
// Now process a real submit on $DB2 (using a different response).
$transaction2 = $DB2->start_delegated_transaction();
$this->load_quba($DB2);
$this->process_submission(array('answer' => 'real response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
// Now commit the first transaction.
$transaction->allow_commit();
// Now commit the other transaction.
$this->save_quba($DB2);
$transaction2->allow_commit();
// Now re-load and check how that is re-displayed.
$this->load_quba();
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->render();
$this->check_output_contains_text_input('answer', 'real response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$DB2->dispose();
}
public function test_concurrent_autosaves(): void {
// This test simulates the following scenario:
// 1. Student opens a page of the quiz in two separate browser.
// 2. Autosave starts in both at the same time.
// In this situation, one autosave will work, and the other one will
// get a unique key violation error. This is OK.
global $DB;
// Open second connection
$cfg = $DB->export_dbconfig();
if (!isset($cfg->dboptions)) {
$cfg->dboptions = array();
}
$DB2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
$DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
// Since we need to commit our transactions in a given order, close the
// standard unit test transaction.
$this->preventResetByRollback();
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->save_quba();
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Start to process an autosave on $DB.
$transaction = $DB->start_delegated_transaction();
$this->load_quba($DB);
$this->process_autosave(array('answer' => 'autosaved response 1'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba($DB); // Don't commit the transaction yet.
// Now process a real submit on $DB2 (using a different response).
$transaction2 = $DB2->start_delegated_transaction();
$this->load_quba($DB2);
$this->process_autosave(array('answer' => 'autosaved response 2'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
// Now commit the first transaction.
$transaction->allow_commit();
// Now commit the other transaction.
$this->expectException('dml_write_exception');
$this->save_quba($DB2);
$transaction2->allow_commit();
// Now re-load and check how that is re-displayed.
$this->load_quba();
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->render();
$this->check_output_contains_text_input('answer', 'autosaved response 1');
$this->check_output_contains_hidden_input(':sequencecheck', 1);
$DB2->dispose();
}
public function test_autosave_with_wrong_seq_number_ignored(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'first response'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave with a sequence number 1 too small (so from the past).
$this->load_quba();
$postdata = $this->response_data_to_post(array('answer' => 'obsolete response'));
$postdata[$this->quba->get_field_prefix($this->slot) . ':sequencecheck'] = $this->get_question_attempt()->get_sequence_check_count() - 1;
$this->quba->process_all_autosaves(null, $postdata);
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->delete_quba();
}
public function test_finish_with_unhandled_autosave_data(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
$this->process_submission(array('answer' => 'cat'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Now check how that is re-displayed.
$this->render();
$this->check_output_contains_text_input('answer', 'cat');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Process an autosave.
$this->load_quba();
$this->process_autosave(array('answer' => 'frog'));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now check how that is re-displayed.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'frog');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
// Now finishe the attempt, without having done anything since the autosave.
$this->finish();
$this->save_quba();
// Now check how that has been graded and is re-displayed.
$this->load_quba();
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(1);
$this->render();
$this->check_output_contains_text_input('answer', 'frog', false);
$this->check_output_contains_hidden_input(':sequencecheck', 4);
$this->delete_quba();
}
/**
* Test that regrading doesn't convert autosave steps to finished steps.
* This can result in students loosing data (due to question_out_of_sequence_exception) if a teacher
* regrades an attempt while it is in progress.
*/
public function test_autosave_and_regrade_then_display(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
// Start attempt at a shortanswer question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// First see if the starting sequence is right.
$this->render();
$this->check_output_contains_hidden_input(':sequencecheck', 1);
// Add a submission.
$this->process_submission(array('answer' => 'first response'));
$this->save_quba();
// Check the submission and that the sequence went up.
$this->render();
$this->check_output_contains_text_input('answer', 'first response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->assertFalse($this->get_question_attempt()->has_autosaved_step());
// Add a autosave response.
$this->load_quba();
$this->process_autosave(array('answer' => 'second response'));
$this->save_quba();
// Confirm that the autosave value shows up, but that the sequence hasn't increased.
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->assertTrue($this->get_question_attempt()->has_autosaved_step());
// Call regrade.
$this->load_quba();
$this->quba->regrade_all_questions();
$this->save_quba();
// Check and see if the autosave response is still there, that the sequence didn't increase,
// and that there is an autosave step.
$this->load_quba();
$this->render();
$this->check_output_contains_text_input('answer', 'second response');
$this->check_output_contains_hidden_input(':sequencecheck', 2);
$this->assertTrue($this->get_question_attempt()->has_autosaved_step());
$this->delete_quba();
}
protected function tearDown(): void {
// This test relies on the destructor for the second DB connection being called before running the next test.
// Without this change - there will be unit test failures on "some" DBs (MySQL).
gc_collect_cycles();
}
}
@@ -0,0 +1,151 @@
<?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/>.
namespace core_question;
use question_bank;
use question_state;
use question_test_recordset;
use question_usage_by_activity;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for loading data into the {@link question_usage_by_activity} class.
*
* @package core_question
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionusagebyactivity_data_test extends \data_loading_method_test_base {
public function test_load(): void {
$scid = \context_system::instance()->id;
$records = new question_test_recordset(array(
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
'questionattemptid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo', null, 1256233705, 1, 'answer', '1'),
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.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(false)->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());
}
public function test_load_data_no_steps(): void {
// The code had a bug where if one question_attempt had no steps,
// load_from_records got stuck in an infinite loop. This test is to
// verify that no longer happens.
$scid = \context_system::instance()->id;
$records = new question_test_recordset(array(
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
'questionattemptid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
array(1, $scid, 'unit_test', 'interactive', 2, 1, 2, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
array(1, $scid, 'unit_test', 'interactive', 3, 1, 3, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
));
question_bank::start_unit_test();
$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());
$this->assertEquals(array(1, 2, 3), $quba->get_slots());
$qa = $quba->get_question_attempt(1);
$this->assertEquals(0, $qa->get_num_steps());
}
public function test_load_data_no_qas(): void {
// The code had a bug where if a question_usage had no question_attempts,
// load_from_records got stuck in an infinite loop. This test is to
// verify that no longer happens.
$scid = \context_system::instance()->id;
$records = new question_test_recordset(array(
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
'questionattemptid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, $scid, 'unit_test', 'interactive', null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null),
));
question_bank::start_unit_test();
$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());
$this->assertEquals(array(), $quba->get_slots());
}
}
@@ -0,0 +1,190 @@
<?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/>.
namespace core_question;
use question_bank;
use question_engine;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the question_usage_by_activity class.
*
* @package core_question
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionusagebyactivity_test extends \advanced_testcase {
public function test_set_get_preferred_model(): void {
// Set up
$quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
// Exercise SUT and verify.
$quba->set_preferred_behaviour('deferredfeedback');
$this->assertEquals('deferredfeedback', $quba->get_preferred_behaviour());
}
public function test_set_get_id(): void {
// Set up
$quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
// Exercise SUT and verify
$quba->set_id_from_database(123);
$this->assertEquals(123, $quba->get_id());
}
public function test_fake_id(): void {
// Set up
$quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
// Exercise SUT and verify
$this->assertNotEmpty($quba->get_id());
}
public function test_create_usage_and_add_question(): void {
// Exercise SUT
$context = \context_system::instance();
$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(): void {
// Set up.
$quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
$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, false));
$this->expectException('moodle_exception');
$quba->get_question($slot + 1, false);
}
public function test_extract_responses(): void {
// 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',
\context_system::instance());
$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(): void {
// 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',
\context_system::instance());
$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->expectException('question_out_of_sequence_exception');
$quba->process_all_actions($slot, $postdata);
}
/**
* Test function preload all step users.
*/
public function test_preload_all_step_users(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Set up.
$quba = question_engine::make_questions_usage_by_activity('unit_test',
\context_system::instance());
// Create an essay question in the DB.
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$essay = $generator->create_question('essay', 'editorfilepicker', ['category' => $cat->id]);
// Start attempt at the question.
$q = question_bank::load_question($essay->id);
$quba->set_preferred_behaviour('deferredfeedback');
$slot = $quba->add_question($q, 10);
$quba->start_question($slot, 1);
// Finish the attempt.
$quba->finish_all_questions();
question_engine::save_questions_usage_by_activity($quba);
// The user information of question attempt step should be loaded.
$quba->preload_all_step_users();
$qa = $quba->get_attempt_iterator()->current();
$steps = $qa->get_full_step_iterator();
$this->assertEquals('Admin User', $steps[0]->get_user_fullname());
}
}
@@ -0,0 +1,229 @@
<?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/>.
namespace core_question;
use question_utils;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
/**
* Unit tests for the {@link question_utils} class.
*
* @package core_question
* @category test
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class questionutils_test extends \advanced_testcase {
public function test_arrays_have_same_keys_and_values(): void {
$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(): void {
$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(): void {
$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(): void {
$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(): void {
$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_letter(): void {
$this->assertEquals('A', question_utils::int_to_letter(1));
$this->assertEquals('B', question_utils::int_to_letter(2));
$this->assertEquals('C', question_utils::int_to_letter(3));
$this->assertEquals('D', question_utils::int_to_letter(4));
$this->assertEquals('E', question_utils::int_to_letter(5));
$this->assertEquals('F', question_utils::int_to_letter(6));
$this->assertEquals('G', question_utils::int_to_letter(7));
$this->assertEquals('H', question_utils::int_to_letter(8));
$this->assertEquals('I', question_utils::int_to_letter(9));
$this->assertEquals('J', question_utils::int_to_letter(10));
$this->assertEquals('K', question_utils::int_to_letter(11));
$this->assertEquals('L', question_utils::int_to_letter(12));
$this->assertEquals('M', question_utils::int_to_letter(13));
$this->assertEquals('N', question_utils::int_to_letter(14));
$this->assertEquals('O', question_utils::int_to_letter(15));
$this->assertEquals('P', question_utils::int_to_letter(16));
$this->assertEquals('Q', question_utils::int_to_letter(17));
$this->assertEquals('R', question_utils::int_to_letter(18));
$this->assertEquals('S', question_utils::int_to_letter(19));
$this->assertEquals('T', question_utils::int_to_letter(20));
$this->assertEquals('U', question_utils::int_to_letter(21));
$this->assertEquals('V', question_utils::int_to_letter(22));
$this->assertEquals('W', question_utils::int_to_letter(23));
$this->assertEquals('X', question_utils::int_to_letter(24));
$this->assertEquals('Y', question_utils::int_to_letter(25));
$this->assertEquals('Z', question_utils::int_to_letter(26));
}
public function test_int_to_roman_too_small(): void {
$this->expectException(\moodle_exception::class);
question_utils::int_to_roman(0);
}
public function test_int_to_roman_too_big(): void {
$this->expectException(\moodle_exception::class);
question_utils::int_to_roman(4000);
}
public function test_int_to_roman_not_int(): void {
$this->expectException(\moodle_exception::class);
question_utils::int_to_roman(1.5);
}
public function test_clean_param_mark(): void {
$this->assertNull(question_utils::clean_param_mark(null));
$this->assertNull(question_utils::clean_param_mark('frog'));
$this->assertSame('', question_utils::clean_param_mark(''));
$this->assertSame(0.0, question_utils::clean_param_mark('0'));
$this->assertSame(1.5, question_utils::clean_param_mark('1.5'));
$this->assertSame(1.5, question_utils::clean_param_mark('1,5'));
$this->assertSame(-1.5, question_utils::clean_param_mark('-1.5'));
$this->assertSame(-1.5, question_utils::clean_param_mark('-1,5'));
}
}
+531
View File
@@ -0,0 +1,531 @@
<?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/>.
namespace core_question;
use question_bank;
use question_hint;
use question_test_recordset;
use question_usage_by_activity;
use testable_question_engine_unit_of_work;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* Unit tests for the {@link question_engine_unit_of_work} class.
*
* @package core_question
* @category test
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class unitofwork_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(): void {
// 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 tearDown(): void {
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', 'maxfraction', '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, 1.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, 1.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, 1.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, 1.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, 1.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, 1.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 0.6666667, 1256233790, 1, 'answer', 'frog'),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 0.6666667, 1256233790, 1, '-submit', 1),
);
}
public function test_initial_state(): void {
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_update_usage(): void {
$this->quba->set_preferred_behaviour('deferredfeedback');
$this->assertTrue($this->observer->get_modified());
}
public function test_add_question(): void {
$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));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_add_and_start_question(): void {
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_process_action(): void {
$this->quba->manual_grade($this->slot, 'Actually, that is not quite right', 0.5, FORMAT_HTML);
// 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);
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_regrade_same_steps(): void {
// Change the question in a minor way and regrade.
$this->quba->get_question($this->slot, false)->answers[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]);
}
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_regrade_losing_steps(): void {
// 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, false)->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', '-submit' => 1),
$seconddeletedstep->get_all_data());
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_tricky_regrade(): void {
// 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, false)->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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_question(): void {
$q = \test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$addedattempts = $this->observer->get_attempts_added();
$this->assertEquals(1, count($addedattempts));
$addedattempt = reset($addedattempts);
$this->assertSame($this->quba->get_question_attempt($this->slot), $addedattempt);
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($this->quba->get_question_attempt($newslot), $updatedattempt);
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_question_then_modify(): void {
$q = \test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$this->quba->process_action($this->slot, array('answer' => 'frog', '-submit' => 1));
$this->quba->manual_grade($newslot, 'Test', 0.5, FORMAT_HTML);
$addedattempts = $this->observer->get_attempts_added();
$this->assertEquals(1, count($addedattempts));
$addedattempt = reset($addedattempts);
$this->assertSame($this->quba->get_question_attempt($this->slot), $addedattempt);
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($this->quba->get_question_attempt($newslot), $updatedattempt);
$newsteps = $this->observer->get_steps_added();
$this->assertEquals(1, count($newsteps));
list($newstep, $qaid, $seq) = reset($newsteps);
$this->assertSame($this->quba->get_question_attempt($newslot)->get_last_step(), $newstep);
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_question_then_move_again(): void {
$originalqa = $this->quba->get_question_attempt($this->slot);
$q1 = \test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q1);
$this->quba->start_question($this->slot);
$q2 = \test_question_maker::make_question('truefalse');
$newslot2 = $this->quba->add_question_in_place_of_other($newslot, $q2);
$this->quba->start_question($newslot);
$addedattempts = $this->observer->get_attempts_added();
$this->assertEquals(2, count($addedattempts));
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($originalqa, $updatedattempt);
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_max_mark(): void {
$this->quba->set_max_mark($this->slot, 6.0);
$this->assertEqualsWithDelta(4.0, $this->quba->get_total_mark(), 0.0000005);
$this->assertEquals(0, count($this->observer->get_attempts_added()));
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($this->quba->get_question_attempt($this->slot), $updatedattempt);
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_question_attempt_metadata(): void {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$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()));
$this->assertEquals(array($this->slot => array('metathingy' => $this->quba->get_question_attempt($this->slot))),
$this->observer->get_metadata_added());
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_question_attempt_metadata_then_change(): void {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'different value');
$this->assertEquals('different value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$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()));
$this->assertEquals(array($this->slot => array('metathingy' => $this->quba->get_question_attempt($this->slot))),
$this->observer->get_metadata_added());
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_metadata_previously_set_but_dont_actually_change(): void {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->observer = new testable_question_engine_unit_of_work($this->quba);
$this->quba->set_observer($this->observer);
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_metadata_previously_set(): void {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->observer = new testable_question_engine_unit_of_work($this->quba);
$this->quba->set_observer($this->observer);
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'different value');
$this->assertEquals('different value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(array($this->slot => array('metathingy' => $this->quba->get_question_attempt($this->slot))),
$this->observer->get_metadata_modified());
}
public function test_set_metadata_in_new_question(): void {
$newslot = $this->quba->add_question(\test_question_maker::make_question('truefalse'));
$this->quba->start_question($newslot);
$this->quba->set_question_attempt_metadata($newslot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($newslot, 'metathingy'));
$this->assertEquals(array($newslot => $this->quba->get_question_attempt($newslot)),
$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()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_metadata_then_move(): void {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$q = \test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($newslot, 'metathingy'));
$this->assertEquals(array($this->slot => $this->quba->get_question_attempt($this->slot)),
$this->observer->get_attempts_added());
$this->assertEquals(array($newslot => $this->quba->get_question_attempt($newslot)),
$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()));
$this->assertEquals(array($newslot => array('metathingy' => $this->quba->get_question_attempt($newslot))),
$this->observer->get_metadata_added());
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_then_set_metadata(): void {
$q = \test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$this->quba->set_question_attempt_metadata($newslot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($newslot, 'metathingy'));
$this->assertEquals(array($this->slot => $this->quba->get_question_attempt($this->slot)),
$this->observer->get_attempts_added());
$this->assertEquals(array($newslot => $this->quba->get_question_attempt($newslot)),
$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()));
$this->assertEquals(array($newslot => array('metathingy' => $this->quba->get_question_attempt($newslot))),
$this->observer->get_metadata_added());
}
/**
* Test add_question_in_place_of_other function.
*
* @covers ::add_question_in_place_of_other
*/
public function test_replace_old_attempt(): void {
// Create a new question.
$q = \test_question_maker::make_question('truefalse');
$currentquestion = $this->quba->get_question_attempt($this->slot)->get_question();
// Replace the current question in the slot with a new one.
$slot = $this->quba->add_question_in_place_of_other($this->slot, $q, null, false);
$newquestion = $this->quba->get_question_attempt($slot)->get_question();
$this->assertEquals($this->slot, $slot);
$this->assertEquals($q->name, $newquestion->name);
$this->assertCount(4, $this->observer->get_steps_deleted());
$this->assertCount(1, $this->observer->get_attempts_modified());
$this->assertCount(0, $this->observer->get_attempts_added());
$this->assertNotEquals($currentquestion->id, $newquestion->id);
}
}
+199
View File
@@ -0,0 +1,199 @@
<?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/>.
namespace core_question;
use question_bank;
use question_display_options;
use question_state;
use test_question_maker;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/helpers.php');
/**
* End-to-end tests of attempting a question.
*
* @package core_question
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class walkthrough_test extends \qbehaviour_walkthrough_test_base {
public function test_regrade_does_not_lose_flag(): void {
// 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);
// Process a true answer.
$this->process_submission(array('answer' => 1));
// Finish the attempt.
$this->quba->finish_all_questions();
// Flag the question.
$this->get_question_attempt()->set_flagged(true);
// Now change the correct answer to the question, and regrade.
$tf->rightanswer = false;
$this->quba->regrade_all_questions();
// Verify the flag has not been lost.
$this->assertTrue($this->get_question_attempt()->is_flagged());
}
/**
* Test action_author function.
*/
public function test_action_author_with_display_options_testcase(): void {
$this->resetAfterTest(true);
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$teacher = $this->getDataGenerator()->create_user();
$student = $this->getDataGenerator()->create_user();
// Create an essay question in the DB.
$cat = $generator->create_question_category();
$essay = $generator->create_question('essay', 'editorfilepicker', ['category' => $cat->id]);
// Start attempt at the question.
$q = question_bank::load_question($essay->id);
// Student attempt the question.
$this->setUser($student);
$this->start_attempt_at_question($q, 'deferredfeedback', 10, 1);
// Simulate some data submitted by the student.
$this->process_submission(['answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML]);
$this->finish();
// Process a manual comment.
$this->setUser($teacher);
$this->manual_grade('Not good enough!', 10, FORMAT_HTML);
$this->render();
$this->save_quba();
// Set display option userinfoinhistory to HIDDEN.
$displayoptions = new question_display_options();
$displayoptions->history = question_display_options::VISIBLE;
$displayoptions->userinfoinhistory = question_display_options::HIDDEN;
$this->load_quba();
$result = $this->quba->render_question($this->slot, $displayoptions);
// The profile user link should not display.
preg_match("/<a ?.*>(.*)<\/a>/", $result, $matches);
$this->assertEquals(false, isset($matches[0]));
// Set display option userinfoinhistory to SHOW_ALL.
$displayoptions = new question_display_options();
$displayoptions->history = question_display_options::VISIBLE;
$displayoptions->userinfoinhistory = question_display_options::SHOW_ALL;
$this->load_quba();
$this->quba->preload_all_step_users();
$result = $this->quba->render_question($this->slot, $displayoptions);
$numsteps = $this->quba->get_question_attempt($this->slot)->get_num_steps();
// All steps in the result should contain user profile link.
preg_match_all("/<a ?.*>(.*)<\/a>/", $result, $matches);
$this->assertEquals($numsteps, count($matches[0]));
// Set the userinfoinhistory to student id.
$displayoptions = new question_display_options();
$displayoptions->history = question_display_options::VISIBLE;
$displayoptions->userinfoinhistory = $student->id;
$this->load_quba();
$result = $this->quba->render_question($this->slot, $displayoptions);
$message = 'Attempt to access the step user before it was initialised.';
$message .= ' Did you forget to call question_usage_by_activity::preload_all_step_users() or similar?';
$this->assertDebuggingCalled($message, DEBUG_DEVELOPER);
$this->resetDebugging();
$this->quba->preload_all_step_users();
$result = $this->quba->render_question($this->slot, $displayoptions);
$this->assertDebuggingNotCalled();
// The step just show the user profile link if the step's userid is different with student id.
preg_match_all("/<a ?.*>(.*)<\/a>/", $result, $matches);
$this->assertEquals(1, count($matches[0]));
}
/**
* @covers \question_usage_by_activity::regrade_question
* @covers \question_attempt::regrade
* @covers \question_attempt::get_attempt_state_data_to_regrade_with_version
*/
public function test_regrading_an_interactive_attempt_while_in_progress(): void {
// Start an attempt at a matching question.
$q = test_question_maker::make_question('match');
$this->start_attempt_at_question($q, 'interactive', 1);
$this->save_quba();
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
$this->check_current_output($this->get_tries_remaining_expectation(1));
// Regrade the attempt.
// Duplicating the question here essential to triggering the bug we are trying to reproduce.
$reloadedquestion = clone($q);
$this->quba->regrade_question($this->slot, false, null, $reloadedquestion);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
$this->check_current_output($this->get_tries_remaining_expectation(1));
}
/**
* @covers \question_usage_by_activity::regrade_question
* @covers \question_attempt::regrade
* @covers \question_attempt::get_attempt_state_data_to_regrade_with_version
*/
public function test_regrading_does_not_lose_metadata(): void {
// Start an attempt at a matching question.
$q = test_question_maker::make_question('match');
$this->start_attempt_at_question($q, 'interactive', 1);
// Like in process_redo_question in mod_quiz.
$this->quba->set_question_attempt_metadata($this->slot, 'originalslot', 42);
$this->save_quba();
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
$this->check_current_output($this->get_tries_remaining_expectation(1));
// Regrade the attempt.
$reloadedquestion = clone($q);
$this->quba->regrade_question($this->slot, false, null, $reloadedquestion);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
$this->check_current_output($this->get_tries_remaining_expectation(1));
$this->assertEquals(42, $this->quba->get_question_attempt_metadata($this->slot, 'originalslot'));
}
}