MDL-72395 composer: Update to newer dependencies
[moodle.git] / analytics / tests / manager_test.php
blob582c45740a2a763ad341e9c18bdeab0dea105628
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.
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/>.
17 /**
18 * Unit tests for the manager.
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
25 defined('MOODLE_INTERNAL') || die();
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');
32 /**
33 * Unit tests for the manager.
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
39 class analytics_manager_testcase extends advanced_testcase {
41 /**
42 * test_deleted_context
44 public function test_deleted_context() {
45 global $DB;
47 $this->resetAfterTest(true);
48 $this->setAdminuser();
49 set_config('enabled_stores', 'logstore_standard', 'tool_log');
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);
57 $model = \core_analytics\model::create($target, $indicators);
58 $modelobj = $model->get_model_obj();
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));
65 $model->enable('\core\analytics\time_splitting\no_splitting');
67 $model->train();
68 $model->predict();
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());
76 $predictioncontextid = $prediction->get_prediction_data()->contextid;
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));
83 \core_analytics\manager::cleanup();
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)));
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);
97 \core_analytics\manager::cleanup();
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)));
104 set_config('enabled_stores', '', 'tool_log');
105 get_log_manager(true);
109 * test_deleted_analysable
111 public function test_deleted_analysable() {
112 global $DB;
114 $this->resetAfterTest(true);
115 $this->setAdminuser();
116 set_config('enabled_stores', 'logstore_standard', 'tool_log');
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);
124 $model = \core_analytics\model::create($target, $indicators);
125 $modelobj = $model->get_model_obj();
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));
132 $model->enable('\core\analytics\time_splitting\no_splitting');
134 $model->train();
135 $model->predict();
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'));
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);
145 \core_analytics\manager::cleanup();
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)));
151 set_config('enabled_stores', '', 'tool_log');
152 get_log_manager(true);
156 * Tests for the {@link \core_analytics\manager::load_default_models_for_component()} implementation.
158 public function test_load_default_models_for_component() {
159 $this->resetAfterTest();
161 // Attempting to load builtin models should always work without throwing exception.
162 \core_analytics\manager::load_default_models_for_component('core');
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'));
167 // Attempting to load from a non-existing subsystem.
168 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_nonexistingsubsystem'));
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'));
173 // Attempting to load from a non-existing plugin type.
174 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('foo_bar2776327736558'));
178 * Tests for the {@link \core_analytics\manager::load_default_models_for_all_components()} implementation.
180 public function test_load_default_models_for_all_components() {
181 $this->resetAfterTest();
183 $models = \core_analytics\manager::load_default_models_for_all_components();
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']);
192 * Tests for the successful execution of the {@link \core_analytics\manager::validate_models_declaration()}.
194 public function test_validate_models_declaration() {
195 $this->resetAfterTest();
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);
203 * Tests for the exceptions thrown by {@link \core_analytics\manager::validate_models_declaration()}.
205 * @dataProvider validate_models_declaration_exceptions_provider
206 * @param array $models Models declaration.
207 * @param string $exception Expected coding exception message.
209 public function test_validate_models_declaration_exceptions(array $models, string $exception) {
210 $this->resetAfterTest();
212 $this->expectException(\coding_exception::class);
213 $this->expectExceptionMessage($exception);
214 \core_analytics\manager::validate_models_declaration($models);
218 * Data provider for the {@link self::test_validate_models_declaration_exceptions()}.
220 * @return array of (string)testcase => [(array)models, (string)expected exception message]
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',
228 'invalid_target' => [
229 $this->load_models_from_fixture_file('invalid_target'),
230 'Invalid target classname',
232 'missing_indicators' => [
233 $this->load_models_from_fixture_file('missing_indicators'),
234 'Missing indicators declaration',
236 'invalid_indicators' => [
237 $this->load_models_from_fixture_file('invalid_indicators'),
238 'Invalid indicator classname',
240 'invalid_time_splitting' => [
241 $this->load_models_from_fixture_file('invalid_time_splitting'),
242 'Invalid time splitting classname',
244 'invalid_time_splitting_fq' => [
245 $this->load_models_from_fixture_file('invalid_time_splitting_fq'),
246 'Expecting fully qualified time splitting classname',
248 'invalid_enabled' => [
249 $this->load_models_from_fixture_file('invalid_enabled'),
250 'Cannot enable a model without time splitting method specified',
256 * Loads models as declared in the given fixture file.
258 * @param string $filename
259 * @return array
261 protected function load_models_from_fixture_file(string $filename) {
262 global $CFG;
264 $models = null;
266 require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php');
268 return $models;
272 * Test the implementation of the {@link \core_analytics\manager::create_declared_model()}.
274 public function test_create_declared_model() {
275 global $DB;
277 $this->resetAfterTest();
278 $this->setAdminuser();
280 $declaration = [
281 'target' => 'test_target_course_level_shortname',
282 'indicators' => [
283 'test_indicator_max',
284 'test_indicator_min',
285 'test_indicator_fullname',
289 $declarationwithtimesplitting = array_merge($declaration, [
290 'timesplitting' => '\core\analytics\time_splitting\no_splitting',
293 $declarationwithtimesplittingenabled = array_merge($declarationwithtimesplitting, [
294 'enabled' => true,
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));
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();
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));
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));
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()]));
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));
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();
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();
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));
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));
358 * Test the implementation of the {@link \core_analytics\manager::update_default_models_for_component()}.
360 public function test_update_default_models_for_component() {
362 $this->resetAfterTest();
363 $this->setAdminuser();
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');
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));
377 foreach (\core_analytics\manager::get_all_models() as $model) {
378 $model->delete();
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));
387 $updated = \core_analytics\manager::update_default_models_for_component('moodle');
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));
401 $repeated = \core_analytics\manager::update_default_models_for_component('moodle');
403 $this->assertSame([], $repeated);
407 * test_get_time_splitting_methods description
408 * @return null
410 public function test_get_time_splitting_methods() {
411 $this->resetAfterTest(true);
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);
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);
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);
425 $sometimesplittings = '\core\analytics\time_splitting\single_range,' .
426 '\core\analytics\time_splitting\tenths';
427 set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics');
429 $defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false);
430 $this->assertArrayNotHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation);
434 * Test the implementation of the {@link \core_analytics\manager::model_declaration_identifier()}.
436 public function test_model_declaration_identifier() {
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');
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))
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))
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))
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)
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)
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)
477 * Tests for the {@link \core_analytics\manager::get_declared_target_and_indicators_instances()}.
479 public function test_get_declared_target_and_indicators_instances() {
480 $this->resetAfterTest();
482 $definition = $this->load_models_from_fixture_file('no_teaching');
484 list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]);
486 $this->assertTrue($target instanceof \core_analytics\local\target\base);
487 $this->assertNotEmpty($indicators);
488 $this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators);
492 * test_get_potential_context_restrictions description
494 public function test_get_potential_context_restrictions() {
495 $this->resetAfterTest();
497 // No potential context restrictions.
498 $this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
500 // Include the all context levels so the misc. category get included.
501 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions());
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]));
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]));
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'));