MDL-59212 analytics: Analyser and static methods tests
authorDavid Monllao <davidm@moodle.com>
Tue, 20 Jun 2017 12:05:48 +0000 (14:05 +0200)
committerDavid Monllao <davidm@moodle.com>
Mon, 24 Jul 2017 06:36:59 +0000 (08:36 +0200)
Part of MDL-57791 epic.

analytics/classes/local/analyser/student_enrolments.php
analytics/tests/analysers_test.php [new file with mode: 0644]
analytics/tests/fixtures/test_static_target_shortname.php
analytics/tests/prediction_test.php
lib/enrollib.php

index dbf26e6..acd56b9 100644 (file)
@@ -153,11 +153,6 @@ class student_enrolments extends by_course {
         // Some course enrolments.
         list($enrolsql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
 
-        $sql = "SELECT ue.id AS enrolmentid, u.* FROM {user_enrolments} ue
-                  JOIN {user} u on ue.userid = u.id
-                  WHERE ue.id $enrolsql";
-        $enrolments = $DB->get_recordset_sql($sql, $params);
-
         $samplesdata = array();
         foreach ($enrolments as $userenrolmentid => $user) {
 
diff --git a/analytics/tests/analysers_test.php b/analytics/tests/analysers_test.php
new file mode 100644 (file)
index 0000000..0ab3e1b
--- /dev/null
@@ -0,0 +1,176 @@
+<?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 analysers.
+ *
+ * @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
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/../../lib/enrollib.php');
+
+/**
+ * Unit tests for core analysers.
+ *
+ * @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
+ */
+class analytics_analysers_testcase extends advanced_testcase {
+
+    /**
+     * test_courses_analyser
+     *
+     * @return void
+     */
+    public function test_courses_analyser() {
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = \context_course::instance($course->id);
+
+        $target = new test_target_shortname();
+        $analyser = new \core_analytics\local\analyser\courses(1, $target, [], [], []);
+        $analysable = new \core_analytics\course($course);
+
+        $this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($course->id));
+
+        $this->assertInstanceOf('\context_course', $analyser->sample_access_context($course->id));
+
+        // Just 1 sample per course.
+        $class = new ReflectionClass('\core_analytics\local\analyser\courses');
+        $method = $class->getMethod('get_all_samples');
+        $method->setAccessible(true);
+        list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
+        $this->assertCount(1, $sampleids);
+        $sampleid = reset($sampleids);
+        $this->assertEquals($course->id, $sampleid);
+        $this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname);
+        $this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
+
+        // To compare it later.
+        $prevsampledata = $samplesdata[$sampleid];
+        list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
+        $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
+        $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
+    }
+
+    /**
+     * test_site_courses_analyser
+     *
+     * @return void
+     */
+    public function test_site_courses_analyser() {
+        $this->resetAfterTest(true);
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $course1context = \context_course::instance($course1->id);
+
+        $target = new test_target_shortname();
+        $analyser = new \core_analytics\local\analyser\site_courses(1, $target, [], [], []);
+        $analysable = new \core_analytics\site();
+
+        $this->assertInstanceOf('\core_analytics\site', $analyser->get_sample_analysable($course1->id));
+        $this->assertInstanceOf('\core_analytics\site', $analyser->get_sample_analysable($course2->id));
+
+        $this->assertInstanceOf('\context_system', $analyser->sample_access_context($course1->id));
+        $this->assertInstanceOf('\context_system', $analyser->sample_access_context($course3->id));
+
+        $class = new ReflectionClass('\core_analytics\local\analyser\site_courses');
+        $method = $class->getMethod('get_all_samples');
+        $method->setAccessible(true);
+        list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
+        $this->assertCount(3, $sampleids);
+
+        // Use course1 it does not really matter.
+        $this->assertArrayHasKey($course1->id, $sampleids);
+        $sampleid = $course1->id;
+        $this->assertEquals($course1->id, $sampleid);
+        $this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname);
+        $this->assertEquals($course1context, $samplesdata[$sampleid]['context']);
+
+        // To compare it later.
+        $prevsampledata = $samplesdata[$sampleid];
+        list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
+        $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
+        $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
+    }
+
+    /**
+     * test_site_courses_analyser
+     *
+     * @return void
+     */
+    public function test_student_enrolments_analyser() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = \context_course::instance($course->id);
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+
+        // Checking that suspended users are also included.
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
+        $ue1 = $DB->get_record('user_enrolments', array('userid' => $user1->id, 'enrolid' => $enrol->id));
+        $ue2 = $DB->get_record('user_enrolments', array('userid' => $user2->id, 'enrolid' => $enrol->id));
+
+        $target = new test_target_shortname();
+        $analyser = new \core_analytics\local\analyser\student_enrolments(1, $target, [], [], []);
+        $analysable = new \core_analytics\course($course);
+
+        $this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($ue1->id));
+        $this->assertInstanceOf('\context_course', $analyser->sample_access_context($ue1->id));
+
+        $class = new ReflectionClass('\core_analytics\local\analyser\student_enrolments');
+        $method = $class->getMethod('get_all_samples');
+        $method->setAccessible(true);
+        list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
+        // Only students.
+        $this->assertCount(2, $sampleids);
+
+        $this->assertArrayHasKey($ue1->id, $sampleids);
+        $this->assertArrayHasKey($ue2->id, $sampleids);
+
+        // Shouldn't matter which one we select.
+        $sampleid = $ue1->id;
+        $this->assertEquals($ue1, $samplesdata[$sampleid]['user_enrolments']);
+        $this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname);
+        $this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
+        $this->assertEquals($user1->firstname, $samplesdata[$sampleid]['user']->firstname);
+
+        // To compare it later.
+        $prevsampledata = $samplesdata[$sampleid];
+        list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
+        $this->assertEquals($prevsampledata['user_enrolments'], $samplesdata[$sampleid]['user_enrolments']);
+        $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
+        $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
+        $this->assertEquals($prevsampledata['user']->firstname, $samplesdata[$sampleid]['user']->firstname);
+    }
+}
index fe2e65c..518c744 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+require_once(__DIR__ . '/test_target_shortname.php');
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
  * Test static target.
  *
+ * Testing target extension to make it static and to use a different analyser
+ * just to try a different one. Method calculate_sample is exactly the same.
+ *
  * @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
  */
-class test_static_target_shortname extends \core_analytics\local\target\binary {
-
-    /**
-     * predictions
-     *
-     * @var array
-     */
-    protected $predictions = array();
+class test_static_target_shortname extends test_target_shortname {
 
     /**
-     * get_analyser_class
+     * based_on_assumptions
      *
-     * @return string
-     */
-    public function get_analyser_class() {
-        return '\core_analytics\local\analyser\site_courses';
-    }
-
-    /**
-     * classes_description
-     *
-     * @return string[]
-     */
-    public static function classes_description() {
-        return array(
-            'Course fullname first char is A',
-            'Course fullname first char is not A'
-        );
-    }
-
-    /**
-     * We don't want to discard results.
-     * @return null
-     */
-    protected function min_prediction_score() {
-        return null;
-    }
-
-    /**
-     * We don't want to discard results.
-     * @return array
-     */
-    protected function ignored_predicted_classes() {
-        return array();
-    }
-
-    /**
-     * is_valid_analysable
-     *
-     * @param \core_analytics\analysable $analysable
-     * @param bool $fortraining
-     * @return bool
-     */
-    public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
-        // This is testing, let's make things easy.
-        return true;
-    }
-
-    /**
-     * is_valid_sample
-     *
-     * @param int $sampleid
-     * @param \core_analytics\analysable $analysable
-     * @param bool $fortraining
      * @return bool
      */
-    public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
-        // We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
-        // In normal circumstances is_valid_sample will return false when they receive a sample that can not be
-        // processed.
-        if (!$fortraining) {
-            return true;
-        }
-
-        $sample = $this->retrieve('course', $sampleid);
-        if ($sample->visible == 0) {
-            return false;
-        }
+    public static function based_on_assumptions() {
         return true;
     }
 
     /**
-     * calculate_sample
+     * Different analyser just to test a different one.
      *
-     * @param int $sampleid
-     * @param \core_analytics\analysable $analysable
-     * @param int $starttime
-     * @param int $endtime
-     * @return float
+     * @return string
      */
-    protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
-        $sample = $this->retrieve('course', $sampleid);
-
-        $firstchar = substr($sample->shortname, 0, 1);
-        if ($firstchar === 'a') {
-            return 1;
-        } else {
-            return 0;
-        }
+    public function get_analyser_class() {
+        return '\core_analytics\local\analyser\courses';
     }
 }
index 6a41a08..12d3567 100644 (file)
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/fixtures/test_indicator_min.php');
 require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
 require_once(__DIR__ . '/fixtures/test_indicator_random.php');
 require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
 
 /**
  * Unit tests for evaluation, training and prediction.
@@ -39,6 +40,65 @@ require_once(__DIR__ . '/fixtures/test_target_shortname.php');
  */
 class core_analytics_prediction_testcase extends advanced_testcase {
 
+    /**
+     * test_static_prediction
+     *
+     * @return void
+     */
+    public function test_static_prediction() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+
+        $model = $this->add_perfect_model('test_static_target_shortname');
+        $model->enable('\\core_analytics\\local\\time_splitting\\no_splitting');
+        $this->assertEquals(1, $model->is_enabled());
+        $this->assertEquals(1, $model->is_trained());
+
+        // No training for static models.
+        $results = $model->train();
+        $trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
+        $this->assertEmpty($trainedsamples);
+        $this->assertEmpty($DB->count_records('analytics_used_files',
+            array('modelid' => $model->get_id(), 'action' => 'trained')));
+
+        // Now we create 2 hidden courses (only hidden courses are getting predictions).
+        $courseparams = array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
+        $course1 = $this->getDataGenerator()->create_course($courseparams);
+        $courseparams = array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
+        $course2 = $this->getDataGenerator()->create_course($courseparams);
+
+        $result = $model->predict();
+
+        // Var $course1 predictions should be 1 == 'a', $course2 predictions should be 0 == 'b'.
+        $correct = array($course1->id => 1, $course2->id => 0);
+        foreach ($result->predictions as $uniquesampleid => $predictiondata) {
+            list($sampleid, $rangeindex) = $model->get_time_splitting()->infer_sample_info($uniquesampleid);
+
+            // The range index is not important here, both ranges prediction will be the same.
+            $this->assertEquals($correct[$sampleid], $predictiondata->prediction);
+        }
+
+        // 1 range for each analysable.
+        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
+        $this->assertCount(2, $predictedranges);
+        $this->assertEquals(1, $DB->count_records('analytics_used_files',
+            array('modelid' => $model->get_id(), 'action' => 'predicted')));
+        // 2 predictions for each range.
+        $this->assertEquals(2, $DB->count_records('analytics_predictions',
+            array('modelid' => $model->get_id())));
+
+        // No new generated files nor records as there are no new courses available.
+        $model->predict();
+        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
+        $this->assertCount(2, $predictedranges);
+        $this->assertEquals(1, $DB->count_records('analytics_used_files',
+            array('modelid' => $model->get_id(), 'action' => 'predicted')));
+        $this->assertEquals(2, $DB->count_records('analytics_predictions',
+            array('modelid' => $model->get_id())));
+    }
+
     /**
      * test_ml_training_and_prediction
      *
@@ -88,30 +148,22 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $this->assertEquals(0, $DB->count_records('analytics_train_samples', array('modelid' => $model->get_id())));
 
         $results = $model->train();
-        $this->assertEquals(1, $model->get_model_obj()->enabled);
-        $this->assertEquals(1, $model->get_model_obj()->trained);
+        $this->assertEquals(1, $model->is_enabled());
+        $this->assertEquals(1, $model->is_trained());
 
         // 1 training file was created.
         $trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
-        $this->assertEquals(1, count($trainedsamples));
+        $this->assertCount(1, $trainedsamples);
         $samples = json_decode(reset($trainedsamples)->sampleids, true);
-        $this->assertEquals($ncourses * 2, count($samples));
+        $this->assertCount($ncourses * 2, $samples);
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'trained')));
 
-        // Now we create 2 hidden courses (they should not be used for training by the target).
         $courseparams = $params + array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
         $course1 = $this->getDataGenerator()->create_course($courseparams);
         $courseparams = $params + array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
         $course2 = $this->getDataGenerator()->create_course($courseparams);
 
-        // No more files should be created as the 2 new courses should be skipped by the target (not ready for training).
-        $results = $model->train();
-        $trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
-        $this->assertEquals(1, count($trainedsamples));
-        $this->assertEquals(1, $DB->count_records('analytics_used_files',
-            array('modelid' => $model->get_id(), 'action' => 'trained')));
-
         // They will not be skipped for prediction though.
         $result = $model->predict();
 
@@ -125,8 +177,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         }
 
         // 2 ranges will be predicted.
-        $trainedsamples = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
-        $this->assertEquals($npredictedranges, count($trainedsamples));
+        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
+        $this->assertCount($npredictedranges, $predictedranges);
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'predicted')));
         // 2 predictions for each range.
@@ -135,8 +187,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
 
         // No new generated files nor records as there are no new courses available.
         $model->predict();
-        $trainedsamples = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
-        $this->assertEquals($npredictedranges, count($trainedsamples));
+        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
+        $this->assertCount($npredictedranges, $predictedranges);
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'predicted')));
         $this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions',
@@ -287,11 +339,12 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     /**
      * add_perfect_model
      *
+     * @param string $targetclass
      * @return \core_analytics\model
      */
-    protected function add_perfect_model() {
+    protected function add_perfect_model($targetclass = 'test_target_shortname') {
 
-        $target = \core_analytics\manager::get_target('test_target_shortname');
+        $target = \core_analytics\manager::get_target($targetclass);
         $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
         foreach ($indicators as $key => $indicator) {
             $indicators[$key] = \core_analytics\manager::get_indicator($indicator);
index 01c3140..7157bc5 100644 (file)
@@ -1476,7 +1476,7 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi
     }
 
     if ($uefilter) {
-        list($uesql, $ueparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
+        list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
         $conditions[] = "ue.id $uesql";
         $params = $params + $ueparams;
     }