Search moodle.org's
Developer Documentation

See Release Notes

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

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

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