Moodle release 3.11.12
[moodle.git] / analytics / tests / manager_test.php
blob90c41d315e43a1f9024b2368c6feb2b0aba6c293
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 namespace core_analytics;
19 defined('MOODLE_INTERNAL') || die();
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');
26 /**
27 * Unit tests for the core_analytics manager.
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
33 class manager_test extends \advanced_testcase {
35 /**
36 * test_deleted_context
38 public function test_deleted_context() {
39 global $DB;
41 $this->resetAfterTest(true);
42 $this->setAdminuser();
43 set_config('enabled_stores', 'logstore_standard', 'tool_log');
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);
51 $model = \core_analytics\model::create($target, $indicators);
52 $modelobj = $model->get_model_obj();
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));
59 $model->enable('\core\analytics\time_splitting\no_splitting');
61 $model->train();
62 $model->predict();
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());
70 $predictioncontextid = $prediction->get_prediction_data()->contextid;
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));
77 \core_analytics\manager::cleanup();
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)));
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);
91 \core_analytics\manager::cleanup();
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)));
98 set_config('enabled_stores', '', 'tool_log');
99 get_log_manager(true);
103 * test_deleted_analysable
105 public function test_deleted_analysable() {
106 global $DB;
108 $this->resetAfterTest(true);
109 $this->setAdminuser();
110 set_config('enabled_stores', 'logstore_standard', 'tool_log');
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);
118 $model = \core_analytics\model::create($target, $indicators);
119 $modelobj = $model->get_model_obj();
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));
126 $model->enable('\core\analytics\time_splitting\no_splitting');
128 $model->train();
129 $model->predict();
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'));
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);
139 \core_analytics\manager::cleanup();
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)));
145 set_config('enabled_stores', '', 'tool_log');
146 get_log_manager(true);
150 * Tests for the {@link \core_analytics\manager::load_default_models_for_component()} implementation.
152 public function test_load_default_models_for_component() {
153 $this->resetAfterTest();
155 // Attempting to load builtin models should always work without throwing exception.
156 \core_analytics\manager::load_default_models_for_component('core');
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'));
161 // Attempting to load from a non-existing subsystem.
162 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_nonexistingsubsystem'));
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'));
167 // Attempting to load from a non-existing plugin type.
168 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('foo_bar2776327736558'));
172 * Tests for the {@link \core_analytics\manager::load_default_models_for_all_components()} implementation.
174 public function test_load_default_models_for_all_components() {
175 $this->resetAfterTest();
177 $models = \core_analytics\manager::load_default_models_for_all_components();
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']);
186 * Tests for the successful execution of the {@link \core_analytics\manager::validate_models_declaration()}.
188 public function test_validate_models_declaration() {
189 $this->resetAfterTest();
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);
197 * Tests for the exceptions thrown by {@link \core_analytics\manager::validate_models_declaration()}.
199 * @dataProvider validate_models_declaration_exceptions_provider
200 * @param array $models Models declaration.
201 * @param string $exception Expected coding exception message.
203 public function test_validate_models_declaration_exceptions(array $models, string $exception) {
204 $this->resetAfterTest();
206 $this->expectException(\coding_exception::class);
207 $this->expectExceptionMessage($exception);
208 \core_analytics\manager::validate_models_declaration($models);
212 * Data provider for the {@link self::test_validate_models_declaration_exceptions()}.
214 * @return array of (string)testcase => [(array)models, (string)expected exception message]
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',
222 'invalid_target' => [
223 $this->load_models_from_fixture_file('invalid_target'),
224 'Invalid target classname',
226 'missing_indicators' => [
227 $this->load_models_from_fixture_file('missing_indicators'),
228 'Missing indicators declaration',
230 'invalid_indicators' => [
231 $this->load_models_from_fixture_file('invalid_indicators'),
232 'Invalid indicator classname',
234 'invalid_time_splitting' => [
235 $this->load_models_from_fixture_file('invalid_time_splitting'),
236 'Invalid time splitting classname',
238 'invalid_time_splitting_fq' => [
239 $this->load_models_from_fixture_file('invalid_time_splitting_fq'),
240 'Expecting fully qualified time splitting classname',
242 'invalid_enabled' => [
243 $this->load_models_from_fixture_file('invalid_enabled'),
244 'Cannot enable a model without time splitting method specified',
250 * Loads models as declared in the given fixture file.
252 * @param string $filename
253 * @return array
255 protected function load_models_from_fixture_file(string $filename) {
256 global $CFG;
258 $models = null;
260 require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php');
262 return $models;
266 * Test the implementation of the {@link \core_analytics\manager::create_declared_model()}.
268 public function test_create_declared_model() {
269 global $DB;
271 $this->resetAfterTest();
272 $this->setAdminuser();
274 $declaration = [
275 'target' => 'test_target_course_level_shortname',
276 'indicators' => [
277 'test_indicator_max',
278 'test_indicator_min',
279 'test_indicator_fullname',
283 $declarationwithtimesplitting = array_merge($declaration, [
284 'timesplitting' => '\core\analytics\time_splitting\no_splitting',
287 $declarationwithtimesplittingenabled = array_merge($declarationwithtimesplitting, [
288 'enabled' => true,
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));
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();
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));
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));
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()]));
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));
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();
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();
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));
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));
352 * Test the implementation of the {@link \core_analytics\manager::update_default_models_for_component()}.
354 public function test_update_default_models_for_component() {
356 $this->resetAfterTest();
357 $this->setAdminuser();
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');
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));
371 foreach (\core_analytics\manager::get_all_models() as $model) {
372 $model->delete();
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));
381 $updated = \core_analytics\manager::update_default_models_for_component('moodle');
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));
395 $repeated = \core_analytics\manager::update_default_models_for_component('moodle');
397 $this->assertSame([], $repeated);
401 * test_get_time_splitting_methods description
402 * @return null
404 public function test_get_time_splitting_methods() {
405 $this->resetAfterTest(true);
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);
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);
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);
419 $sometimesplittings = '\core\analytics\time_splitting\single_range,' .
420 '\core\analytics\time_splitting\tenths';
421 set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics');
423 $defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false);
424 $this->assertArrayNotHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation);
428 * Test the implementation of the {@link \core_analytics\manager::model_declaration_identifier()}.
430 public function test_model_declaration_identifier() {
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');
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))
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))
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))
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)
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)
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)
471 * Tests for the {@link \core_analytics\manager::get_declared_target_and_indicators_instances()}.
473 public function test_get_declared_target_and_indicators_instances() {
474 $this->resetAfterTest();
476 $definition = $this->load_models_from_fixture_file('no_teaching');
478 list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]);
480 $this->assertTrue($target instanceof \core_analytics\local\target\base);
481 $this->assertNotEmpty($indicators);
482 $this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators);
486 * test_get_potential_context_restrictions description
488 public function test_get_potential_context_restrictions() {
489 $this->resetAfterTest();
491 // No potential context restrictions.
492 $this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
494 // Include the all context levels so the misc. category get included.
495 $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions());
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]));
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]));
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'));