MDL-59212 analytics: Core indicators tests
authorDavid Monllao <davidm@moodle.com>
Thu, 22 Jun 2017 12:05:20 +0000 (14:05 +0200)
committerDavid Monllao <davidm@moodle.com>
Mon, 24 Jul 2017 06:37:01 +0000 (08:37 +0200)
Part of MDL-57791 epic.

13 files changed:
analytics/classes/calculable.php
analytics/classes/course.php
analytics/classes/local/indicator/community_of_inquiry_activity.php
analytics/classes/manager.php
analytics/classes/site.php
lib/classes/analytics/indicator/any_access_before_start.php
lib/classes/analytics/indicator/any_write_action.php
lib/classes/analytics/indicator/read_actions.php
lib/classes/analytics/target/course_dropout.php
lib/phpunit/classes/util.php
lib/tests/analysers_test.php [moved from analytics/tests/analysers_test.php with 97% similarity]
lib/tests/indicators_test.php [new file with mode: 0644]
lib/tests/time_splittings_test.php [moved from analytics/tests/time_splittings_test.php with 95% similarity]

index ca6c68d..72813d7 100644 (file)
@@ -163,10 +163,10 @@ abstract class calculable {
 
         $starttimedt = new \DateTime();
         $starttimedt->setTimestamp($starttime);
-        $starttimedt->setTimezone(\DateTimeZone::UTC);
+        $starttimedt->setTimezone(new \DateTimeZone('UTC'));
         $endtimedt = new \DateTime();
         $endtimedt->setTimestamp($endtime);
-        $endtimedt->setTimezone(\DateTimeZone::UTC);
+        $endtimedt->setTimezone(new \DateTimeZone('UTC'));
 
         $diff = $endtimedt->getTimestamp() - $starttimedt->getTimestamp();
         return $diff / WEEKSECS;
index d99d3af..aae5748 100644 (file)
@@ -169,6 +169,15 @@ class course implements \core_analytics\analysable {
         return self::$instances[$courseid];
     }
 
+    /**
+     * Clears all statically cached instances.
+     *
+     * @return void
+     */
+    public static function reset_caches() {
+        self::$instances = array();
+    }
+
     /**
      * get_id
      *
@@ -224,7 +233,9 @@ class course implements \core_analytics\analysable {
             return 0;
         }
 
-        $logstore = \core_analytics\manager::get_analytics_logstore();
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            return 0;
+        }
 
         // We first try to find current course student logs.
         $firstlogs = array();
@@ -431,8 +442,11 @@ class course implements \core_analytics\analysable {
 
         if ($this->ntotallogs === null) {
             list($filterselect, $filterparams) = $this->course_students_query_filter();
-            $logstore = \core_analytics\manager::get_analytics_logstore();
-            $this->ntotallogs = $logstore->get_events_select_count($filterselect, $filterparams);
+            if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+                $this->ntotallogs = 0;
+            } else {
+                $this->ntotallogs = $logstore->get_events_select_count($filterselect, $filterparams);
+            }
         }
 
         return $this->ntotallogs;
index 25e6c1b..f7dbcfd 100644 (file)
@@ -428,7 +428,9 @@ abstract class community_of_inquiry_activity extends linear {
         $params = $contextparams + array('starttime' => $starttime, 'endtime' => $endtime);
 
         // Pity that we need to pass through logging readers API when most of the people just uses the standard one.
-        $logstore = \core_analytics\manager::get_analytics_logstore();
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            throw new \coding_exception('No log store available');
+        }
         $events = $logstore->get_events_select_iterator($select, $params, 'timecreated ASC', 0, 0);
 
         // Returs the logs organised by contextid, userid and eventname so it is easier to calculate activities data later.
index 8029db5..afea72d 100644 (file)
@@ -305,7 +305,7 @@ class manager {
     /**
      * Returns the logstore used for analytics.
      *
-     * @return \core\log\sql_reader
+     * @return \core\log\sql_reader|false False if no log stores are enabled.
      */
     public static function get_analytics_logstore() {
         $readers = get_log_manager()->get_readers('core\log\sql_reader');
@@ -314,12 +314,17 @@ class manager {
             $logstore = reset($readers);
         } else if (!empty($readers[$analyticsstore])) {
             $logstore = $readers[$analyticsstore];
-        } else {
+        } else if (!empty($readers)) {
             $logstore = reset($readers);
             debugging('The selected log store for analytics is not available anymore. Using "' .
                 $logstore->get_name() . '"', DEBUG_DEVELOPER);
         }
 
+        if (empty($logstore)) {
+            debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
+            return false;
+        }
+
         if (!$logstore->is_logging()) {
             debugging('The selected log store for analytics "' . $logstore->get_name() .
                 '" is not logging activity logs', DEBUG_DEVELOPER);
index e84c00a..8e9a634 100644 (file)
@@ -73,7 +73,11 @@ class site implements \core_analytics\analysable {
             return $this->start;
         }
 
-        $logstore = \core_analytics\manager::get_analytics_logstore();
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            $this->start = 0;
+            return $this->start;
+        }
+
         // Basically a SELECT MIN(timecreated) FROM ...
         $events = $logstore->get_events_select("", array(), "timecreated ASC", 0, 1);
         if ($events) {
@@ -97,7 +101,11 @@ class site implements \core_analytics\analysable {
             return $this->end;
         }
 
-        $logstore = \core_analytics\manager::get_analytics_logstore();
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            $this->end = time();
+            return $this->end;
+        }
+
         // Basically a SELECT MAX(timecreated) FROM ...
         $events = $logstore->get_events_select("", array(), "timecreated DESC", 0, 1);
         if ($events) {
index 8d00aa6..e5cada0 100644 (file)
@@ -68,14 +68,17 @@ class any_access_before_start extends \core_analytics\local\indicator\binary {
         $user = $this->retrieve('user', $sampleid);
         $course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
 
-        // Filter by context to use the db table index.
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            throw new \coding_exception('No available log stores');
+        }
+
+        // Filter by context to use the logstore_standard_log db table index.
         $context = $this->retrieve('context', $sampleid);
         $select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
             "timecreated < :start";
         $params = array('userid' => $user->id, 'contextlevel' => $context->contextlevel,
             'contextinstanceid' => $context->instanceid, 'start' => $course->get_start());
 
-        $logstore = \core_analytics\manager::get_analytics_logstore();
         $nlogs = $logstore->get_events_select_count($select, $params);
         if ($nlogs) {
             return self::get_max_value();
index edfd2e1..21aab2c 100644 (file)
@@ -74,13 +74,26 @@ class any_write_action extends \core_analytics\local\indicator\binary {
             $params = $params + array('userid' => $user->id);
         }
 
-        // Filter by context to use the db table index.
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            throw new \coding_exception('No available log stores');
+        }
+
+        // Filter by context to use the logstore_standard_log db table index.
         $context = $this->retrieve('context', $sampleid);
         $select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
-            "(crud = 'c' OR crud = 'u') AND timecreated > :starttime AND timecreated <= :endtime";
+            "(crud = 'c' OR crud = 'u')";
         $params = $params + array('contextlevel' => $context->contextlevel,
-            'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
-        $logstore = \core_analytics\manager::get_analytics_logstore();
+            'contextinstanceid' => $context->instanceid);
+
+        if ($starttime) {
+            $select .= " AND timecreated > :starttime";
+            $params['starttime'] = $starttime;
+        }
+        if ($endtime) {
+            $select .= " AND timecreated <= :endtime";
+            $params['endtime'] = $endtime;
+        }
+
         $nlogs = $logstore->get_events_select_count($select, $params);
         if ($nlogs) {
             return self::get_max_value();
index 03f7315..77641fa 100644 (file)
@@ -65,6 +65,10 @@ class read_actions extends \core_analytics\local\indicator\linear {
      */
     protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
 
+        if (!$starttime || !$endtime) {
+            return null;
+        }
+
         $select = '';
         $params = array();
 
@@ -73,15 +77,17 @@ class read_actions extends \core_analytics\local\indicator\linear {
             $params = $params + array('userid' => $user->id);
         }
 
-        // Filter by context to use the db table index.
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            throw new \coding_exception('No available log stores');
+        }
+
+        // Filter by context to use the logstore_standard_log db table index.
         $context = $this->retrieve('context', $sampleid);
         $select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
             "crud = 'r' AND timecreated > :starttime AND timecreated <= :endtime";
         $params = $params + array('contextlevel' => $context->contextlevel,
             'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
-        $logstore = \core_analytics\manager::get_analytics_logstore();
         $nrecords = $logstore->get_events_select_count($select, $params);
-
         // We define a list of ranges to fit $nrecords into it
         // # Done absolutely nothing
         // # Not much really, just accessing the course once a week
index aeb79e0..80c8890 100644 (file)
@@ -160,7 +160,9 @@ class course_dropout extends \core_analytics\local\target\binary {
             $select = 'courseid = :courseid AND anonymous = :anonymous AND timecreated > :start AND timecreated < :end ' .
                 'AND userid ' . $studentssql;
 
-            $logstore = \core_analytics\manager::get_analytics_logstore();
+            if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+                throw new \coding_exception('No available log stores');
+            }
             $nlogs = $logstore->get_events_select_count($select, array_merge($params, $studentparams));
 
             // At least a minimum of students activity.
@@ -213,12 +215,15 @@ class course_dropout extends \core_analytics\local\target\binary {
             }
         }
 
+        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+            throw new \coding_exception('No available log stores');
+        }
+
         // No logs during the last quarter of the course.
         $courseduration = $course->get_end() - $course->get_start();
         $limit = intval($course->get_end() - ($courseduration / 4));
         $select = "courseid = :courseid AND userid = :userid AND timecreated > :limit";
         $params = array('userid' => $userenrol->userid, 'courseid' => $course->get_id(), 'limit' => $limit);
-        $logstore = \core_analytics\manager::get_analytics_logstore();
         $nlogs = $logstore->get_events_select_count($select, $params);
         if ($nlogs == 0) {
             return 0;
index 57c67c0..716ab3f 100644 (file)
@@ -220,6 +220,7 @@ class phpunit_util extends testing_util {
         if (class_exists('core_media_manager', false)) {
             core_media_manager::reset_caches();
         }
+        \core_analytics\course::reset_caches();
 
         // Reset static unit test options.
         if (class_exists('\availability_date\condition', false)) {
similarity index 97%
rename from analytics/tests/analysers_test.php
rename to lib/tests/analysers_test.php
index 7533eae..4c28a06 100644 (file)
 /**
  * Unit tests for core analysers.
  *
- * @package   core_analytics
+ * @package   core
+ * @category  test
  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
 require_once(__DIR__ . '/../../lib/enrollib.php');
 
 /**
  * Unit tests for core analysers.
  *
- * @package   core_analytics
+ * @package   core
+ * @category  test
  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class analytics_analysers_testcase extends advanced_testcase {
+class core_analytics_analysers_testcase extends advanced_testcase {
 
     /**
      * test_courses_analyser
diff --git a/lib/tests/indicators_test.php b/lib/tests/indicators_test.php
new file mode 100644 (file)
index 0000000..73f623d
--- /dev/null
@@ -0,0 +1,236 @@
+<?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 core indicators.
+ *
+ * @package   core
+ * @category  phpunit
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/../../admin/tool/log/store/standard/tests/fixtures/event.php');
+require_once(__DIR__ . '/../../lib/enrollib.php');
+
+/**
+ * Unit tests for core indicators.
+ *
+ * @package   core
+ * @category  phpunit
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_analytics_indicators_testcase extends advanced_testcase {
+
+    /**
+     * Test all core indicators.
+     *
+     * Single method as it is significantly faster (from 13s to 5s) than having separate
+     * methods because of preventResetByRollback.
+     *
+     * @return void
+     */
+    public function test_core_indicators() {
+
+        $this->preventResetByRollback();
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+
+        set_config('enabled_stores', 'logstore_standard', 'tool_log');
+        set_config('buffersize', 0, 'logstore_standard');
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        // Test any access after end.
+        $params = array(
+            'startdate' => mktime(0, 0, 0, 10, 24, 2015),
+            'enddate' => mktime(0, 0, 0, 10, 24, 2016)
+        );
+        $course = $this->getDataGenerator()->create_course($params);
+        $coursecontext = \context_course::instance($course->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+
+        $indicator = new \core\analytics\indicator\any_access_after_end();
+
+        $sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
+        $data = array($user1->id => array(
+            'context' => $coursecontext,
+            'course' => $course,
+            'user' => $user1
+        ));
+        $data[$user2->id] = $data[$user1->id];
+        $data[$user2->id]['user'] = $user2;
+        $indicator->add_sample_data($data);
+
+        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        \logstore_standard\event\unittest_executed::create(
+            array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
+        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        // Test any access before start.
+        $params = array(
+            'startdate' => 9999999998,
+            'enddate' => 9999999999
+        );
+        // Resetting $course var.
+        $course = $this->getDataGenerator()->create_course($params);
+        $coursecontext = \context_course::instance($course->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+
+        $indicator = new \core\analytics\indicator\any_access_before_start();
+
+        $sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
+        $data = array($user1->id => array(
+            'context' => $coursecontext,
+            'course' => $course,
+            'user' => $user1
+        ));
+        $data[$user2->id] = $data[$user1->id];
+        $data[$user2->id]['user'] = $user2;
+        $indicator->add_sample_data($data);
+
+        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        \logstore_standard\event\unittest_executed::create(
+            array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
+        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        // Test any write action.
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = \context_course::instance($course1->id);
+        $course2 = $this->getDataGenerator()->create_course();
+        $coursecontext2 = \context_course::instance($course2->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id);
+
+        $indicator = new \core\analytics\indicator\any_write_action();
+
+        $sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
+        $data = array($user1->id => array(
+            'context' => $coursecontext1,
+            'course' => $course1,
+            'user' => $user1
+        ));
+        $data[$user2->id] = $data[$user1->id];
+        $data[$user2->id]['user'] = $user2;
+        $indicator->add_sample_data($data);
+
+        $values = $indicator->calculate($sampleids, 'user');
+        $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        $beforecourseeventcreate = time();
+        sleep(1);
+
+        \logstore_standard\event\unittest_executed::create(
+            array('context' => $coursecontext1, 'userid' => $user1->id))->trigger();
+        $values = $indicator->calculate($sampleids, 'user');
+        $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        // Now try with course-level samples where user is not available.
+        $sampleids = array($course1->id => $course1->id, $course2->id => $course2->id);
+        $data = array(
+            $course1->id => array(
+                'context' => $coursecontext1,
+                'course' => $course1,
+            ),
+            $course2->id => array(
+                'context' => $coursecontext2,
+                'course' => $course2,
+            )
+        );
+        $indicator->clear_sample_data();
+        $indicator->add_sample_data($data);
+
+        // Limited by time to avoid previous logs interfering as other logs
+        // have been generated by the system.
+        $values = $indicator->calculate($sampleids, 'course', $beforecourseeventcreate);
+        $this->assertEquals($indicator::get_max_value(), $values[$course1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$course2->id][0]);
+
+        // Test read actions.
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = \context_course::instance($course->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+
+        $indicator = new \core\analytics\indicator\read_actions();
+
+        $sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
+        $data = array($user1->id => array(
+            'context' => $coursecontext,
+            'course' => $course,
+            'user' => $user1
+        ));
+        $data[$user2->id] = $data[$user1->id];
+        $data[$user2->id]['user'] = $user2;
+        $indicator->add_sample_data($data);
+
+        // More or less 4 weeks duration.
+        $startdate = time() - (WEEKSECS * 2);
+        $enddate = time() + (WEEKSECS * 2);
+
+        $this->setAdminUser();
+        $values = $indicator->calculate($sampleids, 'user');
+        $this->assertNull($values[$user1->id][0]);
+        $this->assertNull($values[$user1->id][1]);
+        $this->assertNull($values[$user1->id][0]);
+        $this->assertNull($values[$user2->id][1]);
+
+        // Zero score for 0 accesses.
+        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        // 1/3 score for more than 0 accesses.
+        \core\event\course_viewed::create(
+            array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
+        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        $this->assertEquals(-0.33, $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        // 2/3 score for more than 1 access per week.
+        for ($i = 0; $i < 12; $i++) {
+            \core\event\course_viewed::create(
+                array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
+        }
+        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        $this->assertEquals(0.33, $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+
+        // 100% score for tons of accesses during this period (3 logs per access * 4 weeks * 10 accesses).
+        for ($i = 0; $i < (3 * 10 * 4); $i++) {
+            \core\event\course_viewed::create(
+                array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
+        }
+        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
+    }
+}
similarity index 95%
rename from analytics/tests/time_splittings_test.php
rename to lib/tests/time_splittings_test.php
index 5e4c647..8210245 100644 (file)
 /**
  * Unit tests for core time splitting methods.
  *
- * @package   core_analytics
+ * @package   core
+ * @category  phpunit
  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
 require_once(__DIR__ . '/../../lib/enrollib.php');
 
 /**
  * Unit tests for core time splitting methods.
  *
- * @package   core_analytics
+ * @package   core
+ * @category  phpunit
  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class analytics_time_splittings_testcase extends advanced_testcase {
+class core_analytics_time_splittings_testcase extends advanced_testcase {
 
     /**
      * setUp
@@ -61,7 +63,7 @@ class analytics_time_splittings_testcase extends advanced_testcase {
      */
     public function test_valid_ranges() {
 
-        // All core_analytics time splitting methods.
+        // All core time splitting methods.
         $timesplittings = array(
             '\core\analytics\time_splitting\deciles',
             '\core\analytics\time_splitting\deciles_accum',