MDL-38538 question auto-save back end.
[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');
68
69 // Process an autosave.
70 $this->load_quba();
71 $this->process_autosave(array('answer' => 'second response'));
72 $this->check_current_state(question_state::$complete);
73 $this->check_current_mark(null);
74 $this->check_step_count(3);
75 $this->save_quba();
76
77 // Now check how that is re-displayed.
78 $this->load_quba();
79 $this->render();
80 $this->check_output_contains_text_input('answer', 'second response');
81
82 $this->delete_quba();
83 }
84
85 public function test_autosave_then_autosave_different_data() {
86 $this->resetAfterTest();
87 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
88 $cat = $generator->create_question_category();
89 $question = $generator->create_question('shortanswer', null,
90 array('category' => $cat->id));
91
92 // Start attempt at a shortanswer question.
93 $q = question_bank::load_question($question->id);
94 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
95
96 $this->check_current_state(question_state::$todo);
97 $this->check_current_mark(null);
98 $this->check_step_count(1);
99
100 // Process a response and check the expected result.
101 $this->process_submission(array('answer' => 'first response'));
102
103 $this->check_current_state(question_state::$complete);
104 $this->check_current_mark(null);
105 $this->check_step_count(2);
106 $this->save_quba();
107
108 // Now check how that is re-displayed.
109 $this->render();
110 $this->check_output_contains_text_input('answer', 'first response');
111
112 // Process an autosave.
113 $this->load_quba();
114 $this->process_autosave(array('answer' => 'second response'));
115 $this->check_current_state(question_state::$complete);
116 $this->check_current_mark(null);
117 $this->check_step_count(3);
118 $this->save_quba();
119
120 // Now check how that is re-displayed.
121 $this->load_quba();
122 $this->render();
123 $this->check_output_contains_text_input('answer', 'second response');
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
138 $this->delete_quba();
139 }
140
141 public function test_autosave_then_autosave_same_data() {
142 $this->resetAfterTest();
143 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
144 $cat = $generator->create_question_category();
145 $question = $generator->create_question('shortanswer', null,
146 array('category' => $cat->id));
147
148 // Start attempt at a shortanswer question.
149 $q = question_bank::load_question($question->id);
150 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
151
152 $this->check_current_state(question_state::$todo);
153 $this->check_current_mark(null);
154 $this->check_step_count(1);
155
156 // Process a response and check the expected result.
157 $this->process_submission(array('answer' => 'first response'));
158
159 $this->check_current_state(question_state::$complete);
160 $this->check_current_mark(null);
161 $this->check_step_count(2);
162 $this->save_quba();
163
164 // Now check how that is re-displayed.
165 $this->render();
166 $this->check_output_contains_text_input('answer', 'first response');
167
168 // Process an autosave.
169 $this->load_quba();
170 $this->process_autosave(array('answer' => 'second response'));
171 $this->check_current_state(question_state::$complete);
172 $this->check_current_mark(null);
173 $this->check_step_count(3);
174 $this->save_quba();
175
176 // Now check how that is re-displayed.
177 $this->load_quba();
178 $this->render();
179 $this->check_output_contains_text_input('answer', 'second response');
180
181 $stepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
182
183 // Process a second autosave.
184 $this->load_quba();
185 $this->process_autosave(array('answer' => 'second response'));
186 $this->check_current_state(question_state::$complete);
187 $this->check_current_mark(null);
188 $this->check_step_count(3);
189 $this->save_quba();
190
191 // Try to check it is really the same step
192 $newstepid = $this->quba->get_question_attempt($this->slot)->get_last_step()->get_id();
193 $this->assertEquals($stepid, $newstepid);
194
195 // Now check how that is re-displayed.
196 $this->load_quba();
197 $this->render();
198 $this->check_output_contains_text_input('answer', 'second response');
199
200 $this->delete_quba();
201 }
202
203 public function test_autosave_then_autosave_original_data() {
204 $this->resetAfterTest();
205 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
206 $cat = $generator->create_question_category();
207 $question = $generator->create_question('shortanswer', null,
208 array('category' => $cat->id));
209
210 // Start attempt at a shortanswer question.
211 $q = question_bank::load_question($question->id);
212 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
213
214 $this->check_current_state(question_state::$todo);
215 $this->check_current_mark(null);
216 $this->check_step_count(1);
217
218 // Process a response and check the expected result.
219 $this->process_submission(array('answer' => 'first response'));
220
221 $this->check_current_state(question_state::$complete);
222 $this->check_current_mark(null);
223 $this->check_step_count(2);
224 $this->save_quba();
225
226 // Now check how that is re-displayed.
227 $this->render();
228 $this->check_output_contains_text_input('answer', 'first response');
229
230 // Process an autosave.
231 $this->load_quba();
232 $this->process_autosave(array('answer' => 'second response'));
233 $this->check_current_state(question_state::$complete);
234 $this->check_current_mark(null);
235 $this->check_step_count(3);
236 $this->save_quba();
237
238 // Now check how that is re-displayed.
239 $this->load_quba();
240 $this->render();
241 $this->check_output_contains_text_input('answer', 'second response');
242
243 // Process a second autosave saving the original response.
244 // This should remove the autosave step.
245 $this->load_quba();
246 $this->process_autosave(array('answer' => 'first response'));
247 $this->check_current_state(question_state::$complete);
248 $this->check_current_mark(null);
249 $this->check_step_count(2);
250 $this->save_quba();
251
252 // Now check how that is re-displayed.
253 $this->load_quba();
254 $this->render();
255 $this->check_output_contains_text_input('answer', 'first response');
256
257 $this->delete_quba();
258 }
259
260 public function test_autosave_then_real_save() {
261 $this->resetAfterTest();
262 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
263 $cat = $generator->create_question_category();
264 $question = $generator->create_question('shortanswer', null,
265 array('category' => $cat->id));
266
267 // Start attempt at a shortanswer question.
268 $q = question_bank::load_question($question->id);
269 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
270
271 $this->check_current_state(question_state::$todo);
272 $this->check_current_mark(null);
273 $this->check_step_count(1);
274
275 // Process a response and check the expected result.
276 $this->process_submission(array('answer' => 'first response'));
277
278 $this->check_current_state(question_state::$complete);
279 $this->check_current_mark(null);
280 $this->check_step_count(2);
281 $this->save_quba();
282
283 // Now check how that is re-displayed.
284 $this->render();
285 $this->check_output_contains_text_input('answer', 'first response');
286
287 // Process an autosave.
288 $this->load_quba();
289 $this->process_autosave(array('answer' => 'second response'));
290 $this->check_current_state(question_state::$complete);
291 $this->check_current_mark(null);
292 $this->check_step_count(3);
293 $this->save_quba();
294
295 // Now check how that is re-displayed.
296 $this->load_quba();
297 $this->render();
298 $this->check_output_contains_text_input('answer', 'second response');
299
300 // Now save for real a third response.
301 $this->process_submission(array('answer' => 'third response'));
302
303 $this->check_current_state(question_state::$complete);
304 $this->check_current_mark(null);
305 $this->check_step_count(3);
306 $this->save_quba();
307
308 // Now check how that is re-displayed.
309 $this->render();
310 $this->check_output_contains_text_input('answer', 'third response');
311 }
312
313 public function test_autosave_then_real_save_same() {
314 $this->resetAfterTest();
315 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
316 $cat = $generator->create_question_category();
317 $question = $generator->create_question('shortanswer', null,
318 array('category' => $cat->id));
319
320 // Start attempt at a shortanswer question.
321 $q = question_bank::load_question($question->id);
322 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
323
324 $this->check_current_state(question_state::$todo);
325 $this->check_current_mark(null);
326 $this->check_step_count(1);
327
328 // Process a response and check the expected result.
329 $this->process_submission(array('answer' => 'first response'));
330
331 $this->check_current_state(question_state::$complete);
332 $this->check_current_mark(null);
333 $this->check_step_count(2);
334 $this->save_quba();
335
336 // Now check how that is re-displayed.
337 $this->render();
338 $this->check_output_contains_text_input('answer', 'first response');
339
340 // Process an autosave.
341 $this->load_quba();
342 $this->process_autosave(array('answer' => 'second response'));
343 $this->check_current_state(question_state::$complete);
344 $this->check_current_mark(null);
345 $this->check_step_count(3);
346 $this->save_quba();
347
348 // Now check how that is re-displayed.
349 $this->load_quba();
350 $this->render();
351 $this->check_output_contains_text_input('answer', 'second response');
352
353 // Now save for real of the same response.
354 $this->process_submission(array('answer' => 'second response'));
355
356 $this->check_current_state(question_state::$complete);
357 $this->check_current_mark(null);
358 $this->check_step_count(3);
359 $this->save_quba();
360
361 // Now check how that is re-displayed.
362 $this->render();
363 $this->check_output_contains_text_input('answer', 'second response');
364 }
365
366 public function test_autosave_then_submit() {
367 $this->resetAfterTest();
368 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
369 $cat = $generator->create_question_category();
370 $question = $generator->create_question('shortanswer', null,
371 array('category' => $cat->id));
372
373 // Start attempt at a shortanswer question.
374 $q = question_bank::load_question($question->id);
375 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
376
377 $this->check_current_state(question_state::$todo);
378 $this->check_current_mark(null);
379 $this->check_step_count(1);
380
381 // Process a response and check the expected result.
382 $this->process_submission(array('answer' => 'first response'));
383
384 $this->check_current_state(question_state::$complete);
385 $this->check_current_mark(null);
386 $this->check_step_count(2);
387 $this->save_quba();
388
389 // Now check how that is re-displayed.
390 $this->render();
391 $this->check_output_contains_text_input('answer', 'first response');
392
393 // Process an autosave.
394 $this->load_quba();
395 $this->process_autosave(array('answer' => 'second response'));
396 $this->check_current_state(question_state::$complete);
397 $this->check_current_mark(null);
398 $this->check_step_count(3);
399 $this->save_quba();
400
401 // Now check how that is re-displayed.
402 $this->load_quba();
403 $this->render();
404 $this->check_output_contains_text_input('answer', 'second response');
405
406 // Now submit a third response.
407 $this->process_submission(array('answer' => 'third response'));
408 $this->quba->finish_all_questions();
409
410 $this->check_current_state(question_state::$gradedwrong);
411 $this->check_current_mark(0);
412 $this->check_step_count(4);
413 $this->save_quba();
414
415 // Now check how that is re-displayed.
416 $this->render();
417 $this->check_output_contains_text_input('answer', 'third response', false);
418 }
419
420 public function test_autosave_and_save_concurrently() {
421 // This test simulates the following scenario:
422 // 1. Student looking at a page of the quiz, and edits a field then waits.
423 // 2. Autosave starts.
424 // 3. Student immediately clicks Next, which submits the current page.
425 // In this situation, the real submit should beat the autosave, even
426 // thought they happen concurrently. We simulate this by opening a
427 // second db connections.
428 global $DB;
429
430 // Open second connection
431 $cfg = $DB->export_dbconfig();
432 if (!isset($cfg->dboptions)) {
433 $cfg->dboptions = array();
434 }
435 $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
436 $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
437
438 // Since we need to commit our transactions in a given order, close the
439 // standard unit test transaction.
440 $this->preventResetByRollback();
441
442 $this->resetAfterTest();
443 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
444 $cat = $generator->create_question_category();
445 $question = $generator->create_question('shortanswer', null,
446 array('category' => $cat->id));
447
448 // Start attempt at a shortanswer question.
449 $q = question_bank::load_question($question->id);
450 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
451 $this->save_quba();
452
453 $this->check_current_state(question_state::$todo);
454 $this->check_current_mark(null);
455 $this->check_step_count(1);
456
457 // Start to process an autosave on $DB.
458 $transaction = $DB->start_delegated_transaction();
459 $this->load_quba($DB);
460 $this->process_autosave(array('answer' => 'autosaved response'));
461 $this->check_current_state(question_state::$complete);
462 $this->check_current_mark(null);
463 $this->check_step_count(2);
464 $this->save_quba($DB); // Don't commit the transaction yet.
465
466 // Now process a real submit on $DB2 (using a different response).
467 $transaction2 = $DB2->start_delegated_transaction();
468 $this->load_quba($DB2);
469 $this->process_submission(array('answer' => 'real response'));
470 $this->check_current_state(question_state::$complete);
471 $this->check_current_mark(null);
472 $this->check_step_count(2);
473
474 // Now commit the first transaction.
475 $transaction->allow_commit();
476
477 // Now commit the other transaction.
478 $this->save_quba($DB2);
479 $transaction2->allow_commit();
480
481 // Now re-load and check how that is re-displayed.
482 $this->load_quba();
483 $this->check_current_state(question_state::$complete);
484 $this->check_current_mark(null);
485 $this->check_step_count(2);
486 $this->render();
487 $this->check_output_contains_text_input('answer', 'real response');
488
489 $DB2->dispose();
490 }
491
492 public function test_concurrent_autosaves() {
493 // This test simulates the following scenario:
494 // 1. Student opens a page of the quiz in two separate browser.
495 // 2. Autosave starts in both at the same time.
496 // In this situation, one autosave will work, and the other one will
497 // get a unique key violation error. This is OK.
498 global $DB;
499
500 // Open second connection
501 $cfg = $DB->export_dbconfig();
502 if (!isset($cfg->dboptions)) {
503 $cfg->dboptions = array();
504 }
505 $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
506 $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
507
508 // Since we need to commit our transactions in a given order, close the
509 // standard unit test transaction.
510 $this->preventResetByRollback();
511
512 $this->resetAfterTest();
513 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
514 $cat = $generator->create_question_category();
515 $question = $generator->create_question('shortanswer', null,
516 array('category' => $cat->id));
517
518 // Start attempt at a shortanswer question.
519 $q = question_bank::load_question($question->id);
520 $this->start_attempt_at_question($q, 'deferredfeedback', 1);
521 $this->save_quba();
522
523 $this->check_current_state(question_state::$todo);
524 $this->check_current_mark(null);
525 $this->check_step_count(1);
526
527 // Start to process an autosave on $DB.
528 $transaction = $DB->start_delegated_transaction();
529 $this->load_quba($DB);
530 $this->process_autosave(array('answer' => 'autosaved response 1'));
531 $this->check_current_state(question_state::$complete);
532 $this->check_current_mark(null);
533 $this->check_step_count(2);
534 $this->save_quba($DB); // Don't commit the transaction yet.
535
536 // Now process a real submit on $DB2 (using a different response).
537 $transaction2 = $DB2->start_delegated_transaction();
538 $this->load_quba($DB2);
539 $this->process_autosave(array('answer' => 'autosaved response 2'));
540 $this->check_current_state(question_state::$complete);
541 $this->check_current_mark(null);
542 $this->check_step_count(2);
543
544 // Now commit the first transaction.
545 $transaction->allow_commit();
546
547 // Now commit the other transaction.
548 $this->setExpectedException('dml_write_exception');
549 $this->save_quba($DB2);
550 $transaction2->allow_commit();
551
552 // Now re-load and check how that is re-displayed.
553 $this->load_quba();
554 $this->check_current_state(question_state::$complete);
555 $this->check_current_mark(null);
556 $this->check_step_count(2);
557 $this->render();
558 $this->check_output_contains_text_input('answer', 'autosaved response 1');
559
560 $DB2->dispose();
561 }
562}