Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Unit tests for the model.
  19   *
  20   * @package   core_analytics
  21   * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once (__DIR__ . '/fixtures/test_indicator_max.php');
  28  require_once (__DIR__ . '/fixtures/test_indicator_min.php');
  29  require_once (__DIR__ . '/fixtures/test_indicator_fullname.php');
  30  require_once (__DIR__ . '/fixtures/test_target_shortname.php');
  31  require_once (__DIR__ . '/fixtures/test_static_target_shortname.php');
  32  require_once (__DIR__ . '/fixtures/test_target_course_level_shortname.php');
  33  require_once (__DIR__ . '/fixtures/test_analysis.php');
  34  
  35  /**
  36   * Unit tests for the model.
  37   *
  38   * @package   core_analytics
  39   * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
  40   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class analytics_model_testcase extends advanced_testcase {
  43  
  44      public function setUp(): void {
  45  
  46          $this->setAdminUser();
  47  
  48          $target = \core_analytics\manager::get_target('test_target_shortname');
  49          $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
  50          foreach ($indicators as $key => $indicator) {
  51              $indicators[$key] = \core_analytics\manager::get_indicator($indicator);
  52          }
  53  
  54          $this->model = testable_model::create($target, $indicators);
  55          $this->modelobj = $this->model->get_model_obj();
  56      }
  57  
  58      public function test_enable() {
  59          $this->resetAfterTest(true);
  60  
  61          $this->assertEquals(0, $this->model->get_model_obj()->enabled);
  62          $this->assertEquals(0, $this->model->get_model_obj()->trained);
  63          $this->assertEquals('', $this->model->get_model_obj()->timesplitting);
  64  
  65          $this->model->enable('\core\analytics\time_splitting\quarters');
  66          $this->assertEquals(1, $this->model->get_model_obj()->enabled);
  67          $this->assertEquals(0, $this->model->get_model_obj()->trained);
  68          $this->assertEquals('\core\analytics\time_splitting\quarters', $this->model->get_model_obj()->timesplitting);
  69      }
  70  
  71      public function test_create() {
  72          $this->resetAfterTest(true);
  73  
  74          $target = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout');
  75          $indicators = array(
  76              \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
  77              \core_analytics\manager::get_indicator('\core\analytics\indicator\read_actions')
  78          );
  79          $model = \core_analytics\model::create($target, $indicators);
  80          $this->assertInstanceOf('\core_analytics\model', $model);
  81      }
  82  
  83      /**
  84       * test_delete
  85       */
  86      public function test_delete() {
  87          global $DB;
  88  
  89          $this->resetAfterTest(true);
  90          set_config('enabled_stores', 'logstore_standard', 'tool_log');
  91  
  92          $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
  93          $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
  94          $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
  95          $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
  96  
  97          $this->model->enable('\core\analytics\time_splitting\single_range');
  98  
  99          $this->model->train();
 100          $this->model->predict();
 101  
 102          // Fake evaluation results record to check that it is actually deleted.
 103          $this->add_fake_log();
 104  
 105          $modeloutputdir = $this->model->get_output_dir(array(), true);
 106          $this->assertTrue(is_dir($modeloutputdir));
 107  
 108          // Generate a prediction action to confirm that it is deleted when there is an important update.
 109          $predictions = $DB->get_records('analytics_predictions');
 110          $prediction = reset($predictions);
 111          $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
 112          $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
 113  
 114          $this->model->delete();
 115          $this->assertEmpty($DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
 116          $this->assertEmpty($DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
 117          $this->assertEmpty($DB->count_records('analytics_predictions'));
 118          $this->assertEmpty($DB->count_records('analytics_prediction_actions'));
 119          $this->assertEmpty($DB->count_records('analytics_train_samples'));
 120          $this->assertEmpty($DB->count_records('analytics_predict_samples'));
 121          $this->assertEmpty($DB->count_records('analytics_used_files'));
 122          $this->assertFalse(is_dir($modeloutputdir));
 123  
 124          set_config('enabled_stores', '', 'tool_log');
 125          get_log_manager(true);
 126      }
 127  
 128      /**
 129       * test_clear
 130       */
 131      public function test_clear() {
 132          global $DB;
 133  
 134          $this->resetAfterTest(true);
 135          set_config('enabled_stores', 'logstore_standard', 'tool_log');
 136  
 137          $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
 138          $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
 139          $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
 140          $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
 141  
 142          $this->model->enable('\core\analytics\time_splitting\single_range');
 143  
 144          $this->model->train();
 145          $this->model->predict();
 146  
 147          // Fake evaluation results record to check that it is actually deleted.
 148          $this->add_fake_log();
 149  
 150          // Generate a prediction action to confirm that it is deleted when there is an important update.
 151          $predictions = $DB->get_records('analytics_predictions');
 152          $prediction = reset($predictions);
 153          $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
 154          $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
 155  
 156          $modelversionoutputdir = $this->model->get_output_dir();
 157          $this->assertTrue(is_dir($modelversionoutputdir));
 158  
 159          // Update to an empty time splitting method to force model::clear execution.
 160          $this->model->clear();
 161          $this->assertFalse(is_dir($modelversionoutputdir));
 162  
 163          // Check that most of the stuff got deleted.
 164          $this->assertEquals(1, $DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
 165          $this->assertEquals(1, $DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
 166          $this->assertEmpty($DB->count_records('analytics_predictions'));
 167          $this->assertEmpty($DB->count_records('analytics_prediction_actions'));
 168          $this->assertEmpty($DB->count_records('analytics_train_samples'));
 169          $this->assertEmpty($DB->count_records('analytics_predict_samples'));
 170          $this->assertEmpty($DB->count_records('analytics_used_files'));
 171  
 172          // Check that the model is marked as not trained after clearing (as it is not a static one).
 173          $this->assertEquals(0, $DB->get_field('analytics_models', 'trained', array('id' => $this->modelobj->id)));
 174  
 175          set_config('enabled_stores', '', 'tool_log');
 176          get_log_manager(true);
 177      }
 178  
 179      /**
 180       * Test behaviour of {\core_analytics\model::clear()} for static models.
 181       */
 182      public function test_clear_static() {
 183          global $DB;
 184          $this->resetAfterTest();
 185  
 186          $statictarget = new test_static_target_shortname();
 187          $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
 188          $model = \core_analytics\model::create($statictarget, $indicators, '\core\analytics\time_splitting\quarters');
 189          $modelobj = $model->get_model_obj();
 190  
 191          // Static models are always considered trained.
 192          $this->assertEquals(1, $DB->get_field('analytics_models', 'trained', array('id' => $modelobj->id)));
 193  
 194          $model->clear();
 195  
 196          // Check that the model is still marked as trained even after clearing.
 197          $this->assertEquals(1, $DB->get_field('analytics_models', 'trained', array('id' => $modelobj->id)));
 198      }
 199  
 200      public function test_model_manager() {
 201          $this->resetAfterTest(true);
 202  
 203          $this->assertCount(3, $this->model->get_indicators());
 204          $this->assertInstanceOf('\core_analytics\local\target\binary', $this->model->get_target());
 205  
 206          // Using evaluation as the model is not yet enabled.
 207          $this->model->init_analyser(array('evaluation' => true));
 208          $this->assertInstanceOf('\core_analytics\local\analyser\base', $this->model->get_analyser());
 209  
 210          $this->model->enable('\core\analytics\time_splitting\quarters');
 211          $this->assertInstanceOf('\core\analytics\analyser\site_courses', $this->model->get_analyser());
 212      }
 213  
 214      public function test_output_dir() {
 215          $this->resetAfterTest(true);
 216  
 217          $dir = make_request_directory();
 218          set_config('modeloutputdir', $dir, 'analytics');
 219  
 220          $modeldir = $dir . DIRECTORY_SEPARATOR . $this->modelobj->id . DIRECTORY_SEPARATOR . $this->modelobj->version;
 221          $this->assertEquals($modeldir, $this->model->get_output_dir());
 222          $this->assertEquals($modeldir . DIRECTORY_SEPARATOR . 'testing', $this->model->get_output_dir(array('testing')));
 223      }
 224  
 225      public function test_unique_id() {
 226          global $DB;
 227  
 228          $this->resetAfterTest(true);
 229  
 230          $originaluniqueid = $this->model->get_unique_id();
 231  
 232          // Same id across instances.
 233          $this->model = new testable_model($this->modelobj);
 234          $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 235  
 236          // We will restore it later.
 237          $originalversion = $this->modelobj->version;
 238  
 239          // Generates a different id if timemodified changes.
 240          $this->modelobj->version = $this->modelobj->version + 10;
 241          $DB->update_record('analytics_models', $this->modelobj);
 242          $this->model = new testable_model($this->modelobj);
 243          $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
 244  
 245          // Restore original timemodified to continue testing.
 246          $this->modelobj->version = $originalversion;
 247          $DB->update_record('analytics_models', $this->modelobj);
 248          // Same when updating through an action that changes the model.
 249          $this->model = new testable_model($this->modelobj);
 250  
 251          $this->model->mark_as_trained();
 252          $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 253  
 254          // Wait for the current timestamp to change.
 255          $this->waitForSecond();
 256          $this->model->enable('\core\analytics\time_splitting\deciles');
 257          $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
 258          $uniqueid = $this->model->get_unique_id();
 259  
 260          // Wait for the current timestamp to change.
 261          $this->waitForSecond();
 262          $this->model->enable('\core\analytics\time_splitting\quarters');
 263          $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
 264          $this->assertNotEquals($uniqueid, $this->model->get_unique_id());
 265      }
 266  
 267      /**
 268       * test_exists
 269       *
 270       * @return void
 271       */
 272      public function test_exists() {
 273          $this->resetAfterTest(true);
 274  
 275          $target = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching');
 276          $this->assertTrue(\core_analytics\model::exists($target));
 277  
 278          foreach (\core_analytics\manager::get_all_models() as $model) {
 279              $model->delete();
 280          }
 281  
 282          $this->assertFalse(\core_analytics\model::exists($target));
 283      }
 284  
 285      /**
 286       * test_model_timelimit
 287       *
 288       * @return null
 289       */
 290      public function test_model_timelimit() {
 291          global $DB;
 292  
 293          $this->resetAfterTest(true);
 294  
 295          set_config('modeltimelimit', 2, 'analytics');
 296  
 297          $courses = array();
 298          for ($i = 0; $i < 5; $i++) {
 299              $course = $this->getDataGenerator()->create_course();
 300              $analysable = new \core_analytics\course($course);
 301              $courses[$analysable->get_id()] = $course;
 302          }
 303  
 304          $target = new test_target_course_level_shortname();
 305          $analyser = new \core\analytics\analyser\courses(1, $target, [], [], []);
 306  
 307          $result = new \core_analytics\local\analysis\result_array(1, false, []);
 308          $analysis = new test_analysis($analyser, false, $result);
 309  
 310          // Each analysable element takes 0.5 secs minimum (test_analysis), so the max (and likely) number of analysable
 311          // elements that will be processed is 2.
 312          $analysis->run();
 313          $params = array('modelid' => 1, 'action' => 'prediction');
 314          $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
 315  
 316          $analysis->run();
 317          $this->assertLessThanOrEqual(4, $DB->count_records('analytics_used_analysables', $params));
 318  
 319          // Check that analysable elements have been processed following the analyser order
 320          // (course->sortorder here). We can not check this nicely after next get_unlabelled_data round
 321          // because the first analysed element will be analysed again.
 322          $analysedelems = $DB->get_records('analytics_used_analysables', $params, 'timeanalysed ASC');
 323          // Just a default for the first checked element.
 324          $last = (object)['sortorder' => PHP_INT_MAX];
 325          foreach ($analysedelems as $analysed) {
 326              if ($courses[$analysed->analysableid]->sortorder > $last->sortorder) {
 327                  $this->fail('Analysable elements have not been analysed sorted by course sortorder.');
 328              }
 329              $last = $courses[$analysed->analysableid];
 330          }
 331  
 332          // No time limit now to process the rest.
 333          set_config('modeltimelimit', 1000, 'analytics');
 334  
 335          $analysis->run();
 336          $this->assertEquals(5, $DB->count_records('analytics_used_analysables', $params));
 337  
 338          // New analysable elements are immediately pulled.
 339          $this->getDataGenerator()->create_course();
 340          $analysis->run();
 341          $this->assertEquals(6, $DB->count_records('analytics_used_analysables', $params));
 342  
 343          // Training and prediction data do not get mixed.
 344          $result = new \core_analytics\local\analysis\result_array(1, false, []);
 345          $analysis = new test_analysis($analyser, false, $result);
 346          $analysis->run();
 347          $params = array('modelid' => 1, 'action' => 'training');
 348          $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
 349      }
 350  
 351      /**
 352       * Test model_config::get_class_component.
 353       */
 354      public function test_model_config_get_class_component() {
 355          $this->resetAfterTest(true);
 356  
 357          $this->assertEquals('core',
 358              \core_analytics\model_config::get_class_component('\\core\\analytics\\indicator\\read_actions'));
 359          $this->assertEquals('core',
 360              \core_analytics\model_config::get_class_component('core\\analytics\\indicator\\read_actions'));
 361          $this->assertEquals('core',
 362              \core_analytics\model_config::get_class_component('\\core_course\\analytics\\indicator\\completion_enabled'));
 363          $this->assertEquals('mod_forum',
 364              \core_analytics\model_config::get_class_component('\\mod_forum\\analytics\\indicator\\cognitive_depth'));
 365  
 366          $this->assertEquals('core', \core_analytics\model_config::get_class_component('\\core_class'));
 367      }
 368  
 369      /**
 370       * Test that import_model import models' configurations.
 371       */
 372      public function test_import_model_config() {
 373          $this->resetAfterTest(true);
 374  
 375          $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
 376          $zipfilepath = $this->model->export_model('yeah-config.zip');
 377  
 378          $this->modelobj = $this->model->get_model_obj();
 379  
 380          $importedmodelobj = \core_analytics\model::import_model($zipfilepath)->get_model_obj();
 381  
 382          $this->assertSame($this->modelobj->target, $importedmodelobj->target);
 383          $this->assertSame($this->modelobj->indicators, $importedmodelobj->indicators);
 384          $this->assertSame($this->modelobj->timesplitting, $importedmodelobj->timesplitting);
 385  
 386          $predictionsprocessor = $this->model->get_predictions_processor();
 387          $this->assertSame('\\' . get_class($predictionsprocessor), $importedmodelobj->predictionsprocessor);
 388      }
 389  
 390      /**
 391       * Test can export configuration
 392       */
 393      public function test_can_export_configuration() {
 394          $this->resetAfterTest(true);
 395  
 396          // No time splitting method.
 397          $this->assertFalse($this->model->can_export_configuration());
 398  
 399          $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
 400          $this->assertTrue($this->model->can_export_configuration());
 401  
 402          $this->model->update(true, [], false);
 403          $this->assertFalse($this->model->can_export_configuration());
 404  
 405          $statictarget = new test_static_target_shortname();
 406          $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
 407          $model = \core_analytics\model::create($statictarget, $indicators, '\\core\\analytics\\time_splitting\\quarters');
 408          $this->assertFalse($model->can_export_configuration());
 409      }
 410  
 411      /**
 412       * Test export_config
 413       */
 414      public function test_export_config() {
 415          $this->resetAfterTest(true);
 416  
 417          $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
 418  
 419          $modelconfig = new \core_analytics\model_config($this->model);
 420  
 421          $method = new ReflectionMethod('\\core_analytics\\model_config', 'export_model_data');
 422          $method->setAccessible(true);
 423  
 424          $modeldata = $method->invoke($modelconfig);
 425  
 426          $this->assertArrayHasKey('core', $modeldata->dependencies);
 427          $this->assertIsFloat($modeldata->dependencies['core']);
 428          $this->assertNotEmpty($modeldata->target);
 429          $this->assertNotEmpty($modeldata->timesplitting);
 430          $this->assertCount(3, $modeldata->indicators);
 431  
 432          $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
 433          $this->model->update(true, $indicators, false);
 434  
 435          $modeldata = $method->invoke($modelconfig);
 436  
 437          $this->assertCount(1, $modeldata->indicators);
 438      }
 439  
 440      /**
 441       * Test the implementation of {@link \core_analytics\model::inplace_editable_name()}.
 442       */
 443      public function test_inplace_editable_name() {
 444          global $PAGE;
 445  
 446          $this->resetAfterTest();
 447  
 448          $output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL);
 449  
 450          // Check as a user with permission to edit the name.
 451          $this->setAdminUser();
 452          $ie = $this->model->inplace_editable_name();
 453          $this->assertInstanceOf(\core\output\inplace_editable::class, $ie);
 454          $data = $ie->export_for_template($output);
 455          $this->assertEquals('core_analytics', $data['component']);
 456          $this->assertEquals('modelname', $data['itemtype']);
 457  
 458          // Check as a user without permission to edit the name.
 459          $this->setGuestUser();
 460          $ie = $this->model->inplace_editable_name();
 461          $this->assertInstanceOf(\core\output\inplace_editable::class, $ie);
 462          $data = $ie->export_for_template($output);
 463          $this->assertArrayHasKey('displayvalue', $data);
 464      }
 465  
 466      /**
 467       * Test how the models present themselves in the UI and that they can be renamed.
 468       */
 469      public function test_get_name_and_rename() {
 470          global $PAGE;
 471  
 472          $this->resetAfterTest();
 473  
 474          $output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL);
 475  
 476          // By default, the model exported for template uses its target's name in the name inplace editable element.
 477          $this->assertEquals($this->model->get_name(), $this->model->get_target()->get_name());
 478          $data = $this->model->export($output);
 479          $this->assertEquals($data->name['displayvalue'], $this->model->get_target()->get_name());
 480          $this->assertEquals($data->name['value'], '');
 481  
 482          // Rename the model.
 483          $this->model->rename('NějakĆ½ pokusnĆ½ model');
 484          $this->assertEquals($this->model->get_name(), 'NějakĆ½ pokusnĆ½ model');
 485          $data = $this->model->export($output);
 486          $this->assertEquals($data->name['displayvalue'], 'NějakĆ½ pokusnĆ½ model');
 487          $this->assertEquals($data->name['value'], 'NějakĆ½ pokusnĆ½ model');
 488  
 489          // Undo the renaming.
 490          $this->model->rename('');
 491          $this->assertEquals($this->model->get_name(), $this->model->get_target()->get_name());
 492          $data = $this->model->export($output);
 493          $this->assertEquals($data->name['displayvalue'], $this->model->get_target()->get_name());
 494          $this->assertEquals($data->name['value'], '');
 495      }
 496  
 497      /**
 498       * Tests model::get_potential_timesplittings()
 499       */
 500      public function test_potential_timesplittings() {
 501          $this->resetAfterTest();
 502  
 503          $this->assertArrayNotHasKey('\core\analytics\time_splitting\no_splitting', $this->model->get_potential_timesplittings());
 504          $this->assertArrayHasKey('\core\analytics\time_splitting\single_range', $this->model->get_potential_timesplittings());
 505          $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $this->model->get_potential_timesplittings());
 506      }
 507  
 508      /**
 509       * Tests model::get_samples()
 510       *
 511       * @return null
 512       */
 513      public function test_get_samples() {
 514          $this->resetAfterTest();
 515  
 516          if (!PHPUNIT_LONGTEST) {
 517              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 518          }
 519  
 520          // 10000 should be enough to make oracle and mssql fail, if we want pgsql to fail we need around 70000
 521          // users, that is a few minutes just to create the users.
 522          $nusers = 10000;
 523  
 524          $userids = [];
 525          for ($i = 0; $i < $nusers; $i++) {
 526              $user = $this->getDataGenerator()->create_user();
 527              $userids[] = $user->id;
 528          }
 529  
 530          $upcomingactivities = null;
 531          foreach (\core_analytics\manager::get_all_models() as $model) {
 532              if (get_class($model->get_target()) === 'core_user\\analytics\\target\\upcoming_activities_due') {
 533                  $upcomingactivities = $model;
 534              }
 535          }
 536  
 537          list($sampleids, $samplesdata) = $upcomingactivities->get_samples($userids);
 538          $this->assertCount($nusers, $sampleids);
 539          $this->assertCount($nusers, $samplesdata);
 540  
 541          $subset = array_slice($userids, 0, 100);
 542          list($sampleids, $samplesdata) = $upcomingactivities->get_samples($subset);
 543          $this->assertCount(100, $sampleids);
 544          $this->assertCount(100, $samplesdata);
 545  
 546          $subset = array_slice($userids, 0, 2);
 547          list($sampleids, $samplesdata) = $upcomingactivities->get_samples($subset);
 548          $this->assertCount(2, $sampleids);
 549          $this->assertCount(2, $samplesdata);
 550  
 551          $subset = array_slice($userids, 0, 1);
 552          list($sampleids, $samplesdata) = $upcomingactivities->get_samples($subset);
 553          $this->assertCount(1, $sampleids);
 554          $this->assertCount(1, $samplesdata);
 555  
 556          // Unexisting, so nothing returned, but still 2 arrays.
 557          list($sampleids, $samplesdata) = $upcomingactivities->get_samples([1231231231231231]);
 558          $this->assertEmpty($sampleids);
 559          $this->assertEmpty($samplesdata);
 560  
 561      }
 562  
 563      /**
 564       * Generates a model log record.
 565       */
 566      private function add_fake_log() {
 567          global $DB, $USER;
 568  
 569          $log = new stdClass();
 570          $log->modelid = $this->modelobj->id;
 571          $log->version = $this->modelobj->version;
 572          $log->target = $this->modelobj->target;
 573          $log->indicators = $this->modelobj->indicators;
 574          $log->score = 1;
 575          $log->info = json_encode([]);
 576          $log->dir = 'not important';
 577          $log->timecreated = time();
 578          $log->usermodified = $USER->id;
 579          $DB->insert_record('analytics_models_log', $log);
 580      }
 581  }
 582  
 583  /**
 584   * Testable version to change methods' visibility.
 585   *
 586   * @package   core_analytics
 587   * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
 588   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 589   */
 590  class testable_model extends \core_analytics\model {
 591  
 592      /**
 593       * init_analyser
 594       *
 595       * @param array $options
 596       * @return void
 597       */
 598      public function init_analyser($options = array()) {
 599          parent::init_analyser($options);
 600      }
 601  }