weekly release 2.7dev
[moodle.git] / question / engine / tests / questionusage_autosave_test.php
CommitLineData
0a606a2b
TH
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
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30require_once(dirname(__FILE__) . '/../lib.php');
31require_once(dirname(__FILE__) . '/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 */
40class 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');
c7fbfe46 68 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 82 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 113 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 127 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 141 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 172 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 186 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 206 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 237 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 251 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 266 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 297 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 311 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 324 $this->check_output_contains_hidden_input(':sequencecheck', 3);
0a606a2b
TH
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');
c7fbfe46 353 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 367 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 380 $this->check_output_contains_hidden_input(':sequencecheck', 3);
0a606a2b
TH
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');
c7fbfe46 409 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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');
c7fbfe46 423 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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);
c7fbfe46 437 $this->check_output_contains_hidden_input(':sequencecheck', 4);
0a606a2b
TH
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');
c7fbfe46 508 $this->check_output_contains_hidden_input(':sequencecheck', 2);
0a606a2b
TH
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->setExpectedException('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');
c7fbfe46 580 $this->check_output_contains_hidden_input(':sequencecheck', 1);
0a606a2b
TH
581
582 $DB2->dispose();
583 }
d122fe32
TH
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');
c7fbfe46 611 $this->check_output_contains_hidden_input(':sequencecheck', 2);
d122fe32 612
c7fbfe46 613 // Process an autosave with a sequence number 1 too small (so from the past).
d122fe32
TH
614 $this->load_quba();
615 $postdata = $this->response_data_to_post(array('answer' => 'obsolete response'));
c7fbfe46 616 $postdata[$this->quba->get_field_prefix($this->slot) . ':sequencecheck'] = $this->get_question_attempt()->get_sequence_check_count() - 1;
d122fe32
TH
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');
c7fbfe46 627 $this->check_output_contains_hidden_input(':sequencecheck', 2);
d122fe32
TH
628
629 $this->delete_quba();
630 }
0a606a2b 631}