PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/question/engine/tests/helpers.php

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