Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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  defined('MOODLE_INTERNAL') || die();
  18  
  19  global $CFG;
  20  require_once($CFG->libdir.'/completionlib.php');
  21  
  22  /**
  23   * Completion tests.
  24   *
  25   * @package    core_completion
  26   * @category   test
  27   * @copyright  2008 Sam Marshall
  28   * @copyright  2013 Frédéric Massart
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   * @coversDefaultClass \completion_info
  31   */
  32  class completionlib_test extends advanced_testcase {
  33      protected $course;
  34      protected $user;
  35      protected $module1;
  36      protected $module2;
  37  
  38      protected function mock_setup() {
  39          global $DB, $CFG, $USER;
  40  
  41          $this->resetAfterTest();
  42  
  43          $DB = $this->createMock(get_class($DB));
  44          $CFG->enablecompletion = COMPLETION_ENABLED;
  45          $USER = (object)array('id' => 314159);
  46      }
  47  
  48      /**
  49       * Create course with user and activities.
  50       */
  51      protected function setup_data() {
  52          global $DB, $CFG;
  53  
  54          $this->resetAfterTest();
  55  
  56          // Enable completion before creating modules, otherwise the completion data is not written in DB.
  57          $CFG->enablecompletion = true;
  58  
  59          // Create a course with activities.
  60          $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
  61          $this->user = $this->getDataGenerator()->create_user();
  62          $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
  63  
  64          $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
  65          $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
  66      }
  67  
  68      /**
  69       * Asserts that two variables are equal.
  70       *
  71       * @param  mixed   $expected
  72       * @param  mixed   $actual
  73       * @param  string  $message
  74       * @param  float   $delta
  75       * @param  integer $maxDepth
  76       * @param  boolean $canonicalize
  77       * @param  boolean $ignoreCase
  78       */
  79      public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
  80                                          bool $canonicalize = false, bool $ignoreCase = false): void {
  81          // Nasty cheating hack: prevent random failures on timemodified field.
  82          if (is_array($actual) && (is_object($expected) || is_array($expected))) {
  83              $actual = (object) $actual;
  84              $expected = (object) $expected;
  85          }
  86          if (is_object($expected) and is_object($actual)) {
  87              if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
  88                  if ($expected->timemodified + 1 == $actual->timemodified) {
  89                      $expected = clone($expected);
  90                      $expected->timemodified = $actual->timemodified;
  91                  }
  92              }
  93          }
  94          parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
  95      }
  96  
  97      /**
  98       * @covers ::is_enabled_for_site
  99       * @covers ::is_enabled
 100       */
 101      public function test_is_enabled() {
 102          global $CFG;
 103          $this->mock_setup();
 104  
 105          // Config alone.
 106          $CFG->enablecompletion = COMPLETION_DISABLED;
 107          $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
 108          $CFG->enablecompletion = COMPLETION_ENABLED;
 109          $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
 110  
 111          // Course.
 112          $course = (object)array('id' => 13);
 113          $c = new completion_info($course);
 114          $course->enablecompletion = COMPLETION_DISABLED;
 115          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 116          $course->enablecompletion = COMPLETION_ENABLED;
 117          $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
 118          $CFG->enablecompletion = COMPLETION_DISABLED;
 119          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 120  
 121          // Course and CM.
 122          $cm = new stdClass();
 123          $cm->completion = COMPLETION_TRACKING_MANUAL;
 124          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 125          $CFG->enablecompletion = COMPLETION_ENABLED;
 126          $course->enablecompletion = COMPLETION_DISABLED;
 127          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 128          $course->enablecompletion = COMPLETION_ENABLED;
 129          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
 130          $cm->completion = COMPLETION_TRACKING_NONE;
 131          $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
 132          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 133          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
 134      }
 135  
 136      /**
 137       * @covers ::update_state
 138       */
 139      public function test_update_state() {
 140          $this->mock_setup();
 141  
 142          $mockbuilder = $this->getMockBuilder('completion_info');
 143          $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
 144                                         'user_can_override_completion'));
 145          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 146          $cm = (object)array('id' => 13, 'course' => 42);
 147  
 148          // Not enabled, should do nothing.
 149          $c = $mockbuilder->getMock();
 150          $c->expects($this->once())
 151              ->method('is_enabled')
 152              ->with($cm)
 153              ->will($this->returnValue(false));
 154          $c->update_state($cm);
 155  
 156          // Enabled, but current state is same as possible result, do nothing.
 157          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 158          $c = $mockbuilder->getMock();
 159          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 160          $c->expects($this->once())
 161              ->method('is_enabled')
 162              ->with($cm)
 163              ->will($this->returnValue(true));
 164          $c->expects($this->once())
 165              ->method('get_data')
 166              ->will($this->returnValue($current));
 167          $c->update_state($cm, COMPLETION_COMPLETE);
 168  
 169          // Enabled, but current state is a specific one and new state is just
 170          // complete, so do nothing.
 171          $c = $mockbuilder->getMock();
 172          $current->completionstate = COMPLETION_COMPLETE_PASS;
 173          $c->expects($this->once())
 174              ->method('is_enabled')
 175              ->with($cm)
 176              ->will($this->returnValue(true));
 177          $c->expects($this->once())
 178              ->method('get_data')
 179              ->will($this->returnValue($current));
 180          $c->update_state($cm, COMPLETION_COMPLETE);
 181  
 182          // Manual, change state (no change).
 183          $c = $mockbuilder->getMock();
 184          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 185          $current->completionstate = COMPLETION_COMPLETE;
 186          $c->expects($this->once())
 187              ->method('is_enabled')
 188              ->with($cm)
 189              ->will($this->returnValue(true));
 190          $c->expects($this->once())
 191              ->method('get_data')
 192              ->will($this->returnValue($current));
 193          $c->update_state($cm, COMPLETION_COMPLETE);
 194  
 195          // Manual, change state (change).
 196          $c = $mockbuilder->getMock();
 197          $c->expects($this->once())
 198              ->method('is_enabled')
 199              ->with($cm)
 200              ->will($this->returnValue(true));
 201          $c->expects($this->once())
 202              ->method('get_data')
 203              ->will($this->returnValue($current));
 204          $changed = clone($current);
 205          $changed->timemodified = time();
 206          $changed->completionstate = COMPLETION_INCOMPLETE;
 207          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 208          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 209          $c->expects($this->once())
 210              ->method('internal_set_data')
 211              ->with($cm, $comparewith);
 212          $c->update_state($cm, COMPLETION_INCOMPLETE);
 213  
 214          // Auto, change state.
 215          $c = $mockbuilder->getMock();
 216          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 217          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 218          $c->expects($this->once())
 219              ->method('is_enabled')
 220              ->with($cm)
 221              ->will($this->returnValue(true));
 222          $c->expects($this->once())
 223              ->method('get_data')
 224              ->will($this->returnValue($current));
 225          $c->expects($this->once())
 226              ->method('internal_get_state')
 227              ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
 228          $changed = clone($current);
 229          $changed->timemodified = time();
 230          $changed->completionstate = COMPLETION_COMPLETE_PASS;
 231          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 232          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 233          $c->expects($this->once())
 234              ->method('internal_set_data')
 235              ->with($cm, $comparewith);
 236          $c->update_state($cm, COMPLETION_COMPLETE_PASS);
 237  
 238          // Manual tracking, change state by overriding it manually.
 239          $c = $mockbuilder->getMock();
 240          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 241          $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 242          $current2 = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 243          $c->expects($this->exactly(2))
 244              ->method('is_enabled')
 245              ->with($cm)
 246              ->will($this->returnValue(true));
 247          $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
 248              ->method('user_can_override_completion')
 249              ->will($this->returnValue(true));
 250          $c->expects($this->exactly(2))
 251              ->method('get_data')
 252              ->with($cm, false, 100)
 253              ->willReturnOnConsecutiveCalls($current1, $current2);
 254          $changed1 = clone($current1);
 255          $changed1->timemodified = time();
 256          $changed1->completionstate = COMPLETION_COMPLETE;
 257          $changed1->overrideby = 314159;
 258          $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
 259          $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
 260          $changed2 = clone($current2);
 261          $changed2->timemodified = time();
 262          $changed2->overrideby = null;
 263          $changed2->completionstate = COMPLETION_INCOMPLETE;
 264          $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
 265          $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
 266          $c->expects($this->exactly(2))
 267              ->method('internal_set_data')
 268              ->withConsecutive(
 269                  array($cm, $comparewith1),
 270                  array($cm, $comparewith2)
 271              );
 272          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 273          // And confirm that the status can be changed back to incomplete without an override.
 274          $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
 275  
 276          // Auto, change state via override, incomplete to complete.
 277          $c = $mockbuilder->getMock();
 278          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 279          $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 280          $c->expects($this->once())
 281              ->method('is_enabled')
 282              ->with($cm)
 283              ->will($this->returnValue(true));
 284          $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
 285              ->method('user_can_override_completion')
 286              ->will($this->returnValue(true));
 287          $c->expects($this->once())
 288              ->method('get_data')
 289              ->with($cm, false, 100)
 290              ->will($this->returnValue($current));
 291          $changed = clone($current);
 292          $changed->timemodified = time();
 293          $changed->completionstate = COMPLETION_COMPLETE;
 294          $changed->overrideby = 314159;
 295          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 296          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 297          $c->expects($this->once())
 298              ->method('internal_set_data')
 299              ->with($cm, $comparewith);
 300          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 301  
 302          // Now confirm the status can be changed back from complete to incomplete using an override.
 303          $c = $mockbuilder->getMock();
 304          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 305          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
 306          $c->expects($this->once())
 307              ->method('is_enabled')
 308              ->with($cm)
 309              ->will($this->returnValue(true));
 310          $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
 311              ->method('user_can_override_completion')
 312              ->will($this->returnValue(true));
 313          $c->expects($this->once())
 314              ->method('get_data')
 315              ->with($cm, false, 100)
 316              ->will($this->returnValue($current));
 317          $changed = clone($current);
 318          $changed->timemodified = time();
 319          $changed->completionstate = COMPLETION_INCOMPLETE;
 320          $changed->overrideby = 314159;
 321          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 322          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 323          $c->expects($this->once())
 324              ->method('internal_set_data')
 325              ->with($cm, $comparewith);
 326          $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
 327      }
 328  
 329      /**
 330       * Data provider for test_internal_get_state().
 331       *
 332       * @return array[]
 333       */
 334      public function internal_get_state_provider() {
 335          return [
 336              'View required, but not viewed yet' => [
 337                  COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
 338              ],
 339              'View not required and not viewed yet' => [
 340                  COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
 341              ],
 342              'View not required, grade required but no grade yet, $cm->modname not set' => [
 343                  COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
 344              ],
 345              'View not required, grade required but no grade yet, $cm->course not set' => [
 346                  COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
 347              ],
 348              'View not required, grade not required' => [
 349                  COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
 350              ],
 351          ];
 352      }
 353  
 354      /**
 355       * Test for completion_info::get_state().
 356       *
 357       * @dataProvider internal_get_state_provider
 358       * @param int $completionview
 359       * @param int $completionusegrade
 360       * @param string $unsetfield
 361       * @param int $expectedstate
 362       * @covers ::internal_get_state
 363       */
 364      public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
 365          $this->setup_data();
 366  
 367          /** @var \mod_assign_generator $assigngenerator */
 368          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 369          $assign = $assigngenerator->create_instance([
 370              'course' => $this->course->id,
 371              'completion' => COMPLETION_ENABLED,
 372              'completionview' => $completionview,
 373              'completionusegrade' => $completionusegrade,
 374          ]);
 375  
 376          $userid = $this->user->id;
 377          $this->setUser($userid);
 378  
 379          $cm = get_coursemodule_from_instance('assign', $assign->id);
 380          if ($unsetfield) {
 381              unset($cm->$unsetfield);
 382          }
 383          // If view is required, but they haven't viewed it yet.
 384          $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
 385  
 386          $completioninfo = new completion_info($this->course);
 387          $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
 388      }
 389  
 390      /**
 391       * Provider for the test_internal_get_state_with_grade_criteria.
 392       *
 393       * @return array
 394       */
 395      public function internal_get_state_with_grade_criteria_provider() {
 396          return [
 397              "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
 398                  [
 399                      'completionusegrade' => 1,
 400                      'completionpassgrade' => 1,
 401                      'gradepass' => 50,
 402                  ],
 403                  50,
 404                  COMPLETION_COMPLETE_PASS
 405              ],
 406              "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
 407                  [
 408                      'completionusegrade' => 1,
 409                      'completionpassgrade' => 1,
 410                      'gradepass' => 50,
 411                  ],
 412                  40,
 413                  COMPLETION_COMPLETE_FAIL
 414              ],
 415              "Passing grade not enabled with passing grade set." => [
 416                  [
 417                      'completionusegrade' => 1,
 418                      'gradepass' => 50,
 419                  ],
 420                  50,
 421                  COMPLETION_COMPLETE_PASS
 422              ],
 423              "Passing grade not enabled with passing grade not set." => [
 424                  [
 425                      'completionusegrade' => 1,
 426                  ],
 427                  90,
 428                  COMPLETION_COMPLETE
 429              ],
 430              "Passing grade not enabled with passing grade not set. No submission made." => [
 431                  [
 432                      'completionusegrade' => 1,
 433                  ],
 434                  null,
 435                  COMPLETION_INCOMPLETE
 436              ],
 437          ];
 438      }
 439  
 440      /**
 441       * Tests that the right completion state is being set based on the grade criteria.
 442       *
 443       * @dataProvider internal_get_state_with_grade_criteria_provider
 444       * @param array $completioncriteria The completion criteria to use
 445       * @param int|null $studentgrade Grade to assign to student
 446       * @param int $expectedstate Expected completion state
 447       * @covers ::internal_get_state
 448       */
 449      public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?int $studentgrade, int $expectedstate) {
 450          $this->setup_data();
 451  
 452          /** @var \mod_assign_generator $assigngenerator */
 453          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 454          $assign = $assigngenerator->create_instance([
 455              'course' => $this->course->id,
 456              'completion' => COMPLETION_ENABLED,
 457          ] + $completioncriteria);
 458  
 459          $userid = $this->user->id;
 460  
 461          $cm = get_coursemodule_from_instance('assign', $assign->id);
 462          $usercm = cm_info::create($cm, $userid);
 463  
 464          // Create a teacher account.
 465          $teacher = $this->getDataGenerator()->create_user();
 466          $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
 467          // Log in as the teacher.
 468          $this->setUser($teacher);
 469  
 470          // Grade the student for this assignment.
 471          $assign = new assign($usercm->context, $cm, $cm->course);
 472          if ($studentgrade) {
 473              $data = (object)[
 474                  'sendstudentnotifications' => false,
 475                  'attemptnumber' => 1,
 476                  'grade' => $studentgrade,
 477              ];
 478              $assign->save_grade($userid, $data);
 479          }
 480  
 481          // The target user already received a grade, so internal_get_state should be already complete.
 482          $completioninfo = new completion_info($this->course);
 483          $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
 484      }
 485  
 486      /**
 487       * Covers the case where internal_get_state() is being called for a user different from the logged in user.
 488       *
 489       * @covers ::internal_get_state
 490       */
 491      public function test_internal_get_state_with_different_user() {
 492          $this->setup_data();
 493  
 494          /** @var \mod_assign_generator $assigngenerator */
 495          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 496          $assign = $assigngenerator->create_instance([
 497              'course' => $this->course->id,
 498              'completion' => COMPLETION_ENABLED,
 499              'completionusegrade' => 1,
 500          ]);
 501  
 502          $userid = $this->user->id;
 503  
 504          $cm = get_coursemodule_from_instance('assign', $assign->id);
 505          $usercm = cm_info::create($cm, $userid);
 506  
 507          // Create a teacher account.
 508          $teacher = $this->getDataGenerator()->create_user();
 509          $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
 510          // Log in as the teacher.
 511          $this->setUser($teacher);
 512  
 513          // Grade the student for this assignment.
 514          $assign = new assign($usercm->context, $cm, $cm->course);
 515          $data = (object)[
 516              'sendstudentnotifications' => false,
 517              'attemptnumber' => 1,
 518              'grade' => 90,
 519          ];
 520          $assign->save_grade($userid, $data);
 521  
 522          // The target user already received a grade, so internal_get_state should be already complete.
 523          $completioninfo = new completion_info($this->course);
 524          $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
 525  
 526          // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
 527          $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
 528      }
 529  
 530      /**
 531       * Test for internal_get_state() for an activity that supports custom completion.
 532       *
 533       * @covers ::internal_get_state
 534       */
 535      public function test_internal_get_state_with_custom_completion() {
 536          $this->setup_data();
 537  
 538          $choicerecord = [
 539              'course' => $this->course,
 540              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 541              'completionsubmit' => COMPLETION_ENABLED,
 542          ];
 543          $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
 544          $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
 545  
 546          $completioninfo = new completion_info($this->course);
 547  
 548          // Fetch completion for the user who hasn't made a choice yet.
 549          $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
 550          $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
 551  
 552          // Have the user make a choice.
 553          $choicewithoptions = choice_get_choice($choice->id);
 554          $optionids = array_keys($choicewithoptions->option);
 555          choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
 556          $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
 557          $this->assertEquals(COMPLETION_COMPLETE, $completion);
 558      }
 559  
 560      /**
 561       * @covers ::set_module_viewed
 562       */
 563      public function test_set_module_viewed() {
 564          $this->mock_setup();
 565  
 566          $mockbuilder = $this->getMockBuilder('completion_info');
 567          $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
 568          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 569          $cm = (object)array('id' => 13, 'course' => 42);
 570  
 571          // Not tracking completion, should do nothing.
 572          $c = $mockbuilder->getMock();
 573          $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
 574          $c->set_module_viewed($cm);
 575  
 576          // Tracking completion but completion is disabled, should do nothing.
 577          $c = $mockbuilder->getMock();
 578          $cm->completionview = COMPLETION_VIEW_REQUIRED;
 579          $c->expects($this->once())
 580              ->method('is_enabled')
 581              ->with($cm)
 582              ->will($this->returnValue(false));
 583          $c->set_module_viewed($cm);
 584  
 585          // Now it's enabled, we expect it to get data. If data already has
 586          // viewed, still do nothing.
 587          $c = $mockbuilder->getMock();
 588          $c->expects($this->once())
 589              ->method('is_enabled')
 590              ->with($cm)
 591              ->will($this->returnValue(true));
 592          $c->expects($this->once())
 593              ->method('get_data')
 594              ->with($cm, 0)
 595              ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
 596          $c->set_module_viewed($cm);
 597  
 598          // OK finally one that hasn't been viewed, now it should set it viewed
 599          // and update state.
 600          $c = $mockbuilder->getMock();
 601          $c->expects($this->once())
 602              ->method('is_enabled')
 603              ->with($cm)
 604              ->will($this->returnValue(true));
 605          $c->expects($this->once())
 606              ->method('get_data')
 607              ->with($cm, false, 1337)
 608              ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
 609          $c->expects($this->once())
 610              ->method('internal_set_data')
 611              ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
 612          $c->expects($this->once())
 613              ->method('update_state')
 614              ->with($cm, COMPLETION_COMPLETE, 1337);
 615          $c->set_module_viewed($cm, 1337);
 616      }
 617  
 618      /**
 619       * @covers ::count_user_data
 620       */
 621      public function test_count_user_data() {
 622          global $DB;
 623          $this->mock_setup();
 624  
 625          $course = (object)array('id' => 13);
 626          $cm = (object)array('id' => 42);
 627  
 628          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 629          $DB->expects($this->once())
 630              ->method('get_field_sql')
 631              ->will($this->returnValue(666));
 632  
 633          $c = new completion_info($course);
 634          $this->assertEquals(666, $c->count_user_data($cm));
 635      }
 636  
 637      /**
 638       * @covers ::delete_all_state
 639       */
 640      public function test_delete_all_state() {
 641          global $DB;
 642          $this->mock_setup();
 643  
 644          $course = (object)array('id' => 13);
 645          $cm = (object)array('id' => 42, 'course' => 13);
 646          $c = new completion_info($course);
 647  
 648          // Check it works ok without data in session.
 649          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 650          $DB->expects($this->once())
 651              ->method('delete_records')
 652              ->with('course_modules_completion', array('coursemoduleid' => 42))
 653              ->will($this->returnValue(true));
 654          $c->delete_all_state($cm);
 655      }
 656  
 657      /**
 658       * @covers ::reset_all_state
 659       */
 660      public function test_reset_all_state() {
 661          global $DB;
 662          $this->mock_setup();
 663  
 664          $mockbuilder = $this->getMockBuilder('completion_info');
 665          $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
 666          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 667          $c = $mockbuilder->getMock();
 668  
 669          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 670  
 671          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 672          $DB->expects($this->once())
 673              ->method('get_recordset')
 674              ->will($this->returnValue(
 675                  new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
 676                      (object)array('id' => 2, 'userid' => 101)))));
 677  
 678          $c->expects($this->once())
 679              ->method('delete_all_state')
 680              ->with($cm);
 681  
 682          $c->expects($this->once())
 683              ->method('get_tracked_users')
 684              ->will($this->returnValue(array(
 685              (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
 686              (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
 687  
 688          $c->expects($this->exactly(3))
 689              ->method('update_state')
 690              ->withConsecutive(
 691                  array($cm, COMPLETION_UNKNOWN, 100),
 692                  array($cm, COMPLETION_UNKNOWN, 101),
 693                  array($cm, COMPLETION_UNKNOWN, 201)
 694              );
 695  
 696          $c->reset_all_state($cm);
 697      }
 698  
 699      /**
 700       * Data provider for test_get_data().
 701       *
 702       * @return array[]
 703       */
 704      public function get_data_provider() {
 705          return [
 706              'No completion record' => [
 707                  false, true, false, COMPLETION_INCOMPLETE
 708              ],
 709              'Not completed' => [
 710                  false, true, true, COMPLETION_INCOMPLETE
 711              ],
 712              'Completed' => [
 713                  false, true, true, COMPLETION_COMPLETE
 714              ],
 715              'Whole course, complete' => [
 716                  true, true, true, COMPLETION_COMPLETE
 717              ],
 718              'Get data for another user, result should be not cached' => [
 719                  false, false, true,  COMPLETION_INCOMPLETE
 720              ],
 721              'Get data for another user, including whole course, result should be not cached' => [
 722                  true, false, true,  COMPLETION_INCOMPLETE
 723              ],
 724          ];
 725      }
 726  
 727      /**
 728       * Tests for completion_info::get_data().
 729       *
 730       * @dataProvider get_data_provider
 731       * @param bool $wholecourse Whole course parameter for get_data().
 732       * @param bool $sameuser Whether the user calling get_data() is the user itself.
 733       * @param bool $hasrecord Whether to create a course_modules_completion record.
 734       * @param int $completion The completion state expected.
 735       * @covers ::get_data
 736       */
 737      public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
 738          global $DB;
 739  
 740          $this->setup_data();
 741          $user = $this->user;
 742  
 743          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 744          $choice = $choicegenerator->create_instance([
 745              'course' => $this->course->id,
 746              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 747              'completionview' => true,
 748              'completionsubmit' => true,
 749          ]);
 750  
 751          $cm = get_coursemodule_from_instance('choice', $choice->id);
 752  
 753          // Let's manually create a course completion record instead of going through the hoops to complete an activity.
 754          if ($hasrecord) {
 755              $cmcompletionrecord = (object)[
 756                  'coursemoduleid' => $cm->id,
 757                  'userid' => $user->id,
 758                  'completionstate' => $completion,
 759                  'overrideby' => null,
 760                  'timemodified' => 0,
 761              ];
 762              $cmcompletionviewrecord = (object)[
 763                  'coursemoduleid' => $cm->id,
 764                  'userid' => $user->id,
 765                  'timecreated' => 0,
 766              ];
 767              $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 768              $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
 769          }
 770  
 771          // Whether we expect for the returned completion data to be stored in the cache.
 772          $iscached = true;
 773  
 774          if (!$sameuser) {
 775              $iscached = false;
 776              $this->setAdminUser();
 777          } else {
 778              $this->setUser($user);
 779          }
 780  
 781          // Mock other completion data.
 782          $completioninfo = new completion_info($this->course);
 783  
 784          $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
 785  
 786          // Course module ID of the returned completion data must match this activity's course module ID.
 787          $this->assertEquals($cm->id, $result->coursemoduleid);
 788          // User ID of the returned completion data must match the user's ID.
 789          $this->assertEquals($user->id, $result->userid);
 790          // The completion state of the returned completion data must match the expected completion state.
 791          $this->assertEquals($completion, $result->completionstate);
 792  
 793          // If the user has no completion record, then the default record should be returned.
 794          if (!$hasrecord) {
 795              $this->assertEquals(0, $result->id);
 796          }
 797  
 798          // Check that we are including relevant completion data for the module.
 799          if (!$wholecourse) {
 800              $this->assertTrue(property_exists($result, 'viewed'));
 801              $this->assertTrue(property_exists($result, 'customcompletion'));
 802          }
 803      }
 804  
 805      /**
 806       * @covers ::get_data
 807       */
 808      public function test_get_data_successive_calls(): void {
 809          global $DB;
 810  
 811          $this->setup_data();
 812          $this->setUser($this->user);
 813  
 814          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 815          $choice = $choicegenerator->create_instance([
 816              'course' => $this->course->id,
 817              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 818              'completionview' => true,
 819              'completionsubmit' => true,
 820          ]);
 821  
 822          $cm = get_coursemodule_from_instance('choice', $choice->id);
 823  
 824          // Let's manually create a course completion record instead of going through the hoops to complete an activity.
 825          $cmcompletionrecord = (object) [
 826              'coursemoduleid' => $cm->id,
 827              'userid' => $this->user->id,
 828              'completionstate' => COMPLETION_NOT_VIEWED,
 829              'overrideby' => null,
 830              'timemodified' => 0,
 831          ];
 832          $cmcompletionviewrecord = (object)[
 833              'coursemoduleid' => $cm->id,
 834              'userid' => $this->user->id,
 835              'timecreated' => 0,
 836          ];
 837          $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 838          $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
 839  
 840          // Mock other completion data.
 841          $completioninfo = new completion_info($this->course);
 842  
 843          $modinfo = get_fast_modinfo($this->course);
 844          $results = [];
 845          foreach ($modinfo->cms as $testcm) {
 846              $result = $completioninfo->get_data($testcm, true);
 847              $this->assertTrue(property_exists($result, 'id'));
 848              $this->assertTrue(property_exists($result, 'coursemoduleid'));
 849              $this->assertTrue(property_exists($result, 'userid'));
 850              $this->assertTrue(property_exists($result, 'completionstate'));
 851              $this->assertTrue(property_exists($result, 'viewed'));
 852              $this->assertTrue(property_exists($result, 'overrideby'));
 853              $this->assertTrue(property_exists($result, 'timemodified'));
 854              $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
 855  
 856              $this->assertEquals($testcm->id, $result->coursemoduleid);
 857              $this->assertEquals($this->user->id, $result->userid);
 858  
 859              $results[$testcm->id] = $result;
 860          }
 861  
 862          $result = $completioninfo->get_data($cm);
 863          $this->assertTrue(property_exists($result, 'customcompletion'));
 864  
 865          // The data should match when fetching modules individually.
 866          (cache::make('core', 'completion'))->purge();
 867          foreach ($modinfo->cms as $testcm) {
 868              $result = $completioninfo->get_data($testcm, false);
 869              $this->assertEquals($result, $results[$testcm->id]);
 870          }
 871      }
 872  
 873      /**
 874       * Tests for get_completion_data().
 875       *
 876       * @covers ::get_completion_data
 877       */
 878      public function test_get_completion_data() {
 879          $this->setup_data();
 880          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 881          $choice = $choicegenerator->create_instance([
 882              'course' => $this->course->id,
 883              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 884              'completionview' => true,
 885              'completionsubmit' => true,
 886          ]);
 887          $cm = get_coursemodule_from_instance('choice', $choice->id);
 888  
 889          // Mock other completion data.
 890          $completioninfo = new completion_info($this->course);
 891          // Default data to return when no completion data is found.
 892          $defaultdata = [
 893              'id' => 0,
 894              'coursemoduleid' => $cm->id,
 895              'userid' => $this->user->id,
 896              'completionstate' => 0,
 897              'viewed' => 0,
 898              'overrideby' => null,
 899              'timemodified' => 0,
 900          ];
 901  
 902          $completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
 903          $this->assertTrue(array_key_exists('viewed', $completiondatabeforeview));
 904          $this->assertTrue(array_key_exists('coursemoduleid', $completiondatabeforeview));
 905          $this->assertEquals(0, $completiondatabeforeview['viewed']);
 906          $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
 907  
 908          // Set viewed.
 909          $completioninfo->set_module_viewed($cm, $this->user->id);
 910  
 911          $completiondata = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
 912          $this->assertTrue(array_key_exists('viewed', $completiondata));
 913          $this->assertTrue(array_key_exists('coursemoduleid', $completiondata));
 914          $this->assertEquals(1, $completiondata['viewed']);
 915          $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
 916  
 917          $completioninfo->reset_all_state($cm);
 918  
 919          $completiondataafterreset = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
 920          $this->assertTrue(array_key_exists('viewed', $completiondataafterreset));
 921          $this->assertTrue(array_key_exists('coursemoduleid', $completiondataafterreset));
 922          $this->assertEquals(1, $completiondataafterreset['viewed']);
 923          $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
 924      }
 925  
 926      /**
 927       * Tests for completion_info::get_other_cm_completion_data().
 928       *
 929       * @covers ::get_other_cm_completion_data
 930       */
 931      public function test_get_other_cm_completion_data() {
 932          global $DB;
 933  
 934          $this->setup_data();
 935          $user = $this->user;
 936  
 937          $this->setAdminUser();
 938  
 939          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 940          $choice = $choicegenerator->create_instance([
 941              'course' => $this->course->id,
 942              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 943              'completionsubmit' => true,
 944          ]);
 945  
 946          $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
 947  
 948          $choice2 = $choicegenerator->create_instance([
 949              'course' => $this->course->id,
 950              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 951          ]);
 952  
 953          $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
 954  
 955          $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
 956          $workshop = $workshopgenerator->create_instance([
 957              'course' => $this->course->id,
 958              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 959              // Submission grade required.
 960              'completiongradeitemnumber' => 0,
 961              'completionpassgrade' => 1,
 962          ]);
 963  
 964          $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
 965  
 966          $completioninfo = new completion_info($this->course);
 967  
 968          $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
 969          $method->setAccessible(true);
 970  
 971          // Check that fetching data for a module with custom completion provides its info.
 972          $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
 973  
 974          $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
 975          $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
 976          $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
 977  
 978          // Mock a choice answer so user has completed the requirement.
 979          $choicemockinfo = [
 980              'choiceid' => $cmchoice->instance,
 981              'userid' => $this->user->id
 982          ];
 983          $DB->insert_record('choice_answers', $choicemockinfo, false);
 984  
 985          // Confirm fetching again reflects the completion.
 986          $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
 987          $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
 988  
 989          // Check that fetching data for a module with no custom completion still provides its grade completion status.
 990          $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
 991  
 992          $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
 993          $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
 994          $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
 995          $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
 996          $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['passgrade']);
 997  
 998          // Check that fetching data for a module with no completion conditions does not provide any data.
 999          $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
1000          $this->assertEmpty($choice2completiondata);
1001      }
1002  
1003      /**
1004       * @covers ::internal_set_data
1005       */
1006      public function test_internal_set_data() {
1007          global $DB;
1008          $this->setup_data();
1009  
1010          $this->setUser($this->user);
1011          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1012          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1013          $cm = get_coursemodule_from_instance('forum', $forum->id);
1014          $c = new completion_info($this->course);
1015  
1016          // 1) Test with new data.
1017          $data = new stdClass();
1018          $data->id = 0;
1019          $data->userid = $this->user->id;
1020          $data->coursemoduleid = $cm->id;
1021          $data->completionstate = COMPLETION_COMPLETE;
1022          $data->timemodified = time();
1023          $data->viewed = COMPLETION_NOT_VIEWED;
1024          $data->overrideby = null;
1025  
1026          $c->internal_set_data($cm, $data);
1027          $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
1028          $this->assertEquals($d1, $data->id);
1029          $cache = cache::make('core', 'completion');
1030          // Cache was not set for another user.
1031          $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
1032          $this->assertEquals([
1033              'cacherev' => $this->course->cacherev,
1034              $cm->id => array_merge(
1035                  (array) $data,
1036                  ['other_cm_completion_data_fetched' => true]
1037              ),
1038          ],
1039          $cachevalue);
1040  
1041          // 2) Test with existing data and for different user.
1042          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1043          $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
1044          $newuser = $this->getDataGenerator()->create_user();
1045  
1046          $d2 = new stdClass();
1047          $d2->id = 7;
1048          $d2->userid = $newuser->id;
1049          $d2->coursemoduleid = $cm2->id;
1050          $d2->completionstate = COMPLETION_COMPLETE;
1051          $d2->timemodified = time();
1052          $d2->viewed = COMPLETION_NOT_VIEWED;
1053          $d2->overrideby = null;
1054          $c->internal_set_data($cm2, $d2);
1055          // Cache for current user returns the data.
1056          $cachevalue = $cache->get($data->userid . '_' . $cm->course);
1057          $this->assertEquals(array_merge(
1058              (array) $data,
1059              ['other_cm_completion_data_fetched' => true]
1060          ), $cachevalue[$cm->id]);
1061  
1062          // Cache for another user is not filled.
1063          $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
1064  
1065          // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1066          $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1067          $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
1068          $newuser2 = $this->getDataGenerator()->create_user();
1069          $d3 = new stdClass();
1070          $d3->id = 13;
1071          $d3->userid = $newuser2->id;
1072          $d3->coursemoduleid = $cm3->id;
1073          $d3->completionstate = COMPLETION_COMPLETE;
1074          $d3->timemodified = time();
1075          $d3->viewed = COMPLETION_NOT_VIEWED;
1076          $d3->overrideby = null;
1077          $DB->insert_record('course_modules_completion', $d3);
1078          $c->internal_set_data($cm, $data);
1079  
1080          // 4) Test instant course completions.
1081          $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id),
1082              array('completion' => 1));
1083          $cm = get_coursemodule_from_instance('data', $dataactivity->id);
1084          $c = new completion_info($this->course);
1085          $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid);
1086  
1087          // Add activity completion criteria.
1088          $criteriadata = new stdClass();
1089          $criteriadata->id = $this->course->id;
1090          $criteriadata->criteria_activity = array();
1091          // Some activities.
1092          $criteriadata->criteria_activity[$cmdata->id] = 1;
1093          $class = 'completion_criteria_activity';
1094          $criterion = new $class();
1095          $criterion->update_config($criteriadata);
1096  
1097          $actual = $DB->get_records('course_completions');
1098          $this->assertEmpty($actual);
1099  
1100          $data->coursemoduleid = $cm->id;
1101          $c->internal_set_data($cm, $data);
1102          $actual = $DB->get_records('course_completions');
1103          $this->assertEquals(1, count($actual));
1104          $this->assertEquals($this->user->id, reset($actual)->userid);
1105  
1106          $data->userid = $newuser2->id;
1107          $c->internal_set_data($cm, $data, true);
1108          $actual = $DB->get_records('course_completions');
1109          $this->assertEquals(1, count($actual));
1110          $this->assertEquals($this->user->id, reset($actual)->userid);
1111      }
1112  
1113      /**
1114       * @covers ::get_progress_all
1115       */
1116      public function test_get_progress_all_few() {
1117          global $DB;
1118          $this->mock_setup();
1119  
1120          $mockbuilder = $this->getMockBuilder('completion_info');
1121          $mockbuilder->onlyMethods(array('get_tracked_users'));
1122          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1123          $c = $mockbuilder->getMock();
1124  
1125          // With few results.
1126          $c->expects($this->once())
1127              ->method('get_tracked_users')
1128              ->with(false,  array(),  0,  '',  '',  '',  null)
1129              ->will($this->returnValue(array(
1130                  (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1131                  (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1132          $DB->expects($this->once())
1133              ->method('get_in_or_equal')
1134              ->with(array(100, 201))
1135              ->will($this->returnValue(array(' IN (100, 201)', array())));
1136          $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1137          $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1138          $DB->expects($this->once())
1139              ->method('get_recordset_sql')
1140              ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1141  
1142          $this->assertEquals(array(
1143                  100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1144                      'progress' => array(13 => $progress1)),
1145                  201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1146                      'progress' => array(14 => $progress2)),
1147              ), $c->get_progress_all(false));
1148      }
1149  
1150      /**
1151       * @covers ::get_progress_all
1152       */
1153      public function test_get_progress_all_lots() {
1154          global $DB;
1155          $this->mock_setup();
1156  
1157          $mockbuilder = $this->getMockBuilder('completion_info');
1158          $mockbuilder->onlyMethods(array('get_tracked_users'));
1159          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1160          $c = $mockbuilder->getMock();
1161  
1162          $tracked = array();
1163          $ids = array();
1164          $progress = array();
1165          // With more than 1000 results.
1166          for ($i = 100; $i < 2000; $i++) {
1167              $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1168              $ids[] = $i;
1169              $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1170              $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1171          }
1172          $c->expects($this->once())
1173              ->method('get_tracked_users')
1174              ->with(true,  3,  0,  '',  '',  '',  null)
1175              ->will($this->returnValue($tracked));
1176          $DB->expects($this->exactly(2))
1177              ->method('get_in_or_equal')
1178              ->withConsecutive(
1179                  array(array_slice($ids, 0, 1000)),
1180                  array(array_slice($ids, 1000))
1181              )
1182              ->willReturnOnConsecutiveCalls(
1183                  array(' IN whatever', array()),
1184                  array(' IN whatever2', array()));
1185          $DB->expects($this->exactly(2))
1186              ->method('get_recordset_sql')
1187              ->willReturnOnConsecutiveCalls(
1188                  new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1189                  new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1190  
1191          $result = $c->get_progress_all(true, 3);
1192          $resultok = true;
1193          $resultok = $resultok && ($ids == array_keys($result));
1194  
1195          foreach ($result as $userid => $data) {
1196              $resultok = $resultok && $data->firstname == 'frog';
1197              $resultok = $resultok && $data->lastname == $userid;
1198              $resultok = $resultok && $data->id == $userid;
1199              $cms = $data->progress;
1200              $resultok = $resultok && (array(13, 14) == array_keys($cms));
1201              $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1202              $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1203          }
1204          $this->assertTrue($resultok);
1205          $this->assertCount(count($tracked), $result);
1206      }
1207  
1208      /**
1209       * @covers ::inform_grade_changed
1210       */
1211      public function test_inform_grade_changed() {
1212          $this->mock_setup();
1213  
1214          $mockbuilder = $this->getMockBuilder('completion_info');
1215          $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1216          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1217  
1218          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1219          $item = (object)array('itemnumber' => 3,  'gradepass' => 1,  'hidden' => 0);
1220          $grade = (object)array('userid' => 31337,  'finalgrade' => 0,  'rawgrade' => 0);
1221  
1222          // Not enabled (should do nothing).
1223          $c = $mockbuilder->getMock();
1224          $c->expects($this->once())
1225              ->method('is_enabled')
1226              ->with($cm)
1227              ->will($this->returnValue(false));
1228          $c->inform_grade_changed($cm, $item, $grade, false);
1229  
1230          // Enabled but still no grade completion required,  should still do nothing.
1231          $c = $mockbuilder->getMock();
1232          $c->expects($this->once())
1233              ->method('is_enabled')
1234              ->with($cm)
1235              ->will($this->returnValue(true));
1236          $c->inform_grade_changed($cm, $item, $grade, false);
1237  
1238          // Enabled and completion required but item number is wrong,  does nothing.
1239          $c = $mockbuilder->getMock();
1240          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1241          $c->expects($this->once())
1242              ->method('is_enabled')
1243              ->with($cm)
1244              ->will($this->returnValue(true));
1245          $c->inform_grade_changed($cm, $item, $grade, false);
1246  
1247          // Enabled and completion required and item number right. It is supposed
1248          // to call update_state with the new potential state being obtained from
1249          // internal_get_grade_state.
1250          $c = $mockbuilder->getMock();
1251          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1252          $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1253          $c->expects($this->once())
1254              ->method('is_enabled')
1255              ->with($cm)
1256              ->will($this->returnValue(true));
1257          $c->expects($this->once())
1258              ->method('update_state')
1259              ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1260              ->will($this->returnValue(true));
1261          $c->inform_grade_changed($cm, $item, $grade, false);
1262  
1263          // Same as above but marked deleted. It is supposed to call update_state
1264          // with new potential state being COMPLETION_INCOMPLETE.
1265          $c = $mockbuilder->getMock();
1266          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1267          $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1268          $c->expects($this->once())
1269              ->method('is_enabled')
1270              ->with($cm)
1271              ->will($this->returnValue(true));
1272          $c->expects($this->once())
1273              ->method('update_state')
1274              ->with($cm, COMPLETION_INCOMPLETE, 31337)
1275              ->will($this->returnValue(true));
1276          $c->inform_grade_changed($cm, $item, $grade, true);
1277      }
1278  
1279      /**
1280       * @covers ::internal_get_grade_state
1281       */
1282      public function test_internal_get_grade_state() {
1283          $this->mock_setup();
1284  
1285          $item = new stdClass;
1286          $grade = new stdClass;
1287  
1288          $item->gradepass = 4;
1289          $item->hidden = 0;
1290          $grade->rawgrade = 4.0;
1291          $grade->finalgrade = null;
1292  
1293          // Grade has pass mark and is not hidden,  user passes.
1294          $this->assertEquals(
1295              COMPLETION_COMPLETE_PASS,
1296              completion_info::internal_get_grade_state($item, $grade));
1297  
1298          // Same but user fails.
1299          $grade->rawgrade = 3.9;
1300          $this->assertEquals(
1301              COMPLETION_COMPLETE_FAIL,
1302              completion_info::internal_get_grade_state($item, $grade));
1303  
1304          // User fails on raw grade but passes on final.
1305          $grade->finalgrade = 4.0;
1306          $this->assertEquals(
1307              COMPLETION_COMPLETE_PASS,
1308              completion_info::internal_get_grade_state($item, $grade));
1309  
1310          // Item is hidden.
1311          $item->hidden = 1;
1312          $this->assertEquals(
1313              COMPLETION_COMPLETE,
1314              completion_info::internal_get_grade_state($item, $grade));
1315  
1316          // Item isn't hidden but has no pass mark.
1317          $item->hidden = 0;
1318          $item->gradepass = 0;
1319          $this->assertEquals(
1320              COMPLETION_COMPLETE,
1321              completion_info::internal_get_grade_state($item, $grade));
1322  
1323          // Item is hidden, but returnpassfail is true and the grade is passing.
1324          $item->hidden = 1;
1325          $item->gradepass = 4;
1326          $grade->finalgrade = 5.0;
1327          $this->assertEquals(
1328              COMPLETION_COMPLETE_PASS,
1329              completion_info::internal_get_grade_state($item, $grade, true));
1330  
1331          // Item is hidden, but returnpassfail is true and the grade is failing.
1332          $item->hidden = 1;
1333          $item->gradepass = 4;
1334          $grade->finalgrade = 3.0;
1335          $this->assertEquals(
1336              COMPLETION_COMPLETE_FAIL_HIDDEN,
1337              completion_info::internal_get_grade_state($item, $grade, true));
1338  
1339          // Item is not hidden, but returnpassfail is true and the grade is failing.
1340          $item->hidden = 0;
1341          $item->gradepass = 4;
1342          $grade->finalgrade = 3.0;
1343          $this->assertEquals(
1344              COMPLETION_COMPLETE_FAIL,
1345              completion_info::internal_get_grade_state($item, $grade, true));
1346      }
1347  
1348      /**
1349       * @test ::get_activities
1350       */
1351      public function test_get_activities() {
1352          global $CFG;
1353          $this->resetAfterTest();
1354  
1355          // Enable completion before creating modules, otherwise the completion data is not written in DB.
1356          $CFG->enablecompletion = true;
1357  
1358          // Create a course with mixed auto completion data.
1359          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1360          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1361          $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1362          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1363          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1364          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1365          $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1366  
1367          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1368          $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1369          $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1370  
1371          // Create data in another course to make sure it's not considered.
1372          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1373          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1374          $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1375          $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1376  
1377          $c = new completion_info($course);
1378          $activities = $c->get_activities();
1379          $this->assertCount(3, $activities);
1380          $this->assertTrue(isset($activities[$forum->cmid]));
1381          $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1382          $this->assertTrue(isset($activities[$page->cmid]));
1383          $this->assertSame($page->name, $activities[$page->cmid]->name);
1384          $this->assertTrue(isset($activities[$data->cmid]));
1385          $this->assertSame($data->name, $activities[$data->cmid]->name);
1386  
1387          $this->assertFalse(isset($activities[$forum2->cmid]));
1388          $this->assertFalse(isset($activities[$page2->cmid]));
1389          $this->assertFalse(isset($activities[$data2->cmid]));
1390      }
1391  
1392      /**
1393       * @test ::has_activities
1394       */
1395      public function test_has_activities() {
1396          global $CFG;
1397          $this->resetAfterTest();
1398  
1399          // Enable completion before creating modules, otherwise the completion data is not written in DB.
1400          $CFG->enablecompletion = true;
1401  
1402          // Create a course with mixed auto completion data.
1403          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1404          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1405          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1406          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1407          $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1408          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1409  
1410          $c1 = new completion_info($course);
1411          $c2 = new completion_info($course2);
1412  
1413          $this->assertTrue($c1->has_activities());
1414          $this->assertFalse($c2->has_activities());
1415      }
1416  
1417      /**
1418       * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1419       *
1420       * @covers ::delete_course_completion_data
1421       * @covers ::delete_all_completion_data
1422       */
1423      public function test_course_delete_prerequisite() {
1424          global $DB;
1425  
1426          $this->setup_data();
1427  
1428          $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1429  
1430          $criteriadata = (object) [
1431              'id' => $this->course->id,
1432              'criteria_course' => [$courseprerequisite->id],
1433          ];
1434  
1435          /** @var completion_criteria_course $criteria */
1436          $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1437          $criteria->update_config($criteriadata);
1438  
1439          // Sanity test.
1440          $this->assertTrue($DB->record_exists('course_completion_criteria', [
1441              'course' => $this->course->id,
1442              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1443              'courseinstance' => $courseprerequisite->id,
1444          ]));
1445  
1446          // Deleting the prerequisite course should remove the completion criteria.
1447          delete_course($courseprerequisite, false);
1448  
1449          $this->assertFalse($DB->record_exists('course_completion_criteria', [
1450              'course' => $this->course->id,
1451              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1452              'courseinstance' => $courseprerequisite->id,
1453          ]));
1454      }
1455  
1456      /**
1457       * Test course module completion update event.
1458       *
1459       * @covers \core\event\course_module_completion_updated
1460       */
1461      public function test_course_module_completion_updated_event() {
1462          global $USER, $CFG;
1463  
1464          $this->setup_data();
1465  
1466          $this->setAdminUser();
1467  
1468          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1469          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1470  
1471          $c = new completion_info($this->course);
1472          $activities = $c->get_activities();
1473          $this->assertEquals(1, count($activities));
1474          $this->assertTrue(isset($activities[$forum->cmid]));
1475          $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1476  
1477          $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1478          $current->completionstate = COMPLETION_COMPLETE;
1479          $current->timemodified = time();
1480          $sink = $this->redirectEvents();
1481          $c->internal_set_data($activities[$forum->cmid], $current);
1482          $events = $sink->get_events();
1483          $event = reset($events);
1484          $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1485          $this->assertEquals($forum->cmid,
1486              $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1487          $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1488          $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1489          $this->assertEquals($USER->id, $event->userid);
1490          $this->assertEquals($this->user->id, $event->relateduserid);
1491          $this->assertInstanceOf('moodle_url', $event->get_url());
1492          $this->assertEventLegacyData($current, $event);
1493      }
1494  
1495      /**
1496       * Test course completed event.
1497       *
1498       * @covers \core\event\course_completed
1499       */
1500      public function test_course_completed_event() {
1501          global $USER;
1502  
1503          $this->setup_data();
1504          $this->setAdminUser();
1505  
1506          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1507          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1508  
1509          // Mark course as complete and get triggered event.
1510          $sink = $this->redirectEvents();
1511          $ccompletion->mark_complete();
1512          $events = $sink->get_events();
1513          $event = reset($events);
1514  
1515          $this->assertInstanceOf('\core\event\course_completed', $event);
1516          $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1517          $this->assertEquals($this->course->id, $event->courseid);
1518          $this->assertEquals($USER->id, $event->userid);
1519          $this->assertEquals($this->user->id, $event->relateduserid);
1520          $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1521          $this->assertInstanceOf('moodle_url', $event->get_url());
1522          $data = $ccompletion->get_record_data();
1523          $this->assertEventLegacyData($data, $event);
1524      }
1525  
1526      /**
1527       * Test course completed message.
1528       *
1529       * @covers \core\event\course_completed
1530       */
1531      public function test_course_completed_message() {
1532          $this->setup_data();
1533          $this->setAdminUser();
1534  
1535          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1536          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1537  
1538          // Mark course as complete and get the message.
1539          $sink = $this->redirectMessages();
1540          $ccompletion->mark_complete();
1541          $messages = $sink->get_messages();
1542          $sink->close();
1543  
1544          $this->assertCount(1, $messages);
1545          $message = array_pop($messages);
1546  
1547          $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1548          $this->assertEquals($this->user->id, $message->useridto);
1549          $this->assertEquals('coursecompleted', $message->eventtype);
1550          $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1551          $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1552      }
1553  
1554      /**
1555       * Test course completed event.
1556       *
1557       * @covers \core\event\course_completion_updated
1558       */
1559      public function test_course_completion_updated_event() {
1560          $this->setup_data();
1561          $coursecontext = context_course::instance($this->course->id);
1562          $coursecompletionevent = \core\event\course_completion_updated::create(
1563                  array(
1564                      'courseid' => $this->course->id,
1565                      'context' => $coursecontext
1566                      )
1567                  );
1568  
1569          // Mark course as complete and get triggered event.
1570          $sink = $this->redirectEvents();
1571          $coursecompletionevent->trigger();
1572          $events = $sink->get_events();
1573          $event = array_pop($events);
1574          $sink->close();
1575  
1576          $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1577          $this->assertEquals($this->course->id, $event->courseid);
1578          $this->assertEquals($coursecontext, $event->get_context());
1579          $this->assertInstanceOf('moodle_url', $event->get_url());
1580          $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1581          $this->assertEventLegacyLogData($expectedlegacylog, $event);
1582      }
1583  
1584      /**
1585       * @covers \completion_can_view_data
1586       */
1587      public function test_completion_can_view_data() {
1588          $this->setup_data();
1589  
1590          $student = $this->getDataGenerator()->create_user();
1591          $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1592  
1593          $this->setUser($student);
1594          $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1595          $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1596      }
1597  
1598      /**
1599       * Data provider for test_get_grade_completion().
1600       *
1601       * @return array[]
1602       */
1603      public function get_grade_completion_provider() {
1604          return [
1605              'Grade not required' => [false, false, null, null, null],
1606              'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1607              'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1608              'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1609              'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1610          ];
1611      }
1612  
1613      /**
1614       * Test for \completion_info::get_grade_completion().
1615       *
1616       * @dataProvider get_grade_completion_provider
1617       * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1618       * @param bool $hasgrade Whether to set grade for the user in this activity.
1619       * @param int|null $passinggrade Passing grade to set for the test activity.
1620       * @param string|null $expectedexception Expected exception.
1621       * @param int|null $expectedresult The expected completion status.
1622       * @covers ::get_grade_completion
1623       */
1624      public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
1625          ?string $expectedexception, ?int $expectedresult) {
1626          $this->setup_data();
1627  
1628          /** @var \mod_assign_generator $assigngenerator */
1629          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1630          $assign = $assigngenerator->create_instance([
1631              'course' => $this->course->id,
1632              'completion' => COMPLETION_ENABLED,
1633              'completionusegrade' => $completionusegrade,
1634              'gradepass' => $passinggrade,
1635          ]);
1636  
1637          $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1638          if ($completionusegrade && $hasgrade) {
1639              $assigninstance = new assign($cm->context, $cm, $this->course);
1640              $grade = $assigninstance->get_user_grade($this->user->id, true);
1641              $grade->grade = 75;
1642              $assigninstance->update_grade($grade);
1643          }
1644  
1645          $completioninfo = new completion_info($this->course);
1646          if ($expectedexception) {
1647              $this->expectException($expectedexception);
1648          }
1649          $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1650          $this->assertEquals($expectedresult, $gradecompletion);
1651      }
1652  
1653      /**
1654       * Test the return value for cases when the activity module does not have associated grade_item.
1655       *
1656       * @covers ::get_grade_completion
1657       */
1658      public function test_get_grade_completion_without_grade_item() {
1659          global $DB;
1660  
1661          $this->setup_data();
1662  
1663          $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1664              'course' => $this->course->id,
1665              'completion' => COMPLETION_ENABLED,
1666              'completionusegrade' => true,
1667              'gradepass' => 42,
1668          ]);
1669  
1670          $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1671  
1672          $DB->delete_records('grade_items', [
1673              'courseid' => $this->course->id,
1674              'itemtype' => 'mod',
1675              'itemmodule' => 'assign',
1676              'iteminstance' => $assign->id,
1677          ]);
1678  
1679          // Without the grade_item, the activity is considered incomplete.
1680          $completioninfo = new completion_info($this->course);
1681          $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1682  
1683          // Once the activity is graded, the grade_item is automatically created.
1684          $assigninstance = new assign($cm->context, $cm, $this->course);
1685          $grade = $assigninstance->get_user_grade($this->user->id, true);
1686          $grade->grade = 40;
1687          $assigninstance->update_grade($grade);
1688  
1689          // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1690          $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1691      }
1692  
1693      /**
1694       * Test for aggregate_completions().
1695       *
1696       * @covers \aggregate_completions
1697       */
1698      public function test_aggregate_completions() {
1699          global $DB;
1700          $this->resetAfterTest(true);
1701          $time = time();
1702  
1703          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1704  
1705          for ($i = 0; $i < 4; $i++) {
1706              $students[] = $this->getDataGenerator()->create_user();
1707          }
1708  
1709          $teacher = $this->getDataGenerator()->create_user();
1710          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1711          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1712  
1713          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1714          foreach ($students as $student) {
1715              $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1716          }
1717  
1718          $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
1719              array('completion' => 1));
1720          $cmdata = get_coursemodule_from_id('data', $data->cmid);
1721  
1722          // Add activity completion criteria.
1723          $criteriadata = new stdClass();
1724          $criteriadata->id = $course->id;
1725          $criteriadata->criteria_activity = array();
1726          // Some activities.
1727          $criteriadata->criteria_activity[$cmdata->id] = 1;
1728          $class = 'completion_criteria_activity';
1729          $criterion = new $class();
1730          $criterion->update_config($criteriadata);
1731  
1732          $this->setUser($teacher);
1733  
1734          // Mark activity complete for both students.
1735          $cm = get_coursemodule_from_instance('data', $data->id);
1736          $completioncriteria = $DB->get_record('course_completion_criteria', []);
1737          foreach ($students as $student) {
1738              $cmcompletionrecords[] = (object)[
1739                  'coursemoduleid' => $cm->id,
1740                  'userid' => $student->id,
1741                  'completionstate' => 1,
1742                  'viewed' => 0,
1743                  'overrideby' => null,
1744                  'timemodified' => 0,
1745              ];
1746  
1747              $usercompletions[] = (object)[
1748                  'criteriaid' => $completioncriteria->id,
1749                  'userid' => $student->id,
1750                  'timecompleted' => $time,
1751              ];
1752  
1753              $cc = array(
1754                  'course'    => $course->id,
1755                  'userid'    => $student->id
1756              );
1757              $ccompletion = new completion_completion($cc);
1758              $completion[] = $ccompletion->mark_inprogress($time);
1759          }
1760          $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1761          $DB->insert_records('course_completion_crit_compl', $usercompletions);
1762  
1763          // MDL-33320: for instant completions we need aggregate to work in a single run.
1764          $DB->set_field('course_completions', 'reaggregate', $time - 2);
1765  
1766          foreach ($students as $student) {
1767              $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1768              $this->assertFalse($result);
1769          }
1770  
1771          aggregate_completions($completion[0]);
1772  
1773          $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]);
1774          $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]);
1775          $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]);
1776  
1777          $this->assertIsObject($result1);
1778          $this->assertFalse($result2);
1779          $this->assertFalse($result3);
1780  
1781          aggregate_completions(0);
1782  
1783          foreach ($students as $student) {
1784              $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1785              $this->assertIsObject($result);
1786          }
1787      }
1788  
1789      /**
1790       * Test for completion_completion::_save().
1791       *
1792       * @covers \completion_completion::_save
1793       */
1794      public function test_save() {
1795          global $DB;
1796          $this->resetAfterTest(true);
1797  
1798          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1799  
1800          $student = $this->getDataGenerator()->create_user();
1801          $teacher = $this->getDataGenerator()->create_user();
1802          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1803          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1804  
1805          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1806          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1807  
1808          $this->setUser($teacher);
1809  
1810          $cc = array(
1811              'course'    => $course->id,
1812              'userid'    => $student->id
1813          );
1814          $ccompletion = new completion_completion($cc);
1815  
1816          $completions = $DB->get_records('course_completions');
1817          $this->assertEmpty($completions);
1818  
1819          // We're testing a private method, so we need to setup reflector magic.
1820          $method = new ReflectionMethod($ccompletion, '_save');
1821          $method->setAccessible(true); // Allow accessing of private method.
1822          $completionid = $method->invoke($ccompletion);
1823          $completions = $DB->get_records('course_completions');
1824          $this->assertEquals(count($completions), 1);
1825          $this->assertEquals(reset($completions)->id, $completionid);
1826  
1827          $ccompletion->id = 0;
1828          $method = new ReflectionMethod($ccompletion, '_save');
1829          $method->setAccessible(true); // Allow accessing of private method.
1830          $completionid = $method->invoke($ccompletion);
1831          $this->assertDebuggingCalled('Can not update data object, no id!');
1832          $this->assertNull($completionid);
1833      }
1834  
1835      /**
1836       * Test for completion_completion::mark_enrolled().
1837       *
1838       * @covers \completion_completion::mark_enrolled
1839       */
1840      public function test_mark_enrolled() {
1841          global $DB;
1842          $this->resetAfterTest(true);
1843  
1844          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1845  
1846          $student = $this->getDataGenerator()->create_user();
1847          $teacher = $this->getDataGenerator()->create_user();
1848          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1849          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1850  
1851          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1852          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1853  
1854          $this->setUser($teacher);
1855  
1856          $cc = array(
1857              'course'    => $course->id,
1858              'userid'    => $student->id
1859          );
1860          $ccompletion = new completion_completion($cc);
1861  
1862          $completions = $DB->get_records('course_completions');
1863          $this->assertEmpty($completions);
1864  
1865          $completionid = $ccompletion->mark_enrolled();
1866          $completions = $DB->get_records('course_completions');
1867          $this->assertEquals(count($completions), 1);
1868          $this->assertEquals(reset($completions)->id, $completionid);
1869  
1870          $ccompletion->id = 0;
1871          $completionid = $ccompletion->mark_enrolled();
1872          $this->assertDebuggingCalled('Can not update data object, no id!');
1873          $this->assertNull($completionid);
1874          $completions = $DB->get_records('course_completions');
1875          $this->assertEquals(1, count($completions));
1876      }
1877  
1878      /**
1879       * Test for completion_completion::mark_inprogress().
1880       *
1881       * @covers \completion_completion::mark_inprogress
1882       */
1883      public function test_mark_inprogress() {
1884          global $DB;
1885          $this->resetAfterTest(true);
1886  
1887          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1888  
1889          $student = $this->getDataGenerator()->create_user();
1890          $teacher = $this->getDataGenerator()->create_user();
1891          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1892          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1893  
1894          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1895          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1896  
1897          $this->setUser($teacher);
1898  
1899          $cc = array(
1900              'course'    => $course->id,
1901              'userid'    => $student->id
1902          );
1903          $ccompletion = new completion_completion($cc);
1904  
1905          $completions = $DB->get_records('course_completions');
1906          $this->assertEmpty($completions);
1907  
1908          $completionid = $ccompletion->mark_inprogress();
1909          $completions = $DB->get_records('course_completions');
1910          $this->assertEquals(1, count($completions));
1911          $this->assertEquals(reset($completions)->id, $completionid);
1912  
1913          $ccompletion->id = 0;
1914          $completionid = $ccompletion->mark_inprogress();
1915          $this->assertDebuggingCalled('Can not update data object, no id!');
1916          $this->assertNull($completionid);
1917          $completions = $DB->get_records('course_completions');
1918          $this->assertEquals(1, count($completions));
1919      }
1920  
1921      /**
1922       * Test for completion_completion::mark_complete().
1923       *
1924       * @covers \completion_completion::mark_complete
1925       */
1926      public function test_mark_complete() {
1927          global $DB;
1928          $this->resetAfterTest(true);
1929  
1930          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1931  
1932          $student = $this->getDataGenerator()->create_user();
1933          $teacher = $this->getDataGenerator()->create_user();
1934          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1935          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1936  
1937          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1938          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1939  
1940          $this->setUser($teacher);
1941  
1942          $cc = array(
1943              'course'    => $course->id,
1944              'userid'    => $student->id
1945          );
1946          $ccompletion = new completion_completion($cc);
1947  
1948          $completions = $DB->get_records('course_completions');
1949          $this->assertEmpty($completions);
1950  
1951          $completionid = $ccompletion->mark_complete();
1952          $completions = $DB->get_records('course_completions');
1953          $this->assertEquals(1, count($completions));
1954          $this->assertEquals(reset($completions)->id, $completionid);
1955  
1956          $ccompletion->id = 0;
1957          $completionid = $ccompletion->mark_complete();
1958          $this->assertNull($completionid);
1959          $completions = $DB->get_records('course_completions');
1960          $this->assertEquals(1, count($completions));
1961      }
1962  
1963      /**
1964       * Test for completion_criteria_completion::mark_complete().
1965       *
1966       * @covers \completion_criteria_completion::mark_complete
1967       */
1968      public function test_criteria_mark_complete() {
1969          global $DB;
1970          $this->resetAfterTest(true);
1971  
1972          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1973  
1974          $student = $this->getDataGenerator()->create_user();
1975          $teacher = $this->getDataGenerator()->create_user();
1976          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1977          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1978  
1979          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1980          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1981  
1982          $this->setUser($teacher);
1983  
1984          $record = [
1985              'course'    => $course->id,
1986              'criteriaid'    => 1,
1987              'userid'    => $student->id,
1988              'timecompleted' => time()
1989          ];
1990          $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
1991  
1992          $completions = $DB->get_records('course_completions');
1993          $this->assertEmpty($completions);
1994  
1995          $completionid = $completion->mark_complete($record['timecompleted']);
1996          $completions = $DB->get_records('course_completions');
1997          $this->assertEquals(1, count($completions));
1998          $this->assertEquals(reset($completions)->id, $completionid);
1999      }
2000  
2001      /**
2002       * Test that data is cleaned when we reset a course completion data
2003       *
2004       * @covers ::delete_all_completion_data
2005       */
2006      public function test_course_reset_completion() {
2007          global $DB;
2008  
2009          $this->setup_data();
2010  
2011          $page = $this->getDataGenerator()->create_module('page', [
2012              'course' => $this->course->id,
2013              'completion' => COMPLETION_ENABLED,
2014              'completionview' => COMPLETION_VIEW_REQUIRED,
2015          ]);
2016          $cm = cm_info::create(get_coursemodule_from_instance('page', $page->id));
2017          $completion = new completion_info($this->course);
2018          $completion->set_module_viewed($cm, $this->user->id);
2019          // Sanity test.
2020          $this->assertTrue($DB->record_exists_select('course_modules_completion',
2021              'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2022              ['course' => $this->course->id]
2023          ));
2024          $this->assertTrue($DB->record_exists_select('course_modules_viewed',
2025              'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2026              ['course' => $this->course->id]
2027          ));
2028          // Deleting the prerequisite course should remove the completion criteria.
2029          $resetdata = new \stdClass();
2030          $resetdata->id = $this->course->id;
2031          $resetdata->reset_completion = true;
2032          reset_course_userdata($resetdata);
2033  
2034          $this->assertFalse($DB->record_exists_select('course_modules_completion',
2035              'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2036              ['course' => $this->course->id]
2037          ));
2038          $this->assertFalse($DB->record_exists_select('course_modules_viewed',
2039              'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2040              ['course' => $this->course->id]
2041          ));
2042      }
2043  }
2044  
2045  class core_completionlib_fake_recordset implements Iterator {
2046      protected $closed;
2047      protected $values, $index;
2048  
2049      public function __construct($values) {
2050          $this->values = $values;
2051          $this->index = 0;
2052      }
2053  
2054      #[\ReturnTypeWillChange]
2055      public function current() {
2056          return $this->values[$this->index];
2057      }
2058  
2059      #[\ReturnTypeWillChange]
2060      public function key() {
2061          return $this->values[$this->index];
2062      }
2063  
2064      public function next(): void {
2065          $this->index++;
2066      }
2067  
2068      public function rewind(): void {
2069          $this->index = 0;
2070      }
2071  
2072      public function valid(): bool {
2073          return count($this->values) > $this->index;
2074      }
2075  
2076      public function close() {
2077          $this->closed = true;
2078      }
2079  
2080      public function was_closed() {
2081          return $this->closed;
2082      }
2083  }