dfaa1dea9cb67006f69fbf213492132e5199b66a
[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 PHPUnit\DbUnit\DataSet\ITable[] $csvdata of data read from csv file "questionsXX.csv",
61      *                                                                                  "stepsXX.csv" and "resultsXX.csv".
62      * @dataProvider get_data_for_walkthrough
63      */
64     public function test_walkthrough_from_csv($quizsettings, $csvdata) {
66         // CSV data files for these tests were generated using :
67         // https://github.com/jamiepratt/moodle-quiz-tools/tree/master/responsegenerator
69         $this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);
70     }
72     public function create_quiz($quizsettings, $qs) {
73         global $SITE, $DB;
74         $this->setAdminUser();
76         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
77         $slots = array();
78         $qidsbycat = array();
79         $sumofgrades = 0;
80         for ($rowno = 0; $rowno < $qs->getRowCount(); $rowno++) {
81             $q = $this->explode_dot_separated_keys_to_make_subindexs($qs->getRow($rowno));
83             $catname = array('name' => $q['cat']);
84             if (!$cat = $DB->get_record('question_categories', array('name' => $q['cat']))) {
85                 $cat = $questiongenerator->create_question_category($catname);
86             }
87             $q['catid'] = $cat->id;
88             foreach (array('which' => null, 'overrides' => array()) as $key => $default) {
89                 if (empty($q[$key])) {
90                     $q[$key] = $default;
91                 }
92             }
94             if ($q['type'] !== 'random') {
95                 // Don't actually create random questions here.
96                 $overrides = array('category' => $cat->id, 'defaultmark' => $q['mark']) + $q['overrides'];
97                 if ($q['type'] === 'truefalse') {
98                     // True/false question can never have hints, but sometimes we need to put them
99                     // in the CSV file, to keep it rectangular.
100                     unset($overrides['hint']);
101                 }
102                 $question = $questiongenerator->create_question($q['type'], $q['which'], $overrides);
103                 $q['id'] = $question->id;
105                 if (!isset($qidsbycat[$q['cat']])) {
106                     $qidsbycat[$q['cat']] = array();
107                 }
108                 if (!empty($q['which'])) {
109                     $name = $q['type'].'_'.$q['which'];
110                 } else {
111                     $name = $q['type'];
112                 }
113                 $qidsbycat[$q['catid']][$name] = $q['id'];
114             }
115             if (!empty($q['slot'])) {
116                 $slots[$q['slot']] = $q;
117                 $sumofgrades += $q['mark'];
118             }
119         }
121         ksort($slots);
123         // Make a quiz.
124         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
126         // Settings from param override defaults.
127         $aggregratedsettings = $quizsettings + array('course' => $SITE->id,
128                                                      'questionsperpage' => 0,
129                                                      'grade' => 100.0,
130                                                      'sumgrades' => $sumofgrades);
132         $this->quiz = $quizgenerator->create_instance($aggregratedsettings);
134         $this->randqids = array();
135         foreach ($slots as $slotno => $slotquestion) {
136             if ($slotquestion['type'] !== 'random') {
137                 quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
138             } else {
139                 quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0);
140                 $this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
141             }
142         }
143     }
145     /**
146      * Create quiz, simulate attempts and check results (if resultsXX.csv exists).
147      *
148      * @param array $quizsettings Quiz overrides for this quiz.
149      * @param PHPUnit\DbUnit\DataSet\ITable[] $csvdata Data loaded from csv files for this test.
150      */
151     protected function create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata) {
152         $this->resetAfterTest(true);
153         question_bank::get_qtype('random')->clear_caches_before_testing();
155         $this->create_quiz($quizsettings, $csvdata['questions']);
157         $attemptids = $this->walkthrough_attempts($csvdata['steps']);
159         if (isset($csvdata['results'])) {
160             $this->check_attempts_results($csvdata['results'], $attemptids);
161         }
162     }
164     /**
165      * Get full path of CSV file.
166      *
167      * @param string $setname
168      * @param string $test
169      * @return string full path of file.
170      */
171     protected function get_full_path_of_csv_file($setname, $test) {
172         return  __DIR__."/fixtures/{$setname}{$test}.csv";
173     }
175     /**
176      * Load dataset from CSV file "{$setname}{$test}.csv".
177      *
178      * @param string $setname
179      * @param string $test
180      * @return PHPUnit\DbUnit\DataSet\ITable
181      */
182     protected function load_csv_data_file($setname, $test='') {
183         $files = array($setname => $this->get_full_path_of_csv_file($setname, $test));
184         return $this->createCsvDataSet($files)->getTable($setname);
185     }
187     /**
188      * Break down row of csv data into sub arrays, according to column names.
189      *
190      * @param array $row from csv file with field names with parts separate by '.'.
191      * @return array the row with each part of the field name following a '.' being a separate sub array's index.
192      */
193     protected function explode_dot_separated_keys_to_make_subindexs(array $row) {
194         $parts = array();
195         foreach ($row as $columnkey => $value) {
196             $newkeys = explode('.', trim($columnkey));
197             $placetoputvalue =& $parts;
198             foreach ($newkeys as $newkeydepth => $newkey) {
199                 if ($newkeydepth + 1 === count($newkeys)) {
200                     $placetoputvalue[$newkey] = $value;
201                 } else {
202                     // Going deeper down.
203                     if (!isset($placetoputvalue[$newkey])) {
204                         $placetoputvalue[$newkey] = array();
205                     }
206                     $placetoputvalue =& $placetoputvalue[$newkey];
207                 }
208             }
209         }
210         return $parts;
211     }
213     /**
214      * Data provider method for test_walkthrough_from_csv. Called by PHPUnit.
215      *
216      * @return array One array element for each run of the test. Each element contains an array with the params for
217      *                  test_walkthrough_from_csv.
218      */
219     public function get_data_for_walkthrough() {
220         $quizzes = $this->load_csv_data_file('quizzes');
221         $datasets = array();
222         for ($rowno = 0; $rowno < $quizzes->getRowCount(); $rowno++) {
223             $quizsettings = $quizzes->getRow($rowno);
224             $dataset = array();
225             foreach ($this->files as $file) {
226                 if (file_exists($this->get_full_path_of_csv_file($file, $quizsettings['testnumber']))) {
227                     $dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber']);
228                 }
229             }
230             $datasets[] = array($quizsettings, $dataset);
231         }
232         return $datasets;
233     }
235     /**
236      * @param $steps PHPUnit\DbUnit\DataSet\ITable the step data from the csv file.
237      * @return array attempt no as in csv file => the id of the quiz_attempt as stored in the db.
238      */
239     protected function walkthrough_attempts($steps) {
240         global $DB;
241         $attemptids = array();
242         for ($rowno = 0; $rowno < $steps->getRowCount(); $rowno++) {
244             $step = $this->explode_dot_separated_keys_to_make_subindexs($steps->getRow($rowno));
245             // Find existing user or make a new user to do the quiz.
246             $username = array('firstname' => $step['firstname'],
247                               'lastname'  => $step['lastname']);
249             if (!$user = $DB->get_record('user', $username)) {
250                 $user = $this->getDataGenerator()->create_user($username);
251             }
253             if (!isset($attemptids[$step['quizattempt']])) {
254                 // Start the attempt.
255                 $quizobj = quiz::create($this->quiz->id, $user->id);
256                 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
257                 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
259                 $prevattempts = quiz_get_user_attempts($this->quiz->id, $user->id, 'all', true);
260                 $attemptnumber = count($prevattempts) + 1;
261                 $timenow = time();
262                 $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, false, $user->id);
263                 // Select variant and / or random sub question.
264                 if (!isset($step['variants'])) {
265                     $step['variants'] = array();
266                 }
267                 if (isset($step['randqs'])) {
268                     // Replace 'names' with ids.
269                     foreach ($step['randqs'] as $slotno => $randqname) {
270                         $step['randqs'][$slotno] = $this->randqids[$slotno][$randqname];
271                     }
272                 } else {
273                     $step['randqs'] = array();
274                 }
276                 quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $step['randqs'], $step['variants']);
277                 quiz_attempt_save_started($quizobj, $quba, $attempt);
278                 $attemptid = $attemptids[$step['quizattempt']] = $attempt->id;
279             } else {
280                 $attemptid = $attemptids[$step['quizattempt']];
281             }
283             // Process some responses from the student.
284             $attemptobj = quiz_attempt::create($attemptid);
285             $attemptobj->process_submitted_actions($timenow, false, $step['responses']);
287             // Finish the attempt.
288             if (!isset($step['finished']) || ($step['finished'] == 1)) {
289                 $attemptobj = quiz_attempt::create($attemptid);
290                 $attemptobj->process_finish($timenow, false);
291             }
292         }
293         return $attemptids;
294     }
296     /**
297      * @param $results PHPUnit\DbUnit\DataSet\ITable the results data from the csv file.
298      * @param $attemptids array attempt no as in csv file => the id of the quiz_attempt as stored in the db.
299      */
300     protected function check_attempts_results($results, $attemptids) {
301         for ($rowno = 0; $rowno < $results->getRowCount(); $rowno++) {
302             $result = $this->explode_dot_separated_keys_to_make_subindexs($results->getRow($rowno));
303             // Re-load quiz attempt data.
304             $attemptobj = quiz_attempt::create($attemptids[$result['quizattempt']]);
305             $this->check_attempt_results($result, $attemptobj);
306         }
307     }
309     /**
310      * Check that attempt results are as specified in $result.
311      *
312      * @param array        $result             row of data read from csv file.
313      * @param quiz_attempt $attemptobj         the attempt object loaded from db.
314      * @throws coding_exception
315      */
316     protected function check_attempt_results($result, $attemptobj) {
317         foreach ($result as $fieldname => $value) {
318             if ($value === '!NULL!') {
319                 $value = null;
320             }
321             switch ($fieldname) {
322                 case 'quizattempt' :
323                     break;
324                 case 'attemptnumber' :
325                     $this->assertEquals($value, $attemptobj->get_attempt_number());
326                     break;
327                 case 'slots' :
328                     foreach ($value as $slotno => $slottests) {
329                         foreach ($slottests as $slotfieldname => $slotvalue) {
330                             switch ($slotfieldname) {
331                                 case 'mark' :
332                                     $this->assertEquals(round($slotvalue, 2), $attemptobj->get_question_mark($slotno),
333                                                         "Mark for slot $slotno of attempt {$result['quizattempt']}.");
334                                     break;
335                                 default :
336                                     throw new coding_exception('Unknown slots sub field column in csv file '
337                                                                .s($slotfieldname));
338                             }
339                         }
340                     }
341                     break;
342                 case 'finished' :
343                     $this->assertEquals((bool)$value, $attemptobj->is_finished());
344                     break;
345                 case 'summarks' :
346                     $this->assertEquals((float)$value, $attemptobj->get_sum_marks(),
347                         "Sum of marks of attempt {$result['quizattempt']}.");
348                     break;
349                 case 'quizgrade' :
350                     // Check quiz grades.
351                     $grades = quiz_get_user_grades($attemptobj->get_quiz(), $attemptobj->get_userid());
352                     $grade = array_shift($grades);
353                     $this->assertEquals($value, $grade->rawgrade, "Quiz grade for attempt {$result['quizattempt']}.");
354                     break;
355                 case 'gradebookgrade' :
356                     // Check grade book.
357                     $gradebookgrades = grade_get_grades($attemptobj->get_courseid(),
358                                                         'mod', 'quiz',
359                                                         $attemptobj->get_quizid(),
360                                                         $attemptobj->get_userid());
361                     $gradebookitem = array_shift($gradebookgrades->items);
362                     $gradebookgrade = array_shift($gradebookitem->grades);
363                     $this->assertEquals($value, $gradebookgrade->grade, "Gradebook grade for attempt {$result['quizattempt']}.");
364                     break;
365                 default :
366                     throw new coding_exception('Unknown column in csv file '.s($fieldname));
367             }
368         }
369     }