Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core_analytics; 18 19 defined('MOODLE_INTERNAL') || die(); 20 21 require_once (__DIR__ . '/fixtures/test_indicator_max.php'); 22 require_once (__DIR__ . '/fixtures/test_indicator_min.php'); 23 require_once (__DIR__ . '/fixtures/test_indicator_fullname.php'); 24 require_once (__DIR__ . '/fixtures/test_target_course_level_shortname.php'); 25 26 /** 27 * Unit tests for the core_analytics manager. 28 * 29 * @package core_analytics 30 * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com} 31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 32 */ 33 class manager_test extends \advanced_testcase { 34 35 /** 36 * test_deleted_context 37 */ 38 public function test_deleted_context() { 39 global $DB; 40 41 $this->resetAfterTest(true); 42 $this->setAdminuser(); 43 set_config('enabled_stores', 'logstore_standard', 'tool_log'); 44 45 $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); 46 $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname'); 47 foreach ($indicators as $key => $indicator) { 48 $indicators[$key] = \core_analytics\manager::get_indicator($indicator); 49 } 50 51 $model = \core_analytics\model::create($target, $indicators); 52 $modelobj = $model->get_model_obj(); 53 54 $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0)); 55 $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0)); 56 $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1)); 57 $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1)); 58 59 $model->enable('\core\analytics\time_splitting\no_splitting'); 60 61 $model->train(); 62 $model->predict(); 63 64 // Generate a prediction action to confirm that it is deleted when there is an important update. 65 $predictions = $DB->get_records('analytics_predictions'); 66 $prediction = reset($predictions); 67 $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used')); 68 $prediction->action_executed(\core_analytics\prediction::ACTION_USEFUL, $model->get_target()); 69 70 $predictioncontextid = $prediction->get_prediction_data()->contextid; 71 72 $npredictions = $DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid)); 73 $npredictionactions = $DB->count_records('analytics_prediction_actions', 74 array('predictionid' => $prediction->get_prediction_data()->id)); 75 $nindicatorcalc = $DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid)); 76 77 \core_analytics\manager::cleanup(); 78 79 // Nothing is incorrectly deleted. 80 $this->assertEquals($npredictions, $DB->count_records('analytics_predictions', 81 array('contextid' => $predictioncontextid))); 82 $this->assertEquals($npredictionactions, $DB->count_records('analytics_prediction_actions', 83 array('predictionid' => $prediction->get_prediction_data()->id))); 84 $this->assertEquals($nindicatorcalc, $DB->count_records('analytics_indicator_calc', 85 array('contextid' => $predictioncontextid))); 86 87 // Now we delete a context, the course predictions and prediction actions should be deleted. 88 $deletedcontext = \context::instance_by_id($predictioncontextid); 89 delete_course($deletedcontext->instanceid, false); 90 91 \core_analytics\manager::cleanup(); 92 93 $this->assertEmpty($DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid))); 94 $this->assertEmpty($DB->count_records('analytics_prediction_actions', 95 array('predictionid' => $prediction->get_prediction_data()->id))); 96 $this->assertEmpty($DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid))); 97 98 set_config('enabled_stores', '', 'tool_log'); 99 get_log_manager(true); 100 } 101 102 /** 103 * test_deleted_analysable 104 */ 105 public function test_deleted_analysable() { 106 global $DB; 107 108 $this->resetAfterTest(true); 109 $this->setAdminuser(); 110 set_config('enabled_stores', 'logstore_standard', 'tool_log'); 111 112 $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); 113 $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname'); 114 foreach ($indicators as $key => $indicator) { 115 $indicators[$key] = \core_analytics\manager::get_indicator($indicator); 116 } 117 118 $model = \core_analytics\model::create($target, $indicators); 119 $modelobj = $model->get_model_obj(); 120 121 $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0)); 122 $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0)); 123 $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1)); 124 $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1)); 125 126 $model->enable('\core\analytics\time_splitting\no_splitting'); 127 128 $model->train(); 129 $model->predict(); 130 131 $this->assertNotEmpty($DB->count_records('analytics_predict_samples')); 132 $this->assertNotEmpty($DB->count_records('analytics_train_samples')); 133 $this->assertNotEmpty($DB->count_records('analytics_used_analysables')); 134 135 // Now we delete an analysable, stored predict and training samples should be deleted. 136 $deletedcontext = \context_course::instance($coursepredict1->id); 137 delete_course($coursepredict1, false); 138 139 \core_analytics\manager::cleanup(); 140 141 $this->assertEmpty($DB->count_records('analytics_predict_samples', array('analysableid' => $coursepredict1->id))); 142 $this->assertEmpty($DB->count_records('analytics_train_samples', array('analysableid' => $coursepredict1->id))); 143 $this->assertEmpty($DB->count_records('analytics_used_analysables', array('analysableid' => $coursepredict1->id))); 144 145 set_config('enabled_stores', '', 'tool_log'); 146 get_log_manager(true); 147 } 148 149 /** 150 * Tests for the {@link \core_analytics\manager::load_default_models_for_component()} implementation. 151 */ 152 public function test_load_default_models_for_component() { 153 $this->resetAfterTest(); 154 155 // Attempting to load builtin models should always work without throwing exception. 156 \core_analytics\manager::load_default_models_for_component('core'); 157 158 // Attempting to load from a core subsystem without its own subsystem directory. 159 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_access')); 160 161 // Attempting to load from a non-existing subsystem. 162 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_nonexistingsubsystem')); 163 164 // Attempting to load from a non-existing plugin of a known plugin type. 165 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('mod_foobarbazquaz12240996776')); 166 167 // Attempting to load from a non-existing plugin type. 168 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('foo_bar2776327736558')); 169 } 170 171 /** 172 * Tests for the {@link \core_analytics\manager::load_default_models_for_all_components()} implementation. 173 */ 174 public function test_load_default_models_for_all_components() { 175 $this->resetAfterTest(); 176 177 $models = \core_analytics\manager::load_default_models_for_all_components(); 178 179 $this->assertTrue(is_array($models['core'])); 180 $this->assertNotEmpty($models['core']); 181 $this->assertNotEmpty($models['core'][0]['target']); 182 $this->assertNotEmpty($models['core'][0]['indicators']); 183 } 184 185 /** 186 * Tests for the successful execution of the {@link \core_analytics\manager::validate_models_declaration()}. 187 */ 188 public function test_validate_models_declaration() { 189 $this->resetAfterTest(); 190 191 // This is expected to run without an exception. 192 $models = $this->load_models_from_fixture_file('no_teaching'); 193 \core_analytics\manager::validate_models_declaration($models); 194 } 195 196 /** 197 * Tests for the exceptions thrown by {@link \core_analytics\manager::validate_models_declaration()}. 198 * 199 * @dataProvider validate_models_declaration_exceptions_provider 200 * @param array $models Models declaration. 201 * @param string $exception Expected coding exception message. 202 */ 203 public function test_validate_models_declaration_exceptions(array $models, string $exception) { 204 $this->resetAfterTest(); 205 206 $this->expectException(\coding_exception::class); 207 $this->expectExceptionMessage($exception); 208 \core_analytics\manager::validate_models_declaration($models); 209 } 210 211 /** 212 * Data provider for the {@link self::test_validate_models_declaration_exceptions()}. 213 * 214 * @return array of (string)testcase => [(array)models, (string)expected exception message] 215 */ 216 public function validate_models_declaration_exceptions_provider() { 217 return [ 218 'missing_target' => [ 219 $this->load_models_from_fixture_file('missing_target'), 220 'Missing target declaration', 221 ], 222 'invalid_target' => [ 223 $this->load_models_from_fixture_file('invalid_target'), 224 'Invalid target classname', 225 ], 226 'missing_indicators' => [ 227 $this->load_models_from_fixture_file('missing_indicators'), 228 'Missing indicators declaration', 229 ], 230 'invalid_indicators' => [ 231 $this->load_models_from_fixture_file('invalid_indicators'), 232 'Invalid indicator classname', 233 ], 234 'invalid_time_splitting' => [ 235 $this->load_models_from_fixture_file('invalid_time_splitting'), 236 'Invalid time splitting classname', 237 ], 238 'invalid_time_splitting_fq' => [ 239 $this->load_models_from_fixture_file('invalid_time_splitting_fq'), 240 'Expecting fully qualified time splitting classname', 241 ], 242 'invalid_enabled' => [ 243 $this->load_models_from_fixture_file('invalid_enabled'), 244 'Cannot enable a model without time splitting method specified', 245 ], 246 ]; 247 } 248 249 /** 250 * Loads models as declared in the given fixture file. 251 * 252 * @param string $filename 253 * @return array 254 */ 255 protected function load_models_from_fixture_file(string $filename) { 256 global $CFG; 257 258 $models = null; 259 260 require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php'); 261 262 return $models; 263 } 264 265 /** 266 * Test the implementation of the {@link \core_analytics\manager::create_declared_model()}. 267 */ 268 public function test_create_declared_model() { 269 global $DB; 270 271 $this->resetAfterTest(); 272 $this->setAdminuser(); 273 274 $declaration = [ 275 'target' => 'test_target_course_level_shortname', 276 'indicators' => [ 277 'test_indicator_max', 278 'test_indicator_min', 279 'test_indicator_fullname', 280 ], 281 ]; 282 283 $declarationwithtimesplitting = array_merge($declaration, [ 284 'timesplitting' => '\core\analytics\time_splitting\no_splitting', 285 ]); 286 287 $declarationwithtimesplittingenabled = array_merge($declarationwithtimesplitting, [ 288 'enabled' => true, 289 ]); 290 291 // Check that no such model exists yet. 292 $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); 293 $this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()])); 294 $this->assertFalse(\core_analytics\model::exists($target)); 295 296 // Check that the model is created. 297 $created = \core_analytics\manager::create_declared_model($declaration); 298 $this->assertTrue($created instanceof \core_analytics\model); 299 $this->assertTrue(\core_analytics\model::exists($target)); 300 $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()])); 301 $modelid = $created->get_id(); 302 303 // Check that created models are disabled by default. 304 $existing = new \core_analytics\model($modelid); 305 $this->assertEquals(0, $existing->get_model_obj()->enabled); 306 $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); 307 308 // Let the admin enable the model. 309 $existing->enable('\core\analytics\time_splitting\no_splitting'); 310 $this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); 311 312 // Check that further calls create a new model. 313 $repeated = \core_analytics\manager::create_declared_model($declaration); 314 $this->assertTrue($repeated instanceof \core_analytics\model); 315 $this->assertEquals(2, $DB->count_records('analytics_models', ['target' => $target->get_id()])); 316 317 // Delete the models. 318 $existing->delete(); 319 $repeated->delete(); 320 $this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()])); 321 $this->assertFalse(\core_analytics\model::exists($target)); 322 323 // Create it again, this time with time splitting method specified. 324 $created = \core_analytics\manager::create_declared_model($declarationwithtimesplitting); 325 $this->assertTrue($created instanceof \core_analytics\model); 326 $this->assertTrue(\core_analytics\model::exists($target)); 327 $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()])); 328 $modelid = $created->get_id(); 329 330 // Even if the time splitting method was specified, the model is still not enabled automatically. 331 $existing = new \core_analytics\model($modelid); 332 $this->assertEquals(0, $existing->get_model_obj()->enabled); 333 $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); 334 $existing->delete(); 335 336 // Let's define the model so that it is enabled by default. 337 $enabled = \core_analytics\manager::create_declared_model($declarationwithtimesplittingenabled); 338 $this->assertTrue($enabled instanceof \core_analytics\model); 339 $this->assertTrue(\core_analytics\model::exists($target)); 340 $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()])); 341 $modelid = $enabled->get_id(); 342 $existing = new \core_analytics\model($modelid); 343 $this->assertEquals(1, $existing->get_model_obj()->enabled); 344 $this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); 345 346 // Let the admin disable the model. 347 $existing->update(0, false, false); 348 $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); 349 } 350 351 /** 352 * Test the implementation of the {@link \core_analytics\manager::update_default_models_for_component()}. 353 */ 354 public function test_update_default_models_for_component() { 355 356 $this->resetAfterTest(); 357 $this->setAdminuser(); 358 359 $noteaching = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching'); 360 $dropout = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout'); 361 $upcomingactivities = \core_analytics\manager::get_target('\core_user\analytics\target\upcoming_activities_due'); 362 $norecentaccesses = \core_analytics\manager::get_target('\core_course\analytics\target\no_recent_accesses'); 363 $noaccesssincestart = \core_analytics\manager::get_target('\core_course\analytics\target\no_access_since_course_start'); 364 365 $this->assertTrue(\core_analytics\model::exists($noteaching)); 366 $this->assertTrue(\core_analytics\model::exists($dropout)); 367 $this->assertTrue(\core_analytics\model::exists($upcomingactivities)); 368 $this->assertTrue(\core_analytics\model::exists($norecentaccesses)); 369 $this->assertTrue(\core_analytics\model::exists($noaccesssincestart)); 370 371 foreach (\core_analytics\manager::get_all_models() as $model) { 372 $model->delete(); 373 } 374 375 $this->assertFalse(\core_analytics\model::exists($noteaching)); 376 $this->assertFalse(\core_analytics\model::exists($dropout)); 377 $this->assertFalse(\core_analytics\model::exists($upcomingactivities)); 378 $this->assertFalse(\core_analytics\model::exists($norecentaccesses)); 379 $this->assertFalse(\core_analytics\model::exists($noaccesssincestart)); 380 381 $updated = \core_analytics\manager::update_default_models_for_component('moodle'); 382 383 $this->assertEquals(5, count($updated)); 384 $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); 385 $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); 386 $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); 387 $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); 388 $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); 389 $this->assertTrue(\core_analytics\model::exists($noteaching)); 390 $this->assertTrue(\core_analytics\model::exists($dropout)); 391 $this->assertTrue(\core_analytics\model::exists($upcomingactivities)); 392 $this->assertTrue(\core_analytics\model::exists($norecentaccesses)); 393 $this->assertTrue(\core_analytics\model::exists($noaccesssincestart)); 394 395 $repeated = \core_analytics\manager::update_default_models_for_component('moodle'); 396 397 $this->assertSame([], $repeated); 398 } 399 400 /** 401 * test_get_time_splitting_methods description 402 * @return null 403 */ 404 public function test_get_time_splitting_methods() { 405 $this->resetAfterTest(true); 406 407 $all = \core_analytics\manager::get_all_time_splittings(); 408 $this->assertArrayHasKey('\core\analytics\time_splitting\upcoming_week', $all); 409 $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $all); 410 411 $allforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(true); 412 $this->assertArrayNotHasKey('\core\analytics\time_splitting\upcoming_week', $allforevaluation); 413 $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $allforevaluation); 414 415 $defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false); 416 $this->assertArrayNotHasKey('\core\analytics\time_splitting\upcoming_week', $defaultforevaluation); 417 $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation); 418 419 $sometimesplittings = '\core\analytics\time_splitting\single_range,' . 420 '\core\analytics\time_splitting\tenths'; 421 set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics'); 422 423 $defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false); 424 $this->assertArrayNotHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation); 425 } 426 427 /** 428 * Test the implementation of the {@link \core_analytics\manager::model_declaration_identifier()}. 429 */ 430 public function test_model_declaration_identifier() { 431 432 $noteaching1 = $this->load_models_from_fixture_file('no_teaching'); 433 $noteaching2 = $this->load_models_from_fixture_file('no_teaching'); 434 $noteaching3 = $this->load_models_from_fixture_file('no_teaching'); 435 436 // Same model declaration should always lead to same identifier. 437 $this->assertEquals( 438 \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), 439 \core_analytics\manager::model_declaration_identifier(reset($noteaching2)) 440 ); 441 442 // If something is changed, the identifier should change, too. 443 $noteaching2[0]['target'] .= '_'; 444 $this->assertNotEquals( 445 \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), 446 \core_analytics\manager::model_declaration_identifier(reset($noteaching2)) 447 ); 448 449 $noteaching3[0]['indicators'][] = '\core_analytics\local\indicator\binary'; 450 $this->assertNotEquals( 451 \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), 452 \core_analytics\manager::model_declaration_identifier(reset($noteaching3)) 453 ); 454 455 // The identifier is supposed to contain PARAM_ALPHANUM only. 456 $this->assertEquals( 457 \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), 458 clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching1)), PARAM_ALPHANUM) 459 ); 460 $this->assertEquals( 461 \core_analytics\manager::model_declaration_identifier(reset($noteaching2)), 462 clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching2)), PARAM_ALPHANUM) 463 ); 464 $this->assertEquals( 465 \core_analytics\manager::model_declaration_identifier(reset($noteaching3)), 466 clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching3)), PARAM_ALPHANUM) 467 ); 468 } 469 470 /** 471 * Tests for the {@link \core_analytics\manager::get_declared_target_and_indicators_instances()}. 472 */ 473 public function test_get_declared_target_and_indicators_instances() { 474 $this->resetAfterTest(); 475 476 $definition = $this->load_models_from_fixture_file('no_teaching'); 477 478 list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]); 479 480 $this->assertTrue($target instanceof \core_analytics\local\target\base); 481 $this->assertNotEmpty($indicators); 482 $this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators); 483 } 484 485 /** 486 * test_get_potential_context_restrictions description 487 */ 488 public function test_get_potential_context_restrictions() { 489 $this->resetAfterTest(); 490 491 // No potential context restrictions. 492 $this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([])); 493 494 // Include the all context levels so the misc. category get included. 495 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions()); 496 497 $this->getDataGenerator()->create_course(); 498 $this->getDataGenerator()->create_category(); 499 $this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions()); 500 $this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE, CONTEXT_COURSECAT])); 501 502 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE])); 503 $this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT])); 504 505 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category')); 506 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1')); 507 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Miscellaneous')); 508 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1')); 509 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course')); 510 } 511 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body