MDL-61667 analytics: Fix checking that a given model does not exist
[moodle.git] / analytics / tests / manager_test.php
CommitLineData
f9222c49
DM
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
25defined('MOODLE_INTERNAL') || die();
26
27require_once(__DIR__ . '/fixtures/test_indicator_max.php');
28require_once(__DIR__ . '/fixtures/test_indicator_min.php');
29require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
30require_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 */
39class 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_FIXED, $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 $npredictsamples = $DB->count_records('analytics_predict_samples');
138 $ntrainsamples = $DB->count_records('analytics_train_samples');
139
140 // Now we delete an analysable, stored predict and training samples should be deleted.
141 $deletedcontext = \context_course::instance($coursepredict1->id);
142 delete_course($coursepredict1, false);
143
144 \core_analytics\manager::cleanup();
145
146 $this->assertEmpty($DB->count_records('analytics_predict_samples', array('analysableid' => $coursepredict1->id)));
147 $this->assertEmpty($DB->count_records('analytics_train_samples', array('analysableid' => $coursepredict1->id)));
148
149 set_config('enabled_stores', '', 'tool_log');
150 get_log_manager(true);
151 }
152
f0584045
DM
153 /**
154 * Tests for the {@link \core_analytics\manager::load_default_models_for_component()} implementation.
155 */
156 public function test_load_default_models_for_component() {
157 $this->resetAfterTest();
158
159 // Attempting to load builtin models should always work without throwing exception.
160 \core_analytics\manager::load_default_models_for_component('core');
161
162 // Attempting to load from a core subsystem without its own subsystem directory.
163 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_access'));
164
165 // Attempting to load from a non-existing subsystem.
166 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_nonexistingsubsystem'));
167
168 // Attempting to load from a non-existing plugin of a known plugin type.
169 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('mod_foobarbazquaz12240996776'));
170
171 // Attempting to load from a non-existing plugin type.
172 $this->assertSame([], \core_analytics\manager::load_default_models_for_component('foo_bar2776327736558'));
173 }
174
175 /**
176 * Tests for the successful execution of the {@link \core_analytics\manager::validate_models_declaration()}.
177 */
178 public function test_validate_models_declaration() {
179 $this->resetAfterTest();
180
181 // This is expected to run without an exception.
182 $models = $this->load_models_from_fixture_file('no_teaching');
183 \core_analytics\manager::validate_models_declaration($models);
184 }
185
186 /**
187 * Tests for the exceptions thrown by {@link \core_analytics\manager::validate_models_declaration()}.
188 *
189 * @dataProvider validate_models_declaration_exceptions_provider
190 * @param array $models Models declaration.
191 * @param string $exception Expected coding exception message.
192 */
193 public function test_validate_models_declaration_exceptions(array $models, string $exception) {
194 $this->resetAfterTest();
195
196 $this->expectException(\coding_exception::class);
197 $this->expectExceptionMessage($exception);
198 \core_analytics\manager::validate_models_declaration($models);
199 }
200
201 /**
202 * Data provider for the {@link self::test_validate_models_declaration_exceptions()}.
203 *
204 * @return array of (string)testcase => [(array)models, (string)expected exception message]
205 */
206 public function validate_models_declaration_exceptions_provider() {
207 return [
208 'missing_target' => [
209 $this->load_models_from_fixture_file('missing_target'),
210 'Missing target declaration',
211 ],
212 'invalid_target' => [
213 $this->load_models_from_fixture_file('invalid_target'),
214 'Invalid target classname',
215 ],
216 'missing_indicators' => [
217 $this->load_models_from_fixture_file('missing_indicators'),
218 'Missing indicators declaration',
219 ],
220 'invalid_indicators' => [
221 $this->load_models_from_fixture_file('invalid_indicators'),
222 'Invalid indicator classname',
223 ],
224 'invalid_time_splitting' => [
225 $this->load_models_from_fixture_file('invalid_time_splitting'),
226 'Invalid time splitting classname',
227 ],
228 'invalid_time_splitting_fq' => [
229 $this->load_models_from_fixture_file('invalid_time_splitting_fq'),
230 'Expecting fully qualified time splitting classname',
231 ],
232 'invalid_enabled' => [
233 $this->load_models_from_fixture_file('invalid_enabled'),
234 'Cannot enable a model without time splitting method specified',
235 ],
236 ];
237 }
238
239 /**
240 * Loads models as declared in the given fixture file.
241 *
242 * @param string $filename
243 * @return array
244 */
245 protected function load_models_from_fixture_file(string $filename) {
246 global $CFG;
247
248 $models = null;
249
250 require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php');
251
252 return $models;
253 }
6187213f
DM
254
255 /**
256 * Test the implementation of the {@link \core_analytics\manager::create_declared_model()}.
257 */
258 public function test_create_declared_model() {
259 global $DB;
260
261 $this->resetAfterTest();
262 $this->setAdminuser();
263
264 $declaration = [
265 'target' => 'test_target_course_level_shortname',
266 'indicators' => [
267 'test_indicator_max',
268 'test_indicator_min',
269 'test_indicator_fullname',
270 ],
271 ];
272
273 $declarationwithtimesplitting = array_merge($declaration, [
274 'timesplitting' => '\core\analytics\time_splitting\no_splitting',
275 ]);
276
277 $declarationwithtimesplittingenabled = array_merge($declarationwithtimesplitting, [
278 'enabled' => true,
279 ]);
280
281 // Check that no such model exists yet.
282 $target = \core_analytics\manager::get_target('test_target_course_level_shortname');
283 $this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
aa8af6fc 284 $this->assertFalse(\core_analytics\model::exists($target));
6187213f
DM
285
286 // Check that the model is created.
287 $created = \core_analytics\manager::create_declared_model($declaration);
288 $this->assertTrue($created instanceof \core_analytics\model);
289 $this->assertTrue(\core_analytics\model::exists($target));
290 $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
291 $modelid = $created->get_id();
292
293 // Check that created models are disabled by default.
294 $existing = new \core_analytics\model($modelid);
295 $this->assertEquals(0, $existing->get_model_obj()->enabled);
296 $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
297
298 // Let the admin enable the model.
299 $existing->enable('\core\analytics\time_splitting\no_splitting');
300 $this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
301
302 // Check that further calls create a new model.
303 $repeated = \core_analytics\manager::create_declared_model($declaration);
304 $this->assertTrue($repeated instanceof \core_analytics\model);
305 $this->assertEquals(2, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
306
307 // Delete the models.
308 $existing->delete();
309 $repeated->delete();
310 $this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
311 $this->assertFalse(\core_analytics\model::exists($target));
312
313 // Create it again, this time with time splitting method specified.
314 $created = \core_analytics\manager::create_declared_model($declarationwithtimesplitting);
315 $this->assertTrue($created instanceof \core_analytics\model);
316 $this->assertTrue(\core_analytics\model::exists($target));
317 $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
318 $modelid = $created->get_id();
319
320 // Even if the time splitting method was specified, the model is still not enabled automatically.
321 $existing = new \core_analytics\model($modelid);
322 $this->assertEquals(0, $existing->get_model_obj()->enabled);
323 $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
324 $existing->delete();
325
326 // Let's define the model so that it is enabled by default.
327 $enabled = \core_analytics\manager::create_declared_model($declarationwithtimesplittingenabled);
328 $this->assertTrue($enabled instanceof \core_analytics\model);
329 $this->assertTrue(\core_analytics\model::exists($target));
330 $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
331 $modelid = $enabled->get_id();
332 $existing = new \core_analytics\model($modelid);
333 $this->assertEquals(1, $existing->get_model_obj()->enabled);
334 $this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
335
336 // Let the admin disable the model.
337 $existing->update(0, false, false);
338 $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
339 }
f9222c49 340}