[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/engine/tests/ -> helpers.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1