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 * mod_h5pactivity attempt tests
19 *
20 * @package    mod_h5pactivity
21 * @category   test
22 * @copyright  2020 Ferran Recio <ferran@moodle.com>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26namespace mod_h5pactivity\local;
27
28use \core_xapi\local\statement;
29use \core_xapi\local\statement\item;
30use \core_xapi\local\statement\item_agent;
31use \core_xapi\local\statement\item_activity;
32use \core_xapi\local\statement\item_definition;
33use \core_xapi\local\statement\item_verb;
34use \core_xapi\local\statement\item_result;
35use stdClass;
36
37/**
38 * Attempt tests class for mod_h5pactivity.
39 *
40 * @package    mod_h5pactivity
41 * @category   test
42 * @copyright  2020 Ferran Recio <ferran@moodle.com>
43 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 */
45class attempt_testcase extends \advanced_testcase {
46
47    /**
48     * Generate a scenario to run all tests.
49     * @return array course_modules, user record, course record
50     */
51    private function generate_testing_scenario(): array {
52        $this->resetAfterTest();
53        $this->setAdminUser();
54
55        $course = $this->getDataGenerator()->create_course();
56        $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
57        $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
58        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
59
60        return [$cm, $student, $course];
61    }
62
63    /**
64     * Test for create_attempt method.
65     */
66    public function test_create_attempt() {
67
68        list($cm, $student) = $this->generate_testing_scenario();
69
70        // Create first attempt.
71        $attempt = attempt::new_attempt($student, $cm);
72        $this->assertEquals($student->id, $attempt->get_userid());
73        $this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
74        $this->assertEquals(1, $attempt->get_attempt());
75
76        // Create a second attempt.
77        $attempt = attempt::new_attempt($student, $cm);
78        $this->assertEquals($student->id, $attempt->get_userid());
79        $this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
80        $this->assertEquals(2, $attempt->get_attempt());
81    }
82
83    /**
84     * Test for last_attempt method
85     */
86    public function test_last_attempt() {
87
88        list($cm, $student) = $this->generate_testing_scenario();
89
90        // Create first attempt.
91        $attempt = attempt::last_attempt($student, $cm);
92        $this->assertEquals($student->id, $attempt->get_userid());
93        $this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
94        $this->assertEquals(1, $attempt->get_attempt());
95        $lastid = $attempt->get_id();
96
97        // Get last attempt.
98        $attempt = attempt::last_attempt($student, $cm);
99        $this->assertEquals($student->id, $attempt->get_userid());
100        $this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
101        $this->assertEquals(1, $attempt->get_attempt());
102        $this->assertEquals($lastid, $attempt->get_id());
103
104        // Now force a new attempt.
105        $attempt = attempt::new_attempt($student, $cm);
106        $this->assertEquals($student->id, $attempt->get_userid());
107        $this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
108        $this->assertEquals(2, $attempt->get_attempt());
109        $lastid = $attempt->get_id();
110
111        // Get last attempt.
112        $attempt = attempt::last_attempt($student, $cm);
113        $this->assertEquals($student->id, $attempt->get_userid());
114        $this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
115        $this->assertEquals(2, $attempt->get_attempt());
116        $this->assertEquals($lastid, $attempt->get_id());
117    }
118
119    /**
120     * Test saving statements.
121     *
122     * @dataProvider save_statement_data
123     * @param string $subcontent subcontent identifier
124     * @param bool $hasdefinition generate definition
125     * @param bool $hasresult generate result
126     * @param array $results 0 => insert ok, 1 => maxscore, 2 => rawscore, 3 => count
127     */
128    public function test_save_statement(string $subcontent, bool $hasdefinition, bool $hasresult, array $results) {
129
130        list($cm, $student) = $this->generate_testing_scenario();
131
132        $attempt = attempt::new_attempt($student, $cm);
133        $this->assertEquals(0, $attempt->get_maxscore());
134        $this->assertEquals(0, $attempt->get_rawscore());
135        $this->assertEquals(0, $attempt->count_results());
136        $this->assertEquals(0, $attempt->get_duration());
137        $this->assertNull($attempt->get_completion());
138        $this->assertNull($attempt->get_success());
139        $this->assertFalse($attempt->get_scoreupdated());
140
141        $statement = $this->generate_statement($hasdefinition, $hasresult);
142        $result = $attempt->save_statement($statement, $subcontent);
143        $this->assertEquals($results[0], $result);
144        $this->assertEquals($results[1], $attempt->get_maxscore());
145        $this->assertEquals($results[2], $attempt->get_rawscore());
146        $this->assertEquals($results[3], $attempt->count_results());
147        $this->assertEquals($results[4], $attempt->get_duration());
148        $this->assertEquals($results[5], $attempt->get_completion());
149        $this->assertEquals($results[6], $attempt->get_success());
150        if ($results[5]) {
151            $this->assertTrue($attempt->get_scoreupdated());
152        } else {
153            $this->assertFalse($attempt->get_scoreupdated());
154        }
155    }
156
157    /**
158     * Data provider for data request creation tests.
159     *
160     * @return array
161     */
162    public function save_statement_data(): array {
163        return [
164            'Statement without definition and result' => [
165                '', false, false, [false, 0, 0, 0, 0, null, null]
166            ],
167            'Statement with definition but no result' => [
168                '', true, false, [false, 0, 0, 0, 0, null, null]
169            ],
170            'Statement with result but no definition' => [
171                '', true, false, [false, 0, 0, 0, 0, null, null]
172            ],
173            'Statement subcontent without definition and result' => [
174                '111-222-333', false, false, [false, 0, 0, 0, 0, null, null]
175            ],
176            'Statement subcontent with definition but no result' => [
177                '111-222-333', true, false, [false, 0, 0, 0, 0, null, null]
178            ],
179            'Statement subcontent with result but no definition' => [
180                '111-222-333', true, false, [false, 0, 0, 0, 0, null, null]
181            ],
182            'Statement with definition, result but no subcontent' => [
183                '', true, true, [true, 2, 2, 1, 25, 1, 1]
184            ],
185            'Statement with definition, result and subcontent' => [
186                '111-222-333', true, true, [true, 0, 0, 1, 0, null, null]
187            ],
188        ];
189    }
190
191    /**
192     * Test delete results from attempt.
193     */
194    public function test_delete_results() {
195
196        list($cm, $student) = $this->generate_testing_scenario();
197
198        $attempt = $this->generate_full_attempt($student, $cm);
199        $attempt->delete_results();
200        $this->assertEquals(0, $attempt->count_results());
201    }
202
203    /**
204     * Test delete attempt.
205     */
206    public function test_delete_attempt() {
207        global $DB;
208
209        list($cm, $student) = $this->generate_testing_scenario();
210
211        // Check no previous attempts are created.
212        $count = $DB->count_records('h5pactivity_attempts');
213        $this->assertEquals(0, $count);
214        $count = $DB->count_records('h5pactivity_attempts_results');
215        $this->assertEquals(0, $count);
216
217        // Generate one attempt.
218        $attempt1 = $this->generate_full_attempt($student, $cm);
219        $count = $DB->count_records('h5pactivity_attempts');
220        $this->assertEquals(1, $count);
221        $count = $DB->count_records('h5pactivity_attempts_results');
222        $this->assertEquals(2, $count);
223
224        // Generate a second attempt.
225        $attempt2 = $this->generate_full_attempt($student, $cm);
226        $count = $DB->count_records('h5pactivity_attempts');
227        $this->assertEquals(2, $count);
228        $count = $DB->count_records('h5pactivity_attempts_results');
229        $this->assertEquals(4, $count);
230
231        // Delete the first attempt.
232        attempt::delete_attempt($attempt1);
233        $count = $DB->count_records('h5pactivity_attempts');
234        $this->assertEquals(1, $count);
235        $count = $DB->count_records('h5pactivity_attempts_results');
236        $this->assertEquals(2, $count);
237        $this->assertEquals(2, $attempt2->count_results());
238    }
239
240    /**
241     * Test delete all attempts.
242     *
243     * @dataProvider delete_all_attempts_data
244     * @param bool $hasstudent if user is specificed
245     * @param int[] 0-3 => statements count results, 4-5 => totals
246     */
247    public function test_delete_all_attempts(bool $hasstudent, array $results) {
248        global $DB;
249
250        list($cm, $student, $course) = $this->generate_testing_scenario();
251
252        // For this test we need extra activity and student.
253        $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
254        $cm2 = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
255        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
256
257        // Check no previous attempts are created.
258        $count = $DB->count_records('h5pactivity_attempts');
259        $this->assertEquals(0, $count);
260        $count = $DB->count_records('h5pactivity_attempts_results');
261        $this->assertEquals(0, $count);
262
263        // Generate some attempts attempt on both activities and students.
264        $attempts = [];
265        $attempts[] = $this->generate_full_attempt($student, $cm);
266        $attempts[] = $this->generate_full_attempt($student2, $cm);
267        $attempts[] = $this->generate_full_attempt($student, $cm2);
268        $attempts[] = $this->generate_full_attempt($student2, $cm2);
269        $count = $DB->count_records('h5pactivity_attempts');
270        $this->assertEquals(4, $count);
271        $count = $DB->count_records('h5pactivity_attempts_results');
272        $this->assertEquals(8, $count);
273
274        // Delete all specified attempts.
275        $user = ($hasstudent) ? $student : null;
276        attempt::delete_all_attempts($cm, $user);
277
278        // Check data.
279        for ($assert = 0; $assert < 4; $assert++) {
280            $count = $attempts[$assert]->count_results();
281            $this->assertEquals($results[$assert], $count);
282        }
283        $count = $DB->count_records('h5pactivity_attempts');
284        $this->assertEquals($results[4], $count);
285        $count = $DB->count_records('h5pactivity_attempts_results');
286        $this->assertEquals($results[5], $count);
287    }
288
289    /**
290     * Data provider for data request creation tests.
291     *
292     * @return array
293     */
294    public function delete_all_attempts_data(): array {
295        return [
296            'Delete all attempts from activity' => [
297                false, [0, 0, 2, 2, 2, 4]
298            ],
299            'Delete all attempts from user' => [
300                true, [0, 2, 2, 2, 3, 6]
301            ],
302        ];
303    }
304
305    /**
306     * Test set_score method.
307     *
308     */
309    public function test_set_score(): void {
310        global $DB;
311
312        list($cm, $student, $course) = $this->generate_testing_scenario();
313
314        // Generate one attempt.
315        $attempt = $this->generate_full_attempt($student, $cm);
316
317        $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
318        $this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore());
319        $this->assertEquals(2, $dbattempt->rawscore);
320        $this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore());
321        $this->assertEquals(2, $dbattempt->maxscore);
322        $this->assertEquals(1, $dbattempt->scaled);
323
324        // Set attempt score.
325        $attempt->set_score(5, 10);
326
327        $this->assertEquals(5, $attempt->get_rawscore());
328        $this->assertEquals(10, $attempt->get_maxscore());
329        $this->assertTrue($attempt->get_scoreupdated());
330
331        // Save new score into DB.
332        $attempt->save();
333
334        $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
335        $this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore());
336        $this->assertEquals(5, $dbattempt->rawscore);
337        $this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore());
338        $this->assertEquals(10, $dbattempt->maxscore);
339        $this->assertEquals(0.5, $dbattempt->scaled);
340    }
341
342    /**
343     * Test set_duration method.
344     *
345     * @dataProvider basic_setters_data
346     * @param string $attribute the stribute to test
347     * @param int $oldvalue attribute old value
348     * @param int $newvalue attribute new expected value
349     */
350    public function test_basic_setters(string $attribute, int $oldvalue, int $newvalue): void {
351        global $DB;
352
353        list($cm, $student, $course) = $this->generate_testing_scenario();
354
355        // Generate one attempt.
356        $attempt = $this->generate_full_attempt($student, $cm);
357
358        $setmethod = 'set_'.$attribute;
359        $getmethod = 'get_'.$attribute;
360
361        $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
362        $this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
363        $this->assertEquals($oldvalue, $dbattempt->$attribute);
364
365        // Set attempt attribute.
366        $attempt->$setmethod($newvalue);
367
368        $this->assertEquals($newvalue, $attempt->$getmethod());
369
370        // Save new score into DB.
371        $attempt->save();
372
373        $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
374        $this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
375        $this->assertEquals($newvalue, $dbattempt->$attribute);
376
377        // Set null $attribute.
378        $attempt->$setmethod(null);
379
380        $this->assertNull($attempt->$getmethod());
381
382        // Save new score into DB.
383        $attempt->save();
384
385        $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
386        $this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
387        $this->assertNull($dbattempt->$attribute);
388    }
389
390    /**
391     * Data provider for testing basic setters.
392     *
393     * @return array
394     */
395    public function basic_setters_data(): array {
396        return [
397            'Set attempt duration' => [
398                'duration', 25, 35
399            ],
400            'Set attempt completion' => [
401                'completion', 1, 0
402            ],
403            'Set attempt success' => [
404                'success', 1, 0
405            ],
406        ];
407    }
408
409    /**
410     * Generate a fake attempt with two results.
411     *
412     * @param stdClass $student a user record
413     * @param stdClass $cm a course_module record
414     * @return attempt
415     */
416    private function generate_full_attempt($student, $cm): attempt {
417        $attempt = attempt::new_attempt($student, $cm);
418        $this->assertEquals(0, $attempt->get_maxscore());
419        $this->assertEquals(0, $attempt->get_rawscore());
420        $this->assertEquals(0, $attempt->count_results());
421
422        $statement = $this->generate_statement(true, true);
423        $saveok = $attempt->save_statement($statement, '');
424        $this->assertTrue($saveok);
425        $saveok = $attempt->save_statement($statement, '111-222-333');
426        $this->assertTrue($saveok);
427        $this->assertEquals(2, $attempt->count_results());
428
429        return $attempt;
430    }
431
432    /**
433     * Return a xAPI partial statement with object defined.
434     * @param bool $hasdefinition if has to include definition
435     * @param bool $hasresult if has to include results
436     * @return statement
437     */
438    private function generate_statement(bool $hasdefinition, bool $hasresult): statement {
439        global $USER;
440
441        $statement = new statement();
442        $statement->set_actor(item_agent::create_from_user($USER));
443        $statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
444        $definition = null;
445        if ($hasdefinition) {
446            $definition = item_definition::create_from_data((object)[
447                'interactionType' => 'compound',
448                'correctResponsesPattern' => '1',
449            ]);
450        }
451        $statement->set_object(item_activity::create_from_id('something', $definition));
452        if ($hasresult) {
453            $statement->set_result(item_result::create_from_data((object)[
454                'completion' => true,
455                'success' => true,
456                'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
457                'duration' => 'PT25S',
458            ]));
459        }
460        return $statement;
461    }
462}
463