Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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