[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
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 /** 18 * This file contains helper classes for testing the question engine. 19 * 20 * @package moodlecore 21 * @subpackage questionengine 22 * @copyright 2009 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; 30 require_once(dirname(__FILE__) . '/../lib.php'); 31 32 33 /** 34 * Makes some protected methods of question_attempt public to facilitate testing. 35 * 36 * @copyright 2009 The Open University 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class testable_question_attempt extends question_attempt { 40 public function add_step(question_attempt_step $step) { 41 parent::add_step($step); 42 } 43 public function set_min_fraction($fraction) { 44 $this->minfraction = $fraction; 45 } 46 public function set_max_fraction($fraction) { 47 $this->maxfraction = $fraction; 48 } 49 public function set_behaviour(question_behaviour $behaviour) { 50 $this->behaviour = $behaviour; 51 } 52 } 53 54 55 /** 56 * Test subclass to allow access to some protected data so that the correct 57 * behaviour can be verified. 58 * 59 * @copyright 2012 The Open University 60 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 61 */ 62 class testable_question_engine_unit_of_work extends question_engine_unit_of_work { 63 public function get_modified() { 64 return $this->modified; 65 } 66 67 public function get_attempts_added() { 68 return $this->attemptsadded; 69 } 70 71 public function get_attempts_modified() { 72 return $this->attemptsmodified; 73 } 74 75 public function get_steps_added() { 76 return $this->stepsadded; 77 } 78 79 public function get_steps_modified() { 80 return $this->stepsmodified; 81 } 82 83 public function get_steps_deleted() { 84 return $this->stepsdeleted; 85 } 86 } 87 88 89 /** 90 * Base class for question type test helpers. 91 * 92 * @copyright 2011 The Open University 93 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 94 */ 95 abstract class question_test_helper { 96 /** 97 * @return array of example question names that can be passed as the $which 98 * argument of {@link test_question_maker::make_question} when $qtype is 99 * this question type. 100 */ 101 abstract public function get_test_questions(); 102 103 /** 104 * Set up a form to create a question in $cat. This method also sets cat and contextid on $questiondata object. 105 * @param object $cat the category 106 * @param object $questiondata form initialisation requires question data. 107 * @return moodleform 108 */ 109 public static function get_question_editing_form($cat, $questiondata) { 110 $catcontext = context::instance_by_id($cat->contextid, MUST_EXIST); 111 $contexts = new question_edit_contexts($catcontext); 112 $dataforformconstructor = new stdClass(); 113 $dataforformconstructor->qtype = $questiondata->qtype; 114 $dataforformconstructor->contextid = $questiondata->contextid = $catcontext->id; 115 $dataforformconstructor->category = $questiondata->category = $cat->id; 116 $dataforformconstructor->formoptions = new stdClass(); 117 $dataforformconstructor->formoptions->canmove = true; 118 $dataforformconstructor->formoptions->cansaveasnew = true; 119 $dataforformconstructor->formoptions->canedit = true; 120 $dataforformconstructor->formoptions->repeatelements = true; 121 $qtype = question_bank::get_qtype($questiondata->qtype); 122 return $qtype->create_editing_form('question.php', $dataforformconstructor, $cat, $contexts, true); 123 } 124 } 125 126 127 /** 128 * This class creates questions of various types, which can then be used when 129 * testing. 130 * 131 * @copyright 2009 The Open University 132 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 133 */ 134 class test_question_maker { 135 const STANDARD_OVERALL_CORRECT_FEEDBACK = 'Well done!'; 136 const STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK = 137 'Parts, but only parts, of your response are correct.'; 138 const STANDARD_OVERALL_INCORRECT_FEEDBACK = 'That is not right at all.'; 139 140 /** @var array qtype => qtype test helper class. */ 141 protected static $testhelpers = array(); 142 143 /** 144 * Just make a question_attempt at a question. Useful for unit tests that 145 * need to pass a $qa to methods that call format_text. Probably not safe 146 * to use for anything beyond that. 147 * @param question_definition $question a question. 148 * @param number $maxmark the max mark to set. 149 * @return question_attempt the question attempt. 150 */ 151 public static function get_a_qa($question, $maxmark = 3) { 152 return new question_attempt($question, 13, null, $maxmark); 153 } 154 155 /** 156 * Initialise the common fields of a question of any type. 157 */ 158 public static function initialise_a_question($q) { 159 global $USER; 160 161 $q->id = 0; 162 $q->category = 0; 163 $q->parent = 0; 164 $q->questiontextformat = FORMAT_HTML; 165 $q->generalfeedbackformat = FORMAT_HTML; 166 $q->defaultmark = 1; 167 $q->penalty = 0.3333333; 168 $q->length = 1; 169 $q->stamp = make_unique_id_code(); 170 $q->version = make_unique_id_code(); 171 $q->hidden = 0; 172 $q->timecreated = time(); 173 $q->timemodified = time(); 174 $q->createdby = $USER->id; 175 $q->modifiedby = $USER->id; 176 } 177 178 public static function initialise_question_data($qdata) { 179 global $USER; 180 181 $qdata->id = 0; 182 $qdata->category = 0; 183 $qdata->contextid = 0; 184 $qdata->parent = 0; 185 $qdata->questiontextformat = FORMAT_HTML; 186 $qdata->generalfeedbackformat = FORMAT_HTML; 187 $qdata->defaultmark = 1; 188 $qdata->penalty = 0.3333333; 189 $qdata->length = 1; 190 $qdata->stamp = make_unique_id_code(); 191 $qdata->version = make_unique_id_code(); 192 $qdata->hidden = 0; 193 $qdata->timecreated = time(); 194 $qdata->timemodified = time(); 195 $qdata->createdby = $USER->id; 196 $qdata->modifiedby = $USER->id; 197 $qdata->hints = array(); 198 } 199 200 /** 201 * Get the test helper class for a particular question type. 202 * @param $qtype the question type name, e.g. 'multichoice'. 203 * @return question_test_helper the test helper class. 204 */ 205 public static function get_test_helper($qtype) { 206 global $CFG; 207 208 if (array_key_exists($qtype, self::$testhelpers)) { 209 return self::$testhelpers[$qtype]; 210 } 211 212 $file = core_component::get_plugin_directory('qtype', $qtype) . '/tests/helper.php'; 213 if (!is_readable($file)) { 214 throw new coding_exception('Question type ' . $qtype . 215 ' does not have test helper code.'); 216 } 217 include_once($file); 218 219 $class = 'qtype_' . $qtype . '_test_helper'; 220 if (!class_exists($class)) { 221 throw new coding_exception('Class ' . $class . ' is not defined in ' . $file); 222 } 223 224 self::$testhelpers[$qtype] = new $class(); 225 return self::$testhelpers[$qtype]; 226 } 227 228 /** 229 * Call a method on a qtype_{$qtype}_test_helper class and return the result. 230 * 231 * @param string $methodtemplate e.g. 'make_{qtype}_question_{which}'; 232 * @param string $qtype the question type to get a test question for. 233 * @param string $which one of the names returned by the get_test_questions 234 * method of the relevant qtype_{$qtype}_test_helper class. 235 * @param unknown_type $which 236 */ 237 protected static function call_question_helper_method($methodtemplate, $qtype, $which = null) { 238 $helper = self::get_test_helper($qtype); 239 240 $available = $helper->get_test_questions(); 241 242 if (is_null($which)) { 243 $which = reset($available); 244 } else if (!in_array($which, $available)) { 245 throw new coding_exception('Example question ' . $which . ' of type ' . 246 $qtype . ' does not exist.'); 247 } 248 249 $method = str_replace(array('{qtype}', '{which}'), 250 array($qtype, $which), $methodtemplate); 251 252 if (!method_exists($helper, $method)) { 253 throw new coding_exception('Method ' . $method . ' does not exist on the ' . 254 $qtype . ' question type test helper class.'); 255 } 256 257 return $helper->$method(); 258 } 259 260 /** 261 * Question types can provide a number of test question defintions. 262 * They do this by creating a qtype_{$qtype}_test_helper class that extends 263 * question_test_helper. The get_test_questions method returns the list of 264 * test questions available for this question type. 265 * 266 * @param string $qtype the question type to get a test question for. 267 * @param string $which one of the names returned by the get_test_questions 268 * method of the relevant qtype_{$qtype}_test_helper class. 269 * @return question_definition the requested question object. 270 */ 271 public static function make_question($qtype, $which = null) { 272 return self::call_question_helper_method('make_{qtype}_question_{which}', 273 $qtype, $which); 274 } 275 276 /** 277 * Like {@link make_question()} but returns the datastructure from 278 * get_question_options instead of the question_definition object. 279 * 280 * @param string $qtype the question type to get a test question for. 281 * @param string $which one of the names returned by the get_test_questions 282 * method of the relevant qtype_{$qtype}_test_helper class. 283 * @return stdClass the requested question object. 284 */ 285 public static function get_question_data($qtype, $which = null) { 286 return self::call_question_helper_method('get_{qtype}_question_data_{which}', 287 $qtype, $which); 288 } 289 290 /** 291 * Like {@link make_question()} but returns the data what would be saved from 292 * the question editing form instead of the question_definition object. 293 * 294 * @param string $qtype the question type to get a test question for. 295 * @param string $which one of the names returned by the get_test_questions 296 * method of the relevant qtype_{$qtype}_test_helper class. 297 * @return stdClass the requested question object. 298 */ 299 public static function get_question_form_data($qtype, $which = null) { 300 return self::call_question_helper_method('get_{qtype}_question_form_data_{which}', 301 $qtype, $which); 302 } 303 304 /** 305 * Makes a multichoice question with choices 'A', 'B' and 'C' shuffled. 'A' 306 * is correct, defaultmark 1. 307 * @return qtype_multichoice_single_question 308 */ 309 public static function make_a_multichoice_single_question() { 310 question_bank::load_question_definition_classes('multichoice'); 311 $mc = new qtype_multichoice_single_question(); 312 self::initialise_a_question($mc); 313 $mc->name = 'Multi-choice question, single response'; 314 $mc->questiontext = 'The answer is A.'; 315 $mc->generalfeedback = 'You should have selected A.'; 316 $mc->qtype = question_bank::get_qtype('multichoice'); 317 318 $mc->shuffleanswers = 1; 319 $mc->answernumbering = 'abc'; 320 321 $mc->answers = array( 322 13 => new question_answer(13, 'A', 1, 'A is right', FORMAT_HTML), 323 14 => new question_answer(14, 'B', -0.3333333, 'B is wrong', FORMAT_HTML), 324 15 => new question_answer(15, 'C', -0.3333333, 'C is wrong', FORMAT_HTML), 325 ); 326 327 return $mc; 328 } 329 330 /** 331 * Makes a multichoice question with choices 'A', 'B', 'C' and 'D' shuffled. 332 * 'A' and 'C' is correct, defaultmark 1. 333 * @return qtype_multichoice_multi_question 334 */ 335 public static function make_a_multichoice_multi_question() { 336 question_bank::load_question_definition_classes('multichoice'); 337 $mc = new qtype_multichoice_multi_question(); 338 self::initialise_a_question($mc); 339 $mc->name = 'Multi-choice question, multiple response'; 340 $mc->questiontext = 'The answer is A and C.'; 341 $mc->generalfeedback = 'You should have selected A and C.'; 342 $mc->qtype = question_bank::get_qtype('multichoice'); 343 344 $mc->shuffleanswers = 1; 345 $mc->answernumbering = 'abc'; 346 347 self::set_standard_combined_feedback_fields($mc); 348 349 $mc->answers = array( 350 13 => new question_answer(13, 'A', 0.5, 'A is part of the right answer', FORMAT_HTML), 351 14 => new question_answer(14, 'B', -1, 'B is wrong', FORMAT_HTML), 352 15 => new question_answer(15, 'C', 0.5, 'C is part of the right answer', FORMAT_HTML), 353 16 => new question_answer(16, 'D', -1, 'D is wrong', FORMAT_HTML), 354 ); 355 356 return $mc; 357 } 358 359 /** 360 * Makes a matching question to classify 'Dog', 'Frog', 'Toad' and 'Cat' as 361 * 'Mammal', 'Amphibian' or 'Insect'. 362 * defaultmark 1. Stems are shuffled by default. 363 * @return qtype_match_question 364 */ 365 public static function make_a_matching_question() { 366 question_bank::load_question_definition_classes('match'); 367 $match = new qtype_match_question(); 368 self::initialise_a_question($match); 369 $match->name = 'Matching question'; 370 $match->questiontext = 'Classify the animals.'; 371 $match->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.'; 372 $match->qtype = question_bank::get_qtype('match'); 373 374 $match->shufflestems = 1; 375 376 self::set_standard_combined_feedback_fields($match); 377 378 // Using unset to get 1-based arrays. 379 $match->stems = array('', 'Dog', 'Frog', 'Toad', 'Cat'); 380 $match->stemformat = array('', FORMAT_HTML, FORMAT_HTML, FORMAT_HTML, FORMAT_HTML); 381 $match->choices = array('', 'Mammal', 'Amphibian', 'Insect'); 382 $match->right = array('', 1, 2, 2, 1); 383 unset($match->stems[0]); 384 unset($match->stemformat[0]); 385 unset($match->choices[0]); 386 unset($match->right[0]); 387 388 return $match; 389 } 390 391 /** 392 * Makes a truefalse question with correct ansewer true, defaultmark 1. 393 * @return qtype_essay_question 394 */ 395 public static function make_an_essay_question() { 396 question_bank::load_question_definition_classes('essay'); 397 $essay = new qtype_essay_question(); 398 self::initialise_a_question($essay); 399 $essay->name = 'Essay question'; 400 $essay->questiontext = 'Write an essay.'; 401 $essay->generalfeedback = 'I hope you wrote an interesting essay.'; 402 $essay->penalty = 0; 403 $essay->qtype = question_bank::get_qtype('essay'); 404 405 $essay->responseformat = 'editor'; 406 $essay->responserequired = 1; 407 $essay->responsefieldlines = 15; 408 $essay->attachments = 0; 409 $essay->attachmentsrequired = 0; 410 $essay->responsetemplate = ''; 411 $essay->responsetemplateformat = FORMAT_MOODLE; 412 $essay->graderinfo = ''; 413 $essay->graderinfoformat = FORMAT_MOODLE; 414 415 return $essay; 416 } 417 418 /** 419 * Add some standard overall feedback to a question. You need to use these 420 * specific feedback strings for the corresponding contains_..._feedback 421 * methods in {@link qbehaviour_walkthrough_test_base} to works. 422 * @param question_definition $q the question to add the feedback to. 423 */ 424 public static function set_standard_combined_feedback_fields($q) { 425 $q->correctfeedback = self::STANDARD_OVERALL_CORRECT_FEEDBACK; 426 $q->correctfeedbackformat = FORMAT_HTML; 427 $q->partiallycorrectfeedback = self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK; 428 $q->partiallycorrectfeedbackformat = FORMAT_HTML; 429 $q->shownumcorrect = true; 430 $q->incorrectfeedback = self::STANDARD_OVERALL_INCORRECT_FEEDBACK; 431 $q->incorrectfeedbackformat = FORMAT_HTML; 432 } 433 434 /** 435 * Add some standard overall feedback to a question's form data. 436 */ 437 public static function set_standard_combined_feedback_form_data($form) { 438 $form->correctfeedback = array('text' => self::STANDARD_OVERALL_CORRECT_FEEDBACK, 439 'format' => FORMAT_HTML); 440 $form->partiallycorrectfeedback = array('text' => self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK, 441 'format' => FORMAT_HTML); 442 $form->shownumcorrect = true; 443 $form->incorrectfeedback = array('text' => self::STANDARD_OVERALL_INCORRECT_FEEDBACK, 444 'format' => FORMAT_HTML); 445 } 446 } 447 448 449 /** 450 * Helper for tests that need to simulate records loaded from the database. 451 * 452 * @copyright 2009 The Open University 453 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 454 */ 455 abstract class testing_db_record_builder { 456 public static function build_db_records(array $table) { 457 $columns = array_shift($table); 458 $records = array(); 459 foreach ($table as $row) { 460 if (count($row) != count($columns)) { 461 throw new coding_exception("Row contains the wrong number of fields."); 462 } 463 $rec = new stdClass(); 464 foreach ($columns as $i => $name) { 465 $rec->$name = $row[$i]; 466 } 467 $records[] = $rec; 468 } 469 return $records; 470 } 471 } 472 473 474 /** 475 * Helper base class for tests that need to simulate records loaded from the 476 * database. 477 * 478 * @copyright 2009 The Open University 479 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 480 */ 481 abstract class data_loading_method_test_base extends advanced_testcase { 482 public function build_db_records(array $table) { 483 return testing_db_record_builder::build_db_records($table); 484 } 485 } 486 487 488 abstract class question_testcase extends advanced_testcase { 489 490 public function assert($expectation, $compare, $notused = '') { 491 492 if (get_class($expectation) === 'question_pattern_expectation') { 493 $this->assertRegExp($expectation->pattern, $compare, 494 'Expected regex ' . $expectation->pattern . ' not found in ' . $compare); 495 return; 496 497 } else if (get_class($expectation) === 'question_no_pattern_expectation') { 498 $this->assertNotRegExp($expectation->pattern, $compare, 499 'Unexpected regex ' . $expectation->pattern . ' found in ' . $compare); 500 return; 501 502 } else if (get_class($expectation) === 'question_contains_tag_with_attributes') { 503 $this->assertTag(array('tag'=>$expectation->tag, 'attributes'=>$expectation->expectedvalues), $compare, 504 'Looking for a ' . $expectation->tag . ' with attributes ' . html_writer::attributes($expectation->expectedvalues) . ' in ' . $compare); 505 foreach ($expectation->forbiddenvalues as $k=>$v) { 506 $attr = $expectation->expectedvalues; 507 $attr[$k] = $v; 508 $this->assertNotTag(array('tag'=>$expectation->tag, 'attributes'=>$attr), $compare, 509 $expectation->tag . ' had a ' . $k . ' attribute that should not be there in ' . $compare); 510 } 511 return; 512 513 } else if (get_class($expectation) === 'question_contains_tag_with_attribute') { 514 $attr = array($expectation->attribute=>$expectation->value); 515 $this->assertTag(array('tag'=>$expectation->tag, 'attributes'=>$attr), $compare, 516 'Looking for a ' . $expectation->tag . ' with attribute ' . html_writer::attributes($attr) . ' in ' . $compare); 517 return; 518 519 } else if (get_class($expectation) === 'question_does_not_contain_tag_with_attributes') { 520 $this->assertNotTag(array('tag'=>$expectation->tag, 'attributes'=>$expectation->attributes), $compare, 521 'Unexpected ' . $expectation->tag . ' with attributes ' . html_writer::attributes($expectation->attributes) . ' found in ' . $compare); 522 return; 523 524 } else if (get_class($expectation) === 'question_contains_select_expectation') { 525 $tag = array('tag'=>'select', 'attributes'=>array('name'=>$expectation->name), 526 'children'=>array('count'=>count($expectation->choices))); 527 if ($expectation->enabled === false) { 528 $tag['attributes']['disabled'] = 'disabled'; 529 } else if ($expectation->enabled === true) { 530 // TODO 531 } 532 foreach(array_keys($expectation->choices) as $value) { 533 if ($expectation->selected === $value) { 534 $tag['child'] = array('tag'=>'option', 'attributes'=>array('value'=>$value, 'selected'=>'selected')); 535 } else { 536 $tag['child'] = array('tag'=>'option', 'attributes'=>array('value'=>$value)); 537 } 538 } 539 540 $this->assertTag($tag, $compare, 'expected select not found in ' . $compare); 541 return; 542 543 } else if (get_class($expectation) === 'question_check_specified_fields_expectation') { 544 $expect = (array)$expectation->expect; 545 $compare = (array)$compare; 546 foreach ($expect as $k=>$v) { 547 if (!array_key_exists($k, $compare)) { 548 $this->fail("Property {$k} does not exist"); 549 } 550 if ($v != $compare[$k]) { 551 $this->fail("Property {$k} is different"); 552 } 553 } 554 $this->assertTrue(true); 555 return; 556 557 } else if (get_class($expectation) === 'question_contains_tag_with_contents') { 558 $this->assertTag(array('tag'=>$expectation->tag, 'content'=>$expectation->content), $compare, 559 'Looking for a ' . $expectation->tag . ' with content ' . $expectation->content . ' in ' . $compare); 560 return; 561 } 562 563 throw new coding_exception('Unknown expectiontion:'.get_class($expectation)); 564 } 565 } 566 567 568 class question_contains_tag_with_contents { 569 public $tag; 570 public $content; 571 public $message; 572 573 public function __construct($tag, $content, $message = '') { 574 $this->tag = $tag; 575 $this->content = $content; 576 $this->message = $message; 577 } 578 579 } 580 581 class question_check_specified_fields_expectation { 582 public $expect; 583 public $message; 584 585 function __construct($expected, $message = '') { 586 $this->expect = $expected; 587 $this->message = $message; 588 } 589 } 590 591 592 class question_contains_select_expectation { 593 public $name; 594 public $choices; 595 public $selected; 596 public $enabled; 597 public $message; 598 599 public function __construct($name, $choices, $selected = null, $enabled = null, $message = '') { 600 $this->name = $name; 601 $this->choices = $choices; 602 $this->selected = $selected; 603 $this->enabled = $enabled; 604 $this->message = $message; 605 } 606 } 607 608 609 class question_does_not_contain_tag_with_attributes { 610 public $tag; 611 public $attributes; 612 public $message; 613 614 public function __construct($tag, $attributes, $message = '') { 615 $this->tag = $tag; 616 $this->attributes = $attributes; 617 $this->message = $message; 618 } 619 } 620 621 622 class question_contains_tag_with_attribute { 623 public $tag; 624 public $attribute; 625 public $value; 626 public $message; 627 628 public function __construct($tag, $attribute, $value, $message = '') { 629 $this->tag = $tag; 630 $this->attribute = $attribute; 631 $this->value = $value; 632 $this->message = $message; 633 } 634 } 635 636 637 class question_contains_tag_with_attributes { 638 public $tag; 639 public $expectedvalues = array(); 640 public $forbiddenvalues = array(); 641 public $message; 642 643 public function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '') { 644 $this->tag = $tag; 645 $this->expectedvalues = $expectedvalues; 646 $this->forbiddenvalues = $forbiddenvalues; 647 $this->message = $message; 648 } 649 } 650 651 652 class question_pattern_expectation { 653 public $pattern; 654 public $message; 655 656 public function __construct($pattern, $message = '') { 657 $this->pattern = $pattern; 658 $this->message = $message; 659 } 660 } 661 662 663 class question_no_pattern_expectation { 664 public $pattern; 665 public $message; 666 667 public function __construct($pattern, $message = '') { 668 $this->pattern = $pattern; 669 $this->message = $message; 670 } 671 } 672 673 674 /** 675 * Helper base class for tests that walk a question through a sequents of 676 * interactions under the control of a particular behaviour. 677 * 678 * @copyright 2009 The Open University 679 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 680 */ 681 abstract class qbehaviour_walkthrough_test_base extends question_testcase { 682 /** @var question_display_options */ 683 protected $displayoptions; 684 /** @var question_usage_by_activity */ 685 protected $quba; 686 /** @var integer */ 687 688 protected $slot; 689 /** 690 * @var string after {@link render()} has been called, this contains the 691 * display of the question in its current state. 692 */ 693 protected $currentoutput = ''; 694 695 protected function setUp() { 696 parent::setUp(); 697 $this->resetAfterTest(true); 698 699 $this->displayoptions = new question_display_options(); 700 $this->quba = question_engine::make_questions_usage_by_activity('unit_test', 701 context_system::instance()); 702 } 703 704 protected function tearDown() { 705 $this->displayoptions = null; 706 $this->quba = null; 707 parent::tearDown(); 708 } 709 710 protected function start_attempt_at_question($question, $preferredbehaviour, 711 $maxmark = null, $variant = 1) { 712 $this->quba->set_preferred_behaviour($preferredbehaviour); 713 $this->slot = $this->quba->add_question($question, $maxmark); 714 $this->quba->start_question($this->slot, $variant); 715 } 716 717 /** 718 * Convert an array of data destined for one question to the equivalent POST data. 719 * @param array $data the data for the quetsion. 720 * @return array the complete post data. 721 */ 722 protected function response_data_to_post($data) { 723 $prefix = $this->quba->get_field_prefix($this->slot); 724 $fulldata = array( 725 'slots' => $this->slot, 726 $prefix . ':sequencecheck' => $this->get_question_attempt()->get_sequence_check_count(), 727 ); 728 foreach ($data as $name => $value) { 729 $fulldata[$prefix . $name] = $value; 730 } 731 return $fulldata; 732 } 733 734 protected function process_submission($data) { 735 // Backwards compatibility. 736 reset($data); 737 if (count($data) == 1 && key($data) === '-finish') { 738 $this->finish(); 739 } 740 741 $this->quba->process_all_actions(time(), $this->response_data_to_post($data)); 742 } 743 744 protected function process_autosave($data) { 745 $this->quba->process_all_autosaves(null, $this->response_data_to_post($data)); 746 } 747 748 protected function finish() { 749 $this->quba->finish_all_questions(); 750 } 751 752 protected function manual_grade($comment, $mark, $commentformat = null) { 753 $this->quba->manual_grade($this->slot, $comment, $mark, $commentformat); 754 } 755 756 protected function save_quba(moodle_database $db = null) { 757 question_engine::save_questions_usage_by_activity($this->quba, $db); 758 } 759 760 protected function load_quba(moodle_database $db = null) { 761 $this->quba = question_engine::load_questions_usage_by_activity($this->quba->get_id(), $db); 762 } 763 764 protected function delete_quba() { 765 question_engine::delete_questions_usage_by_activity($this->quba->get_id()); 766 $this->quba = null; 767 } 768 769 protected function check_current_state($state) { 770 $this->assertEquals($state, $this->quba->get_question_state($this->slot), 771 'Questions is in the wrong state.'); 772 } 773 774 protected function check_current_mark($mark) { 775 if (is_null($mark)) { 776 $this->assertNull($this->quba->get_question_mark($this->slot)); 777 } else { 778 if ($mark == 0) { 779 // PHP will think a null mark and a mark of 0 are equal, 780 // so explicity check not null in this case. 781 $this->assertNotNull($this->quba->get_question_mark($this->slot)); 782 } 783 $this->assertEquals($mark, $this->quba->get_question_mark($this->slot), 784 'Expected mark and actual mark differ.', 0.000001); 785 } 786 } 787 788 /** 789 * Generate the HTML rendering of the question in its current state in 790 * $this->currentoutput so that it can be verified. 791 */ 792 protected function render() { 793 $this->currentoutput = $this->quba->render_question($this->slot, $this->displayoptions); 794 } 795 796 protected function check_output_contains_text_input($name, $value = null, $enabled = true) { 797 $attributes = array( 798 'type' => 'text', 799 'name' => $this->quba->get_field_prefix($this->slot) . $name, 800 ); 801 if (!is_null($value)) { 802 $attributes['value'] = $value; 803 } 804 if (!$enabled) { 805 $attributes['readonly'] = 'readonly'; 806 } 807 $matcher = $this->get_tag_matcher('input', $attributes); 808 $this->assertTag($matcher, $this->currentoutput, 809 'Looking for an input with attributes ' . html_writer::attributes($attributes) . ' in ' . $this->currentoutput); 810 811 if ($enabled) { 812 $matcher['attributes']['readonly'] = 'readonly'; 813 $this->assertNotTag($matcher, $this->currentoutput, 814 'input with attributes ' . html_writer::attributes($attributes) . 815 ' should not be read-only in ' . $this->currentoutput); 816 } 817 } 818 819 protected function check_output_contains_text_input_with_class($name, $class = null) { 820 $attributes = array( 821 'type' => 'text', 822 'name' => $this->quba->get_field_prefix($this->slot) . $name, 823 ); 824 if (!is_null($class)) { 825 $attributes['class'] = 'regexp:/\b' . $class . '\b/'; 826 } 827 828 $matcher = $this->get_tag_matcher('input', $attributes); 829 $this->assertTag($matcher, $this->currentoutput, 830 'Looking for an input with attributes ' . html_writer::attributes($attributes) . ' in ' . $this->currentoutput); 831 } 832 833 protected function check_output_does_not_contain_text_input_with_class($name, $class = null) { 834 $attributes = array( 835 'type' => 'text', 836 'name' => $this->quba->get_field_prefix($this->slot) . $name, 837 ); 838 if (!is_null($class)) { 839 $attributes['class'] = 'regexp:/\b' . $class . '\b/'; 840 } 841 842 $matcher = $this->get_tag_matcher('input', $attributes); 843 $this->assertNotTag($matcher, $this->currentoutput, 844 'Unexpected input with attributes ' . html_writer::attributes($attributes) . ' found in ' . $this->currentoutput); 845 } 846 847 protected function check_output_contains_hidden_input($name, $value) { 848 $attributes = array( 849 'type' => 'hidden', 850 'name' => $this->quba->get_field_prefix($this->slot) . $name, 851 'value' => $value, 852 ); 853 $this->assertTag($this->get_tag_matcher('input', $attributes), $this->currentoutput, 854 'Looking for a hidden input with attributes ' . html_writer::attributes($attributes) . ' in ' . $this->currentoutput); 855 } 856 857 protected function check_output_contains($string) { 858 $this->render(); 859 $this->assertContains($string, $this->currentoutput, 860 'Expected string ' . $string . ' not found in ' . $this->currentoutput); 861 } 862 863 protected function check_output_does_not_contain($string) { 864 $this->render(); 865 $this->assertNotContains($string, $this->currentoutput, 866 'String ' . $string . ' unexpectedly found in ' . $this->currentoutput); 867 } 868 869 protected function check_output_contains_lang_string($identifier, $component = '', $a = null) { 870 $this->check_output_contains(get_string($identifier, $component, $a)); 871 } 872 873 protected function get_tag_matcher($tag, $attributes) { 874 return array( 875 'tag' => $tag, 876 'attributes' => $attributes, 877 ); 878 } 879 880 /** 881 * @param $condition one or more Expectations. (users varargs). 882 */ 883 protected function check_current_output() { 884 $html = $this->quba->render_question($this->slot, $this->displayoptions); 885 foreach (func_get_args() as $condition) { 886 $this->assert($condition, $html); 887 } 888 } 889 890 protected function get_question_attempt() { 891 return $this->quba->get_question_attempt($this->slot); 892 } 893 894 protected function get_step_count() { 895 return $this->get_question_attempt()->get_num_steps(); 896 } 897 898 protected function check_step_count($expectednumsteps) { 899 $this->assertEquals($expectednumsteps, $this->get_step_count()); 900 } 901 902 protected function get_step($stepnum) { 903 return $this->get_question_attempt()->get_step($stepnum); 904 } 905 906 protected function get_contains_question_text_expectation($question) { 907 return new question_pattern_expectation('/' . preg_quote($question->questiontext, '/') . '/'); 908 } 909 910 protected function get_contains_general_feedback_expectation($question) { 911 return new question_pattern_expectation('/' . preg_quote($question->generalfeedback, '/') . '/'); 912 } 913 914 protected function get_does_not_contain_correctness_expectation() { 915 return new question_no_pattern_expectation('/class=\"correctness/'); 916 } 917 918 protected function get_contains_correct_expectation() { 919 return new question_pattern_expectation('/' . preg_quote(get_string('correct', 'question'), '/') . '/'); 920 } 921 922 protected function get_contains_partcorrect_expectation() { 923 return new question_pattern_expectation('/' . 924 preg_quote(get_string('partiallycorrect', 'question'), '/') . '/'); 925 } 926 927 protected function get_contains_incorrect_expectation() { 928 return new question_pattern_expectation('/' . preg_quote(get_string('incorrect', 'question'), '/') . '/'); 929 } 930 931 protected function get_contains_standard_correct_combined_feedback_expectation() { 932 return new question_pattern_expectation('/' . 933 preg_quote(test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK, '/') . '/'); 934 } 935 936 protected function get_contains_standard_partiallycorrect_combined_feedback_expectation() { 937 return new question_pattern_expectation('/' . 938 preg_quote(test_question_maker::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK, '/') . '/'); 939 } 940 941 protected function get_contains_standard_incorrect_combined_feedback_expectation() { 942 return new question_pattern_expectation('/' . 943 preg_quote(test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK, '/') . '/'); 944 } 945 946 protected function get_does_not_contain_feedback_expectation() { 947 return new question_no_pattern_expectation('/class="feedback"/'); 948 } 949 950 protected function get_does_not_contain_num_parts_correct() { 951 return new question_no_pattern_expectation('/class="numpartscorrect"/'); 952 } 953 954 protected function get_contains_num_parts_correct($num) { 955 $a = new stdClass(); 956 $a->num = $num; 957 return new question_pattern_expectation('/<div class="numpartscorrect">' . 958 preg_quote(get_string('yougotnright', 'question', $a), '/') . '/'); 959 } 960 961 protected function get_does_not_contain_specific_feedback_expectation() { 962 return new question_no_pattern_expectation('/class="specificfeedback"/'); 963 } 964 965 protected function get_contains_validation_error_expectation() { 966 return new question_contains_tag_with_attribute('div', 'class', 'validationerror'); 967 } 968 969 protected function get_does_not_contain_validation_error_expectation() { 970 return new question_no_pattern_expectation('/class="validationerror"/'); 971 } 972 973 protected function get_contains_mark_summary($mark) { 974 $a = new stdClass(); 975 $a->mark = format_float($mark, $this->displayoptions->markdp); 976 $a->max = format_float($this->quba->get_question_max_mark($this->slot), 977 $this->displayoptions->markdp); 978 return new question_pattern_expectation('/' . 979 preg_quote(get_string('markoutofmax', 'question', $a), '/') . '/'); 980 } 981 982 protected function get_contains_marked_out_of_summary() { 983 $max = format_float($this->quba->get_question_max_mark($this->slot), 984 $this->displayoptions->markdp); 985 return new question_pattern_expectation('/' . 986 preg_quote(get_string('markedoutofmax', 'question', $max), '/') . '/'); 987 } 988 989 protected function get_does_not_contain_mark_summary() { 990 return new question_no_pattern_expectation('/<div class="grade">/'); 991 } 992 993 protected function get_contains_checkbox_expectation($baseattr, $enabled, $checked) { 994 $expectedattributes = $baseattr; 995 $forbiddenattributes = array(); 996 $expectedattributes['type'] = 'checkbox'; 997 if ($enabled === true) { 998 $forbiddenattributes['disabled'] = 'disabled'; 999 } else if ($enabled === false) { 1000 $expectedattributes['disabled'] = 'disabled'; 1001 } 1002 if ($checked === true) { 1003 $expectedattributes['checked'] = 'checked'; 1004 } else if ($checked === false) { 1005 $forbiddenattributes['checked'] = 'checked'; 1006 } 1007 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1008 } 1009 1010 protected function get_contains_mc_checkbox_expectation($index, $enabled = null, 1011 $checked = null) { 1012 return $this->get_contains_checkbox_expectation(array( 1013 'name' => $this->quba->get_field_prefix($this->slot) . $index, 1014 'value' => 1, 1015 ), $enabled, $checked); 1016 } 1017 1018 protected function get_contains_radio_expectation($baseattr, $enabled, $checked) { 1019 $expectedattributes = $baseattr; 1020 $forbiddenattributes = array(); 1021 $expectedattributes['type'] = 'radio'; 1022 if ($enabled === true) { 1023 $forbiddenattributes['disabled'] = 'disabled'; 1024 } else if ($enabled === false) { 1025 $expectedattributes['disabled'] = 'disabled'; 1026 } 1027 if ($checked === true) { 1028 $expectedattributes['checked'] = 'checked'; 1029 } else if ($checked === false) { 1030 $forbiddenattributes['checked'] = 'checked'; 1031 } 1032 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1033 } 1034 1035 protected function get_contains_mc_radio_expectation($index, $enabled = null, $checked = null) { 1036 return $this->get_contains_radio_expectation(array( 1037 'name' => $this->quba->get_field_prefix($this->slot) . 'answer', 1038 'value' => $index, 1039 ), $enabled, $checked); 1040 } 1041 1042 protected function get_contains_hidden_expectation($name, $value = null) { 1043 $expectedattributes = array('type' => 'hidden', 'name' => s($name)); 1044 if (!is_null($value)) { 1045 $expectedattributes['value'] = s($value); 1046 } 1047 return new question_contains_tag_with_attributes('input', $expectedattributes); 1048 } 1049 1050 protected function get_does_not_contain_hidden_expectation($name, $value = null) { 1051 $expectedattributes = array('type' => 'hidden', 'name' => s($name)); 1052 if (!is_null($value)) { 1053 $expectedattributes['value'] = s($value); 1054 } 1055 return new question_does_not_contain_tag_with_attributes('input', $expectedattributes); 1056 } 1057 1058 protected function get_contains_tf_true_radio_expectation($enabled = null, $checked = null) { 1059 return $this->get_contains_radio_expectation(array( 1060 'name' => $this->quba->get_field_prefix($this->slot) . 'answer', 1061 'value' => 1, 1062 ), $enabled, $checked); 1063 } 1064 1065 protected function get_contains_tf_false_radio_expectation($enabled = null, $checked = null) { 1066 return $this->get_contains_radio_expectation(array( 1067 'name' => $this->quba->get_field_prefix($this->slot) . 'answer', 1068 'value' => 0, 1069 ), $enabled, $checked); 1070 } 1071 1072 protected function get_contains_cbm_radio_expectation($certainty, $enabled = null, 1073 $checked = null) { 1074 return $this->get_contains_radio_expectation(array( 1075 'name' => $this->quba->get_field_prefix($this->slot) . '-certainty', 1076 'value' => $certainty, 1077 ), $enabled, $checked); 1078 } 1079 1080 protected function get_contains_button_expectation($name, $value = null, $enabled = null) { 1081 $expectedattributes = array( 1082 'type' => 'submit', 1083 'name' => $name, 1084 ); 1085 $forbiddenattributes = array(); 1086 if (!is_null($value)) { 1087 $expectedattributes['value'] = $value; 1088 } 1089 if ($enabled === true) { 1090 $forbiddenattributes['disabled'] = 'disabled'; 1091 } else if ($enabled === false) { 1092 $expectedattributes['disabled'] = 'disabled'; 1093 } 1094 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1095 } 1096 1097 protected function get_contains_submit_button_expectation($enabled = null) { 1098 return $this->get_contains_button_expectation( 1099 $this->quba->get_field_prefix($this->slot) . '-submit', null, $enabled); 1100 } 1101 1102 protected function get_tries_remaining_expectation($n) { 1103 return new question_pattern_expectation('/' . 1104 preg_quote(get_string('triesremaining', 'qbehaviour_interactive', $n), '/') . '/'); 1105 } 1106 1107 protected function get_invalid_answer_expectation() { 1108 return new question_pattern_expectation('/' . 1109 preg_quote(get_string('invalidanswer', 'question'), '/') . '/'); 1110 } 1111 1112 protected function get_contains_try_again_button_expectation($enabled = null) { 1113 $expectedattributes = array( 1114 'type' => 'submit', 1115 'name' => $this->quba->get_field_prefix($this->slot) . '-tryagain', 1116 ); 1117 $forbiddenattributes = array(); 1118 if ($enabled === true) { 1119 $forbiddenattributes['disabled'] = 'disabled'; 1120 } else if ($enabled === false) { 1121 $expectedattributes['disabled'] = 'disabled'; 1122 } 1123 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1124 } 1125 1126 protected function get_does_not_contain_try_again_button_expectation() { 1127 return new question_no_pattern_expectation('/name="' . 1128 $this->quba->get_field_prefix($this->slot) . '-tryagain"/'); 1129 } 1130 1131 protected function get_contains_select_expectation($name, $choices, 1132 $selected = null, $enabled = null) { 1133 $fullname = $this->quba->get_field_prefix($this->slot) . $name; 1134 return new question_contains_select_expectation($fullname, $choices, $selected, $enabled); 1135 } 1136 1137 protected function get_mc_right_answer_index($mc) { 1138 $order = $mc->get_order($this->get_question_attempt()); 1139 foreach ($order as $i => $ansid) { 1140 if ($mc->answers[$ansid]->fraction == 1) { 1141 return $i; 1142 } 1143 } 1144 $this->fail('This multiple choice question does not seem to have a right answer!'); 1145 } 1146 1147 protected function get_no_hint_visible_expectation() { 1148 return new question_no_pattern_expectation('/class="hint"/'); 1149 } 1150 1151 protected function get_contains_hint_expectation($hinttext) { 1152 // Does not currently verify hint text. 1153 return new question_contains_tag_with_attribute('div', 'class', 'hint'); 1154 } 1155 } 1156 1157 /** 1158 * Simple class that implements the {@link moodle_recordset} API based on an 1159 * array of test data. 1160 * 1161 * See the {@link question_attempt_step_db_test} class in 1162 * question/engine/tests/testquestionattemptstep.php for an example of how 1163 * this is used. 1164 * 1165 * @copyright 2011 The Open University 1166 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1167 */ 1168 class question_test_recordset extends moodle_recordset { 1169 protected $records; 1170 1171 /** 1172 * Constructor 1173 * @param $table as for {@link testing_db_record_builder::build_db_records()} 1174 * but does not need a unique first column. 1175 */ 1176 public function __construct(array $table) { 1177 $columns = array_shift($table); 1178 $this->records = array(); 1179 foreach ($table as $row) { 1180 if (count($row) != count($columns)) { 1181 throw new coding_exception("Row contains the wrong number of fields."); 1182 } 1183 $rec = array(); 1184 foreach ($columns as $i => $name) { 1185 $rec[$name] = $row[$i]; 1186 } 1187 $this->records[] = $rec; 1188 } 1189 reset($this->records); 1190 } 1191 1192 public function __destruct() { 1193 $this->close(); 1194 } 1195 1196 public function current() { 1197 return (object) current($this->records); 1198 } 1199 1200 public function key() { 1201 if (is_null(key($this->records))) { 1202 return false; 1203 } 1204 $current = current($this->records); 1205 return reset($current); 1206 } 1207 1208 public function next() { 1209 next($this->records); 1210 } 1211 1212 public function valid() { 1213 return !is_null(key($this->records)); 1214 } 1215 1216 public function close() { 1217 $this->records = null; 1218 } 1219 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |