Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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  /**
  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  class core_completionlib_testcase 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_object($expected) and is_object($actual)) {
  83              if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
  84                  if ($expected->timemodified + 1 == $actual->timemodified) {
  85                      $expected = clone($expected);
  86                      $expected->timemodified = $actual->timemodified;
  87                  }
  88              }
  89          }
  90          parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
  91      }
  92  
  93      public function test_is_enabled() {
  94          global $CFG;
  95          $this->mock_setup();
  96  
  97          // Config alone.
  98          $CFG->enablecompletion = COMPLETION_DISABLED;
  99          $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
 100          $CFG->enablecompletion = COMPLETION_ENABLED;
 101          $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
 102  
 103          // Course.
 104          $course = (object)array('id' =>13);
 105          $c = new completion_info($course);
 106          $course->enablecompletion = COMPLETION_DISABLED;
 107          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 108          $course->enablecompletion = COMPLETION_ENABLED;
 109          $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
 110          $CFG->enablecompletion = COMPLETION_DISABLED;
 111          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
 112  
 113          // Course and CM.
 114          $cm = new stdClass();
 115          $cm->completion = COMPLETION_TRACKING_MANUAL;
 116          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 117          $CFG->enablecompletion = COMPLETION_ENABLED;
 118          $course->enablecompletion = COMPLETION_DISABLED;
 119          $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
 120          $course->enablecompletion = COMPLETION_ENABLED;
 121          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
 122          $cm->completion = COMPLETION_TRACKING_NONE;
 123          $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
 124          $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
 125          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
 126      }
 127  
 128      public function test_update_state() {
 129          $this->mock_setup();
 130  
 131          $mockbuilder = $this->getMockBuilder('completion_info');
 132          $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
 133                                         'user_can_override_completion'));
 134          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 135          $c = $mockbuilder->getMock();
 136          $cm = (object)array('id'=>13, 'course'=>42);
 137  
 138          // Not enabled, should do nothing.
 139          $c->expects($this->at(0))
 140              ->method('is_enabled')
 141              ->with($cm)
 142              ->will($this->returnValue(false));
 143          $c->update_state($cm);
 144  
 145          // Enabled, but current state is same as possible result, do nothing.
 146          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 147          $c->expects($this->at(0))
 148              ->method('is_enabled')
 149              ->with($cm)
 150              ->will($this->returnValue(true));
 151          $c->expects($this->at(1))
 152              ->method('get_data')
 153              ->with($cm, false, 0)
 154              ->will($this->returnValue($current));
 155          $c->update_state($cm, COMPLETION_COMPLETE);
 156  
 157          // Enabled, but current state is a specific one and new state is just
 158          // complete, so do nothing.
 159          $current->completionstate = COMPLETION_COMPLETE_PASS;
 160          $c->expects($this->at(0))
 161              ->method('is_enabled')
 162              ->with($cm)
 163              ->will($this->returnValue(true));
 164          $c->expects($this->at(1))
 165              ->method('get_data')
 166              ->with($cm, false, 0)
 167              ->will($this->returnValue($current));
 168          $c->update_state($cm, COMPLETION_COMPLETE);
 169  
 170          // Manual, change state (no change).
 171          $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
 172          $current->completionstate=COMPLETION_COMPLETE;
 173          $c->expects($this->at(0))
 174              ->method('is_enabled')
 175              ->with($cm)
 176              ->will($this->returnValue(true));
 177          $c->expects($this->at(1))
 178              ->method('get_data')
 179              ->with($cm, false, 0)
 180              ->will($this->returnValue($current));
 181          $c->update_state($cm, COMPLETION_COMPLETE);
 182  
 183          // Manual, change state (change).
 184          $c->expects($this->at(0))
 185              ->method('is_enabled')
 186              ->with($cm)
 187              ->will($this->returnValue(true));
 188          $c->expects($this->at(1))
 189              ->method('get_data')
 190              ->with($cm, false, 0)
 191              ->will($this->returnValue($current));
 192          $changed = clone($current);
 193          $changed->timemodified = time();
 194          $changed->completionstate = COMPLETION_INCOMPLETE;
 195          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 196          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 197          $c->expects($this->at(2))
 198              ->method('internal_set_data')
 199              ->with($cm, $comparewith);
 200          $c->update_state($cm, COMPLETION_INCOMPLETE);
 201  
 202          // Auto, change state.
 203          $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
 204          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
 205          $c->expects($this->at(0))
 206              ->method('is_enabled')
 207              ->with($cm)
 208              ->will($this->returnValue(true));
 209          $c->expects($this->at(1))
 210              ->method('get_data')
 211              ->with($cm, false, 0)
 212              ->will($this->returnValue($current));
 213          $c->expects($this->at(2))
 214              ->method('internal_get_state')
 215              ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
 216          $changed = clone($current);
 217          $changed->timemodified = time();
 218          $changed->completionstate = COMPLETION_COMPLETE_PASS;
 219          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 220          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 221          $c->expects($this->at(3))
 222              ->method('internal_set_data')
 223              ->with($cm, $comparewith);
 224          $c->update_state($cm, COMPLETION_COMPLETE_PASS);
 225  
 226          // Manual tracking, change state by overriding it manually.
 227          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
 228          $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 229          $c->expects($this->at(0))
 230              ->method('is_enabled')
 231              ->with($cm)
 232              ->will($this->returnValue(true));
 233          $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
 234              ->method('user_can_override_completion')
 235              ->will($this->returnValue(true));
 236          $c->expects($this->at(2))
 237              ->method('get_data')
 238              ->with($cm, false, 100)
 239              ->will($this->returnValue($current));
 240          $changed = clone($current);
 241          $changed->timemodified = time();
 242          $changed->completionstate = COMPLETION_COMPLETE;
 243          $changed->overrideby = 314159;
 244          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 245          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 246          $c->expects($this->at(3))
 247              ->method('internal_set_data')
 248              ->with($cm, $comparewith);
 249          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 250          // And confirm that the status can be changed back to incomplete without an override.
 251          $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
 252          $c->expects($this->at(0))
 253              ->method('get_data')
 254              ->with($cm, false, 100)
 255              ->will($this->returnValue($current));
 256          $c->get_data($cm, false, 100);
 257  
 258          // Auto, change state via override, incomplete to complete.
 259          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 260          $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
 261          $c->expects($this->at(0))
 262              ->method('is_enabled')
 263              ->with($cm)
 264              ->will($this->returnValue(true));
 265          $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
 266              ->method('user_can_override_completion')
 267              ->will($this->returnValue(true));
 268          $c->expects($this->at(2))
 269              ->method('get_data')
 270              ->with($cm, false, 100)
 271              ->will($this->returnValue($current));
 272          $changed = clone($current);
 273          $changed->timemodified = time();
 274          $changed->completionstate = COMPLETION_COMPLETE;
 275          $changed->overrideby = 314159;
 276          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 277          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 278          $c->expects($this->at(3))
 279              ->method('internal_set_data')
 280              ->with($cm, $comparewith);
 281          $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
 282          $c->expects($this->at(0))
 283              ->method('get_data')
 284              ->with($cm, false, 100)
 285              ->will($this->returnValue($changed));
 286          $c->get_data($cm, false, 100);
 287  
 288          // Now confirm that the status cannot be changed back to incomplete without an override.
 289          // I.e. test that automatic completion won't trigger a change back to COMPLETION_INCOMPLETE when overridden.
 290          $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
 291          $c->expects($this->at(0))
 292              ->method('get_data')
 293              ->with($cm, false, 100)
 294              ->will($this->returnValue($changed));
 295          $c->get_data($cm, false, 100);
 296  
 297          // Now confirm the status can be changed back from complete to incomplete using an override.
 298          $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
 299          $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
 300          $c->expects($this->at(0))
 301              ->method('is_enabled')
 302              ->with($cm)
 303              ->will($this->returnValue(true));
 304          $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
 305          ->method('user_can_override_completion')
 306              ->will($this->returnValue(true));
 307          $c->expects($this->at(2))
 308              ->method('get_data')
 309              ->with($cm, false, 100)
 310              ->will($this->returnValue($current));
 311          $changed = clone($current);
 312          $changed->timemodified = time();
 313          $changed->completionstate = COMPLETION_INCOMPLETE;
 314          $changed->overrideby = 314159;
 315          $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
 316          $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
 317          $c->expects($this->at(3))
 318              ->method('internal_set_data')
 319              ->with($cm, $comparewith);
 320          $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
 321          $c->expects($this->at(0))
 322              ->method('get_data')
 323              ->with($cm, false, 100)
 324              ->will($this->returnValue($changed));
 325          $c->get_data($cm, false, 100);
 326      }
 327  
 328      public function test_internal_get_state() {
 329          global $DB;
 330          $this->mock_setup();
 331  
 332          $mockbuilder = $this->getMockBuilder('completion_info');
 333          $mockbuilder->setMethods(array('internal_get_grade_state'));
 334          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 335          $c = $mockbuilder->getMock();
 336  
 337          $cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null);
 338  
 339          // If view is required, but they haven't viewed it yet.
 340          $cm->completionview = COMPLETION_VIEW_REQUIRED;
 341          $current = (object)array('viewed'=>COMPLETION_NOT_VIEWED);
 342          $this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current));
 343  
 344          // OK set view not required.
 345          $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
 346  
 347          // Test not getting module name.
 348          $cm->modname='label';
 349          $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
 350  
 351          // Test getting module name.
 352          $cm->module = 13;
 353          unset($cm->modname);
 354          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 355          $DB->expects($this->once())
 356              ->method('get_field')
 357              ->with('modules', 'name', array('id'=>13))
 358              ->will($this->returnValue('lable'));
 359          $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
 360  
 361          // Note: This function is not fully tested (including kind of the main part) because:
 362          // * the grade_item/grade_grade calls are static and can't be mocked,
 363          // * the plugin_supports call is static and can't be mocked.
 364      }
 365  
 366      public function test_set_module_viewed() {
 367          $this->mock_setup();
 368  
 369          $mockbuilder = $this->getMockBuilder('completion_info');
 370          $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
 371          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 372          $c = $mockbuilder->getMock();
 373          $cm = (object)array('id'=>13, 'course'=>42);
 374  
 375          // Not tracking completion, should do nothing.
 376          $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
 377          $c->set_module_viewed($cm);
 378  
 379          // Tracking completion but completion is disabled, should do nothing.
 380          $cm->completionview = COMPLETION_VIEW_REQUIRED;
 381          $c->expects($this->at(0))
 382              ->method('is_enabled')
 383              ->with($cm)
 384              ->will($this->returnValue(false));
 385          $c->set_module_viewed($cm);
 386  
 387          // Now it's enabled, we expect it to get data. If data already has
 388          // viewed, still do nothing.
 389          $c->expects($this->at(0))
 390              ->method('is_enabled')
 391              ->with($cm)
 392              ->will($this->returnValue(true));
 393          $c->expects($this->at(1))
 394              ->method('get_data')
 395              ->with($cm, 0)
 396              ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
 397          $c->set_module_viewed($cm);
 398  
 399          // OK finally one that hasn't been viewed, now it should set it viewed
 400          // and update state.
 401          $c->expects($this->at(0))
 402              ->method('is_enabled')
 403              ->with($cm)
 404              ->will($this->returnValue(true));
 405          $c->expects($this->at(1))
 406              ->method('get_data')
 407              ->with($cm, false, 1337)
 408              ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
 409          $c->expects($this->at(2))
 410              ->method('internal_set_data')
 411              ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
 412          $c->expects($this->at(3))
 413              ->method('update_state')
 414              ->with($cm, COMPLETION_COMPLETE, 1337);
 415          $c->set_module_viewed($cm, 1337);
 416      }
 417  
 418      public function test_count_user_data() {
 419          global $DB;
 420          $this->mock_setup();
 421  
 422          $course = (object)array('id'=>13);
 423          $cm = (object)array('id'=>42);
 424  
 425          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 426          $DB->expects($this->at(0))
 427              ->method('get_field_sql')
 428              ->will($this->returnValue(666));
 429  
 430          $c = new completion_info($course);
 431          $this->assertEquals(666, $c->count_user_data($cm));
 432      }
 433  
 434      public function test_delete_all_state() {
 435          global $DB;
 436          $this->mock_setup();
 437  
 438          $course = (object)array('id'=>13);
 439          $cm = (object)array('id'=>42, 'course'=>13);
 440          $c = new completion_info($course);
 441  
 442          // Check it works ok without data in session.
 443          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 444          $DB->expects($this->at(0))
 445              ->method('delete_records')
 446              ->with('course_modules_completion', array('coursemoduleid'=>42))
 447              ->will($this->returnValue(true));
 448          $c->delete_all_state($cm);
 449      }
 450  
 451      public function test_reset_all_state() {
 452          global $DB;
 453          $this->mock_setup();
 454  
 455          $mockbuilder = $this->getMockBuilder('completion_info');
 456          $mockbuilder->setMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
 457          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 458          $c = $mockbuilder->getMock();
 459  
 460          $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
 461  
 462          /** @var $DB PHPUnit_Framework_MockObject_MockObject */
 463          $DB->expects($this->at(0))
 464              ->method('get_recordset')
 465              ->will($this->returnValue(
 466                  new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
 467  
 468          $c->expects($this->at(0))
 469              ->method('delete_all_state')
 470              ->with($cm);
 471  
 472          $c->expects($this->at(1))
 473              ->method('get_tracked_users')
 474              ->will($this->returnValue(array(
 475              (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
 476              (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
 477  
 478          $c->expects($this->at(2))
 479              ->method('update_state')
 480              ->with($cm, COMPLETION_UNKNOWN, 100);
 481          $c->expects($this->at(3))
 482              ->method('update_state')
 483              ->with($cm, COMPLETION_UNKNOWN, 101);
 484          $c->expects($this->at(4))
 485              ->method('update_state')
 486              ->with($cm, COMPLETION_UNKNOWN, 201);
 487  
 488          $c->reset_all_state($cm);
 489      }
 490  
 491      /**
 492       * Data provider for test_get_data().
 493       *
 494       * @return array[]
 495       */
 496      public function get_data_provider() {
 497          return [
 498              'No completion record' => [
 499                  false, true, false, COMPLETION_INCOMPLETE
 500              ],
 501              'Not completed' => [
 502                  false, true, true, COMPLETION_INCOMPLETE
 503              ],
 504              'Completed' => [
 505                  false, true, true, COMPLETION_COMPLETE
 506              ],
 507              'Whole course, complete' => [
 508                  true, true, true, COMPLETION_COMPLETE
 509              ],
 510              'Get data for another user, result should be not cached' => [
 511                  false, false, true,  COMPLETION_INCOMPLETE
 512              ],
 513              'Get data for another user, including whole course, result should be not cached' => [
 514                  true, false, true,  COMPLETION_INCOMPLETE
 515              ],
 516          ];
 517      }
 518  
 519      /**
 520       * Tests for completion_info::get_data().
 521       *
 522       * @dataProvider get_data_provider
 523       * @param bool $wholecourse Whole course parameter for get_data().
 524       * @param bool $sameuser Whether the user calling get_data() is the user itself.
 525       * @param bool $hasrecord Whether to create a course_modules_completion record.
 526       * @param int $completion The completion state expected.
 527       */
 528      public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
 529          global $DB;
 530  
 531          $this->setup_data();
 532          $user = $this->user;
 533  
 534          /** @var \mod_choice_generator $choicegenerator */
 535          $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
 536          $choice = $choicegenerator->create_instance([
 537              'course' => $this->course->id,
 538              'completion' => true,
 539              'completionview' => true,
 540          ]);
 541  
 542          $cm = get_coursemodule_from_instance('choice', $choice->id);
 543  
 544          // Let's manually create a course completion record instead of going thru the hoops to complete an activity.
 545          if ($hasrecord) {
 546              $cmcompletionrecord = (object)[
 547                  'coursemoduleid' => $cm->id,
 548                  'userid' => $user->id,
 549                  'completionstate' => $completion,
 550                  'viewed' => 0,
 551                  'overrideby' => null,
 552                  'timemodified' => 0,
 553              ];
 554              $DB->insert_record('course_modules_completion', $cmcompletionrecord);
 555          }
 556  
 557          // Whether we expect for the returned completion data to be stored in the cache.
 558          $iscached = true;
 559  
 560          if (!$sameuser) {
 561              $iscached = false;
 562              $this->setAdminUser();
 563          } else {
 564              $this->setUser($user);
 565          }
 566  
 567          // Mock other completion data.
 568          $completioninfo = new completion_info($this->course);
 569  
 570          $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
 571          // Course module ID of the returned completion data must match this activity's course module ID.
 572          $this->assertEquals($cm->id, $result->coursemoduleid);
 573          // User ID of the returned completion data must match the user's ID.
 574          $this->assertEquals($user->id, $result->userid);
 575          // The completion state of the returned completion data must match the expected completion state.
 576          $this->assertEquals($completion, $result->completionstate);
 577  
 578          // If the user has no completion record, then the default record should be returned.
 579          if (!$hasrecord) {
 580              $this->assertEquals(0, $result->id);
 581          }
 582  
 583          // Check caching.
 584          $key = "{$user->id}_{$this->course->id}";
 585          $cache = cache::make('core', 'completion');
 586          if ($iscached) {
 587              // If we expect this to be cached, then fetching the result must match the cached data.
 588              $this->assertEquals($result, (object)$cache->get($key)[$cm->id]);
 589  
 590              // Check cached data for other course modules in the course.
 591              // The sample module created in setup_data() should suffice to confirm this.
 592              $othercm = get_coursemodule_from_instance('forum', $this->module1->id);
 593              if ($wholecourse) {
 594                  $this->assertArrayHasKey($othercm->id, $cache->get($key));
 595              } else {
 596                  $this->assertArrayNotHasKey($othercm->id, $cache->get($key));
 597              }
 598          } else {
 599              // Otherwise, this should not be cached.
 600              $this->assertFalse($cache->get($key));
 601          }
 602      }
 603  
 604      public function test_internal_set_data() {
 605          global $DB;
 606          $this->setup_data();
 607  
 608          $this->setUser($this->user);
 609          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
 610          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 611          $cm = get_coursemodule_from_instance('forum', $forum->id);
 612          $c = new completion_info($this->course);
 613  
 614          // 1) Test with new data.
 615          $data = new stdClass();
 616          $data->id = 0;
 617          $data->userid = $this->user->id;
 618          $data->coursemoduleid = $cm->id;
 619          $data->completionstate = COMPLETION_COMPLETE;
 620          $data->timemodified = time();
 621          $data->viewed = COMPLETION_NOT_VIEWED;
 622          $data->overrideby = null;
 623  
 624          $c->internal_set_data($cm, $data);
 625          $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
 626          $this->assertEquals($d1, $data->id);
 627          $cache = cache::make('core', 'completion');
 628          // Cache was not set for another user.
 629          $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data),
 630              $cache->get($data->userid . '_' . $cm->course));
 631  
 632          // 2) Test with existing data and for different user.
 633          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 634          $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
 635          $newuser = $this->getDataGenerator()->create_user();
 636  
 637          $d2 = new stdClass();
 638          $d2->id = 7;
 639          $d2->userid = $newuser->id;
 640          $d2->coursemoduleid = $cm2->id;
 641          $d2->completionstate = COMPLETION_COMPLETE;
 642          $d2->timemodified = time();
 643          $d2->viewed = COMPLETION_NOT_VIEWED;
 644          $d2->overrideby = null;
 645          $c->internal_set_data($cm2, $d2);
 646          // Cache for current user returns the data.
 647          $cachevalue = $cache->get($data->userid . '_' . $cm->course);
 648          $this->assertEquals($data, $cachevalue[$cm->id]);
 649          // Cache for another user is not filled.
 650          $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
 651  
 652          // 3) Test where it THINKS the data is new (from cache) but actually
 653          //    in the database it has been set since.
 654          // 1) Test with new data.
 655          $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 656          $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
 657          $newuser2 = $this->getDataGenerator()->create_user();
 658          $d3 = new stdClass();
 659          $d3->id = 13;
 660          $d3->userid = $newuser2->id;
 661          $d3->coursemoduleid = $cm3->id;
 662          $d3->completionstate = COMPLETION_COMPLETE;
 663          $d3->timemodified = time();
 664          $d3->viewed = COMPLETION_NOT_VIEWED;
 665          $d3->overrideby = null;
 666          $DB->insert_record('course_modules_completion', $d3);
 667          $c->internal_set_data($cm, $data);
 668      }
 669  
 670      public function test_get_progress_all() {
 671          global $DB;
 672          $this->mock_setup();
 673  
 674          $mockbuilder = $this->getMockBuilder('completion_info');
 675          $mockbuilder->setMethods(array('get_tracked_users'));
 676          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 677          $c = $mockbuilder->getMock();
 678  
 679          // 1) Basic usage.
 680          $c->expects($this->at(0))
 681              ->method('get_tracked_users')
 682              ->with(false,  array(),  0,  '',  '',  '',  null)
 683              ->will($this->returnValue(array(
 684                  (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
 685                  (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
 686          $DB->expects($this->at(0))
 687              ->method('get_in_or_equal')
 688              ->with(array(100, 201))
 689              ->will($this->returnValue(array(' IN (100, 201)', array())));
 690          $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
 691          $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
 692          $DB->expects($this->at(1))
 693              ->method('get_recordset_sql')
 694              ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
 695  
 696          $this->assertEquals(array(
 697                  100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
 698                      'progress'=>array(13=>$progress1)),
 699                  201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
 700                      'progress'=>array(14=>$progress2)),
 701              ), $c->get_progress_all(false));
 702  
 703          // 2) With more than 1, 000 results.
 704          $tracked = array();
 705          $ids = array();
 706          $progress = array();
 707          for ($i = 100; $i<2000; $i++) {
 708              $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
 709              $ids[] = $i;
 710              $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
 711              $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
 712          }
 713          $c->expects($this->at(0))
 714              ->method('get_tracked_users')
 715              ->with(true,  3,  0,  '',  '',  '',  null)
 716              ->will($this->returnValue($tracked));
 717          $DB->expects($this->at(0))
 718              ->method('get_in_or_equal')
 719              ->with(array_slice($ids, 0, 1000))
 720              ->will($this->returnValue(array(' IN whatever', array())));
 721          $DB->expects($this->at(1))
 722              ->method('get_recordset_sql')
 723              ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
 724  
 725          $DB->expects($this->at(2))
 726              ->method('get_in_or_equal')
 727              ->with(array_slice($ids, 1000))
 728              ->will($this->returnValue(array(' IN whatever2', array())));
 729          $DB->expects($this->at(3))
 730              ->method('get_recordset_sql')
 731              ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
 732  
 733          $result = $c->get_progress_all(true, 3);
 734          $resultok = true;
 735          $resultok  =  $resultok && ($ids == array_keys($result));
 736  
 737          foreach ($result as $userid => $data) {
 738              $resultok  =  $resultok && $data->firstname == 'frog';
 739              $resultok  =  $resultok && $data->lastname == $userid;
 740              $resultok  =  $resultok && $data->id == $userid;
 741              $cms = $data->progress;
 742              $resultok =  $resultok && (array(13, 14) == array_keys($cms));
 743              $resultok =  $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
 744              $resultok =  $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
 745          }
 746          $this->assertTrue($resultok);
 747      }
 748  
 749      public function test_inform_grade_changed() {
 750          $this->mock_setup();
 751  
 752          $mockbuilder = $this->getMockBuilder('completion_info');
 753          $mockbuilder->setMethods(array('is_enabled', 'update_state'));
 754          $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
 755          $c = $mockbuilder->getMock();
 756  
 757          $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
 758          $item = (object)array('itemnumber'=>3,  'gradepass'=>1,  'hidden'=>0);
 759          $grade = (object)array('userid'=>31337,  'finalgrade'=>0,  'rawgrade'=>0);
 760  
 761          // Not enabled (should do nothing).
 762          $c->expects($this->at(0))
 763              ->method('is_enabled')
 764              ->with($cm)
 765              ->will($this->returnValue(false));
 766          $c->inform_grade_changed($cm, $item, $grade, false);
 767  
 768          // Enabled but still no grade completion required,  should still do nothing.
 769          $c->expects($this->at(0))
 770              ->method('is_enabled')
 771              ->with($cm)
 772              ->will($this->returnValue(true));
 773          $c->inform_grade_changed($cm, $item, $grade, false);
 774  
 775          // Enabled and completion required but item number is wrong,  does nothing.
 776          $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
 777          $c->expects($this->at(0))
 778              ->method('is_enabled')
 779              ->with($cm)
 780              ->will($this->returnValue(true));
 781          $c->inform_grade_changed($cm, $item, $grade, false);
 782  
 783          // Enabled and completion required and item number right. It is supposed
 784          // to call update_state with the new potential state being obtained from
 785          // internal_get_grade_state.
 786          $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
 787          $grade = (object)array('userid'=>31337,  'finalgrade'=>1,  'rawgrade'=>0);
 788          $c->expects($this->at(0))
 789              ->method('is_enabled')
 790              ->with($cm)
 791              ->will($this->returnValue(true));
 792          $c->expects($this->at(1))
 793              ->method('update_state')
 794              ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
 795              ->will($this->returnValue(true));
 796          $c->inform_grade_changed($cm, $item, $grade, false);
 797  
 798          // Same as above but marked deleted. It is supposed to call update_state
 799          // with new potential state being COMPLETION_INCOMPLETE.
 800          $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
 801          $grade = (object)array('userid'=>31337,  'finalgrade'=>1,  'rawgrade'=>0);
 802          $c->expects($this->at(0))
 803              ->method('is_enabled')
 804              ->with($cm)
 805              ->will($this->returnValue(true));
 806          $c->expects($this->at(1))
 807              ->method('update_state')
 808              ->with($cm, COMPLETION_INCOMPLETE, 31337)
 809              ->will($this->returnValue(true));
 810          $c->inform_grade_changed($cm, $item, $grade, true);
 811      }
 812  
 813      public function test_internal_get_grade_state() {
 814          $this->mock_setup();
 815  
 816          $item = new stdClass;
 817          $grade = new stdClass;
 818  
 819          $item->gradepass = 4;
 820          $item->hidden = 0;
 821          $grade->rawgrade = 4.0;
 822          $grade->finalgrade = null;
 823  
 824          // Grade has pass mark and is not hidden,  user passes.
 825          $this->assertEquals(
 826              COMPLETION_COMPLETE_PASS,
 827              completion_info::internal_get_grade_state($item, $grade));
 828  
 829          // Same but user fails.
 830          $grade->rawgrade = 3.9;
 831          $this->assertEquals(
 832              COMPLETION_COMPLETE_FAIL,
 833              completion_info::internal_get_grade_state($item, $grade));
 834  
 835          // User fails on raw grade but passes on final.
 836          $grade->finalgrade = 4.0;
 837          $this->assertEquals(
 838              COMPLETION_COMPLETE_PASS,
 839              completion_info::internal_get_grade_state($item, $grade));
 840  
 841          // Item is hidden.
 842          $item->hidden = 1;
 843          $this->assertEquals(
 844              COMPLETION_COMPLETE,
 845              completion_info::internal_get_grade_state($item, $grade));
 846  
 847          // Item isn't hidden but has no pass mark.
 848          $item->hidden = 0;
 849          $item->gradepass = 0;
 850          $this->assertEquals(
 851              COMPLETION_COMPLETE,
 852              completion_info::internal_get_grade_state($item, $grade));
 853      }
 854  
 855      public function test_get_activities() {
 856          global $CFG;
 857          $this->resetAfterTest();
 858  
 859          // Enable completion before creating modules, otherwise the completion data is not written in DB.
 860          $CFG->enablecompletion = true;
 861  
 862          // Create a course with mixed auto completion data.
 863          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
 864          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
 865          $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
 866          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
 867          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
 868          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
 869          $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
 870  
 871          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
 872          $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
 873          $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
 874  
 875          // Create data in another course to make sure it's not considered.
 876          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
 877          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
 878          $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
 879          $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
 880  
 881          $c = new completion_info($course);
 882          $activities = $c->get_activities();
 883          $this->assertCount(3, $activities);
 884          $this->assertTrue(isset($activities[$forum->cmid]));
 885          $this->assertSame($forum->name, $activities[$forum->cmid]->name);
 886          $this->assertTrue(isset($activities[$page->cmid]));
 887          $this->assertSame($page->name, $activities[$page->cmid]->name);
 888          $this->assertTrue(isset($activities[$data->cmid]));
 889          $this->assertSame($data->name, $activities[$data->cmid]->name);
 890  
 891          $this->assertFalse(isset($activities[$forum2->cmid]));
 892          $this->assertFalse(isset($activities[$page2->cmid]));
 893          $this->assertFalse(isset($activities[$data2->cmid]));
 894      }
 895  
 896      public function test_has_activities() {
 897          global $CFG;
 898          $this->resetAfterTest();
 899  
 900          // Enable completion before creating modules, otherwise the completion data is not written in DB.
 901          $CFG->enablecompletion = true;
 902  
 903          // Create a course with mixed auto completion data.
 904          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
 905          $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
 906          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
 907          $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
 908          $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
 909          $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
 910  
 911          $c1 = new completion_info($course);
 912          $c2 = new completion_info($course2);
 913  
 914          $this->assertTrue($c1->has_activities());
 915          $this->assertFalse($c2->has_activities());
 916      }
 917  
 918      /**
 919       * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
 920       *
 921       * @return void
 922       */
 923      public function test_course_delete_prerequisite() {
 924          global $DB;
 925  
 926          $this->setup_data();
 927  
 928          $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
 929  
 930          $criteriadata = (object) [
 931              'id' => $this->course->id,
 932              'criteria_course' => [$courseprerequisite->id],
 933          ];
 934  
 935          /** @var completion_criteria_course $criteria */
 936          $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
 937          $criteria->update_config($criteriadata);
 938  
 939          // Sanity test.
 940          $this->assertTrue($DB->record_exists('course_completion_criteria', [
 941              'course' => $this->course->id,
 942              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
 943              'courseinstance' => $courseprerequisite->id,
 944          ]));
 945  
 946          // Deleting the prerequisite course should remove the completion criteria.
 947          delete_course($courseprerequisite, false);
 948  
 949          $this->assertFalse($DB->record_exists('course_completion_criteria', [
 950              'course' => $this->course->id,
 951              'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
 952              'courseinstance' => $courseprerequisite->id,
 953          ]));
 954      }
 955  
 956      /**
 957       * Test course module completion update event.
 958       */
 959      public function test_course_module_completion_updated_event() {
 960          global $USER, $CFG;
 961  
 962          $this->setup_data();
 963  
 964          $this->setAdminUser();
 965  
 966          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
 967          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 968  
 969          $c = new completion_info($this->course);
 970          $activities = $c->get_activities();
 971          $this->assertEquals(1, count($activities));
 972          $this->assertTrue(isset($activities[$forum->cmid]));
 973          $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
 974  
 975          $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
 976          $current->completionstate = COMPLETION_COMPLETE;
 977          $current->timemodified = time();
 978          $sink = $this->redirectEvents();
 979          $c->internal_set_data($activities[$forum->cmid], $current);
 980          $events = $sink->get_events();
 981          $event = reset($events);
 982          $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
 983          $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
 984          $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
 985          $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
 986          $this->assertEquals($USER->id, $event->userid);
 987          $this->assertEquals($this->user->id, $event->relateduserid);
 988          $this->assertInstanceOf('moodle_url', $event->get_url());
 989          $this->assertEventLegacyData($current, $event);
 990      }
 991  
 992      /**
 993       * Test course completed event.
 994       */
 995      public function test_course_completed_event() {
 996          global $USER;
 997  
 998          $this->setup_data();
 999          $this->setAdminUser();
1000  
1001          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1002          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1003  
1004          // Mark course as complete and get triggered event.
1005          $sink = $this->redirectEvents();
1006          $ccompletion->mark_complete();
1007          $events = $sink->get_events();
1008          $event = reset($events);
1009  
1010          $this->assertInstanceOf('\core\event\course_completed', $event);
1011          $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1012          $this->assertEquals($this->course->id, $event->courseid);
1013          $this->assertEquals($USER->id, $event->userid);
1014          $this->assertEquals($this->user->id, $event->relateduserid);
1015          $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1016          $this->assertInstanceOf('moodle_url', $event->get_url());
1017          $data = $ccompletion->get_record_data();
1018          $this->assertEventLegacyData($data, $event);
1019      }
1020  
1021      /**
1022       * Test course completed event.
1023       */
1024      public function test_course_completion_updated_event() {
1025          $this->setup_data();
1026          $coursecontext = context_course::instance($this->course->id);
1027          $coursecompletionevent = \core\event\course_completion_updated::create(
1028                  array(
1029                      'courseid' => $this->course->id,
1030                      'context' => $coursecontext
1031                      )
1032                  );
1033  
1034          // Mark course as complete and get triggered event.
1035          $sink = $this->redirectEvents();
1036          $coursecompletionevent->trigger();
1037          $events = $sink->get_events();
1038          $event = array_pop($events);
1039          $sink->close();
1040  
1041          $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1042          $this->assertEquals($this->course->id, $event->courseid);
1043          $this->assertEquals($coursecontext, $event->get_context());
1044          $this->assertInstanceOf('moodle_url', $event->get_url());
1045          $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1046          $this->assertEventLegacyLogData($expectedlegacylog, $event);
1047      }
1048  
1049      public function test_completion_can_view_data() {
1050          $this->setup_data();
1051  
1052          $student = $this->getDataGenerator()->create_user();
1053          $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1054  
1055          $this->setUser($student);
1056          $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1057          $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1058      }
1059  }
1060  
1061  class core_completionlib_fake_recordset implements Iterator {
1062      protected $closed;
1063      protected $values, $index;
1064  
1065      public function __construct($values) {
1066          $this->values = $values;
1067          $this->index = 0;
1068      }
1069  
1070      public function current() {
1071          return $this->values[$this->index];
1072      }
1073  
1074      public function key() {
1075          return $this->values[$this->index];
1076      }
1077  
1078      public function next() {
1079          $this->index++;
1080      }
1081  
1082      public function rewind() {
1083          $this->index = 0;
1084      }
1085  
1086      public function valid() {
1087          return count($this->values) > $this->index;
1088      }
1089  
1090      public function close() {
1091          $this->closed = true;
1092      }
1093  
1094      public function was_closed() {
1095          return $this->closed;
1096      }
1097  }