MDL-67673 phpunit: Move tests to use new phpunit_dataset
[moodle.git] / mod / quiz / tests / attempt_walkthrough_from_csv_test.php
CommitLineData
da6bb0c5
JP
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 * Quiz attempt walk through using data from csv file.
19 *
20 * @package mod_quiz
21 * @category phpunit
22 * @copyright 2013 The Open University
23 * @author Jamie Pratt <me@jamiep.org>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
da6bb0c5
JP
30require_once($CFG->dirroot . '/mod/quiz/locallib.php');
31
32/**
33 * Quiz attempt walk through using data from csv file.
34 *
35 * @package mod_quiz
36 * @category phpunit
37 * @copyright 2013 The Open University
38 * @author Jamie Pratt <me@jamiep.org>
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 */
41class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
42
3652dddd
JP
43 protected $files = array('questions', 'steps', 'results');
44
da6bb0c5
JP
45 /**
46 * @var stdClass the quiz record we create.
47 */
48 protected $quiz;
49
50 /**
51 * @var array with slot no => question name => questionid. Question ids of questions created in the same category as random q.
52 */
53 protected $randqids;
54
ea6db479
JP
55 /**
56 * The only test in this class. This is run multiple times depending on how many sets of files there are in fixtures/
57 * directory.
58 *
59 * @param array $quizsettings of settings read from csv file quizzes.csv
1f13dff1 60 * @param array $csvdata of data read from csv file "questionsXX.csv", "stepsXX.csv" and "resultsXX.csv".
ea6db479
JP
61 * @dataProvider get_data_for_walkthrough
62 */
63 public function test_walkthrough_from_csv($quizsettings, $csvdata) {
64
65 // CSV data files for these tests were generated using :
66 // https://github.com/jamiepratt/moodle-quiz-tools/tree/master/responsegenerator
67
68 $this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);
69 }
70
764f6153 71 public function create_quiz($quizsettings, $qs) {
da6bb0c5
JP
72 global $SITE, $DB;
73 $this->setAdminUser();
74
75 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
76 $slots = array();
77 $qidsbycat = array();
78 $sumofgrades = 0;
1f13dff1
EL
79 foreach ($qs as $qsrow) {
80 $q = $this->explode_dot_separated_keys_to_make_subindexs($qsrow);
da6bb0c5
JP
81
82 $catname = array('name' => $q['cat']);
83 if (!$cat = $DB->get_record('question_categories', array('name' => $q['cat']))) {
84 $cat = $questiongenerator->create_question_category($catname);
85 }
86 $q['catid'] = $cat->id;
87 foreach (array('which' => null, 'overrides' => array()) as $key => $default) {
88 if (empty($q[$key])) {
89 $q[$key] = $default;
90 }
91 }
92
93 if ($q['type'] !== 'random') {
94 // Don't actually create random questions here.
95 $overrides = array('category' => $cat->id, 'defaultmark' => $q['mark']) + $q['overrides'];
5c3f700a
TH
96 if ($q['type'] === 'truefalse') {
97 // True/false question can never have hints, but sometimes we need to put them
98 // in the CSV file, to keep it rectangular.
99 unset($overrides['hint']);
100 }
da6bb0c5
JP
101 $question = $questiongenerator->create_question($q['type'], $q['which'], $overrides);
102 $q['id'] = $question->id;
103
104 if (!isset($qidsbycat[$q['cat']])) {
105 $qidsbycat[$q['cat']] = array();
106 }
107 if (!empty($q['which'])) {
108 $name = $q['type'].'_'.$q['which'];
109 } else {
110 $name = $q['type'];
111 }
112 $qidsbycat[$q['catid']][$name] = $q['id'];
113 }
114 if (!empty($q['slot'])) {
115 $slots[$q['slot']] = $q;
116 $sumofgrades += $q['mark'];
117 }
118 }
119
120 ksort($slots);
121
122 // Make a quiz.
123 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
764f6153
JP
124
125 // Settings from param override defaults.
ea6db479 126 $aggregratedsettings = $quizsettings + array('course' => $SITE->id,
764f6153
JP
127 'questionsperpage' => 0,
128 'grade' => 100.0,
129 'sumgrades' => $sumofgrades);
130
131 $this->quiz = $quizgenerator->create_instance($aggregratedsettings);
da6bb0c5
JP
132
133 $this->randqids = array();
134 foreach ($slots as $slotno => $slotquestion) {
135 if ($slotquestion['type'] !== 'random') {
ccba5b88 136 quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
da6bb0c5
JP
137 } else {
138 quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0);
139 $this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
140 }
141 }
142 }
143
ea6db479
JP
144 /**
145 * Create quiz, simulate attempts and check results (if resultsXX.csv exists).
146 *
147 * @param array $quizsettings Quiz overrides for this quiz.
1f13dff1 148 * @param array $csvdata Data loaded from csv files for this test.
ea6db479
JP
149 */
150 protected function create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata) {
151 $this->resetAfterTest(true);
152 question_bank::get_qtype('random')->clear_caches_before_testing();
153
154 $this->create_quiz($quizsettings, $csvdata['questions']);
155
156 $attemptids = $this->walkthrough_attempts($csvdata['steps']);
157
158 if (isset($csvdata['results'])) {
159 $this->check_attempts_results($csvdata['results'], $attemptids);
160 }
161 }
162
da6bb0c5
JP
163 /**
164 * Get full path of CSV file.
165 *
166 * @param string $setname
167 * @param string $test
168 * @return string full path of file.
169 */
170 protected function get_full_path_of_csv_file($setname, $test) {
171 return __DIR__."/fixtures/{$setname}{$test}.csv";
172 }
173
174 /**
175 * Load dataset from CSV file "{$setname}{$test}.csv".
176 *
177 * @param string $setname
178 * @param string $test
1f13dff1 179 * @return array
da6bb0c5 180 */
764f6153 181 protected function load_csv_data_file($setname, $test='') {
da6bb0c5 182 $files = array($setname => $this->get_full_path_of_csv_file($setname, $test));
1f13dff1 183 return $this->dataset_from_files($files)->get_rows([$setname]);
da6bb0c5
JP
184 }
185
e39a2faa
JP
186 /**
187 * Break down row of csv data into sub arrays, according to column names.
188 *
189 * @param array $row from csv file with field names with parts separate by '.'.
190 * @return array the row with each part of the field name following a '.' being a separate sub array's index.
191 */
192 protected function explode_dot_separated_keys_to_make_subindexs(array $row) {
193 $parts = array();
194 foreach ($row as $columnkey => $value) {
195 $newkeys = explode('.', trim($columnkey));
196 $placetoputvalue =& $parts;
197 foreach ($newkeys as $newkeydepth => $newkey) {
198 if ($newkeydepth + 1 === count($newkeys)) {
199 $placetoputvalue[$newkey] = $value;
200 } else {
201 // Going deeper down.
202 if (!isset($placetoputvalue[$newkey])) {
203 $placetoputvalue[$newkey] = array();
204 }
205 $placetoputvalue =& $placetoputvalue[$newkey];
206 }
207 }
208 }
209 return $parts;
210 }
211
3652dddd
JP
212 /**
213 * Data provider method for test_walkthrough_from_csv. Called by PHPUnit.
214 *
764f6153 215 * @return array One array element for each run of the test. Each element contains an array with the params for
3652dddd
JP
216 * test_walkthrough_from_csv.
217 */
218 public function get_data_for_walkthrough() {
1f13dff1 219 $quizzes = $this->load_csv_data_file('quizzes')['quizzes'];
3652dddd 220 $datasets = array();
1f13dff1 221 foreach ($quizzes as $quizsettings) {
3652dddd
JP
222 $dataset = array();
223 foreach ($this->files as $file) {
038014c4 224 if (file_exists($this->get_full_path_of_csv_file($file, $quizsettings['testnumber']))) {
1f13dff1 225 $dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber'])[$file];
038014c4 226 }
3652dddd 227 }
764f6153 228 $datasets[] = array($quizsettings, $dataset);
3652dddd
JP
229 }
230 return $datasets;
231 }
232
3652dddd 233 /**
1f13dff1 234 * @param $steps array the step data from the csv file.
3652dddd
JP
235 * @return array attempt no as in csv file => the id of the quiz_attempt as stored in the db.
236 */
237 protected function walkthrough_attempts($steps) {
238 global $DB;
e39a2faa 239 $attemptids = array();
1f13dff1 240 foreach ($steps as $steprow) {
da6bb0c5 241
1f13dff1 242 $step = $this->explode_dot_separated_keys_to_make_subindexs($steprow);
da6bb0c5
JP
243 // Find existing user or make a new user to do the quiz.
244 $username = array('firstname' => $step['firstname'],
3652dddd 245 'lastname' => $step['lastname']);
da6bb0c5
JP
246
247 if (!$user = $DB->get_record('user', $username)) {
248 $user = $this->getDataGenerator()->create_user($username);
249 }
764f6153
JP
250
251 if (!isset($attemptids[$step['quizattempt']])) {
252 // Start the attempt.
253 $quizobj = quiz::create($this->quiz->id, $user->id);
254 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
255 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
256
257 $prevattempts = quiz_get_user_attempts($this->quiz->id, $user->id, 'all', true);
258 $attemptnumber = count($prevattempts) + 1;
259 $timenow = time();
260 $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, false, $user->id);
261 // Select variant and / or random sub question.
262 if (!isset($step['variants'])) {
263 $step['variants'] = array();
da6bb0c5 264 }
764f6153
JP
265 if (isset($step['randqs'])) {
266 // Replace 'names' with ids.
267 foreach ($step['randqs'] as $slotno => $randqname) {
268 $step['randqs'][$slotno] = $this->randqids[$slotno][$randqname];
269 }
270 } else {
271 $step['randqs'] = array();
272 }
273
274 quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $step['randqs'], $step['variants']);
275 quiz_attempt_save_started($quizobj, $quba, $attempt);
276 $attemptid = $attemptids[$step['quizattempt']] = $attempt->id;
da6bb0c5 277 } else {
764f6153 278 $attemptid = $attemptids[$step['quizattempt']];
da6bb0c5 279 }
da6bb0c5
JP
280
281 // Process some responses from the student.
764f6153 282 $attemptobj = quiz_attempt::create($attemptid);
da6bb0c5
JP
283 $attemptobj->process_submitted_actions($timenow, false, $step['responses']);
284
285 // Finish the attempt.
764f6153
JP
286 if (!isset($step['finished']) || ($step['finished'] == 1)) {
287 $attemptobj = quiz_attempt::create($attemptid);
288 $attemptobj->process_finish($timenow, false);
289 }
e39a2faa 290 }
3652dddd
JP
291 return $attemptids;
292 }
e39a2faa 293
3652dddd 294 /**
1f13dff1 295 * @param $results array the results data from the csv file.
3652dddd
JP
296 * @param $attemptids array attempt no as in csv file => the id of the quiz_attempt as stored in the db.
297 */
298 protected function check_attempts_results($results, $attemptids) {
1f13dff1
EL
299 foreach ($results as $resultrow) {
300 $result = $this->explode_dot_separated_keys_to_make_subindexs($resultrow);
da6bb0c5 301 // Re-load quiz attempt data.
e39a2faa
JP
302 $attemptobj = quiz_attempt::create($attemptids[$result['quizattempt']]);
303 $this->check_attempt_results($result, $attemptobj);
da6bb0c5
JP
304 }
305 }
306
307 /**
e39a2faa 308 * Check that attempt results are as specified in $result.
da6bb0c5 309 *
e39a2faa
JP
310 * @param array $result row of data read from csv file.
311 * @param quiz_attempt $attemptobj the attempt object loaded from db.
312 * @throws coding_exception
da6bb0c5 313 */
e39a2faa
JP
314 protected function check_attempt_results($result, $attemptobj) {
315 foreach ($result as $fieldname => $value) {
764f6153
JP
316 if ($value === '!NULL!') {
317 $value = null;
318 }
e39a2faa
JP
319 switch ($fieldname) {
320 case 'quizattempt' :
321 break;
322 case 'attemptnumber' :
323 $this->assertEquals($value, $attemptobj->get_attempt_number());
324 break;
325 case 'slots' :
326 foreach ($value as $slotno => $slottests) {
327 foreach ($slottests as $slotfieldname => $slotvalue) {
328 switch ($slotfieldname) {
329 case 'mark' :
330 $this->assertEquals(round($slotvalue, 2), $attemptobj->get_question_mark($slotno),
331 "Mark for slot $slotno of attempt {$result['quizattempt']}.");
332 break;
333 default :
334 throw new coding_exception('Unknown slots sub field column in csv file '
3652dddd 335 .s($slotfieldname));
e39a2faa
JP
336 }
337 }
da6bb0c5 338 }
e39a2faa
JP
339 break;
340 case 'finished' :
764f6153 341 $this->assertEquals((bool)$value, $attemptobj->is_finished());
e39a2faa
JP
342 break;
343 case 'summarks' :
6bcc5dfd
JP
344 $this->assertEquals((float)$value, $attemptobj->get_sum_marks(),
345 "Sum of marks of attempt {$result['quizattempt']}.");
e39a2faa
JP
346 break;
347 case 'quizgrade' :
348 // Check quiz grades.
349 $grades = quiz_get_user_grades($attemptobj->get_quiz(), $attemptobj->get_userid());
350 $grade = array_shift($grades);
351 $this->assertEquals($value, $grade->rawgrade, "Quiz grade for attempt {$result['quizattempt']}.");
352 break;
353 case 'gradebookgrade' :
354 // Check grade book.
355 $gradebookgrades = grade_get_grades($attemptobj->get_courseid(),
356 'mod', 'quiz',
357 $attemptobj->get_quizid(),
358 $attemptobj->get_userid());
359 $gradebookitem = array_shift($gradebookgrades->items);
360 $gradebookgrade = array_shift($gradebookitem->grades);
361 $this->assertEquals($value, $gradebookgrade->grade, "Gradebook grade for attempt {$result['quizattempt']}.");
362 break;
363 default :
364 throw new coding_exception('Unknown column in csv file '.s($fieldname));
da6bb0c5
JP
365 }
366 }
da6bb0c5
JP
367 }
368}