Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 39 and 311]

   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  namespace core_question;
  18  
  19  use question_bank;
  20  use question_state;
  21  
  22  defined('MOODLE_INTERNAL') || die();
  23  
  24  global $CFG;
  25  require_once (__DIR__ . '/../lib.php');
  26  require_once (__DIR__ . '/helpers.php');
  27  
  28  /**
  29   * Unit tests for the autosave parts of the {@link question_usage} class.
  30   *
  31   * @package   core_question
  32   * @category  test
  33   * @copyright 2013 The Open University
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class questionusage_autosave_test extends \qbehaviour_walkthrough_test_base {
  37  
  38      public function test_autosave_then_display() {
  39          $this->resetAfterTest();
  40          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  41          $cat = $generator->create_question_category();
  42          $question = $generator->create_question('shortanswer', null,
  43                  array('category' => $cat->id));
  44  
  45          // Start attempt at a shortanswer question.
  46          $q = question_bank::load_question($question->id);
  47          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
  48  
  49          $this->check_current_state(question_state::$todo);
  50          $this->check_current_mark(null);
  51          $this->check_step_count(1);
  52  
  53          // Process a response and check the expected result.
  54          $this->process_submission(array('answer' => 'first response'));
  55  
  56          $this->check_current_state(question_state::$complete);
  57          $this->check_current_mark(null);
  58          $this->check_step_count(2);
  59          $this->save_quba();
  60  
  61          // Now check how that is re-displayed.
  62          $this->render();
  63          $this->check_output_contains_text_input('answer', 'first response');
  64          $this->check_output_contains_hidden_input(':sequencecheck', 2);
  65  
  66          // Process an autosave.
  67          $this->load_quba();
  68          $this->process_autosave(array('answer' => 'second response'));
  69          $this->check_current_state(question_state::$complete);
  70          $this->check_current_mark(null);
  71          $this->check_step_count(3);
  72          $this->save_quba();
  73  
  74          // Now check how that is re-displayed.
  75          $this->load_quba();
  76          $this->render();
  77          $this->check_output_contains_text_input('answer', 'second response');
  78          $this->check_output_contains_hidden_input(':sequencecheck', 2);
  79  
  80          $this->delete_quba();
  81      }
  82  
  83      public function test_autosave_then_autosave_different_data() {
  84          $this->resetAfterTest();
  85          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  86          $cat = $generator->create_question_category();
  87          $question = $generator->create_question('shortanswer', null,
  88                  array('category' => $cat->id));
  89  
  90          // Start attempt at a shortanswer question.
  91          $q = question_bank::load_question($question->id);
  92          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
  93  
  94          $this->check_current_state(question_state::$todo);
  95          $this->check_current_mark(null);
  96          $this->check_step_count(1);
  97  
  98          // Process a response and check the expected result.
  99          $this->process_submission(array('answer' => 'first response'));
 100  
 101          $this->check_current_state(question_state::$complete);
 102          $this->check_current_mark(null);
 103          $this->check_step_count(2);
 104          $this->save_quba();
 105  
 106          // Now check how that is re-displayed.
 107          $this->render();
 108          $this->check_output_contains_text_input('answer', 'first response');
 109          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 110  
 111          // Process an autosave.
 112          $this->load_quba();
 113          $this->process_autosave(array('answer' => 'second response'));
 114          $this->check_current_state(question_state::$complete);
 115          $this->check_current_mark(null);
 116          $this->check_step_count(3);
 117          $this->save_quba();
 118  
 119          // Now check how that is re-displayed.
 120          $this->load_quba();
 121          $this->render();
 122          $this->check_output_contains_text_input('answer', 'second response');
 123          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 124  
 125          // Process a second autosave.
 126          $this->load_quba();
 127          $this->process_autosave(array('answer' => 'third response'));
 128          $this->check_current_state(question_state::$complete);
 129          $this->check_current_mark(null);
 130          $this->check_step_count(3);
 131          $this->save_quba();
 132  
 133          // Now check how that is re-displayed.
 134          $this->load_quba();
 135          $this->render();
 136          $this->check_output_contains_text_input('answer', 'third response');
 137          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 138  
 139          $this->delete_quba();
 140      }
 141  
 142      public function test_autosave_then_autosave_same_data() {
 143          $this->resetAfterTest();
 144          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 145          $cat = $generator->create_question_category();
 146          $question = $generator->create_question('shortanswer', null,
 147                  array('category' => $cat->id));
 148  
 149          // Start attempt at a shortanswer question.
 150          $q = question_bank::load_question($question->id);
 151          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 152  
 153          $this->check_current_state(question_state::$todo);
 154          $this->check_current_mark(null);
 155          $this->check_step_count(1);
 156  
 157          // Process a response and check the expected result.
 158          $this->process_submission(array('answer' => 'first response'));
 159  
 160          $this->check_current_state(question_state::$complete);
 161          $this->check_current_mark(null);
 162          $this->check_step_count(2);
 163          $this->save_quba();
 164  
 165          // Now check how that is re-displayed.
 166          $this->render();
 167          $this->check_output_contains_text_input('answer', 'first response');
 168          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 169  
 170          // Process an autosave.
 171          $this->load_quba();
 172          $this->process_autosave(array('answer' => 'second response'));
 173          $this->check_current_state(question_state::$complete);
 174          $this->check_current_mark(null);
 175          $this->check_step_count(3);
 176          $this->save_quba();
 177  
 178          // Now check how that is re-displayed.
 179          $this->load_quba();
 180          $this->render();
 181          $this->check_output_contains_text_input('answer', 'second response');
 182          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 183  
 184          $stepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
 185  
 186          // Process a second autosave.
 187          $this->load_quba();
 188          $this->process_autosave(array('answer' => 'second response'));
 189          $this->check_current_state(question_state::$complete);
 190          $this->check_current_mark(null);
 191          $this->check_step_count(3);
 192          $this->save_quba();
 193  
 194          // Try to check it is really the same step
 195          $newstepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
 196          $this->assertEquals($stepid, $newstepid);
 197  
 198          // Now check how that is re-displayed.
 199          $this->load_quba();
 200          $this->render();
 201          $this->check_output_contains_text_input('answer', 'second response');
 202          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 203  
 204          $this->delete_quba();
 205      }
 206  
 207      public function test_autosave_then_autosave_original_data() {
 208          $this->resetAfterTest();
 209          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 210          $cat = $generator->create_question_category();
 211          $question = $generator->create_question('shortanswer', null,
 212                  array('category' => $cat->id));
 213  
 214          // Start attempt at a shortanswer question.
 215          $q = question_bank::load_question($question->id);
 216          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 217  
 218          $this->check_current_state(question_state::$todo);
 219          $this->check_current_mark(null);
 220          $this->check_step_count(1);
 221  
 222          // Process a response and check the expected result.
 223          $this->process_submission(array('answer' => 'first response'));
 224  
 225          $this->check_current_state(question_state::$complete);
 226          $this->check_current_mark(null);
 227          $this->check_step_count(2);
 228          $this->save_quba();
 229  
 230          // Now check how that is re-displayed.
 231          $this->render();
 232          $this->check_output_contains_text_input('answer', 'first response');
 233          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 234  
 235          // Process an autosave.
 236          $this->load_quba();
 237          $this->process_autosave(array('answer' => 'second response'));
 238          $this->check_current_state(question_state::$complete);
 239          $this->check_current_mark(null);
 240          $this->check_step_count(3);
 241          $this->save_quba();
 242  
 243          // Now check how that is re-displayed.
 244          $this->load_quba();
 245          $this->render();
 246          $this->check_output_contains_text_input('answer', 'second response');
 247          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 248  
 249          // Process a second autosave saving the original response.
 250          // This should remove the autosave step.
 251          $this->load_quba();
 252          $this->process_autosave(array('answer' => 'first response'));
 253          $this->check_current_state(question_state::$complete);
 254          $this->check_current_mark(null);
 255          $this->check_step_count(2);
 256          $this->save_quba();
 257  
 258          // Now check how that is re-displayed.
 259          $this->load_quba();
 260          $this->render();
 261          $this->check_output_contains_text_input('answer', 'first response');
 262          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 263  
 264          $this->delete_quba();
 265      }
 266  
 267      public function test_autosave_then_real_save() {
 268          $this->resetAfterTest();
 269          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 270          $cat = $generator->create_question_category();
 271          $question = $generator->create_question('shortanswer', null,
 272                  array('category' => $cat->id));
 273  
 274          // Start attempt at a shortanswer question.
 275          $q = question_bank::load_question($question->id);
 276          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 277  
 278          $this->check_current_state(question_state::$todo);
 279          $this->check_current_mark(null);
 280          $this->check_step_count(1);
 281  
 282          // Process a response and check the expected result.
 283          $this->process_submission(array('answer' => 'first response'));
 284  
 285          $this->check_current_state(question_state::$complete);
 286          $this->check_current_mark(null);
 287          $this->check_step_count(2);
 288          $this->save_quba();
 289  
 290          // Now check how that is re-displayed.
 291          $this->render();
 292          $this->check_output_contains_text_input('answer', 'first response');
 293          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 294  
 295          // Process an autosave.
 296          $this->load_quba();
 297          $this->process_autosave(array('answer' => 'second response'));
 298          $this->check_current_state(question_state::$complete);
 299          $this->check_current_mark(null);
 300          $this->check_step_count(3);
 301          $this->save_quba();
 302  
 303          // Now check how that is re-displayed.
 304          $this->load_quba();
 305          $this->render();
 306          $this->check_output_contains_text_input('answer', 'second response');
 307          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 308  
 309          // Now save for real a third response.
 310          $this->process_submission(array('answer' => 'third response'));
 311  
 312          $this->check_current_state(question_state::$complete);
 313          $this->check_current_mark(null);
 314          $this->check_step_count(3);
 315          $this->save_quba();
 316  
 317          // Now check how that is re-displayed.
 318          $this->render();
 319          $this->check_output_contains_text_input('answer', 'third response');
 320          $this->check_output_contains_hidden_input(':sequencecheck', 3);
 321      }
 322  
 323      public function test_autosave_then_real_save_same() {
 324          $this->resetAfterTest();
 325          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 326          $cat = $generator->create_question_category();
 327          $question = $generator->create_question('shortanswer', null,
 328                  array('category' => $cat->id));
 329  
 330          // Start attempt at a shortanswer question.
 331          $q = question_bank::load_question($question->id);
 332          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 333  
 334          $this->check_current_state(question_state::$todo);
 335          $this->check_current_mark(null);
 336          $this->check_step_count(1);
 337  
 338          // Process a response and check the expected result.
 339          $this->process_submission(array('answer' => 'first response'));
 340  
 341          $this->check_current_state(question_state::$complete);
 342          $this->check_current_mark(null);
 343          $this->check_step_count(2);
 344          $this->save_quba();
 345  
 346          // Now check how that is re-displayed.
 347          $this->render();
 348          $this->check_output_contains_text_input('answer', 'first response');
 349          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 350  
 351          // Process an autosave.
 352          $this->load_quba();
 353          $this->process_autosave(array('answer' => 'second response'));
 354          $this->check_current_state(question_state::$complete);
 355          $this->check_current_mark(null);
 356          $this->check_step_count(3);
 357          $this->save_quba();
 358  
 359          // Now check how that is re-displayed.
 360          $this->load_quba();
 361          $this->render();
 362          $this->check_output_contains_text_input('answer', 'second response');
 363          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 364  
 365          // Now save for real of the same response.
 366          $this->process_submission(array('answer' => 'second response'));
 367  
 368          $this->check_current_state(question_state::$complete);
 369          $this->check_current_mark(null);
 370          $this->check_step_count(3);
 371          $this->save_quba();
 372  
 373          // Now check how that is re-displayed.
 374          $this->render();
 375          $this->check_output_contains_text_input('answer', 'second response');
 376          $this->check_output_contains_hidden_input(':sequencecheck', 3);
 377      }
 378  
 379      public function test_autosave_then_submit() {
 380          $this->resetAfterTest();
 381          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 382          $cat = $generator->create_question_category();
 383          $question = $generator->create_question('shortanswer', null,
 384                  array('category' => $cat->id));
 385  
 386          // Start attempt at a shortanswer question.
 387          $q = question_bank::load_question($question->id);
 388          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 389  
 390          $this->check_current_state(question_state::$todo);
 391          $this->check_current_mark(null);
 392          $this->check_step_count(1);
 393  
 394          // Process a response and check the expected result.
 395          $this->process_submission(array('answer' => 'first response'));
 396  
 397          $this->check_current_state(question_state::$complete);
 398          $this->check_current_mark(null);
 399          $this->check_step_count(2);
 400          $this->save_quba();
 401  
 402          // Now check how that is re-displayed.
 403          $this->render();
 404          $this->check_output_contains_text_input('answer', 'first response');
 405          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 406  
 407          // Process an autosave.
 408          $this->load_quba();
 409          $this->process_autosave(array('answer' => 'second response'));
 410          $this->check_current_state(question_state::$complete);
 411          $this->check_current_mark(null);
 412          $this->check_step_count(3);
 413          $this->save_quba();
 414  
 415          // Now check how that is re-displayed.
 416          $this->load_quba();
 417          $this->render();
 418          $this->check_output_contains_text_input('answer', 'second response');
 419          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 420  
 421          // Now submit a third response.
 422          $this->process_submission(array('answer' => 'third response'));
 423          $this->quba->finish_all_questions();
 424  
 425          $this->check_current_state(question_state::$gradedwrong);
 426          $this->check_current_mark(0);
 427          $this->check_step_count(4);
 428          $this->save_quba();
 429  
 430          // Now check how that is re-displayed.
 431          $this->render();
 432          $this->check_output_contains_text_input('answer', 'third response', false);
 433          $this->check_output_contains_hidden_input(':sequencecheck', 4);
 434      }
 435  
 436      public function test_autosave_and_save_concurrently() {
 437          // This test simulates the following scenario:
 438          // 1. Student looking at a page of the quiz, and edits a field then waits.
 439          // 2. Autosave starts.
 440          // 3. Student immediately clicks Next, which submits the current page.
 441          // In this situation, the real submit should beat the autosave, even
 442          // thought they happen concurrently. We simulate this by opening a
 443          // second db connections.
 444          global $DB;
 445  
 446          // Open second connection
 447          $cfg = $DB->export_dbconfig();
 448          if (!isset($cfg->dboptions)) {
 449              $cfg->dboptions = array();
 450          }
 451          $DB2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
 452          $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
 453  
 454          // Since we need to commit our transactions in a given order, close the
 455          // standard unit test transaction.
 456          $this->preventResetByRollback();
 457  
 458          $this->resetAfterTest();
 459          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 460          $cat = $generator->create_question_category();
 461          $question = $generator->create_question('shortanswer', null,
 462                  array('category' => $cat->id));
 463  
 464          // Start attempt at a shortanswer question.
 465          $q = question_bank::load_question($question->id);
 466          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 467          $this->save_quba();
 468  
 469          $this->check_current_state(question_state::$todo);
 470          $this->check_current_mark(null);
 471          $this->check_step_count(1);
 472  
 473          // Start to process an autosave on $DB.
 474          $transaction = $DB->start_delegated_transaction();
 475          $this->load_quba($DB);
 476          $this->process_autosave(array('answer' => 'autosaved response'));
 477          $this->check_current_state(question_state::$complete);
 478          $this->check_current_mark(null);
 479          $this->check_step_count(2);
 480          $this->save_quba($DB); // Don't commit the transaction yet.
 481  
 482          // Now process a real submit on $DB2 (using a different response).
 483          $transaction2 = $DB2->start_delegated_transaction();
 484          $this->load_quba($DB2);
 485          $this->process_submission(array('answer' => 'real response'));
 486          $this->check_current_state(question_state::$complete);
 487          $this->check_current_mark(null);
 488          $this->check_step_count(2);
 489  
 490          // Now commit the first transaction.
 491          $transaction->allow_commit();
 492  
 493          // Now commit the other transaction.
 494          $this->save_quba($DB2);
 495          $transaction2->allow_commit();
 496  
 497          // Now re-load and check how that is re-displayed.
 498          $this->load_quba();
 499          $this->check_current_state(question_state::$complete);
 500          $this->check_current_mark(null);
 501          $this->check_step_count(2);
 502          $this->render();
 503          $this->check_output_contains_text_input('answer', 'real response');
 504          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 505  
 506          $DB2->dispose();
 507      }
 508  
 509      public function test_concurrent_autosaves() {
 510          // This test simulates the following scenario:
 511          // 1. Student opens  a page of the quiz in two separate browser.
 512          // 2. Autosave starts in both at the same time.
 513          // In this situation, one autosave will work, and the other one will
 514          // get a unique key violation error. This is OK.
 515          global $DB;
 516  
 517          // Open second connection
 518          $cfg = $DB->export_dbconfig();
 519          if (!isset($cfg->dboptions)) {
 520              $cfg->dboptions = array();
 521          }
 522          $DB2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
 523          $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
 524  
 525          // Since we need to commit our transactions in a given order, close the
 526          // standard unit test transaction.
 527          $this->preventResetByRollback();
 528  
 529          $this->resetAfterTest();
 530          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 531          $cat = $generator->create_question_category();
 532          $question = $generator->create_question('shortanswer', null,
 533                  array('category' => $cat->id));
 534  
 535          // Start attempt at a shortanswer question.
 536          $q = question_bank::load_question($question->id);
 537          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 538          $this->save_quba();
 539  
 540          $this->check_current_state(question_state::$todo);
 541          $this->check_current_mark(null);
 542          $this->check_step_count(1);
 543  
 544          // Start to process an autosave on $DB.
 545          $transaction = $DB->start_delegated_transaction();
 546          $this->load_quba($DB);
 547          $this->process_autosave(array('answer' => 'autosaved response 1'));
 548          $this->check_current_state(question_state::$complete);
 549          $this->check_current_mark(null);
 550          $this->check_step_count(2);
 551          $this->save_quba($DB); // Don't commit the transaction yet.
 552  
 553          // Now process a real submit on $DB2 (using a different response).
 554          $transaction2 = $DB2->start_delegated_transaction();
 555          $this->load_quba($DB2);
 556          $this->process_autosave(array('answer' => 'autosaved response 2'));
 557          $this->check_current_state(question_state::$complete);
 558          $this->check_current_mark(null);
 559          $this->check_step_count(2);
 560  
 561          // Now commit the first transaction.
 562          $transaction->allow_commit();
 563  
 564          // Now commit the other transaction.
 565          $this->expectException('dml_write_exception');
 566          $this->save_quba($DB2);
 567          $transaction2->allow_commit();
 568  
 569          // Now re-load and check how that is re-displayed.
 570          $this->load_quba();
 571          $this->check_current_state(question_state::$complete);
 572          $this->check_current_mark(null);
 573          $this->check_step_count(2);
 574          $this->render();
 575          $this->check_output_contains_text_input('answer', 'autosaved response 1');
 576          $this->check_output_contains_hidden_input(':sequencecheck', 1);
 577  
 578          $DB2->dispose();
 579      }
 580  
 581      public function test_autosave_with_wrong_seq_number_ignored() {
 582          $this->resetAfterTest();
 583          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 584          $cat = $generator->create_question_category();
 585          $question = $generator->create_question('shortanswer', null,
 586                  array('category' => $cat->id));
 587  
 588          // Start attempt at a shortanswer question.
 589          $q = question_bank::load_question($question->id);
 590          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 591  
 592          $this->check_current_state(question_state::$todo);
 593          $this->check_current_mark(null);
 594          $this->check_step_count(1);
 595  
 596          // Process a response and check the expected result.
 597          $this->process_submission(array('answer' => 'first response'));
 598  
 599          $this->check_current_state(question_state::$complete);
 600          $this->check_current_mark(null);
 601          $this->check_step_count(2);
 602          $this->save_quba();
 603  
 604          // Now check how that is re-displayed.
 605          $this->render();
 606          $this->check_output_contains_text_input('answer', 'first response');
 607          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 608  
 609          // Process an autosave with a sequence number 1 too small (so from the past).
 610          $this->load_quba();
 611          $postdata = $this->response_data_to_post(array('answer' => 'obsolete response'));
 612          $postdata[$this->quba->get_field_prefix($this->slot) . ':sequencecheck'] = $this->get_question_attempt()->get_sequence_check_count() - 1;
 613          $this->quba->process_all_autosaves(null, $postdata);
 614          $this->check_current_state(question_state::$complete);
 615          $this->check_current_mark(null);
 616          $this->check_step_count(2);
 617          $this->save_quba();
 618  
 619          // Now check how that is re-displayed.
 620          $this->load_quba();
 621          $this->render();
 622          $this->check_output_contains_text_input('answer', 'first response');
 623          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 624  
 625          $this->delete_quba();
 626      }
 627  
 628      public function test_finish_with_unhandled_autosave_data() {
 629          $this->resetAfterTest();
 630          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 631          $cat = $generator->create_question_category();
 632          $question = $generator->create_question('shortanswer', null,
 633                  array('category' => $cat->id));
 634  
 635          // Start attempt at a shortanswer question.
 636          $q = question_bank::load_question($question->id);
 637          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 638  
 639          $this->check_current_state(question_state::$todo);
 640          $this->check_current_mark(null);
 641          $this->check_step_count(1);
 642  
 643          // Process a response and check the expected result.
 644          $this->process_submission(array('answer' => 'cat'));
 645  
 646          $this->check_current_state(question_state::$complete);
 647          $this->check_current_mark(null);
 648          $this->check_step_count(2);
 649          $this->save_quba();
 650  
 651          // Now check how that is re-displayed.
 652          $this->render();
 653          $this->check_output_contains_text_input('answer', 'cat');
 654          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 655  
 656          // Process an autosave.
 657          $this->load_quba();
 658          $this->process_autosave(array('answer' => 'frog'));
 659          $this->check_current_state(question_state::$complete);
 660          $this->check_current_mark(null);
 661          $this->check_step_count(3);
 662          $this->save_quba();
 663  
 664          // Now check how that is re-displayed.
 665          $this->load_quba();
 666          $this->render();
 667          $this->check_output_contains_text_input('answer', 'frog');
 668          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 669  
 670          // Now finishe the attempt, without having done anything since the autosave.
 671          $this->finish();
 672          $this->save_quba();
 673  
 674          // Now check how that has been graded and is re-displayed.
 675          $this->load_quba();
 676          $this->check_current_state(question_state::$gradedright);
 677          $this->check_current_mark(1);
 678          $this->render();
 679          $this->check_output_contains_text_input('answer', 'frog', false);
 680          $this->check_output_contains_hidden_input(':sequencecheck', 4);
 681  
 682          $this->delete_quba();
 683      }
 684  
 685      /**
 686       * Test that regrading doesn't convert autosave steps to finished steps.
 687       * This can result in students loosing data (due to question_out_of_sequence_exception) if a teacher
 688       * regrades an attempt while it is in progress.
 689       */
 690      public function test_autosave_and_regrade_then_display() {
 691          $this->resetAfterTest();
 692          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 693          $cat = $generator->create_question_category();
 694          $question = $generator->create_question('shortanswer', null,
 695                  array('category' => $cat->id));
 696  
 697          // Start attempt at a shortanswer question.
 698          $q = question_bank::load_question($question->id);
 699          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 700  
 701          $this->check_current_state(question_state::$todo);
 702          $this->check_current_mark(null);
 703          $this->check_step_count(1);
 704  
 705          // First see if the starting sequence is right.
 706          $this->render();
 707          $this->check_output_contains_hidden_input(':sequencecheck', 1);
 708  
 709          // Add a submission.
 710          $this->process_submission(array('answer' => 'first response'));
 711          $this->save_quba();
 712  
 713          // Check the submission and that the sequence went up.
 714          $this->render();
 715          $this->check_output_contains_text_input('answer', 'first response');
 716          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 717          $this->assertFalse($this->get_question_attempt()->has_autosaved_step());
 718  
 719          // Add a autosave response.
 720          $this->load_quba();
 721          $this->process_autosave(array('answer' => 'second response'));
 722          $this->save_quba();
 723  
 724          // Confirm that the autosave value shows up, but that the sequence hasn't increased.
 725          $this->render();
 726          $this->check_output_contains_text_input('answer', 'second response');
 727          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 728          $this->assertTrue($this->get_question_attempt()->has_autosaved_step());
 729  
 730          // Call regrade.
 731          $this->load_quba();
 732          $this->quba->regrade_all_questions();
 733          $this->save_quba();
 734  
 735          // Check and see if the autosave response is still there, that the sequence didn't increase,
 736          // and that there is an autosave step.
 737          $this->load_quba();
 738          $this->render();
 739          $this->check_output_contains_text_input('answer', 'second response');
 740          $this->check_output_contains_hidden_input(':sequencecheck', 2);
 741          $this->assertTrue($this->get_question_attempt()->has_autosaved_step());
 742  
 743          $this->delete_quba();
 744      }
 745  
 746      protected function tearDown(): void {
 747          // This test relies on the destructor for the second DB connection being called before running the next test.
 748          // Without this change - there will be unit test failures on "some" DBs (MySQL).
 749          gc_collect_cycles();
 750      }
 751  }