2
// This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18
* This file contains tests for the autosave code in the question_usage class.
21
* @subpackage questionengine
22
* @copyright 2013 The Open University
23
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
defined('MOODLE_INTERNAL') || die();
30
require_once(dirname(__FILE__) . '/../lib.php');
31
require_once(dirname(__FILE__) . '/helpers.php');
35
* Unit tests for the autosave parts of the {@link question_usage} class.
37
* @copyright 2013 The Open University
38
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
class question_usage_autosave_test extends qbehaviour_walkthrough_test_base {
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));
49
// Start attempt at a shortanswer question.
50
$q = question_bank::load_question($question->id);
51
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
53
$this->check_current_state(question_state::$todo);
54
$this->check_current_mark(null);
55
$this->check_step_count(1);
57
// Process a response and check the expected result.
58
$this->process_submission(array('answer' => 'first response'));
60
$this->check_current_state(question_state::$complete);
61
$this->check_current_mark(null);
62
$this->check_step_count(2);
65
// Now check how that is re-displayed.
67
$this->check_output_contains_text_input('answer', 'first response');
68
$this->check_output_contains_hidden_input(':sequencecheck', 2);
70
// Process an autosave.
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);
78
// Now check how that is re-displayed.
81
$this->check_output_contains_text_input('answer', 'second response');
82
$this->check_output_contains_hidden_input(':sequencecheck', 2);
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));
94
// Start attempt at a shortanswer question.
95
$q = question_bank::load_question($question->id);
96
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
98
$this->check_current_state(question_state::$todo);
99
$this->check_current_mark(null);
100
$this->check_step_count(1);
102
// Process a response and check the expected result.
103
$this->process_submission(array('answer' => 'first response'));
105
$this->check_current_state(question_state::$complete);
106
$this->check_current_mark(null);
107
$this->check_step_count(2);
110
// Now check how that is re-displayed.
112
$this->check_output_contains_text_input('answer', 'first response');
113
$this->check_output_contains_hidden_input(':sequencecheck', 2);
115
// Process an autosave.
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);
123
// Now check how that is re-displayed.
126
$this->check_output_contains_text_input('answer', 'second response');
127
$this->check_output_contains_hidden_input(':sequencecheck', 2);
129
// Process a second autosave.
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);
137
// Now check how that is re-displayed.
140
$this->check_output_contains_text_input('answer', 'third response');
141
$this->check_output_contains_hidden_input(':sequencecheck', 2);
143
$this->delete_quba();
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));
153
// Start attempt at a shortanswer question.
154
$q = question_bank::load_question($question->id);
155
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
157
$this->check_current_state(question_state::$todo);
158
$this->check_current_mark(null);
159
$this->check_step_count(1);
161
// Process a response and check the expected result.
162
$this->process_submission(array('answer' => 'first response'));
164
$this->check_current_state(question_state::$complete);
165
$this->check_current_mark(null);
166
$this->check_step_count(2);
169
// Now check how that is re-displayed.
171
$this->check_output_contains_text_input('answer', 'first response');
172
$this->check_output_contains_hidden_input(':sequencecheck', 2);
174
// Process an autosave.
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);
182
// Now check how that is re-displayed.
185
$this->check_output_contains_text_input('answer', 'second response');
186
$this->check_output_contains_hidden_input(':sequencecheck', 2);
188
$stepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
190
// Process a second autosave.
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);
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);
202
// Now check how that is re-displayed.
205
$this->check_output_contains_text_input('answer', 'second response');
206
$this->check_output_contains_hidden_input(':sequencecheck', 2);
208
$this->delete_quba();
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));
218
// Start attempt at a shortanswer question.
219
$q = question_bank::load_question($question->id);
220
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
222
$this->check_current_state(question_state::$todo);
223
$this->check_current_mark(null);
224
$this->check_step_count(1);
226
// Process a response and check the expected result.
227
$this->process_submission(array('answer' => 'first response'));
229
$this->check_current_state(question_state::$complete);
230
$this->check_current_mark(null);
231
$this->check_step_count(2);
234
// Now check how that is re-displayed.
236
$this->check_output_contains_text_input('answer', 'first response');
237
$this->check_output_contains_hidden_input(':sequencecheck', 2);
239
// Process an autosave.
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);
247
// Now check how that is re-displayed.
250
$this->check_output_contains_text_input('answer', 'second response');
251
$this->check_output_contains_hidden_input(':sequencecheck', 2);
253
// Process a second autosave saving the original response.
254
// This should remove the autosave step.
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);
262
// Now check how that is re-displayed.
265
$this->check_output_contains_text_input('answer', 'first response');
266
$this->check_output_contains_hidden_input(':sequencecheck', 2);
268
$this->delete_quba();
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));
278
// Start attempt at a shortanswer question.
279
$q = question_bank::load_question($question->id);
280
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
282
$this->check_current_state(question_state::$todo);
283
$this->check_current_mark(null);
284
$this->check_step_count(1);
286
// Process a response and check the expected result.
287
$this->process_submission(array('answer' => 'first response'));
289
$this->check_current_state(question_state::$complete);
290
$this->check_current_mark(null);
291
$this->check_step_count(2);
294
// Now check how that is re-displayed.
296
$this->check_output_contains_text_input('answer', 'first response');
297
$this->check_output_contains_hidden_input(':sequencecheck', 2);
299
// Process an autosave.
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);
307
// Now check how that is re-displayed.
310
$this->check_output_contains_text_input('answer', 'second response');
311
$this->check_output_contains_hidden_input(':sequencecheck', 2);
313
// Now save for real a third response.
314
$this->process_submission(array('answer' => 'third response'));
316
$this->check_current_state(question_state::$complete);
317
$this->check_current_mark(null);
318
$this->check_step_count(3);
321
// Now check how that is re-displayed.
323
$this->check_output_contains_text_input('answer', 'third response');
324
$this->check_output_contains_hidden_input(':sequencecheck', 3);
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));
334
// Start attempt at a shortanswer question.
335
$q = question_bank::load_question($question->id);
336
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
338
$this->check_current_state(question_state::$todo);
339
$this->check_current_mark(null);
340
$this->check_step_count(1);
342
// Process a response and check the expected result.
343
$this->process_submission(array('answer' => 'first response'));
345
$this->check_current_state(question_state::$complete);
346
$this->check_current_mark(null);
347
$this->check_step_count(2);
350
// Now check how that is re-displayed.
352
$this->check_output_contains_text_input('answer', 'first response');
353
$this->check_output_contains_hidden_input(':sequencecheck', 2);
355
// Process an autosave.
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);
363
// Now check how that is re-displayed.
366
$this->check_output_contains_text_input('answer', 'second response');
367
$this->check_output_contains_hidden_input(':sequencecheck', 2);
369
// Now save for real of the same response.
370
$this->process_submission(array('answer' => 'second response'));
372
$this->check_current_state(question_state::$complete);
373
$this->check_current_mark(null);
374
$this->check_step_count(3);
377
// Now check how that is re-displayed.
379
$this->check_output_contains_text_input('answer', 'second response');
380
$this->check_output_contains_hidden_input(':sequencecheck', 3);
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));
390
// Start attempt at a shortanswer question.
391
$q = question_bank::load_question($question->id);
392
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
394
$this->check_current_state(question_state::$todo);
395
$this->check_current_mark(null);
396
$this->check_step_count(1);
398
// Process a response and check the expected result.
399
$this->process_submission(array('answer' => 'first response'));
401
$this->check_current_state(question_state::$complete);
402
$this->check_current_mark(null);
403
$this->check_step_count(2);
406
// Now check how that is re-displayed.
408
$this->check_output_contains_text_input('answer', 'first response');
409
$this->check_output_contains_hidden_input(':sequencecheck', 2);
411
// Process an autosave.
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);
419
// Now check how that is re-displayed.
422
$this->check_output_contains_text_input('answer', 'second response');
423
$this->check_output_contains_hidden_input(':sequencecheck', 2);
425
// Now submit a third response.
426
$this->process_submission(array('answer' => 'third response'));
427
$this->quba->finish_all_questions();
429
$this->check_current_state(question_state::$gradedwrong);
430
$this->check_current_mark(0);
431
$this->check_step_count(4);
434
// Now check how that is re-displayed.
436
$this->check_output_contains_text_input('answer', 'third response', false);
437
$this->check_output_contains_hidden_input(':sequencecheck', 4);
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.
450
// Open second connection
451
$cfg = $DB->export_dbconfig();
452
if (!isset($cfg->dboptions)) {
453
$cfg->dboptions = array();
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);
458
// Since we need to commit our transactions in a given order, close the
459
// standard unit test transaction.
460
$this->preventResetByRollback();
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));
468
// Start attempt at a shortanswer question.
469
$q = question_bank::load_question($question->id);
470
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
473
$this->check_current_state(question_state::$todo);
474
$this->check_current_mark(null);
475
$this->check_step_count(1);
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.
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);
494
// Now commit the first transaction.
495
$transaction->allow_commit();
497
// Now commit the other transaction.
498
$this->save_quba($DB2);
499
$transaction2->allow_commit();
501
// Now re-load and check how that is re-displayed.
503
$this->check_current_state(question_state::$complete);
504
$this->check_current_mark(null);
505
$this->check_step_count(2);
507
$this->check_output_contains_text_input('answer', 'real response');
508
$this->check_output_contains_hidden_input(':sequencecheck', 2);
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.
521
// Open second connection
522
$cfg = $DB->export_dbconfig();
523
if (!isset($cfg->dboptions)) {
524
$cfg->dboptions = array();
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);
529
// Since we need to commit our transactions in a given order, close the
530
// standard unit test transaction.
531
$this->preventResetByRollback();
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));
539
// Start attempt at a shortanswer question.
540
$q = question_bank::load_question($question->id);
541
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
544
$this->check_current_state(question_state::$todo);
545
$this->check_current_mark(null);
546
$this->check_step_count(1);
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.
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);
565
// Now commit the first transaction.
566
$transaction->allow_commit();
568
// Now commit the other transaction.
569
$this->setExpectedException('dml_write_exception');
570
$this->save_quba($DB2);
571
$transaction2->allow_commit();
573
// Now re-load and check how that is re-displayed.
575
$this->check_current_state(question_state::$complete);
576
$this->check_current_mark(null);
577
$this->check_step_count(2);
579
$this->check_output_contains_text_input('answer', 'autosaved response 1');
580
$this->check_output_contains_hidden_input(':sequencecheck', 1);
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));
592
// Start attempt at a shortanswer question.
593
$q = question_bank::load_question($question->id);
594
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
596
$this->check_current_state(question_state::$todo);
597
$this->check_current_mark(null);
598
$this->check_step_count(1);
600
// Process a response and check the expected result.
601
$this->process_submission(array('answer' => 'first response'));
603
$this->check_current_state(question_state::$complete);
604
$this->check_current_mark(null);
605
$this->check_step_count(2);
608
// Now check how that is re-displayed.
610
$this->check_output_contains_text_input('answer', 'first response');
611
$this->check_output_contains_hidden_input(':sequencecheck', 2);
613
// Process an autosave with a sequence number 1 too small (so from the past).
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);
623
// Now check how that is re-displayed.
626
$this->check_output_contains_text_input('answer', 'first response');
627
$this->check_output_contains_hidden_input(':sequencecheck', 2);
629
$this->delete_quba();