Search moodle.org's
Developer Documentation

See Release Notes

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

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

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