Differences Between: [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 /** 17 * Unit tests for export/import description (info) for question category in the Moodle XML format. 18 * 19 * @package qformat_xml 20 * @copyright 2014 Nikita Nikitsky, Volgograd State Technical University 21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 */ 23 defined('MOODLE_INTERNAL') || die(); 24 global $CFG; 25 require_once($CFG->libdir . '/questionlib.php'); 26 require_once($CFG->dirroot . '/question/format/xml/format.php'); 27 require_once($CFG->dirroot . '/question/format.php'); 28 require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); 29 require_once($CFG->dirroot . '/question/editlib.php'); 30 31 /** 32 * Unit tests for the XML question format import and export. 33 * 34 * @copyright 2014 Nikita Nikitsky, Volgograd State Technical University 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class qformat_xml_import_export_test extends advanced_testcase { 38 /** 39 * Create object qformat_xml for test. 40 * @param string $filename with name for testing file. 41 * @param stdClass $course 42 * @return qformat_xml XML question format object. 43 */ 44 public function create_qformat($filename, $course) { 45 global $DB; 46 $qformat = new qformat_xml(); 47 $contexts = $DB->get_records('context'); 48 $importfile = __DIR__ . '/fixtures/' .$filename; 49 $realfilename = $filename; 50 $qformat->setContexts($contexts); 51 $qformat->setCourse($course); 52 $qformat->setFilename($importfile); 53 $qformat->setRealfilename($realfilename); 54 $qformat->setMatchgrades('error'); 55 $qformat->setCatfromfile(1); 56 $qformat->setContextfromfile(1); 57 $qformat->setStoponerror(1); 58 $qformat->setCattofile(1); 59 $qformat->setContexttofile(1); 60 $qformat->set_display_progress(false); 61 62 return $qformat; 63 } 64 65 /** 66 * Check xml for compliance. 67 * @param string $expectedxml with correct string. 68 * @param string $xml you want to check. 69 */ 70 public function assert_same_xml($expectedxml, $xml) { 71 $this->assertEquals($this->normalise_xml($expectedxml), 72 $this->normalise_xml($xml)); 73 } 74 75 /** 76 * Clean up some XML to remove irrelevant differences, before it is compared. 77 * @param string $xml some XML. 78 * @return string cleaned-up XML. 79 */ 80 protected function normalise_xml($xml) { 81 // Normalise line endings. 82 $xml = str_replace("\r\n", "\n", $xml); 83 $xml = preg_replace("~\n$~", "", $xml); // Strip final newline in file. 84 85 // Replace all numbers in question id comments with 0. 86 $xml = preg_replace('~(?<=<!-- question: )([0-9]+)(?= -->)~', '0', $xml); 87 88 // Deal with how different databases output numbers. Only match when only thing in a tag. 89 $xml = preg_replace("~>.0000000<~", '>0<', $xml); // How Oracle outputs 0.0000000. 90 $xml = preg_replace("~(\.(:?[0-9]*[1-9])?)0*<~", '$1<', $xml); // Other cases of trailing 0s 91 $xml = preg_replace("~([0-9]).<~", '$1<', $xml); // Stray . in 1. after last step. 92 93 return $xml; 94 } 95 96 /** 97 * Check imported category. 98 * @param string $name imported category name. 99 * @param string $info imported category info field (description of category). 100 * @param int $infoformat imported category info field format. 101 */ 102 public function assert_category_imported($name, $info, $infoformat, $idnumber = null) { 103 global $DB; 104 $category = $DB->get_record('question_categories', ['name' => $name], '*', MUST_EXIST); 105 $this->assertEquals($info, $category->info); 106 $this->assertEquals($infoformat, $category->infoformat); 107 $this->assertSame($idnumber, $category->idnumber); 108 } 109 110 /** 111 * Check a question category has a given parent. 112 * @param string $catname Name of the question category 113 * @param string $parentname Name of the parent category 114 * @throws dml_exception 115 */ 116 public function assert_category_has_parent($catname, $parentname) { 117 global $DB; 118 $sql = 'SELECT qc1.* 119 FROM {question_categories} qc1 120 JOIN {question_categories} qc2 ON qc1.parent = qc2.id 121 WHERE qc1.name = ? 122 AND qc2.name = ?'; 123 $categories = $DB->get_records_sql($sql, [$catname, $parentname]); 124 $this->assertTrue(count($categories) == 1); 125 } 126 127 /** 128 * Check a question exists in a category. 129 * @param string $qname The name of the question 130 * @param string $catname The name of the category 131 * @throws dml_exception 132 */ 133 public function assert_question_in_category($qname, $catname) { 134 global $DB; 135 $question = $DB->get_record('question', ['name' => $qname], '*', MUST_EXIST); 136 $category = $DB->get_record('question_categories', ['name' => $catname], '*', MUST_EXIST); 137 $this->assertEquals($category->id, $question->category); 138 } 139 140 /** 141 * Simple check for importing a category with a description. 142 */ 143 public function test_import_category() { 144 $this->resetAfterTest(true); 145 $course = $this->getDataGenerator()->create_course(); 146 $this->setAdminUser(); 147 $qformat = $this->create_qformat('category_with_description.xml', $course); 148 $imported = $qformat->importprocess(); 149 $this->assertTrue($imported); 150 $this->assert_category_imported('Alpha', 151 'This is Alpha category for test', FORMAT_MOODLE, 'alpha-idnumber'); 152 $this->assert_category_has_parent('Alpha', 'top'); 153 } 154 155 /** 156 * Check importing nested categories. 157 */ 158 public function test_import_nested_categories() { 159 $this->resetAfterTest(true); 160 $course = $this->getDataGenerator()->create_course(); 161 $this->setAdminUser(); 162 $qformat = $this->create_qformat('nested_categories.xml', $course); 163 $imported = $qformat->importprocess(); 164 $this->assertTrue($imported); 165 $this->assert_category_imported('Delta', 'This is Delta category for test', FORMAT_PLAIN); 166 $this->assert_category_imported('Epsilon', 'This is Epsilon category for test', FORMAT_MARKDOWN); 167 $this->assert_category_imported('Zeta', 'This is Zeta category for test', FORMAT_MOODLE); 168 $this->assert_category_has_parent('Delta', 'top'); 169 $this->assert_category_has_parent('Epsilon', 'Delta'); 170 $this->assert_category_has_parent('Zeta', 'Epsilon'); 171 } 172 173 /** 174 * Check importing nested categories contain the right questions. 175 */ 176 public function test_import_nested_categories_with_questions() { 177 $this->resetAfterTest(true); 178 $course = $this->getDataGenerator()->create_course(); 179 $this->setAdminUser(); 180 $qformat = $this->create_qformat('nested_categories_with_questions.xml', $course); 181 $imported = $qformat->importprocess(); 182 $this->assertTrue($imported); 183 $this->assert_category_imported('Iota', 'This is Iota category for test', FORMAT_PLAIN); 184 $this->assert_category_imported('Kappa', 'This is Kappa category for test', FORMAT_MARKDOWN); 185 $this->assert_category_imported('Lambda', 'This is Lambda category for test', FORMAT_MOODLE); 186 $this->assert_category_imported('Mu', 'This is Mu category for test', FORMAT_MOODLE); 187 $this->assert_question_in_category('Iota Question', 'Iota'); 188 $this->assert_question_in_category('Kappa Question', 'Kappa'); 189 $this->assert_question_in_category('Lambda Question', 'Lambda'); 190 $this->assert_question_in_category('Mu Question', 'Mu'); 191 $this->assert_category_has_parent('Iota', 'top'); 192 $this->assert_category_has_parent('Kappa', 'Iota'); 193 $this->assert_category_has_parent('Lambda', 'Kappa'); 194 $this->assert_category_has_parent('Mu', 'Iota'); 195 } 196 197 /** 198 * Check import of an old file (without format), for backward compatability. 199 */ 200 public function test_import_old_format() { 201 $this->resetAfterTest(true); 202 $course = $this->getDataGenerator()->create_course(); 203 $this->setAdminUser(); 204 $qformat = $this->create_qformat('old_format_file.xml', $course); 205 $imported = $qformat->importprocess(); 206 $this->assertTrue($imported); 207 $this->assert_category_imported('Pi', '', FORMAT_MOODLE); 208 $this->assert_category_imported('Rho', '', FORMAT_MOODLE); 209 $this->assert_question_in_category('Pi Question', 'Pi'); 210 $this->assert_question_in_category('Rho Question', 'Rho'); 211 $this->assert_category_has_parent('Pi', 'top'); 212 $this->assert_category_has_parent('Rho', 'Pi'); 213 } 214 215 /** 216 * Check the import of an xml file where the child category exists before the parent category. 217 */ 218 public function test_import_categories_in_reverse_order() { 219 $this->resetAfterTest(true); 220 $course = $this->getDataGenerator()->create_course(); 221 $this->setAdminUser(); 222 $qformat = $this->create_qformat('categories_reverse_order.xml', $course); 223 $imported = $qformat->importprocess(); 224 $this->assertTrue($imported); 225 $this->assert_category_imported('Sigma', 'This is Sigma category for test', FORMAT_HTML); 226 $this->assert_category_imported('Tau', 'This is Tau category for test', FORMAT_HTML); 227 $this->assert_question_in_category('Sigma Question', 'Sigma'); 228 $this->assert_question_in_category('Tau Question', 'Tau'); 229 $this->assert_category_has_parent('Sigma', 'top'); 230 $this->assert_category_has_parent('Tau', 'Sigma'); 231 } 232 233 /** 234 * Simple check for exporting a category. 235 */ 236 public function test_export_category() { 237 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 238 $this->resetAfterTest(true); 239 $course = $this->getDataGenerator()->create_course(); 240 $this->setAdminUser(); 241 // Note while this loads $qformat with all the 'right' data from the xml file, 242 // the call to setCategory, followed by exportprocess will actually only export data 243 // from the database (created by the generator). 244 $qformat = $this->create_qformat('export_category.xml', $course); 245 246 $category = $generator->create_question_category([ 247 'name' => 'Alpha', 248 'contextid' => '2', 249 'info' => 'This is Alpha category for test', 250 'infoformat' => '0', 251 'idnumber' => 'alpha-idnumber', 252 'stamp' => make_unique_id_code(), 253 'parent' => '0', 254 'sortorder' => '999']); 255 $question = $generator->create_question('truefalse', null, [ 256 'category' => $category->id, 257 'name' => 'Alpha Question', 258 'questiontext' => ['format' => '1', 'text' => '<p>Testing Alpha Question</p>'], 259 'generalfeedback' => ['format' => '1', 'text' => ''], 260 'correctanswer' => '1', 261 'feedbacktrue' => ['format' => '1', 'text' => ''], 262 'feedbackfalse' => ['format' => '1', 'text' => ''], 263 'penalty' => '1']); 264 $qformat->setCategory($category); 265 266 $expectedxml = file_get_contents(__DIR__ . '/fixtures/export_category.xml'); 267 $this->assert_same_xml($expectedxml, $qformat->exportprocess()); 268 } 269 270 /** 271 * Check exporting nested categories. 272 */ 273 public function test_export_nested_categories() { 274 $this->resetAfterTest(true); 275 $course = $this->getDataGenerator()->create_course(); 276 $this->setAdminUser(); 277 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 278 $qformat = $this->create_qformat('nested_categories.zml', $course); 279 280 $categorydelta = $generator->create_question_category([ 281 'name' => 'Delta', 282 'contextid' => '2', 283 'info' => 'This is Delta category for test', 284 'infoformat' => '2', 285 'stamp' => make_unique_id_code(), 286 'parent' => '0', 287 'sortorder' => '999']); 288 $categoryepsilon = $generator->create_question_category([ 289 'name' => 'Epsilon', 290 'contextid' => '2', 291 'info' => 'This is Epsilon category for test', 292 'infoformat' => '4', 293 'stamp' => make_unique_id_code(), 294 'parent' => $categorydelta->id, 295 'sortorder' => '999']); 296 $categoryzeta = $generator->create_question_category([ 297 'name' => 'Zeta', 298 'contextid' => '2', 299 'info' => 'This is Zeta category for test', 300 'infoformat' => '0', 301 'stamp' => make_unique_id_code(), 302 'parent' => $categoryepsilon->id, 303 'sortorder' => '999']); 304 $question = $generator->create_question('truefalse', null, [ 305 'category' => $categoryzeta->id, 306 'name' => 'Zeta Question', 307 'questiontext' => [ 308 'format' => '1', 309 'text' => '<p>Testing Zeta Question</p>'], 310 'generalfeedback' => ['format' => '1', 'text' => ''], 311 'correctanswer' => '1', 312 'feedbacktrue' => ['format' => '1', 'text' => ''], 313 'feedbackfalse' => ['format' => '1', 'text' => ''], 314 'penalty' => '1']); 315 $qformat->setCategory($categorydelta); 316 $qformat->setCategory($categoryepsilon); 317 $qformat->setCategory($categoryzeta); 318 319 $expectedxml = file_get_contents(__DIR__ . '/fixtures/nested_categories.xml'); 320 $this->assert_same_xml($expectedxml, $qformat->exportprocess()); 321 } 322 323 /** 324 * Check exporting nested categories contain the right questions. 325 */ 326 public function test_export_nested_categories_with_questions() { 327 $this->resetAfterTest(true); 328 $course = $this->getDataGenerator()->create_course(); 329 $this->setAdminUser(); 330 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 331 $qformat = $this->create_qformat('nested_categories_with_questions.xml', $course); 332 333 $categoryiota = $generator->create_question_category([ 334 'name' => 'Iota', 335 'contextid' => '2', 336 'info' => 'This is Iota category for test', 337 'infoformat' => '2', 338 'stamp' => make_unique_id_code(), 339 'parent' => '0', 340 'sortorder' => '999']); 341 $iotaquestion = $generator->create_question('truefalse', null, [ 342 'category' => $categoryiota->id, 343 'name' => 'Iota Question', 344 'questiontext' => [ 345 'format' => '1', 346 'text' => '<p>Testing Iota Question</p>'], 347 'generalfeedback' => ['format' => '1', 'text' => ''], 348 'correctanswer' => '1', 349 'feedbacktrue' => ['format' => '1', 'text' => ''], 350 'feedbackfalse' => ['format' => '1', 'text' => ''], 351 'penalty' => '1']); 352 $categorykappa = $generator->create_question_category([ 353 'name' => 'Kappa', 354 'contextid' => '2', 355 'info' => 'This is Kappa category for test', 356 'infoformat' => '4', 357 'stamp' => make_unique_id_code(), 358 'parent' => $categoryiota->id, 359 'sortorder' => '999']); 360 $kappaquestion = $generator->create_question('essay', null, [ 361 'category' => $categorykappa->id, 362 'name' => 'Kappa Essay Question', 363 'questiontext' => ['text' => 'Testing Kappa Essay Question'], 364 'generalfeedback' => '', 365 'responseformat' => 'editor', 366 'responserequired' => 1, 367 'responsefieldlines' => 10, 368 'attachments' => 0, 369 'attachmentsrequired' => 0, 370 'graderinfo' => ['format' => '1', 'text' => ''], 371 'responsetemplate' => ['format' => '1', 'text' => ''], 372 'idnumber' => '']); 373 $kappaquestion1 = $generator->create_question('truefalse', null, [ 374 'category' => $categorykappa->id, 375 'name' => 'Kappa Question', 376 'questiontext' => [ 377 'format' => '1', 378 'text' => '<p>Testing Kappa Question</p>'], 379 'generalfeedback' => ['format' => '1', 'text' => ''], 380 'correctanswer' => '1', 381 'feedbacktrue' => ['format' => '1', 'text' => ''], 382 'feedbackfalse' => ['format' => '1', 'text' => ''], 383 'penalty' => '1', 384 'idnumber' => '']); 385 $categorylambda = $generator->create_question_category([ 386 'name' => 'Lambda', 387 'contextid' => '2', 388 'info' => 'This is Lambda category for test', 389 'infoformat' => '0', 390 'stamp' => make_unique_id_code(), 391 'parent' => $categorykappa->id, 392 'sortorder' => '999']); 393 $lambdaquestion = $generator->create_question('truefalse', null, [ 394 'category' => $categorylambda->id, 395 'name' => 'Lambda Question', 396 'questiontext' => [ 397 'format' => '1', 398 'text' => '<p>Testing Lambda Question</p>'], 399 'generalfeedback' => ['format' => '1', 'text' => ''], 400 'correctanswer' => '1', 401 'feedbacktrue' => ['format' => '1', 'text' => ''], 402 'feedbackfalse' => ['format' => '1', 'text' => ''], 403 'penalty' => '1']); 404 $categorymu = $generator->create_question_category([ 405 'name' => 'Mu', 406 'contextid' => '2', 407 'info' => 'This is Mu category for test', 408 'infoformat' => '0', 409 'stamp' => make_unique_id_code(), 410 'parent' => $categoryiota->id, 411 'sortorder' => '999']); 412 $muquestion = $generator->create_question('truefalse', null, [ 413 'category' => $categorymu->id, 414 'name' => 'Mu Question', 415 'questiontext' => [ 416 'format' => '1', 417 'text' => '<p>Testing Mu Question</p>'], 418 'generalfeedback' => ['format' => '1', 'text' => ''], 419 'correctanswer' => '1', 420 'feedbacktrue' => ['format' => '1', 'text' => ''], 421 'feedbackfalse' => ['format' => '1', 'text' => ''], 422 'penalty' => '1']); 423 $qformat->setCategory($categoryiota); 424 425 $expectedxml = file_get_contents(__DIR__ . '/fixtures/nested_categories_with_questions.xml'); 426 $this->assert_same_xml($expectedxml, $qformat->exportprocess()); 427 } 428 429 /** 430 * Simple check for exporting a category. 431 */ 432 public function test_export_category_with_special_chars() { 433 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 434 $this->resetAfterTest(true); 435 $course = $this->getDataGenerator()->create_course(); 436 $this->setAdminUser(); 437 // Note while this loads $qformat with all the 'right' data from the xml file, 438 // the call to setCategory, followed by exportprocess will actually only export data 439 // from the database (created by the generator). 440 $qformat = $this->create_qformat('export_category.xml', $course); 441 442 $category = $generator->create_question_category([ 443 'name' => 'Alpha', 444 'contextid' => '2', 445 'info' => 'This is Alpha category for test', 446 'infoformat' => '0', 447 'idnumber' => 'The inequalities < & >', 448 'stamp' => make_unique_id_code(), 449 'parent' => '0', 450 'sortorder' => '999']); 451 $generator->create_question('truefalse', null, [ 452 'category' => $category->id, 453 'name' => 'Alpha Question', 454 'questiontext' => ['format' => '1', 'text' => '<p>Testing Alpha Question</p>'], 455 'generalfeedback' => ['format' => '1', 'text' => ''], 456 'idnumber' => 'T & F', 457 'correctanswer' => '1', 458 'feedbacktrue' => ['format' => '1', 'text' => ''], 459 'feedbackfalse' => ['format' => '1', 'text' => ''], 460 'penalty' => '1']); 461 $qformat->setCategory($category); 462 463 $expectedxml = file_get_contents(__DIR__ . '/fixtures/html_chars_in_idnumbers.xml'); 464 $this->assert_same_xml($expectedxml, $qformat->exportprocess()); 465 } 466 467 /** 468 * Test that bad multianswer questions are not imported. 469 */ 470 public function test_import_broken_multianswer_questions() { 471 $lines = file(__DIR__ . '/fixtures/broken_cloze_questions.xml'); 472 $importer = $qformat = new qformat_xml(); 473 474 // The importer echoes some errors, so we need to capture and check that. 475 ob_start(); 476 $questions = $importer->readquestions($lines); 477 $output = ob_get_contents(); 478 ob_end_clean(); 479 480 // Check that there were some expected errors. 481 $this->assertStringContainsString('Error importing question', $output); 482 $this->assertStringContainsString('Invalid embedded answers (Cloze) question', $output); 483 $this->assertStringContainsString('This type of question requires at least 2 choices', $output); 484 $this->assertStringContainsString('The answer must be a number, for example -1.234 or 3e8, or \'*\'.', $output); 485 $this->assertStringContainsString('One of the answers should have a score of 100% so it is possible to get full marks for this question.', 486 $output); 487 $this->assertStringContainsString('The question text must include at least one embedded answer.', $output); 488 489 // No question have been imported. 490 $this->assertCount(0, $questions); 491 } 492 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body