From aa2ef1fe9ccad7b250da98a6f4ffeac9f3d51267 Mon Sep 17 00:00:00 2001 From: Mathew May Date: Thu, 18 Jan 2024 14:41:30 +0800 Subject: [PATCH] MDL-80416 qtype_ordering: Tests for reuse functionality Part of: MDL-79863 --- question/type/ordering/backup/moodle1/lib.php | 1 + .../restore_qtype_ordering_plugin.class.php | 1 + .../ordering/classes/privacy/provider.php | 1 + question/type/ordering/questiontype.php | 4 +- question/type/ordering/tests/backup_test.php | 96 ++++++++++++ .../tests/fixtures/testexport.gift.txt | 10 ++ .../tests/fixtures/testexport.moodle.xml | 68 +++++++++ .../tests/fixtures/testimport.gift.txt | 12 ++ .../tests/fixtures/testimport.moodle.xml | 64 ++++++++ .../tests/fixtures/testimportempty.moodle.xml | 64 ++++++++ .../tests/fixtures/testimportlong.moodle.xml | 64 ++++++++ .../type/ordering/tests/questiontype_test.php | 142 +++++++++++++++++- 12 files changed, 519 insertions(+), 8 deletions(-) create mode 100644 question/type/ordering/tests/backup_test.php create mode 100644 question/type/ordering/tests/fixtures/testexport.gift.txt create mode 100644 question/type/ordering/tests/fixtures/testexport.moodle.xml create mode 100644 question/type/ordering/tests/fixtures/testimport.gift.txt create mode 100644 question/type/ordering/tests/fixtures/testimport.moodle.xml create mode 100644 question/type/ordering/tests/fixtures/testimportempty.moodle.xml create mode 100644 question/type/ordering/tests/fixtures/testimportlong.moodle.xml diff --git a/question/type/ordering/backup/moodle1/lib.php b/question/type/ordering/backup/moodle1/lib.php index 905e5f30dd9..5acfcda85ed 100644 --- a/question/type/ordering/backup/moodle1/lib.php +++ b/question/type/ordering/backup/moodle1/lib.php @@ -27,6 +27,7 @@ * * @copyright 2013 Gordon Bateson (gordon.bateson@gmail.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore For old Moodle sites in which upgrades are always risky. */ class moodle1_qtype_ordering_handler extends moodle1_qtype_handler { diff --git a/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php b/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php index 593cabda1ed..ae0fbd051ea 100644 --- a/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php +++ b/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php @@ -85,6 +85,7 @@ class restore_qtype_ordering_plugin extends restore_qtype_plugin { * * @param object $state * @return string|false + * @codeCoverageIgnore Restoring from 2.0 is risky business and hopefully not needed. */ public function recode_legacy_state_answer($state): string|false { $answer = $state->answer; diff --git a/question/type/ordering/classes/privacy/provider.php b/question/type/ordering/classes/privacy/provider.php index d2011c228c9..0f77ab9729c 100644 --- a/question/type/ordering/classes/privacy/provider.php +++ b/question/type/ordering/classes/privacy/provider.php @@ -31,6 +31,7 @@ class provider implements \core_privacy\local\metadata\null_provider { * file to explain why this plugin stores no data. * * @return string + * @codeCoverageIgnore A null provider so no special handling for us. */ public static function get_reason(): string { return 'privacy:metadata'; diff --git a/question/type/ordering/questiontype.php b/question/type/ordering/questiontype.php index 1fbb04c5908..35b79da86e9 100644 --- a/question/type/ordering/questiontype.php +++ b/question/type/ordering/questiontype.php @@ -398,12 +398,12 @@ class qtype_ordering extends question_type { * Import question from GIFT format * * @param array $lines - * @param stdClass $question + * @param stdClass|null $question * @param qformat_gift $format * @param string|null $extra (optional, default=null) * @return stdClass|bool Question instance */ - public function import_from_gift(array $lines, stdClass $question, qformat_gift $format, string $extra = null): bool|stdClass { + public function import_from_gift(array $lines, ?stdClass $question, qformat_gift $format, string $extra = null): bool|stdClass { global $CFG; require_once($CFG->dirroot.'/question/type/ordering/question.php'); diff --git a/question/type/ordering/tests/backup_test.php b/question/type/ordering/tests/backup_test.php new file mode 100644 index 00000000000..6a4377654f4 --- /dev/null +++ b/question/type/ordering/tests/backup_test.php @@ -0,0 +1,96 @@ +. + +namespace qtype_ordering; + +use question_bank; +use test_question_maker; + +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 . '/course/externallib.php'); + +/** + * Tests for the orderinging question type backup and restore logic. + * + * @package qtype_ordering + * @copyright 2020 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class backup_test extends \advanced_testcase { + + /** + * Duplicate quiz with a orderinging question, and check it worked. + */ + public function test_duplicate_ordering_question(): void { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $coregenerator = $this->getDataGenerator(); + $questiongenerator = $coregenerator->get_plugin_generator('core_question'); + + // Create a course with a page that embeds a question. + $course = $coregenerator->create_course(); + $quiz = $coregenerator->create_module('quiz', ['course' => $course->id]); + $quizcontext = \context_module::instance($quiz->cmid); + + $cat = $questiongenerator->create_question_category(['contextid' => $quizcontext->id]); + $question = $questiongenerator->create_question('ordering', 'moodle', ['category' => $cat->id]); + + // Store some counts. + $numquizzes = count(get_fast_modinfo($course)->instances['quiz']); + $numorderingquestions = $DB->count_records('question', ['qtype' => 'ordering']); + + // Duplicate the page. + duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid)); + + // Verify the copied quiz exists. + $this->assertCount($numquizzes + 1, get_fast_modinfo($course)->instances['quiz']); + + // Verify the copied question. + $this->assertEquals($numorderingquestions + 1, $DB->count_records('question', ['qtype' => 'ordering'])); + $neworderingid = $DB->get_field_sql(" + SELECT MAX(id) + FROM {question} + WHERE qtype = ? + ", ['ordering']); + + // Declare some parts of the question to be compared. + $existingorderingdata = question_bank::load_question_data($question->id); + $orderingdata = question_bank::load_question_data($neworderingid); + $existinganswers = array_values((array) $existingorderingdata->options->answers); + $answers = array_values((array) $orderingdata->options->answers); + + // Verify the copied question has the same values without being too verbose. + foreach ((array) $existingorderingdata as $key => $value) { + if (in_array($key, ['questiontext', 'generalfeedback', 'partiallycorrectfeedback'])) { + $this->assertEquals($value, $orderingdata->$key); + } + } + // Verify some parts of the new question we'll know that will be different. + $this->assertNotEquals($existingorderingdata->id, $orderingdata->id); + + for ($i = 0; $i < count($existinganswers); $i++) { + $this->assertEquals($existinganswers[$i]->answer, $answers[$i]->answer); + $this->assertEquals($existinganswers[$i]->fraction, $answers[$i]->fraction); + $this->assertEquals($existinganswers[$i]->feedback, $answers[$i]->feedback); + } + } +} diff --git a/question/type/ordering/tests/fixtures/testexport.gift.txt b/question/type/ordering/tests/fixtures/testexport.gift.txt new file mode 100644 index 00000000000..cb01abcf087 --- /dev/null +++ b/question/type/ordering/tests/fixtures/testexport.gift.txt @@ -0,0 +1,10 @@ +// question: 409000 name: Moodle +// [id:myid] +::Moodle::[html]Put these words in order.{>0 none +Modular +Object +Oriented +Dynamic +Learning +Environment +} diff --git a/question/type/ordering/tests/fixtures/testexport.moodle.xml b/question/type/ordering/tests/fixtures/testexport.moodle.xml new file mode 100644 index 00000000000..cb3158fd57b --- /dev/null +++ b/question/type/ordering/tests/fixtures/testexport.moodle.xml @@ -0,0 +1,68 @@ + + + + Moodle + + + Put these words in order. + + + + + 1 + 0.3333333 + 0 + myid + + + 2 + + + none + + Well done! + + + Parts, but only parts, of your response are correct. + + + That is not right at all. + + 1 + + Modular + + Modular is correct. + + + + Object + + Object is correct. + + + + Oriented + + Oriented is correct. + + + + Dynamic + + Dynamic is correct. + + + + Learning + + Learning is correct. + + + + Environment + + Environment is correct. + + + diff --git a/question/type/ordering/tests/fixtures/testimport.gift.txt b/question/type/ordering/tests/fixtures/testimport.gift.txt new file mode 100644 index 00000000000..c23189a0b2d --- /dev/null +++ b/question/type/ordering/tests/fixtures/testimport.gift.txt @@ -0,0 +1,12 @@ +// question: 25 name: Moodle +// [id:myid] +::Moodle::[html]Put these words in order.{>4 CONTIGUOUS VERT ABSOLUTE TRUE ABCD +Modular +Object +Oriented +Dynamic +Learning +Environment +#### The correct answer is "Modular Object Oriented Dynamic Learning Environment". +} + diff --git a/question/type/ordering/tests/fixtures/testimport.moodle.xml b/question/type/ordering/tests/fixtures/testimport.moodle.xml new file mode 100644 index 00000000000..821cbf99210 --- /dev/null +++ b/question/type/ordering/tests/fixtures/testimport.moodle.xml @@ -0,0 +1,64 @@ + + + Moodle + + + Put these words in order. + + + + + + + + 1 + 0.3333333 + 0 + myid + none + + Your answer is correct.

]]>
+
+ + Your answer is partially correct.

]]>
+
+ + Your answer is incorrect.

]]>
+
+ + Modular + + + + + + Object + + + + + + Oriented + + + + + + Dynamic + + + + + + Learning + + + + + + Environment + + + + +
diff --git a/question/type/ordering/tests/fixtures/testimportempty.moodle.xml b/question/type/ordering/tests/fixtures/testimportempty.moodle.xml new file mode 100644 index 00000000000..df84d708588 --- /dev/null +++ b/question/type/ordering/tests/fixtures/testimportempty.moodle.xml @@ -0,0 +1,64 @@ + + + + + + Put these words in order. + + + + + + + + 1 + 0.3333333 + 0 + myid + none + + Your answer is correct.

]]>
+
+ + Your answer is partially correct.

]]>
+
+ + Your answer is incorrect.

]]>
+
+ + Modular + + + + + + Object + + + + + + Oriented + + + + + + Dynamic + + + + + + Learning + + + + + + Environment + + + + +
diff --git a/question/type/ordering/tests/fixtures/testimportlong.moodle.xml b/question/type/ordering/tests/fixtures/testimportlong.moodle.xml new file mode 100644 index 00000000000..dc884e6084e --- /dev/null +++ b/question/type/ordering/tests/fixtures/testimportlong.moodle.xml @@ -0,0 +1,64 @@ + + + Moodle Moodle Moodle Moodle Moodle Moodle Moodle + + + Put these words in order. + + + + + + + + 1 + 0.3333333 + 0 + myid + none + + Your answer is correct.

]]>
+
+ + Your answer is partially correct.

]]>
+
+ + Your answer is incorrect.

]]>
+
+ + Modular + + + + + + Object + + + + + + Oriented + + + + + + Dynamic + + + + + + Learning + + + + + + Environment + + + + +
diff --git a/question/type/ordering/tests/questiontype_test.php b/question/type/ordering/tests/questiontype_test.php index a11056d0fc1..6cfb6ac8838 100644 --- a/question/type/ordering/tests/questiontype_test.php +++ b/question/type/ordering/tests/questiontype_test.php @@ -24,23 +24,31 @@ namespace qtype_ordering; +use core_question_generator; use qtype_ordering; -use test_question_maker; -use qtype_ordering_edit_form; use qtype_ordering_test_helper; +use qtype_ordering_edit_form; +use qtype_ordering_question; +use test_question_maker; use question_bank; use question_possible_response; -use qtype_ordering_question; -use core_question_generator; + +use qformat_gift; +use question_check_specified_fields_expectation; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); -require_once($CFG->dirroot . '/question/type/ordering/questiontype.php'); require_once($CFG->dirroot . '/question/type/edit_question_form.php'); +require_once($CFG->dirroot . '/question/type/ordering/questiontype.php'); require_once($CFG->dirroot . '/question/type/ordering/edit_ordering_form.php'); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/format/gift/format.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); + /** * Unit tests for the ordering question type class. * @@ -48,16 +56,43 @@ require_once($CFG->dirroot . '/question/type/ordering/edit_ordering_form.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \qtype_ordering */ -class questiontype_test extends \advanced_testcase { +class questiontype_test extends \question_testcase { /** @var qtype_ordering instance of the question type class to test. */ protected $qtype; + /** @var object Default import object to compare against. */ + protected $expectedimportobj; + protected function setUp(): void { $this->qtype = new qtype_ordering(); + $this->expectedimportobj = (object) [ + 'qtype' => 'ordering', + 'idnumber' => 'myid', + 'name' => 'Moodle', + 'length' => 1, + 'penalty' => 0.3333333, + 'questiontext' => 'Put these words in order.', + 'questiontextformat' => 1, + 'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".', + 'generalfeedbackformat' => 1, + 'defaultmark' => 1, + ]; } protected function tearDown(): void { $this->qtype = null; + $this->expectedimportobj = null; + } + + /** + * Asserts that two XML strings are the same, ignoring differences in line endings. + * + * @param string $expectedxml + * @param string $xml + */ + public function assert_same_xml(string $expectedxml, string $xml): void { + $this->assertEquals(str_replace("\r\n", "\n", $expectedxml), + str_replace("\r\n", "\n", $xml)); } public function test_name(): void { @@ -215,4 +250,99 @@ class questiontype_test extends \advanced_testcase { $actual = $this->qtype->get_numberingstyle($questiondata); $this->assertEquals($expected, $actual); } + + public function test_xml_import(): void { + $this->resetAfterTest(); + // Import a question from XML + $xml = file_get_contents(__DIR__ . '/fixtures/testimport.moodle.xml'); + $xmldata = xmlize($xml); + $format = new \qformat_xml(); + $imported = $format->try_importing_using_qtypes( + $xmldata['question'], null, null, 'ordering'); + + $this->assert(new question_check_specified_fields_expectation($this->expectedimportobj), $imported); + } + + public function test_xml_import_empty(): void { + $this->resetAfterTest(); + // Import a question from XML + $xml = file_get_contents(__DIR__ . '/fixtures/testimportempty.moodle.xml'); + $xmldata = xmlize($xml); + $format = new \qformat_xml(); + $imported = $format->try_importing_using_qtypes( + $xmldata['question'], null, null, 'ordering'); + + $this->expectedimportobj->name = 'Put these words in order.'; + + $this->assert(new question_check_specified_fields_expectation($this->expectedimportobj), $imported); + } + + public function test_xml_import_long(): void { + $this->resetAfterTest(); + // Import a question from XML + $xml = file_get_contents(__DIR__ . '/fixtures/testimportlong.moodle.xml'); + $xmldata = xmlize($xml); + $format = new \qformat_xml(); + $imported = $format->try_importing_using_qtypes( + $xmldata['question'], null, null, 'ordering'); + + $this->expectedimportobj->name = 'Moodle Moodle Moodle Moodle Moodle Moodle ...'; + + $this->assert(new question_check_specified_fields_expectation($this->expectedimportobj), $imported); + } + + public function test_xml_export(): void { + $this->resetAfterTest(); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $category = $generator->create_question_category(); + $question = $generator->create_question('ordering', 'moodle', + ['category' => $category->id, 'idnumber' => 'myid']); + + // Export it. + $questiondata = question_bank::load_question_data($question->id); + // Add some feedback to ensure it comes through the export. + foreach ($questiondata->options->answers as $answer) { + $answer->feedback = $answer->answer . ' is correct.'; + $answer->feedbackformat = FORMAT_HTML; + $answer->feedbackfiles = 0; + } + + $exporter = new \qformat_xml(); + $xml = $exporter->writequestion($questiondata); + + $expectedxml = file_get_contents(__DIR__ . '/fixtures/testexport.moodle.xml'); + + $this->assert_same_xml($expectedxml, $xml); + } + + public function test_gift_import(): void { + $this->resetAfterTest(); + // Import a question from GIFT + $gift = file_get_contents(__DIR__ . '/fixtures/testimport.gift.txt'); + $format = new qformat_gift(); + $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift)); + $imported = $format->readquestion($lines); + + // TODO - MDL-XXXXX format_gift: Set ID & tags from comment for third parties. + // $this->assert(new question_check_specified_fields_expectation($this->expectedimportobj), $imported); + } + + public function test_gift_export(): void { + $this->resetAfterTest(); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $category = $generator->create_question_category(); + $question = $generator->create_question('ordering', 'moodle', + ['category' => $category->id, 'idnumber' => 'myid']); + + // Export it. + $questiondata = question_bank::load_question_data($question->id); + + $exporter = new qformat_gift(); + $gift = $exporter->writequestion($questiondata); + + $expectedgift = file_get_contents(__DIR__ . '/fixtures/testexport.gift.txt'); + + // TODO - MDL-XXXXX format_gift: Set ID & tags from comment for third parties. + // $this->assertEquals($expectedgift, $gift); + } }