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