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