Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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   * Unit tests for the Moodle XML format.
  19   *
  20   * @package    qformat_xml
  21   * @copyright  2010 The Open University
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  require_once($CFG->libdir . '/questionlib.php');
  30  require_once($CFG->dirroot . '/question/format/xml/format.php');
  31  require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  32  
  33  
  34  /**
  35   * Unit tests for the matching question definition class.
  36   *
  37   * @copyright  2009 The Open University
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class qformat_xml_test extends question_testcase {
  41      public function assert_same_xml($expectedxml, $xml) {
  42          $this->assertEquals(str_replace("\r\n", "\n", $expectedxml),
  43                  str_replace("\r\n", "\n", $xml));
  44      }
  45  
  46      public function make_test_question() {
  47          global $USER;
  48          $q = new stdClass();
  49          $q->id = 0;
  50          $q->contextid = 0;
  51          $q->idnumber = null;
  52          $q->category = 0;
  53          $q->parent = 0;
  54          $q->questiontextformat = FORMAT_HTML;
  55          $q->generalfeedbackformat = FORMAT_HTML;
  56          $q->defaultmark = 1;
  57          $q->penalty = 0.3333333;
  58          $q->length = 1;
  59          $q->stamp = make_unique_id_code();
  60          $q->version = make_unique_id_code();
  61          $q->hidden = 0;
  62          $q->timecreated = time();
  63          $q->timemodified = time();
  64          $q->createdby = $USER->id;
  65          $q->modifiedby = $USER->id;
  66          return $q;
  67      }
  68  
  69      /**
  70       * The data the XML import format sends to save_question is not exactly
  71       * the same as the data returned from the editing form, so this method
  72       * makes necessary changes to the return value of
  73       * test_question_maker::get_question_form_data so that the tests can work.
  74       * @param object $expectedq as returned by get_question_form_data.
  75       * @return object one more likely to match the return value of import_...().
  76       */
  77      public function remove_irrelevant_form_data_fields($expectedq) {
  78          return $this->itemid_to_files($expectedq);
  79      }
  80  
  81      /**
  82       * Becuase XML import uses a files array instead of an itemid integer to
  83       * handle saving files with a question, we need to covert the output of
  84       * test_question_maker::get_question_form_data to match. This method recursively
  85       * replaces all array elements with key itemid with an array entry with
  86       * key files and value an empty array.
  87       *
  88       * @param mixed $var any data structure.
  89       * @return mixed an equivalent structure with the relacements made.
  90       */
  91      protected function itemid_to_files($var) {
  92          if (is_object($var)) {
  93              $newvar = new stdClass();
  94              foreach (get_object_vars($var) as $field => $value) {
  95                  $newvar->$field = $this->itemid_to_files($value);
  96              }
  97  
  98          } else if (is_array($var)) {
  99              $newvar = array();
 100              foreach ($var as $index => $value) {
 101                  if ($index === 'itemid') {
 102                      $newvar['files'] = array();
 103                  } else {
 104                      $newvar[$index] = $this->itemid_to_files($value);
 105                  }
 106              }
 107  
 108          } else {
 109              $newvar = $var;
 110          }
 111  
 112          return $newvar;
 113      }
 114  
 115      public function test_xml_escape_simple_input_not_escaped() {
 116          $exporter = new qformat_xml();
 117          $string = 'Nothing funny here. Even if we go to a café or to 日本.';
 118          $this->assertEquals($string, $exporter->xml_escape($string));
 119      }
 120  
 121      public function test_xml_escape_html_wrapped_in_cdata() {
 122          $exporter = new qformat_xml();
 123          $string = '<p>Nothing <b>funny<b> here. Even if we go to a café or to 日本.</p>';
 124          $this->assertEquals('<![CDATA[' . $string . ']]>', $exporter->xml_escape($string));
 125      }
 126  
 127      public function test_xml_escape_script_tag_handled_ok() {
 128          $exporter = new qformat_xml();
 129          $input = '<script><![CDATA[alert(1<2);]]></script>';
 130          $expected = '<![CDATA[<script><![CDATA[alert(1<2);]]]]><![CDATA[></script>]]>';
 131          $this->assertEquals($expected, $exporter->xml_escape($input));
 132  
 133          // Check that parsing the expected result does give the input again.
 134          $parsed = simplexml_load_string('<div>' . $expected . '</div>');
 135          $this->assertEquals($input, $parsed->xpath('//div')[0]);
 136      }
 137  
 138      public function test_xml_escape_code_that_looks_like_cdata_end_ok() {
 139          $exporter = new qformat_xml();
 140          $input = "if (x[[0]]>a) print('hah');";
 141          $expected = "<![CDATA[if (x[[0]]]]><![CDATA[>a) print('hah');]]>";
 142          $this->assertEquals($expected, $exporter->xml_escape($input));
 143  
 144          // Check that parsing the expected result does give the input again.
 145          $parsed = simplexml_load_string('<div>' . $expected . '</div>');
 146          $this->assertEquals($input, $parsed->xpath('//div')[0]);
 147      }
 148  
 149      public function test_write_hint_basic() {
 150          $q = $this->make_test_question();
 151          $q->contextid = \context_system::instance()->id;
 152          $q->name = 'Short answer question';
 153          $q->questiontext = 'Name an amphibian: __________';
 154          $q->generalfeedback = 'Generalfeedback: frog or toad would have been OK.';
 155          if (!isset($q->options)) {
 156              $q->options = new stdClass();
 157          }
 158          $q->options->usecase = false;
 159          $q->options->answers = array(
 160              13 => new question_answer(13, 'frog', 1.0, 'Frog is a very good answer.', FORMAT_HTML),
 161              14 => new question_answer(14, 'toad', 0.8, 'Toad is an OK good answer.', FORMAT_HTML),
 162              15 => new question_answer(15, '*', 0.0, 'That is a bad answer.', FORMAT_HTML),
 163          );
 164          $q->qtype = 'shortanswer';
 165          $q->hints = array(
 166              new question_hint(0, 'This is the first hint.', FORMAT_MOODLE),
 167          );
 168  
 169          $exporter = new qformat_xml();
 170          $xml = $exporter->writequestion($q);
 171  
 172          $this->assertRegExp('|<hint format=\"moodle_auto_format\">\s*<text>\s*' .
 173                  'This is the first hint\.\s*</text>\s*</hint>|', $xml);
 174          $this->assertNotRegExp('|<shownumcorrect/>|', $xml);
 175          $this->assertNotRegExp('|<clearwrong/>|', $xml);
 176          $this->assertNotRegExp('|<options>|', $xml);
 177      }
 178  
 179      public function test_write_hint_with_parts() {
 180          $q = $this->make_test_question();
 181          $q->contextid = \context_system::instance()->id;
 182          $q->name = 'Matching question';
 183          $q->questiontext = 'Classify the animals.';
 184          $q->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.';
 185          $q->qtype = 'match';
 186  
 187          if (!isset($q->options)) {
 188              $q->options = new stdClass();
 189          }
 190          $q->options->shuffleanswers = 1;
 191          $q->options->correctfeedback = '';
 192          $q->options->correctfeedbackformat = FORMAT_HTML;
 193          $q->options->partiallycorrectfeedback = '';
 194          $q->options->partiallycorrectfeedbackformat = FORMAT_HTML;
 195          $q->options->incorrectfeedback = '';
 196          $q->options->incorrectfeedbackformat = FORMAT_HTML;
 197  
 198          $q->options->subquestions = array();
 199          $q->hints = array(
 200              new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, true),
 201              new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, false),
 202          );
 203  
 204          $exporter = new qformat_xml();
 205          $xml = $exporter->writequestion($q);
 206  
 207          $this->assertRegExp(
 208                  '|<hint format=\"html\">\s*<text>\s*This is the first hint\.\s*</text>|', $xml);
 209          $this->assertRegExp(
 210                  '|<hint format=\"html\">\s*<text>\s*This is the second hint\.\s*</text>|', $xml);
 211          list($ignored, $hint1, $hint2) = explode('<hint', $xml);
 212          $this->assertNotRegExp('|<shownumcorrect/>|', $hint1);
 213          $this->assertRegExp('|<clearwrong/>|', $hint1);
 214          $this->assertRegExp('|<shownumcorrect/>|', $hint2);
 215          $this->assertNotRegExp('|<clearwrong/>|', $hint2);
 216          $this->assertNotRegExp('|<options>|', $xml);
 217      }
 218  
 219      public function test_import_hints_no_parts() {
 220          $xml = <<<END
 221  <question>
 222      <hint>
 223          <text>This is the first hint</text>
 224          <clearwrong/>
 225      </hint>
 226      <hint>
 227          <text>This is the second hint</text>
 228          <shownumcorrect/>
 229      </hint>
 230  </question>
 231  END;
 232  
 233          $questionxml = xmlize($xml);
 234          $qo = new stdClass();
 235  
 236          $importer = new qformat_xml();
 237          $importer->import_hints($qo, $questionxml['question'], false, false, 'html');
 238  
 239          $this->assertEquals(array(
 240                  array('text' => 'This is the first hint',
 241                          'format' => FORMAT_HTML),
 242                  array('text' => 'This is the second hint',
 243                          'format' => FORMAT_HTML),
 244                  ), $qo->hint);
 245          $this->assertFalse(isset($qo->hintclearwrong));
 246          $this->assertFalse(isset($qo->hintshownumcorrect));
 247      }
 248  
 249      public function test_import_hints_with_parts() {
 250          $xml = <<<END
 251  <question>
 252      <hint>
 253          <text>This is the first hint</text>
 254          <clearwrong/>
 255      </hint>
 256      <hint>
 257          <text>This is the second hint</text>
 258          <shownumcorrect/>
 259      </hint>
 260  </question>
 261  END;
 262  
 263          $questionxml = xmlize($xml);
 264          $qo = new stdClass();
 265  
 266          $importer = new qformat_xml();
 267          $importer->import_hints($qo, $questionxml['question'], true, true, 'html');
 268  
 269          $this->assertEquals(array(
 270                  array('text' => 'This is the first hint',
 271                          'format' => FORMAT_HTML),
 272                  array('text' => 'This is the second hint',
 273                          'format' => FORMAT_HTML),
 274                  ), $qo->hint);
 275          $this->assertEquals(array(1, 0), $qo->hintclearwrong);
 276          $this->assertEquals(array(0, 1), $qo->hintshownumcorrect);
 277      }
 278  
 279      public function test_import_no_hints_no_error() {
 280          $xml = <<<END
 281  <question>
 282  </question>
 283  END;
 284  
 285          $questionxml = xmlize($xml);
 286          $qo = new stdClass();
 287  
 288          $importer = new qformat_xml();
 289          $importer->import_hints($qo, $questionxml['question'], 'html');
 290  
 291          $this->assertFalse(isset($qo->hint));
 292      }
 293  
 294      public function test_import_description() {
 295          $xml = '  <question type="description">
 296      <name>
 297        <text>A description</text>
 298      </name>
 299      <questiontext format="html">
 300        <text>The question text.</text>
 301      </questiontext>
 302      <generalfeedback>
 303        <text>Here is some general feedback.</text>
 304      </generalfeedback>
 305      <defaultgrade>0</defaultgrade>
 306      <penalty>0</penalty>
 307      <hidden>0</hidden>
 308      <tags>
 309        <tag><text>tagDescription</text></tag>
 310        <tag><text>tagTest</text></tag>
 311      </tags>
 312    </question>';
 313          $xmldata = xmlize($xml);
 314  
 315          $importer = new qformat_xml();
 316          $q = $importer->import_description($xmldata['question']);
 317  
 318          $expectedq = new stdClass();
 319          $expectedq->qtype = 'description';
 320          $expectedq->name = 'A description';
 321          $expectedq->questiontext = 'The question text.';
 322          $expectedq->questiontextformat = FORMAT_HTML;
 323          $expectedq->generalfeedback = 'Here is some general feedback.';
 324          $expectedq->defaultmark = 0;
 325          $expectedq->length = 0;
 326          $expectedq->penalty = 0;
 327          $expectedq->tags = array('tagDescription', 'tagTest');
 328  
 329          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
 330      }
 331  
 332      public function test_export_description() {
 333          $qdata = new stdClass();
 334          $qdata->id = 123;
 335          $qdata->contextid = \context_system::instance()->id;
 336          $qdata->qtype = 'description';
 337          $qdata->name = 'A description';
 338          $qdata->questiontext = 'The question text.';
 339          $qdata->questiontextformat = FORMAT_HTML;
 340          $qdata->generalfeedback = 'Here is some general feedback.';
 341          $qdata->generalfeedbackformat = FORMAT_HTML;
 342          $qdata->defaultmark = 0;
 343          $qdata->length = 0;
 344          $qdata->penalty = 0;
 345          $qdata->hidden = 0;
 346          $qdata->idnumber = null;
 347  
 348          $exporter = new qformat_xml();
 349          $xml = $exporter->writequestion($qdata);
 350  
 351          $expectedxml = '<!-- question: 123  -->
 352    <question type="description">
 353      <name>
 354        <text>A description</text>
 355      </name>
 356      <questiontext format="html">
 357        <text>The question text.</text>
 358      </questiontext>
 359      <generalfeedback format="html">
 360        <text>Here is some general feedback.</text>
 361      </generalfeedback>
 362      <defaultgrade>0</defaultgrade>
 363      <penalty>0</penalty>
 364      <hidden>0</hidden>
 365      <idnumber></idnumber>
 366    </question>
 367  ';
 368  
 369          $this->assert_same_xml($expectedxml, $xml);
 370      }
 371  
 372      public function test_import_essay_20() {
 373          $xml = '  <question type="essay">
 374      <name>
 375        <text>An essay</text>
 376      </name>
 377      <questiontext format="moodle_auto_format">
 378        <text>Write something.</text>
 379      </questiontext>
 380      <generalfeedback>
 381        <text>I hope you wrote something interesting.</text>
 382      </generalfeedback>
 383      <defaultgrade>1</defaultgrade>
 384      <penalty>0</penalty>
 385      <hidden>0</hidden>
 386      <tags>
 387        <tag><text>tagEssay</text></tag>
 388        <tag><text>tagEssay20</text></tag>
 389        <tag><text>tagTest</text></tag>
 390      </tags>
 391    </question>';
 392          $xmldata = xmlize($xml);
 393  
 394          $importer = new qformat_xml();
 395          $q = $importer->import_essay($xmldata['question']);
 396  
 397          $expectedq = new stdClass();
 398          $expectedq->qtype = 'essay';
 399          $expectedq->name = 'An essay';
 400          $expectedq->questiontext = 'Write something.';
 401          $expectedq->questiontextformat = FORMAT_MOODLE;
 402          $expectedq->generalfeedback = 'I hope you wrote something interesting.';
 403          $expectedq->defaultmark = 1;
 404          $expectedq->length = 1;
 405          $expectedq->penalty = 0;
 406          $expectedq->responseformat = 'editor';
 407          $expectedq->responserequired = 1;
 408          $expectedq->responsefieldlines = 15;
 409          $expectedq->attachments = 0;
 410          $expectedq->attachmentsrequired = 0;
 411          $expectedq->filetypeslist = null;
 412          $expectedq->graderinfo['text'] = '';
 413          $expectedq->graderinfo['format'] = FORMAT_MOODLE;
 414          $expectedq->responsetemplate['text'] = '';
 415          $expectedq->responsetemplate['format'] = FORMAT_MOODLE;
 416          $expectedq->tags = array('tagEssay', 'tagEssay20', 'tagTest');
 417  
 418          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
 419      }
 420  
 421      public function test_import_essay_21() {
 422          $xml = '  <question type="essay">
 423      <name>
 424        <text>An essay</text>
 425      </name>
 426      <questiontext format="moodle_auto_format">
 427        <text>Write something.</text>
 428      </questiontext>
 429      <generalfeedback>
 430        <text>I hope you wrote something interesting.</text>
 431      </generalfeedback>
 432      <defaultgrade>1</defaultgrade>
 433      <penalty>0</penalty>
 434      <hidden>0</hidden>
 435      <responseformat>monospaced</responseformat>
 436      <responserequired>0</responserequired>
 437      <responsefieldlines>42</responsefieldlines>
 438      <attachments>-1</attachments>
 439      <attachmentsrequired>1</attachmentsrequired>
 440      <graderinfo format="html">
 441          <text><![CDATA[<p>Grade <b>generously</b>!</p>]]></text>
 442      </graderinfo>
 443      <responsetemplate format="html">
 444          <text><![CDATA[<p>Here is something <b>really</b> interesting.</p>]]></text>
 445      </responsetemplate>
 446      <tags>
 447        <tag><text>tagEssay</text></tag>
 448        <tag><text>tagEssay21</text></tag>
 449        <tag><text>tagTest</text></tag>
 450      </tags>
 451    </question>';
 452          $xmldata = xmlize($xml);
 453  
 454          $importer = new qformat_xml();
 455          $q = $importer->import_essay($xmldata['question']);
 456  
 457          $expectedq = new stdClass();
 458          $expectedq->qtype = 'essay';
 459          $expectedq->name = 'An essay';
 460          $expectedq->questiontext = 'Write something.';
 461          $expectedq->questiontextformat = FORMAT_MOODLE;
 462          $expectedq->generalfeedback = 'I hope you wrote something interesting.';
 463          $expectedq->defaultmark = 1;
 464          $expectedq->length = 1;
 465          $expectedq->penalty = 0;
 466          $expectedq->responseformat = 'monospaced';
 467          $expectedq->responserequired = 0;
 468          $expectedq->responsefieldlines = 42;
 469          $expectedq->attachments = -1;
 470          $expectedq->attachmentsrequired = 1;
 471          $expectedq->filetypeslist = null;
 472          $expectedq->graderinfo['text'] = '<p>Grade <b>generously</b>!</p>';
 473          $expectedq->graderinfo['format'] = FORMAT_HTML;
 474          $expectedq->responsetemplate['text'] = '<p>Here is something <b>really</b> interesting.</p>';
 475          $expectedq->responsetemplate['format'] = FORMAT_HTML;
 476          $expectedq->tags = array('tagEssay', 'tagEssay21', 'tagTest');
 477  
 478          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
 479      }
 480  
 481      public function test_export_essay() {
 482          $qdata = new stdClass();
 483          $qdata->id = 123;
 484          $qdata->contextid = \context_system::instance()->id;
 485          $qdata->qtype = 'essay';
 486          $qdata->name = 'An essay';
 487          $qdata->questiontext = 'Write something.';
 488          $qdata->questiontextformat = FORMAT_MOODLE;
 489          $qdata->generalfeedback = 'I hope you wrote something interesting.';
 490          $qdata->generalfeedbackformat = FORMAT_MOODLE;
 491          $qdata->defaultmark = 1;
 492          $qdata->length = 1;
 493          $qdata->penalty = 0;
 494          $qdata->hidden = 0;
 495          $qdata->idnumber = null;
 496          $qdata->options = new stdClass();
 497          $qdata->options->id = 456;
 498          $qdata->options->questionid = 123;
 499          $qdata->options->responseformat = 'monospaced';
 500          $qdata->options->responserequired = 0;
 501          $qdata->options->responsefieldlines = 42;
 502          $qdata->options->attachments = -1;
 503          $qdata->options->attachmentsrequired = 1;
 504          $qdata->options->graderinfo = '<p>Grade <b>generously</b>!</p>';
 505          $qdata->options->graderinfoformat = FORMAT_HTML;
 506          $qdata->options->responsetemplate = '<p>Here is something <b>really</b> interesting.</p>';
 507          $qdata->options->responsetemplateformat = FORMAT_HTML;
 508          $qdata->options->filetypeslist = '.pdf,.zip.,.docx';
 509          $exporter = new qformat_xml();
 510          $xml = $exporter->writequestion($qdata);
 511  
 512          $expectedxml = '<!-- question: 123  -->
 513    <question type="essay">
 514      <name>
 515        <text>An essay</text>
 516      </name>
 517      <questiontext format="moodle_auto_format">
 518        <text>Write something.</text>
 519      </questiontext>
 520      <generalfeedback format="moodle_auto_format">
 521        <text>I hope you wrote something interesting.</text>
 522      </generalfeedback>
 523      <defaultgrade>1</defaultgrade>
 524      <penalty>0</penalty>
 525      <hidden>0</hidden>
 526      <idnumber></idnumber>
 527      <responseformat>monospaced</responseformat>
 528      <responserequired>0</responserequired>
 529      <responsefieldlines>42</responsefieldlines>
 530      <attachments>-1</attachments>
 531      <attachmentsrequired>1</attachmentsrequired>
 532      <filetypeslist>.pdf,.zip.,.docx</filetypeslist>
 533      <graderinfo format="html">
 534        <text><![CDATA[<p>Grade <b>generously</b>!</p>]]></text>
 535      </graderinfo>
 536      <responsetemplate format="html">
 537        <text><![CDATA[<p>Here is something <b>really</b> interesting.</p>]]></text>
 538      </responsetemplate>
 539    </question>
 540  ';
 541  
 542          $this->assert_same_xml($expectedxml, $xml);
 543      }
 544  
 545      public function test_import_match_19() {
 546          $xml = '  <question type="matching">
 547      <name>
 548        <text>Matching question</text>
 549      </name>
 550      <questiontext format="html">
 551        <text>Match the upper and lower case letters.</text>
 552      </questiontext>
 553      <generalfeedback>
 554        <text>The answer is A -> a, B -> b and C -> c.</text>
 555      </generalfeedback>
 556      <defaultgrade>1</defaultgrade>
 557      <penalty>0.3333333</penalty>
 558      <hidden>0</hidden>
 559      <shuffleanswers>false</shuffleanswers>
 560      <correctfeedback>
 561        <text>Well done.</text>
 562      </correctfeedback>
 563      <partiallycorrectfeedback>
 564        <text>Not entirely.</text>
 565      </partiallycorrectfeedback>
 566      <incorrectfeedback>
 567        <text>Completely wrong!</text>
 568      </incorrectfeedback>
 569      <subquestion>
 570        <text>A</text>
 571        <answer>
 572          <text>a</text>
 573        </answer>
 574      </subquestion>
 575      <subquestion>
 576        <text>B</text>
 577        <answer>
 578          <text>b</text>
 579        </answer>
 580      </subquestion>
 581      <subquestion>
 582        <text>C</text>
 583        <answer>
 584          <text>c</text>
 585        </answer>
 586      </subquestion>
 587      <subquestion>
 588        <text></text>
 589        <answer>
 590          <text>d</text>
 591        </answer>
 592      </subquestion>
 593      <hint>
 594        <text>Hint 1</text>
 595        <shownumcorrect />
 596      </hint>
 597      <hint>
 598        <text></text>
 599        <shownumcorrect />
 600        <clearwrong />
 601      </hint>
 602      <tags>
 603        <tag><text>tagMatching</text></tag>
 604        <tag><text>tagTest</text></tag>
 605      </tags>
 606    </question>';
 607          $xmldata = xmlize($xml);
 608  
 609          $importer = new qformat_xml();
 610          $q = $importer->import_match($xmldata['question']);
 611  
 612          $expectedq = new stdClass();
 613          $expectedq->qtype = 'match';
 614          $expectedq->name = 'Matching question';
 615          $expectedq->questiontext = 'Match the upper and lower case letters.';
 616          $expectedq->questiontextformat = FORMAT_HTML;
 617          $expectedq->correctfeedback = array('text' => 'Well done.',
 618                  'format' => FORMAT_HTML);
 619          $expectedq->partiallycorrectfeedback = array('text' => 'Not entirely.',
 620                  'format' => FORMAT_HTML);
 621          $expectedq->shownumcorrect = false;
 622          $expectedq->incorrectfeedback = array('text' => 'Completely wrong!',
 623                  'format' => FORMAT_HTML);
 624          $expectedq->generalfeedback = 'The answer is A -> a, B -> b and C -> c.';
 625          $expectedq->generalfeedbackformat = FORMAT_HTML;
 626          $expectedq->defaultmark = 1;
 627          $expectedq->length = 1;
 628          $expectedq->penalty = 0.3333333;
 629          $expectedq->shuffleanswers = 0;
 630          $expectedq->subquestions = array(
 631              array('text' => 'A', 'format' => FORMAT_HTML),
 632              array('text' => 'B', 'format' => FORMAT_HTML),
 633              array('text' => 'C', 'format' => FORMAT_HTML),
 634              array('text' => '', 'format' => FORMAT_HTML));
 635          $expectedq->subanswers = array('a', 'b', 'c', 'd');
 636          $expectedq->hint = array(
 637              array('text' => 'Hint 1', 'format' => FORMAT_HTML),
 638              array('text' => '', 'format' => FORMAT_HTML),
 639          );
 640          $expectedq->hintshownumcorrect = array(true, true);
 641          $expectedq->hintclearwrong = array(false, true);
 642          $expectedq->tags = array('tagMatching', 'tagTest');
 643  
 644          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
 645      }
 646  
 647      public function test_export_match() {
 648          $qdata = new stdClass();
 649          $qdata->id = 123;
 650          $qdata->contextid = \context_system::instance()->id;
 651          $qdata->qtype = 'match';
 652          $qdata->name = 'Matching question';
 653          $qdata->questiontext = 'Match the upper and lower case letters.';
 654          $qdata->questiontextformat = FORMAT_HTML;
 655          $qdata->generalfeedback = 'The answer is A -> a, B -> b and C -> c.';
 656          $qdata->generalfeedbackformat = FORMAT_HTML;
 657          $qdata->defaultmark = 1;
 658          $qdata->length = 1;
 659          $qdata->penalty = 0.3333333;
 660          $qdata->hidden = 0;
 661          $qdata->idnumber = null;
 662  
 663          $qdata->options = new stdClass();
 664          $qdata->options->shuffleanswers = 1;
 665          $qdata->options->correctfeedback = 'Well done.';
 666          $qdata->options->correctfeedbackformat = FORMAT_HTML;
 667          $qdata->options->partiallycorrectfeedback = 'Not entirely.';
 668          $qdata->options->partiallycorrectfeedbackformat = FORMAT_HTML;
 669          $qdata->options->shownumcorrect = false;
 670          $qdata->options->incorrectfeedback = 'Completely wrong!';
 671          $qdata->options->incorrectfeedbackformat = FORMAT_HTML;
 672  
 673          $subq1 = new stdClass();
 674          $subq1->id = -4;
 675          $subq1->questiontext = 'A';
 676          $subq1->questiontextformat = FORMAT_HTML;
 677          $subq1->answertext = 'a';
 678  
 679          $subq2 = new stdClass();
 680          $subq2->id = -3;
 681          $subq2->questiontext = 'B';
 682          $subq2->questiontextformat = FORMAT_HTML;
 683          $subq2->answertext = 'b';
 684  
 685          $subq3 = new stdClass();
 686          $subq3->id = -2;
 687          $subq3->questiontext = 'C';
 688          $subq3->questiontextformat = FORMAT_HTML;
 689          $subq3->answertext = 'c';
 690  
 691          $subq4 = new stdClass();
 692          $subq4->id = -1;
 693          $subq4->questiontext = '';
 694          $subq4->questiontextformat = FORMAT_HTML;
 695          $subq4->answertext = 'd';
 696  
 697          $qdata->options->subquestions = array(
 698                  $subq1, $subq2, $subq3, $subq4);
 699  
 700          $qdata->hints = array(
 701              new question_hint_with_parts(0, 'Hint 1', FORMAT_HTML, true, false),
 702              new question_hint_with_parts(0, '', FORMAT_HTML, true, true),
 703          );
 704  
 705          $exporter = new qformat_xml();
 706          $xml = $exporter->writequestion($qdata);
 707  
 708          $expectedxml = '<!-- question: 123  -->
 709    <question type="matching">
 710      <name>
 711        <text>Matching question</text>
 712      </name>
 713      <questiontext format="html">
 714        <text>Match the upper and lower case letters.</text>
 715      </questiontext>
 716      <generalfeedback format="html">
 717        <text><![CDATA[The answer is A -> a, B -> b and C -> c.]]></text>
 718      </generalfeedback>
 719      <defaultgrade>1</defaultgrade>
 720      <penalty>0.3333333</penalty>
 721      <hidden>0</hidden>
 722      <idnumber></idnumber>
 723      <shuffleanswers>true</shuffleanswers>
 724      <correctfeedback format="html">
 725        <text>Well done.</text>
 726      </correctfeedback>
 727      <partiallycorrectfeedback format="html">
 728        <text>Not entirely.</text>
 729      </partiallycorrectfeedback>
 730      <incorrectfeedback format="html">
 731        <text>Completely wrong!</text>
 732      </incorrectfeedback>
 733      <subquestion format="html">
 734        <text>A</text>
 735        <answer>
 736          <text>a</text>
 737        </answer>
 738      </subquestion>
 739      <subquestion format="html">
 740        <text>B</text>
 741        <answer>
 742          <text>b</text>
 743        </answer>
 744      </subquestion>
 745      <subquestion format="html">
 746        <text>C</text>
 747        <answer>
 748          <text>c</text>
 749        </answer>
 750      </subquestion>
 751      <subquestion format="html">
 752        <text></text>
 753        <answer>
 754          <text>d</text>
 755        </answer>
 756      </subquestion>
 757      <hint format="html">
 758        <text>Hint 1</text>
 759        <shownumcorrect/>
 760      </hint>
 761      <hint format="html">
 762        <text></text>
 763        <shownumcorrect/>
 764        <clearwrong/>
 765      </hint>
 766    </question>
 767  ';
 768  
 769          $this->assert_same_xml($expectedxml, $xml);
 770      }
 771  
 772      public function test_import_multichoice_19() {
 773          $xml = '  <question type="multichoice">
 774      <name>
 775        <text>Multiple choice question</text>
 776      </name>
 777      <questiontext format="html">
 778        <text>Which are the even numbers?</text>
 779      </questiontext>
 780      <generalfeedback>
 781        <text>The even numbers are 2 and 4.</text>
 782      </generalfeedback>
 783      <defaultgrade>2</defaultgrade>
 784      <penalty>0.3333333</penalty>
 785      <hidden>0</hidden>
 786      <single>false</single>
 787      <shuffleanswers>false</shuffleanswers>
 788      <answernumbering>abc</answernumbering>
 789      <correctfeedback>
 790        <text><![CDATA[<p>Your answer is correct.</p>]]></text>
 791      </correctfeedback>
 792      <partiallycorrectfeedback>
 793        <text><![CDATA[<p>Your answer is partially correct.</p>]]></text>
 794      </partiallycorrectfeedback>
 795      <incorrectfeedback>
 796        <text><![CDATA[<p>Your answer is incorrect.</p>]]></text>
 797      </incorrectfeedback>
 798      <shownumcorrect/>
 799      <answer fraction="0">
 800        <text>1</text>
 801        <feedback>
 802          <text></text>
 803        </feedback>
 804      </answer>
 805      <answer fraction="100">
 806        <text>2</text>
 807        <feedback>
 808          <text></text>
 809        </feedback>
 810      </answer>
 811      <answer fraction="0">
 812        <text>3</text>
 813        <feedback>
 814          <text></text>
 815        </feedback>
 816      </answer>
 817      <answer fraction="100">
 818        <text>4</text>
 819        <feedback>
 820          <text></text>
 821        </feedback>
 822      </answer>
 823      <hint>
 824        <text>Hint 1.</text>
 825      </hint>
 826      <hint>
 827        <text>Hint 2.</text>
 828      </hint>
 829    </question>';
 830          $xmldata = xmlize($xml);
 831  
 832          $importer = new qformat_xml();
 833          $q = $importer->import_multichoice($xmldata['question']);
 834  
 835          $expectedq = new stdClass();
 836          $expectedq->qtype = 'multichoice';
 837          $expectedq->name = 'Multiple choice question';
 838          $expectedq->questiontext = 'Which are the even numbers?';
 839          $expectedq->questiontextformat = FORMAT_HTML;
 840          $expectedq->correctfeedback = array(
 841                  'text'   => '<p>Your answer is correct.</p>',
 842                  'format' => FORMAT_HTML);
 843          $expectedq->shownumcorrect = false;
 844          $expectedq->partiallycorrectfeedback = array(
 845                  'text'   => '<p>Your answer is partially correct.</p>',
 846                  'format' => FORMAT_HTML);
 847          $expectedq->shownumcorrect = true;
 848          $expectedq->incorrectfeedback = array(
 849                  'text'   => '<p>Your answer is incorrect.</p>',
 850                  'format' => FORMAT_HTML);
 851          $expectedq->generalfeedback = 'The even numbers are 2 and 4.';
 852          $expectedq->defaultmark = 2;
 853          $expectedq->length = 1;
 854          $expectedq->penalty = 0.3333333;
 855          $expectedq->shuffleanswers = 0;
 856          $expectedq->single = false;
 857  
 858          $expectedq->answer = array(
 859              array('text' => '1', 'format' => FORMAT_HTML),
 860              array('text' => '2', 'format' => FORMAT_HTML),
 861              array('text' => '3', 'format' => FORMAT_HTML),
 862              array('text' => '4', 'format' => FORMAT_HTML));
 863          $expectedq->fraction = array(0, 1, 0, 1);
 864          $expectedq->feedback = array(
 865              array('text' => '', 'format' => FORMAT_HTML),
 866              array('text' => '', 'format' => FORMAT_HTML),
 867              array('text' => '', 'format' => FORMAT_HTML),
 868              array('text' => '', 'format' => FORMAT_HTML));
 869  
 870          $expectedq->hint = array(
 871              array('text' => 'Hint 1.', 'format' => FORMAT_HTML),
 872              array('text' => 'Hint 2.', 'format' => FORMAT_HTML),
 873          );
 874          $expectedq->hintshownumcorrect = array(false, false);
 875          $expectedq->hintclearwrong = array(false, false);
 876  
 877          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
 878      }
 879  
 880      public function test_export_multichoice() {
 881          $qdata = new stdClass();
 882          $qdata->id = 123;
 883          $qdata->contextid = \context_system::instance()->id;
 884          $qdata->qtype = 'multichoice';
 885          $qdata->name = 'Multiple choice question';
 886          $qdata->questiontext = 'Which are the even numbers?';
 887          $qdata->questiontextformat = FORMAT_HTML;
 888          $qdata->generalfeedback = 'The even numbers are 2 and 4.';
 889          $qdata->generalfeedbackformat = FORMAT_HTML;
 890          $qdata->defaultmark = 2;
 891          $qdata->length = 1;
 892          $qdata->penalty = 0.3333333;
 893          $qdata->hidden = 0;
 894          $qdata->idnumber = null;
 895  
 896          $qdata->options = new stdClass();
 897          $qdata->options->single = 0;
 898          $qdata->options->shuffleanswers = 0;
 899          $qdata->options->answernumbering = 'abc';
 900          $qdata->options->showstandardinstruction = 0;
 901          $qdata->options->correctfeedback = '<p>Your answer is correct.</p>';
 902          $qdata->options->correctfeedbackformat = FORMAT_HTML;
 903          $qdata->options->partiallycorrectfeedback = '<p>Your answer is partially correct.</p>';
 904          $qdata->options->partiallycorrectfeedbackformat = FORMAT_HTML;
 905          $qdata->options->shownumcorrect = 1;
 906          $qdata->options->incorrectfeedback = '<p>Your answer is incorrect.</p>';
 907          $qdata->options->incorrectfeedbackformat = FORMAT_HTML;
 908  
 909          $qdata->options->answers = array(
 910              13 => new question_answer(13, '1', 0, '', FORMAT_HTML),
 911              14 => new question_answer(14, '2', 1, '', FORMAT_HTML),
 912              15 => new question_answer(15, '3', 0, '', FORMAT_HTML),
 913              16 => new question_answer(16, '4', 1, '', FORMAT_HTML),
 914          );
 915  
 916          $qdata->hints = array(
 917              new question_hint_with_parts(0, 'Hint 1.', FORMAT_HTML, false, false),
 918              new question_hint_with_parts(0, 'Hint 2.', FORMAT_HTML, false, false),
 919          );
 920  
 921          $exporter = new qformat_xml();
 922          $xml = $exporter->writequestion($qdata);
 923  
 924          $expectedxml = '<!-- question: 123  -->
 925    <question type="multichoice">
 926      <name>
 927        <text>Multiple choice question</text>
 928      </name>
 929      <questiontext format="html">
 930        <text>Which are the even numbers?</text>
 931      </questiontext>
 932      <generalfeedback format="html">
 933        <text>The even numbers are 2 and 4.</text>
 934      </generalfeedback>
 935      <defaultgrade>2</defaultgrade>
 936      <penalty>0.3333333</penalty>
 937      <hidden>0</hidden>
 938      <idnumber></idnumber>
 939      <single>false</single>
 940      <shuffleanswers>false</shuffleanswers>
 941      <answernumbering>abc</answernumbering>
 942      <showstandardinstruction>0</showstandardinstruction>
 943      <correctfeedback format="html">
 944        <text><![CDATA[<p>Your answer is correct.</p>]]></text>
 945      </correctfeedback>
 946      <partiallycorrectfeedback format="html">
 947        <text><![CDATA[<p>Your answer is partially correct.</p>]]></text>
 948      </partiallycorrectfeedback>
 949      <incorrectfeedback format="html">
 950        <text><![CDATA[<p>Your answer is incorrect.</p>]]></text>
 951      </incorrectfeedback>
 952      <shownumcorrect/>
 953      <answer fraction="0" format="plain_text">
 954        <text>1</text>
 955        <feedback format="html">
 956          <text></text>
 957        </feedback>
 958      </answer>
 959      <answer fraction="100" format="plain_text">
 960        <text>2</text>
 961        <feedback format="html">
 962          <text></text>
 963        </feedback>
 964      </answer>
 965      <answer fraction="0" format="plain_text">
 966        <text>3</text>
 967        <feedback format="html">
 968          <text></text>
 969        </feedback>
 970      </answer>
 971      <answer fraction="100" format="plain_text">
 972        <text>4</text>
 973        <feedback format="html">
 974          <text></text>
 975        </feedback>
 976      </answer>
 977      <hint format="html">
 978        <text>Hint 1.</text>
 979      </hint>
 980      <hint format="html">
 981        <text>Hint 2.</text>
 982      </hint>
 983    </question>
 984  ';
 985  
 986          $this->assert_same_xml($expectedxml, $xml);
 987      }
 988  
 989      public function test_import_numerical_19() {
 990          $xml = '  <question type="numerical">
 991      <name>
 992        <text>Numerical question</text>
 993      </name>
 994      <questiontext format="html">
 995        <text>What is the answer?</text>
 996      </questiontext>
 997      <generalfeedback>
 998        <text>General feedback: Think Hitch-hikers guide to the Galaxy.</text>
 999      </generalfeedback>
1000      <defaultgrade>1</defaultgrade>
1001      <penalty>0.1</penalty>
1002      <hidden>0</hidden>
1003      <answer fraction="100">
1004        <text>42</text>
1005        <feedback>
1006          <text>Well done!</text>
1007        </feedback>
1008        <tolerance>0.001</tolerance>
1009      </answer>
1010      <answer fraction="0">
1011        <text>13</text>
1012        <feedback>
1013          <text>What were you thinking?!</text>
1014        </feedback>
1015        <tolerance>1</tolerance>
1016      </answer>
1017      <answer fraction="0">
1018        <text>*</text>
1019        <feedback>
1020          <text>Completely wrong.</text>
1021        </feedback>
1022        <tolerance></tolerance>
1023      </answer>
1024    </question>';
1025          $xmldata = xmlize($xml);
1026  
1027          $importer = new qformat_xml();
1028          $q = $importer->import_numerical($xmldata['question']);
1029  
1030          $expectedq = new stdClass();
1031          $expectedq->qtype = 'numerical';
1032          $expectedq->name = 'Numerical question';
1033          $expectedq->questiontext = 'What is the answer?';
1034          $expectedq->questiontextformat = FORMAT_HTML;
1035          $expectedq->generalfeedback = 'General feedback: Think Hitch-hikers guide to the Galaxy.';
1036          $expectedq->generalfeedbackformat = FORMAT_HTML;
1037          $expectedq->defaultmark = 1;
1038          $expectedq->length = 1;
1039          $expectedq->penalty = 0.1;
1040  
1041          $expectedq->answer = array('42', '13', '*');
1042          $expectedq->fraction = array(1, 0, 0);
1043          $expectedq->feedback = array(
1044              array('text' => 'Well done!',
1045                      'format' => FORMAT_HTML),
1046              array('text' => 'What were you thinking?!',
1047                      'format' => FORMAT_HTML),
1048              array('text' => 'Completely wrong.',
1049                      'format' => FORMAT_HTML));
1050          $expectedq->tolerance = array(0.001, 1, 0);
1051  
1052          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
1053      }
1054  
1055      public function test_export_numerical() {
1056          question_bank::load_question_definition_classes('numerical');
1057  
1058          $qdata = new stdClass();
1059          $qdata->id = 123;
1060          $qdata->contextid = \context_system::instance()->id;
1061          $qdata->qtype = 'numerical';
1062          $qdata->name = 'Numerical question';
1063          $qdata->questiontext = 'What is the answer?';
1064          $qdata->questiontextformat = FORMAT_HTML;
1065          $qdata->generalfeedback = 'General feedback: Think Hitch-hikers guide to the Galaxy.';
1066          $qdata->generalfeedbackformat = FORMAT_HTML;
1067          $qdata->defaultmark = 1;
1068          $qdata->length = 1;
1069          $qdata->penalty = 0.1;
1070          $qdata->hidden = 0;
1071          $qdata->idnumber = null;
1072  
1073          $qdata->options = new stdClass();
1074          $qdata->options->answers = array(
1075              13 => new qtype_numerical_answer(13, '42', 1, 'Well done!',
1076                      FORMAT_HTML, 0.001),
1077              14 => new qtype_numerical_answer(14, '13', 0, 'What were you thinking?!',
1078                      FORMAT_HTML, 1),
1079              15 => new qtype_numerical_answer(15, '*', 0, 'Completely wrong.',
1080                      FORMAT_HTML, ''),
1081          );
1082  
1083          $qdata->options->units = array();
1084  
1085          $exporter = new qformat_xml();
1086          $xml = $exporter->writequestion($qdata);
1087  
1088          $expectedxml = '<!-- question: 123  -->
1089    <question type="numerical">
1090      <name>
1091        <text>Numerical question</text>
1092      </name>
1093      <questiontext format="html">
1094        <text>What is the answer?</text>
1095      </questiontext>
1096      <generalfeedback format="html">
1097        <text>General feedback: Think Hitch-hikers guide to the Galaxy.</text>
1098      </generalfeedback>
1099      <defaultgrade>1</defaultgrade>
1100      <penalty>0.1</penalty>
1101      <hidden>0</hidden>
1102      <idnumber></idnumber>
1103      <answer fraction="100" format="plain_text">
1104        <text>42</text>
1105        <feedback format="html">
1106          <text>Well done!</text>
1107        </feedback>
1108        <tolerance>0.001</tolerance>
1109      </answer>
1110      <answer fraction="0" format="plain_text">
1111        <text>13</text>
1112        <feedback format="html">
1113          <text>What were you thinking?!</text>
1114        </feedback>
1115        <tolerance>1</tolerance>
1116      </answer>
1117      <answer fraction="0" format="plain_text">
1118        <text>*</text>
1119        <feedback format="html">
1120          <text>Completely wrong.</text>
1121        </feedback>
1122        <tolerance>0</tolerance>
1123      </answer>
1124    </question>
1125  ';
1126  
1127          $this->assert_same_xml($expectedxml, $xml);
1128      }
1129  
1130      public function test_import_shortanswer_19() {
1131          $xml = '  <question type="shortanswer">
1132      <name>
1133        <text>Short answer question</text>
1134      </name>
1135      <questiontext format="html">
1136        <text>Fill in the gap in this sequence: Alpha, ________, Gamma.</text>
1137      </questiontext>
1138      <generalfeedback>
1139        <text>The answer is Beta.</text>
1140      </generalfeedback>
1141      <defaultgrade>1</defaultgrade>
1142      <penalty>0.3333333</penalty>
1143      <hidden>0</hidden>
1144      <usecase>0</usecase>
1145      <answer fraction="100" format="plain_text">
1146        <text>Beta</text>
1147        <feedback>
1148          <text>Well done!</text>
1149        </feedback>
1150      </answer>
1151      <answer fraction="0" format="plain_text">
1152        <text>*</text>
1153        <feedback>
1154          <text>Doh!</text>
1155        </feedback>
1156      </answer>
1157      <hint>
1158        <text>Hint 1</text>
1159      </hint>
1160      <hint>
1161        <text>Hint 2</text>
1162      </hint>
1163    </question>';
1164          $xmldata = xmlize($xml);
1165  
1166          $importer = new qformat_xml();
1167          $q = $importer->import_shortanswer($xmldata['question']);
1168  
1169          $expectedq = new stdClass();
1170          $expectedq->qtype = 'shortanswer';
1171          $expectedq->name = 'Short answer question';
1172          $expectedq->questiontext = 'Fill in the gap in this sequence: Alpha, ________, Gamma.';
1173          $expectedq->questiontextformat = FORMAT_HTML;
1174          $expectedq->generalfeedback = 'The answer is Beta.';
1175          $expectedq->usecase = false;
1176          $expectedq->defaultmark = 1;
1177          $expectedq->length = 1;
1178          $expectedq->penalty = 0.3333333;
1179  
1180          $expectedq->answer = array('Beta', '*');
1181          $expectedq->fraction = array(1, 0);
1182          $expectedq->feedback = array(
1183              array('text' => 'Well done!', 'format' => FORMAT_HTML),
1184              array('text' => 'Doh!', 'format' => FORMAT_HTML));
1185  
1186          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
1187      }
1188  
1189      public function test_export_shortanswer() {
1190          $qdata = new stdClass();
1191          $qdata->id = 123;
1192          $qdata->contextid = \context_system::instance()->id;
1193          $qdata->qtype = 'shortanswer';
1194          $qdata->name = 'Short answer question';
1195          $qdata->questiontext = 'Fill in the gap in this sequence: Alpha, ________, Gamma.';
1196          $qdata->questiontextformat = FORMAT_HTML;
1197          $qdata->generalfeedback = 'The answer is Beta.';
1198          $qdata->generalfeedbackformat = FORMAT_HTML;
1199          $qdata->defaultmark = 1;
1200          $qdata->length = 1;
1201          $qdata->penalty = 0.3333333;
1202          $qdata->hidden = 0;
1203          $qdata->idnumber = null;
1204  
1205          $qdata->options = new stdClass();
1206          $qdata->options->usecase = 0;
1207  
1208          $qdata->options->answers = array(
1209              13 => new question_answer(13, 'Beta', 1, 'Well done!', FORMAT_HTML),
1210              14 => new question_answer(14, '*', 0, 'Doh!', FORMAT_HTML),
1211          );
1212  
1213          $qdata->hints = array(
1214              new question_hint(0, 'Hint 1', FORMAT_HTML),
1215              new question_hint(0, 'Hint 2', FORMAT_HTML),
1216          );
1217  
1218          $exporter = new qformat_xml();
1219          $xml = $exporter->writequestion($qdata);
1220  
1221          $expectedxml = '<!-- question: 123  -->
1222    <question type="shortanswer">
1223      <name>
1224        <text>Short answer question</text>
1225      </name>
1226      <questiontext format="html">
1227        <text>Fill in the gap in this sequence: Alpha, ________, Gamma.</text>
1228      </questiontext>
1229      <generalfeedback format="html">
1230        <text>The answer is Beta.</text>
1231      </generalfeedback>
1232      <defaultgrade>1</defaultgrade>
1233      <penalty>0.3333333</penalty>
1234      <hidden>0</hidden>
1235      <idnumber></idnumber>
1236      <usecase>0</usecase>
1237      <answer fraction="100" format="plain_text">
1238        <text>Beta</text>
1239        <feedback format="html">
1240          <text>Well done!</text>
1241        </feedback>
1242      </answer>
1243      <answer fraction="0" format="plain_text">
1244        <text>*</text>
1245        <feedback format="html">
1246          <text>Doh!</text>
1247        </feedback>
1248      </answer>
1249      <hint format="html">
1250        <text>Hint 1</text>
1251      </hint>
1252      <hint format="html">
1253        <text>Hint 2</text>
1254      </hint>
1255    </question>
1256  ';
1257  
1258          $this->assert_same_xml($expectedxml, $xml);
1259      }
1260  
1261      public function test_import_truefalse_19() {
1262          $xml = '  <question type="truefalse">
1263      <name>
1264        <text>True false question</text>
1265      </name>
1266      <questiontext format="html">
1267        <text>The answer is true.</text>
1268      </questiontext>
1269      <generalfeedback>
1270        <text>General feedback: You should have chosen true.</text>
1271      </generalfeedback>
1272      <defaultgrade>1</defaultgrade>
1273      <penalty>1</penalty>
1274      <hidden>0</hidden>
1275      <answer fraction="100">
1276        <text>true</text>
1277        <feedback>
1278          <text>Well done!</text>
1279        </feedback>
1280      </answer>
1281      <answer fraction="0">
1282        <text>false</text>
1283        <feedback>
1284          <text>Doh!</text>
1285        </feedback>
1286      </answer>
1287    </question>';
1288          $xmldata = xmlize($xml);
1289  
1290          $importer = new qformat_xml();
1291          $q = $importer->import_truefalse($xmldata['question']);
1292  
1293          $expectedq = new stdClass();
1294          $expectedq->qtype = 'truefalse';
1295          $expectedq->name = 'True false question';
1296          $expectedq->questiontext = 'The answer is true.';
1297          $expectedq->questiontextformat = FORMAT_HTML;
1298          $expectedq->generalfeedback = 'General feedback: You should have chosen true.';
1299          $expectedq->defaultmark = 1;
1300          $expectedq->length = 1;
1301          $expectedq->penalty = 1;
1302  
1303          $expectedq->feedbacktrue = array('text' => 'Well done!',
1304                  'format' => FORMAT_HTML);
1305          $expectedq->feedbackfalse = array('text' => 'Doh!',
1306                  'format' => FORMAT_HTML);
1307          $expectedq->correctanswer = true;
1308  
1309          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
1310      }
1311  
1312      public function test_import_truefalse_with_idnumber() {
1313          $xml = '  <question type="truefalse">
1314      <name>
1315        <text>True false question</text>
1316      </name>
1317      <questiontext format="html">
1318        <text>The answer is true.</text>
1319      </questiontext>
1320      <generalfeedback>
1321        <text>General feedback: You should have chosen true.</text>
1322      </generalfeedback>
1323      <defaultgrade>1</defaultgrade>
1324      <penalty>1</penalty>
1325      <hidden>0</hidden>
1326      <idnumber>TestIdNum1</idnumber>
1327      <answer fraction="100">
1328        <text>true</text>
1329        <feedback>
1330          <text>Well done!</text>
1331        </feedback>
1332      </answer>
1333      <answer fraction="0">
1334        <text>false</text>
1335        <feedback>
1336          <text>Doh!</text>
1337        </feedback>
1338      </answer>
1339    </question>';
1340          $xmldata = xmlize($xml);
1341  
1342          $importer = new qformat_xml();
1343          $q = $importer->import_truefalse($xmldata['question']);
1344  
1345          $expectedq = new stdClass();
1346          $expectedq->qtype = 'truefalse';
1347          $expectedq->name = 'True false question';
1348          $expectedq->questiontext = 'The answer is true.';
1349          $expectedq->questiontextformat = FORMAT_HTML;
1350          $expectedq->generalfeedback = 'General feedback: You should have chosen true.';
1351          $expectedq->defaultmark = 1;
1352          $expectedq->length = 1;
1353          $expectedq->penalty = 1;
1354          $expectedq->idnumber = 'TestIdNum1';
1355  
1356          $expectedq->feedbacktrue = array('text' => 'Well done!',
1357                  'format' => FORMAT_HTML);
1358          $expectedq->feedbackfalse = array('text' => 'Doh!',
1359                  'format' => FORMAT_HTML);
1360          $expectedq->correctanswer = true;
1361  
1362          $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
1363      }
1364  
1365      public function test_export_truefalse() {
1366          $qdata = new stdClass();
1367          $qdata->id = 12;
1368          $qdata->contextid = \context_system::instance()->id;
1369          $qdata->qtype = 'truefalse';
1370          $qdata->name = 'True false question';
1371          $qdata->questiontext = 'The answer is true.';
1372          $qdata->questiontextformat = FORMAT_HTML;
1373          $qdata->generalfeedback = 'General feedback: You should have chosen true.';
1374          $qdata->generalfeedbackformat = FORMAT_HTML;
1375          $qdata->defaultmark = 1;
1376          $qdata->length = 1;
1377          $qdata->penalty = 1;
1378          $qdata->hidden = 0;
1379          $qdata->idnumber = null;
1380  
1381          $qdata->options = new stdClass();
1382          $qdata->options->answers = array(
1383              1 => new question_answer(1, 'True', 1, 'Well done!', FORMAT_HTML),
1384              2 => new question_answer(2, 'False', 0, 'Doh!', FORMAT_HTML),
1385          );
1386          $qdata->options->trueanswer = 1;
1387          $qdata->options->falseanswer = 2;
1388  
1389          $exporter = new qformat_xml();
1390          $xml = $exporter->writequestion($qdata);
1391  
1392          $expectedxml = '<!-- question: 12  -->
1393    <question type="truefalse">
1394      <name>
1395        <text>True false question</text>
1396      </name>
1397      <questiontext format="html">
1398        <text>The answer is true.</text>
1399      </questiontext>
1400      <generalfeedback format="html">
1401        <text>General feedback: You should have chosen true.</text>
1402      </generalfeedback>
1403      <defaultgrade>1</defaultgrade>
1404      <penalty>1</penalty>
1405      <hidden>0</hidden>
1406      <idnumber></idnumber>
1407      <answer fraction="100" format="plain_text">
1408        <text>true</text>
1409        <feedback format="html">
1410          <text>Well done!</text>
1411        </feedback>
1412      </answer>
1413      <answer fraction="0" format="plain_text">
1414        <text>false</text>
1415        <feedback format="html">
1416          <text>Doh!</text>
1417        </feedback>
1418      </answer>
1419    </question>
1420  ';
1421  
1422          $this->assert_same_xml($expectedxml, $xml);
1423      }
1424  
1425      public function test_export_truefalse_with_idnumber() {
1426          $qdata = new stdClass();
1427          $qdata->id = 12;
1428          $qdata->contextid = \context_system::instance()->id;
1429          $qdata->qtype = 'truefalse';
1430          $qdata->name = 'True false question';
1431          $qdata->questiontext = 'The answer is true.';
1432          $qdata->questiontextformat = FORMAT_HTML;
1433          $qdata->generalfeedback = 'General feedback: You should have chosen true.';
1434          $qdata->generalfeedbackformat = FORMAT_HTML;
1435          $qdata->defaultmark = 1;
1436          $qdata->length = 1;
1437          $qdata->penalty = 1;
1438          $qdata->hidden = 0;
1439          $qdata->idnumber = 'TestIDNum2';
1440  
1441          $qdata->options = new stdClass();
1442          $qdata->options->answers = array(
1443                  1 => new question_answer(1, 'True', 1, 'Well done!', FORMAT_HTML),
1444                  2 => new question_answer(2, 'False', 0, 'Doh!', FORMAT_HTML),
1445          );
1446          $qdata->options->trueanswer = 1;
1447          $qdata->options->falseanswer = 2;
1448  
1449          $exporter = new qformat_xml();
1450          $xml = $exporter->writequestion($qdata);
1451  
1452          $expectedxml = '<!-- question: 12  -->
1453    <question type="truefalse">
1454      <name>
1455        <text>True false question</text>
1456      </name>
1457      <questiontext format="html">
1458        <text>The answer is true.</text>
1459      </questiontext>
1460      <generalfeedback format="html">
1461        <text>General feedback: You should have chosen true.</text>
1462      </generalfeedback>
1463      <defaultgrade>1</defaultgrade>
1464      <penalty>1</penalty>
1465      <hidden>0</hidden>
1466      <idnumber>TestIDNum2</idnumber>
1467      <answer fraction="100" format="plain_text">
1468        <text>true</text>
1469        <feedback format="html">
1470          <text>Well done!</text>
1471        </feedback>
1472      </answer>
1473      <answer fraction="0" format="plain_text">
1474        <text>false</text>
1475        <feedback format="html">
1476          <text>Doh!</text>
1477        </feedback>
1478      </answer>
1479    </question>
1480  ';
1481  
1482          $this->assert_same_xml($expectedxml, $xml);
1483      }
1484  
1485      public function test_import_multianswer() {
1486          $xml = '  <question type="cloze">
1487      <name>
1488        <text>Simple multianswer</text>
1489      </name>
1490      <questiontext format="html">
1491        <text><![CDATA[Complete this opening line of verse: "The {1:SHORTANSWER:Dog#Wrong, silly!~=Owl#Well done!~*#Wrong answer} and the {1:MULTICHOICE:Bow-wow#You seem to have a dog obsessions!~Wiggly worm#Now you are just being ridiculous!~=Pussy-cat#Well done!} went to sea".]]></text>
1492      </questiontext>
1493      <generalfeedback format="html">
1494        <text><![CDATA[General feedback: It\'s from "The Owl and the Pussy-cat" by Lear: "The owl and the pussycat went to sea".]]></text>
1495      </generalfeedback>
1496      <penalty>0.5</penalty>
1497      <hidden>0</hidden>
1498      <hint format="html">
1499        <text>Hint 1</text>
1500      </hint>
1501      <hint format="html">
1502        <text>Hint 2</text>
1503      </hint>
1504      <tags>
1505        <tag><text>tagCloze</text></tag>
1506        <tag><text>tagTest</text></tag>
1507      </tags>
1508    </question>
1509  ';
1510          $xmldata = xmlize($xml);
1511  
1512          $importer = new qformat_xml();
1513          $q = $importer->import_multianswer($xmldata['question']);
1514  
1515          // Annoyingly, import works in a weird way (it duplicates code, rather
1516          // than just calling save_question) so we cannot use
1517          // test_question_maker::get_question_form_data('multianswer', 'twosubq').
1518          $expectedqa = new stdClass();
1519          $expectedqa->name = 'Simple multianswer';
1520          $expectedqa->qtype = 'multianswer';
1521          $expectedqa->questiontext = 'Complete this opening line of verse: "The {#1} and the {#2} went to sea".';
1522          $expectedqa->generalfeedback =
1523                  'General feedback: It\'s from "The Owl and the Pussy-cat" by Lear: "The owl and the pussycat went to sea".';
1524          $expectedqa->defaultmark = 2;
1525          $expectedqa->penalty = 0.5;
1526  
1527          $expectedqa->hint = array(
1528              array('text' => 'Hint 1', 'format' => FORMAT_HTML),
1529              array('text' => 'Hint 2', 'format' => FORMAT_HTML),
1530          );
1531  
1532          $sa = new stdClass();
1533  
1534          $sa->questiontext = array('text' => '{1:SHORTANSWER:Dog#Wrong, silly!~=Owl#Well done!~*#Wrong answer}',
1535                  'format' => FORMAT_HTML, 'itemid' => null);
1536          $sa->generalfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
1537          $sa->defaultmark = 1.0;
1538          $sa->qtype = 'shortanswer';
1539          $sa->usecase = 0;
1540  
1541          $sa->answer = array('Dog', 'Owl', '*');
1542          $sa->fraction = array(0, 1, 0);
1543          $sa->feedback = array(
1544              array('text' => 'Wrong, silly!', 'format' => FORMAT_HTML, 'itemid' => null),
1545              array('text' => 'Well done!',    'format' => FORMAT_HTML, 'itemid' => null),
1546              array('text' => 'Wrong answer',  'format' => FORMAT_HTML, 'itemid' => null),
1547          );
1548  
1549          $mc = new stdClass();
1550  
1551          $mc->generalfeedback = '';
1552          $mc->questiontext = array('text' => '{1:MULTICHOICE:Bow-wow#You seem to have a dog obsessions!~' .
1553                  'Wiggly worm#Now you are just being ridiculous!~=Pussy-cat#Well done!}',
1554                  'format' => FORMAT_HTML, 'itemid' => null);
1555          $mc->generalfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
1556          $mc->defaultmark = 1.0;
1557          $mc->qtype = 'multichoice';
1558  
1559          $mc->layout = 0;
1560          $mc->single = 1;
1561          $mc->shuffleanswers = 0;
1562          $mc->correctfeedback =          array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
1563          $mc->partiallycorrectfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
1564          $mc->incorrectfeedback =        array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
1565          $mc->answernumbering = 0;
1566  
1567          $mc->answer = array(
1568              array('text' => 'Bow-wow',     'format' => FORMAT_HTML, 'itemid' => null),
1569              array('text' => 'Wiggly worm', 'format' => FORMAT_HTML, 'itemid' => null),
1570              array('text' => 'Pussy-cat',   'format' => FORMAT_HTML, 'itemid' => null),
1571          );
1572          $mc->fraction = array(0, 0, 1);
1573          $mc->feedback = array(
1574              array('text' => 'You seem to have a dog obsessions!', 'format' => FORMAT_HTML, 'itemid' => null),
1575              array('text' => 'Now you are just being ridiculous!', 'format' => FORMAT_HTML, 'itemid' => null),
1576              array('text' => 'Well done!',                         'format' => FORMAT_HTML, 'itemid' => null),
1577          );
1578  
1579          $expectedqa->options = new stdClass();
1580          $expectedqa->options->questions = array(
1581              1 => $sa,
1582              2 => $mc,
1583          );
1584          $expectedqa->tags = array('tagCloze', 'tagTest');
1585  
1586          $this->assertEquals($expectedqa->hint, $q->hint);
1587          $this->assertEquals($expectedqa->options->questions[1], $q->options->questions[1]);
1588          $this->assertEquals($expectedqa->options->questions[2], $q->options->questions[2]);
1589          $this->assert(new question_check_specified_fields_expectation($expectedqa), $q);
1590      }
1591  
1592      public function test_export_multianswer() {
1593          $qdata = test_question_maker::get_question_data('multianswer', 'twosubq');
1594          $qdata->contextid = \context_system::instance()->id;
1595          $exporter = new qformat_xml();
1596          $xml = $exporter->writequestion($qdata);
1597  
1598          $expectedxml = '<!-- question: 0  -->
1599    <question type="cloze">
1600      <name>
1601        <text>Simple multianswer</text>
1602      </name>
1603      <questiontext format="html">
1604        <text><![CDATA[Complete this opening line of verse: "The {1:SHORTANSWER:Dog#Wrong, silly!~=Owl#Well done!~*#Wrong answer} and the {1:MULTICHOICE:Bow-wow#You seem to have a dog obsessions!~Wiggly worm#Now you are just being ridiculous!~=Pussy-cat#Well done!} went to sea".]]></text>
1605      </questiontext>
1606      <generalfeedback format="html">
1607        <text><![CDATA[General feedback: It\'s from "The Owl and the Pussy-cat" by Lear: "The owl and the pussycat went to sea]]></text>
1608      </generalfeedback>
1609      <penalty>0.3333333</penalty>
1610      <hidden>0</hidden>
1611      <idnumber></idnumber>
1612      <hint format="html">
1613        <text>Hint 1</text>
1614      </hint>
1615      <hint format="html">
1616        <text>Hint 2</text>
1617      </hint>
1618    </question>
1619  ';
1620  
1621          $this->assert_same_xml($expectedxml, $xml);
1622      }
1623  
1624      public function test_export_multianswer_withdollars() {
1625          $qdata = test_question_maker::get_question_data('multianswer', 'dollarsigns');
1626          $qdata->contextid = \context_system::instance()->id;
1627          $exporter = new qformat_xml();
1628          $xml = $exporter->writequestion($qdata);
1629  
1630          $expectedxml = '<!-- question: 0  -->
1631    <question type="cloze">
1632      <name>
1633        <text>Multianswer with $s</text>
1634      </name>
1635      <questiontext format="html">
1636        <text>Which is the right order? {1:MULTICHOICE:=y,y,$3~$3,y,y}</text>
1637      </questiontext>
1638      <generalfeedback format="html">
1639        <text></text>
1640      </generalfeedback>
1641      <penalty>0.3333333</penalty>
1642      <hidden>0</hidden>
1643      <idnumber></idnumber>
1644    </question>
1645  ';
1646  
1647          $this->assert_same_xml($expectedxml, $xml);
1648      }
1649  
1650      public function test_import_files_as_draft() {
1651          $this->resetAfterTest();
1652          $this->setAdminUser();
1653  
1654          $xml = <<<END
1655  <questiontext format="html">
1656      <text><![CDATA[<p><a href="@@PLUGINFILE@@/moodle.txt">This text file</a> contains the word 'Moodle'.</p>]]></text>
1657      <file name="moodle.txt" encoding="base64">TW9vZGxl</file>
1658  </questiontext>
1659  END;
1660  
1661          $textxml = xmlize($xml);
1662          $qo = new stdClass();
1663  
1664          $importer = new qformat_xml();
1665          $draftitemid = $importer->import_files_as_draft($textxml['questiontext']['#']['file']);
1666          $files = file_get_drafarea_files($draftitemid);
1667  
1668          $this->assertEquals(1, count($files->list));
1669  
1670          $file = $files->list[0];
1671          $this->assertEquals('moodle.txt', $file->filename);
1672          $this->assertEquals('/',          $file->filepath);
1673          $this->assertEquals(6,            $file->size);
1674      }
1675  
1676      public function test_import_truefalse_wih_files() {
1677          $this->resetAfterTest();
1678          $this->setAdminUser();
1679  
1680          $xml = '<question type="truefalse">
1681      <name>
1682        <text>truefalse</text>
1683      </name>
1684      <questiontext format="html">
1685        <text><![CDATA[<p><a href="@@PLUGINFILE@@/myfolder/moodle.txt">This text file</a> contains the word Moodle.</p>]]></text>
1686  <file name="moodle.txt" path="/myfolder/" encoding="base64">TW9vZGxl</file>
1687      </questiontext>
1688      <generalfeedback format="html">
1689        <text><![CDATA[<p>For further information, see the documentation about Moodle.</p>]]></text>
1690  </generalfeedback>
1691      <defaultgrade>1.0000000</defaultgrade>
1692      <penalty>1.0000000</penalty>
1693      <hidden>0</hidden>
1694      <answer fraction="100" format="moodle_auto_format">
1695        <text>true</text>
1696        <feedback format="html">
1697          <text></text>
1698        </feedback>
1699      </answer>
1700      <answer fraction="0" format="moodle_auto_format">
1701        <text>false</text>
1702        <feedback format="html">
1703          <text></text>
1704        </feedback>
1705      </answer>
1706    </question>';
1707          $xmldata = xmlize($xml);
1708  
1709          $importer = new qformat_xml();
1710          $q = $importer->import_truefalse($xmldata['question']);
1711  
1712          $draftitemid = $q->questiontextitemid;
1713          $files = file_get_drafarea_files($draftitemid, '/myfolder/');
1714  
1715          $this->assertEquals(1, count($files->list));
1716  
1717          $file = $files->list[0];
1718          $this->assertEquals('moodle.txt', $file->filename);
1719          $this->assertEquals('/myfolder/', $file->filepath);
1720          $this->assertEquals(6,            $file->size);
1721      }
1722  
1723      public function test_create_dummy_question() {
1724  
1725          $testobject = new mock_qformat_xml();
1726          $categoryname = 'name1';
1727          $categoryinfo = new stdClass();
1728          $categoryinfo->info = 'info1';
1729          $categoryinfo->infoformat = 'infoformat1';
1730          $categoryinfo->idnumber = null;
1731          $dummyquestion = $testobject->mock_create_dummy_question_representing_category($categoryname, $categoryinfo);
1732  
1733          $this->assertEquals('category', $dummyquestion->qtype);
1734          $this->assertEquals($categoryname, $dummyquestion->category);
1735          $this->assertEquals($categoryinfo->info, $dummyquestion->info);
1736          $this->assertEquals($categoryinfo->infoformat, $dummyquestion->infoformat);
1737          $this->assertEquals('Switch category to ' . $categoryname, $dummyquestion->name);
1738          $this->assertEquals(0, $dummyquestion->id);
1739          $this->assertEquals('', $dummyquestion->questiontextformat);
1740          $this->assertEquals(0, $dummyquestion->contextid);
1741      }
1742  }
1743  
1744  /**
1745   * Class mock_qformat_xml exists only to enable testing of the create dummy question category.
1746   * @package    qformat_xml
1747   * @copyright  2018 The Open University
1748   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1749   */
1750  class mock_qformat_xml extends qformat_xml {
1751      /**
1752       * Make public an otherwise protected function.
1753       * @param string $categoryname the name of the category
1754       * @param object $categoryinfo description of the category
1755       */
1756      public function mock_create_dummy_question_representing_category(string $categoryname, $categoryinfo) {
1757          return $this->create_dummy_question_representing_category($categoryname, $categoryinfo);
1758      }
1759  }