MDL-61667 analytics: Fix checking that a given model does not exist
[moodle.git] / analytics / tests / model_test.php
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/>.
17 /**
18  * Unit tests for the model.
19  *
20  * @package   core_analytics
21  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
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_shortname.php');
31 require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
32 require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
33 require_once(__DIR__ . '/fixtures/test_analyser.php');
35 /**
36  * Unit tests for the model.
37  *
38  * @package   core_analytics
39  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
40  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class analytics_model_testcase extends advanced_testcase {
44     public function setUp() {
46         $this->setAdminUser();
48         $target = \core_analytics\manager::get_target('test_target_shortname');
49         $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
50         foreach ($indicators as $key => $indicator) {
51             $indicators[$key] = \core_analytics\manager::get_indicator($indicator);
52         }
54         $this->model = testable_model::create($target, $indicators);
55         $this->modelobj = $this->model->get_model_obj();
56     }
58     public function test_enable() {
59         $this->resetAfterTest(true);
61         $this->assertEquals(0, $this->model->get_model_obj()->enabled);
62         $this->assertEquals(0, $this->model->get_model_obj()->trained);
63         $this->assertEquals('', $this->model->get_model_obj()->timesplitting);
65         $this->model->enable('\core\analytics\time_splitting\quarters');
66         $this->assertEquals(1, $this->model->get_model_obj()->enabled);
67         $this->assertEquals(0, $this->model->get_model_obj()->trained);
68         $this->assertEquals('\core\analytics\time_splitting\quarters', $this->model->get_model_obj()->timesplitting);
69     }
71     public function test_create() {
72         $this->resetAfterTest(true);
74         $target = \core_analytics\manager::get_target('\core\analytics\target\course_dropout');
75         $indicators = array(
76             \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
77             \core_analytics\manager::get_indicator('\core\analytics\indicator\read_actions')
78         );
79         $model = \core_analytics\model::create($target, $indicators);
80         $this->assertInstanceOf('\core_analytics\model', $model);
81     }
83     /**
84      * test_delete
85      */
86     public function test_delete() {
87         global $DB;
89         $this->resetAfterTest(true);
90         set_config('enabled_stores', 'logstore_standard', 'tool_log');
92         $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
93         $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
94         $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
95         $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
97         $this->model->enable('\core\analytics\time_splitting\no_splitting');
99         $this->model->train();
100         $this->model->predict();
102         // Fake evaluation results record to check that it is actually deleted.
103         $this->add_fake_log();
105         $modeloutputdir = $this->model->get_output_dir(array(), true);
106         $this->assertTrue(is_dir($modeloutputdir));
108         // Generate a prediction action to confirm that it is deleted when there is an important update.
109         $predictions = $DB->get_records('analytics_predictions');
110         $prediction = reset($predictions);
111         $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
112         $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
114         $this->model->delete();
115         $this->assertEmpty($DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
116         $this->assertEmpty($DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
117         $this->assertEmpty($DB->count_records('analytics_predictions'));
118         $this->assertEmpty($DB->count_records('analytics_prediction_actions'));
119         $this->assertEmpty($DB->count_records('analytics_train_samples'));
120         $this->assertEmpty($DB->count_records('analytics_predict_samples'));
121         $this->assertEmpty($DB->count_records('analytics_used_files'));
122         $this->assertFalse(is_dir($modeloutputdir));
124         set_config('enabled_stores', '', 'tool_log');
125         get_log_manager(true);
126     }
128     /**
129      * test_clear
130      */
131     public function test_clear() {
132         global $DB;
134         $this->resetAfterTest(true);
135         set_config('enabled_stores', 'logstore_standard', 'tool_log');
137         $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
138         $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
139         $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
140         $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
142         $this->model->enable('\core\analytics\time_splitting\no_splitting');
144         $this->model->train();
145         $this->model->predict();
147         // Fake evaluation results record to check that it is actually deleted.
148         $this->add_fake_log();
150         // Generate a prediction action to confirm that it is deleted when there is an important update.
151         $predictions = $DB->get_records('analytics_predictions');
152         $prediction = reset($predictions);
153         $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
154         $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
156         $modelversionoutputdir = $this->model->get_output_dir();
157         $this->assertTrue(is_dir($modelversionoutputdir));
159         // Update to an empty time splitting method to force model::clear execution.
160         $this->model->clear();
161         $this->assertFalse(is_dir($modelversionoutputdir));
163         // Check that most of the stuff got deleted.
164         $this->assertEquals(1, $DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
165         $this->assertEquals(1, $DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
166         $this->assertEmpty($DB->count_records('analytics_predictions'));
167         $this->assertEmpty($DB->count_records('analytics_prediction_actions'));
168         $this->assertEmpty($DB->count_records('analytics_train_samples'));
169         $this->assertEmpty($DB->count_records('analytics_predict_samples'));
170         $this->assertEmpty($DB->count_records('analytics_used_files'));
172         // Check that the model is marked as not trained after clearing (as it is not a static one).
173         $this->assertEquals(0, $DB->get_field('analytics_models', 'trained', array('id' => $this->modelobj->id)));
175         set_config('enabled_stores', '', 'tool_log');
176         get_log_manager(true);
177     }
179     /**
180      * Test behaviour of {\core_analytics\model::clear()} for static models.
181      */
182     public function test_clear_static() {
183         global $DB;
184         $this->resetAfterTest();
186         $statictarget = new test_static_target_shortname();
187         $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
188         $model = \core_analytics\model::create($statictarget, $indicators, '\core\analytics\time_splitting\quarters');
189         $modelobj = $model->get_model_obj();
191         // Static models are always considered trained.
192         $this->assertEquals(1, $DB->get_field('analytics_models', 'trained', array('id' => $modelobj->id)));
194         $model->clear();
196         // Check that the model is still marked as trained even after clearing.
197         $this->assertEquals(1, $DB->get_field('analytics_models', 'trained', array('id' => $modelobj->id)));
198     }
200     public function test_model_manager() {
201         $this->resetAfterTest(true);
203         $this->assertCount(3, $this->model->get_indicators());
204         $this->assertInstanceOf('\core_analytics\local\target\binary', $this->model->get_target());
206         // Using evaluation as the model is not yet enabled.
207         $this->model->init_analyser(array('evaluation' => true));
208         $this->assertInstanceOf('\core_analytics\local\analyser\base', $this->model->get_analyser());
210         $this->model->enable('\core\analytics\time_splitting\quarters');
211         $this->assertInstanceOf('\core\analytics\analyser\site_courses', $this->model->get_analyser());
212     }
214     public function test_output_dir() {
215         $this->resetAfterTest(true);
217         $dir = make_request_directory();
218         set_config('modeloutputdir', $dir, 'analytics');
220         $modeldir = $dir . DIRECTORY_SEPARATOR . $this->modelobj->id . DIRECTORY_SEPARATOR . $this->modelobj->version;
221         $this->assertEquals($modeldir, $this->model->get_output_dir());
222         $this->assertEquals($modeldir . DIRECTORY_SEPARATOR . 'testing', $this->model->get_output_dir(array('testing')));
223     }
225     public function test_unique_id() {
226         global $DB;
228         $this->resetAfterTest(true);
230         $originaluniqueid = $this->model->get_unique_id();
232         // Same id across instances.
233         $this->model = new testable_model($this->modelobj);
234         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
236         // We will restore it later.
237         $originalversion = $this->modelobj->version;
239         // Generates a different id if timemodified changes.
240         $this->modelobj->version = $this->modelobj->version + 10;
241         $DB->update_record('analytics_models', $this->modelobj);
242         $this->model = new testable_model($this->modelobj);
243         $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
245         // Restore original timemodified to continue testing.
246         $this->modelobj->version = $originalversion;
247         $DB->update_record('analytics_models', $this->modelobj);
248         // Same when updating through an action that changes the model.
249         $this->model = new testable_model($this->modelobj);
251         $this->model->mark_as_trained();
252         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
254         // Wait for the current timestamp to change.
255         $this->waitForSecond();
256         $this->model->enable('\core\analytics\time_splitting\deciles');
257         $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
258         $uniqueid = $this->model->get_unique_id();
260         // Wait for the current timestamp to change.
261         $this->waitForSecond();
262         $this->model->enable('\core\analytics\time_splitting\quarters');
263         $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
264         $this->assertNotEquals($uniqueid, $this->model->get_unique_id());
265     }
267     /**
268      * test_exists
269      *
270      * @return void
271      */
272     public function test_exists() {
273         $this->resetAfterTest(true);
275         $target = \core_analytics\manager::get_target('\core\analytics\target\no_teaching');
276         $this->assertTrue(\core_analytics\model::exists($target));
278         foreach (\core_analytics\manager::get_all_models() as $model) {
279             $model->delete();
280         }
282         $this->assertFalse(\core_analytics\model::exists($target));
283     }
285     /**
286      * test_model_timelimit
287      *
288      * @return null
289      */
290     public function test_model_timelimit() {
291         global $DB;
293         $this->resetAfterTest(true);
295         set_config('modeltimelimit', 2, 'analytics');
297         $courses = array();
298         for ($i = 0; $i < 5; $i++) {
299             $course = $this->getDataGenerator()->create_course();
300             $analysable = new \core_analytics\course($course);
301             $courses[$analysable->get_id()] = $course;
302         }
304         $target = new test_target_course_level_shortname();
305         $analyser = new test_analyser(1, $target, [], [], []);
307         // Each analysable element takes 1.1 secs, so the max (and likely) number of analysable
308         // elements that will be processed is 2.
309         $analyser->get_analysable_data(false);
310         $params = array('modelid' => 1, 'action' => 'prediction');
311         $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
313         $analyser->get_analysable_data(false);
314         $this->assertLessThanOrEqual(4, $DB->count_records('analytics_used_analysables', $params));
316         // Check that analysable elements have been processed following the analyser order
317         // (course->sortorder here). We can not check this nicely after next get_analysable_data round
318         // because the first analysed element will be analysed again.
319         $analysedelems = $DB->get_records('analytics_used_analysables', $params, 'timeanalysed ASC');
320         // Just a default for the first checked element.
321         $last = (object)['sortorder' => PHP_INT_MAX];
322         foreach ($analysedelems as $analysed) {
323             if ($courses[$analysed->analysableid]->sortorder > $last->sortorder) {
324                 $this->fail('Analysable elements have not been analysed sorted by course sortorder.');
325             }
326             $last = $courses[$analysed->analysableid];
327         }
329         $analyser->get_analysable_data(false);
330         $this->assertGreaterThanOrEqual(5, $DB->count_records('analytics_used_analysables', $params));
332         // New analysable elements are immediately pulled.
333         $this->getDataGenerator()->create_course();
334         $analyser->get_analysable_data(false);
335         $this->assertGreaterThanOrEqual(6, $DB->count_records('analytics_used_analysables', $params));
337         // Training and prediction data do not get mixed.
338         $analyser->get_analysable_data(true);
339         $params = array('modelid' => 1, 'action' => 'training');
340         $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
341     }
343     /**
344      * Test model_config::get_class_component.
345      */
346     public function test_model_config_get_class_component() {
347         $this->resetAfterTest(true);
349         $this->assertEquals('core',
350             \core_analytics\model_config::get_class_component('\\core\\analytics\\indicator\\read_actions'));
351         $this->assertEquals('core',
352             \core_analytics\model_config::get_class_component('core\\analytics\\indicator\\read_actions'));
353         $this->assertEquals('core',
354             \core_analytics\model_config::get_class_component('\\core_course\\analytics\\indicator\\completion_enabled'));
355         $this->assertEquals('mod_forum',
356             \core_analytics\model_config::get_class_component('\\mod_forum\\analytics\\indicator\\cognitive_depth'));
358         $this->assertEquals('core', \core_analytics\model_config::get_class_component('\\core_class'));
359     }
361     /**
362      * Test that import_model import models' configurations.
363      */
364     public function test_import_model_config() {
365         $this->resetAfterTest(true);
367         $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
368         $zipfilepath = $this->model->export_model('yeah-config.zip');
370         $this->modelobj = $this->model->get_model_obj();
372         $importedmodelobj = \core_analytics\model::import_model($zipfilepath)->get_model_obj();
374         $this->assertSame($this->modelobj->target, $importedmodelobj->target);
375         $this->assertSame($this->modelobj->indicators, $importedmodelobj->indicators);
376         $this->assertSame($this->modelobj->timesplitting, $importedmodelobj->timesplitting);
378         $predictionsprocessor = $this->model->get_predictions_processor();
379         $this->assertSame('\\' . get_class($predictionsprocessor), $importedmodelobj->predictionsprocessor);
380     }
382     /**
383      * Test can export configuration
384      */
385     public function test_can_export_configuration() {
386         $this->resetAfterTest(true);
388         // No time splitting method.
389         $this->assertFalse($this->model->can_export_configuration());
391         $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
392         $this->assertTrue($this->model->can_export_configuration());
394         $this->model->update(true, [], false);
395         $this->assertFalse($this->model->can_export_configuration());
397         $statictarget = new test_static_target_shortname();
398         $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
399         $model = \core_analytics\model::create($statictarget, $indicators, '\\core\\analytics\\time_splitting\\quarters');
400         $this->assertFalse($model->can_export_configuration());
401     }
403     /**
404      * Test export_config
405      */
406     public function test_export_config() {
407         $this->resetAfterTest(true);
409         $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
411         $modelconfig = new \core_analytics\model_config($this->model);
413         $method = new ReflectionMethod('\\core_analytics\\model_config', 'export_model_data');
414         $method->setAccessible(true);
416         $modeldata = $method->invoke($modelconfig);
418         $this->assertArrayHasKey('core', $modeldata->dependencies);
419         $this->assertInternalType('float', $modeldata->dependencies['core']);
420         $this->assertNotEmpty($modeldata->target);
421         $this->assertNotEmpty($modeldata->timesplitting);
422         $this->assertCount(3, $modeldata->indicators);
424         $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
425         $this->model->update(true, $indicators, false);
427         $modeldata = $method->invoke($modelconfig);
429         $this->assertCount(1, $modeldata->indicators);
430     }
432     /**
433      * Generates a model log record.
434      */
435     private function add_fake_log() {
436         global $DB, $USER;
438         $log = new stdClass();
439         $log->modelid = $this->modelobj->id;
440         $log->version = $this->modelobj->version;
441         $log->target = $this->modelobj->target;
442         $log->indicators = $this->modelobj->indicators;
443         $log->score = 1;
444         $log->info = json_encode([]);
445         $log->dir = 'not important';
446         $log->timecreated = time();
447         $log->usermodified = $USER->id;
448         $DB->insert_record('analytics_models_log', $log);
449     }
452 /**
453  * Testable version to change methods' visibility.
454  *
455  * @package   core_analytics
456  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
457  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
458  */
459 class testable_model extends \core_analytics\model {
461     /**
462      * init_analyser
463      *
464      * @param array $options
465      * @return void
466      */
467     public function init_analyser($options = array()) {
468         parent::init_analyser($options);
469     }