Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

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