Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Completion tests.
  19   *
  20   * @package    core_completion
  21   * @category   phpunit
  22   * @copyright  2008 Sam Marshall
  23   * @copyright  2013 Frédéric Massart
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  global $CFG;
  30  require_once($CFG->libdir.'/completionlib.php');
  31  
  32  /**
  33   * Completion tests.
  34   *
  35   * @package    core_completion
  36   * @category   phpunit
  37   * @copyright  2008 Sam Marshall
  38   * @copyright  2013 Frédéric Massart
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   * @coversDefaultClass \completion_info
  41   */
  42  class completionlib_test extends advanced_testcase {
  43      protected $course;
  44      protected $user;
  45      protected $module1;
  46      protected $module2;
  47  
  48      protected function mock_setup() {
  49          global $DB, $CFG, $USER;
  50  
  51          $this->resetAfterTest();
  52  
  53          $DB = $this->createMock(get_class($DB));
  54          $CFG->enablecompletion = COMPLETION_ENABLED;
  55          $USER = (object)array('id' => 314159);
  56      }
  57  
  58      /**
  59       * Create course with user and activities.
  60       */
  61      protected function setup_data() {
  62          global $DB, $CFG;
  63  
  64          $this->resetAfterTest();
  65  
  66          // Enable completion before creating modules, otherwise the completion data is not written in DB.
  67          $CFG->enablecompletion = true;
  68  
  69          // Create a course with activities.
  70          $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
  71          $this->user = $this->getDataGenerator()->create_user();
  72          $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
  73  
  74          $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
  75          $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
  76      }
  77  
  78      /**
  79       * Asserts that two variables are equal.
  80       *
  81       * @param  mixed   $expected
  82       * @param  mixed   $actual
  83       * @param  string  $message
  84       * @param  float   $delta
  85       * @param  integer $maxDepth
  86       * @param  boolean $canonicalize
  87       * @param  boolean $ignoreCase
  88       */
  89      public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
  90                                          bool $canonicalize = false, bool $ignoreCase = false): void {
  91          // Nasty cheating hack: prevent random failures on timemodified field.
  92          if (is_array($actual) && (is_object($expected) || is_array($expected))) {
  93              $actual = (object) $actual;
  94              $expected = (object) $expected;
  95          }
  96          if (is_object($expected) and is_object($actual)) {
  97              if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
  98                  if ($expected->timemodified + 1 == $actual->timemodified) {
  99                      $expected = clone($expected);
 100                      $expected->timemodified = $actual->timemodified;
 101                  }
 102              }
 103          }
 104          parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
 105      }
 106  
 107      /**
 108       * @covers ::is_enabled_for_site
 109       * @covers ::is_enabled
 110       */
 111      public function test_is_enabled() {
 112          global $CFG;
 113          $this->mock_setup();
 114  
 115          // Config alone.
 116          $CFG->enablecompletion = COMPLETION_DISABLED;
 117          $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
 118          $CFG->enablecompletion = COMPLETION_ENABLED;
 119          $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
 120  
 121          // Course.
 122          $course = (object)array('id' => 13);
 123          $c = new completion_info($course);
 124          $course->enablecompletion = COMPLETION_DISABLED;
 125          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 126          $course->enablecompletion = COMPLETION_ENABLED;
 127          $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
 128          $CFG->enablecompletion = COMPLETION_DISABLED;
 129          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 130  
 131          // Course and CM.
 132          $cm = new stdClass();
 133          $cm->completion = COMPLETION_TRACKING_MANUAL;
 134          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 135          $CFG->enablecompletion = COMPLETION_ENABLED;
 136          $course->enablecompletion = COMPLETION_DISABLED;
 137          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 138          $course->enablecompletion = COMPLETION_ENABLED;
 139          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
 140          $cm->completion = COMPLETION_TRACKING_NONE;
 141          $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
 142          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 143          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
 144      }
 145  
 146      /**
 147       * @covers ::update_state
 148       */
 149      public function test_update_state() {
 150          $this->mock_setup();
 151  
 152          $mockbuilder = $this->getMockBuilder('completion_info');
 153          $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
 154                                         'user_can_override_completion'));
 155          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 156          $cm = (object)array('id' => 13, 'course' => 42);
 157  
 158          // Not enabled, should do nothing.
 159          $c = $mockbuilder->getMock();
 160          $c->expects($this->once())
 161              ->method('is_enabled')
 162              ->with($cm)
 163              ->will($this->returnValue(false));
 164          $c->update_state($cm);
 165  
 166          // Enabled, but current state is same as possible result, do nothing.
 167          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 168          $c = $mockbuilder->getMock();
 169          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 170          $c->expects($this->once())
 171              ->method('is_enabled')
 172              ->with($cm)
 173              ->will($this->returnValue(true));
 174          $c->expects($this->once())
 175              ->method('get_data')
 176              ->will($this->returnValue($current));
 177          $c->update_state($cm, COMPLETION_COMPLETE);
 178  
 179          // Enabled, but current state is a specific one and new state is just
 180          // complete, so do nothing.
 181          $c = $mockbuilder->getMock();
 182          $current->completionstate = COMPLETION_COMPLETE_PASS;
 183          $c->expects($this->once())
 184              ->method('is_enabled')
 185              ->with($cm)
 186              ->will($this->returnValue(true));
 187          $c->expects($this->once())
 188              ->method('get_data')
 189              ->will($this->returnValue($current));
 190          $c->update_state($cm, COMPLETION_COMPLETE);
 191  
 192          // Manual, change state (no change).
 193          $c = $mockbuilder->getMock();
 194          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 195          $current->completionstate = COMPLETION_COMPLETE;
 196          $c->expects($this->once())
 197              ->method('is_enabled')
 198              ->with($cm)
 199              ->will($this->returnValue(true));
 200          $c->expects($this->once())
 201              ->method('get_data')
 202              ->will($this->returnValue($current));
 203          $c->update_state($cm, COMPLETION_COMPLETE);
 204  
 205          // Manual, change state (change).
 206          $c = $mockbuilder->getMock();
 207          $c->expects($this->once())
 208              ->method('is_enabled')
 209              ->with($cm)
 210              ->will($this->returnValue(true));
 211          $c->expects($this->once())
 212              ->method('get_data')
 213              ->will($this->returnValue($current));
 214          $changed = clone($current);
 215          $changed->timemodified = time();
 216          $changed->completionstate = COMPLETION_INCOMPLETE;
 217          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 218          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 219          $c->expects($this->once())
 220              ->method('internal_set_data')
 221              ->with($cm, $comparewith);
 222          $c->update_state($cm, COMPLETION_INCOMPLETE);
 223  
 224          // Auto, change state.
 225          $c = $mockbuilder->getMock();
 226          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 227          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 228          $c->expects($this->once())
 229              ->method('is_enabled')
 230              ->with($cm)
 231              ->will($this->returnValue(true));
 232          $c->expects($this->once())
 233              ->method('get_data')
 234              ->will($this->returnValue($current));
 235          $c->expects($this->once())
 236              ->method('internal_get_state')
 237              ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
 238          $changed = clone($current);
 239          $changed->timemodified = time();
 240          $changed->completionstate = COMPLETION_COMPLETE_PASS;
 241          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 242          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 243          $c->expects($this->once())
 244              ->method('internal_set_data')
 245              ->with($cm, $comparewith);
 246          $c->update_state($cm, COMPLETION_COMPLETE_PASS);
 247  
 248          // Manual tracking, change state by overriding it manually.
 249          $c = $mockbuilder->getMock();
 250          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 251          $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 252          $current2 = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 253          $c->expects($this->exactly(2))
 254              ->method('is_enabled')
 255              ->with($cm)
 256              ->will($this->returnValue(true));
 257          $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
 258              ->method('user_can_override_completion')
 259              ->will($this->returnValue(true));
 260          $c->expects($this->exactly(2))
 261              ->method('get_data')
 262              ->with($cm, false, 100)
 263              ->willReturnOnConsecutiveCalls($current1, $current2);
 264          $changed1 = clone($current1);
 265          $changed1->timemodified = time();
 266          $changed1->completionstate = COMPLETION_COMPLETE;
 267          $changed1->overrideby = 314159;
 268          $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
 269          $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
 270          $changed2 = clone($current2);
 271          $changed2->timemodified = time();
 272          $changed2->overrideby = null;
 273          $changed2->completionstate = COMPLETION_INCOMPLETE;
 274          $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
 275          $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
 276          $c->expects($this->exactly(2))
 277              ->method('internal_set_data')
 278              ->withConsecutive(
 279                  array($cm, $comparewith1),
 280                  array($cm, $comparewith2)
 281              );
 282          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 283          // And confirm that the status can be changed back to incomplete without an override.
 284          $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
 285  
 286          // Auto, change state via override, incomplete to complete.
 287          $c = $mockbuilder->getMock();
 288          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 289          $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 290          $c->expects($this->once())
 291              ->method('is_enabled')
 292              ->with($cm)
 293              ->will($this->returnValue(true));
 294          $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
 295              ->method('user_can_override_completion')
 296              ->will($this->returnValue(true));
 297          $c->expects($this->once())
 298              ->method('get_data')
 299              ->with($cm, false, 100)
 300              ->will($this->returnValue($current));
 301          $changed = clone($current);
 302          $changed->timemodified = time();
 303          $changed->completionstate = COMPLETION_COMPLETE;
 304          $changed->overrideby = 314159;
 305          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 306          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 307          $c->expects($this->once())
 308              ->method('internal_set_data')
 309              ->with($cm, $comparewith);
 310          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 311  
 312          // Now confirm the status can be changed back from complete to incomplete using an override.
 313          $c = $mockbuilder->getMock();
 314          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 315          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
 316          $c->expects($this->once())
 317              ->method('is_enabled')
 318              ->with($cm)
 319              ->will($this->returnValue(true));
 320          $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
 321              ->method('user_can_override_completion')
 322              ->will($this->returnValue(true));
 323          $c->expects($this->once())
 324              ->method('get_data')
 325              ->with($cm, false, 100)
 326              ->will($this->returnValue($current));
 327          $changed = clone($current);
 328          $changed->timemodified = time();
 329          $changed->completionstate = COMPLETION_INCOMPLETE;
 330          $changed->overrideby = 314159;
 331          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 332          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 333          $c->expects($this->once())
 334              ->method('internal_set_data')
 335              ->with($cm, $comparewith);
 336          $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
 337      }
 338  
 339      /**
 340       * Data provider for test_internal_get_state().
 341       *
 342       * @return array[]
 343       */
 344      public function internal_get_state_provider() {
 345          return [
 346              'View required, but not viewed yet' => [
 347                  COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
 348              ],
 349              'View not required and not viewed yet' => [
 350                  COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
 351              ],
 352              'View not required, grade required but no grade yet, $cm->modname not set' => [
 353                  COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
 354              ],
 355              'View not required, grade required but no grade yet, $cm->course not set' => [
 356                  COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
 357              ],
 358              'View not required, grade not required' => [
 359                  COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
 360              ],
 361          ];
 362      }
 363  
 364      /**
 365       * Test for completion_info::get_state().
 366       *
 367       * @dataProvider internal_get_state_provider
 368       * @param int $completionview
 369       * @param int $completionusegrade
 370       * @param string $unsetfield
 371       * @param int $expectedstate
 372       * @covers ::internal_get_state
 373       */
 374      public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
 375          $this->setup_data();
 376  
 377          /** @var \mod_assign_generator $assigngenerator */
 378          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 379          $assign = $assigngenerator->create_instance([
 380              'course' => $this->course->id,
 381              'completion' => COMPLETION_ENABLED,
 382              'completionview' => $completionview,
 383              'completionusegrade' => $completionusegrade,
 384          ]);
 385  
 386          $userid = $this->user->id;
 387          $this->setUser($userid);
 388  
 389          $cm = get_coursemodule_from_instance('assign', $assign->id);
 390          if ($unsetfield) {
 391              unset($cm->$unsetfield);
 392          }
 393          // If view is required, but they haven't viewed it yet.
 394          $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
 395  
 396          $completioninfo = new completion_info($this->course);
 397          $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
 398      }
 399  
 400      /**
 401       * Covers the case where internal_get_state() is being called for a user different from the logged in user.
 402       *
 403       * @covers ::internal_get_state
 404       */
 405      public function test_internal_get_state_with_different_user() {
 406          $this->setup_data();
 407  
 408          /** @var \mod_assign_generator $assigngenerator */
 409          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 410          $assign = $assigngenerator->create_instance([
 411              'course' => $this->course->id,
 412              'completion' => COMPLETION_ENABLED,
 413              'completionusegrade' => 1,
 414          ]);
 415  
 416          $userid = $this->user->id;
 417  
 418          $cm = get_coursemodule_from_instance('assign', $assign->id);
 419          $usercm = cm_info::create($cm, $userid);
 420  
 421          // Create a teacher account.
 422          $teacher = $this->getDataGenerator()->create_user();
 423          $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
 424          // Log in as the teacher.
 425          $this->setUser($teacher);
 426  
 427          // Grade the student for this assignment.
 428          $assign = new assign($usercm->context, $cm, $cm->course);
 429          $data = (object)[
 430              'sendstudentnotifications' => false,
 431              'attemptnumber' => 1,
 432              'grade' => 90,
 433          ];
 434          $assign->save_grade($userid, $data);
 435  
 436          // The target user already received a grade, so internal_get_state should be already complete.
 437          $completioninfo = new completion_info($this->course);
 438          $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
 439  
 440          // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
 441          $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
 442      }
 443  
 444      /**
 445       * Test for internal_get_state() for an activity that supports custom completion.
 446       *
 447       * @covers ::internal_get_state
 448       */
 449      public function test_internal_get_state_with_custom_completion() {
 450          $this->setup_data();
 451  
 452          $choicerecord = [
 453              'course' => $this->course,
 454              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 455              'completionsubmit' => COMPLETION_ENABLED,
 456          ];
 457          $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
 458          $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
 459  
 460          $completioninfo = new completion_info($this->course);
 461  
 462          // Fetch completion for the user who hasn't made a choice yet.
 463          $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
 464          $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
 465  
 466          // Have the user make a choice.
 467          $choicewithoptions = choice_get_choice($choice->id);
 468          $optionids = array_keys($choicewithoptions->option);
 469          choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
 470          $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
 471          $this->assertEquals(COMPLETION_COMPLETE, $completion);
 472      }
 473  
 474      /**
 475       * @covers ::set_module_viewed
 476       */
 477      public function test_set_module_viewed() {
 478          $this->mock_setup();
 479  
 480          $mockbuilder = $this->getMockBuilder('completion_info');
 481          $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
 482          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 483          $cm = (object)array('id' => 13, 'course' => 42);
 484  
 485          // Not tracking completion, should do nothing.
 486          $c = $mockbuilder->getMock();
 487          $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
 488          $c->set_module_viewed($cm);
 489  
 490          // Tracking completion but completion is disabled, should do nothing.
 491          $c = $mockbuilder->getMock();
 492          $cm->completionview = COMPLETION_VIEW_REQUIRED;
 493          $c->expects($this->once())
 494              ->method('is_enabled')
 495              ->with($cm)
 496              ->will($this->returnValue(false));
 497          $c->set_module_viewed($cm);
 498  
 499          // Now it's enabled, we expect it to get data. If data already has
 500          // viewed, still do nothing.
 501          $c = $mockbuilder->getMock();
 502          $c->expects($this->once())
 503              ->method('is_enabled')
 504              ->with($cm)
 505              ->will($this->returnValue(true));
 506          $c->expects($this->once())
 507              ->method('get_data')
 508              ->with($cm, 0)
 509              ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
 510          $c->set_module_viewed($cm);
 511  
 512          // OK finally one that hasn't been viewed, now it should set it viewed
 513          // and update state.
 514          $c = $mockbuilder->getMock();
 515          $c->expects($this->once())
 516              ->method('is_enabled')
 517              ->with($cm)
 518              ->will($this->returnValue(true));
 519          $c->expects($this->once())
 520              ->method('get_data')
 521              ->with($cm, false, 1337)
 522              ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
 523          $c->expects($this->once())
 524              ->method('internal_set_data')
 525              ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
 526          $c->expects($this->once())
 527              ->method('update_state')
 528              ->with($cm, COMPLETION_COMPLETE, 1337);
 529          $c->set_module_viewed($cm, 1337);
 530      }
 531  
 532      /**
 533       * @covers ::count_user_data
 534       */
 535      public function test_count_user_data() {
 536          global $DB;
 537          $this->mock_setup();
 538  
 539          $course = (object)array('id' => 13);
 540          $cm = (object)array('id' => 42);
 541  
 542          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 543          $DB->expects($this->once())
 544              ->method('get_field_sql')
 545              ->will($this->returnValue(666));
 546  
 547          $c = new completion_info($course);
 548          $this->assertEquals(666, $c->count_user_data($cm));
 549      }
 550  
 551      /**
 552       * @covers ::delete_all_state
 553       */
 554      public function test_delete_all_state() {
 555          global $DB;
 556          $this->mock_setup();
 557  
 558          $course = (object)array('id' => 13);
 559          $cm = (object)array('id' => 42, 'course' => 13);
 560          $c = new completion_info($course);
 561  
 562          // Check it works ok without data in session.
 563          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 564          $DB->expects($this->once())
 565              ->method('delete_records')
 566              ->with('course_modules_completion', array('coursemoduleid' => 42))
 567              ->will($this->returnValue(true));
 568          $c->delete_all_state($cm);
 569      }
 570  
 571      /**
 572       * @covers ::reset_all_state
 573       */
 574      public function test_reset_all_state() {
 575          global $DB;
 576          $this->mock_setup();
 577  
 578          $mockbuilder = $this->getMockBuilder('completion_info');
 579          $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
 580          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 581          $c = $mockbuilder->getMock();
 582  
 583          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 584  
 585          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 586          $DB->expects($this->once())
 587              ->method('get_recordset')
 588              ->will($this->returnValue(
 589                  new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
 590                      (object)array('id' => 2, 'userid' => 101)))));
 591  
 592          $c->expects($this->once())
 593              ->method('delete_all_state')
 594              ->with($cm);
 595  
 596          $c->expects($this->once())
 597              ->method('get_tracked_users')
 598              ->will($this->returnValue(array(
 599              (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
 600              (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
 601  
 602          $c->expects($this->exactly(3))
 603              ->method('update_state')
 604              ->withConsecutive(
 605                  array($cm, COMPLETION_UNKNOWN, 100),
 606                  array($cm, COMPLETION_UNKNOWN, 101),
 607                  array($cm, COMPLETION_UNKNOWN, 201)
 608              );
 609  
 610          $c->reset_all_state($cm);
 611      }
 612  
 613      /**
 614       * Data provider for test_get_data().
 615       *
 616       * @return array[]
 617       */
 618      public function get_data_provider() {
 619          return [
 620              'No completion record' => [
 621                  false, true, false, COMPLETION_INCOMPLETE
 622              ],
 623              'Not completed' => [
 624                  false, true, true, COMPLETION_INCOMPLETE
 625              ],
 626              'Completed' => [
 627                  false, true, true, COMPLETION_COMPLETE
 628              ],
 629              'Whole course, complete' => [
 630                  true, true, true, COMPLETION_COMPLETE
 631              ],
 632              'Get data for another user, result should be not cached' => [
 633                  false, false, true,  COMPLETION_INCOMPLETE
 634              ],
 635              'Get data for another user, including whole course, result should be not cached' => [
 636                  true, false, true,  COMPLETION_INCOMPLETE
 637              ],
 638          ];
 639      }
 640  
 641      /**
 642       * Tests for completion_info::get_data().
 643       *
 644       * @dataProvider get_data_provider
 645       * @param bool $wholecourse Whole course parameter for get_data().
 646       * @param bool $sameuser Whether the user calling get_data() is the user itself.
 647       * @param bool $hasrecord Whether to create a course_modules_completion record.
 648       * @param int $completion The completion state expected.
 649       * @covers ::get_data
 650       */
 651      public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
 652          global $DB;
 653  
 654          $this->setup_data();
 655          $user = $this->user;
 656  
 657          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 658          $choice = $choicegenerator->create_instance([
 659              'course' => $this->course->id,
 660              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 661              'completionview' => true,
 662              'completionsubmit' => true,
 663          ]);
 664  
 665          $cm = get_coursemodule_from_instance('choice', $choice->id);
 666  
 667          // Let's manually create a course completion record instead of going through the hoops to complete an activity.
 668          if ($hasrecord) {
 669              $cmcompletionrecord = (object)[
 670                  'coursemoduleid' => $cm->id,
 671                  'userid' => $user->id,
 672                  'completionstate' => $completion,
 673                  'viewed' => 0,
 674                  'overrideby' => null,
 675                  'timemodified' => 0,
 676              ];
 677              $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 678          }
 679  
 680          // Whether we expect for the returned completion data to be stored in the cache.
 681          $iscached = true;
 682  
 683          if (!$sameuser) {
 684              $iscached = false;
 685              $this->setAdminUser();
 686          } else {
 687              $this->setUser($user);
 688          }
 689  
 690          // Mock other completion data.
 691          $completioninfo = new completion_info($this->course);
 692  
 693          $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
 694  
 695          // Course module ID of the returned completion data must match this activity's course module ID.
 696          $this->assertEquals($cm->id, $result->coursemoduleid);
 697          // User ID of the returned completion data must match the user's ID.
 698          $this->assertEquals($user->id, $result->userid);
 699          // The completion state of the returned completion data must match the expected completion state.
 700          $this->assertEquals($completion, $result->completionstate);
 701  
 702          // If the user has no completion record, then the default record should be returned.
 703          if (!$hasrecord) {
 704              $this->assertEquals(0, $result->id);
 705          }
 706  
 707          // Check that we are including relevant completion data for the module.
 708          if (!$wholecourse) {
 709              $this->assertTrue(property_exists($result, 'viewed'));
 710              $this->assertTrue(property_exists($result, 'customcompletion'));
 711          }
 712      }
 713  
 714      /**
 715       * @covers ::get_data
 716       */
 717      public function test_get_data_successive_calls(): void {
 718          global $DB;
 719  
 720          $this->setup_data();
 721          $this->setUser($this->user);
 722  
 723          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 724          $choice = $choicegenerator->create_instance([
 725              'course' => $this->course->id,
 726              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 727              'completionview' => true,
 728              'completionsubmit' => true,
 729          ]);
 730  
 731          $cm = get_coursemodule_from_instance('choice', $choice->id);
 732  
 733          // Let's manually create a course completion record instead of going through the hoops to complete an activity.
 734          $cmcompletionrecord = (object) [
 735              'coursemoduleid' => $cm->id,
 736              'userid' => $this->user->id,
 737              'completionstate' => COMPLETION_NOT_VIEWED,
 738              'viewed' => 0,
 739              'overrideby' => null,
 740              'timemodified' => 0,
 741          ];
 742          $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 743  
 744          // Mock other completion data.
 745          $completioninfo = new completion_info($this->course);
 746  
 747          $modinfo = get_fast_modinfo($this->course);
 748          $results = [];
 749          foreach ($modinfo->cms as $testcm) {
 750              $result = $completioninfo->get_data($testcm, true);
 751              $this->assertTrue(property_exists($result, 'id'));
 752              $this->assertTrue(property_exists($result, 'coursemoduleid'));
 753              $this->assertTrue(property_exists($result, 'userid'));
 754              $this->assertTrue(property_exists($result, 'completionstate'));
 755              $this->assertTrue(property_exists($result, 'viewed'));
 756              $this->assertTrue(property_exists($result, 'overrideby'));
 757              $this->assertTrue(property_exists($result, 'timemodified'));
 758              $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
 759  
 760              $this->assertEquals($testcm->id, $result->coursemoduleid);
 761              $this->assertEquals($this->user->id, $result->userid);
 762              $this->assertEquals(0, $result->viewed);
 763  
 764              $results[$testcm->id] = $result;
 765          }
 766  
 767          $result = $completioninfo->get_data($cm);
 768          $this->assertTrue(property_exists($result, 'customcompletion'));
 769  
 770          // The data should match when fetching modules individually.
 771          (cache::make('core', 'completion'))->purge();
 772          foreach ($modinfo->cms as $testcm) {
 773              $result = $completioninfo->get_data($testcm, false);
 774              $this->assertEquals($result, $results[$testcm->id]);
 775          }
 776      }
 777  
 778      /**
 779       * Tests for completion_info::get_other_cm_completion_data().
 780       *
 781       * @covers ::get_other_cm_completion_data
 782       */
 783      public function test_get_other_cm_completion_data() {
 784          global $DB;
 785  
 786          $this->setup_data();
 787          $user = $this->user;
 788  
 789          $this->setAdminUser();
 790  
 791          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 792          $choice = $choicegenerator->create_instance([
 793              'course' => $this->course->id,
 794              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 795              'completionsubmit' => true,
 796          ]);
 797  
 798          $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
 799  
 800          $choice2 = $choicegenerator->create_instance([
 801              'course' => $this->course->id,
 802              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 803          ]);
 804  
 805          $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
 806  
 807          $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
 808          $workshop = $workshopgenerator->create_instance([
 809              'course' => $this->course->id,
 810              'completion' => COMPLETION_TRACKING_AUTOMATIC,
 811              // Submission grade required.
 812              'completiongradeitemnumber' => 0,
 813          ]);
 814  
 815          $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
 816  
 817          $completioninfo = new completion_info($this->course);
 818  
 819          $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
 820          $method->setAccessible(true);
 821  
 822          // Check that fetching data for a module with custom completion provides its info.
 823          $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
 824  
 825          $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
 826          $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
 827          $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
 828  
 829          // Mock a choice answer so user has completed the requirement.
 830          $choicemockinfo = [
 831              'choiceid' => $cmchoice->instance,
 832              'userid' => $this->user->id
 833          ];
 834          $DB->insert_record('choice_answers', $choicemockinfo, false);
 835  
 836          // Confirm fetching again reflects the completion.
 837          $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
 838          $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
 839  
 840          // Check that fetching data for a module with no custom completion still provides its grade completion status.
 841          $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
 842  
 843          $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
 844          $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
 845          $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
 846  
 847          // Check that fetching data for a module with no completion conditions does not provide any data.
 848          $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
 849          $this->assertEmpty($choice2completiondata);
 850      }
 851  
 852      /**
 853       * @covers ::internal_set_data
 854       */
 855      public function test_internal_set_data() {
 856          global $DB;
 857          $this->setup_data();
 858  
 859          $this->setUser($this->user);
 860          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
 861          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 862          $cm = get_coursemodule_from_instance('forum', $forum->id);
 863          $c = new completion_info($this->course);
 864  
 865          // 1) Test with new data.
 866          $data = new stdClass();
 867          $data->id = 0;
 868          $data->userid = $this->user->id;
 869          $data->coursemoduleid = $cm->id;
 870          $data->completionstate = COMPLETION_COMPLETE;
 871          $data->timemodified = time();
 872          $data->viewed = COMPLETION_NOT_VIEWED;
 873          $data->overrideby = null;
 874  
 875          $c->internal_set_data($cm, $data);
 876          $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
 877          $this->assertEquals($d1, $data->id);
 878          $cache = cache::make('core', 'completion');
 879          // Cache was not set for another user.
 880          $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
 881          $this->assertEquals([
 882              'cacherev' => $this->course->cacherev,
 883              $cm->id => array_merge(
 884                  (array) $data,
 885                  ['other_cm_completion_data_fetched' => true]
 886              ),
 887          ],
 888          $cachevalue);
 889  
 890          // 2) Test with existing data and for different user.
 891          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 892          $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
 893          $newuser = $this->getDataGenerator()->create_user();
 894  
 895          $d2 = new stdClass();
 896          $d2->id = 7;
 897          $d2->userid = $newuser->id;
 898          $d2->coursemoduleid = $cm2->id;
 899          $d2->completionstate = COMPLETION_COMPLETE;
 900          $d2->timemodified = time();
 901          $d2->viewed = COMPLETION_NOT_VIEWED;
 902          $d2->overrideby = null;
 903          $c->internal_set_data($cm2, $d2);
 904          // Cache for current user returns the data.
 905          $cachevalue = $cache->get($data->userid . '_' . $cm->course);
 906          $this->assertEquals(array_merge(
 907              (array) $data,
 908              ['other_cm_completion_data_fetched' => true]
 909          ), $cachevalue[$cm->id]);
 910  
 911          // Cache for another user is not filled.
 912          $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
 913  
 914          // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
 915          $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 916          $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
 917          $newuser2 = $this->getDataGenerator()->create_user();
 918          $d3 = new stdClass();
 919          $d3->id = 13;
 920          $d3->userid = $newuser2->id;
 921          $d3->coursemoduleid = $cm3->id;
 922          $d3->completionstate = COMPLETION_COMPLETE;
 923          $d3->timemodified = time();
 924          $d3->viewed = COMPLETION_NOT_VIEWED;
 925          $d3->overrideby = null;
 926          $DB->insert_record('course_modules_completion', $d3);
 927          $c->internal_set_data($cm, $data);
 928      }
 929  
 930      /**
 931       * @covers ::get_progress_all
 932       */
 933      public function test_get_progress_all_few() {
 934          global $DB;
 935          $this->mock_setup();
 936  
 937          $mockbuilder = $this->getMockBuilder('completion_info');
 938          $mockbuilder->onlyMethods(array('get_tracked_users'));
 939          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 940          $c = $mockbuilder->getMock();
 941  
 942          // With few results.
 943          $c->expects($this->once())
 944              ->method('get_tracked_users')
 945              ->with(false,  array(),  0,  '',  '',  '',  null)
 946              ->will($this->returnValue(array(
 947                  (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
 948                  (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
 949          $DB->expects($this->once())
 950              ->method('get_in_or_equal')
 951              ->with(array(100, 201))
 952              ->will($this->returnValue(array(' IN (100, 201)', array())));
 953          $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
 954          $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
 955          $DB->expects($this->once())
 956              ->method('get_recordset_sql')
 957              ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
 958  
 959          $this->assertEquals(array(
 960                  100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
 961                      'progress' => array(13 => $progress1)),
 962                  201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
 963                      'progress' => array(14 => $progress2)),
 964              ), $c->get_progress_all(false));
 965      }
 966  
 967      /**
 968       * @covers ::get_progress_all
 969       */
 970      public function test_get_progress_all_lots() {
 971          global $DB;
 972          $this->mock_setup();
 973  
 974          $mockbuilder = $this->getMockBuilder('completion_info');
 975          $mockbuilder->onlyMethods(array('get_tracked_users'));
 976          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 977          $c = $mockbuilder->getMock();
 978  
 979          $tracked = array();
 980          $ids = array();
 981          $progress = array();
 982          // With more than 1000 results.
 983          for ($i = 100; $i < 2000; $i++) {
 984              $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
 985              $ids[] = $i;
 986              $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
 987              $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
 988          }
 989          $c->expects($this->once())
 990              ->method('get_tracked_users')
 991              ->with(true,  3,  0,  '',  '',  '',  null)
 992              ->will($this->returnValue($tracked));
 993          $DB->expects($this->exactly(2))
 994              ->method('get_in_or_equal')
 995              ->withConsecutive(
 996                  array(array_slice($ids, 0, 1000)),
 997                  array(array_slice($ids, 1000))
 998              )
 999              ->willReturnOnConsecutiveCalls(
1000                  array(' IN whatever', array()),
1001                  array(' IN whatever2', array()));
1002          $DB->expects($this->exactly(2))
1003              ->method('get_recordset_sql')
1004              ->willReturnOnConsecutiveCalls(
1005                  new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1006                  new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1007  
1008          $result = $c->get_progress_all(true, 3);
1009          $resultok = true;
1010          $resultok = $resultok && ($ids == array_keys($result));
1011  
1012          foreach ($result as $userid => $data) {
1013              $resultok = $resultok && $data->firstname == 'frog';
1014              $resultok = $resultok && $data->lastname == $userid;
1015              $resultok = $resultok && $data->id == $userid;
1016              $cms = $data->progress;
1017              $resultok = $resultok && (array(13, 14) == array_keys($cms));
1018              $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1019              $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1020          }
1021          $this->assertTrue($resultok);
1022          $this->assertCount(count($tracked), $result);
1023      }
1024  
1025      /**
1026       * @covers ::inform_grade_changed
1027       */
1028      public function test_inform_grade_changed() {
1029          $this->mock_setup();
1030  
1031          $mockbuilder = $this->getMockBuilder('completion_info');
1032          $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1033          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1034  
1035          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1036          $item = (object)array('itemnumber' => 3,  'gradepass' => 1,  'hidden' => 0);
1037          $grade = (object)array('userid' => 31337,  'finalgrade' => 0,  'rawgrade' => 0);
1038  
1039          // Not enabled (should do nothing).
1040          $c = $mockbuilder->getMock();
1041          $c->expects($this->once())
1042              ->method('is_enabled')
1043              ->with($cm)
1044              ->will($this->returnValue(false));
1045          $c->inform_grade_changed($cm, $item, $grade, false);
1046  
1047          // Enabled but still no grade completion required,  should still do nothing.
1048          $c = $mockbuilder->getMock();
1049          $c->expects($this->once())
1050              ->method('is_enabled')
1051              ->with($cm)
1052              ->will($this->returnValue(true));
1053          $c->inform_grade_changed($cm, $item, $grade, false);
1054  
1055          // Enabled and completion required but item number is wrong,  does nothing.
1056          $c = $mockbuilder->getMock();
1057          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1058          $c->expects($this->once())
1059              ->method('is_enabled')
1060              ->with($cm)
1061              ->will($this->returnValue(true));
1062          $c->inform_grade_changed($cm, $item, $grade, false);
1063  
1064          // Enabled and completion required and item number right. It is supposed
1065          // to call update_state with the new potential state being obtained from
1066          // internal_get_grade_state.
1067          $c = $mockbuilder->getMock();
1068          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1069          $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1070          $c->expects($this->once())
1071              ->method('is_enabled')
1072              ->with($cm)
1073              ->will($this->returnValue(true));
1074          $c->expects($this->once())
1075              ->method('update_state')
1076              ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1077              ->will($this->returnValue(true));
1078          $c->inform_grade_changed($cm, $item, $grade, false);
1079  
1080          // Same as above but marked deleted. It is supposed to call update_state
1081          // with new potential state being COMPLETION_INCOMPLETE.
1082          $c = $mockbuilder->getMock();
1083          $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1084          $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1085          $c->expects($this->once())
1086              ->method('is_enabled')
1087              ->with($cm)
1088              ->will($this->returnValue(true));
1089          $c->expects($this->once())
1090              ->method('update_state')
1091              ->with($cm, COMPLETION_INCOMPLETE, 31337)
1092              ->will($this->returnValue(true));
1093          $c->inform_grade_changed($cm, $item, $grade, true);
1094      }
1095  
1096      /**
1097       * @covers ::internal_get_grade_state
1098       */
1099      public function test_internal_get_grade_state() {
1100          $this->mock_setup();
1101  
1102          $item = new stdClass;
1103          $grade = new stdClass;
1104  
1105          $item->gradepass = 4;
1106          $item->hidden = 0;
1107          $grade->rawgrade = 4.0;
1108          $grade->finalgrade = null;
1109  
1110          // Grade has pass mark and is not hidden,  user passes.
1111          $this->assertEquals(
1112              COMPLETION_COMPLETE_PASS,
1113              completion_info::internal_get_grade_state($item, $grade));
1114  
1115          // Same but user fails.
1116          $grade->rawgrade = 3.9;
1117          $this->assertEquals(
1118              COMPLETION_COMPLETE_FAIL,
1119              completion_info::internal_get_grade_state($item, $grade));
1120  
1121          // User fails on raw grade but passes on final.
1122          $grade->finalgrade = 4.0;
1123          $this->assertEquals(
1124              COMPLETION_COMPLETE_PASS,
1125              completion_info::internal_get_grade_state($item, $grade));
1126  
1127          // Item is hidden.
1128          $item->hidden = 1;
1129          $this->assertEquals(
1130              COMPLETION_COMPLETE,
1131              completion_info::internal_get_grade_state($item, $grade));
1132  
1133          // Item isn't hidden but has no pass mark.
1134          $item->hidden = 0;
1135          $item->gradepass = 0;
1136          $this->assertEquals(
1137              COMPLETION_COMPLETE,
1138              completion_info::internal_get_grade_state($item, $grade));
1139      }
1140  
1141      /**
1142       * @test ::get_activities
1143       */
1144      public function test_get_activities() {
1145          global $CFG;
1146          $this->resetAfterTest();
1147  
1148          // Enable completion before creating modules, otherwise the completion data is not written in DB.
1149          $CFG->enablecompletion = true;
1150  
1151          // Create a course with mixed auto completion data.
1152          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1153          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1154          $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1155          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1156          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1157          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1158          $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1159  
1160          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1161          $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1162          $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1163  
1164          // Create data in another course to make sure it's not considered.
1165          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1166          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1167          $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1168          $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1169  
1170          $c = new completion_info($course);
1171          $activities = $c->get_activities();
1172          $this->assertCount(3, $activities);
1173          $this->assertTrue(isset($activities[$forum->cmid]));
1174          $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1175          $this->assertTrue(isset($activities[$page->cmid]));
1176          $this->assertSame($page->name, $activities[$page->cmid]->name);
1177          $this->assertTrue(isset($activities[$data->cmid]));
1178          $this->assertSame($data->name, $activities[$data->cmid]->name);
1179  
1180          $this->assertFalse(isset($activities[$forum2->cmid]));
1181          $this->assertFalse(isset($activities[$page2->cmid]));
1182          $this->assertFalse(isset($activities[$data2->cmid]));
1183      }
1184  
1185      /**
1186       * @test ::has_activities
1187       */
1188      public function test_has_activities() {
1189          global $CFG;
1190          $this->resetAfterTest();
1191  
1192          // Enable completion before creating modules, otherwise the completion data is not written in DB.
1193          $CFG->enablecompletion = true;
1194  
1195          // Create a course with mixed auto completion data.
1196          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1197          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1198          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1199          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1200          $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1201          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1202  
1203          $c1 = new completion_info($course);
1204          $c2 = new completion_info($course2);
1205  
1206          $this->assertTrue($c1->has_activities());
1207          $this->assertFalse($c2->has_activities());
1208      }
1209  
1210      /**
1211       * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1212       *
1213       * @covers ::delete_course_completion_data
1214       * @covers ::delete_all_completion_data
1215       */
1216      public function test_course_delete_prerequisite() {
1217          global $DB;
1218  
1219          $this->setup_data();
1220  
1221          $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1222  
1223          $criteriadata = (object) [
1224              'id' => $this->course->id,
1225              'criteria_course' => [$courseprerequisite->id],
1226          ];
1227  
1228          /** @var completion_criteria_course $criteria */
1229          $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1230          $criteria->update_config($criteriadata);
1231  
1232          // Sanity test.
1233          $this->assertTrue($DB->record_exists('course_completion_criteria', [
1234              'course' => $this->course->id,
1235              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1236              'courseinstance' => $courseprerequisite->id,
1237          ]));
1238  
1239          // Deleting the prerequisite course should remove the completion criteria.
1240          delete_course($courseprerequisite, false);
1241  
1242          $this->assertFalse($DB->record_exists('course_completion_criteria', [
1243              'course' => $this->course->id,
1244              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1245              'courseinstance' => $courseprerequisite->id,
1246          ]));
1247      }
1248  
1249      /**
1250       * Test course module completion update event.
1251       *
1252       * @covers \core\event\course_module_completion_updated
1253       */
1254      public function test_course_module_completion_updated_event() {
1255          global $USER, $CFG;
1256  
1257          $this->setup_data();
1258  
1259          $this->setAdminUser();
1260  
1261          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1262          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1263  
1264          $c = new completion_info($this->course);
1265          $activities = $c->get_activities();
1266          $this->assertEquals(1, count($activities));
1267          $this->assertTrue(isset($activities[$forum->cmid]));
1268          $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1269  
1270          $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1271          $current->completionstate = COMPLETION_COMPLETE;
1272          $current->timemodified = time();
1273          $sink = $this->redirectEvents();
1274          $c->internal_set_data($activities[$forum->cmid], $current);
1275          $events = $sink->get_events();
1276          $event = reset($events);
1277          $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1278          $this->assertEquals($forum->cmid,
1279              $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1280          $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1281          $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1282          $this->assertEquals($USER->id, $event->userid);
1283          $this->assertEquals($this->user->id, $event->relateduserid);
1284          $this->assertInstanceOf('moodle_url', $event->get_url());
1285          $this->assertEventLegacyData($current, $event);
1286      }
1287  
1288      /**
1289       * Test course completed event.
1290       *
1291       * @covers \core\event\course_completed
1292       */
1293      public function test_course_completed_event() {
1294          global $USER;
1295  
1296          $this->setup_data();
1297          $this->setAdminUser();
1298  
1299          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1300          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1301  
1302          // Mark course as complete and get triggered event.
1303          $sink = $this->redirectEvents();
1304          $ccompletion->mark_complete();
1305          $events = $sink->get_events();
1306          $event = reset($events);
1307  
1308          $this->assertInstanceOf('\core\event\course_completed', $event);
1309          $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1310          $this->assertEquals($this->course->id, $event->courseid);
1311          $this->assertEquals($USER->id, $event->userid);
1312          $this->assertEquals($this->user->id, $event->relateduserid);
1313          $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1314          $this->assertInstanceOf('moodle_url', $event->get_url());
1315          $data = $ccompletion->get_record_data();
1316          $this->assertEventLegacyData($data, $event);
1317      }
1318  
1319      /**
1320       * Test course completed message.
1321       *
1322       * @covers \core\event\course_completed
1323       */
1324      public function test_course_completed_message() {
1325          $this->setup_data();
1326          $this->setAdminUser();
1327  
1328          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1329          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1330  
1331          // Mark course as complete and get the message.
1332          $sink = $this->redirectMessages();
1333          $ccompletion->mark_complete();
1334          $messages = $sink->get_messages();
1335          $sink->close();
1336  
1337          $this->assertCount(1, $messages);
1338          $message = array_pop($messages);
1339  
1340          $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1341          $this->assertEquals($this->user->id, $message->useridto);
1342          $this->assertEquals('coursecompleted', $message->eventtype);
1343          $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1344          $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1345      }
1346  
1347      /**
1348       * Test course completed event.
1349       *
1350       * @covers \core\event\course_completion_updated
1351       */
1352      public function test_course_completion_updated_event() {
1353          $this->setup_data();
1354          $coursecontext = context_course::instance($this->course->id);
1355          $coursecompletionevent = \core\event\course_completion_updated::create(
1356                  array(
1357                      'courseid' => $this->course->id,
1358                      'context' => $coursecontext
1359                      )
1360                  );
1361  
1362          // Mark course as complete and get triggered event.
1363          $sink = $this->redirectEvents();
1364          $coursecompletionevent->trigger();
1365          $events = $sink->get_events();
1366          $event = array_pop($events);
1367          $sink->close();
1368  
1369          $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1370          $this->assertEquals($this->course->id, $event->courseid);
1371          $this->assertEquals($coursecontext, $event->get_context());
1372          $this->assertInstanceOf('moodle_url', $event->get_url());
1373          $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1374          $this->assertEventLegacyLogData($expectedlegacylog, $event);
1375      }
1376  
1377      /**
1378       * @covers \completion_can_view_data
1379       */
1380      public function test_completion_can_view_data() {
1381          $this->setup_data();
1382  
1383          $student = $this->getDataGenerator()->create_user();
1384          $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1385  
1386          $this->setUser($student);
1387          $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1388          $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1389      }
1390  
1391      /**
1392       * Data provider for test_get_grade_completion().
1393       *
1394       * @return array[]
1395       */
1396      public function get_grade_completion_provider() {
1397          return [
1398              'Grade not required' => [false, false, null, null, null],
1399              'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1400              'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1401              'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1402              'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1403          ];
1404      }
1405  
1406      /**
1407       * Test for \completion_info::get_grade_completion().
1408       *
1409       * @dataProvider get_grade_completion_provider
1410       * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1411       * @param bool $hasgrade Whether to set grade for the user in this activity.
1412       * @param int|null $passinggrade Passing grade to set for the test activity.
1413       * @param string|null $expectedexception Expected exception.
1414       * @param int|null $expectedresult The expected completion status.
1415       * @covers ::get_grade_completion
1416       */
1417      public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
1418          ?string $expectedexception, ?int $expectedresult) {
1419          $this->setup_data();
1420  
1421          /** @var \mod_assign_generator $assigngenerator */
1422          $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1423          $assign = $assigngenerator->create_instance([
1424              'course' => $this->course->id,
1425              'completion' => COMPLETION_ENABLED,
1426              'completionusegrade' => $completionusegrade,
1427              'gradepass' => $passinggrade,
1428          ]);
1429  
1430          $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1431          if ($completionusegrade && $hasgrade) {
1432              $assigninstance = new assign($cm->context, $cm, $this->course);
1433              $grade = $assigninstance->get_user_grade($this->user->id, true);
1434              $grade->grade = 75;
1435              $assigninstance->update_grade($grade);
1436          }
1437  
1438          $completioninfo = new completion_info($this->course);
1439          if ($expectedexception) {
1440              $this->expectException($expectedexception);
1441          }
1442          $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1443          $this->assertEquals($expectedresult, $gradecompletion);
1444      }
1445  
1446      /**
1447       * Test the return value for cases when the activity module does not have associated grade_item.
1448       *
1449       * @covers ::get_grade_completion
1450       */
1451      public function test_get_grade_completion_without_grade_item() {
1452          global $DB;
1453  
1454          $this->setup_data();
1455  
1456          $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1457              'course' => $this->course->id,
1458              'completion' => COMPLETION_ENABLED,
1459              'completionusegrade' => true,
1460              'gradepass' => 42,
1461          ]);
1462  
1463          $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1464  
1465          $DB->delete_records('grade_items', [
1466              'courseid' => $this->course->id,
1467              'itemtype' => 'mod',
1468              'itemmodule' => 'assign',
1469              'iteminstance' => $assign->id,
1470          ]);
1471  
1472          // Without the grade_item, the activity is considered incomplete.
1473          $completioninfo = new completion_info($this->course);
1474          $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1475  
1476          // Once the activity is graded, the grade_item is automatically created.
1477          $assigninstance = new assign($cm->context, $cm, $this->course);
1478          $grade = $assigninstance->get_user_grade($this->user->id, true);
1479          $grade->grade = 40;
1480          $assigninstance->update_grade($grade);
1481  
1482          // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1483          $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1484      }
1485  }
1486  
1487  class core_completionlib_fake_recordset implements Iterator {
1488      protected $closed;
1489      protected $values, $index;
1490  
1491      public function __construct($values) {
1492          $this->values = $values;
1493          $this->index = 0;
1494      }
1495  
1496      public function current() {
1497          return $this->values[$this->index];
1498      }
1499  
1500      public function key() {
1501          return $this->values[$this->index];
1502      }
1503  
1504      public function next() {
1505          $this->index++;
1506      }
1507  
1508      public function rewind() {
1509          $this->index = 0;
1510      }
1511  
1512      public function valid() {
1513          return count($this->values) > $this->index;
1514      }
1515  
1516      public function close() {
1517          $this->closed = true;
1518      }
1519  
1520      public function was_closed() {
1521          return $this->closed;
1522      }
1523  }