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);
+ }
}