See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>.< /** < * Unit tests for the manager. < * < * @package core_analytics < * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com} < * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later < */> namespace core_analytics;defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/fixtures/test_indicator_max.php'); require_once(__DIR__ . '/fixtures/test_indicator_min.php'); require_once(__DIR__ . '/fixtures/test_indicator_fullname.php'); require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php'); /**< * Unit tests for the manager.> * Unit tests for the core_analytics manager.* * @package core_analytics * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later> * @covers \core_analytics\manager*/< class analytics_manager_testcase extends advanced_testcase {> class manager_test extends \advanced_testcase {/** * test_deleted_context */ public function test_deleted_context() { global $DB; $this->resetAfterTest(true); $this->setAdminuser(); set_config('enabled_stores', 'logstore_standard', 'tool_log'); $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname'); foreach ($indicators as $key => $indicator) { $indicators[$key] = \core_analytics\manager::get_indicator($indicator); } $model = \core_analytics\model::create($target, $indicators); $modelobj = $model->get_model_obj(); $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0)); $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0)); $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1)); $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1)); $model->enable('\core\analytics\time_splitting\no_splitting'); $model->train(); $model->predict(); // Generate a prediction action to confirm that it is deleted when there is an important update. $predictions = $DB->get_records('analytics_predictions'); $prediction = reset($predictions); $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used')); $prediction->action_executed(\core_analytics\prediction::ACTION_USEFUL, $model->get_target()); $predictioncontextid = $prediction->get_prediction_data()->contextid; $npredictions = $DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid)); $npredictionactions = $DB->count_records('analytics_prediction_actions', array('predictionid' => $prediction->get_prediction_data()->id)); $nindicatorcalc = $DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid)); \core_analytics\manager::cleanup(); // Nothing is incorrectly deleted. $this->assertEquals($npredictions, $DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid))); $this->assertEquals($npredictionactions, $DB->count_records('analytics_prediction_actions', array('predictionid' => $prediction->get_prediction_data()->id))); $this->assertEquals($nindicatorcalc, $DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid))); // Now we delete a context, the course predictions and prediction actions should be deleted. $deletedcontext = \context::instance_by_id($predictioncontextid); delete_course($deletedcontext->instanceid, false); \core_analytics\manager::cleanup(); $this->assertEmpty($DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid))); $this->assertEmpty($DB->count_records('analytics_prediction_actions', array('predictionid' => $prediction->get_prediction_data()->id))); $this->assertEmpty($DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid))); set_config('enabled_stores', '', 'tool_log'); get_log_manager(true); } /** * test_deleted_analysable */ public function test_deleted_analysable() { global $DB; $this->resetAfterTest(true); $this->setAdminuser(); set_config('enabled_stores', 'logstore_standard', 'tool_log'); $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname'); foreach ($indicators as $key => $indicator) { $indicators[$key] = \core_analytics\manager::get_indicator($indicator); } $model = \core_analytics\model::create($target, $indicators); $modelobj = $model->get_model_obj(); $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0)); $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0)); $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1)); $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1)); $model->enable('\core\analytics\time_splitting\no_splitting'); $model->train(); $model->predict(); $this->assertNotEmpty($DB->count_records('analytics_predict_samples')); $this->assertNotEmpty($DB->count_records('analytics_train_samples')); $this->assertNotEmpty($DB->count_records('analytics_used_analysables')); // Now we delete an analysable, stored predict and training samples should be deleted. $deletedcontext = \context_course::instance($coursepredict1->id); delete_course($coursepredict1, false); \core_analytics\manager::cleanup(); $this->assertEmpty($DB->count_records('analytics_predict_samples', array('analysableid' => $coursepredict1->id))); $this->assertEmpty($DB->count_records('analytics_train_samples', array('analysableid' => $coursepredict1->id))); $this->assertEmpty($DB->count_records('analytics_used_analysables', array('analysableid' => $coursepredict1->id))); set_config('enabled_stores', '', 'tool_log'); get_log_manager(true); } /** * Tests for the {@link \core_analytics\manager::load_default_models_for_component()} implementation. */ public function test_load_default_models_for_component() { $this->resetAfterTest(); // Attempting to load builtin models should always work without throwing exception. \core_analytics\manager::load_default_models_for_component('core'); // Attempting to load from a core subsystem without its own subsystem directory. $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_access')); // Attempting to load from a non-existing subsystem. $this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_nonexistingsubsystem')); // Attempting to load from a non-existing plugin of a known plugin type. $this->assertSame([], \core_analytics\manager::load_default_models_for_component('mod_foobarbazquaz12240996776')); // Attempting to load from a non-existing plugin type. $this->assertSame([], \core_analytics\manager::load_default_models_for_component('foo_bar2776327736558')); } /** * Tests for the {@link \core_analytics\manager::load_default_models_for_all_components()} implementation. */ public function test_load_default_models_for_all_components() { $this->resetAfterTest(); $models = \core_analytics\manager::load_default_models_for_all_components(); $this->assertTrue(is_array($models['core'])); $this->assertNotEmpty($models['core']); $this->assertNotEmpty($models['core'][0]['target']); $this->assertNotEmpty($models['core'][0]['indicators']); } /** * Tests for the successful execution of the {@link \core_analytics\manager::validate_models_declaration()}. */ public function test_validate_models_declaration() { $this->resetAfterTest(); // This is expected to run without an exception. $models = $this->load_models_from_fixture_file('no_teaching'); \core_analytics\manager::validate_models_declaration($models); } /** * Tests for the exceptions thrown by {@link \core_analytics\manager::validate_models_declaration()}. * * @dataProvider validate_models_declaration_exceptions_provider * @param array $models Models declaration. * @param string $exception Expected coding exception message. */ public function test_validate_models_declaration_exceptions(array $models, string $exception) { $this->resetAfterTest(); $this->expectException(\coding_exception::class); $this->expectExceptionMessage($exception); \core_analytics\manager::validate_models_declaration($models); } /** * Data provider for the {@link self::test_validate_models_declaration_exceptions()}. * * @return array of (string)testcase => [(array)models, (string)expected exception message] */ public function validate_models_declaration_exceptions_provider() { return [ 'missing_target' => [ $this->load_models_from_fixture_file('missing_target'), 'Missing target declaration', ], 'invalid_target' => [ $this->load_models_from_fixture_file('invalid_target'), 'Invalid target classname', ], 'missing_indicators' => [ $this->load_models_from_fixture_file('missing_indicators'), 'Missing indicators declaration', ], 'invalid_indicators' => [ $this->load_models_from_fixture_file('invalid_indicators'), 'Invalid indicator classname', ], 'invalid_time_splitting' => [ $this->load_models_from_fixture_file('invalid_time_splitting'), 'Invalid time splitting classname', ], 'invalid_time_splitting_fq' => [ $this->load_models_from_fixture_file('invalid_time_splitting_fq'), 'Expecting fully qualified time splitting classname', ], 'invalid_enabled' => [ $this->load_models_from_fixture_file('invalid_enabled'), 'Cannot enable a model without time splitting method specified', ], ]; } /** * Loads models as declared in the given fixture file. * * @param string $filename * @return array */ protected function load_models_from_fixture_file(string $filename) { global $CFG; $models = null; require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php'); return $models; } /** * Test the implementation of the {@link \core_analytics\manager::create_declared_model()}. */ public function test_create_declared_model() { global $DB; $this->resetAfterTest(); $this->setAdminuser(); $declaration = [ 'target' => 'test_target_course_level_shortname', 'indicators' => [ 'test_indicator_max', 'test_indicator_min', 'test_indicator_fullname', ], ]; $declarationwithtimesplitting = array_merge($declaration, [ 'timesplitting' => '\core\analytics\time_splitting\no_splitting', ]); $declarationwithtimesplittingenabled = array_merge($declarationwithtimesplitting, [ 'enabled' => true, ]); // Check that no such model exists yet. $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); $this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()])); $this->assertFalse(\core_analytics\model::exists($target)); // Check that the model is created. $created = \core_analytics\manager::create_declared_model($declaration); $this->assertTrue($created instanceof \core_analytics\model); $this->assertTrue(\core_analytics\model::exists($target)); $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()])); $modelid = $created->get_id(); // Check that created models are disabled by default. $existing = new \core_analytics\model($modelid); $this->assertEquals(0, $existing->get_model_obj()->enabled); $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); // Let the admin enable the model. $existing->enable('\core\analytics\time_splitting\no_splitting'); $this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); // Check that further calls create a new model. $repeated = \core_analytics\manager::create_declared_model($declaration); $this->assertTrue($repeated instanceof \core_analytics\model); $this->assertEquals(2, $DB->count_records('analytics_models', ['target' => $target->get_id()])); // Delete the models. $existing->delete(); $repeated->delete(); $this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()])); $this->assertFalse(\core_analytics\model::exists($target)); // Create it again, this time with time splitting method specified. $created = \core_analytics\manager::create_declared_model($declarationwithtimesplitting); $this->assertTrue($created instanceof \core_analytics\model); $this->assertTrue(\core_analytics\model::exists($target)); $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()])); $modelid = $created->get_id(); // Even if the time splitting method was specified, the model is still not enabled automatically. $existing = new \core_analytics\model($modelid); $this->assertEquals(0, $existing->get_model_obj()->enabled); $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); $existing->delete(); // Let's define the model so that it is enabled by default. $enabled = \core_analytics\manager::create_declared_model($declarationwithtimesplittingenabled); $this->assertTrue($enabled instanceof \core_analytics\model); $this->assertTrue(\core_analytics\model::exists($target)); $this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()])); $modelid = $enabled->get_id(); $existing = new \core_analytics\model($modelid); $this->assertEquals(1, $existing->get_model_obj()->enabled); $this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); // Let the admin disable the model. $existing->update(0, false, false); $this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST)); } /** * Test the implementation of the {@link \core_analytics\manager::update_default_models_for_component()}. */ public function test_update_default_models_for_component() { $this->resetAfterTest(); $this->setAdminuser(); $noteaching = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching'); $dropout = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout'); $upcomingactivities = \core_analytics\manager::get_target('\core_user\analytics\target\upcoming_activities_due'); $norecentaccesses = \core_analytics\manager::get_target('\core_course\analytics\target\no_recent_accesses'); $noaccesssincestart = \core_analytics\manager::get_target('\core_course\analytics\target\no_access_since_course_start'); $this->assertTrue(\core_analytics\model::exists($noteaching)); $this->assertTrue(\core_analytics\model::exists($dropout)); $this->assertTrue(\core_analytics\model::exists($upcomingactivities)); $this->assertTrue(\core_analytics\model::exists($norecentaccesses)); $this->assertTrue(\core_analytics\model::exists($noaccesssincestart)); foreach (\core_analytics\manager::get_all_models() as $model) { $model->delete(); } $this->assertFalse(\core_analytics\model::exists($noteaching)); $this->assertFalse(\core_analytics\model::exists($dropout)); $this->assertFalse(\core_analytics\model::exists($upcomingactivities)); $this->assertFalse(\core_analytics\model::exists($norecentaccesses)); $this->assertFalse(\core_analytics\model::exists($noaccesssincestart)); $updated = \core_analytics\manager::update_default_models_for_component('moodle'); $this->assertEquals(5, count($updated)); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(array_pop($updated) instanceof \core_analytics\model); $this->assertTrue(\core_analytics\model::exists($noteaching)); $this->assertTrue(\core_analytics\model::exists($dropout)); $this->assertTrue(\core_analytics\model::exists($upcomingactivities)); $this->assertTrue(\core_analytics\model::exists($norecentaccesses)); $this->assertTrue(\core_analytics\model::exists($noaccesssincestart)); $repeated = \core_analytics\manager::update_default_models_for_component('moodle'); $this->assertSame([], $repeated); } /** * test_get_time_splitting_methods description * @return null */ public function test_get_time_splitting_methods() { $this->resetAfterTest(true); $all = \core_analytics\manager::get_all_time_splittings(); $this->assertArrayHasKey('\core\analytics\time_splitting\upcoming_week', $all); $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $all); $allforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(true); $this->assertArrayNotHasKey('\core\analytics\time_splitting\upcoming_week', $allforevaluation); $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $allforevaluation); $defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false); $this->assertArrayNotHasKey('\core\analytics\time_splitting\upcoming_week', $defaultforevaluation); $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation); $sometimesplittings = '\core\analytics\time_splitting\single_range,' . '\core\analytics\time_splitting\tenths'; set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics'); $defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false); $this->assertArrayNotHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation); } /** * Test the implementation of the {@link \core_analytics\manager::model_declaration_identifier()}. */ public function test_model_declaration_identifier() { $noteaching1 = $this->load_models_from_fixture_file('no_teaching'); $noteaching2 = $this->load_models_from_fixture_file('no_teaching'); $noteaching3 = $this->load_models_from_fixture_file('no_teaching'); // Same model declaration should always lead to same identifier. $this->assertEquals( \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), \core_analytics\manager::model_declaration_identifier(reset($noteaching2)) ); // If something is changed, the identifier should change, too. $noteaching2[0]['target'] .= '_'; $this->assertNotEquals( \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), \core_analytics\manager::model_declaration_identifier(reset($noteaching2)) ); $noteaching3[0]['indicators'][] = '\core_analytics\local\indicator\binary'; $this->assertNotEquals( \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), \core_analytics\manager::model_declaration_identifier(reset($noteaching3)) ); // The identifier is supposed to contain PARAM_ALPHANUM only. $this->assertEquals( \core_analytics\manager::model_declaration_identifier(reset($noteaching1)), clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching1)), PARAM_ALPHANUM) ); $this->assertEquals( \core_analytics\manager::model_declaration_identifier(reset($noteaching2)), clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching2)), PARAM_ALPHANUM) ); $this->assertEquals( \core_analytics\manager::model_declaration_identifier(reset($noteaching3)), clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching3)), PARAM_ALPHANUM) ); } /** * Tests for the {@link \core_analytics\manager::get_declared_target_and_indicators_instances()}. */ public function test_get_declared_target_and_indicators_instances() { $this->resetAfterTest(); $definition = $this->load_models_from_fixture_file('no_teaching'); list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]); $this->assertTrue($target instanceof \core_analytics\local\target\base); $this->assertNotEmpty($indicators); $this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators); } /** * test_get_potential_context_restrictions description */ public function test_get_potential_context_restrictions() { $this->resetAfterTest(); // No potential context restrictions. $this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));> $defaultcategory = \core_course_category::get_default(); // Include the all context levels so the misc. category get included. > $defaultcategorycontext = $defaultcategory->get_context(); $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions()); >< $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions());> $this->assertEquals([ > $defaultcategorycontext->id => "Category: {$defaultcategory->name}", > ], manager::get_potential_context_restrictions()); > > $category = $this->getDataGenerator()->create_category(['name' => 'My category']); > $categorycontext = $category->get_context(); > > $courseone = $this->getDataGenerator()->create_course(['fullname' => 'Course one', 'shortname' => 'CS1']); > $courseonecontext = \context_course::instance($courseone->id); > > $coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Course two', 'shortname' => 'CS2']); > $coursetwocontext = \context_course::instance($coursetwo->id); > > // All context levels. > $this->assertEqualsCanonicalizing([ > $defaultcategorycontext->id => "Category: {$defaultcategory->name}", > $categorycontext->id => "Category: {$category->name}", > $courseonecontext->id => "Course: {$courseone->shortname}", > $coursetwocontext->id => "Course: {$coursetwo->shortname}", > ], manager::get_potential_context_restrictions()); > > // All category/course context levels. > $this->assertEqualsCanonicalizing([ > $defaultcategorycontext->id => "Category: {$defaultcategory->name}", > $categorycontext->id => "Category: {$category->name}", > $courseonecontext->id => "Course: {$courseone->shortname}", > $coursetwocontext->id => "Course: {$coursetwo->shortname}", > ], manager::get_potential_context_restrictions([CONTEXT_COURSECAT, CONTEXT_COURSE])); > > // All category context levels. > $this->assertEqualsCanonicalizing([ > $defaultcategorycontext->id => "Category: {$defaultcategory->name}", > $categorycontext->id => "Category: {$category->name}", > ], manager::get_potential_context_restrictions([CONTEXT_COURSECAT])); > > // Filtered category context levels. > $this->assertEquals([ > $categorycontext->id => "Category: {$category->name}", > ], manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'My cat')); > > $this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'nothing')); > > // All course context levels. > $this->assertEqualsCanonicalizing([ > $courseonecontext->id => "Course: {$courseone->shortname}", > $coursetwocontext->id => "Course: {$coursetwo->shortname}", > ], manager::get_potential_context_restrictions([CONTEXT_COURSE])); > > // Filtered course context levels. > $this->assertEquals([ > $courseonecontext->id => "Course: {$courseone->shortname}", > ], manager::get_potential_context_restrictions([CONTEXT_COURSE], 'one'));< $this->getDataGenerator()->create_course(); < $this->getDataGenerator()->create_category(); < $this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions()); < $this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE, CONTEXT_COURSECAT])); < < $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE])); < $this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT])); < < $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category')); < $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1')); < $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Miscellaneous')); < $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1')); < $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course'));> $this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSE], 'nothing'));} }