MDL-67673 phpunit: Move tests to use new phpunit_dataset
[moodle.git] / mod / quiz / tests / attempt_walkthrough_from_csv_test.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
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  */
41 class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
43     protected $files = array('questions', 'steps', 'results');
45     /**
46      * @var stdClass the quiz record we create.
47      */
48     protected $quiz;
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;
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
60      * @param array $csvdata of data read from csv file "questionsXX.csv", "stepsXX.csv" and "resultsXX.csv".
61      * @dataProvider get_data_for_walkthrough
62      */
63     public function test_walkthrough_from_csv($quizsettings, $csvdata) {
65         // CSV data files for these tests were generated using :
66         // https://github.com/jamiepratt/moodle-quiz-tools/tree/master/responsegenerator
68         $this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);
69     }
71     public function create_quiz($quizsettings, $qs) {
72         global $SITE, $DB;
73         $this->setAdminUser();
75         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
76         $slots = array();
77         $qidsbycat = array();
78         $sumofgrades = 0;
79         foreach ($qs as $qsrow) {
80             $q = $this->explode_dot_separated_keys_to_make_subindexs($qsrow);
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             }
93             if ($q['type'] !== 'random') {
94                 // Don't actually create random questions here.
95                 $overrides = array('category' => $cat->id, 'defaultmark' => $q['mark']) + $q['overrides'];
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                 }
101                 $question = $questiongenerator->create_question($q['type'], $q['which'], $overrides);
102                 $q['id'] = $question->id;
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         }
120         ksort($slots);
122         // Make a quiz.
123         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
125         // Settings from param override defaults.
126         $aggregratedsettings = $quizsettings + array('course' => $SITE->id,
127                                                      'questionsperpage' => 0,
128                                                      'grade' => 100.0,
129                                                      'sumgrades' => $sumofgrades);
131         $this->quiz = $quizgenerator->create_instance($aggregratedsettings);
133         $this->randqids = array();
134         foreach ($slots as $slotno => $slotquestion) {
135             if ($slotquestion['type'] !== 'random') {
136                 quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
137             } else {
138                 quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0);
139                 $this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
140             }
141         }
142     }
144     /**
145      * Create quiz, simulate attempts and check results (if resultsXX.csv exists).
146      *
147      * @param array $quizsettings Quiz overrides for this quiz.
148      * @param array $csvdata Data loaded from csv files for this test.
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();
154         $this->create_quiz($quizsettings, $csvdata['questions']);
156         $attemptids = $this->walkthrough_attempts($csvdata['steps']);
158         if (isset($csvdata['results'])) {
159             $this->check_attempts_results($csvdata['results'], $attemptids);
160         }
161     }
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     }
174     /**
175      * Load dataset from CSV file "{$setname}{$test}.csv".
176      *
177      * @param string $setname
178      * @param string $test
179      * @return array
180      */
181     protected function load_csv_data_file($setname, $test='') {
182         $files = array($setname => $this->get_full_path_of_csv_file($setname, $test));
183         return $this->dataset_from_files($files)->get_rows([$setname]);
184     }
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     }
212     /**
213      * Data provider method for test_walkthrough_from_csv. Called by PHPUnit.
214      *
215      * @return array One array element for each run of the test. Each element contains an array with the params for
216      *                  test_walkthrough_from_csv.
217      */
218     public function get_data_for_walkthrough() {
219         $quizzes = $this->load_csv_data_file('quizzes')['quizzes'];
220         $datasets = array();
221         foreach ($quizzes as $quizsettings) {
222             $dataset = array();
223             foreach ($this->files as $file) {
224                 if (file_exists($this->get_full_path_of_csv_file($file, $quizsettings['testnumber']))) {
225                     $dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber'])[$file];
226                 }
227             }
228             $datasets[] = array($quizsettings, $dataset);
229         }
230         return $datasets;
231     }
233     /**
234      * @param $steps array the step data from the csv file.
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;
239         $attemptids = array();
240         foreach ($steps as $steprow) {
242             $step = $this->explode_dot_separated_keys_to_make_subindexs($steprow);
243             // Find existing user or make a new user to do the quiz.
244             $username = array('firstname' => $step['firstname'],
245                               'lastname'  => $step['lastname']);
247             if (!$user = $DB->get_record('user', $username)) {
248                 $user = $this->getDataGenerator()->create_user($username);
249             }
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);
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();
264                 }
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                 }
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;
277             } else {
278                 $attemptid = $attemptids[$step['quizattempt']];
279             }
281             // Process some responses from the student.
282             $attemptobj = quiz_attempt::create($attemptid);
283             $attemptobj->process_submitted_actions($timenow, false, $step['responses']);
285             // Finish the attempt.
286             if (!isset($step['finished']) || ($step['finished'] == 1)) {
287                 $attemptobj = quiz_attempt::create($attemptid);
288                 $attemptobj->process_finish($timenow, false);
289             }
290         }
291         return $attemptids;
292     }
294     /**
295      * @param $results array the results data from the csv file.
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) {
299         foreach ($results as $resultrow) {
300             $result = $this->explode_dot_separated_keys_to_make_subindexs($resultrow);
301             // Re-load quiz attempt data.
302             $attemptobj = quiz_attempt::create($attemptids[$result['quizattempt']]);
303             $this->check_attempt_results($result, $attemptobj);
304         }
305     }
307     /**
308      * Check that attempt results are as specified in $result.
309      *
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
313      */
314     protected function check_attempt_results($result, $attemptobj) {
315         foreach ($result as $fieldname => $value) {
316             if ($value === '!NULL!') {
317                 $value = null;
318             }
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 '
335                                                                .s($slotfieldname));
336                             }
337                         }
338                     }
339                     break;
340                 case 'finished' :
341                     $this->assertEquals((bool)$value, $attemptobj->is_finished());
342                     break;
343                 case 'summarks' :
344                     $this->assertEquals((float)$value, $attemptobj->get_sum_marks(),
345                         "Sum of marks of attempt {$result['quizattempt']}.");
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));
365             }
366         }
367     }