Merge branch 'MDL-64506' of git://github.com/Chocolate-lightning/moodle
authorSara Arjona <sara@moodle.com>
Wed, 3 Apr 2019 17:30:28 +0000 (19:30 +0200)
committerSara Arjona <sara@moodle.com>
Wed, 3 Apr 2019 17:30:28 +0000 (19:30 +0200)
100 files changed:
admin/settings/server.php
admin/tool/analytics/amd/build/model.min.js
admin/tool/analytics/amd/src/model.js
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/export_options.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/metadata_registry_test.php
admin/tool/log/tests/privacy_test.php
analytics/classes/model.php
analytics/classes/model_config.php
analytics/tests/prediction_test.php
composer.json
composer.lock
config-dist.php
lang/en/admin.php
lang/en/moodle.php
lib/classes/analytics/target/course_competencies.php [new file with mode: 0644]
lib/classes/dml/table.php [new file with mode: 0644]
lib/classes/session/manager.php
lib/ddl/database_manager.php
lib/dml/moodle_database.php
lib/dml/tests/dml_table_test.php [new file with mode: 0644]
lib/dml/tests/dml_test.php
lib/filestorage/tests/file_storage_test.php
lib/filestorage/tests/file_system_filedir_test.php
lib/filestorage/tests/file_system_test.php
lib/mlbackend/python/classes/processor.php
lib/mlbackend/python/lang/en/mlbackend_python.php
lib/mlbackend/python/tests/processor_test.php [new file with mode: 0644]
lib/moodlelib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/autoloader.php
lib/phpunit/classes/basic_testcase.php
lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php
lib/phpunit/classes/database_driver_testcase.php
lib/phpunit/classes/hint_resultprinter.php
lib/templates/email_html.mustache
lib/templates/email_subject.mustache
lib/templates/email_text.mustache
lib/tests/behat/action_modal.feature
lib/tests/completionlib_test.php
lib/tests/message_test.php
lib/tests/session_manager_test.php
lib/tests/targets_test.php
lib/upgradelib.php
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/templates/message_drawer_view_contact_body_content.mustache
message/templates/message_drawer_view_contacts_body_section_requests_list.mustache
message/templates/message_drawer_view_contacts_header.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/templates/message_drawer_view_conversation_header_placeholder.mustache
message/templates/message_drawer_view_group_info_body_content.mustache
message/templates/message_drawer_view_group_info_participants_list.mustache
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_search_header.mustache
message/templates/message_drawer_view_settings_header.mustache
message/templates/message_popover.mustache
message/templates/notification_preferences_processor.mustache
message/tests/behat/message_drawer_manage_contacts.feature
mod/assign/tests/events_test.php
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/assign/tests/privacy_test.php
mod/feedback/classes/complete_form.php
mod/forum/classes/local/vaults/discussion_list.php
mod/forum/classes/local/vaults/forum.php
mod/forum/classes/local/vaults/preprocessors/extract_record.php
mod/lesson/pagetypes/shortanswer.php
mod/quiz/tests/attempt_walkthrough_from_csv_test.php
mod/workshop/tests/privacy_provider_test.php
phpunit.xml.dist
privacy/tests/approved_contextlist_test.php
privacy/tests/approved_userlist_test.php
privacy/tests/collection_test.php
privacy/tests/contextlist_base_test.php
privacy/tests/contextlist_collection_test.php
privacy/tests/contextlist_test.php
privacy/tests/legacy_polyfill_test.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/provider_test.php
privacy/tests/request_helper_test.php
privacy/tests/request_transform_test.php
privacy/tests/types_database_table_test.php
privacy/tests/types_external_location_test.php
privacy/tests/types_plugintype_link_test.php
privacy/tests/types_subsystem_link_test.php
privacy/tests/types_user_preference_test.php
privacy/tests/userlist_base_test.php
privacy/tests/userlist_collection.php
privacy/tests/userlist_test.php
privacy/tests/writer_test.php
question/type/multichoice/tests/helper.php
question/type/multichoice/tests/upgradelibnewqe_test.php

index 8c47a56..a869ce6 100644 (file)
@@ -354,6 +354,8 @@ $choices = array(new lang_string('never', 'admin'),
                  new lang_string('onlynoreply', 'admin'));
 $temp->add(new admin_setting_configselect('emailfromvia', new lang_string('emailfromvia', 'admin'),
           new lang_string('configemailfromvia', 'admin'), 1, $choices));
+    $temp->add(new admin_setting_configtext('emailsubjectprefix', new lang_string('emailsubjectprefix', 'admin'),
+        new lang_string('configemailsubjectprefix', 'admin'), '', PARAM_RAW));
 
 $ADMIN->add('email', $temp);
 
index 180805d..9e958db 100644 (file)
Binary files a/admin/tool/analytics/amd/build/model.min.js and b/admin/tool/analytics/amd/build/model.min.js differ
index f39c577..8d35e86 100644 (file)
@@ -149,6 +149,71 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
                         return;
                     });
 
+                    modal.show();
+                    return modal;
+                }).fail(Notification.exception);
+            });
+        },
+
+        /**
+         * Displays export options.
+         *
+         * We have two main options: export training data and export configuration.
+         * The 2nd option has an extra option: include the trained algorithm weights.
+         *
+         * @param  {String}  actionId
+         * @param  {Boolean} isTrained
+         */
+        selectExportOptions: function(actionId, isTrained) {
+            $('[data-action-id="' + actionId + '"]').on('click', function(ev) {
+                ev.preventDefault();
+
+                var a = $(ev.currentTarget);
+
+                if (!isTrained) {
+                    // Export the model configuration if the model is not trained. We can't export anything else.
+                    a.attr('href', a.attr('href') + '&action=exportmodel&includeweights=0');
+                    window.location.href = a.attr('href');
+                    return;
+                }
+
+                var stringsPromise = Str.get_strings([
+                    {
+                        key: 'export',
+                        component: 'tool_analytics'
+                    }
+                ]);
+                var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
+                var bodyPromise = Templates.render('tool_analytics/export_options', {});
+
+                $.when(stringsPromise, modalPromise).then(function(strings, modal) {
+
+                    modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));
+
+                    modal.setTitle(strings[0]);
+                    modal.setSaveButtonText(strings[0]);
+                    modal.setBody(bodyPromise);
+
+                    modal.getRoot().on(ModalEvents.save, function() {
+
+                        var exportOption = $("input[name='exportoption']:checked").val();
+
+                        if (exportOption == 'exportdata') {
+                            a.attr('href', a.attr('href') + '&action=exportdata');
+
+                        } else {
+                            a.attr('href', a.attr('href') + '&action=exportmodel');
+                            if ($("#id-includeweights").is(':checked')) {
+                                a.attr('href', a.attr('href') + '&includeweights=1');
+                            } else {
+                                a.attr('href', a.attr('href') + '&includeweights=0');
+                            }
+                        }
+
+                        window.location.href = a.attr('href');
+                        return;
+                    });
+
                     modal.show();
                     return modal;
                 }).fail(Notification.exception);
index 351dae4..5152b6d 100644 (file)
@@ -235,22 +235,27 @@ class models_list implements \renderable, \templatable {
                 $actionsmenu->add($icon);
             }
 
-            // Export training data.
-            if (!$model->is_static() && $model->is_trained()) {
-                $urlparams['action'] = 'exportdata';
-                $url = new \moodle_url('model.php', $urlparams);
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
-                    get_string('exporttrainingdata', 'tool_analytics')), get_string('exporttrainingdata', 'tool_analytics'));
-                $actionsmenu->add($icon);
-            }
+            // Export.
+            if (!$model->is_static()) {
 
-            // Export model.
-            if (!$model->is_static() && $model->get_indicators() && !empty($modeldata->timesplitting)) {
-                $urlparams['action'] = 'exportmodel';
-                $url = new \moodle_url('model.php', $urlparams);
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/backup',
-                    get_string('exportmodel', 'tool_analytics')), get_string('exportmodel', 'tool_analytics'));
-                $actionsmenu->add($icon);
+                $fullysetup = $model->get_indicators() && !empty($modeldata->timesplitting);
+                $istrained = $model->is_trained();
+
+                if ($fullysetup || $istrained) {
+
+                    $url = new \moodle_url('model.php', $urlparams);
+                    // Clear the previous action param from the URL, we will set it in JS.
+                    $url->remove_params('action');
+
+                    $actionid = 'export-' . $model->get_id();
+                    $PAGE->requires->js_call_amd('tool_analytics/model', 'selectExportOptions',
+                        [$actionid, $istrained]);
+
+                    $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
+                        get_string('export', 'tool_analytics')), get_string('export', 'tool_analytics'),
+                        ['data-action-id' => $actionid]);
+                    $actionsmenu->add($icon);
+                }
             }
 
             // Invalid analysables.
index 354dc96..dc0c5c9 100644 (file)
@@ -66,6 +66,8 @@ $string['evaluationmodecoltrainedmodel'] = 'Trained model';
 $string['evaluationmodecolconfiguration'] = 'Configuration';
 $string['evaluationmodeconfiguration'] = 'Evaluate the model configuration';
 $string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
+$string['export'] = 'Export';
+$string['exportincludeweights'] = 'Include the weights of the trained model';
 $string['exportmodel'] = 'Export configuration';
 $string['exporttrainingdata'] = 'Export training data';
 $string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) time-splitting method';
index a58b5d7..3cca64e 100644 (file)
@@ -235,8 +235,11 @@ switch ($action) {
         break;
 
     case 'exportmodel':
+
+        $includeweights = optional_param('includeweights', 1, PARAM_INT);
+
         $zipfilename = 'model-' . $model->get_unique_id() . '-' . microtime(false) . '.zip';
-        $zipfilepath = $model->export_model($zipfilename);
+        $zipfilepath = $model->export_model($zipfilename, $includeweights);
         send_temp_file($zipfilepath, $zipfilename);
         break;
 
diff --git a/admin/tool/analytics/templates/export_options.mustache b/admin/tool/analytics/templates/export_options.mustache
new file mode 100644 (file)
index 0000000..b7180ab
--- /dev/null
@@ -0,0 +1,57 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_analytics/export_options
+
+    Export options.
+
+    The purpose of this template is to render the exporting options.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="exportoption" id="id-mode-exportdata" value="exportdata">
+    <label class="form-check-label" for="id-mode-exportdata">{{#str}} exporttrainingdata, tool_analytics {{/str}}</label>
+</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="exportoption" id="id-mode-exportmodel" value="exportmodel" checked>
+    <label class="form-check-label" for="id-mode-exportmodel">{{#str}} exportmodel, tool_analytics {{/str}}</label>
+</div>
+<div class="form-check m-l-2" id="id-includeweights-container">
+  <input class="form-check-input" type="checkbox" id="id-includeweights" value="1" checked>
+  <label class="form-check-label" for="id-includeweights">{{#str}} exportincludeweights, tool_analytics {{/str}}</label>
+</div>
+
+{{#js}}
+    require(['jquery'], function($) {
+        $("input[name='exportoption']:radio").change(function() {
+            if ($(this).val() == 'exportdata') {
+                $('#id-includeweights-container').hide();
+            } else {
+                $('#id-includeweights-container').show();
+            }
+        });
+    });
+{{/js}}
index 2fc88a4..143d6b8 100644 (file)
@@ -100,6 +100,6 @@ class tool_dataprivacy_metadata_registry_testcase extends advanced_testcase {
         $this->assertEquals(1, $corerating['compliant']);
         $this->assertNotEmpty($corerating['metadata']);
         $this->assertEquals('database_table', $corerating['metadata'][0]['type']);
-        $this->assertNotEmpty('database_table', $corerating['metadata'][0]['fields']);
+        $this->assertNotEmpty($corerating['metadata'][0]['fields']);
     }
 }
index 9e5be71..ae85b86 100644 (file)
@@ -66,7 +66,7 @@ class tool_log_privacy_testcase extends provider_testcase {
         $manager = get_log_manager(true);
 
         $this->setUser($u1);
-        $this->assertEmpty(provider::get_contexts_for_userid($u1->id)->get_contextids(), []);
+        $this->assertEmpty(provider::get_contexts_for_userid($u1->id)->get_contextids());
         $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
         $e->trigger();
         $this->assertEquals($c1ctx->id, provider::get_contexts_for_userid($u1->id)->get_contextids()[0]);
index c0da4c0..5ca4f2c 100644 (file)
@@ -1414,14 +1414,15 @@ class model {
      * Exports the model data to a zip file.
      *
      * @param string $zipfilename
+     * @param bool $includeweights Include the model weights if available
      * @return string Zip file path
      */
-    public function export_model(string $zipfilename) : string {
+    public function export_model(string $zipfilename, bool $includeweights = true) : string {
 
         \core_analytics\manager::check_can_manage_models();
 
         $modelconfig = new model_config($this);
-        return $modelconfig->export($zipfilename);
+        return $modelconfig->export($zipfilename, $includeweights);
     }
 
     /**
index 2cb32cc..75b31f1 100644 (file)
@@ -58,9 +58,10 @@ class model_config {
      * Exports a model to a zip using the provided file name.
      *
      * @param string $zipfilename
+     * @param bool $includeweights Include the model weights if available
      * @return string
      */
-    public function export(string $zipfilename) : string {
+    public function export(string $zipfilename, bool $includeweights = true) : string {
 
         if (!$this->model) {
             throw new \coding_exception('No model object provided.');
@@ -84,7 +85,7 @@ class model_config {
         $zipfiles[self::CONFIG_FILE_NAME] = $jsonfilepath;
 
         // ML backend.
-        if ($this->model->is_trained()) {
+        if ($includeweights && $this->model->is_trained()) {
             $processor = $this->model->get_predictions_processor(true);
             $outputdir = $this->model->get_output_dir(array('execution'));
             $mlbackenddir = $processor->export($this->model->get_unique_id(), $outputdir);
index 7728648..3dea376 100644 (file)
@@ -305,6 +305,10 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $zipfilename = 'model-zip-' . microtime() . '.zip';
         $zipfilepath = $model->export_model($zipfilename);
 
+        $modelconfig = new \core_analytics\model_config();
+        list($modelconfig, $mlbackend) = $modelconfig->extract_import_contents($zipfilepath);
+        $this->assertNotFalse($mlbackend);
+
         $importmodel = \core_analytics\model::import_model($zipfilepath);
         $importmodel->enable();
 
@@ -317,6 +321,13 @@ class core_analytics_prediction_testcase extends advanced_testcase {
 
         $this->assertFalse($importmodel->trained_locally());
 
+        $zipfilename = 'model-zip-' . microtime() . '.zip';
+        $zipfilepath = $model->export_model($zipfilename, false);
+
+        $modelconfig = new \core_analytics\model_config();
+        list($modelconfig, $mlbackend) = $modelconfig->extract_import_contents($zipfilepath);
+        $this->assertFalse($mlbackend);
+
         set_config('enabled_stores', '', 'tool_log');
         get_log_manager(true);
     }
index 006768a..6dc963d 100644 (file)
@@ -5,8 +5,8 @@
     "type": "project",
     "homepage": "https://moodle.org",
     "require-dev": {
-        "phpunit/phpunit": "6.5.*",
-        "phpunit/dbunit": "3.0.*",
+        "phpunit/phpunit": "7.5.*",
+        "phpunit/dbunit": "4.0.*",
         "moodlehq/behat-extension": "3.37.0",
         "mikey179/vfsstream": "^1.6"
     }
index 7179371..30f9518 100644 (file)
@@ -1,10 +1,10 @@
 {
     "_readme": [
         "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "fafc7b770f55956d32d202645be66abf",
+    "content-hash": "3517a4473544055cd8523bb076cad8f6",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/gherkin",
-            "version": "v4.5.1",
+            "version": "v4.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Gherkin.git",
-                "reference": "74ac03d52c5e23ad8abd5c5cce4ab0e8dc1b530a"
+                "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/74ac03d52c5e23ad8abd5c5cce4ab0e8dc1b530a",
-                "reference": "74ac03d52c5e23ad8abd5c5cce4ab0e8dc1b530a",
+                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07",
+                "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "phpunit/phpunit": "~4.5|~5",
-                "symfony/phpunit-bridge": "~2.7|~3",
-                "symfony/yaml": "~2.3|~3"
+                "symfony/phpunit-bridge": "~2.7|~3|~4",
+                "symfony/yaml": "~2.3|~3|~4"
             },
             "suggest": {
                 "symfony/yaml": "If you want to parse features, represented in YAML files"
                 "gherkin",
                 "parser"
             ],
-            "time": "2017-08-30T11:04:43+00:00"
+            "time": "2019-01-16T14:22:17+00:00"
         },
         {
             "name": "behat/mink",
         },
         {
             "name": "doctrine/instantiator",
-            "version": "1.1.0",
+            "version": "1.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda"
+                "reference": "a2c590166b2133a4633738648b6b064edae0814a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
-                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+                "reference": "a2c590166b2133a4633738648b6b064edae0814a",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.1"
             },
             "require-dev": {
-                "athletic/athletic": "~0.1.8",
+                "doctrine/coding-standard": "^6.0",
                 "ext-pdo": "*",
                 "ext-phar": "*",
-                "phpunit/phpunit": "^6.2.3",
-                "squizlabs/php_codesniffer": "^3.0.2"
+                "phpbench/phpbench": "^0.13",
+                "phpstan/phpstan-phpunit": "^0.11",
+                "phpstan/phpstan-shim": "^0.11",
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 }
             ],
             "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
-            "homepage": "https://github.com/doctrine/instantiator",
+            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
             "keywords": [
                 "constructor",
                 "instantiate"
             ],
-            "time": "2017-07-22T11:58:36+00:00"
+            "time": "2019-03-17T17:37:11+00:00"
         },
         {
             "name": "fabpot/goutte",
         },
         {
             "name": "phar-io/manifest",
-            "version": "1.0.1",
+            "version": "1.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/manifest.git",
-                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
-                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-phar": "*",
-                "phar-io/version": "^1.0.1",
+                "phar-io/version": "^2.0",
                 "php": "^5.6 || ^7.0"
             },
             "type": "library",
                 }
             ],
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
-            "time": "2017-03-05T18:14:27+00:00"
+            "time": "2018-07-08T19:23:20+00:00"
         },
         {
             "name": "phar-io/version",
-            "version": "1.0.1",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/version.git",
-                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
-                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Library for handling version information and constraints",
-            "time": "2017-03-05T17:38:23+00:00"
+            "time": "2018-07-08T19:19:57+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
         },
         {
             "name": "phpunit/dbunit",
-            "version": "3.0.3",
+            "version": "4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/dbunit.git",
-                "reference": "0fa4329e490480ab957fe7b1185ea0996ca11f44"
+                "reference": "e77b469c3962b5a563f09a2a989f1c9bd38b8615"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/0fa4329e490480ab957fe7b1185ea0996ca11f44",
-                "reference": "0fa4329e490480ab957fe7b1185ea0996ca11f44",
+                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/e77b469c3962b5a563f09a2a989f1c9bd38b8615",
+                "reference": "e77b469c3962b5a563f09a2a989f1c9bd38b8615",
                 "shasum": ""
             },
             "require": {
                 "ext-pdo": "*",
                 "ext-simplexml": "*",
-                "php": "^7.0",
-                "phpunit/phpunit": "^6.0",
+                "php": "^7.1",
+                "phpunit/phpunit": "^7.0",
                 "symfony/yaml": "^3.0 || ^4.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0.x-dev"
+                    "dev-master": "4.0-dev"
                 }
             },
             "autoload": {
                 "xunit"
             ],
             "abandoned": true,
-            "time": "2018-01-23T13:32:26+00:00"
+            "time": "2018-02-07T06:47:59+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.3.2",
+            "version": "6.1.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
+                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
-                "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-xmlwriter": "*",
-                "php": "^7.0",
-                "phpunit/php-file-iterator": "^1.4.2",
+                "php": "^7.1",
+                "phpunit/php-file-iterator": "^2.0",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^2.0.1",
+                "phpunit/php-token-stream": "^3.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
-                "sebastian/environment": "^3.0",
+                "sebastian/environment": "^3.1 || ^4.0",
                 "sebastian/version": "^2.0.1",
                 "theseer/tokenizer": "^1.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "phpunit/phpunit": "^7.0"
             },
             "suggest": {
-                "ext-xdebug": "^2.5.5"
+                "ext-xdebug": "^2.6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.3.x-dev"
+                    "dev-master": "6.1-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-04-06T15:36:58+00:00"
+            "time": "2018-10-31T16:06:48+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.5",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+                "reference": "050bedf145a257b1ff02746c31894800e5122946"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
-                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
+                "reference": "050bedf145a257b1ff02746c31894800e5122946",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4.x-dev"
+                    "dev-master": "2.0.x-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
                 "filesystem",
                 "iterator"
             ],
-            "time": "2017-11-27T13:52:08+00:00"
+            "time": "2018-09-13T20:33:42+00:00"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "phpunit/php-timer",
-            "version": "1.0.9",
+            "version": "2.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+                "reference": "8b389aebe1b8b0578430bda0c7c95a829608e059"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
-                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b389aebe1b8b0578430bda0c7c95a829608e059",
+                "reference": "8b389aebe1b8b0578430bda0c7c95a829608e059",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "2.1-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
             "keywords": [
                 "timer"
             ],
-            "time": "2017-02-26T11:10:40+00:00"
+            "time": "2019-02-20T10:12:59+00:00"
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "2.0.2",
+            "version": "3.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "791198a2c6254db10131eecfe8c06670700904db"
+                "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
-                "reference": "791198a2c6254db10131eecfe8c06670700904db",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18",
+                "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2.4"
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2017-11-27T05:48:46+00:00"
+            "time": "2018-10-30T05:52:18+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.5.13",
+            "version": "7.5.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
+                "reference": "eb343b86753d26de07ecba7868fa983104361948"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
-                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/eb343b86753d26de07ecba7868fa983104361948",
+                "reference": "eb343b86753d26de07ecba7868fa983104361948",
                 "shasum": ""
             },
             "require": {
+                "doctrine/instantiator": "^1.1",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
                 "ext-xml": "*",
-                "myclabs/deep-copy": "^1.6.1",
-                "phar-io/manifest": "^1.0.1",
-                "phar-io/version": "^1.0",
-                "php": "^7.0",
+                "myclabs/deep-copy": "^1.7",
+                "phar-io/manifest": "^1.0.2",
+                "phar-io/version": "^2.0",
+                "php": "^7.1",
                 "phpspec/prophecy": "^1.7",
-                "phpunit/php-code-coverage": "^5.3",
-                "phpunit/php-file-iterator": "^1.4.3",
+                "phpunit/php-code-coverage": "^6.0.7",
+                "phpunit/php-file-iterator": "^2.0.1",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-timer": "^1.0.9",
-                "phpunit/phpunit-mock-objects": "^5.0.9",
-                "sebastian/comparator": "^2.1",
-                "sebastian/diff": "^2.0",
-                "sebastian/environment": "^3.1",
+                "phpunit/php-timer": "^2.1",
+                "sebastian/comparator": "^3.0",
+                "sebastian/diff": "^3.0",
+                "sebastian/environment": "^4.0",
                 "sebastian/exporter": "^3.1",
                 "sebastian/global-state": "^2.0",
                 "sebastian/object-enumerator": "^3.0.3",
-                "sebastian/resource-operations": "^1.0",
+                "sebastian/resource-operations": "^2.0",
                 "sebastian/version": "^2.0.1"
             },
             "conflict": {
-                "phpdocumentor/reflection-docblock": "3.0.2",
-                "phpunit/dbunit": "<3.0"
+                "phpunit/phpunit-mock-objects": "*"
             },
             "require-dev": {
                 "ext-pdo": "*"
             },
             "suggest": {
+                "ext-soap": "*",
                 "ext-xdebug": "*",
-                "phpunit/php-invoker": "^1.1"
+                "phpunit/php-invoker": "^2.0"
             },
             "bin": [
                 "phpunit"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.5.x-dev"
+                    "dev-master": "7.5-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-09-08T15:10:43+00:00"
-        },
-        {
-            "name": "phpunit/phpunit-mock-objects",
-            "version": "5.0.10",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
-                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.0.5",
-                "php": "^7.0",
-                "phpunit/php-text-template": "^1.2.1",
-                "sebastian/exporter": "^3.1"
-            },
-            "conflict": {
-                "phpunit/phpunit": "<6.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^6.5.11"
-            },
-            "suggest": {
-                "ext-soap": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Mock Object library for PHPUnit",
-            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
-            "keywords": [
-                "mock",
-                "xunit"
-            ],
-            "time": "2018-08-09T05:50:03+00:00"
+            "time": "2019-03-16T07:31:17+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "sebastian/comparator",
-            "version": "2.1.3",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
-                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
-                "sebastian/diff": "^2.0 || ^3.0",
+                "php": "^7.1",
+                "sebastian/diff": "^3.0",
                 "sebastian/exporter": "^3.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.4"
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.1.x-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
                 "compare",
                 "equality"
             ],
-            "time": "2018-02-01T13:46:46+00:00"
+            "time": "2018-07-12T15:12:46+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "2.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
+                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
-                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2"
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "symfony/process": "^2 || ^3.3 || ^4"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
             "description": "Diff implementation",
             "homepage": "https://github.com/sebastianbergmann/diff",
             "keywords": [
-                "diff"
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
             ],
-            "time": "2017-08-03T08:09:46+00:00"
+            "time": "2019-02-04T06:01:07+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "3.1.0",
+            "version": "4.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
+                "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
-                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6fda8ce1974b62b14935adc02a9ed38252eca656",
+                "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.1"
+                "phpunit/phpunit": "^7.5"
+            },
+            "suggest": {
+                "ext-posix": "*"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1.x-dev"
+                    "dev-master": "4.1-dev"
                 }
             },
             "autoload": {
                 "environment",
                 "hhvm"
             ],
-            "time": "2017-07-01T08:51:00+00:00"
+            "time": "2019-02-01T05:27:49+00:00"
         },
         {
             "name": "sebastian/exporter",
         },
         {
             "name": "sebastian/resource-operations",
-            "version": "1.0.0",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/resource-operations.git",
-                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
-                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.6.0"
+                "php": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Provides a list of PHP built-in functions that operate on resources",
             "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "time": "2015-07-28T20:34:47+00:00"
+            "time": "2018-10-04T04:07:39+00:00"
         },
         {
             "name": "sebastian/version",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v4.2.1",
+            "version": "v4.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "db7e59fec9c82d45e745eb500e6ede2d96f4a6e9"
+                "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/db7e59fec9c82d45e745eb500e6ede2d96f4a6e9",
-                "reference": "db7e59fec9c82d45e745eb500e6ede2d96f4a6e9",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/61d85c5af2fc058014c7c89504c3944e73a086f0",
+                "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-26T11:49:31+00:00"
+            "time": "2019-02-23T15:17:42+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.20",
+            "version": "v3.4.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "420458095cf60025eb0841276717e0da7f75e50e"
+                "reference": "4459eef5298dedfb69f771186a580062b8516497"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/420458095cf60025eb0841276717e0da7f75e50e",
-                "reference": "420458095cf60025eb0841276717e0da7f75e50e",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/4459eef5298dedfb69f771186a580062b8516497",
+                "reference": "4459eef5298dedfb69f771186a580062b8516497",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-11T19:48:54+00:00"
+            "time": "2019-01-16T09:39:14+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.20",
+            "version": "v3.4.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "8a660daeb65dedbe0b099529f65e61866c055081"
+                "reference": "177a276c01575253c95cefe0866e3d1b57637fe0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/8a660daeb65dedbe0b099529f65e61866c055081",
-                "reference": "8a660daeb65dedbe0b099529f65e61866c055081",
+                "url": "https://api.github.com/repos/symfony/config/zipball/177a276c01575253c95cefe0866e3d1b57637fe0",
+                "reference": "177a276c01575253c95cefe0866e3d1b57637fe0",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-26T10:17:44+00:00"
+            "time": "2019-02-23T15:06:07+00:00"
         },
         {
             "name": "symfony/console",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.20",
+            "version": "v3.4.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "345b9a48595d1ab9630db791dbc3e721bf0233e8"
+                "reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/345b9a48595d1ab9630db791dbc3e721bf0233e8",
-                "reference": "345b9a48595d1ab9630db791dbc3e721bf0233e8",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
+                "reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-11T19:48:54+00:00"
+            "time": "2019-01-16T09:39:14+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.20",
+            "version": "v3.4.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "a2233f555ddf55e5600f386fba7781cea1cb82d3"
+                "reference": "8d8a9e877b3fcdc50ddecf8dcea146059753f782"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/a2233f555ddf55e5600f386fba7781cea1cb82d3",
-                "reference": "a2233f555ddf55e5600f386fba7781cea1cb82d3",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/8d8a9e877b3fcdc50ddecf8dcea146059753f782",
+                "reference": "8d8a9e877b3fcdc50ddecf8dcea146059753f782",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-27T12:43:10+00:00"
+            "time": "2019-02-24T15:45:11+00:00"
         },
         {
             "name": "symfony/dependency-injection",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.2.1",
+            "version": "v4.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "7438a32108fdd555295f443605d6de2cce473159"
+                "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7438a32108fdd555295f443605d6de2cce473159",
-                "reference": "7438a32108fdd555295f443605d6de2cce473159",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb",
+                "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-26T10:55:26+00:00"
+            "time": "2019-02-23T15:17:42+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.20",
+            "version": "v3.4.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "cc35e84adbb15c26ae6868e1efbf705a917be6b5"
+                "reference": "ec625e2fff7f584eeb91754821807317b2e79236"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/cc35e84adbb15c26ae6868e1efbf705a917be6b5",
-                "reference": "cc35e84adbb15c26ae6868e1efbf705a917be6b5",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ec625e2fff7f584eeb91754821807317b2e79236",
+                "reference": "ec625e2fff7f584eeb91754821807317b2e79236",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-30T18:07:24+00:00"
+            "time": "2019-02-23T15:06:07+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.2.1",
+            "version": "v4.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710"
+                "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/2f4c8b999b3b7cadb2a69390b01af70886753710",
-                "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601",
+                "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-11T19:52:12+00:00"
+            "time": "2019-02-07T11:40:08+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.10.0",
+            "version": "v1.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+                "reference": "82ebae02209c21113908c229e9883c419720738a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
-                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
+                "reference": "82ebae02209c21113908c229e9883c419720738a",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.11-dev"
                 }
             },
             "autoload": {
                 },
                 {
                     "name": "Gert de Pagter",
-                    "email": "BackEndTea@gmail.com"
+                    "email": "backendtea@gmail.com"
                 }
             ],
             "description": "Symfony polyfill for ctype functions",
                 "polyfill",
                 "portable"
             ],
-            "time": "2018-08-06T14:22:27+00:00"
+            "time": "2019-02-06T07:57:58+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.10.0",
+            "version": "v1.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+                "reference": "fe5e94c604826c35a32fa832f35bd036b6799609"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609",
+                "reference": "fe5e94c604826c35a32fa832f35bd036b6799609",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.11-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2018-09-21T13:07:52+00:00"
+            "time": "2019-02-06T07:57:58+00:00"
         },
         {
             "name": "symfony/process",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.3.0",
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "0df1908962e7a3071564e857d86874dad1ef204a"
+                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
-                "reference": "0df1908962e7a3071564e857d86874dad1ef204a",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
+                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0"
+                "php": "^5.3.3 || ^7.0",
+                "symfony/polyfill-ctype": "^1.8"
             },
             "require-dev": {
                 "phpunit/phpunit": "^4.6",
                 "check",
                 "validate"
             ],
-            "time": "2018-01-29T19:49:41+00:00"
+            "time": "2018-12-25T11:19:39+00:00"
         }
     ],
     "aliases": [],
index 201a907..5f4258a 100644 (file)
@@ -614,6 +614,12 @@ $CFG->admin = 'admin';
 //
 //      $CFG->expectedcronfrequency = 200;
 //
+// Session lock warning threshold. Long running pages should release the session using \core\session\manager::write_close().
+// Set this threshold to any value greater than 0 to add developer warnings when a page locks the session for too long.
+// The session should rarely be locked for more than 1 second. The input should be in seconds and may be a float.
+//
+//      $CFG->debugsessionlock = 5;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index c8b2644..b5964e9 100644 (file)
@@ -221,6 +221,7 @@ $string['configeditordictionary'] = 'This value will be used if aspell doesn\'t
 $string['configeditorfontlist'] = 'Select the fonts that should appear in the editor\'s drop-down list.';
 $string['configemailchangeconfirmation'] = 'Require an email confirmation step when users change their email address in their profile.';
 $string['configemailfromvia'] = 'Add via information in the "From" section of outgoing email. This informs the recipient from where this email came from and also helps combat recipients accidentally replying to no-reply email addresses.';
+$string['configemailsubjectprefix'] = 'Text to be prefixed to the subject line of all outgoing mail.';
 $string['configenablecalendarexport'] = 'Enable exporting or subscribing to calendars.';
 $string['configenablecomments'] = 'Enable comments';
 $string['configenablecourserequests'] = 'This will allow any user to request a course be created.';
@@ -497,6 +498,7 @@ $string['editorspellinghelp'] = 'Enable or disable spell-checking. When enabled,
 $string['editstrings'] = 'Edit words or phrases';
 $string['emailchangeconfirmation'] = 'Email change confirmation';
 $string['emailfromvia'] = 'Email via information';
+$string['emailsubjectprefix'] = 'Email subject prefix text';
 $string['emoticontext'] = 'Text';
 $string['emoticonimagename'] = 'Image name';
 $string['emoticonalt'] = 'Alternative text';
index 03dac10..5fb1e31 100644 (file)
@@ -1972,12 +1972,16 @@ $string['tagmanagement'] = 'Add/delete tags ...';
 $string['tags'] = 'Tags';
 $string['target:coursecompletion'] = 'Students at risk of not meeting the course completion conditions';
 $string['target:coursecompletion_help'] = 'This target describes whether the student is considered at risk of not meeting the course completion conditions.';
+$string['target:coursecompetencies'] = 'Students at risk of not achieving the competencies assigned to a course';
+$string['target:coursecompetencies_help'] = 'This target describes whether a student is at risk of not achieving the competencies assigned to a course. This target considers that all competencies assigned to the course must be achieved by the end of the course.';
 $string['target:coursedropout'] = 'Students at risk of dropping out';
 $string['target:coursedropout_help'] = 'This target describes whether the student is considered at risk of dropping out.';
 $string['target:noteachingactivity'] = 'No teaching';
 $string['target:noteachingactivity_help'] = 'This target describes whether courses due to start in the coming week will have teaching activity.';
 $string['targetlabelstudentcompletionno'] = 'Student who is likely to meet the course completion conditions';
 $string['targetlabelstudentcompletionyes'] = 'Student at risk of not meeting the course completion conditions';
+$string['targetlabelstudentcompetenciesno'] = 'Student who is likely to achieve the competencies assigned to a course';
+$string['targetlabelstudentcompetenciesyes'] = 'Student at risk of not achieving the competencies assigned to a course';
 $string['targetlabelstudentdropoutyes'] = 'Student at risk of dropping out';
 $string['targetlabelstudentdropoutno'] = 'Not at risk';
 $string['targetlabelteachingyes'] = 'Users with teaching capabilities have access to the course';
diff --git a/lib/classes/analytics/target/course_competencies.php b/lib/classes/analytics/target/course_competencies.php
new file mode 100644 (file)
index 0000000..b8a6c71
--- /dev/null
@@ -0,0 +1,136 @@
+<?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/>.
+
+/**
+ * Course competencies achievement target.
+ *
+ * @package   core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\analytics\target;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Course competencies achievement target.
+ *
+ * @package   core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_competencies extends \core\analytics\target\course_enrolments {
+
+    /**
+     * Number of competencies assigned per course.
+     * @var int[]
+     */
+    protected $coursecompetencies = array();
+
+    /**
+     * Count the competencies in a course.
+     *
+     * Save the value in $coursecompetencies array to prevent new accesses to the database.
+     *
+     * @param int $courseid The course id.
+     * @return int Number of competencies assigned to the course.
+     */
+    protected function get_num_competencies_in_course ($courseid) {
+
+        if (!isset($this->coursecompetencies[$courseid])) {
+            $ccs = \core_competency\api::count_competencies_in_course($courseid);
+            // Save the number of competencies per course to avoid another database access in calculate_sample().
+            $this->coursecompetencies[$courseid] = $ccs;
+        } else {
+            $ccs = $this->coursecompetencies[$courseid];
+        }
+        return $ccs;
+    }
+
+    /**
+     * Returns the name.
+     *
+     * If there is a corresponding '_help' string this will be shown as well.
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        return new \lang_string('target:coursecompetencies');
+    }
+
+    /**
+     * Returns descriptions for each of the values the target calculation can return.
+     *
+     * @return string[]
+     */
+    protected static function classes_description() {
+        return array(
+            get_string('targetlabelstudentcompetenciesno'),
+            get_string('targetlabelstudentcompetenciesyes'),
+        );
+    }
+
+    /**
+     * Discards courses that are not yet ready to be used for training or prediction.
+     *
+     * @param \core_analytics\analysable $course
+     * @param bool $fortraining
+     * @return true|string
+     */
+    public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
+        $isvalid = parent::is_valid_analysable($course, $fortraining);
+
+        if (is_string($isvalid)) {
+            return $isvalid;
+        }
+
+        $ccs = $this->get_num_competencies_in_course($course->get_id());
+
+        if (!$ccs) {
+            return get_string('nocompetenciesincourse', 'tool_lp');
+        }
+
+        return true;
+    }
+
+    /**
+     * To have the proficiency or not in each of the competencies assigned to the course sets the target value.
+     *
+     * @param int $sampleid
+     * @param \core_analytics\analysable $course
+     * @param int $starttime
+     * @param int $endtime
+     * @return float 0 -> competencies achieved, 1 -> competencies not achieved
+     */
+    protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
+
+        $userenrol = $this->retrieve('user_enrolments', $sampleid);
+
+        $key = $course->get_id();
+        // Number of competencies in the course.
+        $ccs = $this->get_num_competencies_in_course($key);
+        // Number of proficient competencies in the same course for the user.
+        $ucs = \core_competency\api::count_proficient_competencies_in_course_for_user($key, $userenrol->userid);
+
+        // If they are the equals, the user achieved all the competencies assigned to the course.
+        if ($ccs == $ucs) {
+            return 0;
+        }
+
+        return 1;
+    }
+}
diff --git a/lib/classes/dml/table.php b/lib/classes/dml/table.php
new file mode 100644 (file)
index 0000000..3cf86d0
--- /dev/null
@@ -0,0 +1,133 @@
+<?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/>.
+
+/**
+ * Helpers and methods relating to DML tables.
+ *
+ * @since      Moodle 3.7
+ * @package    core
+ * @category   dml
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\dml;
+
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helpers and methods relating to DML tables.
+ *
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class table {
+
+    /** @var string Name of the table that this class represents */
+    protected $tablename;
+
+    /** @var string Table alias */
+    protected $tablealias;
+
+    /** @var string Prefix to place before each field */
+    protected $fieldprefix;
+
+    /** @var array List of fields */
+    protected $fields;
+
+    /**
+     * Constructor for the table class.
+     *
+     * @param   string  $tablename The name of the table that this instance represents.
+     * @param   string  $tablealias The alias to use when selecting the table
+     * @param   string  $fieldprefix The prefix to use when selecting fields.
+     */
+    public function __construct(string $tablename, string $tablealias, string $fieldprefix) {
+        $this->tablename = $tablename;
+        $this->tablealias = $tablealias;
+        $this->fieldprefix = $fieldprefix;
+    }
+
+    /**
+     * Get the from TABLE ALIAS part of the FROM/JOIN string.
+     *
+     * @return  string
+     */
+    public function get_from_sql() : string {
+        return "{{$this->tablename}} {$this->tablealias}";
+    }
+
+    /**
+     * Get the list of fields in a table for use in preloading fields.
+     *
+     * @return  array       The list of columns in a table. The array key is the column name with an applied prefix.
+     */
+    protected function get_fieldlist() : array {
+        global $DB;
+
+        if (null === $this->fields) {
+            $fields = [];
+            foreach (array_keys($DB->get_columns($this->tablename)) as $fieldname) {
+                $fields["{$this->fieldprefix}{$fieldname}"] = $fieldname;
+            }
+
+            $this->fields = $fields;
+        }
+
+        return $this->fields;
+    }
+
+    /**
+     * Get the SELECT SQL to select a set of columns for this table.
+     *
+     * This function is intended to be used in combination with extract_from_result().
+     *
+     * @return  string      The SQL to use in the SELECT
+     */
+    public function get_field_select() : string {
+        $fieldlist = $this->get_fieldlist();
+
+        return implode(', ', array_map(function($fieldname, $fieldalias) {
+            return "{$this->tablealias}.{$fieldname} AS {$fieldalias}";
+        }, $fieldlist, array_keys($fieldlist)));
+    }
+
+    /**
+     * Extract fields from the specified result. The fields are removed from the original object.
+     *
+     * This function is intended to be used in combination with get_field_select().
+     *
+     * @param   stdClass    $result The result retrieved from the database with fields to be extracted
+     * @return  stdClass    The extracted result
+     */
+    public function extract_from_result(stdClass $result) : stdClass {
+        $record = new stdClass();
+
+        $fieldlist = $this->get_fieldlist();
+        foreach ($fieldlist as $fieldalias => $fieldname) {
+            if (property_exists($result, $fieldalias)) {
+                $record->$fieldname = $result->$fieldalias;
+                unset($result->$fieldalias);
+            } else {
+                debugging("Field '{$fieldname}' not found", DEBUG_DEVELOPER);
+            }
+        }
+
+        return $record;
+    }
+}
index a84424c..875ff82 100644 (file)
@@ -26,6 +26,8 @@ namespace core\session;
 
 defined('MOODLE_INTERNAL') || die();
 
+use html_writer;
+
 /**
  * Session manager, this is the public Moodle API for sessions.
  *
@@ -55,13 +57,15 @@ class manager {
      * Note: This is intended to be called only from lib/setup.php!
      */
     public static function start() {
-        global $CFG, $DB;
+        global $CFG, $DB, $PERF;
 
         if (isset(self::$sessionactive)) {
             debugging('Session was already started!', DEBUG_DEVELOPER);
             return;
         }
 
+        // Grab the time before session lock starts.
+        $PERF->sessionlock['start'] = microtime(true);
         self::load_handler();
 
         // Init the session handler only if everything initialised properly in lib/setup.php file
@@ -82,6 +86,9 @@ class manager {
                 throw new \core\session\exception(get_string('servererror'));
             }
 
+            // Grab the time when session lock starts.
+            $PERF->sessionlock['gained'] = microtime(true);
+            $PERF->sessionlock['wait'] = $PERF->sessionlock['gained'] - $PERF->sessionlock['start'];
             self::initialise_user_session($isnewsession);
             self::$sessionactive = true; // Set here, so the session can be cleared if the security check fails.
             self::check_security();
@@ -109,6 +116,8 @@ class manager {
      * @return array perf info
      */
     public static function get_performance_info() {
+        global $CFG, $PERF;
+
         if (!session_id()) {
             return array();
         }
@@ -119,9 +128,26 @@ class manager {
 
         $info = array();
         $info['size'] = $size;
-        $info['html'] = "<span class=\"sessionsize\">Session ($handler): $size</span> ";
+        $info['html'] = html_writer::div("Session ($handler): $size", "sessionsize");
         $info['txt'] = "Session ($handler): $size ";
 
+        if (!empty($CFG->debugsessionlock)) {
+            $sessionlock = self::get_session_lock_info();
+            if (!empty($sessionlock['held'])) {
+                // The page displays the footer and the session has been closed.
+                $sessionlocktext = "Session lock held: ".number_format($sessionlock['held'], 3)." secs";
+            } else {
+                // The session hasn't yet been closed and so we assume now with microtime.
+                $sessionlockheld = microtime(true) - $PERF->sessionlock['gained'];
+                $sessionlocktext = "Session lock open: ".number_format($sessionlockheld, 3)." secs";
+            }
+            $info['txt'] .= $sessionlocktext;
+            $info['html'] .= html_writer::div($sessionlocktext, "sessionlockstart");
+            $sessionlockwaittext = "Session lock wait: ".number_format($sessionlock['wait'], 3)." secs";
+            $info['txt'] .= $sessionlockwaittext;
+            $info['html'] .= html_writer::div($sessionlockwaittext, "sessionlockwait");
+        }
+
         return $info;
     }
 
@@ -530,6 +556,19 @@ class manager {
      * Unblocks the sessions, other scripts may start executing in parallel.
      */
     public static function write_close() {
+        global $PERF;
+
+        if (self::$sessionactive) {
+            // Grab the time when session lock is released.
+            $PERF->sessionlock['released'] = microtime(true);
+            if (!empty($PERF->sessionlock['gained'])) {
+                $PERF->sessionlock['held'] = $PERF->sessionlock['released'] - $PERF->sessionlock['gained'];
+            }
+            $PERF->sessionlock['url'] = me();
+            self::update_recent_session_locks($PERF->sessionlock);
+            self::sessionlock_debugging();
+        }
+
         if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
             // More control over whether session data
             // is persisted or not.
@@ -1024,4 +1063,134 @@ class manager {
         }
         return true;
     }
+
+    /**
+     * Get the recent session locks array.
+     *
+     * @return array Recent session locks array.
+     */
+    public static function get_recent_session_locks() {
+        global $SESSION;
+
+        if (!isset($SESSION->recentsessionlocks)) {
+            // This will hold the pages that blocks other page.
+            $SESSION->recentsessionlocks = array();
+        }
+
+        return $SESSION->recentsessionlocks;
+    }
+
+    /**
+     * Updates the recent session locks.
+     *
+     * This function will store session lock info of all the pages visited.
+     *
+     * @param array $sessionlock Session lock array.
+     */
+    public static function update_recent_session_locks($sessionlock) {
+        global $CFG, $SESSION;
+
+        if (empty($CFG->debugsessionlock)) {
+            return;
+        }
+
+        $SESSION->recentsessionlocks = self::get_recent_session_locks();
+        array_push($SESSION->recentsessionlocks, $sessionlock);
+
+        self::cleanup_recent_session_locks();
+    }
+
+    /**
+     * Reset recent session locks array if there is a 10 seconds time gap.
+     *
+     * @return array Recent session locks array.
+     */
+    public static function cleanup_recent_session_locks() {
+        global $SESSION;
+
+        $locks = self::get_recent_session_locks();
+        if (count($locks) > 2) {
+            for ($i = count($locks) - 1; $i > 0; $i--) {
+                // Calculate the gap between session locks.
+                $gap = $locks[$i]['released'] - $locks[$i - 1]['start'];
+                if ($gap >= 10) {
+                    // Remove previous locks if the gap is 10 seconds or more.
+                    $SESSION->recentsessionlocks = array_slice($locks, $i);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Get the page that blocks other pages at a specific timestamp.
+     *
+     * Look for a page whose lock was gained before that timestamp, and released after that timestamp.
+     *
+     * @param  float $time Time before session lock starts.
+     * @return array|null
+     */
+    public static function get_locked_page_at($time) {
+        $recentsessionlocks = self::get_recent_session_locks();
+        foreach ($recentsessionlocks as $recentsessionlock) {
+            if ($time >= $recentsessionlock['gained'] &&
+                $time <= $recentsessionlock['released']) {
+                return $recentsessionlock;
+            }
+        }
+    }
+
+    /**
+     * Display the page which blocks other pages.
+     *
+     * @return string
+     */
+    public static function display_blocking_page() {
+        global $PERF;
+
+        $page = self::get_locked_page_at($PERF->sessionlock['start']);
+        $output = "Script ".me()." was blocked for ";
+        $output .= number_format($PERF->sessionlock['wait'], 3);
+        if ($page != null) {
+            $output .= " second(s) by script: ";
+            $output .= $page['url'];
+        } else {
+            $output .= " second(s) by an unknown script.";
+        }
+
+        return $output;
+    }
+
+    /**
+     * Get session lock info of the current page.
+     *
+     * @return array
+     */
+    public static function get_session_lock_info() {
+        global $PERF;
+
+        if (!isset($PERF->sessionlock)) {
+            return null;
+        }
+        return $PERF->sessionlock;
+    }
+
+    /**
+     * Display debugging info about slow and blocked script.
+     */
+    public static function sessionlock_debugging() {
+        global $CFG, $PERF;
+
+        if (!empty($CFG->debugsessionlock)) {
+            if (isset($PERF->sessionlock['held']) && $PERF->sessionlock['held'] > $CFG->debugsessionlock) {
+                debugging("Script ".me()." locked the session for ".number_format($PERF->sessionlock['held'], 3)
+                ." seconds, it should close the session using \core\session\manager::write_close().", DEBUG_NORMAL);
+            }
+
+            if (isset($PERF->sessionlock['wait']) && $PERF->sessionlock['wait'] > $CFG->debugsessionlock) {
+                $output = self::display_blocking_page();
+                debugging($output, DEBUG_DEVELOPER);
+            }
+        }
+    }
 }
index 41d7cda..24feb65 100644 (file)
@@ -898,6 +898,27 @@ class database_manager {
         $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
     }
 
+    /**
+     * Get the list of install.xml files.
+     *
+     * @return array
+     */
+    public function get_install_xml_files(): array {
+        global $CFG;
+        require_once($CFG->libdir.'/adminlib.php');
+
+        $files = [];
+        $dbdirs = get_db_directories();
+        foreach ($dbdirs as $dbdir) {
+            $filename = "{$dbdir}/install.xml";
+            if (file_exists($filename)) {
+                $files[] = $filename;
+            }
+        }
+
+        return $files;
+    }
+
     /**
      * Reads the install.xml files for Moodle core and modules and returns an array of
      * xmldb_structure object with xmldb_table from these files.
@@ -909,10 +930,10 @@ class database_manager {
 
         $schema = new xmldb_structure('export');
         $schema->setVersion($CFG->version);
-        $dbdirs = get_db_directories();
-        foreach ($dbdirs as $dbdir) {
-            $xmldb_file = new xmldb_file($dbdir.'/install.xml');
-            if (!$xmldb_file->fileExists() or !$xmldb_file->loadXMLStructure()) {
+
+        foreach ($this->get_install_xml_file_list() as $filename)  {
+            $xmldb_file = new xmldb_file($filename);
+            if (!$xmldb_file->loadXMLStructure()) {
                 continue;
             }
             $structure = $xmldb_file->getStructure();
index 72d78b2..9e781cd 100644 (file)
@@ -828,65 +828,6 @@ abstract class moodle_database {
         return array($sql, $params);
     }
 
-    /**
-     * Get the SELECT SQL to preload columns for the specified fieldlist and table alias.
-     *
-     * This function is intended to be used in combination with get_preload_columns_sql and extract_from_fields.
-     *
-     * @param   array       $fieldlist The list of fields from get_preload_columns
-     * @param   string      $tablealias The table alias used in the FROM/JOIN field
-     * @return  string      The SQL to use in the SELECT
-     */
-    public function get_preload_columns_sql(array $fieldlist, string $tablealias) : string {
-        return implode(', ', array_map(function($fieldname, $alias) use ($tablealias) {
-            return "{$tablealias}.{$fieldname} AS {$alias}";
-        }, $fieldlist, array_keys($fieldlist)));
-    }
-
-    /**
-     * Extract fields from the specified data.
-     * The fields are removed from the original object.
-     *
-     * This function is intended to be used in combination with get_preload_columns and get_preload_columns_sql.
-     *
-     * @param   array       $fieldlist The list of fields from get_preload_columns
-     * @param   \stdClass   $data The data retrieved from the database with fields to be extracted
-     * @return  string      The SQL to use in the SELECT
-     */
-    public function extract_fields_from_object(array $fieldlist, \stdClass $data) : \stdClass {
-        $newdata = (object) [];
-        foreach ($fieldlist as $alias => $fieldname) {
-            if (property_exists($data, $alias)) {
-                $newdata->$fieldname = $data->$alias;
-                unset($data->$alias);
-            } else {
-                debugging("Field '{$fieldname}' not found", DEBUG_DEVELOPER);
-            }
-        }
-
-        return $newdata;
-    }
-
-    /**
-     * Get the preload columns for the specified table and use the specified prefix in the column alias.
-     *
-     * This function is intended to be used in combination with get_preload_columns_sql and extract_from_fields.
-     *
-     * @param   string      $table
-     * @param   string      $prefix
-     * @return  array       The list of columns in a table. The array key is the column name with an applied prefix.
-     */
-    public function get_preload_columns(string $table, string $prefix) : array {
-        global $DB;
-
-        $fields = [];
-        foreach (array_keys($this->get_columns($table)) as $fieldname) {
-            $fields["{$prefix}{$fieldname}"] = $fieldname;
-        }
-
-        return $fields;
-    }
-
     /**
      * Converts short table name {tablename} to the real prefixed table name in given sql.
      * @param string $sql The sql to be operated on.
diff --git a/lib/dml/tests/dml_table_test.php b/lib/dml/tests/dml_table_test.php
new file mode 100644 (file)
index 0000000..4d3cd85
--- /dev/null
@@ -0,0 +1,228 @@
+<?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/>.
+
+/**
+ * DML Table tests.
+ *
+ * @package    core_dml
+ * @category   phpunit
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core\dml\table;
+
+/**
+ * DML Table tests.
+ *
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\dml\table
+ * @covers ::<!public>
+ */
+class core_dml_table_testcase extends database_driver_testcase {
+
+    /**
+     * Data provider for various \core\dml\table method tests.
+     *
+     * @return  array
+     */
+    public function get_field_select_provider() : array {
+        return [
+            'single field' => [
+                'tablename' => 'test_table_single',
+                'fieldlist' => [
+                    'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
+                ],
+                'primarykey' => 'id',
+                'fieldprefix' => 'ban',
+                'tablealias' => 'banana',
+                'banana.id AS banid',
+            ],
+            'multiple fields' => [
+                'tablename' => 'test_table_multiple',
+                'fieldlist' => [
+                    'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
+                    'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'],
+                    'name' => ['name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'],
+                ],
+                'primarykey' => 'id',
+                'fieldprefix' => 'ban',
+                'tablealias' => 'banana',
+                'banana.id AS banid, banana.course AS bancourse, banana.name AS banname',
+            ],
+        ];
+    }
+
+    /**
+     * Ensure that \core\dml\table::get_field_select() works as expected.
+     *
+     * @dataProvider get_field_select_provider
+     * @covers ::get_field_select
+     * @param   string      $tablename The name of the table
+     * @param   array       $fieldlist The list of fields
+     * @param   string      $primarykey The name of the primary key
+     * @param   string      $fieldprefix The prefix to use for each field
+     * @param   string      $tablealias The table AS alias name
+     * @param   string      $expected The expected SQL
+     */
+    public function test_get_field_select(
+        string $tablename,
+        array $fieldlist,
+        string $primarykey,
+        string $fieldprefix,
+        string $tablealias,
+        string $expected
+    ) {
+        $dbman = $this->tdb->get_manager();
+
+        $xmldbtable = new xmldb_table($tablename);
+        $xmldbtable->setComment("This is a test'n drop table. You can drop it safely");
+
+        foreach ($fieldlist as $args) {
+            call_user_func_array([$xmldbtable, 'add_field'], $args);
+        }
+        $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]);
+        $dbman->create_table($xmldbtable);
+
+        $table = new table($tablename, $tablealias, $fieldprefix);
+        $this->assertEquals($expected, $table->get_field_select());
+    }
+
+    /**
+     * Data provider for \core\dml\table::extract_from_result() tests.
+     *
+     * @return  array
+     */
+    public function extract_from_result_provider() : array {
+        return [
+            'single table' => [
+                'fieldlist' => [
+                    'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
+                    'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'],
+                    'flag' => ['flag', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'],
+                ],
+                'primarykey' => 'id',
+                'prefix' => 's',
+                'result' => (object) [
+                    'sid' => 1,
+                    'scourse' => 42,
+                    'sflag' => 'foo',
+                ],
+                'expectedrecord' => (object) [
+                    'id' => 1,
+                    'course' => 42,
+                    'flag' => 'foo',
+                ],
+            ],
+            'single table amongst others' => [
+                'fieldlist' => [
+                    'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
+                    'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'],
+                    'flag' => ['flag', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'],
+                ],
+                'primarykey' => 'id',
+                'prefix' => 's',
+                'result' => (object) [
+                    'sid' => 1,
+                    'scourse' => 42,
+                    'sflag' => 'foo',
+                    'oid' => 'id',
+                    'ocourse' => 'course',
+                    'oflag' => 'flag',
+                ],
+                'expectedrecord' => (object) [
+                    'id' => 1,
+                    'course' => 42,
+                    'flag' => 'foo',
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * Ensure that \core\dml\table::extract_from_result() works as expected.
+     *
+     * @dataProvider        extract_from_result_provider
+     * @covers ::extract_from_result
+     * @param   array       $fieldlist The list of fields
+     * @param   string      $primarykey The name of the primary key
+     * @param   string      $fieldprefix The prefix to use for each field
+     * @param   stdClass    $result The result of the get_records_sql
+     * @param   stdClass    $expected The expected output
+     */
+    public function test_extract_fields_from_result(
+        array $fieldlist,
+        string $primarykey,
+        string $fieldprefix,
+        stdClass $result,
+        stdClass $expected
+    ) {
+        $dbman = $this->tdb->get_manager();
+
+        $tablename = 'test_table_extraction';
+        $xmldbtable = new xmldb_table($tablename);
+        $xmldbtable->setComment("This is a test'n drop table. You can drop it safely");
+
+        foreach ($fieldlist as $args) {
+            call_user_func_array([$xmldbtable, 'add_field'], $args);
+        }
+        $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]);
+        $dbman->create_table($xmldbtable);
+
+        $table = new table($tablename, 'footable', $fieldprefix);
+        $this->assertEquals($expected, $table->extract_from_result($result));
+    }
+
+    /**
+     * Ensure that \core\dml\table::get_from_sql() works as expected.
+     *
+     * @dataProvider get_field_select_provider
+     * @covers ::get_from_sql
+     * @param   string      $tablename The name of the table
+     * @param   array       $fieldlist The list of fields
+     * @param   string      $primarykey The name of the primary key
+     * @param   string      $fieldprefix The prefix to use for each field
+     * @param   string      $tablealias The table AS alias name
+     * @param   string      $expected The expected SQL
+     */
+    public function test_get_from_sql(
+        string $tablename,
+        array $fieldlist,
+        string $primarykey,
+        string $fieldprefix,
+        string $tablealias,
+        string $expected
+    ) {
+        $dbman = $this->tdb->get_manager();
+
+        $tablename = 'test_table_extraction';
+        $xmldbtable = new xmldb_table($tablename);
+        $xmldbtable->setComment("This is a test'n drop table. You can drop it safely");
+
+        foreach ($fieldlist as $args) {
+            call_user_func_array([$xmldbtable, 'add_field'], $args);
+        }
+        $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]);
+        $dbman->create_table($xmldbtable);
+
+        $table = new table($tablename, $tablealias, $fieldprefix);
+
+        $this->assertEquals("{{$tablename}} {$tablealias}", $table->get_from_sql());
+    }
+}
index f89378a..fb144e9 100644 (file)
@@ -794,136 +794,6 @@ class core_dml_testcase extends database_driver_testcase {
         $this->assertFalse($columns['id']->auto_increment);
     }
 
-    public function test_get_preload_columns() {
-        $DB = $this->tdb;
-        $dbman = $this->tdb->get_manager();
-
-        $table = $this->get_test_table();
-        $tablename = $table->getName();
-
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
-        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala');
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $dbman->create_table($table);
-
-        $expected = [
-            'aid' => 'id',
-            'acourse' => 'course',
-            'aname' => 'name',
-        ];
-        $columns = $DB->get_preload_columns($tablename, 'a');
-        $this->assertCount(3, $columns);
-        $this->assertEquals($expected, $columns);
-    }
-
-    /**
-     * Ensure that get_preload_columns_sql works as expected.
-     *
-     * @dataProvider get_preload_columns_sql_provider
-     * @param   array       $fieldlist The list of fields
-     * @param   string      $tablealias The alias to use
-     * @param   string      $expected The string to match
-     */
-    public function test_get_preload_columns_sql(array $fieldlist, string $tablealias, string $expected) {
-        $this->assertEquals($expected, $this->tdb->get_preload_columns_sql($fieldlist, $tablealias));
-    }
-
-    /**
-     * Data provider for get_preload_columns_sql tests.
-     *
-     * @return  array
-     */
-    public function get_preload_columns_sql_provider() : array {
-        return [
-            'single field' => [
-                [
-                    'xid' => 'id',
-                ],
-                'x',
-                'x.id AS xid',
-            ],
-            'multiple fields' => [
-                [
-                    'bananaid' => 'id',
-                    'bananacourse' => 'course',
-                    'bananafoo' => 'foo',
-                ],
-                'banana',
-                'banana.id AS bananaid, banana.course AS bananacourse, banana.foo AS bananafoo',
-            ],
-        ];
-    }
-
-    /**
-     * Ensure that extract_fields_from_object works as expected.
-     *
-     * @dataProvider        extract_fields_from_object_provider
-     * @param   array       $fieldlist The list of fields
-     * @param   stdClass    $in Input values for the test
-     * @param   stdClass    $out The expected output
-     * @param   stdClass    $modified Expected value of $in after it's been modified
-     */
-    public function test_extract_fields_from_object(array $fieldlist, \stdClass $in, \stdClass $out, \stdClass $modified) {
-        $result = $this->tdb->extract_fields_from_object($fieldlist, $in);
-        $this->assertEquals($out, $result);
-        $this->assertEquals($modified, $in);
-    }
-
-    /**
-     * Data provider for extract_fields_from_object tests.
-     *
-     * @return  array
-     */
-    public function extract_fields_from_object_provider() : array {
-        return [
-            'single table' => [
-                [
-                    'sid' => 'id',
-                    'scourse' => 'course',
-                    'sflag' => 'flag',
-                ],
-                (object) [
-                    'sid' => 1,
-                    'scourse' => 42,
-                    'sflag' => 'foo',
-                ],
-                (object) [
-                    'id' => 1,
-                    'course' => 42,
-                    'flag' => 'foo',
-                ],
-                (object) [
-                ],
-            ],
-            'single table amongst others' => [
-                [
-                    'sid' => 'id',
-                    'scourse' => 'course',
-                    'sflag' => 'flag',
-                ],
-                (object) [
-                    'sid' => 1,
-                    'scourse' => 42,
-                    'sflag' => 'foo',
-                    'oid' => 'id',
-                    'ocourse' => 'course',
-                    'oflag' => 'flag',
-                ],
-                (object) [
-                    'id' => 1,
-                    'course' => 42,
-                    'flag' => 'foo',
-                ],
-                (object) [
-                    'oid' => 'id',
-                    'ocourse' => 'course',
-                    'oflag' => 'flag',
-                ],
-            ],
-        ];
-    }
-
     public function test_get_manager() {
         $DB = $this->tdb;
         $dbman = $this->tdb->get_manager();
index 65210b2..c667040 100644 (file)
@@ -30,10 +30,20 @@ require_once($CFG->libdir . '/filelib.php');
 require_once($CFG->dirroot . '/repository/lib.php');
 require_once($CFG->libdir . '/filestorage/stored_file.php');
 
+/**
+ * Unit tests for /lib/filestorage/file_storage.php
+ *
+ * @copyright 2012 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass file_storage
+ */
 class core_files_file_storage_testcase extends advanced_testcase {
 
     /**
      * Files can be created from strings.
+     *
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string() {
         global $DB;
@@ -107,6 +117,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
 
     /**
      * Local files can be added to the filepool
+     *
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname() {
         global $CFG, $DB;
@@ -189,6 +202,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
 
     /**
      * Tests get get file.
+     *
+     * @covers ::get_file
+     * @covers ::<!public>
      */
     public function test_get_file() {
         global $CFG;
@@ -226,7 +242,10 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * Local images can be added to the filepool and their preview can be obtained
      *
+     * @param stored_file $file
      * @depends test_get_file
+     * @covers ::get_file_preview
+     * @covers ::<!public>
      */
     public function test_get_file_preview(stored_file $file) {
         global $CFG;
@@ -246,6 +265,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->get_file_preview($file, 'amodewhichdoesntexist');
     }
 
+    /**
+     * Tests for get_file_preview without an image.
+     *
+     * @covers ::get_file_preview
+     * @covers ::<!public>
+     */
     public function test_get_file_preview_nonimage() {
         $this->resetAfterTest(true);
         $syscontext = context_system::instance();
@@ -271,6 +296,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Make sure renaming is working
      *
      * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
+     * @covers stored_file::rename
+     * @covers ::<!public>
      */
     public function test_file_renaming() {
         global $CFG;
@@ -316,6 +343,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Create file from reference tests
      *
      * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
+     * @covers ::create_file_from_reference
+     * @covers ::<!public>
      */
     public function test_create_file_from_reference() {
         global $CFG, $DB;
@@ -401,6 +430,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Create file from reference tests
      *
      * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
+     * @covers ::create_file_from_reference
+     * @covers ::<!public>
      */
     public function test_create_file_from_reference_with_content_hash() {
         global $CFG, $DB;
@@ -506,6 +537,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         return $user;
     }
 
+    /**
+     * Tests for get_area_files
+     *
+     * @covers ::get_area_files
+     * @covers ::<!public>
+     */
     public function test_get_area_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -561,6 +598,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEmpty($areafiles);
     }
 
+    /**
+     * Tests for get_area_tree
+     *
+     * @covers ::get_area_tree
+     * @covers ::<!public>
+     */
     public function test_get_area_tree() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -615,6 +658,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals($filerecord['filename'], $subdirfile->get_filename());
     }
 
+    /**
+     * Tests for get_file_by_id
+     *
+     * @covers ::get_file_by_id
+     * @covers ::<!public>
+     */
     public function test_get_file_by_id() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -631,6 +680,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertFalse($doesntexist);
     }
 
+    /**
+     * Tests for get_file_by_hash
+     *
+     * @covers ::get_file_by_hash
+     * @covers ::<!public>
+     */
     public function test_get_file_by_hash() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -646,6 +701,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertFalse($doesntexist);
     }
 
+    /**
+     * Tests for get_external_files
+     *
+     * @covers ::get_external_files
+     * @covers ::<!public>
+     */
     public function test_get_external_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -707,6 +768,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals($aliasfile3->get_referencefileid(), $aliasfile2->get_referencefileid());
     }
 
+    /**
+     * Tests for create_directory with a negative contextid.
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_contextid_negative() {
         $fs = get_file_storage();
 
@@ -714,6 +781,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory(-1, 'core', 'unittest', 0, '/');
     }
 
+    /**
+     * Tests for create_directory with an invalid contextid.
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_contextid_invalid() {
         $fs = get_file_storage();
 
@@ -721,6 +794,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory('not an int', 'core', 'unittest', 0, '/');
     }
 
+    /**
+     * Tests for create_directory with an invalid component.
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_component_invalid() {
         $fs = get_file_storage();
         $syscontext = context_system::instance();
@@ -729,6 +808,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory($syscontext->id, 'bad/component', 'unittest', 0, '/');
     }
 
+    /**
+     * Tests for create_directory with an invalid filearea.
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_filearea_invalid() {
         $fs = get_file_storage();
         $syscontext = context_system::instance();
@@ -737,6 +822,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory($syscontext->id, 'core', 'bad-filearea', 0, '/');
     }
 
+    /**
+     * Tests for create_directory with a negative itemid
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_itemid_negative() {
         $fs = get_file_storage();
         $syscontext = context_system::instance();
@@ -745,6 +836,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory($syscontext->id, 'core', 'unittest', -1, '/');
     }
 
+    /**
+     * Tests for create_directory with an invalid itemid
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_itemid_invalid() {
         $fs = get_file_storage();
         $syscontext = context_system::instance();
@@ -753,6 +850,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory($syscontext->id, 'core', 'unittest', 'notanint', '/');
     }
 
+    /**
+     * Tests for create_directory with an invalid filepath
+     *
+     * @covers ::create_directory
+     * @covers ::<!public>
+     */
     public function test_create_directory_filepath_invalid() {
         $fs = get_file_storage();
         $syscontext = context_system::instance();
@@ -761,6 +864,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_directory($syscontext->id, 'core', 'unittest', 0, '/not-with-trailing/or-leading-slash');
     }
 
+    /**
+     * Tests for get_directory_files.
+     *
+     * @covers ::get_directory_files
+     * @covers ::<!public>
+     */
     public function test_get_directory_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -818,6 +927,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Tests for search_references.
+     *
+     * @covers ::search_references
+     * @covers ::<!public>
+     */
     public function test_search_references() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -890,6 +1005,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertTrue($exceptionthrown);
     }
 
+    /**
+     * Tests for delete_area_files.
+     *
+     * @covers ::delete_area_files
+     * @covers ::<!public>
+     */
     public function test_delete_area_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -905,6 +1026,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals(0, count($areafiles));
     }
 
+    /**
+     * Tests for delete_area_files using an itemid.
+     *
+     * @covers ::delete_area_files
+     * @covers ::<!public>
+     */
     public function test_delete_area_files_itemid() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -919,6 +1046,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals(4, count($areafiles));
     }
 
+    /**
+     * Tests for delete_area_files_select.
+     *
+     * @covers ::delete_area_files_select
+     * @covers ::<!public>
+     */
     public function test_delete_area_files_select() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -934,6 +1067,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals(0, count($areafiles));
     }
 
+    /**
+     * Tests for delete_component_files.
+     *
+     * @covers ::delete_component_files
+     * @covers ::<!public>
+     */
     public function test_delete_component_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -945,6 +1084,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals(0, count($areafiles));
     }
 
+    /**
+     * Tests for create_file_from_url.
+     *
+     * @covers ::create_file_from_url
+     * @covers ::<!public>
+     */
     public function test_create_file_from_url() {
         $this->resetAfterTest(true);
 
@@ -975,6 +1120,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $file3 = $this->assertInstanceOf('stored_file', $file3);
     }
 
+    /**
+     * Tests for cron.
+     *
+     * @covers ::cron
+     * @covers ::<!public>
+     */
     public function test_cron() {
         $this->resetAfterTest(true);
 
@@ -986,6 +1137,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->cron();
     }
 
+    /**
+     * Tests for is_area_empty.
+     *
+     * @covers ::is_area_empty
+     * @covers ::<!public>
+     */
     public function test_is_area_empty() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -998,6 +1155,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertTrue($fs->is_area_empty($user->ctxid, 'user', 'private', 9999, false));
     }
 
+    /**
+     * Tests for move_area_files_to_new_context.
+     *
+     * @covers ::move_area_files_to_new_context
+     * @covers ::<!public>
+     */
     public function test_move_area_files_to_new_context() {
         $this->resetAfterTest(true);
 
@@ -1047,6 +1210,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertEquals($movedfile->get_contenthash(), $originalfile->get_contenthash());
     }
 
+    /**
+     * Tests for convert_image.
+     *
+     * @covers ::convert_image
+     * @covers ::<!public>
+     */
     public function test_convert_image() {
         global $CFG;
 
@@ -1075,6 +1244,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertInstanceOf('stored_file', $converted);
     }
 
+    /**
+     * Tests for convert_image with a PNG.
+     *
+     * @covers ::convert_image
+     * @covers ::<!public>
+     */
     public function test_convert_image_png() {
         global $CFG;
 
@@ -1153,6 +1328,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
 
     /**
      * @expectedException        file_exception
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_file_invalid() {
         $this->resetAfterTest(true);
@@ -1168,6 +1345,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid contextid
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_contextid_invalid() {
         $this->resetAfterTest(true);
@@ -1187,6 +1366,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid component
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_component_invalid() {
         $this->resetAfterTest(true);
@@ -1206,6 +1387,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid filearea
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_filearea_invalid() {
         $this->resetAfterTest(true);
@@ -1225,6 +1408,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid itemid
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_itemid_invalid() {
         $this->resetAfterTest(true);
@@ -1244,6 +1429,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file path
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_filepath_invalid() {
         $this->resetAfterTest(true);
@@ -1263,6 +1450,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file name
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_filename_invalid() {
         $this->resetAfterTest(true);
@@ -1281,6 +1470,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file timecreated
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_timecreated_invalid() {
         $this->resetAfterTest(true);
@@ -1300,6 +1491,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file timemodified
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_timemodified_invalid() {
         $this->resetAfterTest(true);
@@ -1319,6 +1512,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        stored_file_creation_exception
      * @expectedExceptionMessage Can not create file "1/core/phpunit/0/testfile.txt"
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
      */
     public function test_create_file_from_storedfile_duplicate() {
         $this->resetAfterTest(true);
@@ -1333,6 +1528,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $fs->create_file_from_storedfile($filerecord, $file1->get_id());
     }
 
+    /**
+     * Tests for create_file_from_storedfile.
+     *
+     * @covers ::create_file_from_storedfile
+     * @covers ::<!public>
+     */
     public function test_create_file_from_storedfile() {
         $this->resetAfterTest(true);
 
@@ -1370,6 +1571,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid contextid
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_contextid_invalid() {
         $this->resetAfterTest(true);
@@ -1385,6 +1588,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid component
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_component_invalid() {
         $this->resetAfterTest(true);
@@ -1400,6 +1605,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid filearea
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_filearea_invalid() {
         $this->resetAfterTest(true);
@@ -1415,6 +1622,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid itemid
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_itemid_invalid() {
         $this->resetAfterTest(true);
@@ -1430,6 +1639,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file path
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_filepath_invalid() {
         $this->resetAfterTest(true);
@@ -1445,6 +1656,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file name
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_filename_invalid() {
         $this->resetAfterTest(true);
@@ -1460,6 +1673,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file timecreated
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_timecreated_invalid() {
         $this->resetAfterTest(true);
@@ -1477,6 +1692,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file timemodified
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
      */
     public function test_create_file_from_string_timemodified_invalid() {
         $this->resetAfterTest(true);
@@ -1489,6 +1706,11 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $file1 = $fs->create_file_from_string($filerecord, 'text contents');
     }
 
+    /**
+     * Tests for create_file_from_string with a duplicate string.
+     * @covers ::create_file_from_string
+     * @covers ::<!public>
+     */
     public function test_create_file_from_string_duplicate() {
         $this->resetAfterTest(true);
 
@@ -1505,6 +1727,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid contextid
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_contextid_invalid() {
         global $CFG;
@@ -1523,6 +1747,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid component
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_component_invalid() {
         global $CFG;
@@ -1541,6 +1767,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid filearea
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_filearea_invalid() {
         global $CFG;
@@ -1559,6 +1787,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid itemid
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_itemid_invalid() {
         global $CFG;
@@ -1577,6 +1807,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file path
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_filepath_invalid() {
         global $CFG;
@@ -1595,6 +1827,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file name
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_filename_invalid() {
         global $CFG;
@@ -1613,6 +1847,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file timecreated
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_timecreated_invalid() {
         global $CFG;
@@ -1631,6 +1867,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        file_exception
      * @expectedExceptionMessage Invalid file timemodified
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_timemodified_invalid() {
         global $CFG;
@@ -1649,6 +1887,8 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * @expectedException        stored_file_creation_exception
      * @expectedExceptionMessage Can not create file "1/core/phpunit/0/testfile.txt"
+     * @covers ::create_file_from_pathname
+     * @covers ::<!public>
      */
     public function test_create_file_from_pathname_duplicate_file() {
         global $CFG;
@@ -1668,6 +1908,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
 
     /**
      * Calling stored_file::delete_reference() on a non-reference file throws coding_exception
+     *
+     * @covers stored_file::delete_reference
+     * @covers ::<!public>
      */
     public function test_delete_reference_on_nonreference() {
 
@@ -1694,6 +1937,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * Calling stored_file::delete_reference() on a reference file does not affect other
      * symlinks to the same original
+     *
+     * @covers stored_file::delete_reference
+     * @covers ::<!public>
      */
     public function test_delete_reference_one_symlink_does_not_rule_them_all() {
 
@@ -1859,6 +2105,12 @@ class core_files_file_storage_testcase extends advanced_testcase {
         $this->assertNull($symlink2->get_referencefileid());
     }
 
+    /**
+     * Tests for get_unused_filename.
+     *
+     * @covers ::get_unused_filename
+     * @covers ::<!public>
+     */
     public function test_get_unused_filename() {
         global $USER;
         $this->resetAfterTest(true);
@@ -1924,6 +2176,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_file returns appropriate output when the
      * file could not be found.
+     *
+     * @covers ::mimetype
+     * @covers ::<!public>
      */
     public function test_mimetype_not_found() {
         $mimetype = file_storage::mimetype('/path/to/nonexistent/file');
@@ -1937,6 +2192,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Note: this is not intended to check that functions outside of this
      * file works. It is intended to validate the codepath contains no
      * errors and behaves as expected.
+     *
+     * @covers ::mimetype
+     * @covers ::<!public>
      */
     public function test_mimetype_known() {
         $filepath = __DIR__ . '/fixtures/testimage.jpg';
@@ -1947,6 +2205,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_file returns appropriate output when the
      * file could not be found.
+     *
+     * @covers ::mimetype
+     * @covers ::<!public>
      */
     public function test_mimetype_from_file_not_found() {
         $mimetype = file_storage::mimetype_from_file('/path/to/nonexistent/file');
@@ -1960,6 +2221,9 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Note: this is not intended to check that functions outside of this
      * file works. It is intended to validate the codepath contains no
      * errors and behaves as expected.
+     *
+     * @covers ::mimetype
+     * @covers ::<!public>
      */
     public function test_mimetype_from_file_known() {
         $filepath = __DIR__ . '/fixtures/testimage.jpg';
index fa4c6c2..76f75a2 100644 (file)
@@ -36,6 +36,7 @@ require_once($CFG->libdir . '/filestorage/file_system_filedir.php');
  * @category  files
  * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass file_system_filedir
  */
 class core_files_file_system_filedir_testcase extends advanced_testcase {
 
@@ -137,6 +138,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Ensure that an appropriate error is shown when the filedir directory
      * is not writable.
+     *
+     * @covers ::__construct
+     * @covers ::<!public>
      */
     public function test_readonly_filesystem_filedir() {
         $this->resetAfterTest();
@@ -159,6 +163,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Ensure that an appropriate error is shown when the trash directory
      * is not writable.
+     *
+     * @covers ::__construct
+     * @covers ::<!public>
      */
     public function test_readonly_filesystem_trashdir() {
         $this->resetAfterTest();
@@ -180,6 +187,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Test that the standard Moodle warning message is put into the filedir.
+     *
+     * @covers ::__construct
+     * @covers ::<!public>
      */
     public function test_warnings_put_in_place() {
         $this->resetAfterTest();
@@ -198,6 +208,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Ensure that the default implementation of get_remote_path_from_hash
      * simply calls get_local_path_from_hash.
+     *
+     * @covers ::get_remote_path_from_hash
+     * @covers ::<!public>
      */
     public function test_get_remote_path_from_hash() {
         $filecontent = 'example content';
@@ -223,6 +236,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
      * a failed recovery.
+     *
+     * @covers ::get_local_path_from_storedfile
+     * @covers ::<!public>
      */
     public function test_get_local_path_from_storedfile_with_recovery() {
         $filecontent = 'example content';
@@ -251,6 +267,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
      * a failed recovery.
+     *
+     * @covers ::get_local_path_from_storedfile
+     * @covers ::<!public>
      */
     public function test_get_local_path_from_storedfile_without_recovery() {
         $filecontent = 'example content';
@@ -282,6 +301,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
      * @dataProvider contenthash_dataprovider
      * @param   string  $hash contenthash to test
      * @param   string  $hashdir Expected format of content directory
+     *
+     * @covers ::get_fulldir_from_hash
+     * @covers ::<!public>
      */
     public function test_get_fulldir_from_hash($hash, $hashdir) {
         global $CFG;
@@ -302,6 +324,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
      * @dataProvider contenthash_dataprovider
      * @param   string  $hash contenthash to test
      * @param   string  $hashdir Expected format of content directory
+     *
+     * @covers ::get_fulldir_from_storedfile
+     * @covers ::<!public>
      */
     public function test_get_fulldir_from_storedfile($hash, $hashdir) {
         global $CFG;
@@ -332,6 +357,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
      * @dataProvider contenthash_dataprovider
      * @param   string  $hash contenthash to test
      * @param   string  $hashdir Expected format of content directory
+     *
+     * @covers ::get_contentdir_from_hash
+     * @covers ::<!public>
      */
     public function test_get_contentdir_from_hash($hash, $hashdir) {
         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash');
@@ -350,6 +378,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
      * @dataProvider contenthash_dataprovider
      * @param   string  $hash contenthash to test
      * @param   string  $hashdir Expected format of content directory
+     *
+     * @covers ::get_contentpath_from_hash
+     * @covers ::<!public>
      */
     public function test_get_contentpath_from_hash($hash, $hashdir) {
         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash');
@@ -369,6 +400,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
      * @dataProvider contenthash_dataprovider
      * @param   string  $hash contenthash to test
      * @param   string  $hashdir Expected format of content directory
+     *
+     * @covers ::get_trash_fullpath_from_hash
+     * @covers ::<!public>
      */
     public function test_get_trash_fullpath_from_hash($hash, $hashdir) {
         global $CFG;
@@ -389,6 +423,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
      * @dataProvider contenthash_dataprovider
      * @param   string  $hash contenthash to test
      * @param   string  $hashdir Expected format of content directory
+     *
+     * @covers ::get_trash_fulldir_from_hash
+     * @covers ::<!public>
      */
     public function test_get_trash_fulldir_from_hash($hash, $hashdir) {
         global $CFG;
@@ -404,6 +441,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Ensure that copying a file to a target from a stored_file works as anticipated.
+     *
+     * @covers ::copy_content_from_storedfile
+     * @covers ::<!public>
      */
     public function test_copy_content_from_storedfile() {
         $this->resetAfterTest();
@@ -440,6 +480,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Ensure that content recovery works.
+     *
+     * @covers ::recover_file
+     * @covers ::<!public>
      */
     public function test_recover_file() {
         $this->resetAfterTest();
@@ -478,6 +521,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Ensure that content recovery works.
+     *
+     * @covers ::recover_file
+     * @covers ::<!public>
      */
     public function test_recover_file_already_present() {
         $this->resetAfterTest();
@@ -515,6 +561,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Ensure that content recovery works.
+     *
+     * @covers ::recover_file
+     * @covers ::<!public>
      */
     public function test_recover_file_size_mismatch() {
         $this->resetAfterTest();
@@ -550,6 +599,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Ensure that content recovery works.
+     *
+     * @covers ::recover_file
+     * @covers ::<!public>
      */
     public function test_recover_file_has_mismatch() {
         $this->resetAfterTest();
@@ -586,6 +638,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Ensure that content recovery works when the content file is in the
      * alt trash directory.
+     *
+     * @covers ::recover_file
+     * @covers ::<!public>
      */
     public function test_recover_file_alttrash() {
         $this->resetAfterTest();
@@ -619,6 +674,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test that an appropriate error message is generated when adding a
      * file to the pool when the pool directory structure is not writable.
+     *
+     * @covers ::recover_file
+     * @covers ::<!public>
      */
     public function test_recover_file_contentdir_readonly() {
         $this->resetAfterTest();
@@ -654,6 +712,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Test adding a file to the pool.
+     *
+     * @covers ::add_file_from_path
+     * @covers ::<!public>
      */
     public function test_add_file_from_path() {
         $this->resetAfterTest();
@@ -688,6 +749,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test that an appropriate error message is generated when adding an
      * unavailable file to the pool is attempted.
+     *
+     * @covers ::add_file_from_path
+     * @covers ::<!public>
      */
     public function test_add_file_from_path_file_unavailable() {
         $this->resetAfterTest();
@@ -706,6 +770,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test that an appropriate error message is generated when specifying
      * the wrong contenthash when adding a file to the pool.
+     *
+     * @covers ::add_file_from_path
+     * @covers ::<!public>
      */
     public function test_add_file_from_path_mismatched_hash() {
         $this->resetAfterTest();
@@ -726,6 +793,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test that an appropriate error message is generated when an existing
      * file in the pool has the wrong contenthash
+     *
+     * @covers ::add_file_from_path
+     * @covers ::<!public>
      */
     public function test_add_file_from_path_existing_content_invalid() {
         $this->resetAfterTest();
@@ -769,6 +839,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test that an appropriate error message is generated when adding a
      * file to the pool when the pool directory structure is not writable.
+     *
+     * @covers ::add_file_from_path
+     * @covers ::<!public>
      */
     public function test_add_file_from_path_existing_cannot_write_hashpath() {
         $this->resetAfterTest();
@@ -800,6 +873,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Test adding a string to the pool.
+     *
+     * @covers ::add_file_from_string
+     * @covers ::<!public>
      */
     public function test_add_file_from_string() {
         $this->resetAfterTest();
@@ -825,6 +901,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test that an appropriate error message is generated when adding a
      * string to the pool when the pool directory structure is not writable.
+     *
+     * @covers ::add_file_from_string
+     * @covers ::<!public>
      */
     public function test_add_file_from_string_existing_cannot_write_hashpath() {
         $this->resetAfterTest();
@@ -854,6 +933,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test adding a string to the pool when an item with the same
      * contenthash is already present.
+     *
+     * @covers ::add_file_from_string
+     * @covers ::<!public>
      */
     public function test_add_file_from_string_existing_matches() {
         $this->resetAfterTest();
@@ -886,6 +968,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Test the cleanup of deleted files when there are no files to delete.
+     *
+     * @covers ::remove_file
+     * @covers ::<!public>
      */
     public function test_remove_file_missing() {
         $this->resetAfterTest();
@@ -907,6 +992,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Test the cleanup of deleted files when a file already exists in the
      * trash for that path.
+     *
+     * @covers ::remove_file
+     * @covers ::<!public>
      */
     public function test_remove_file_existing_trash() {
         $this->resetAfterTest();
@@ -934,6 +1022,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Ensure that remove_file does nothing with an empty file.
+     *
+     * @covers ::remove_file
+     * @covers ::<!public>
      */
     public function test_remove_file_empty() {
         $this->resetAfterTest();
@@ -955,6 +1046,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Ensure that remove_file does nothing when a file is still
      * in use.
+     *
+     * @covers ::remove_file
+     * @covers ::<!public>
      */
     public function test_remove_file_in_use() {
         $this->resetAfterTest();
@@ -986,6 +1080,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
     /**
      * Ensure that remove_file removes the file when it is no
      * longer in use.
+     *
+     * @covers ::remove_file
+     * @covers ::<!public>
      */
     public function test_remove_file_expired() {
         $this->resetAfterTest();
@@ -1016,6 +1113,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
 
     /**
      * Test purging the cache.
+     *
+     * @covers ::empty_trash
+     * @covers ::<!public>
      */
     public function test_empty_trash() {
         $this->resetAfterTest();
index 159f06c..fbcba72 100644 (file)
@@ -35,6 +35,7 @@ require_once($CFG->libdir . '/filestorage/file_system.php');
  * @category  phpunit
  * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass file_system
  */
 class core_files_file_system_testcase extends advanced_testcase {
 
@@ -107,6 +108,8 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that the file system is not clonable.
+     *
+     * @covers ::<!public>
      */
     public function test_not_cloneable() {
         $reflection = new ReflectionClass('file_system');
@@ -115,6 +118,8 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that the filedir file_system extension is used by default.
+     *
+     * @covers ::<!public>
      */
     public function test_default_class() {
         $this->resetAfterTest();
@@ -131,6 +136,8 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that the specified file_system extension class is used.
+     *
+     * @covers ::<!public>
      */
     public function test_supplied_class() {
         global $CFG;
@@ -151,6 +158,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test that the readfile function outputs content to disk.
+     *
+     * @covers ::readfile
+     * @covers ::<!public>
      */
     public function test_readfile_remote() {
         global $CFG;
@@ -182,6 +192,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test that the readfile function outputs content to disk.
+     *
+     * @covers ::readfile
+     * @covers ::<!public>
      */
     public function test_readfile_local() {
         global $CFG;
@@ -218,6 +231,9 @@ class core_files_file_system_testcase extends advanced_testcase {
      * @dataProvider get_local_path_from_storedfile_provider
      * @param   array   $args The additional args to pass to get_local_path_from_storedfile
      * @param   bool    $fetch Whether the combination of args should have caused a fetch
+     *
+     * @covers ::get_local_path_from_storedfile
+     * @covers ::<!public>
      */
     public function test_get_local_path_from_storedfile($args, $fetch) {
         $filepath = '/path/to/file';
@@ -245,6 +261,9 @@ class core_files_file_system_testcase extends advanced_testcase {
      * Ensure that the default implementation of get_remote_path_from_storedfile
      * simply calls get_local_path_from_storedfile without requiring a
      * fetch.
+     *
+     * @covers ::get_remote_path_from_storedfile
+     * @covers ::<!public>
      */
     public function test_get_remote_path_from_storedfile() {
         $filepath = '/path/to/file';
@@ -275,6 +294,9 @@ class core_files_file_system_testcase extends advanced_testcase {
      * of the file.
      *
      * Fetching the file is optional.
+     *
+     * @covers ::is_file_readable_locally_by_hash
+     * @covers ::<!public>
      */
     public function test_is_file_readable_locally_by_hash() {
         $filecontent = 'example content';
@@ -294,6 +316,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_locally_by_hash with an empty file.
+     *
+     * @covers ::is_file_readable_locally_by_hash
+     * @covers ::<!public>
      */
     public function test_is_file_readable_locally_by_hash_empty() {
         $filecontent = '';
@@ -311,6 +336,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_remotely_by_hash
+     * @covers ::<!public>
      */
     public function test_is_file_readable_remotely_by_hash() {
         $filecontent = 'example content';
@@ -329,6 +357,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_remotely_by_hash
+     * @covers ::<!public>
      */
     public function test_is_file_readable_remotely_by_hash_empty() {
         $filecontent = '';
@@ -346,6 +377,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_remotely_by_hash
+     * @covers ::<!public>
      */
     public function test_is_file_readable_remotely_by_hash_not_found() {
         $filecontent = 'example content';
@@ -364,6 +398,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_remotely_by_storedfile
+     * @covers ::<!public>
      */
     public function test_is_file_readable_remotely_by_storedfile() {
         $file = $this->get_stored_file('example content');
@@ -380,6 +417,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_remotely_by_storedfile
+     * @covers ::<!public>
      */
     public function test_is_file_readable_remotely_by_storedfile_empty() {
         $fs = $this->get_testable_mock([
@@ -395,6 +435,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_locally_by_storedfile with an empty file.
+     *
+     * @covers ::is_file_readable_locally_by_storedfile
+     * @covers ::<!public>
      */
     public function test_is_file_readable_locally_by_storedfile_empty() {
         $fs = $this->get_testable_mock([
@@ -410,6 +453,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_locally_by_storedfile
+     * @covers ::<!public>
      */
     public function test_is_file_readable_remotely_by_storedfile_not_found() {
         $file = $this->get_stored_file('example content');
@@ -426,6 +472,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_locally_by_storedfile with a valid file.
+     *
+     * @covers ::is_file_readable_locally_by_storedfile
+     * @covers ::<!public>
      */
     public function test_is_file_readable_locally_by_storedfile_unreadable() {
         $fs = $this->get_testable_mock([
@@ -442,6 +491,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of is_file_readable_locally_by_storedfile with a valid file should pass fetch.
+     *
+     * @covers ::is_file_readable_locally_by_storedfile
+     * @covers ::<!public>
      */
     public function test_is_file_readable_locally_by_storedfile_passes_fetch() {
         $fs = $this->get_testable_mock([
@@ -458,6 +510,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that is_file_removable returns correctly for an empty file.
+     *
+     * @covers ::is_file_removable
+     * @covers ::<!public>
      */
     public function test_is_file_removable_empty() {
         $filecontent = '';
@@ -471,6 +526,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that is_file_removable returns false if the file is still in use.
+     *
+     * @covers ::is_file_removable
+     * @covers ::<!public>
      */
     public function test_is_file_removable_in_use() {
         $this->resetAfterTest();
@@ -493,6 +551,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that is_file_removable returns false if the file is not in use.
+     *
+     * @covers ::is_file_removable
+     * @covers ::<!public>
      */
     public function test_is_file_removable_not_in_use() {
         $this->resetAfterTest();
@@ -515,6 +576,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of get_content.
+     *
+     * @covers ::get_content
+     * @covers ::<!public>
      */
     public function test_get_content() {
         global $CFG;
@@ -538,6 +602,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Test the stock implementation of get_content.
+     *
+     * @covers ::get_content
+     * @covers ::<!public>
      */
     public function test_get_content_empty() {
         global $CFG;
@@ -559,6 +626,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that the list_files function requires a local copy of the
      * file, and passes the path to the packer.
+     *
+     * @covers ::list_files
+     * @covers ::<!public>
      */
     public function test_list_files() {
         $filecontent = 'example content';
@@ -589,6 +659,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that the extract_to_pathname function requires a local copy of the
      * file, and passes the path to the packer.
+     *
+     * @covers ::extract_to_pathname
+     * @covers ::<!public>
      */
     public function test_extract_to_pathname() {
         $filecontent = 'example content';
@@ -620,6 +693,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that the extract_to_storage function requires a local copy of the
      * file, and passes the path to the packer.
+     *
+     * @covers ::extract_to_storage
+     * @covers ::<!public>
      */
     public function test_extract_to_storage() {
         $filecontent = 'example content';
@@ -660,6 +736,8 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that the add_storedfile_to_archive function requires a local copy of the
      * file, and passes the path to the archive.
+     *
+     * @covers ::<!public>
      */
     public function test_add_storedfile_to_archive_directory() {
         $file = $this->get_stored_file('', '.');
@@ -695,6 +773,8 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that the add_storedfile_to_archive function requires a local copy of the
      * file, and passes the path to the archive.
+     *
+     * @covers ::<!public>
      */
     public function test_add_storedfile_to_archive_file() {
         $file = $this->get_stored_file('example content');
@@ -734,6 +814,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that the add_to_curl_request function requires a local copy of the
      * file, and passes the path to curl_file_create.
+     *
+     * @covers ::add_to_curl_request
+     * @covers ::<!public>
      */
     public function test_add_to_curl_request() {
         $file = $this->get_stored_file('example content');
@@ -756,6 +839,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that test_get_imageinfo_not_image returns false if the file
      * passed was deemed to not be an image.
+     *
+     * @covers ::get_imageinfo
+     * @covers ::<!public>
      */
     public function test_get_imageinfo_not_image() {
         $filecontent = 'example content';
@@ -775,6 +861,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that test_get_imageinfo_not_image returns imageinfo.
+     *
+     * @covers ::get_imageinfo
+     * @covers ::<!public>
      */
     public function test_get_imageinfo() {
         $filepath = '/path/to/file';
@@ -809,6 +898,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Ensure that is_image_from_storedfile always returns false for an
      * empty file size.
+     *
+     * @covers ::is_image_from_storedfile
+     * @covers ::<!public>
      */
     public function test_is_image_empty_filesize() {
         $filecontent = 'example content';
@@ -829,6 +921,8 @@ class core_files_file_system_testcase extends advanced_testcase {
      * @dataProvider is_image_from_storedfile_provider
      * @param   string  $mimetype Mimetype to test
      * @param   bool    $isimage Whether this mimetype should be detected as an image
+     * @covers ::is_image_from_storedfile
+     * @covers ::<!public>
      */
     public function test_is_image_from_storedfile_mimetype($mimetype, $isimage) {
         $filecontent = 'example content';
@@ -845,6 +939,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that get_imageinfo_from_path returns an appropriate response
      * for an image.
+     *
+     * @covers ::get_imageinfo_from_path
+     * @covers ::<!public>
      */
     public function test_get_imageinfo_from_path() {
         $filepath = __DIR__ . "/fixtures/testimage.jpg";
@@ -865,6 +962,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that get_imageinfo_from_path returns an appropriate response
      * for a file which is not an image.
+     *
+     * @covers ::get_imageinfo_from_path
+     * @covers ::<!public>
      */
     public function test_get_imageinfo_from_path_no_image() {
         $filepath = __FILE__;
@@ -881,6 +981,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that get_content_file_handle returns a valid file handle.
+     *
+     * @covers ::get_content_file_handle
+     * @covers ::<!public>
      */
     public function test_get_content_file_handle_default() {
         $filecontent = 'example content';
@@ -899,6 +1002,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that get_content_file_handle returns a valid file handle for a gz file.
+     *
+     * @covers ::get_content_file_handle
+     * @covers ::<!public>
      */
     public function test_get_content_file_handle_gz() {
         $filecontent = 'example content';
@@ -916,6 +1022,9 @@ class core_files_file_system_testcase extends advanced_testcase {
 
     /**
      * Ensure that get_content_file_handle returns an exception when calling for a invalid file handle type.
+     *
+     * @covers ::get_content_file_handle
+     * @covers ::<!public>
      */
     public function test_get_content_file_handle_invalid() {
         $filecontent = 'example content';
@@ -932,6 +1041,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_hash returns the correct mimetype with
      * a file whose filename suggests mimetype.
+     *
+     * @covers ::mimetype_from_hash
+     * @covers ::<!public>
      */
     public function test_mimetype_from_hash_using_filename() {
         $filepath = '/path/to/file/not/currently/on/disk';
@@ -949,6 +1061,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_hash returns the correct mimetype with
      * a locally available file whose filename does not suggest mimetype.
+     *
+     * @covers ::mimetype_from_hash
+     * @covers ::<!public>
      */
     public function test_mimetype_from_hash_using_file_content() {
         $filecontent = 'example content';
@@ -966,6 +1081,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_hash returns the correct mimetype with
      * a remotely available file whose filename does not suggest mimetype.
+     *
+     * @covers ::mimetype_from_hash
+     * @covers ::<!public>
      */
     public function test_mimetype_from_hash_using_file_content_remote() {
         $filepath = '/path/to/file/not/currently/on/disk';
@@ -992,6 +1110,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_storedfile returns the correct mimetype with
      * a file whose filename suggests mimetype.
+     *
+     * @covers ::mimetype_from_storedfile
+     * @covers ::<!public>
      */
     public function test_mimetype_from_storedfile_empty() {
         $file = $this->get_stored_file('');
@@ -1004,6 +1125,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_storedfile returns the correct mimetype with
      * a file whose filename suggests mimetype.
+     *
+     * @covers ::mimetype_from_storedfile
+     * @covers ::<!public>
      */
     public function test_mimetype_from_storedfile_using_filename() {
         $filepath = '/path/to/file/not/currently/on/disk';
@@ -1019,6 +1143,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_storedfile returns the correct mimetype with
      * a locally available file whose filename does not suggest mimetype.
+     *
+     * @covers ::mimetype_from_storedfile
+     * @covers ::<!public>
      */
     public function test_mimetype_from_storedfile_using_file_content() {
         $filepath = __DIR__ . "/fixtures/testimage.jpg";
@@ -1034,6 +1161,9 @@ class core_files_file_system_testcase extends advanced_testcase {
     /**
      * Test that mimetype_from_storedfile returns the correct mimetype with
      * a remotely available file whose filename does not suggest mimetype.
+     *
+     * @covers ::mimetype_from_storedfile
+     * @covers ::<!public>
      */
     public function test_mimetype_from_storedfile_using_file_content_remote() {
         $filepath = __DIR__ . "/fixtures/testimage.jpg";
index e74de28..fa6bfe5 100644 (file)
@@ -78,7 +78,9 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
         // Execute it sending the standard error to $output.
         $result = exec($cmd . ' 2>&1', $output, $exitcode);
 
-        if ($result === self::REQUIRED_PIP_PACKAGE_VERSION) {
+        $vercheck = self::check_pip_package_version($result);
+
+        if ($vercheck === 0) {
             return true;
         }
 
@@ -87,8 +89,17 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
         }
 
         if ($result) {
-            $a = (object)array('installed' => $result, 'required' => self::REQUIRED_PIP_PACKAGE_VERSION);
-            return get_string('packageinstalledshouldbe', 'mlbackend_python', $a);
+            $a = [
+                'installed' => $result,
+                'required' => self::REQUIRED_PIP_PACKAGE_VERSION,
+            ];
+
+            if ($vercheck < 0) {
+                return get_string('packageinstalledshouldbe', 'mlbackend_python', $a);
+
+            } else if ($vercheck > 0) {
+                return get_string('packageinstalledtoohigh', 'mlbackend_python', $a);
+            }
         }
 
         return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
@@ -395,4 +406,39 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
         // This is not ideal, but there is no read access to moodle filesystem files.
         return $file->copy_content_to_temp('core_analytics');
     }
+
+    /**
+     * Check that the given package version can be used and return the error status.
+     *
+     * When evaluating the version, we assume the sematic versioning scheme as described at
+     * https://semver.org/.
+     *
+     * @param string $actual The actual Python package version
+     * @param string $required The required version of the package
+     * @return int -1 = actual version is too low, 1 = actual version too high, 0 = actual version is ok
+     */
+    public static function check_pip_package_version($actual, $required = self::REQUIRED_PIP_PACKAGE_VERSION) {
+
+        if (empty($actual)) {
+            return -1;
+        }
+
+        if (version_compare($actual, $required, '<')) {
+            return -1;
+        }
+
+        $parts = explode('.', $required);
+        $requiredapiver = reset($parts);
+
+        $parts = explode('.', $actual);
+        $actualapiver = reset($parts);
+
+        if ($requiredapiver > 0 || $actualapiver > 1) {
+            if (version_compare($actual, $requiredapiver + 1, '>=')) {
+                return 1;
+            }
+        }
+
+        return 0;
+    }
 }
index 1a1bb4f..f5bcb72 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 $string['packageinstalledshouldbe'] = '"moodlemlbackend" python package should be updated. The required version is "{$a->required}" and the installed version is "{$a->installed}"';
+$string['packageinstalledtoohigh'] = '"moodlemlbackend" python package is not compatible with this Moodle version. The required version is "{$a->required}" or higher as long as it is API-compatible. The installed version "{$a->installed}" is too high.';
 $string['pluginname'] = 'Python machine learning backend';
 $string['privacy:metadata'] = 'The Python machine learning backend plugin does not store any personal data.';
 $string['pythonpackagenotinstalled'] = '"moodlemlbackend" python package is not installed or there is a problem with it. Please execute "{$a}" from command line interface for more info';
diff --git a/lib/mlbackend/python/tests/processor_test.php b/lib/mlbackend/python/tests/processor_test.php
new file mode 100644 (file)
index 0000000..e9ceb51
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Provides the {@link mlbackend_python_processor_testcase} class.
+ *
+ * @package     mlbackend_python
+ * @category    test
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for the {@link \mlbackend_python\processor} class.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mlbackend_python_processor_testcase extends advanced_testcase {
+
+    /**
+     * Test implementation of the {@link \mlbackend_python\processor::check_pip_package_version()} method.
+     *
+     * @dataProvider check_pip_package_versions
+     * @param string $actual A sample of the actual package version
+     * @param string $required A sample of the required package version
+     * @param int $result Expected value returned by the tested method
+     */
+    public function test_check_pip_package_version($actual, $required, $result) {
+        $this->assertSame($result, \mlbackend_python\processor::check_pip_package_version($actual, $required));
+    }
+
+    /**
+     * Check that the {@link \mlbackend_python\processor::check_pip_package_version()} can be called with single argument.
+     */
+    public function test_check_pip_package_version_default() {
+
+        $this->assertSame(-1, \mlbackend_python\processor::check_pip_package_version('0.0.1'));
+        $this->assertSame(0, \mlbackend_python\processor::check_pip_package_version(
+            \mlbackend_python\processor::REQUIRED_PIP_PACKAGE_VERSION));
+    }
+
+    /**
+     * Provides data samples for the {@link self::test_check_pip_package_version()}.
+     *
+     * @return array
+     */
+    public function check_pip_package_versions() {
+        return [
+            // Exact match.
+            [
+                '0.0.5',
+                '0.0.5',
+                0,
+            ],
+            [
+                '1.0.0',
+                '1.0.0',
+                0,
+            ],
+            // Actual version higher than required, yet still API compatible.
+            [
+                '1.0.3',
+                '1.0.1',
+                0,
+            ],
+            [
+                '2.1.3',
+                '2.0.0',
+                0,
+            ],
+            [
+                '1.1.5',
+                '1.1',
+                0,
+            ],
+            [
+                '2.0.3',
+                '2',
+                0,
+            ],
+            // Actual version not high enough to meet the requirements.
+            [
+                '0.0.5',
+                '1.0.0',
+                -1,
+            ],
+            [
+                '0.37.0',
+                '1.0.0',
+                -1,
+            ],
+            [
+                '0.0.5',
+                '0.37.0',
+                -1,
+            ],
+            [
+                '2.0.0',
+                '2.0.2',
+                -1,
+            ],
+            [
+                '2.7.0',
+                '3.0',
+                -1,
+            ],
+            [
+                '2.8.9-beta1',
+                '3.0',
+                -1,
+            ],
+            [
+                '1.1.0-rc1',
+                '1.1.0',
+                -1,
+            ],
+            // Actual version too high and no longer API compatible.
+            [
+                '2.0.0',
+                '1.0.0',
+                1,
+            ],
+            [
+                '3.1.5',
+                '2.0',
+                1,
+            ],
+            [
+                '3.0.0',
+                '1.0',
+                1,
+            ],
+            [
+                '2.0.0',
+                '0.0.5',
+                1,
+            ],
+            [
+                '3.0.2',
+                '0.37.0',
+                1,
+            ],
+            // Zero major version requirement is fulfilled with 1.x API (0.x are not considered stable APIs).
+            [
+                '1.0.0',
+                '0.0.5',
+                0,
+            ],
+            [
+                '1.8.6',
+                '0.37.0',
+                0,
+            ],
+            // Empty version is never good enough.
+            [
+                '',
+                '1.0.0',
+                -1,
+            ],
+            [
+                '0.0.0',
+                '0.37.0',
+                -1,
+            ],
+        ];
+    }
+}
index de6960d..003ca15 100644 (file)
@@ -6047,6 +6047,7 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
         'siteshortname' => $SITE->shortname,
         'sitewwwroot' => $CFG->wwwroot,
         'subject' => $subject,
+        'prefix' => $CFG->emailsubjectprefix,
         'to' => $user->email,
         'toname' => fullname($user),
         'from' => $mail->From,
index e18fddf..b65db0c 100644 (file)
@@ -63,7 +63,7 @@ abstract class advanced_testcase extends base_testcase {
      * Runs the bare test sequence.
      * @return void
      */
-    final public function runBare() {
+    final public function runBare(): void {
         global $DB;
 
         if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
index c39ec4a..50052db 100644 (file)
@@ -43,7 +43,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class phpunit_autoloader implements \PHPUnit\Runner\TestSuiteLoader {
-    public function load($suiteClassName, $suiteClassFile = '') {
+    public function load(string $suiteClassName, string $suiteClassFile = ''): ReflectionClass {
         global $CFG;
 
         // Let's guess what user entered on the commandline...
@@ -197,7 +197,7 @@ class phpunit_autoloader implements \PHPUnit\Runner\TestSuiteLoader {
         return new ReflectionClass($classname);
     }
 
-    public function reload(ReflectionClass $aClass) {
+    public function reload(ReflectionClass $aClass): ReflectionClass {
         return $aClass;
     }
 }
index ba7d000..497f260 100644 (file)
@@ -57,7 +57,7 @@ abstract class basic_testcase extends base_testcase {
      * Runs the bare test sequence and log any changes in global state or database.
      * @return void
      */
-    final public function runBare() {
+    final public function runBare(): void {
         global $DB;
 
         try {
index 9407b7a..5fa1d25 100644 (file)
@@ -39,6 +39,20 @@ class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit\Framewo
      */
     protected $keys = array();
 
+    /**
+     * @var mixed $value Need to keep it here because it became private for PHPUnit 7.x and up
+     */
+    protected $capturedvalue;
+
+    /**
+     * Override constructor to capture value
+     */
+    public function __construct($value, float $delta = 0.0, int $maxDepth = 10, bool $canonicalize = false,
+                                bool $ignoreCase = false) {
+        parent::__construct($value, $delta, $maxDepth, $canonicalize, $ignoreCase);
+        $this->capturedvalue = $value;
+    }
+
     /**
      * Add an exception for the named key to use a different comparison
      * method. Any assertion provided by PHPUnit\Framework\Assert is
@@ -69,13 +83,13 @@ class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit\Framewo
      */
     public function evaluate($other, $description = '', $shouldreturnesult = false) {
         foreach ($this->keys as $key => $comparison) {
-            if (isset($other->$key) || isset($this->value->$key)) {
+            if (isset($other->$key) || isset($this->capturedvalue->$key)) {
                 // One of the keys is present, therefore run the comparison.
-                PHPUnit\Framework\Assert::$comparison($this->value->$key, $other->$key);
+                PHPUnit\Framework\Assert::$comparison($this->capturedvalue->$key, $other->$key);
 
                 // Unset the keys, otherwise the standard evaluation will take place.
                 unset($other->$key);
-                unset($this->value->$key);
+                unset($this->capturedvalue->$key);
             }
         }
 
index 5236a6e..63a99d4 100644 (file)
@@ -138,7 +138,7 @@ abstract class database_driver_testcase extends base_testcase {
      * Runs the bare test sequence.
      * @return void
      */
-    public function runBare() {
+    public function runBare(): void {
         try {
             parent::runBare();
 
index fb10e5a..228db3e 100644 (file)
@@ -48,7 +48,7 @@ class Hint_ResultPrinter extends PHPUnit\TextUI\ResultPrinter {
         parent::__construct(null, false, self::COLOR_DEFAULT, false);
     }
 
-    protected function printDefectTrace(PHPUnit\Framework\TestFailure $defect) {
+    protected function printDefectTrace(PHPUnit\Framework\TestFailure $defect): void {
         global $CFG;
 
         parent::printDefectTrace($defect);
index a8d93d9..562e22b 100644 (file)
     * replyto
     * replytoname
     * body
+    * prefix
 
     Example context (json):
     {
+        "prefix": "[Prefix Text]",
         "body": "Email body"
     }
 }}
index 8454983..88deb2e 100644 (file)
     * fromname
     * replyto
     * replytoname
+    * prefix
 
     Example context (json):
     {
+        "prefix": "[Prefix Text]",
         "subject": "Email subject"
     }
 }}
-{{{subject}}}
+{{#prefix}}{{{prefix}}} {{/prefix}}{{{subject}}}
index 7e537c1..6c55c30 100644 (file)
     * replyto
     * replytoname
     * body
+    * prefix
 
     Example context (json):
     {
+        "prefix": "[Prefix Text]",
         "body": "Email body"
     }
 }}
index 837b7a6..8ddcae4 100644 (file)
@@ -26,13 +26,14 @@ Feature: Close modals by clicking outside them
   @javascript
   Scenario: The popup closes when clicked on dead space - Modal
     Given I log in as "admin"
-    And I am on site homepage
-    And I click on "Calendar" "link"
-    And I click on "New event" "button"
+    And I follow "This month"
+    And I press "New event"
     When I click on "[data-region='modal-container']" "css_element"
     # The modal does not close becaue it contains a form.
     Then ".modal-backdrop" "css_element" should be visible
-    And ".modal-content" "css_element" should be visible
+    # Confirm that the contents of the new calendar event modal are visible.
+    And I should see "New event" in the ".modal-title" "css_element"
+    And I should see "Event title" in the ".modal-body" "css_element"
 
   @javascript
   Scenario: The popup help closes when clicked
index cc2dfbb..208aeb1 100644 (file)
@@ -76,7 +76,8 @@ class core_completionlib_testcase extends advanced_testcase {
      * @param  boolean $canonicalize
      * @param  boolean $ignoreCase
      */
-    public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
+    public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
+                                        bool $canonicalize = false, bool $ignoreCase = false): void {
         // Nasty cheating hack: prevent random failures on timemodified field.
         if (is_object($expected) and is_object($actual)) {
             if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
index d2e0265..fd3023a 100644 (file)
@@ -225,4 +225,45 @@ class core_message_testcase extends advanced_testcase {
         $eventsink->close();
         $sink->close();
     }
+
+    public function test_send_message_with_prefix() {
+        global $DB, $CFG;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        set_config('emailsubjectprefix', '[Prefix Text]');
+
+        // Test basic email processor.
+        $this->assertFileExists("$CFG->dirroot/message/output/email/version.php");
+        $this->assertFileExists("$CFG->dirroot/message/output/popup/version.php");
+
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+
+        // Check that prefix is ammended to the subject of the email.
+        $message = new \core\message\message();
+        $message->courseid = 1;
+        $message->component = 'moodle';
+        $message->name = 'instantmessage';
+        $message->userfrom = $user1;
+        $message->userto = $user2;
+        $message->subject = get_string('unreadnewmessage', 'message', fullname($user1));
+        $message->fullmessage = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml = '<p>message body</p>';
+        $message->smallmessage = 'small message';
+        $message->notification = '0';
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        $this->assertSame('[Prefix Text] '. get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
+        $sink->clear();
+    }
 }
index b3b8c9d..0c71552 100644 (file)
@@ -653,4 +653,205 @@ class core_session_manager_testcase extends advanced_testcase {
         $this->assertEquals($real, $user1);
         $this->assertSame($_SESSION['REALUSER'], $real);
     }
+
+    /**
+     * Session lock info on pages.
+     *
+     * @return array
+     */
+    public function pages_sessionlocks() {
+        return [
+            [
+                'url'      => '/good.php',
+                'start'    => 1500000001.000,
+                'gained'   => 1500000002.000,
+                'released' => 1500000003.000,
+                'wait'     => 1.0,
+                'held'     => 1.0
+            ],
+            [
+                'url'      => '/bad.php?wait=5',
+                'start'    => 1500000003.000,
+                'gained'   => 1500000005.000,
+                'released' => 1500000007.000,
+                'held'     => 2.0,
+                'wait'     => 2.0
+            ]
+        ];
+    }
+
+    /**
+     * Test to get recent session locks.
+     */
+    public function test_get_recent_session_locks() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->debugsessionlock = 5;
+        $pages = $this->pages_sessionlocks();
+        // Recent session locks must be empty at first.
+        $recentsessionlocks = \core\session\manager::get_recent_session_locks();
+        $this->assertEmpty($recentsessionlocks);
+
+        // Add page to the recentsessionlocks array.
+        \core\session\manager::update_recent_session_locks($pages[0]);
+        $recentsessionlocks = \core\session\manager::get_recent_session_locks();
+        // Make sure we are getting the first page we added.
+        $this->assertEquals($pages[0], $recentsessionlocks[0]);
+        // There should be 1 page in the array.
+        $this->assertCount(1, $recentsessionlocks);
+
+        // Add second page to the recentsessionlocks array.
+        \core\session\manager::update_recent_session_locks($pages[1]);
+        $recentsessionlocks = \core\session\manager::get_recent_session_locks();
+        // Make sure we are getting the second page we added.
+        $this->assertEquals($pages[1], $recentsessionlocks[1]);
+        // There should be 2 pages in the array.
+        $this->assertCount(2, $recentsessionlocks);
+    }
+
+    /**
+     * Test to update recent session locks.
+     */
+    public function test_update_recent_session_locks() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->debugsessionlock = 5;
+        $pages = $this->pages_sessionlocks();
+
+        \core\session\manager::update_recent_session_locks($pages[0]);
+        \core\session\manager::update_recent_session_locks($pages[1]);
+        $recentsessionlocks = \core\session\manager::get_recent_session_locks();
+        // There should be 2 pages in the array.
+        $this->assertCount(2, $recentsessionlocks);
+        // Make sure the last page is added at the end of the array.
+        $this->assertEquals($pages[1], end($recentsessionlocks));
+
+    }
+
+    /**
+     * Test to get session lock info.
+     */
+    public function test_get_session_lock_info() {
+        global $PERF;
+
+        $this->resetAfterTest();
+
+        $pages = $this->pages_sessionlocks();
+        $PERF->sessionlock = $pages[0];
+        $sessionlock = \core\session\manager::get_session_lock_info();
+        $this->assertEquals($pages[0], $sessionlock);
+    }
+
+    /**
+     * Session lock info on some pages to serve as history.
+     *
+     * @return array
+     */
+    public function sessionlock_history() {
+        return [
+            [
+                'url'      => '/good.php',
+                'start'    => 1500000001.000,
+                'gained'   => 1500000001.100,
+                'released' => 1500000001.500,
+                'wait'     => 0.1
+            ],
+            [
+                // This bad request doesn't release the session for 10 seconds.
+                'url'      => '/bad.php',
+                'start'    => 1500000012.000,
+                'gained'   => 1500000012.200,
+                'released' => 1500000020.200,
+                'wait'     => 0.2
+            ],
+            [
+                // All subsequent requests are blocked and need to wait.
+                'url'      => '/good.php?id=1',
+                'start'    => 1500000012.900,
+                'gained'   => 1500000020.200,
+                'released' => 1500000022.000,
+                'wait'     => 7.29
+            ],
+            [
+                'url'      => '/good.php?id=2',
+                'start'    => 1500000014.000,
+                'gained'   => 1500000022.000,
+                'released' => 1500000025.000,
+                'wait'     => 8.0
+            ],
+            [
+                'url'      => '/good.php?id=3',
+                'start'    => 1500000015.000,
+                'gained'   => 1500000025.000,
+                'released' => 1500000026.000,
+                'wait'     => 10.0
+            ],
+            [
+                'url'      => '/good.php?id=4',
+                'start'    => 1500000016.000,
+                'gained'   => 1500000026.000,
+                'released' => 1500000027.000,
+                'wait'     => 10.0
+            ]
+        ];
+    }
+
+    /**
+     * Data provider for test_get_locked_page_at function.
+     *
+     * @return array
+     */
+    public function sessionlocks_info_provider() : array {
+        return [
+            [
+                'url'      => null,
+                'time'    => 1500000001.000
+            ],
+            [
+                'url'      => '/bad.php',
+                'time'    => 1500000014.000
+            ],
+            [
+                'url'      => '/good.php?id=2',
+                'time'    => 1500000022.500
+            ],
+        ];
+    }
+
+    /**
+     * Test to get locked page at a speficic timestamp.
+     *
+     * @dataProvider sessionlocks_info_provider
+     * @param array $url Session lock page url.
+     * @param array $time Session lock time.
+     */
+    public function test_get_locked_page_at($url, $time) {
+        global $CFG, $SESSION;
+
+        $this->resetAfterTest();
+        $CFG->debugsessionlock = 5;
+        $SESSION->recentsessionlocks = $this->sessionlock_history();
+
+        $page = \core\session\manager::get_locked_page_at($time);
+        $this->assertEquals($url, $page['url']);
+    }
+
+    /**
+     * Test cleanup recent session locks.
+     */
+    public function test_cleanup_recent_session_locks() {
+        global $CFG, $SESSION;
+
+        $this->resetAfterTest();
+        $CFG->debugsessionlock = 5;
+
+        $SESSION->recentsessionlocks = $this->sessionlock_history();
+        $this->assertCount(6, $SESSION->recentsessionlocks);
+        \core\session\manager::cleanup_recent_session_locks();
+        // Make sure the session history has been cleaned up and only has the latest page.
+        $this->assertCount(1, $SESSION->recentsessionlocks);
+        $this->assertEquals('/good.php?id=4', $SESSION->recentsessionlocks[0]['url']);
+    }
 }
index 8c19457..e40e486 100644 (file)
@@ -164,7 +164,7 @@ class core_analytics_targets_testcase extends advanced_testcase {
     }
 
     /**
-     * Test valid analysable conditions.
+     * Test the conditions of a valid analysable, both common and specific to this target (course_completion).
      *
      * @dataProvider analysable_provider
      * @param mixed $courseparams Course data
@@ -217,7 +217,7 @@ class core_analytics_targets_testcase extends advanced_testcase {
     }
 
     /**
-     * Test valid sample conditions.
+     * Test the conditions of a valid sample, both common and specific to this target (course_completion).
      *
      * @dataProvider sample_provider
      * @param int $coursestart Course start date
@@ -252,4 +252,93 @@ class core_analytics_targets_testcase extends advanced_testcase {
 
         $this->assertEquals($isvalid, $target->is_valid_sample($sampleid, $analysable));
     }
+
+    /**
+     * Setup user, framework, competencies and course competencies.
+     */
+    protected function setup_competencies_environment() {
+        $this->resetAfterTest(true);
+        $now = time();
+        $this->setAdminUser();
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+
+        $course = $dg->create_course(array('startdate' => $now - WEEKSECS, 'enddate' => $now - DAYSECS));
+        $coursenocompetencies = $dg->create_course(array('startdate' => $now - WEEKSECS, 'enddate' => $now - DAYSECS));
+
+        $u1 = $dg->create_user();
+        $this->getDataGenerator()->enrol_user($u1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($u1->id, $coursenocompetencies->id);
+        $f1 = $lpg->create_framework();
+        $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $c4 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $cc1 = $lpg->create_course_competency(array('competencyid' => $c1->get('id'), 'courseid' => $course->id,
+            'ruleoutcome' => \core_competency\course_competency::OUTCOME_NONE));
+        $cc2 = $lpg->create_course_competency(array('competencyid' => $c2->get('id'), 'courseid' => $course->id,
+            'ruleoutcome' => \core_competency\course_competency::OUTCOME_EVIDENCE));
+        $cc3 = $lpg->create_course_competency(array('competencyid' => $c3->get('id'), 'courseid' => $course->id,
+            'ruleoutcome' => \core_competency\course_competency::OUTCOME_RECOMMEND));
+        $cc4 = $lpg->create_course_competency(array('competencyid' => $c4->get('id'), 'courseid' => $course->id,
+            'ruleoutcome' => \core_competency\course_competency::OUTCOME_COMPLETE));
+
+        return array(
+            'course' => $course,
+            'coursenocompetencies' => $coursenocompetencies,
+            'user' => $u1,
+            'course_competencies' => array($cc1, $cc2, $cc3, $cc4)
+        );
+    }
+
+     /**
+      * Test the specific conditions of a valid analysable for the course_competencies target.
+      */
+    public function test_core_target_course_competencies_analysable() {
+
+        $data = $this->setup_competencies_environment();
+
+        $analysable = new \core_analytics\course($data['course']);
+        $target = new \core\analytics\target\course_competencies();
+
+        $this->assertTrue($target->is_valid_analysable($analysable));
+
+        $analysable = new \core_analytics\course($data['coursenocompetencies']);
+        $this->assertEquals(get_string('nocompetenciesincourse', 'tool_lp'), $target->is_valid_analysable($analysable));
+    }
+
+    /**
+     * Test the target value calculation.
+     */
+    public function test_core_target_course_competencies_calculate() {
+
+        $data = $this->setup_competencies_environment();
+
+        $target = new \core\analytics\target\course_competencies();
+        $analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []);
+        $analysable = new \core_analytics\course($data['course']);
+
+        $class = new ReflectionClass('\core\analytics\analyser\student_enrolments');
+        $method = $class->getMethod('get_all_samples');
+        $method->setAccessible(true);
+
+        list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
+        $target->add_sample_data($samplesdata);
+        $sampleid = reset($sampleids);
+
+        $class = new ReflectionClass('\core\analytics\target\course_competencies');
+        $method = $class->getMethod('calculate_sample');
+        $method->setAccessible(true);
+
+        // Method calculate_sample() returns 1 when the user has not achieved all the competencies assigned to the course.
+        $this->assertEquals(1, $method->invoke($target, $sampleid, $analysable));
+
+        // Grading of all the competences assigned to the course, in such way that the user achieves them all.
+        foreach ($data['course_competencies'] as $competency) {
+            \core_competency\api::grade_competency_in_course($data['course']->id, $data['user']->id,
+                    $competency->get('competencyid'), 3, 'Unit test');
+        }
+        // Method calculate_sample() returns 0 when the user has achieved all the competencies assigned to the course.
+        $this->assertEquals(0, $method->invoke($target, $sampleid, $analysable));
+    }
 }
index 318d570..21c2e3a 100644 (file)
@@ -430,6 +430,12 @@ function upgrade_stale_php_files_present() {
     global $CFG;
 
     $someexamplesofremovedfiles = array(
+        // Removed in 3.7.
+        '/lib/form/yui/src/showadvanced/js/showadvanced.js',
+        '/lib/tests/output_external_test.php',
+        '/message/amd/src/message_area.js',
+        '/message/templates/message_area.mustache',
+        '/question/yui/src/qbankmanager/build.json',
         // Removed in 3.6.
         '/lib/classes/session/memcache.php',
         '/lib/eventslib.php',
index bb395da..8d24769 100644 (file)
@@ -49,6 +49,7 @@
         {{/conversationid}}
         data-contact-user-id="{{id}}"
         data-region="contact"
+        role="button"
     >
         <img
             class="rounded-circle"
index df496af..d6d753e 100644 (file)
@@ -42,6 +42,7 @@
         {{#userid}}
             data-user-id="{{.}}"
         {{/userid}}
+        role="button"
     >
         {{#imageurl}}
             <img
index fb96cf6..97332f7 100644 (file)
@@ -48,6 +48,7 @@
             data-route-param-3="{{userid}}"
         {{/conversationid}}
         data-conversation-id="{{conversationid}}"
+        role="button"
     >
         <img
             class="rounded-circle"
index 9da0778..ae5c9a9 100644 (file)
@@ -47,6 +47,7 @@
             data-route-param-2="create"
             data-route-param-3="{{id}}"
         {{/conversationid}}
+        role="button"
     >
         <img
             class="rounded-circle"
index 472e438..ea355c2 100644 (file)
@@ -35,7 +35,7 @@
 
 }}
 
-<a class="align-self-center" href="#" data-route-back>
+<a class="align-self-center" href="#" data-route-back role="button">
     {{> core_message/message_drawer_icon_back }}
 </a>
 <div class="px-3 pb-3">
index 17a9eea..4ca2482 100644 (file)
@@ -49,6 +49,7 @@
         {{/conversationid}}
         data-request-id="{{id}}"
         data-region="contact-request"
+        role="button"
     >
         <img
             class="rounded-circle"
index b4634e4..6c816bf 100644 (file)
@@ -37,7 +37,7 @@
 <div class="hidden border-bottom px-2 py-3" aria-hidden="true" data-region="view-contacts">
     <div class="d-flex align-items-center">
         <div class="align-self-stretch">
-            <a class="h-100 d-flex align-items-center mr-2" href="#" data-route-back>
+            <a class="h-100 d-flex align-items-center mr-2" href="#" data-route-back role="button">
                 {{> core_message/message_drawer_icon_back }}
             </a>
         </div>
@@ -45,7 +45,7 @@
             {{#str}} contacts, core_message {{/str}}
         </div>
         <div class="ml-auto">
-            <a href="#" data-route="view-search">
+            <a href="#" data-route="view-search" role="button" aria-label="{{#str}} search, core_search {{/str}}">
                 {{#pix}} a/search, core {{/pix}}
             </a>
         </div>
index da37323..e28b29a 100644 (file)
 
 <div class="d-flex align-items-center">
     <div class="align-self-stretch" >
-        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back>
+        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
             {{> core_message/message_drawer_icon_back }}
         </a>
     </div>
     <div class="d-flex text-truncate">
-        <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-contact">
+        <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-contact" role="button">
             {{#imageurl}}
                 <div class="d-flex align-items-center">
                     <img
@@ -57,7 +57,8 @@
             <div class="w-100 text-truncate ml-2">
                 <div class="d-flex">
                     <strong class="m-0 text-truncate">{{name}}</strong>
-                    <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
+                    <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container"
+                    aria-label="{{#str}} favourites, core {{/str}}">
                         {{#pix}} i/star-rating, core {{/pix}}
                     </span>
                     <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
index 4422fcd..d35d686 100644 (file)
@@ -37,7 +37,7 @@
 
 <div class="d-flex align-items-center">
     <div class="align-self-stretch" >
-        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back>
+        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
             {{> core_message/message_drawer_icon_back }}
         </a>
     </div>
@@ -56,7 +56,8 @@
         <div class="w-100 text-truncate ml-2">
             <div class="d-flex">
                 <strong class="m-0 text-truncate">{{name}}</strong>
-                <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
+                <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container"
+                aria-label="{{#str}} favourites, core {{/str}}">
                     {{#pix}} i/star-rating, core {{/pix}}
                 </span>
                 <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
index 5a75cc6..7423faa 100644 (file)
 <div class="d-flex flex-column">
     <div class="d-flex align-items-center">
         <div class="align-self-stretch" >
-            <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back>
+            <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
                 {{> core_message/message_drawer_icon_back }}
             </a>
         </div>
         <div class="d-flex text-truncate">
-            <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-group-info">
+            <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-group-info" role="button">
                 {{#imageurl}}
                     <img
                         class="rounded-circle"
@@ -56,7 +56,8 @@
                 <div class="w-100 text-truncate ml-2">
                     <div class="d-flex">
                         <strong class="m-0 text-truncate">{{name}}</strong>
-                        <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
+                        <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary"
+                        data-region="favourite-icon-container" aria-label="{{#str}} favourites, core {{/str}}">
                             {{#pix}} i/star-rating, core {{/pix}}
                         </span>
                         <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
@@ -95,7 +96,7 @@
         <div class="mr-2 icon" aria-hidden="true"></div>
         {{#imageurl}}<div style="width: 38px" aria-hidden="true"></div>{{/imageurl}}
         <!-- End placeholders -->
-        <a class="text-decoration-none line-height-3 ml-2" href="#" data-action="view-group-info">
+        <a class="text-decoration-none line-height-3 ml-2" href="#" data-action="view-group-info" role="button">
             <small class="m-0 text-muted text-truncate">
                 {{#str}} numparticipants, core_message, {{totalmembercount}} {{/str}}
             </small>
index 1327ec1..1cbdbda 100644 (file)
@@ -36,7 +36,7 @@
 }}
 <div class="d-flex">
     <div class="align-self-stretch" >
-        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back>
+        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
             {{> core_message/message_drawer_icon_back }}
         </a>
     </div>
index 1e484ab..e7ce405 100644 (file)
@@ -35,7 +35,7 @@
 
 }}
 
-<a class="px-2 align-self-start" href="#" data-route-back>
+<a class="px-2 align-self-start" href="#" data-route-back role="button">
     {{> core_message/message_drawer_icon_back }}
 </a>
 <div class="px-2">
index fc7d839..c5630c4 100644 (file)
@@ -49,6 +49,7 @@
         {{/conversationid}}
         data-contact-user-id="{{id}}"
         data-region="contact"
+        role="button"
     >
         <img
             class="rounded-circle"
index 7779456..460fcd3 100644 (file)
                 href="#"
                 data-route="view-settings"
                 data-route-param="{{loggedinuser.id}}"
+                aria-label="{{#str}} settings, core_message {{/str}}"
+                role="button"
             >
                 {{#pix}} t/edit, core {{/pix}}
             </a>
         </div>
     </div>
     <div class="text-right mt-3">
-        <a href="#" data-route="view-contacts">
+        <a href="#" data-route="view-contacts" role="button">
             {{#pix}} i/user, core {{/pix}}
             {{#str}} contacts, core_message {{/str}}
             <span class="badge bg-primary ml-2 {{^contactrequestcount}}hidden{{/contactrequestcount}}" data-region="contact-request-count">
index 8ced531..627bc24 100644 (file)
@@ -41,6 +41,7 @@
             href="#"
             data-route-back
             data-action="cancel-search"
+            role="button"
         >
             {{> core_message/message_drawer_icon_back }}
         </a>
@@ -57,6 +58,7 @@
                     class="btn btn-outline-secondary"
                     type="button"
                     data-action="search"
+                    aria-label="{{#str}} search, core_search {{/str}}"
                 >
                     <span data-region="search-icon-container">
                         {{#pix}} a/search, core {{/pix}}
index 3411da4..9ac89fb 100644 (file)
@@ -37,7 +37,7 @@
 <div class="hidden border-bottom px-2 py-3" aria-hidden="true" data-region="view-settings">
     <div class="d-flex align-items-center">
         <div class="align-self-stretch" >
-            <a class="h-100 d-flex mr-2 align-items-center" href="#" data-route-back>
+            <a class="h-100 d-flex mr-2 align-items-center" href="#" data-route-back role="button">
                 {{> core_message/message_drawer_icon_back }}
             </a>
         </div>
index 09c1748..e314506 100644 (file)
@@ -36,7 +36,8 @@
 
 }}
 <div class="float-right popover-region collapsed">
-    <a id="message-drawer-toggle-{{uniqid}}" class="nav-link d-inline-block popover-region-toggle position-relative" href="#">
+    <a id="message-drawer-toggle-{{uniqid}}" class="nav-link d-inline-block popover-region-toggle position-relative" href="#"
+            role="button">
         {{#pix}} t/message, core, {{#str}} togglemessagemenu, message {{/str}} {{/pix}}
         <div class="count-container {{^unreadcount}}hidden{{/unreadcount}}" data-region="count-container">{{unreadcount}}</div>
     </a>
index b030b51..d14a157 100644 (file)
@@ -47,7 +47,8 @@
             data-processor-setting
             data-user-id="{{userid}}"
             data-context-id="{{contextid}}"
-            data-name="{{name}}">
+            data-name="{{name}}"
+            role="button">
 
             {{< core/hover_tooltip }}
                 {{$anchor}}
index e1ec87f..07190be 100644 (file)
@@ -39,6 +39,7 @@ Feature: Manage contacts
     Then I should see "2" in the "//div[@data-region='view-contacts']//*[@data-region='contact-request-count']" "xpath_element"
     And I click on "Requests" "link_or_button"
     And I click on "Student 1 Would like to contact you" "link"
+    Then I should see "Accept and add to contacts"
     And I click on "Accept and add to contacts" "link_or_button"
     And I log out
     And I log in as "student1"
@@ -61,6 +62,7 @@ Feature: Manage contacts
     Then I should see "1" in the "//div[@data-region='view-contacts']//*[@data-region='contact-request-count']" "xpath_element"
     And I click on "Requests" "link_or_button"
     And I click on "Student 1 Would like to contact you" "link"
+    Then I should see "Accept and add to contacts"
     And I click on "Decline" "link_or_button"
     And I open contact menu
     Then I should see "Add to contacts" in the "//div[@data-region='header-container']" "xpath_element"
index fe50250..9b6eb99 100644 (file)
@@ -629,7 +629,7 @@ class assign_events_testcase extends advanced_testcase {
         );
         $assign->testable_process_save_quick_grades($data);
         $grade = $assign->get_user_grade($student->id, false);
-        $this->assertEquals('60.0', $grade->grade);
+        $this->assertEquals(60.0, $grade->grade);
 
         $events = $sink->get_events();
         $this->assertCount(3, $events);
@@ -655,7 +655,7 @@ class assign_events_testcase extends advanced_testcase {
         $data->grade = '50.0';
         $assign->update_grade($data);
         $grade = $assign->get_user_grade($student->id, false, 0);
-        $this->assertEquals('50.0', $grade->grade);
+        $this->assertEquals(50.0, $grade->grade);
         $events = $sink->get_events();
 
         $this->assertCount(3, $events);
index e0b3e63..30ebeb5 100644 (file)
@@ -1134,7 +1134,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $result = mod_assign_external::get_grades(array($instance->id));
         $result = external_api::clean_returnvalue(mod_assign_external::get_grades_returns(), $result);
 
-        $this->assertEquals($result['assignments'][0]['grades'][0]['grade'], '50.0');
+        $this->assertEquals((float)$result['assignments'][0]['grades'][0]['grade'], '50.0');
     }
 
     /**
@@ -1284,13 +1284,13 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
                                          array('userid' => $student1->id, 'assignment' => $instance->id),
                                          '*',
                                          MUST_EXIST);
-        $this->assertEquals($student1grade->grade, '50.0');
+        $this->assertEquals((float)$student1grade->grade, '50.0');
 
         $student2grade = $DB->get_record('assign_grades',
                                          array('userid' => $student2->id, 'assignment' => $instance->id),
                                          '*',
                                          MUST_EXIST);
-        $this->assertEquals($student2grade->grade, '100.0');
+        $this->assertEquals((float)$student2grade->grade, '100.0');
     }
 
     /**
index 602d0d9..b9b7dc1 100644 (file)
@@ -3640,7 +3640,7 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $result = $assign->testable_process_save_quick_grades($data);
         $this->assertContains(get_string('quickgradingchangessaved', 'assign'), $result);
         $grade = $assign->get_user_grade($student->id, false);
-        $this->assertEquals('60.0', $grade->grade);
+        $this->assertEquals(60.0, $grade->grade);
 
         // Attempt to grade with a past attempts grade info.
         $assign->testable_process_add_attempt($student->id);
@@ -3664,7 +3664,7 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $result = $assign->testable_process_save_quick_grades($data);
         $this->assertContains(get_string('quickgradingchangessaved', 'assign'), $result);
         $grade = $assign->get_user_grade($student->id, false);
-        $this->assertEquals('40.0', $grade->grade);
+        $this->assertEquals(40.0, $grade->grade);
 
         // Catch grade update conflicts.
         // Save old data for later.
@@ -3678,13 +3678,13 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $result = $assign->testable_process_save_quick_grades($data);
         $this->assertContains(get_string('quickgradingchangessaved', 'assign'), $result);
         $grade = $assign->get_user_grade($student->id, false);
-        $this->assertEquals('30.0', $grade->grade);
+        $this->assertEquals(30.0, $grade->grade);
 
         // Now update using 'old' data. Should fail.
         $result = $assign->testable_process_save_quick_grades($pastdata);
         $this->assertContains(get_string('errorrecordmodified', 'assign'), $result);
         $grade = $assign->get_user_grade($student->id, false);
-        $this->assertEquals('30.0', $grade->grade);
+        $this->assertEquals(30.0, $grade->grade);
     }
 
     /**
index 185d5aa..fe2f9aa 100644 (file)
@@ -315,8 +315,8 @@ class mod_assign_privacy_testcase extends provider_testcase {
         $this->assertEquals(1, $writer->get_data(['attempt 1', 'submission'])->attemptnumber);
         $this->assertEquals(2, $writer->get_data(['attempt 2', 'submission'])->attemptnumber);
         // Check grades.
-        $this->assertEquals($grade1, $writer->get_data(['attempt 1', 'grade'])->grade);
-        $this->assertEquals($grade2, $writer->get_data(['attempt 2', 'grade'])->grade);
+        $this->assertEquals((float)$grade1, $writer->get_data(['attempt 1', 'grade'])->grade);
+        $this->assertEquals((float)$grade2, $writer->get_data(['attempt 2', 'grade'])->grade);
         // Check feedback.
         $this->assertContains($teachercommenttext, $writer->get_data(['attempt 1', 'Feedback comments'])->commenttext);
         $this->assertContains($teachercommenttext2, $writer->get_data(['attempt 2', 'Feedback comments'])->commenttext);
@@ -425,11 +425,11 @@ class mod_assign_privacy_testcase extends provider_testcase {
 
         // Check for student grades given.
         $student1grade = $writer->get_data(['studentsubmissions', $user1->id, 'attempt 1', 'grade']);
-        $this->assertEquals($grade1, $student1grade->grade);
+        $this->assertEquals((float)$grade1, $student1grade->grade);
         $student2grade1 = $writer->get_data(['studentsubmissions', $user2->id, 'attempt 1', 'grade']);
-        $this->assertEquals($grade2, $student2grade1->grade);
+        $this->assertEquals((float)$grade2, $student2grade1->grade);
         $student2grade2 = $writer->get_data(['studentsubmissions', $user2->id, 'attempt 2', 'grade']);
-        $this->assertEquals($grade3, $student2grade2->grade);
+        $this->assertEquals((float)$grade3, $student2grade2->grade);
         // Check for feedback given to students.
         $this->assertContains($teachercommenttext, $writer->get_data(['studentsubmissions', $user1->id, 'attempt 1',
                 'Feedback comments'])->commenttext);
index 7bf40ab..2bca6e4 100644 (file)
@@ -323,7 +323,7 @@ class mod_feedback_complete_form extends moodleform {
 
         // Set default value.
         if ($setdefaultvalue && ($tmpvalue = $this->get_item_value($item))) {
-            $this->_form->setDefault($element->getName(), $tmpvalue);
+            $this->_form->setDefault($element->getName(), htmlspecialchars_decode($tmpvalue, ENT_QUOTES));
         }
 
         // Freeze if needed.
index 3339cfe..4047f41 100644 (file)
@@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor;
 use mod_forum\local\vaults\preprocessors\extract_user as extract_user_preprocessor;
 use mod_forum\local\renderers\discussion_list as discussion_list_renderer;
+use core\dml\table as dml_table;
 use stdClass;
 
 /**
@@ -89,22 +90,22 @@ class discussion_list extends db_table_vault {
         // - First post
         // - Author
         // - Most recent editor.
-        $tablefields = $db->get_preload_columns(self::TABLE, $alias);
-        $postfields = $db->get_preload_columns('forum_posts', 'p_');
+        $thistable = new dml_table(self::TABLE, $alias, $alias);
+        $posttable = new dml_table('forum_posts', 'fp', 'p_');
         $firstauthorfields = \user_picture::fields('fa', null, self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS);
         $latestuserfields = \user_picture::fields('la', null, self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS);
 
         $fields = implode(', ', [
-            $db->get_preload_columns_sql($tablefields, $alias),
-            $db->get_preload_columns_sql($postfields, 'fp'),
+            $thistable->get_field_select(),
+            $posttable->get_field_select(),
             $firstauthorfields,
             $latestuserfields,
         ]);
 
-        $tables = '{' . self::TABLE . '} ' . $alias;
+        $tables = $thistable->get_from_sql();
         $tables .= ' JOIN {user} fa ON fa.id = ' . $alias . '.userid';
         $tables .= ' JOIN {user} la ON la.id = ' . $alias . '.usermodified';
-        $tables .= ' JOIN {forum_posts} fp ON fp.id = ' . $alias . '.firstpost';
+        $tables .= ' JOIN ' . $posttable->get_from_sql() . ' ON fp.id = ' . $alias . '.firstpost';
 
         $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables;
         $selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
@@ -139,8 +140,8 @@ class discussion_list extends db_table_vault {
         return array_merge(
             parent::get_preprocessors(),
             [
-                'discussion' => new extract_record_preprocessor($this->get_db(), self::TABLE, $this->get_table_alias()),
-                'firstpost' => new extract_record_preprocessor($this->get_db(), 'forum_posts', 'p_'),
+                'discussion' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()),
+                'firstpost' => new extract_record_preprocessor('forum_posts', 'p_'),
                 'firstpostauthor' => new extract_user_preprocessor(self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS),
                 'latestpostauthor' => new extract_user_preprocessor(self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS),
             ]
index 5804514..b224466 100644 (file)
@@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 use mod_forum\local\entities\forum as forum_entity;
 use mod_forum\local\vaults\preprocessors\extract_context as extract_context_preprocessor;
 use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor;
+use core\dml\table as dml_table;
 use context_helper;
 
 /**
@@ -65,22 +66,23 @@ class forum extends db_table_vault {
     protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string {
         $db = $this->get_db();
         $alias = $this->get_table_alias();
-        $tablefields = $db->get_preload_columns(self::TABLE, $alias);
-        $coursemodulefields = $db->get_preload_columns('course_modules', 'cm_');
-        $coursefields = $db->get_preload_columns('course', 'c_');
+
+        $thistable = new dml_table(self::TABLE, $alias, $alias);
+        $cmtable = new dml_table('course_modules', 'cm', 'cm_');
+        $coursetable = new dml_table('course', 'c', 'c_');
 
         $fields = implode(', ', [
-            $db->get_preload_columns_sql($tablefields, $alias),
+            $thistable->get_field_select(),
             context_helper::get_preload_record_columns_sql('ctx'),
-            $db->get_preload_columns_sql($coursemodulefields, 'cm'),
-            $db->get_preload_columns_sql($coursefields, 'c'),
+            $cmtable->get_field_select(),
+            $coursetable->get_field_select(),
         ]);
 
-        $tables = '{' . self::TABLE . '} ' . $alias;
+        $tables = $thistable->get_from_sql();
         $tables .= " JOIN {modules} m ON m.name = 'forum'";
-        $tables .= " JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = {$alias}.id";
+        $tables .= " JOIN " . $cmtable->get_from_sql() . " ON cm.module = m.id AND cm.instance = {$alias}.id";
         $tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE .  ' AND ctx.instanceid = cm.id';
-        $tables .= " JOIN {course} c ON c.id = {$alias}.course";
+        $tables .= " JOIN " . $coursetable->get_from_sql() . " ON c.id = {$alias}.course";
 
         $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables;
         $selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
@@ -99,9 +101,9 @@ class forum extends db_table_vault {
         return array_merge(
             parent::get_preprocessors(),
             [
-                'forum' => new extract_record_preprocessor($this->get_db(), self::TABLE, $this->get_table_alias()),
-                'course_module' => new extract_record_preprocessor($this->get_db(), 'course_modules', 'cm_'),
-                'course' => new extract_record_preprocessor($this->get_db(), 'course', 'c_'),
+                'forum' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()),
+                'course_module' => new extract_record_preprocessor('course_modules', 'cm_'),
+                'course' => new extract_record_preprocessor('course', 'c_'),
                 'context' => new extract_context_preprocessor(),
             ]
         );
index 0943d21..2bccefd 100644 (file)
@@ -27,6 +27,7 @@ namespace mod_forum\local\vaults\preprocessors;
 defined('MOODLE_INTERNAL') || die();
 
 use moodle_database;
+use core\dml\table as dml_table;
 
 /**
  * Extract record vault preprocessor.
@@ -38,24 +39,17 @@ use moodle_database;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class extract_record {
-    /** @var moodle_database $db A moodle database */
-    private $db;
-    /** @var string $table The table name where the records were loaded from */
+    /** @var \core\dml\table $table The table object relating to the table that the records were loaded from */
     private $table;
-    /** @var string $alias The table alias used as the record property prefix */
-    private $alias;
 
     /**
      * Constructor.
      *
-     * @param moodle_database $db A moodle database
      * @param string $table The table name where the records were loaded from
      * @param string $alias The table alias used as the record property prefix
      */
-    public function __construct(moodle_database $db, string $table, string $alias) {
-        $this->db = $db;
-        $this->table = $table;
-        $this->alias = $alias;
+    public function __construct(string $table, string $alias) {
+        $this->table = new dml_table($table, $alias, $alias);
     }
 
     /**
@@ -66,11 +60,8 @@ class extract_record {
      * @return stdClass[] The extracted records
      */
     public function execute(array $records) : array {
-        $db = $this->db;
-        $fields = $this->db->get_preload_columns($this->table, $this->alias);
-
-        return array_map(function($record) use ($db, $fields) {
-            return $db->extract_fields_from_object($fields, $record);
+        return array_map(function($record) {
+            return $this->table->extract_from_result($record);
         }, $records);
     }
 }
index 9b5c930..6f6d6b9 100644 (file)
@@ -170,6 +170,7 @@ class lesson_page_type_shortanswer extends lesson_page {
                 $result->newpageid = $answer->jumpto;
                 $options = new stdClass();
                 $options->para = false;
+                $options->noclean = true;
                 $result->response = format_text($answer->response, $answer->responseformat, $options);
                 $result->answerid = $answer->id;
                 break; // quit answer analysis immediately after a match has been found
index 3556793..0ec00de 100644 (file)
@@ -338,7 +338,8 @@ class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
                     $this->assertEquals((bool)$value, $attemptobj->is_finished());
                     break;
                 case 'summarks' :
-                    $this->assertEquals($value, $attemptobj->get_sum_marks(), "Sum of marks of attempt {$result['quizattempt']}.");
+                    $this->assertEquals((float)$value, $attemptobj->get_sum_marks(),
+                        "Sum of marks of attempt {$result['quizattempt']}.");
                     break;
                 case 'quizgrade' :
                     // Check quiz grades.
index e0c6d81..a59388d 100644 (file)
@@ -189,19 +189,19 @@ class mod_workshop_privacy_provider_testcase extends advanced_testcase {
         // Student1 has data in workshop11 (author + self reviewer), workshop12 (author) and workshop21 (reviewer).
         $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student1->id);
         $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist);
-        $this->assertEquals([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids(), null, 0.0, 10, true);
+        $this->assertEquals([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids(), '', 0.0, 10, true);
 
         // Student2 has data in workshop11 (reviewer), workshop12 (reviewer) and workshop21 (author).
         $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student2->id);
-        $this->assertEquals([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids(), null, 0.0, 10, true);
+        $this->assertEquals([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids(), '', 0.0, 10, true);
 
         // Student3 has data in workshop11 (reviewer).
         $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student3->id);
-        $this->assertEquals([$context11->id], $contextlist->get_contextids(), null, 0.0, 10, true);
+        $this->assertEquals([$context11->id], $contextlist->get_contextids(), '', 0.0, 10, true);
 
         // Teacher4 has data in workshop12 (gradeoverby) and workshop21 (gradinggradeoverby).
         $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->teacher4->id);
-        $this->assertEquals([$context21->id, $context12->id], $contextlist->get_contextids(), null, 0.0, 10, true);
+        $this->assertEquals([$context21->id, $context12->id], $contextlist->get_contextids(), '', 0.0, 10, true);
     }
 
     /**
index 4c262dd..2361ad6 100644 (file)
@@ -9,6 +9,7 @@
         processIsolation="false"
         backupGlobals="false"
         backupStaticAttributes="false"
+        forceCoversAnnotation="true"
         stopOnError="false"
         stopOnFailure="false"
         stopOnIncomplete="false"
index 6f4d7d5..45f6ad5 100644 (file)
@@ -34,10 +34,16 @@ use \core_privacy\local\request\approved_contextlist;
  *
  * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_privacy\local\request\approved_contextlist
  */
 class approved_contextlist_test extends advanced_testcase {
+
     /**
      * The approved contextlist should not be modifiable once set.
+     *
+     * @covers ::__construct
+     * @covers ::<!public>
+     * @covers \core_privacy\local\request\approved_contextlist<extended>
      */
     public function test_default_values_set() {
         $testuser = \core_user::get_user_by_username('admin');
index edb79fd..3be87c7 100644 (file)
@@ -35,10 +35,15 @@ use \core_privacy\local\request\userlist;
  *
  * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_privacy\local\request\approved_userlist
  */
 class approved_userlist_test extends advanced_testcase {
     /**
      * The approved userlist should not be modifiable once set.
+     *
+     * @covers ::__construct
+     * @covers \core_privacy\local\request\approved_userlist<extended>
+     * @covers ::<!public>
      */
     public function test_default_values_set() {
         $this->resetAfterTest();
@@ -68,6 +73,11 @@ class approved_userlist_test extends advanced_testcase {
         $this->assertEquals($expected, $result);
     }
 
+    /**
+     * @covers ::create_from_userlist
+     * @covers \core_privacy\local\request\approved_userlist<extended>
+     * @covers ::<!public>
+     */
     public function test_create_from_userlist() {
         $this->resetAfterTest();
 
index 3835175..b7403f7 100644 (file)
@@ -35,12 +35,15 @@ use \core_privacy\local\metadata\types;
  *
  * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_privacy\local\metadata\collection
  */
 class core_privacy_metadata_collection extends advanced_testcase {
 
-
     /**
      * Test that adding an unknown type causes the type to be added to the collection.
+     *
+     * @covers ::add_type
+     * @covers ::<!public>
      */
     public function test_add_type_generic_type() {
         $collection = new collection('core_privacy');
@@ -56,6 +59,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that adding a known type works as anticipated.
+     *
+     * @covers ::add_type
+     * @covers ::<!public>
      */
     public function test_add_type_known_type() {
         $collection = new collection('core_privacy');
@@ -70,6 +76,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that adding multiple types returns them all.
+     *
+     * @covers ::add_type
+     * @covers ::<!public>
      */
     public function test_add_type_multiple() {
         $collection = new collection('core_privacy');
@@ -86,6 +95,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that the add_database_table function adds a database table.
+     *
+     * @covers ::add_database_table
+     * @covers ::<!public>
      */
     public function test_add_database_table() {
         $collection = new collection('core_privacy');
@@ -107,6 +119,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that the add_user_preference function adds a single user preference.
+     *
+     * @covers ::add_user_preference
+     * @covers ::<!public>
      */
     public function test_add_user_preference() {
         $collection = new collection('core_privacy');
@@ -126,6 +141,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that the link_external_location function links an external location.
+     *
+     * @covers ::link_external_location
+     * @covers ::<!public>
      */
     public function test_link_external_location() {
         $collection = new collection('core_privacy');
@@ -147,6 +165,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that the link_subsystem function links the subsystem.
+     *
+     * @covers ::link_subsystem
+     * @covers ::<!public>
      */
     public function test_link_subsystem() {
         $collection = new collection('core_privacy');
@@ -166,6 +187,9 @@ class core_privacy_metadata_collection extends advanced_testcase {
 
     /**
      * Test that the link_plugintype function links the plugin.
+     *
+     * @covers ::link_plugintype
+     * @covers ::<!public>
      */
     public function test_link_plugintype() {
         $collection = new collection('core_privacy');
@@ -202,6 +226,8 @@ class core_privacy_metadata_collection extends advanced_testcase {
      *
      * @dataProvider component_list_provider
      * @param   string  $component The component to test
+     * @covers ::get_component
+     * @covers ::<!public>
      */
     public function test_get_component($component) {
         $collection = new collection($component);
index 0dc4f79..742ec33 100644 (file)
@@ -34,6 +34,7 @@ use \core_privacy\local\request\contextlist_base;
  *
  * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_privacy\local\request\contextlist_base
  */
 class contextlist_base_test extends advanced_testcase {
     /**
@@ -43,6 +44,8 @@ class contextlist_base_test extends advanced_testcase {
      * @param   array   $input List of context IDs
      *