Autoría | Ultima modificación | Ver Log |
<?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 mod_quiz\backup;use advanced_testcase;use backup_controller;use restore_controller;use quiz_question_helper_test_trait;use backup;defined('MOODLE_INTERNAL') || die();global $CFG;require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');require_once($CFG->dirroot . '/question/engine/lib.php');require_once($CFG->dirroot . '/mod/quiz/locallib.php');require_once($CFG->dirroot . '/course/lib.php');require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');/*** Test repeatedly restoring a quiz into another course.** @package mod_quiz* @category test* @copyright Julien Rädler* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @covers \restore_questions_parser_processor* @covers \restore_create_categories_and_questions*/final class repeated_restore_test extends advanced_testcase {use quiz_question_helper_test_trait;/*** Restore a quiz twice into the same target course, and verify the quiz uses the restored questions both times.*/public function test_restore_quiz_into_other_course_twice(): void {global $USER;$this->resetAfterTest();$this->setAdminUser();// Step 1: Create two courses and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$course2 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$generator->enrol_user($teacher->id, $course2->id, 'editingteacher');// Create a quiz with questions in the first course.$quiz = $this->create_test_quiz($course1);$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create a short answer question.$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);// Update the question to simulate editing.$questiongenerator->update_question($saq);// Add question to quiz.quiz_add_quiz_question($saq->id, $quiz);// Create a numerical question.$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);// Update the question to simulate multiple versions.$questiongenerator->update_question($numq);$questiongenerator->update_question($numq);// Add question to quiz.quiz_add_quiz_question($numq->id, $quiz);// Create a true false question.$tfq = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);// Update the question to simulate multiple versions.$questiongenerator->update_question($tfq);$questiongenerator->update_question($tfq);// Add question to quiz.quiz_add_quiz_question($tfq->id, $quiz);// Capture original question IDs for verification after import.$modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz');$module1 = reset($modules1);$questionscourse1 = \mod_quiz\question\bank\qbank_helper::get_question_structure($module1->instance, $module1->context);$originalquestionids = [];foreach ($questionscourse1 as $slot) {array_push($originalquestionids, intval($slot->questionid));}// Step 2: Backup the first course.$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Step 3: Import the backup into the second course.$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify the question ids from the quiz in the original course are different// from the question ids in the duplicated quiz in the second course.$modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');$module2 = reset($modules2);$questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure($module2->instance, $module2->context);foreach ($questionscourse2firstimport as $slot) {$this->assertNotContains(intval($slot->questionid), $originalquestionids,"Question ID $slot->questionid should not be in the original course's question IDs.");}// Repeat the backup and import process to simulate a second import.$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify that the second restore has used the same new questions that were created by the first restore.$modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');$module3 = end($modules3);$questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure($module3->instance, $module3->context);foreach ($questionscourse2secondimport as $slot) {$this->assertEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);}}/*** Restore a copy of a quiz to the same course, using questions that include line breaks in the text.*/public function test_restore_question_with_linebreaks(): void {global $USER;$this->resetAfterTest();$this->setAdminUser();// Step 1: Create two courses and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$course2 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$generator->enrol_user($teacher->id, $course2->id, 'editingteacher');// Create a quiz with questions in the first course.$quiz = $this->create_test_quiz($course1);$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create questions and add to the quiz.$q1 = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id,'questiontext' => ['text' => "<p>Question</p>\r\n<p>One</p>", 'format' => FORMAT_MOODLE]]);$q2 = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id,'questiontext' => ['text' => "<p>Question</p>\n<p>Two</p>", 'format' => FORMAT_MOODLE]]);// Add question to quiz.quiz_add_quiz_question($q1->id, $quiz);quiz_add_quiz_question($q2->id, $quiz);// Capture original question IDs for verification after import.$modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz');$module1 = reset($modules1);$originalslots = \mod_quiz\question\bank\qbank_helper::get_question_structure($module1->instance, $module1->context);$originalquestionids = [];foreach ($originalslots as $slot) {array_push($originalquestionids, intval($slot->questionid));}$this->assertCount(2, get_questions_category($cat, false));// Step 2: Backup the quiz$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Step 3: Import the backup into the same course.$rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify the question ids from the new quiz match the first.$modules2 = get_fast_modinfo($course1->id)->get_instances_of('quiz');$this->assertCount(2, $modules2);$module2 = end($modules2);$copyslots = \mod_quiz\question\bank\qbank_helper::get_question_structure($module2->instance, $module2->context);foreach ($copyslots as $slot) {$this->assertContains(intval($slot->questionid), $originalquestionids);}// The category should still only contain 2 question, neither question should be duplicated.$this->assertCount(2, get_questions_category($cat, false));}/*** Return a list of qtypes with valid generators in their helper class.** This will check all installed qtypes for a test helper class, then find a defined test question which has a corresponding* form_data method and return it. If the helper doesn't have a form_data method for any test question, it will return a* null test question name for that qtype.** @return array*/public static function get_qtype_generators(): array {global $CFG;$generators = [];foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) {if ($qtype->name == 'random') {continue;}$helperpath = "{$CFG->dirroot}/question/type/{$qtype->name}/tests/helper.php";if (!file_exists($helperpath)) {continue;}require_once($helperpath);$helperclass = "qtype_{$qtype->name}_test_helper";if (!class_exists($helperclass)) {continue;}$helper = new $helperclass();$testquestion = null;foreach ($helper->get_test_questions() as $question) {if (method_exists($helper, "get_{$qtype->name}_question_form_data_{$question}")) {$testquestion = $question;break;}}$generators[$qtype->name] = ['qtype' => $qtype->name,'testquestion' => $testquestion,];}return $generators;}/*** Restore a quiz with questions of same stamp into the same course, but different answers.** @dataProvider get_qtype_generators* @param string $qtype The name of the qtype plugin to test* @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped* with a message.*/public function test_restore_quiz_with_same_stamp_questions(string $qtype, ?string $testquestion): void {global $DB, $USER;if (is_null($testquestion)) {$this->markTestSkipped("Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " ."test helper class.");}$this->resetAfterTest();$this->setAdminUser();// Create a course and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create 2 quizzes with 2 questions multichoice.$quiz1 = $this->create_test_quiz($course1);$question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question1->id, $quiz1, 0);$question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question2->id, $quiz1, 0);// Update question2 to have the same stamp as question1.$DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);// Change the answers of the question2 to be different to question1.$question2data = \question_bank::load_question_data($question2->id);if (!isset($question2data->options->answers) || empty($question2data->options->answers)) {$this->markTestSkipped("Cannot test edited answers for qtype_{$qtype} as it does not use answers.",);}foreach ($question2data->options->answers as $answer) {$DB->set_field('question_answers', 'answer', 'edited', ['id' => $answer->id]);}// Backup quiz1.$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Restore the backup into the same course.$rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify that the newly-restored quiz uses the same question as quiz2.$modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');$this->assertCount(2, $modules);$quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz1->id,\context_module::instance($quiz1->cmid),);$quiz2 = end($modules);$quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);$this->assertEquals($quiz2structure[1]->questionid, $quiz2structure[1]->questionid);$this->assertEquals($quiz2structure[2]->questionid, $quiz2structure[2]->questionid);}/*** Restore a quiz with duplicate questions (same stamp and questions) into the same course.** This is a contrived case, but this test serves as a control for the other tests in this class, proving that the hashing* process will match an identical question.** @dataProvider get_qtype_generators* @param string $qtype The name of the qtype plugin to test* @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped* with a message.*/public function test_restore_quiz_with_duplicate_questions(string $qtype, ?string $testquestion): void {global $DB, $USER;if (is_null($testquestion)) {$this->markTestSkipped("Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " ."test helper class.");}$this->resetAfterTest();$this->setAdminUser();// Create a course and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create a quiz with 2 identical but separate questions.$quiz1 = $this->create_test_quiz($course1);$question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question1->id, $quiz1, 0);$question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question2->id, $quiz1, 0);// Update question2 to have the same times and stamp as question1.$DB->update_record('question', ['id' => $question2->id,'stamp' => $question1->stamp,'timecreated' => $question1->timecreated,'timemodified' => $question1->timemodified,]);// Backup quiz.$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Restore the backup into the same course.$rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Expect that the restored quiz will have the second question in both its slots// by virtue of identical stamp, version, and hash of question answer texts.$modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');$this->assertCount(2, $modules);$quiz2 = end($modules);$quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);$this->assertEquals($quiz2structure[1]->questionid, $quiz2structure[2]->questionid);}/*** Restore a quiz with questions that have the same stamp but different text.** @dataProvider get_qtype_generators* @param string $qtype The name of the qtype plugin to test* @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped* with a message.*/public function test_restore_quiz_with_edited_questions(string $qtype, ?string $testquestion): void {global $DB, $USER;if (is_null($testquestion)) {$this->markTestSkipped("Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " ."test helper class.");}$this->resetAfterTest();$this->setAdminUser();// Create a course and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create a quiz with 2 identical but separate questions.$quiz1 = $this->create_test_quiz($course1);$question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question1->id, $quiz1);$question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);// Edit question 2 to have the same stamp and times as question1, but different text.$DB->update_record('question', ['id' => $question2->id,'questiontext' => 'edited','stamp' => $question1->stamp,'timecreated' => $question1->timecreated,'timemodified' => $question1->timemodified,]);quiz_add_quiz_question($question2->id, $quiz1);// Backup quiz.$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Restore the backup into the same course.$rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// The quiz should contain both questions, as they have different text.$modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');$this->assertCount(2, $modules);$quiz2 = end($modules);$quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);$this->assertEquals($quiz2structure[1]->questionid, $question1->id);$this->assertEquals($quiz2structure[2]->questionid, $question2->id);}/*** Restore a course to another course having questions with the same stamp in a shared question bank context category.** @dataProvider get_qtype_generators* @param string $qtype The name of the qtype plugin to test* @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped* with a message.*/public function test_restore_course_with_same_stamp_questions(string $qtype, ?string $testquestion): void {global $DB, $USER;if (is_null($testquestion)) {$this->markTestSkipped("Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " ."test helper class.");}$this->resetAfterTest();$this->setAdminUser();// Create two courses and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$course2 = $generator->create_course();$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course2->id]);$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$generator->enrol_user($teacher->id, $course2->id, 'editingteacher');$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create quiz with question.$quiz1 = $this->create_test_quiz($course1);$question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question1->id, $quiz1, 0);$quiz2 = $this->create_test_quiz($course1);$question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question2->id, $quiz2, 0);// Update question2 to have the same stamp as question1.$DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);// Change the answers of the question2 to be different to question1.$question2data = \question_bank::load_question_data($question2->id);if (!isset($question2data->options->answers) || empty($question2data->options->answers)) {$this->markTestSkipped("Cannot test edited answers for qtype_{$qtype} as it does not use answers.",);}if ($DB->count_records('question_answers') === 0) {$this->markTestSkipped("Cannot test edited answers for qtype_{$qtype} as it does not use the question_answers table.",);}foreach ($question2data->options->answers as $answer) {$answer->answer = 'New answer ' . $answer->id;$DB->update_record('question_answers', $answer);}$course1q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz1->id, \context_module::instance($quiz1->cmid));$this->assertEquals($question1->id, $course1q1structure[1]->questionid);$course1q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->id, \context_module::instance($quiz2->cmid));$this->assertEquals($question2->id, $course1q2structure[1]->questionid);// Backup course1.$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Restore the backup, adding to course2.$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify that the newly-restored course's quizzes use the same questions as their counterparts of course1.$modules = get_fast_modinfo($course2->id)->get_instances_of('quiz');$course1q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz1->id, \context_module::instance($quiz1->cmid));$course2quiz1 = array_shift($modules);$course2q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($course2quiz1->instance, $course2quiz1->context);$this->assertEquals($question1->id, $course1q1structure[1]->questionid);$this->assertEquals($question1->id, $course2q1structure[1]->questionid);$course1q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->id, \context_module::instance($quiz2->cmid));$course2quiz2 = array_shift($modules);$course2q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($course2quiz2->instance, $course2quiz2->context);$this->assertEquals($question2->id, $course1q2structure[1]->questionid);$this->assertEquals($question2->id, $course2q2structure[1]->questionid);}/*** Restore a quiz with questions of same stamp into the same course, but different hints.** @dataProvider get_qtype_generators* @param string $qtype The name of the qtype plugin to test* @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped* with a message.*/public function test_restore_quiz_with_same_stamp_questions_edited_hints(string $qtype, ?string $testquestion): void {global $DB, $USER;if (is_null($testquestion)) {$this->markTestSkipped("Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " ."test helper class.");}$this->resetAfterTest();$this->setAdminUser();// Create a course and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// Create 2 questions multichoice.$quiz1 = $this->create_test_quiz($course1);$question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question1->id, $quiz1, 0);$question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);quiz_add_quiz_question($question2->id, $quiz1, 0);// Update question2 to have the same stamp as question1.$DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);// Change the hints of the question2 to be different to question1.$hints = $DB->get_records('question_hints', ['questionid' => $question2->id]);if (empty($hints)) {$this->markTestSkipped("Cannot test edited hints for qtype_{$qtype} as test question {$testquestion} does not use hints.",);}foreach ($hints as $hint) {$DB->set_field('question_hints', 'hint', "{$hint->hint} edited", ['id' => $hint->id]);}// Backup quiz1.$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Restore the backup into the same course.$rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify that the newly-restored quiz uses the same question as quiz2.$modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');$this->assertCount(2, $modules);$quiz1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz1->id,\context_module::instance($quiz1->cmid),);$quiz2 = end($modules);$quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);$this->assertEquals($quiz1structure[1]->questionid, $quiz2structure[1]->questionid);$this->assertEquals($quiz1structure[2]->questionid, $quiz2structure[2]->questionid);}/*** Return a set of options fields and new values.** @return array*/public static function get_edited_option_fields(): array {return ['single' => ['single','0',],'shuffleanswers' => ['shuffleanswers','0',],'answernumbering' => ['answernumbering','ABCD',],'shownumcorrect' => ['shownumcorrect','0',],'showstandardinstruction' => ['showstandardinstruction','1',],'correctfeedback' => ['correctfeedback','edited',],'partiallycorrectfeedback' => ['partiallycorrectfeedback','edited',],'incorrectfeedback' => ['incorrectfeedback','edited',],];}/*** Restore a quiz with questions of same stamp into the same course, but different qtype-specific options.** @dataProvider get_edited_option_fields* @param string $field The answer field to edit* @param string $value The value to set*/public function test_restore_quiz_with_same_stamp_questions_edited_options(string $field, string $value): void {global $DB, $USER;$this->resetAfterTest();$this->setAdminUser();// Create a course and a user with editing teacher capabilities.$generator = $this->getDataGenerator();$course1 = $generator->create_course();$teacher = $USER;$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);$context = \context_module::instance($qbank->cmid);$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');// Create a question category.$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);// A quiz with 2 multichoice questions.$quiz1 = $this->create_test_quiz($course1);$question1 = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);quiz_add_quiz_question($question1->id, $quiz1, 0);$question2 = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);quiz_add_quiz_question($question2->id, $quiz1, 0);// Update question2 to have the same stamp as question1.$DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);// Change the options of question2 to be different to question1.$DB->set_field('qtype_multichoice_options', $field, $value, ['questionid' => $question2->id]);// Backup quiz.$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);$backupid = $bc->get_backupid();$bc->execute_plan();$bc->destroy();// Restore the backup into the same course.$rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,$teacher->id, backup::TARGET_CURRENT_ADDING);$rc->execute_precheck();$rc->execute_plan();$rc->destroy();// Verify that the newly-restored quiz questions match their quiz1 counterparts.$modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');$this->assertCount(2, $modules);$quiz1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz1->id,\context_module::instance($quiz1->cmid),);$quiz2 = end($modules);$quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);$this->assertEquals($quiz1structure[1]->questionid, $quiz2structure[1]->questionid);$this->assertEquals($quiz1structure[2]->questionid, $quiz2structure[2]->questionid);}}