Merge branch 'wip-MDL-32395-m23' of git://github.com/samhemelryk/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 16 Apr 2012 02:32:09 +0000 (10:32 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 16 Apr 2012 02:32:09 +0000 (10:32 +0800)
210 files changed:
admin/tool/phpunit/cli/init.bat [new file with mode: 0644]
admin/tool/phpunit/cli/init.php [new file with mode: 0644]
admin/tool/phpunit/cli/init.sh [new file with mode: 0755]
admin/tool/phpunit/cli/util.php
admin/tool/phpunit/index.php
admin/tool/phpunit/settings.php
admin/tool/phpunit/version.php
admin/tool/phpunit/webrunner.php [new file with mode: 0644]
backup/moodle2/backup_activity_task.class.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_activity_task.class.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/util/factories/tests/factories_test.php
backup/util/loggers/simpletest/testlogger.php
backup/util/loggers/tests/logger_test.php
blocks/online_users/tests/generator/lib.php [new file with mode: 0644]
blocks/online_users/tests/generator_test.php [new file with mode: 0644]
blog/locallib.php
blog/tests/bloglib_test.php
config-dist.php
grade/report/grader/lib.php
index.php
install/lang/de/error.php
install/lang/de_comm/langconfig.php [new file with mode: 0644]
lang/en/backup.php
lang/en/question.php
lib/adminlib.php
lib/clilib.php
lib/cronlib.php
lib/ddl/mssql_sql_generator.php
lib/ddl/simpletest/testddl.php
lib/ddl/tests/ddl_test.php
lib/dml/database_column_info.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/simpletest/testdml.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/enrollib.php
lib/externallib.php
lib/grade/tests/fixtures/lib.php [new file with mode: 0644]
lib/grade/tests/grade_category_test.php [new file with mode: 0644]
lib/grade/tests/grade_grade_test.php [new file with mode: 0644]
lib/grade/tests/grade_item_test.php [new file with mode: 0644]
lib/grade/tests/grade_outcome_test.php [new file with mode: 0644]
lib/grade/tests/grade_scale_test.php [new file with mode: 0644]
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/phpunit/bootstrap.php
lib/phpunit/bootstraplib.php [new file with mode: 0644]
lib/phpunit/generatorlib.php
lib/phpunit/lib.php
lib/phpunit/phpunit.xsd [new file with mode: 0644]
lib/phpunit/readme.md
lib/setup.php
lib/setuplib.php
lib/tests/fixtures/sample_dataset.csv [new file with mode: 0644]
lib/tests/fixtures/sample_dataset.xml [new file with mode: 0644]
lib/tests/phpunit_test.php
lib/upgradelib.php
mod/assignment/tests/generator/lib.php [new file with mode: 0644]
mod/assignment/tests/generator_test.php [new file with mode: 0644]
mod/data/tests/fixtures/test_data_content.csv [new file with mode: 0644]
mod/data/tests/fixtures/test_data_fields.csv [new file with mode: 0644]
mod/data/tests/fixtures/test_data_records.csv [new file with mode: 0644]
mod/data/tests/generator/lib.php [new file with mode: 0644]
mod/data/tests/generator_test.php [new file with mode: 0644]
mod/data/tests/search_test.php [new file with mode: 0644]
mod/feedback/item/label/label_form.php
mod/feedback/item/label/lib.php
mod/feedback/show_nonrespondents.php
mod/forum/tests/generator/lib.php [new file with mode: 0644]
mod/forum/tests/generator_test.php [new file with mode: 0644]
mod/page/tests/generator/lib.php [new file with mode: 0644]
mod/page/tests/generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/delaybetweenattempts/rule.php
mod/quiz/editlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/mod_form.php
mod/quiz/report/grading/gradingsettings_form.php
mod/quiz/report/grading/lang/en/quiz_grading.php
mod/quiz/report/grading/report.php
mod/quiz/report/overview/overview_table.php
mod/quiz/report/overview/report.php
mod/quiz/report/responses/report.php
mod/quiz/report/responses/responses_table.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/styles.css
mod/scorm/player.js
phpunit.xml.dist
question/behaviour/adaptive/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/adaptivenopenalty/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/deferredcbm/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/deferredfeedback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/immediatecbm/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/immediatefeedback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/informationitem/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/interactive/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/interactivecountback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/manualgraded/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/missing/tests/missingbehaviour_test.php [new file with mode: 0644]
question/engine/questionusage.php
question/engine/tests/datalib_test.php [new file with mode: 0644]
question/engine/tests/helpers.php [new file with mode: 0644]
question/engine/tests/questionattempt_test.php [new file with mode: 0644]
question/engine/tests/questionattemptiterator_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstep_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstepiterator_test.php [new file with mode: 0644]
question/engine/tests/questionbank_test.php [new file with mode: 0644]
question/engine/tests/questioncbm_test.php [new file with mode: 0644]
question/engine/tests/questionengine_test.php [new file with mode: 0644]
question/engine/tests/questionstate_test.php [new file with mode: 0644]
question/engine/tests/questionusagebyactivity_test.php [new file with mode: 0644]
question/engine/tests/questionutils_test.php [new file with mode: 0644]
question/engine/tests/unitofwork_test.php [new file with mode: 0644]
question/engine/upgrade/tests/helper.php [new file with mode: 0644]
question/engine/upgrade/upgradelib.php
question/format/gift/tests/fixtures/questions.gift.txt [new file with mode: 0644]
question/format/gift/tests/giftformat_test.php [new file with mode: 0644]
question/format/xml/tests/xmlformat_test.php [new file with mode: 0644]
question/tests/importexport_test.php [new file with mode: 0644]
question/type/calculated/tests/helper.php [new file with mode: 0644]
question/type/calculated/tests/question_test.php [new file with mode: 0644]
question/type/calculated/tests/questiontype_test.php [new file with mode: 0644]
question/type/calculated/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculated/tests/variablesubstituter_test.php [new file with mode: 0644]
question/type/calculated/tests/walkthrough_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/helper.php [new file with mode: 0644]
question/type/calculatedmulti/tests/question_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/walkthrough_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/helper.php [new file with mode: 0644]
question/type/calculatedsimple/tests/question_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/walkthrough_test.php [new file with mode: 0644]
question/type/description/tests/helper.php [new file with mode: 0644]
question/type/description/tests/questiontype_test.php [new file with mode: 0644]
question/type/description/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/description/tests/walkthrough_test.php [new file with mode: 0644]
question/type/edit_question_form.php
question/type/essay/questiontype.php
question/type/essay/tests/question_test.php [new file with mode: 0644]
question/type/essay/tests/questiontype_test.php [new file with mode: 0644]
question/type/essay/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/match/tests/question_test.php [new file with mode: 0644]
question/type/match/tests/questiontype_test.php [new file with mode: 0644]
question/type/match/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/match/tests/walkthrough_test.php [new file with mode: 0644]
question/type/missingtype/tests/missingtype_test.php [new file with mode: 0644]
question/type/multianswer/tests/helper.php [new file with mode: 0644]
question/type/multianswer/tests/question_test.php [new file with mode: 0644]
question/type/multianswer/tests/questiontype_test.php [new file with mode: 0644]
question/type/multianswer/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/multianswer/tests/walkthrough_test.php [new file with mode: 0644]
question/type/multichoice/questiontype.php
question/type/multichoice/tests/question_test.php [new file with mode: 0644]
question/type/multichoice/tests/questiontype_test.php [new file with mode: 0644]
question/type/multichoice/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/multichoice/tests/walkthrough_test.php [new file with mode: 0644]
question/type/numerical/question.php
question/type/numerical/questiontype.php
question/type/numerical/tests/answer_test.php [new file with mode: 0644]
question/type/numerical/tests/answerprocessor_test.php [new file with mode: 0644]
question/type/numerical/tests/form_test.php [new file with mode: 0644]
question/type/numerical/tests/helper.php [new file with mode: 0644]
question/type/numerical/tests/question_test.php [new file with mode: 0644]
question/type/numerical/tests/questiontype_test.php [new file with mode: 0644]
question/type/numerical/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/numerical/tests/walkthrough_test.php [new file with mode: 0644]
question/type/questiontypebase.php
question/type/random/tests/questiontype_test.php [new file with mode: 0644]
question/type/random/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/shortanswer/tests/helper.php [new file with mode: 0644]
question/type/shortanswer/tests/question_test.php [new file with mode: 0644]
question/type/shortanswer/tests/questiontype_test.php [new file with mode: 0644]
question/type/shortanswer/tests/tupgradelibnewqe_test.php [new file with mode: 0644]
question/type/tests/questionbase_test.php [new file with mode: 0644]
question/type/tests/questiontype_test.php [new file with mode: 0644]
question/type/truefalse/questiontype.php
question/type/truefalse/tests/helper.php [new file with mode: 0644]
question/type/truefalse/tests/question_test.php [new file with mode: 0644]
question/type/truefalse/tests/questiontype_test.php [new file with mode: 0644]
question/type/truefalse/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/truefalse/tests/walkthrough_test.php [new file with mode: 0644]
question/type/upgrade.txt
rating/index.php
theme/formal_white/config.php
theme/formal_white/lang/en/theme_formal_white.php
theme/formal_white/lib.php
theme/formal_white/settings.php
theme/formal_white/style/core.css
theme/formal_white/style/course.css
theme/formal_white/style/formal_white.css
theme/formal_white/style/menu.css
theme/formal_white/style/pagelayout.css
theme/formal_white/style/quiz.css
theme/formal_white/version.php
version.php

diff --git a/admin/tool/phpunit/cli/init.bat b/admin/tool/phpunit/cli/init.bat
new file mode 100644 (file)
index 0000000..d57029f
--- /dev/null
@@ -0,0 +1,22 @@
+@ECHO OFF
+ECHO Initialising Moodle PHPUnit test environment...
+
+CALL php %~dp0\util.php --diag > NUL 2>&1
+
+IF ERRORLEVEL 133 GOTO drop
+IF ERRORLEVEL 132 GOTO install
+IF ERRORLEVEL 1 GOTO unknown
+GOTO done
+
+:drop
+CALL php %~dp0\util.php --drop
+IF ERRORLEVEL 1 GOTO done
+
+:install
+CALL php %~dp0\util.php --install
+GOTO done
+
+:unknown
+CALL php %~dp0\util.php --diag
+
+:done
diff --git a/admin/tool/phpunit/cli/init.php b/admin/tool/phpunit/cli/init.php
new file mode 100644 (file)
index 0000000..f091a9f
--- /dev/null
@@ -0,0 +1,66 @@
+<?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/>.
+
+/**
+ * All in one init script - PHP version.
+ *
+ * @package    tool_phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+if (isset($_SERVER['REMOTE_ADDR'])) {
+    die; // no access from web!
+}
+
+require_once(__DIR__.'/../../../../lib/clilib.php');
+require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
+
+echo "Initialising Moodle PHPUnit test environment...\n";
+
+$output = null;
+exec('php --version', $output, $code);
+if ($code != 0) {
+    phpunit_bootstrap_error(1, 'Can not execute \'php\' binary.');
+}
+
+chdir(__DIR__);
+$output = null;
+exec("php util.php --diag", $output, $code);
+if ($code == 0) {
+    // everything is ready
+
+} else if ($code == 132) {
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else if ($code == 133) {
+    passthru("php util.php --drop", $code);
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else {
+    echo implode("\n", $output)."\n";
+    exit($code);
+}
+
+passthru("php util.php --buildconfig", $code);
+
+exit(0);
diff --git a/admin/tool/phpunit/cli/init.sh b/admin/tool/phpunit/cli/init.sh
new file mode 100755 (executable)
index 0000000..b3616ee
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+SOURCE="${BASH_SOURCE[0]}"
+while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done
+CLIDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
+
+UTIL="$CLIDIR/util.php"
+
+echo "Initialising Moodle PHPUnit test environment..."
+
+DIGERROR=`php $UTIL --diag`
+DIAG=$?
+if [ $DIAG -eq 132 ] ; then
+    php $UTIL --install
+else
+    if [ $DIAG -eq 133 ] ; then
+        php $UTIL --drop
+        RESULT=$?
+        if [ $RESULT -gt 0 ] ; then
+            exit $RESULT
+        fi
+        php $UTIL --install
+    else
+        if [ $DIAG -gt 0 ] ; then
+            echo $DIGERROR
+            exit $DIAG
+        fi
+    fi
+fi
+
+php $UTIL --buildconfig
index 18a08db..0ab46c0 100644 (file)
  * Exit codes:
  *  0   - success
  *  1   - general error
- *  130 - coding error
+ *  130 - missing PHPUnit library error
  *  131 - configuration problem
+ *  132 - install new test database
  *  133 - drop existing data before installing
+ *  134 - can not create main phpunit.xml
  *
  * @package    tool_phpunit
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define('PHPUNIT_UTIL', true);
+if (isset($_SERVER['REMOTE_ADDR'])) {
+    die; // no access from web!
+}
 
-require(__DIR__ . '/../../../../lib/phpunit/bootstrap.php');
-require_once($CFG->libdir.'/phpunit/lib.php');
-require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/upgradelib.php');
-require_once($CFG->libdir.'/clilib.php');
-require_once($CFG->libdir.'/pluginlib.php');
-require_once($CFG->libdir.'/installlib.php');
+require_once(__DIR__.'/../../../../lib/clilib.php');
+require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 
 // now get cli options
 list($options, $unrecognized) = cli_get_params(
@@ -45,6 +44,9 @@ list($options, $unrecognized) = cli_get_params(
         'drop'        => false,
         'install'     => false,
         'buildconfig' => false,
+        'diag'        => false,
+        'phpunitdir'  => false,
+        'run'         => false,
         'help'        => false,
     ),
     array(
@@ -52,37 +54,105 @@ list($options, $unrecognized) = cli_get_params(
     )
 );
 
+if ($options['phpunitdir']) {
+    // nasty skodak's hack for testing of future PHPUnit versions - intentionally not documented
+    if (!file_exists($options['phpunitdir'])) {
+        cli_error('Invalid custom PHPUnit lib location');
+    }
+    $files = scandir($options['phpunitdir']);
+    foreach ($files as $file) {
+        $path = $options['phpunitdir'].'/'.$file;
+        if (!is_dir($path) or strpos($file, '.') === 0) {
+            continue;
+        }
+        ini_set('include_path', $path . PATH_SEPARATOR . ini_get('include_path'));
+    }
+    unset($files);
+    unset($file);
+}
+
+// verify PHPUnit libs are loaded
+if (!@include_once('PHPUnit/Autoload.php')) {
+    phpunit_bootstrap_error(130);
+}
+
+if (!@include_once('PHPUnit/Extensions/Database/Autoload.php')) {
+    phpunit_bootstrap_error(130);
+}
+
+if ($options['run']) {
+    unset($options);
+    unset($unrecognized);
+
+    foreach ($_SERVER['argv'] as $k=>$v) {
+        if (strpos($v, '--run') === 0 or strpos($v, '--phpunitdir') === 0) {
+            unset($_SERVER['argv'][$k]);
+            $_SERVER['argc'] = $_SERVER['argc'] - 1;
+        }
+    }
+    $_SERVER['argv'] = array_values($_SERVER['argv']);
+    PHPUnit_TextUI_Command::main();
+    exit(0);
+}
+
+define('PHPUNIT_UTIL', true);
+
+require(__DIR__ . '/../../../../lib/phpunit/bootstrap.php');
+
+// from now on this is a regular moodle CLI_SCRIPT
+
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/upgradelib.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->libdir.'/pluginlib.php');
+require_once($CFG->libdir.'/installlib.php');
+
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
     cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
 }
 
+$diag = $options['diag'];
 $drop = $options['drop'];
 $install = $options['install'];
 $buildconfig = $options['buildconfig'];
 
-if ($options['help'] or (!$drop and !$install and !$buildconfig)) {
+if ($options['help'] or (!$drop and !$install and !$buildconfig and !$diag)) {
     $help = "Various PHPUnit utility functions
 
 Options:
 --drop                Drop database and dataroot
 --install             Install database
 --buildconfig         Build /phpunit.xml from /phpunit.xml.dist that includes suites for all plugins and core
+--diag                Diagnose installation and return error code only
+--run                 Execute PHPUnit tests (alternative for standard phpunit binary)
 
 -h, --help            Print out this help
 
 Example:
-\$/usr/bin/php lib/phpunit/tool.php
+\$/usr/bin/php lib/phpunit/tool.php --install
 ";
     echo $help;
-    die;
+    exit(0);
 }
 
-if ($buildconfig) {
-    phpunit_util::build_config_file();
+if ($diag) {
+    list($errorcode, $message) = phpunit_util::testing_ready_problem();
+    if ($errorcode) {
+        phpunit_bootstrap_error($errorcode, $message);
+    }
     exit(0);
 
+} else if ($buildconfig) {
+    if (phpunit_util::build_config_file()) {
+        exit(0);
+    } else {
+        phpunit_bootstrap_error(134);
+    }
+
 } else if ($drop) {
+    // make sure tests do not run in parallel
+    phpunit_util::acquire_test_lock();
     phpunit_util::drop_site();
     // note: we must stop here because $CFG is messed up and we can not reinstall, sorry
     exit(0);
index 1dbaded..774d4ee 100644 (file)
@@ -22,8 +22,6 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define('NO_OUTPUT_BUFFERING', true);
-
 require(dirname(__FILE__) . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 
@@ -37,4 +35,4 @@ $info = file_get_contents("$CFG->libdir/phpunit/readme.md");
 echo markdown_to_html($info);
 
 echo $OUTPUT->box_end();
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index 8e06b2f..efefd32 100644 (file)
@@ -25,4 +25,8 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$ADMIN->add('development', new admin_externalpage('toolphpunit', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/index.php"));
+if ($hassiteconfig) {
+    $ADMIN->add('development', new admin_externalpage('toolphpunit', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/index.php"));
+    $ADMIN->add('development', new admin_externalpage('toolphpunitwebrunner', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/webrunner.php",
+        'moodle/site:config', true));
+}
index e745026..9e59434 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012030800; // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2012030100; // Requires this Moodle version
+$plugin->version   = 2012040500; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2012040500; // Requires this Moodle version
 $plugin->component = 'tool_phpunit'; // Full name of the plugin (used for diagnostics)
 
diff --git a/admin/tool/phpunit/webrunner.php b/admin/tool/phpunit/webrunner.php
new file mode 100644 (file)
index 0000000..ef51685
--- /dev/null
@@ -0,0 +1,192 @@
+<?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/>.
+
+/**
+ * PHPUnit shell execution wrapper
+ *
+ * @package    tool_phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
+require(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$testpath  = optional_param('testpath', '', PARAM_PATH);
+$testclass = optional_param('testclass', '', PARAM_ALPHANUMEXT);
+$execute   = optional_param('execute', 0, PARAM_BOOL);
+
+navigation_node::override_active_url(new moodle_url('/admin/tool/phpunit/index.php'));
+admin_externalpage_setup('toolphpunitwebrunner');
+
+if (!debugging('', DEBUG_DEVELOPER)) {
+    error('Not available on production sites, sorry.');
+}
+
+set_time_limit(60*30);
+
+$oldcwd = getcwd();
+$code = 0;
+
+if (!isset($CFG->phpunit_dataroot) or !isset($CFG->phpunit_prefix)) {
+    tool_phpunit_problem('Missing $CFG->phpunit_dataroot or $CFG->phpunit_prefix, can not execute tests.');
+}
+if (!file_exists($CFG->phpunit_dataroot)) {
+    mkdir($CFG->phpunit_dataroot, 02777, true);
+}
+if (!is_writable($CFG->phpunit_dataroot)) {
+    tool_phpunit_problem('$CFG->phpunit_dataroot in not writable, can not execute tests.');
+}
+$output = null;
+exec('php --version', $output, $code);
+if ($code != 0) {
+    tool_phpunit_problem('Can not execute \'php\' binary.');
+}
+
+if ($execute) {
+    require_sesskey();
+
+    chdir($CFG->dirroot);
+    $output = null;
+    exec("php $CFG->admin/tool/phpunit/cli/util.php --diag", $output, $code);
+    if ($code == 0) {
+        // everything is ready
+
+    } else if ($code == 132) {
+        tool_phpunit_header();
+        echo $OUTPUT->box_start('generalbox');
+        echo '<pre>';
+        echo "Initialising test database:\n\n";
+        chdir($CFG->dirroot);
+        ignore_user_abort(true);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --install", $code);
+        chdir($oldcwd);
+        echo '</pre>';
+        echo $OUTPUT->box_end();
+        if ($code != 0) {
+            tool_phpunit_problem('Can not initialize database');
+        }
+        $CFG->debug = 0; // no pesky redirect warning, we really want to redirect
+        redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
+        echo $OUTPUT->footer();
+        die();
+
+    } else if ($code == 133) {
+        tool_phpunit_header();
+        echo $OUTPUT->box_start('generalbox');
+        echo '<pre>';
+        echo "Reinitialising test database:\n\n";
+        chdir($CFG->dirroot);
+        ignore_user_abort(true);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --drop", $code);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --install", $code);
+        chdir($oldcwd);
+        echo '</pre>';
+        echo $OUTPUT->box_end();
+        if ($code != 0) {
+            tool_phpunit_problem('Can not initialize database');
+        }
+        $CFG->debug = 0; // no pesky redirect warning, we really want to redirect
+        redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
+        die();
+
+    } else {
+        tool_phpunit_header();
+        echo $OUTPUT->box_start('generalbox');
+        echo '<pre>';
+        echo "Error: $code\n\n";
+        echo implode("\n", $output);
+        echo '</pre>';
+        echo $OUTPUT->box_end();
+        tool_phpunit_problem('Can not execute tests');
+        die();
+    }
+
+    tool_phpunit_header();
+    echo $OUTPUT->box_start('generalbox');
+    echo '<pre>';
+
+    // use the dataroot file
+    $configdir = "$CFG->phpunit_dataroot/phpunit/webrunner.xml";
+    if (!file_exists($configdir)) {
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
+        if ($code != 0) {
+            tool_phpunit_problem('Can not create configuration file');
+        }
+    }
+    $configdir = escapeshellarg($configdir);
+    // no cleanup of path - this is tricky because we can not use escapeshellarg and friends for escaping,
+    // this is from admin user so PARAM_PATH must be enough
+    chdir($CFG->dirroot);
+    passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $testclass $testpath", $code);
+    chdir($oldcwd);
+
+    echo '</pre>';
+    echo $OUTPUT->box_end();
+
+} else {
+    tool_phpunit_header();
+}
+
+echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
+echo '<form method="get" action="webrunner.php">';
+echo '<fieldset class="invisiblefieldset">';
+echo '<label for="testpath">Test one file</label> ';
+echo '<input type="text" id="testpath" name="testpath" value="'.s($testpath).'" size="50" /> (all test cases from webrunner.xml if empty)';
+echo '</p>';
+echo '<label for="testclass">Class name</label> ';
+echo '<input type="text" id="testclass" name="testclass" value="'.s($testclass).'" size="50" /> (first class in file if empty)';
+echo '</p>';
+echo '<input type="submit" value="Run" />';
+echo '<input type="hidden" name="execute" value="1" />';
+echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
+echo '</fieldset>';
+echo '</form>';
+echo $OUTPUT->box_end();
+echo $OUTPUT->footer();
+die;
+
+
+
+//========================================
+
+/**
+ * Print headers and experimental warning
+ * @return void
+ */
+function tool_phpunit_header() {
+    global $OUTPUT;
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('pluginname', 'tool_phpunit'));
+    echo $OUTPUT->box('EXPERIMENTAL: it is recommended to execute PHPUnit tests and init scripts only from command line.', array('generalbox'));
+}
+
+/**
+ * Called when PHPUnit can not execute.
+ * @param string $message
+ * @return void
+ */
+function tool_phpunit_problem($message) {
+    global $PAGE;
+    if (!$PAGE->headerprinted) {
+        tool_phpunit_header();
+    }
+    notice($message, new moodle_url('/admin/tool/phpunit/'));
+}
index 360dd63..fef94cc 100644 (file)
@@ -167,6 +167,11 @@ abstract class backup_activity_task extends backup_task {
             $this->add_step(new backup_activity_logs_structure_step('activity_logs', 'logs.xml'));
         }
 
+        // Generate the calendar events file (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new backup_calendarevents_structure_step('activity_calendar', 'calendar.xml'));
+        }
+
         // Fetch all the activity grade items and put them to backup_ids
         $this->add_step(new backup_activity_grade_items_to_ids('fetch_activity_grade_items'));
 
index 8c35274..6cc262b 100644 (file)
@@ -106,6 +106,11 @@ class backup_course_task extends backup_task {
             $this->add_step(new backup_comments_structure_step('course_comments', 'comments.xml'));
         }
 
+        // Generate the calender events file (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new backup_calendarevents_structure_step('course_calendar', 'calendar.xml'));
+        }
+
         // Generate the logs file (conditionally)
         if ($this->get_setting_value('logs')) {
             $this->add_step(new backup_course_logs_structure_step('course_logs', 'logs.xml'));
index 861be13..3bd46cf 100644 (file)
@@ -119,6 +119,12 @@ class backup_root_task extends backup_task {
         $this->add_setting($comments);
         $users->add_dependency($comments);
 
+        // Define calendar events (dependent of users)
+        $events = new backup_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, true);
+        $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup')));
+        $this->add_setting($events);
+        $users->add_dependency($events);
+
         // Define completion (dependent of users)
         $completion = new backup_userscompletion_setting('userscompletion', base_setting::IS_BOOLEAN, true);
         $completion->set_ui(new backup_setting_ui_checkbox($completion, get_string('rootsettinguserscompletion', 'backup')));
index 74914aa..ddd6aa2 100644 (file)
@@ -101,6 +101,13 @@ class backup_logs_setting extends backup_anonymize_setting {}
  */
 class backup_comments_setting extends backup_anonymize_setting {}
 
+/**
+ * root setting to control if backup will include
+ * calender events or no (any level), depends of @backup_users_setting
+ * exactly in the same way than @backup_anonymize_setting so we extend from it
+ */
+class backup_calendarevents_setting extends backup_anonymize_setting {}
+
 /**
  * root setting to control if backup will include
  * users completion data or no (any level), depends of @backup_users_setting
index e22ff35..47d4040 100644 (file)
@@ -762,6 +762,48 @@ class backup_comments_structure_step extends backup_structure_step {
     }
 }
 
+/**
+ * structure step in charge of constructing the calender.xml file for all the events found
+ * in a given context
+ */
+class backup_calendarevents_structure_step extends backup_structure_step {
+
+    protected function define_structure() {
+
+        // Define each element separated
+
+        $events = new backup_nested_element('events');
+
+        $event = new backup_nested_element('event', array('id'), array(
+                'name', 'description', 'format', 'courseid', 'groupid', 'userid',
+                'repeatid', 'modulename', 'instance', 'eventtype', 'timestart',
+                'timeduration', 'visible', 'uuid', 'sequence', 'timemodified'));
+
+        // Build the tree
+        $events->add_child($event);
+
+        // Define sources
+        if ($this->name == 'course_calendar') {
+            $calendar_items_sql ="SELECT * FROM {event}
+                        WHERE courseid = :courseid
+                        AND (eventtype = 'course' OR eventtype = 'group')";
+            $calendar_items_params = array('courseid'=>backup::VAR_COURSEID);
+            $event->set_source_sql($calendar_items_sql, $calendar_items_params);
+        } else {
+            $event->set_source_table('event', array('courseid' => backup::VAR_COURSEID, 'instance' => backup::VAR_ACTIVITYID, 'modulename' => backup::VAR_MODNAME));
+        }
+
+        // Define id annotations
+
+        $event->annotate_ids('user', 'userid');
+        $event->annotate_ids('group', 'groupid');
+        $event->annotate_files('calendar', 'event_description', 'id');
+
+        // Return the root element (events)
+        return $events;
+    }
+}
+
 /**
  * structure step in charge of constructing the gradebook.xml file for all the gradebook config in the course
  * NOTE: the backup of the grade items themselves is handled by backup_activity_grades_structure_step
index 4872fc0..6917b33 100644 (file)
@@ -152,6 +152,11 @@ abstract class restore_activity_task extends restore_task {
             $this->add_step(new restore_comments_structure_step('activity_comments', 'comments.xml'));
         }
 
+        // Calendar events (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new restore_calendarevents_structure_step('activity_calendar', 'calendar.xml'));
+        }
+
         // Grades (module-related, rest of gradebook is restored later if possible: cats, calculations...)
         $this->add_step(new restore_activity_grades_structure_step('activity_grades', 'grades.xml'));
 
index 87828b4..97a2376 100644 (file)
@@ -89,6 +89,11 @@ class restore_course_task extends restore_task {
             $this->add_step(new restore_comments_structure_step('course_comments', 'comments.xml'));
         }
 
+        // Calendar events (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new restore_calendarevents_structure_step('course_calendar', 'calendar.xml'));
+        }
+
         // At the end, mark it as built
         $this->built = true;
     }
index 319fe08..6917f1c 100644 (file)
@@ -174,6 +174,19 @@ class restore_root_task extends restore_task {
         $this->add_setting($comments);
         $users->add_dependency($comments);
 
+        // Define Calendar events (dependent of users)
+        $defaultvalue = false;                      // Safer default
+        $changeable = false;
+        if (isset($rootsettings['calendarevents']) && $rootsettings['calendarevents']) { // Only enabled when available
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $events = new restore_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, $defaultvalue);
+        $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup')));
+        $events->get_ui()->set_changeable($changeable);
+        $this->add_setting($events);
+        $users->add_dependency($events);
+
         // Define completion (dependent of users)
         $defaultvalue = false;                      // Safer default
         $changeable = false;
index 09f92fe..59c09e1 100644 (file)
@@ -63,6 +63,13 @@ class restore_activities_setting extends restore_generic_setting {}
  */
 class restore_comments_setting extends restore_role_assignments_setting {}
 
+/**
+ * root setting to control if restore will create
+ * events or no, depends of @restore_users_setting
+ * exactly in the same way than @restore_role_assignments_setting so we extend from it
+ */
+class restore_calendarevents_setting extends restore_role_assignments_setting {}
+
 /**
  * root setting to control if restore will create
  * completion info or no, depends of @restore_users_setting
index ae2ae0d..1472012 100644 (file)
@@ -1596,6 +1596,76 @@ class restore_comments_structure_step extends restore_structure_step {
     }
 }
 
+/**
+ * This structure steps restores the calendar events
+ */
+class restore_calendarevents_structure_step extends restore_structure_step {
+
+    protected function define_structure() {
+
+        $paths = array();
+
+        $paths[] = new restore_path_element('calendarevents', '/events/event');
+
+        return $paths;
+    }
+
+    public function process_calendarevents($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $oldid = $data->id;
+        $restorefiles = true; // We'll restore the files
+        // Find the userid and the groupid associated with the event. Return if not found.
+        $data->userid = $this->get_mappingid('user', $data->userid);
+        if ($data->userid === false) {
+            return;
+        }
+        if (!empty($data->groupid)) {
+            $data->groupid = $this->get_mappingid('group', $data->groupid);
+            if ($data->groupid === false) {
+                return;
+            }
+        }
+
+        $params = array(
+                'name'           => $data->name,
+                'description'    => $data->description,
+                'format'         => $data->format,
+                'courseid'       => $this->get_courseid(),
+                'groupid'        => $data->groupid,
+                'userid'         => $data->userid,
+                'repeatid'       => $data->repeatid,
+                'modulename'     => $data->modulename,
+                'eventtype'      => $data->eventtype,
+                'timestart'      => $this->apply_date_offset($data->timestart),
+                'timeduration'   => $data->timeduration,
+                'visible'        => $data->visible,
+                'uuid'           => $data->uuid,
+                'sequence'       => $data->sequence,
+                'timemodified'    => $this->apply_date_offset($data->timemodified));
+        if ($this->name == 'activity_calendar') {
+            $params['instance'] = $this->task->get_activityid();
+        } else {
+            $params['instance'] = 0;
+        }
+        $sql = 'SELECT id FROM {event} WHERE name = ? AND courseid = ? AND
+                repeatid = ? AND modulename = ? AND timestart = ? AND timeduration =?
+                AND ' . $DB->sql_compare_text('description', 255) . ' = ' . $DB->sql_compare_text('?', 255);
+        $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
+        $result = $DB->record_exists_sql($sql, $arg);
+        if (empty($result)) {
+            $newitemid = $DB->insert_record('event', $params);
+            $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
+        }
+
+    }
+    protected function after_execute() {
+        // Add related files
+        $this->add_related_files('calendar', 'event_description', 'event_description');
+    }
+}
+
 class restore_course_completion_structure_step extends restore_structure_step {
 
     /**
index 9e3a482..54dc4e9 100644 (file)
@@ -111,7 +111,7 @@ class backup_factories_testcase extends advanced_testcase {
         $this->assertTrue($logger2 instanceof file_logger);  // 2nd logger is file_logger (output_indented_logger skiped)
 
         // Define extra file logger and instantiate, should be 5th and last logger
-        $CFG->backup_file_logger_extra = '/tmp/test.html';
+        $CFG->backup_file_logger_extra = $CFG->tempdir.'/test.html';
         $CFG->backup_file_logger_level_extra = backup::LOG_NONE;
         $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test');
         $logger2 = $logger1->get_next();
index 9374346..c4f49fe 100644 (file)
@@ -272,6 +272,7 @@ class logger_test extends UnitTestCase {
         unlink($file); // delete file
 
         // Try one html file
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $options = array('depth' => 1);
         $lo = new file_logger(backup::LOG_ERROR, true, true, $file);
@@ -289,6 +290,7 @@ class logger_test extends UnitTestCase {
         unlink($file); // delete file
 
         // Instantiate, write something, force deletion, try to write again
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $lo = new mock_file_logger(backup::LOG_ERROR, true, true, $file);
         $this->assertTrue(file_exists($file));
index 799632f..67f8e74 100644 (file)
@@ -267,6 +267,7 @@ class backup_logger_testcase extends basic_testcase {
         unlink($file); // delete file
 
         // Try one html file
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $options = array('depth' => 1);
         $lo = new file_logger(backup::LOG_ERROR, true, true, $file);
@@ -281,9 +282,11 @@ class backup_logger_testcase extends basic_testcase {
         $this->assertTrue(strpos($fcontents, '[error]') !== false);
         $this->assertTrue(strpos($fcontents, '&nbsp;&nbsp;') !== false);
         $this->assertTrue(substr_count($fcontents , '] ') >= 2);
+        $lo->__destruct(); // closes file handle
         unlink($file); // delete file
 
         // Instantiate, write something, force deletion, try to write again
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $lo = new mock_file_logger(backup::LOG_ERROR, true, true, $file);
         $this->assertTrue(file_exists($file));
diff --git a/blocks/online_users/tests/generator/lib.php b/blocks/online_users/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..d762f42
--- /dev/null
@@ -0,0 +1,63 @@
+<?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/>.
+
+/**
+ * block_online_users data generator
+ *
+ * @package    block_online_users
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Page module PHPUnit data generator class
+ *
+ * @package    mod_page
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_generator extends phpunit_block_generator {
+
+    /**
+     * Create new block instance
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record with extra cmid field
+     */
+    public function create_instance($record = null, array $options = null) {
+        global $DB, $CFG;
+        require_once("$CFG->dirroot/mod/page/locallib.php");
+
+        $this->instancecount++;
+
+        $record = (object)(array)$record;
+        $options = (array)$options;
+
+        $record = $this->prepare_record($record);
+
+        $id = $DB->insert_record('block_instances', $record);
+        context_block::instance($id);
+
+        $instance = $DB->get_record('block_instances', array('id'=>$id), '*', MUST_EXIST);
+
+        return $instance;
+    }
+}
diff --git a/blocks/online_users/tests/generator_test.php b/blocks/online_users/tests/generator_test.php
new file mode 100644 (file)
index 0000000..1ffcaba
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * PHPUnit data generator tests
+ *
+ * @package    block_online_users
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * PHPUnit data generator testcase
+ *
+ * @package    block_online_users
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_generator_testcase extends advanced_testcase {
+    public function test_generator() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $beforeblocks = $DB->count_records('block_instances');
+        $beforecontexts = $DB->count_records('context');
+
+        /** @var block_online_users_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users');
+        $this->assertInstanceOf('block_online_users_generator', $generator);
+        $this->assertEquals('online_users', $generator->get_blockname());
+
+        $generator->create_instance();
+        $generator->create_instance();
+        $bi = $generator->create_instance();
+        $this->assertEquals($beforeblocks+3, $DB->count_records('block_instances'));
+
+    }
+}
index b8385da..9eb1e8f 100644 (file)
@@ -292,9 +292,16 @@ class blog_entry {
 
         $contentcell->text .= $OUTPUT->container_start('commands');
 
-        if (blog_user_can_edit_entry($this) && empty($this->uniquehash)) {
-            $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php', array('action' => 'edit', 'entryid' => $this->id)), $stredit) . ' | ';
-            $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php', array('action' => 'delete', 'entryid' => $this->id)), $strdelete) . ' | ';
+        if (blog_user_can_edit_entry($this)) {
+            if (empty($this->uniquehash)) {
+                //External blog entries should not be edited
+                $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php',
+                                                        array('action' => 'edit', 'entryid' => $this->id)),
+                                                        $stredit) . ' | ';
+            }
+            $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php',
+                                                    array('action' => 'delete', 'entryid' => $this->id)),
+                                                    $strdelete) . ' | ';
         }
 
         $contentcell->text .= html_writer::link(new moodle_url('/blog/index.php', array('entryid' => $this->id)), get_string('permalink', 'blog'));
index 3ad949c..f385eb3 100644 (file)
@@ -14,7 +14,6 @@
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-
 /**
  * Unit tests for blog
  *
@@ -35,19 +34,23 @@ require_once($CFG->dirroot . '/blog/lib.php');
 class bloglib_testcase extends advanced_testcase {
 
     private $courseid; // To store important ids to be used in tests
+    private $cmid;
     private $groupid;
     private $userid;
     private $tagid;
+    private $postid;
 
     protected function setUp() {
         global $DB;
         parent::setUp();
 
-        $this->resetAfterTest(true);
+        $this->resetAfterTest();
 
         // Create default course
-        $course = $this->getDataGenerator()->create_course(array('category'=>1, 'fullname'=>'Anonymous test course', 'shortname'=>'ANON'));
+        $course = $this->getDataGenerator()->create_course(array('category'=>1, 'shortname'=>'ANON'));
+        $this->assertNotEmpty($course);
         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
+        $this->assertNotEmpty($page);
 
         // Create default group
         $group = new stdClass();
@@ -75,17 +78,20 @@ class bloglib_testcase extends advanced_testcase {
 
         // Grab important ids
         $this->courseid = $course->id;
+        $this->cmid = $page->cmid;
         $this->groupid  = $group->id;
         $this->userid  = $user->id;
         $this->tagid  = $tag->id;
+        $this->postid = $post->id;
     }
 
 
     public function test_overrides() {
+        global $SITE;
 
         // Try all the filters at once: Only the entry filter is active
-        $filters = array('site' => 1, 'course' => $this->courseid, 'module' => 1,
-            'group' => $this->groupid, 'user' => 1, 'tag' => 1, 'entry' => 1);
+        $filters = array('site' => $SITE->id, 'course' => $this->courseid, 'module' => $this->cmid,
+            'group' => $this->groupid, 'user' => $this->userid, 'tag' => $this->tagid, 'entry' => $this->postid);
         $blog_listing = new blog_listing($filters);
         $this->assertFalse(array_key_exists('site', $blog_listing->filters));
         $this->assertFalse(array_key_exists('course', $blog_listing->filters));
@@ -96,8 +102,8 @@ class bloglib_testcase extends advanced_testcase {
         $this->assertTrue(array_key_exists('entry', $blog_listing->filters));
 
         // Again, but without the entry filter: This time, the tag, user and module filters are active
-        $filters = array('site' => 1, 'course' => $this->courseid, 'module' => 1,
-            'group' => $this->groupid, 'user' => 1, 'tag' => 1);
+        $filters = array('site' => $SITE->id, 'course' => $this->courseid, 'module' => $this->cmid,
+            'group' => $this->groupid, 'user' => $this->userid, 'tag' => $this->postid);
         $blog_listing = new blog_listing($filters);
         $this->assertFalse(array_key_exists('site', $blog_listing->filters));
         $this->assertFalse(array_key_exists('course', $blog_listing->filters));
@@ -107,7 +113,7 @@ class bloglib_testcase extends advanced_testcase {
         $this->assertTrue(array_key_exists('tag', $blog_listing->filters));
 
         // We should get the same result by removing the 3 inactive filters: site, course and group:
-        $filters = array('module' => 1, 'user' => 1, 'tag' => 1);
+        $filters = array('module' => $this->cmid, 'user' => $this->userid, 'tag' => $this->tagid);
         $blog_listing = new blog_listing($filters);
         $this->assertFalse(array_key_exists('site', $blog_listing->filters));
         $this->assertFalse(array_key_exists('course', $blog_listing->filters));
@@ -135,9 +141,10 @@ class bloglib_testcase extends advanced_testcase {
 
     public function test_blog_get_headers_case_7() {
         global $CFG, $PAGE, $OUTPUT;
-        $blog_headers = blog_get_headers(NULL, 1);
+        $blog_headers = blog_get_headers(NULL, $this->groupid);
         $this->assertNotEquals($blog_headers['heading'], '');
     }
+
     public function test_blog_get_headers_case_10() {
         global $CFG, $PAGE, $OUTPUT;
         $blog_headers = blog_get_headers($this->courseid);
index 36eb3d9..0aec77a 100644 (file)
@@ -490,12 +490,6 @@ $CFG->admin = 'admin';
 // $CFG->phpunit_prefix = 'phpu_';
 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
 // $CFG->phpunit_directorypermissions = 02777; // optional
-// $CFG->phpunit_extra_drivers = array(
-//      1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'),
-//      2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'),
-//      3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'),
-//      4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'),
-// ); // for database driver testing only, DB is selected via PHPUNIT_TEST_DRIVER=n
 
 //=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
index 7601fea..402bc84 100644 (file)
@@ -783,7 +783,7 @@ class grade_report_grader extends grade_report {
                     $headerlink = $this->gtree->get_element_header($element, true, $this->get_pref('showactivityicons'), false);
 
                     $itemcell = new html_table_cell();
-                    $itemcell->attributes['class'] = $type . ' ' . $catlevel . 'highlightable';
+                    $itemcell->attributes['class'] = $type . ' ' . $catlevel . ' highlightable';
 
                     if ($element['object']->is_hidden()) {
                         $itemcell->attributes['class'] .= ' hidden';
index 4885770..e18d2c9 100644 (file)
--- a/index.php
+++ b/index.php
@@ -35,7 +35,7 @@
     redirect_if_major_upgrade_required();
 
     $urlparams = array();
-    if ($CFG->defaulthomepage == HOMEPAGE_MY && optional_param('redirect', 1, PARAM_BOOL) === 0) {
+    if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY) && optional_param('redirect', 1, PARAM_BOOL) === 0) {
         $urlparams['redirect'] = 0;
     }
     $PAGE->set_url('/', $urlparams);
@@ -62,9 +62,9 @@
         // Redirect logged-in users to My Moodle overview if required
         if (optional_param('setdefaulthome', false, PARAM_BOOL)) {
             set_user_preference('user_home_page_preference', HOMEPAGE_SITE);
-        } else if ($CFG->defaulthomepage == HOMEPAGE_MY && optional_param('redirect', 1, PARAM_BOOL) === 1) {
+        } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY) && optional_param('redirect', 1, PARAM_BOOL) === 1) {
             redirect($CFG->wwwroot .'/my/');
-        } else if (!empty($CFG->defaulthomepage) && $CFG->defaulthomepage == HOMEPAGE_USER) {
+        } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER)) {
             $PAGE->settingsnav->get('usercurrentsettings')->add(get_string('makethismyhome'), new moodle_url('/', array('setdefaulthome'=>true)), navigation_node::TYPE_SETTING);
         }
     }
index ddd0051..f62cb9c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatelangdir'] = 'Verzeichnis "lang" kann nicht angelegt werden';
-$string['cannotcreatetempdir'] = 'Das Verzeichnis "temp" kann nicht angelegt werden.';
-$string['cannotdownloadcomponents'] = 'Einige Komponenten können nicht heruntergeladen werden.';
+$string['cannotcreatelangdir'] = 'Verzeichnis \'lang\' wurde nicht angelegt';
+$string['cannotcreatetempdir'] = 'Das Verzeichnis \'temp\' wurde nicht angelegt';
+$string['cannotdownloadcomponents'] = 'Einige Komponenten können nicht geladen werden.';
 $string['cannotdownloadzipfile'] = 'ZIP-Datei kann nicht heruntergeladen werden.';
-$string['cannotfindcomponent'] = 'Eine Komponente kann nicht gefunden werden';
-$string['cannotsavemd5file'] = 'Die md5-Datei kann nicht gespeichert werden';
-$string['cannotsavezipfile'] = 'Die ZIP-Datei kann nicht gespeichert werden';
+$string['cannotfindcomponent'] = 'Komponente wurde nicht gefunden';
+$string['cannotsavemd5file'] = 'Die md5-Datei wurde nicht gespeichert';
+$string['cannotsavezipfile'] = 'Die ZIP-Datei wurde nicht gespeichert';
 $string['cannotunzipfile'] = 'Die Datei kann nicht entpackt werden';
 $string['componentisuptodate'] = 'Die Komponente ist aktuell.';
 $string['downloadedfilecheckfailed'] = 'Die Überprüfung der heruntergeladenen Datei ist gescheitert';
diff --git a/install/lang/de_comm/langconfig.php b/install/lang/de_comm/langconfig.php
new file mode 100644 (file)
index 0000000..636a641
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle 2.3dev installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'de';
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'Deutsch community';
index 0af62aa..e8c2a70 100644 (file)
@@ -203,6 +203,7 @@ $string['rootsettingactivities'] = 'Include activities';
 $string['rootsettingblocks'] = 'Include blocks';
 $string['rootsettingfilters'] = 'Include filters';
 $string['rootsettingcomments'] = 'Include comments';
+$string['rootsettingcalendarevents'] = 'Include calendar events';
 $string['rootsettinguserscompletion'] = 'Include user completion details';
 $string['rootsettinglogs'] = 'Include course logs';
 $string['rootsettinggradehistories'] = 'Include grade history';
index 8fdab4f..d0c2862 100644 (file)
@@ -323,14 +323,18 @@ $string['fillincorrect'] = 'Fill in correct responses';
 $string['flagged'] = 'Flagged';
 $string['flagthisquestion'] = 'Flag this question';
 $string['generalfeedback'] = 'General feedback';
-$string['generalfeedback_help'] = 'General feedback is shown to the student after they have attempted the question. Unlike feedback, which depends on the question type and what response the student gave, the same general feedback text is shown to all students.
+$string['generalfeedback_help'] = 'General feedback is shown to the student after they have completed the question. Unlike specific feedback, which depends on the question type and what response the student gave, the same general feedback text is shown to all students.
 
-You can use the general feedback to give students some background to what knowledge the question was testing, or give them a link to more information they can use if they did not understand the questions.';
+You can use the general feedback to give students a fully worked answer and perhaps a link to more information they can use if they did not understand the questions.';
 $string['hidden'] = 'Hidden';
 $string['hintn'] = 'Hint {no}';
 $string['hinttext'] = 'Hint text';
 $string['howquestionsbehave'] = 'How questions behave';
-$string['howquestionsbehave_help'] = 'Students can interact with the questions in the quiz in various different ways. For example, you may wish the students to enter an answer to each question and then submit the entire quiz, before anything is graded or they get any feedback. That would be \'Deferred feedback\' mode. Alternatively, you may wish for students to submit each question as they go along to get immediate feedback, and if they do not get it right immediately, have another try for fewer marks. That would be \'Interactive with multiple tries\' mode.';
+$string['howquestionsbehave_help'] = 'Students can interact with the questions in the quiz in various different ways. For example, you may wish the students to enter an answer to each question and then submit the entire quiz, before anything is graded or they get any feedback. That would be \'Deferred feedback\' mode.
+
+Alternatively, you may wish for students to submit each question as they go along to get immediate feedback, and if they do not get it right immediately, have another try for fewer marks. That would be \'Interactive with multiple tries\' mode.
+
+Those are probably the two most commonly used modes of behaviour. ';
 $string['importfromcoursefiles'] = '... or choose a course file to import.';
 $string['importfromupload'] = 'Select a file to upload ...';
 $string['includesubcategories'] = 'Also show questions from sub-categories';
@@ -377,6 +381,7 @@ $string['responsehistory'] = 'Response history';
 $string['restart'] = 'Start again';
 $string['restartwiththeseoptions'] = 'Start again with these options';
 $string['rightanswer'] = 'Right answer';
+$string['rightanswer_help'] = 'an automatically generated summary of the correct response. This can be limited, so you may wish to consider explaining the correct solution in the general feedback for the question, and turning this option off.';
 $string['saved'] = 'Saved: {$a}';
 $string['saveflags'] = 'Save the state of the flags';
 $string['settingsformultipletries'] = 'Settings for multiple tries';
@@ -388,6 +393,7 @@ $string['shown'] = 'Shown';
 $string['shownumpartscorrect'] = 'Show the number of correct responses';
 $string['shownumpartscorrectwhenfinished'] = 'Show the number of correct responses once the question has finished';
 $string['specificfeedback'] = 'Specific feedback';
+$string['specificfeedback_help'] = 'Feedback that depends on what response the student gave.';
 $string['started'] = 'Started';
 $string['state'] = 'State';
 $string['step'] = 'Step';
@@ -401,6 +407,7 @@ $string['unknownquestion'] = 'Unknown question: {$a}.';
 $string['unknownquestioncatregory'] = 'Unknown question category: {$a}.';
 $string['unknownquestiontype'] = 'Unknown question type: {$a}.';
 $string['whethercorrect'] = 'Whether correct';
+$string['whethercorrect_help'] = 'This covers both the textual description \'Correct\', \'Partially correct\' or \'Incorrect\', and any coloured highlighting that conveys the same information.';
 $string['withselected'] = 'With selected';
 $string['xoutofmax'] = '{$a->mark} out of {$a->max}';
 $string['yougotnright'] = 'You have correctly selected {$a->num}.';
index 48c9099..d26e0f9 100644 (file)
@@ -269,6 +269,10 @@ function uninstall_plugin($type, $name) {
 
     // perform clean-up task common for all the plugin/subplugin types
 
+    //delete the web service functions and pre-built services
+    require_once($CFG->dirroot.'/lib/externallib.php');
+    external_delete_descriptions($component);
+
     // delete calendar events
     $DB->delete_records('event', array('modulename' => $pluginname));
 
index b39de3f..32272de 100644 (file)
@@ -24,7 +24,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-defined('MOODLE_INTERNAL') || die();
+// NOTE: no MOODLE_INTERNAL test here, sometimes we use this before requiring Moodle libs!
 
 /**
  * Get input from user
index 74fc4ef..e0e6c91 100644 (file)
@@ -650,7 +650,8 @@ function notify_login_failures() {
 
     // Now, select all the login error logged records belonging to the ips and infos
     // since lastnotifyfailure, that we have stored in the cache_flags table
-    $sql = "SELECT l.*, u.firstname, u.lastname
+    $sql = "SELECT * FROM (
+        SELECT l.*, u.firstname, u.lastname
               FROM {log} l
               JOIN {cache_flags} cf ON l.ip = cf.name
          LEFT JOIN {user} u         ON l.userid = u.id
@@ -664,8 +665,8 @@ function notify_login_failures() {
          LEFT JOIN {user} u         ON l.userid = u.id
              WHERE l.module = 'login' AND l.action = 'error'
                    AND l.time > ?
-                   AND cf.flagtype = 'login_failure_by_info'
-          ORDER BY time DESC";
+                   AND cf.flagtype = 'login_failure_by_info') t
+        ORDER BY t.time DESC";
     $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure);
 
     // Init some variables
index b24ab45..aa684b6 100644 (file)
@@ -82,12 +82,22 @@ class mssql_sql_generator extends sql_generator {
             $table = new xmldb_table($table);
         }
 
-        // From http://msdn.microsoft.com/en-us/library/ms176057.aspx
         $value = (int)$this->mdb->get_field_sql('SELECT MAX(id) FROM {'. $table->getName() . '}');
+        $sqls = array();
+
+        // MSSQL has one non-consistent behavior to create the first identity value, depending
+        // if the table has been truncated or no. If you are really interested, you can find the
+        // whole description of the problem at:
+        //     http://www.justinneff.com/archive/tag/dbcc-checkident
         if ($value == 0) {
+            // truncate to get consistent result from reseed
+            $sqls[] = "TRUNCATE TABLE " . $this->getTableName($table);
             $value = 1;
         }
-        return array("DBCC CHECKIDENT ('" . $this->getTableName($table) . "', RESEED, $value)");
+
+        // From http://msdn.microsoft.com/en-us/library/ms176057.aspx
+        $sqls[] = "DBCC CHECKIDENT ('" . $this->getTableName($table) . "', RESEED, $value)";
+        return $sqls;
     }
 
     /**
index 56e4583..3224ecd 100644 (file)
@@ -1626,7 +1626,14 @@ class ddl_test extends UnitTestCase {
 
         $record = (object)array('id'=>666, 'course'=>10);
         $DB->import_record('testtable', $record);
-        $DB->delete_records('testtable');
+        $DB->delete_records('testtable'); // This delete performs one TRUNCATE
+
+        $dbman->reset_sequence($table); // using xmldb object
+        $this->assertEqual(1, $DB->insert_record('testtable', (object)array('course'=>13)));
+
+        $record = (object)array('id'=>666, 'course'=>10);
+        $DB->import_record('testtable', $record);
+        $DB->delete_records('testtable', array()); // This delete performs one DELETE
 
         $dbman->reset_sequence($table); // using xmldb object
         $this->assertEqual(1, $DB->insert_record('testtable', (object)array('course'=>13)));
index 9f7385a..41bd729 100644 (file)
@@ -31,10 +31,8 @@ class ddl_testcase extends database_driver_testcase {
     private $records= array();
 
     protected function setUp() {
-        //global $CFG;
-        //require_once($CFG->libdir . '/adminlib.php');
-
         parent::setUp();
+        $dbman = $this->tdb->get_manager(); // loads DDL libs
 
         $table = new xmldb_table('test_table0');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
@@ -131,6 +129,8 @@ class ddl_testcase extends database_driver_testcase {
      * Fill the given test table with some records, as far as
      * DDL behaviour must be tested both with real data and
      * with empty tables
+     * @param string $tablename
+     * @return int count of records
      */
     private function fill_deftable($tablename) {
         $DB = $this->tdb; // do not use global $DB!
@@ -1600,7 +1600,14 @@ class ddl_testcase extends database_driver_testcase {
 
         $record = (object)array('id'=>666, 'course'=>10);
         $DB->import_record('testtable', $record);
-        $DB->delete_records('testtable');
+        $DB->delete_records('testtable'); // This delete performs one TRUNCATE
+
+        $dbman->reset_sequence($table); // using xmldb object
+        $this->assertEquals(1, $DB->insert_record('testtable', (object)array('course'=>13)));
+
+        $record = (object)array('id'=>666, 'course'=>10);
+        $DB->import_record('testtable', $record);
+        $DB->delete_records('testtable', array()); // This delete performs one DELETE
 
         $dbman->reset_sequence($table); // using xmldb object
         $this->assertEquals(1, $DB->insert_record('testtable', (object)array('course'=>13)));
index a31d362..b619883 100644 (file)
@@ -152,7 +152,6 @@ class database_column_info {
 
         switch ($this->meta_type) {
             case 'R': // normalise counters (usually 'id')
-                $this->auto_increment = true;
                 $this->binary         = false;
                 $this->has_default    = false;
                 $this->default_value  = null;
index 9341b86..faf097e 100644 (file)
@@ -323,7 +323,11 @@ abstract class moodle_database {
             $lowesttransaction = end($this->transactions);
             $backtrace = $lowesttransaction->get_backtrace();
 
-            error_log('Potential coding error - active database transaction detected when disposing database:'."\n".format_backtrace($backtrace, true));
+            if (defined('PHPUNIT_TEST') and PHPUNIT_TEST) {
+                //no need to log sudden exits in our PHPunit test cases
+            } else {
+                error_log('Potential coding error - active database transaction detected when disposing database:'."\n".format_backtrace($backtrace, true));
+            }
             $this->force_transaction_rollback();
         }
         if ($this->used_for_db_sessions) {
index 6e8b357..212f44f 100644 (file)
@@ -287,6 +287,13 @@ class mssql_native_moodle_database extends moodle_database {
         return $info;
     }
 
+    /**
+     * Returns if the RDBMS server fulfills the required version
+     *
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Delete this unused and protected method. MDL-32392
+     */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
         $server = $server['version'];
@@ -805,6 +812,7 @@ class mssql_native_moodle_database extends moodle_database {
         }
 
         $returning = "";
+        $isidentity = false;
 
         if ($customsequence) {
             if (!isset($params['id'])) {
@@ -812,13 +820,21 @@ class mssql_native_moodle_database extends moodle_database {
             }
             $returnid = false;
 
-            // Disable IDENTITY column before inserting record with id
-            $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
-            list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $this->free_result($result);
+            $columns = $this->get_columns($table);
+            if (isset($columns['id']) and $columns['id']->auto_increment) {
+                $isidentity = true;
+            }
+
+            // Disable IDENTITY column before inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
+                list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
+                $this->query_start($sql, null, SQL_QUERY_AUX);
+                $result = mssql_query($sql, $this->mssql);
+                $this->query_end($result);
+                $this->free_result($result);
+            }
 
         } else {
             unset($params['id']);
@@ -851,13 +867,16 @@ class mssql_native_moodle_database extends moodle_database {
         $this->free_result($result);
 
         if ($customsequence) {
-            // Enable IDENTITY column after inserting record with id
-            $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
-            list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $this->free_result($result);
+            // Enable IDENTITY column after inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
+                list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
+                $this->query_start($sql, null, SQL_QUERY_AUX);
+                $result = mssql_query($sql, $this->mssql);
+                $this->query_end($result);
+                $this->free_result($result);
+            }
         }
 
         if (!$returnid) {
index 1920973..c834eb4 100644 (file)
@@ -485,7 +485,6 @@ class mysqli_native_moodle_database extends moodle_database {
                 $rawcolumn->is_nullable              = $rawcolumn->null; unset($rawcolumn->null);
                 $rawcolumn->column_default           = $rawcolumn->default; unset($rawcolumn->default);
                 $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->default);
-                $rawcolumn->extra                    = ($rawcolumn->column_name === 'id') ? 'auto_increment' : '';
 
                 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
                     $rawcolumn->data_type = $matches[1];
@@ -955,7 +954,7 @@ class mysqli_native_moodle_database extends moodle_database {
         $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
         $this->query_end($result);
 
-        if (!$id) {
+        if (!$customsequence and !$id) {
             throw new dml_write_exception('unknown error fetching inserted id');
         }
 
index a529277..046e377 100644 (file)
@@ -296,6 +296,13 @@ class oci_native_moodle_database extends moodle_database {
         return $info;
     }
 
+    /**
+     * Returns if the RDBMS server fulfills the required version
+     *
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Delete this unused and protected method. MDL-32392
+     */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
         $server = $server['version'];
@@ -493,9 +500,13 @@ class oci_native_moodle_database extends moodle_database {
 
         // We give precedence to CHAR_LENGTH for VARCHAR2 columns over WIDTH because the former is always
         // BYTE based and, for cross-db operations, we want CHAR based results. See MDL-29415
-        $sql = "SELECT CNAME, COLTYPE, nvl(CHAR_LENGTH, WIDTH) AS WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL
+        // Instead of guessing sequence based exclusively on name, check tables against user_triggers to
+        // ensure the table has a 'before each row' trigger to assume 'id' is auto_increment. MDL-32365
+        $sql = "SELECT CNAME, COLTYPE, nvl(CHAR_LENGTH, WIDTH) AS WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL,
+                  DECODE(NVL(TRIGGER_NAME, '0'), '0', '0', '1') HASTRIGGER
                   FROM COL c
              LEFT JOIN USER_TAB_COLUMNS u ON (u.TABLE_NAME = c.TNAME AND u.COLUMN_NAME = c.CNAME AND u.DATA_TYPE = 'VARCHAR2')
+             LEFT JOIN USER_TRIGGERS t ON (t.TABLE_NAME = c.TNAME AND TRIGGER_TYPE = 'BEFORE EACH ROW' AND c.CNAME = 'ID')
                  WHERE TNAME = UPPER('{" . $table . "}')
               ORDER BY COLNO";
 
@@ -517,6 +528,7 @@ class oci_native_moodle_database extends moodle_database {
 
             $info = new stdClass();
             $info->name = strtolower($rawcolumn->CNAME);
+            $info->auto_increment = ((int)$rawcolumn->HASTRIGGER) ? true : false;
             $matches = null;
 
             if ($rawcolumn->COLTYPE === 'VARCHAR2'
@@ -550,7 +562,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->binary        = false;
                 $info->unsigned      = null;
-                $info->auto_increment= false;
                 $info->unique        = null;
 
             } else if ($rawcolumn->COLTYPE === 'NUMBER') {
@@ -563,13 +574,11 @@ class oci_native_moodle_database extends moodle_database {
                         $info->primary_key   = true;
                         $info->meta_type     = 'R';
                         $info->unique        = true;
-                        $info->auto_increment= true;
                         $info->has_default   = false;
                     } else {
                         $info->primary_key   = false;
                         $info->meta_type     = 'I';
                         $info->unique        = null;
-                        $info->auto_increment= false;
                     }
                     $info->scale = null;
 
@@ -578,7 +587,6 @@ class oci_native_moodle_database extends moodle_database {
                     $info->meta_type     = 'N';
                     $info->primary_key   = false;
                     $info->unsigned      = null;
-                    $info->auto_increment= false;
                     $info->unique        = null;
                     $info->scale         = $rawcolumn->SCALE;
                 }
@@ -596,7 +604,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->meta_type     = 'N';
                 $info->unique        = null;
-                $info->auto_increment= false;
                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
                 if ($info->has_default) {
@@ -632,7 +639,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->binary        = false;
                 $info->unsigned      = null;
-                $info->auto_increment= false;
                 $info->unique        = null;
 
             } else if ($rawcolumn->COLTYPE === 'BLOB') {
@@ -661,7 +667,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->binary        = true;
                 $info->unsigned      = null;
-                $info->auto_increment= false;
                 $info->unique        = null;
 
             } else {
index 3d9771d..f8367fe 100644 (file)
@@ -179,8 +179,14 @@ class pgsql_native_moodle_database extends moodle_database {
         pg_set_client_encoding($this->pgsql, 'utf8');
         $this->query_end(true);
 
-        // find out the bytea oid
-        $sql = "SELECT oid FROM pg_type WHERE typname = 'bytea'";
+        $sql = '';
+        // Only for 9.0 and upwards, set bytea encoding to old format.
+        if ($this->is_min_version('9.0')) {
+            $sql = "SET bytea_output = 'escape'; ";
+        }
+
+        // Find out the bytea oid.
+        $sql .= "SELECT oid FROM pg_type WHERE typname = 'bytea'";
         $this->query_start($sql, null, SQL_QUERY_AUX);
         $result = pg_query($this->pgsql, $sql);
         $this->query_end($result);
@@ -251,6 +257,13 @@ class pgsql_native_moodle_database extends moodle_database {
         return array('description'=>$info['server'], 'version'=>$info['server']);
     }
 
+    /**
+     * Returns if the RDBMS server fulfills the required version
+     *
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Make this method private. MDL-32392
+     */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
         $server = $server['version'];
index 6a9c832..d3261c9 100644 (file)
@@ -825,6 +825,18 @@ class dml_test extends UnitTestCase {
         // Test get_columns for non-existing table returns empty array. MDL-30147
         $columns = $DB->get_columns('xxxx');
         $this->assertEqual(array(), $columns);
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $columns = $DB->get_columns($tablename);
+        $this->assertFalse($columns['id']->auto_increment);
     }
 
     public function test_get_manager() {
@@ -1848,6 +1860,20 @@ class dml_test extends UnitTestCase {
         } catch (dml_exception $ex) {
             $this->assertTrue(true);
         }
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $record = (object)array('id'=>5, 'course' => 1);
+        $DB->insert_record_raw($tablename, $record, false, false, true);
+        $record = $DB->get_record($tablename, array());
+        $this->assertEqual(5, $record->id);
     }
 
     public function test_insert_record() {
index 5fe2ab2..996b4e8 100644 (file)
@@ -152,7 +152,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
          * Log all Errors.
          */
         sqlsrv_configure("WarningsReturnAsErrors", FALSE);
-        sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_ALL);
+        sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF);
         sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR);
 
         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
@@ -277,10 +277,11 @@ class sqlsrv_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Get the minimum SQL allowed
+     * Returns if the RDBMS server fulfills the required version
      *
-     * @param mixed $version
-     * @return mixed
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Delete this unused and protected method. MDL-32392
      */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
@@ -391,7 +392,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         $this->tables = array ();
         $prefix = str_replace('_', '\\_', $this->prefix);
         $sql = "SELECT table_name
-                  FROM information_schema.tables
+                  FROM INFORMATION_SCHEMA.TABLES
                  WHERE table_name LIKE '$prefix%' ESCAPE '\\' AND table_type = 'BASE TABLE'";
 
         $this->query_start($sql, null, SQL_QUERY_AUX);
@@ -497,7 +498,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
                            is_nullable AS is_nullable,
                            columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
                            column_default AS default_value
-                      FROM information_schema.columns
+                      FROM INFORMATION_SCHEMA.COLUMNS
                      WHERE table_name = '{".$table."}'
                   ORDER BY ordinal_position";
         } else { // temp table, get metadata from tempdb schema
@@ -509,7 +510,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
                            is_nullable AS is_nullable,
                            columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
                            column_default AS default_value
-                      FROM tempdb.information_schema.columns ".
+                      FROM tempdb.INFORMATION_SCHEMA.COLUMNS ".
             // check this statement
             // JOIN tempdb..sysobjects ON name = table_name
             // WHERE id = object_id('tempdb..{".$table."}')
@@ -871,14 +872,26 @@ class sqlsrv_native_moodle_database extends moodle_database {
         if (!is_array($params)) {
             $params = (array)$params;
         }
+
+        $isidentity = false;
+
         if ($customsequence) {
             if (!isset($params['id'])) {
                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
             }
+
             $returnid = false;
-            // Disable IDENTITY column before inserting record with id
-            $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!!
-            $this->do_query($sql, null, SQL_QUERY_AUX);
+            $columns = $this->get_columns($table);
+            if (isset($columns['id']) and $columns['id']->auto_increment) {
+                $isidentity = true;
+            }
+
+            // Disable IDENTITY column before inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!!
+                $this->do_query($sql, null, SQL_QUERY_AUX);
+            }
 
         } else {
             unset($params['id']);
@@ -894,9 +907,12 @@ class sqlsrv_native_moodle_database extends moodle_database {
         $query_id = $this->do_query($sql, $params, SQL_QUERY_INSERT);
 
         if ($customsequence) {
-            // Enable IDENTITY column after inserting record with id
-            $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!!
-            $this->do_query($sql, null, SQL_QUERY_AUX);
+            // Enable IDENTITY column after inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!!
+                $this->do_query($sql, null, SQL_QUERY_AUX);
+            }
         }
 
         if ($returnid) {
index 9fe16a4..e91dbdf 100644 (file)
@@ -28,6 +28,11 @@ defined('MOODLE_INTERNAL') || die();
 
 class dml_testcase extends database_driver_testcase {
 
+    protected function setUp() {
+        parent::setUp();
+        $dbman = $this->tdb->get_manager(); // loads DDL libs
+    }
+
     /**
      * Get a xmldb_table object for testing, deleting any existing table
      * of the same name, for example if one was left over from a previous test
@@ -778,6 +783,18 @@ class dml_testcase extends database_driver_testcase {
         // Test get_columns for non-existing table returns empty array. MDL-30147
         $columns = $DB->get_columns('xxxx');
         $this->assertEquals(array(), $columns);
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $columns = $DB->get_columns($tablename);
+        $this->assertFalse($columns['id']->auto_increment);
     }
 
     public function test_get_manager() {
@@ -1801,6 +1818,20 @@ class dml_testcase extends database_driver_testcase {
         } catch (dml_exception $ex) {
             $this->assertTrue(true);
         }
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $record = (object)array('id'=>5, 'course' => 1);
+        $DB->insert_record_raw($tablename, $record, false, false, true);
+        $record = $DB->get_record($tablename, array());
+        $this->assertEquals(5, $record->id);
     }
 
     public function test_insert_record() {
@@ -4372,7 +4403,7 @@ class dml_testcase extends database_driver_testcase {
 
         // make sure reserved words do not cause fatal problems in query parameters
 
-        $DB->execute("UPDATE {{$tablename}} SET course = 1 WHERE ID = :select", array('select'=>1));
+        $DB->execute("UPDATE {{$tablename}} SET course = 1 WHERE id = :select", array('select'=>1));
         $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
         $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
         $rs->close();
index befe5d2..c90139d 100644 (file)
@@ -204,11 +204,8 @@ function enrol_check_plugins($user) {
         return;
     }
 
-    if (is_siteadmin()) {
-        // no sync for admin user, please use admin accounts only for admin tasks like the unix root user!
-        // if plugin fails on sync admins need to be able to log in and fix the settings
-        return;
-    }
+    // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
+    // which proved it was actually not necessary.
 
     static $inprogress = array();  // To prevent this function being called more than once in an invocation
 
@@ -264,8 +261,8 @@ function enrol_sharing_course($user1, $user2) {
 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
     global $DB, $CFG;
 
-    $user1 = !empty($user1->id) ? $user1->id : $user1;
-    $user2 = !empty($user2->id) ? $user2->id : $user2;
+    $user1 = isset($user1->id) ? $user1->id : $user1;
+    $user2 = isset($user2->id) ? $user2->id : $user2;
 
     if (empty($user1) or empty($user2)) {
         return false;
index 8875307..2cb1118 100644 (file)
@@ -487,4 +487,24 @@ function external_create_service_token($servicename, $context){
     global $USER, $DB;
     $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST);
     return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0);
+}
+
+/**
+ * Delete all pre-built services (+ related tokens) and external functions information defined in the specified component.
+ *
+ * @param string $component name of component (moodle, mod_assignment, etc.)
+ */
+function external_delete_descriptions($component) {
+    global $DB;
+
+    $params = array($component);
+
+    $DB->delete_records_select('external_tokens',
+            "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
+    $DB->delete_records_select('external_services_users',
+            "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
+    $DB->delete_records_select('external_services_functions',
+            "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", $params);
+    $DB->delete_records('external_services', array('component'=>$component));
+    $DB->delete_records('external_functions', array('component'=>$component));
 }
\ No newline at end of file
diff --git a/lib/grade/tests/fixtures/lib.php b/lib/grade/tests/fixtures/lib.php
new file mode 100644 (file)
index 0000000..7ca2520
--- /dev/null
@@ -0,0 +1,773 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/gradelib.php');
+
+
+/**
+ * Shared code for all grade related tests.
+ *
+ * Here is a brief explanation of the test data set up in these unit tests.
+ * category1 => array(category2 => array(grade_item1, grade_item2), category3 => array(grade_item3))
+ * 3 users for 3 grade_items
+ */
+class grade_base_testcase extends advanced_testcase {
+
+    protected $course;
+    protected $activities = array();
+    protected $grade_items = array();
+    protected $grade_categories = array();
+    protected $grade_grades = array();
+    protected $grade_outcomes = array();
+    protected $scale = array();
+    protected $scalemax = array();
+
+    protected $courseid;
+    protected $userid;
+
+    protected function setUp() {
+        global $CFG;
+        parent::setup();
+
+        $this->resetAfterTest(true);
+
+        $CFG->grade_droplow = -1;
+        $CFG->grade_keephigh = -1;
+        $CFG->grade_aggregation = -1;
+        $CFG->grade_aggregateonlygraded = -1;
+        $CFG->grade_aggregateoutcomes = -1;
+        $CFG->grade_aggregatesubcats = -1;
+
+        $this->course = $this->getDataGenerator()->create_course();
+        $this->courseid = $this->course->id;
+
+        $this->user[0] = $this->getDataGenerator()->create_user();
+        $this->user[1] = $this->getDataGenerator()->create_user();
+        $this->user[2] = $this->getDataGenerator()->create_user();
+        $this->user[3] = $this->getDataGenerator()->create_user();
+        $this->userid = $this->user[0]->id;
+
+        $this->load_modules();
+
+        $this->load_scales();
+        $this->load_grade_categories();
+        $this->load_grade_items();
+        $this->load_grade_grades();
+        $this->load_grade_outcomes();
+    }
+
+    public function test_void () {
+        // empty method to keep PHPUnit happy
+    }
+
+    private function load_modules() {
+        $this->activities[0] = $this->getDataGenerator()->create_module('assignment', array('course'=>$this->course->id));
+        $this->course_module[0] = get_coursemodule_from_instance('assignment', $this->activities[0]->id);
+
+        $this->activities[1] = $this->getDataGenerator()->create_module('assignment', array('course'=>$this->course->id));
+        $this->course_module[1] = get_coursemodule_from_instance('assignment', $this->activities[1]->id);
+
+        $this->activities[2] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[2] = get_coursemodule_from_instance('forum', $this->activities[2]->id);
+
+        $this->activities[3] = $this->getDataGenerator()->create_module('page', array('course'=>$this->course->id));
+        $this->course_module[3] = get_coursemodule_from_instance('page', $this->activities[3]->id);
+
+        $this->activities[4] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[4] = get_coursemodule_from_instance('forum', $this->activities[4]->id);
+
+        $this->activities[5] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[5] = get_coursemodule_from_instance('forum', $this->activities[5]->id);
+
+        $this->activities[6] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[6] = get_coursemodule_from_instance('forum', $this->activities[6]->id);
+    }
+
+    private function load_scales() {
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale1';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Way off topic, Not very helpful, Fairly neutral, Fairly helpful, Supportive, Some good information, Perfect answer!';
+        $scale->description = 'This scale defines some of qualities that make posts helpful within the Moodle help forums.\n Your feedback will help others see how their posts are being received.';
+
+        $this->scale[0] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[0] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale2';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Distinction, Very Good, Good, Pass, Fail';
+        $scale->description = 'This scale is used to mark standard assignments.';
+
+        $this->scale[1] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[1] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale3';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Loner, Contentious, Disinterested, Participative, Follower, Leader';
+        $scale->description = 'Describes the level of teamwork of a student.';
+        $temp  = explode(',', $scale->scale);
+        $scale->max         = count($temp) -1;
+
+        $this->scale[2] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[2] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale4';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Does not understand theory, Understands theory but fails practice, Manages through, Excels';
+        $scale->description = 'Level of expertise at a technical task, with a theoretical framework.';
+        $temp  = explode(',', $scale->scale);
+        $scale->max         = count($temp) -1;
+
+        $this->scale[3] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[3] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale5';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Insufficient, Acceptable, Excellent.';
+        $scale->description = 'Description of skills.';
+
+        $this->scale[4] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[4] = substr_count($scale->scale, ',');
+    }
+
+    /**
+     * Load grade_category data into the database, and adds the corresponding objects to this class' variable.
+     * category structure:
+                              course category
+                                    |
+                           +--------+-------------+
+                           |                      |
+             unittestcategory1               level1category
+                  |
+         +--------+-------------+
+         |                      |
+        unittestcategory2  unittestcategory3
+     */
+    private function load_grade_categories() {
+        global $DB;
+
+        $course_category = grade_category::fetch_course_category($this->course->id);
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'unittestcategory1';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $course_category->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 2;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = '/'.$course_category->id.'/'.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[0] = $grade_category;
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'unittestcategory2';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $this->grade_categories[0]->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 3;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = $this->grade_categories[0]->path.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[1] = $grade_category;
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'unittestcategory3';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $this->grade_categories[0]->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 3;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = $this->grade_categories[0]->path.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[2] = $grade_category;
+
+        // A category with no parent, but grade_items as children
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'level1category';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $course_category->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 2;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = '/'.$course_category->id.'/'.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[3] = $grade_category;
+    }
+
+    /**
+     * Load grade_item data into the database, and adds the corresponding objects to this class' variable.
+     */
+    protected function load_grade_items() {
+        global $DB;
+
+        $course_category = grade_category::fetch_course_category($this->course->id);
+
+        // id = 0
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitem1';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[0]->modname;
+        $grade_item->iteminstance = $this->course_module[0]->instance;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 30;
+        $grade_item->grademax = 110;
+        $grade_item->itemnumber = 1;
+        $grade_item->idnumber = 'item id 0';
+        $grade_item->iteminfo = 'Grade item 0 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 3;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[0] = $grade_item;
+
+        // id = 1
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitem2';
+        $grade_item->itemtype = 'import';
+        $grade_item->itemmodule = $this->course_module[1]->modname;
+        $grade_item->iteminstance = $this->course_module[1]->instance;
+        $grade_item->calculation = '= ##gi'.$this->grade_items[0]->id.'## + 30 + [[item id 0]] - [[item id 0]]';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->itemnumber = null;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 1 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 4;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[1] = $grade_item;
+
+        // id = 2
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[2]->id;
+        $grade_item->itemname = 'unittestgradeitem3';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[2]->modname;
+        $grade_item->iteminstance = $this->course_module[2]->instance;
+        $grade_item->gradetype = GRADE_TYPE_SCALE;
+        $grade_item->scaleid = $this->scale[0]->id;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = $this->scalemax[0];
+        $grade_item->iteminfo = 'Grade item 2 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 6;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[2] = $grade_item;
+
+        // Load grade_items associated with the 3 categories
+        // id = 3
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->iteminstance = $this->grade_categories[0]->id;
+        $grade_item->itemname = 'unittestgradeitemcategory1';
+        $grade_item->needsupdate = 0;
+        $grade_item->itemtype = 'category';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 3 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 1;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[3] = $grade_item;
+
+        // id = 4
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->iteminstance = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitemcategory2';
+        $grade_item->itemtype = 'category';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->needsupdate = 0;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 4 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 2;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[4] = $grade_item;
+
+        // id = 5
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->iteminstance = $this->grade_categories[2]->id;
+        $grade_item->itemname = 'unittestgradeitemcategory3';
+        $grade_item->itemtype = 'category';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->needsupdate = true;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 5 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 5;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[5] = $grade_item;
+
+        // Orphan grade_item
+        // id = 6
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $course_category->id;
+        $grade_item->itemname = 'unittestorphangradeitem1';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[4]->modname;
+        $grade_item->iteminstance = $this->course_module[4]->instance;
+        $grade_item->itemnumber = 0;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 10;
+        $grade_item->grademax = 120;
+        $grade_item->locked = time();
+        $grade_item->iteminfo = 'Orphan Grade 6 item used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 7;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[6] = $grade_item;
+
+        // 2 grade items under level1category
+        // id = 7
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[3]->id;
+        $grade_item->itemname = 'singleparentitem1';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[5]->modname;
+        $grade_item->iteminstance = $this->course_module[5]->instance;
+        $grade_item->gradetype = GRADE_TYPE_SCALE;
+        $grade_item->scaleid = $this->scale[0]->id;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = $this->scalemax[0];
+        $grade_item->iteminfo = 'Grade item 7 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 9;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[7] = $grade_item;
+
+        // id = 8
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[3]->id;
+        $grade_item->itemname = 'singleparentitem2';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[6]->modname;
+        $grade_item->iteminstance = $this->course_module[6]->instance;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 8 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 10;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[8] = $grade_item;
+
+        // Grade_item for level1category
+        // id = 9
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->itemname = 'grade_item for level1 category';
+        $grade_item->itemtype = 'category';
+        $grade_item->iteminstance = $this->grade_categories[3]->id;
+        $grade_item->needsupdate = true;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Orphan Grade item 9 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 8;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[9] = $grade_item;
+
+        // Manual grade_item
+        // id = 10
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $course_category->id;
+        $grade_item->itemname = 'manual grade_item';
+        $grade_item->itemtype = 'manual';
+        $grade_item->itemnumber = 0;
+        $grade_item->needsupdate = false;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Manual grade item 10 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[10] = $grade_item;
+    }
+
+    /**
+     * Load grade_grades data into the database, and adds the corresponding objects to this class' variable.
+     */
+    private function load_grade_grades() {
+        global $DB;
+
+        //this method is called once for each test method. Avoid adding things to $this->grade_grades multiple times
+        $this->grade_grades = array();
+
+        // Grades for grade_item 1
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[0]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 15; // too small
+        $grade->finalgrade = 30;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '1 of 17 grade_grades';
+        $grade->informationformat = FORMAT_PLAIN;
+        $grade->feedback = 'Good, but not good enough..';
+        $grade->feedbackformat = FORMAT_PLAIN;
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[0] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[0]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 40;
+        $grade->finalgrade = 40;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '2 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[1] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[0]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 170; // too big
+        $grade->finalgrade = 110;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '3 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[2] = $grade;
+
+
+        // No raw grades for grade_item 2 - it is calculated
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[1]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->finalgrade = 60;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '4 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[3] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[1]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->finalgrade = 70;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '5 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[4] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[1]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->finalgrade = 100;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '6 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[5] = $grade;
+
+
+        // Grades for grade_item 3
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[2]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 2;
+        $grade->finalgrade = 6;
+        $grade->scaleid = $this->scale[3]->id;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '7 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[6] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[2]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 3;
+        $grade->finalgrade = 2;
+        $grade->scaleid = $this->scale[3]->id;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '8 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[2]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 1;
+        $grade->finalgrade = 3;
+        $grade->scaleid = $this->scale[3]->id;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '9 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        // Grades for grade_item 7
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[6]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 97;
+        $grade->finalgrade = 69;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '10 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[6]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 49;
+        $grade->finalgrade = 87;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '11 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[6]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 67;
+        $grade->finalgrade = 94;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '12 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        // Grades for grade_item 8
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[7]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 3;
+        $grade->finalgrade = 3;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '13 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[7]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 6;
+        $grade->finalgrade = 6;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '14 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        // Grades for grade_item 9
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[8]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 20;
+        $grade->finalgrade = 20;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '15 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[8]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 50;
+        $grade->finalgrade = 50;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '16 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[8]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 100;
+        $grade->finalgrade = 100;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '17 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+    }
+
+    /**
+     * Load grade_outcome data into the database, and adds the corresponding objects to this class' variable.
+     */
+    private function load_grade_outcomes() {
+        global $DB;
+
+        //this method is called once for each test method. Avoid adding things to $this->grade_outcomes multiple times
+        $this->grade_outcomes = array();
+
+        // Calculation for grade_item 1
+        $grade_outcome = new stdClass();
+        $grade_outcome->fullname = 'Team work';
+        $grade_outcome->shortname = 'Team work';
+        $grade_outcome->fullname = 'Team work outcome';
+        $grade_outcome->timecreated = time();
+        $grade_outcome->timemodified = time();
+        $grade_outcome->scaleid = $this->scale[2]->id;
+
+        $grade_outcome->id = $DB->insert_record('grade_outcomes', $grade_outcome);
+        $this->grade_outcomes[] = $grade_outcome;
+
+        // Calculation for grade_item 2
+        $grade_outcome = new stdClass();
+        $grade_outcome->fullname = 'Complete circuit board';
+        $grade_outcome->shortname = 'Complete circuit board';
+        $grade_outcome->fullname = 'Complete circuit board';
+        $grade_outcome->timecreated = time();
+        $grade_outcome->timemodified = time();
+        $grade_outcome->scaleid = $this->scale[3]->id;
+
+        $grade_outcome->id = $DB->insert_record('grade_outcomes', $grade_outcome);
+        $this->grade_outcomes[] = $grade_outcome;
+
+        // Calculation for grade_item 3
+        $grade_outcome = new stdClass();
+        $grade_outcome->fullname = 'Debug Java program';
+        $grade_outcome->shortname = 'Debug Java program';
+        $grade_outcome->fullname = 'Debug Java program';
+        $grade_outcome->timecreated = time();
+        $grade_outcome->timemodified = time();
+        $grade_outcome->scaleid = $this->scale[4]->id;
+
+        $grade_outcome->id = $DB->insert_record('grade_outcomes', $grade_outcome);
+        $this->grade_outcomes[] = $grade_outcome;
+    }
+}
+
+
diff --git a/lib/grade/tests/grade_category_test.php b/lib/grade/tests/grade_category_test.php
new file mode 100644 (file)
index 0000000..102ef86
--- /dev/null
@@ -0,0 +1,646 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_category_testcase extends grade_base_testcase {
+
+    public function test_grade_category() {
+        $this->sub_test_grade_category_construct();
+        $this->sub_test_grade_category_build_path();
+        $this->sub_test_grade_category_fetch();
+        $this->sub_test_grade_category_fetch_all();
+        $this->sub_test_grade_category_update();
+        $this->sub_test_grade_category_delete();
+        $this->sub_test_grade_category_insert();
+        $this->sub_test_grade_category_qualifies_for_regrading();
+        $this->sub_test_grade_category_force_regrading();
+        $this->sub_test_grade_category_aggregate_grades();
+        $this->sub_test_grade_category_apply_limit_rules();
+        $this->sub_test_grade_category_is_aggregationcoef_used();
+        $this->sub_test_grade_category_fetch_course_tree();
+        $this->sub_test_grade_category_get_children();
+        $this->sub_test_grade_category_load_grade_item();
+        $this->sub_test_grade_category_get_grade_item();
+        $this->sub_test_grade_category_load_parent_category();
+        $this->sub_test_grade_category_get_parent_category();
+        $this->sub_test_grade_category_get_name();
+        $this->sub_test_grade_category_set_parent();
+        $this->sub_test_grade_category_get_final();
+        $this->sub_test_grade_category_get_sortorder();
+        $this->sub_test_grade_category_set_sortorder();
+        $this->sub_test_grade_category_is_editable();
+        $this->sub_test_grade_category_move_after_sortorder();
+        $this->sub_test_grade_category_is_course_category();
+        $this->sub_test_grade_category_fetch_course_category();
+        $this->sub_test_grade_category_is_locked();
+        $this->sub_test_grade_category_set_locked();
+        $this->sub_test_grade_category_is_hidden();
+        $this->sub_test_grade_category_set_hidden();
+
+        //this won't work until MDL-11837 is complete
+        //$this->sub_test_grade_category_generate_grades();
+
+        //do this last as adding a second course category messes up the data
+        $this->sub_test_grade_category_insert_course_category();
+    }
+
+    //adds 3 new grade categories at various depths
+    protected function sub_test_grade_category_construct() {
+        $course_category = grade_category::fetch_course_category($this->courseid);
+
+        $params = new stdClass();
+
+        $params->courseid = $this->courseid;
+        $params->fullname = 'unittestcategory4';
+
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+        $this->grade_categories[] = $grade_category;
+
+        $this->assertEquals($params->courseid, $grade_category->courseid);
+        $this->assertEquals($params->fullname, $grade_category->fullname);
+        $this->assertEquals(2, $grade_category->depth);
+        $this->assertEquals("/$course_category->id/$grade_category->id/", $grade_category->path);
+        $parentpath = $grade_category->path;
+
+        // Test a child category
+        $params->parent = $grade_category->id;
+        $params->fullname = 'unittestcategory5';
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+        $this->grade_categories[] = $grade_category;
+
+        $this->assertEquals(3, $grade_category->depth);
+        $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
+        $parentpath = $grade_category->path;
+
+        // Test a third depth category
+        $params->parent = $grade_category->id;
+        $params->fullname = 'unittestcategory6';
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+        $this->grade_categories[50] = $grade_category;//going to delete this one later hence the special index
+
+        $this->assertEquals(4, $grade_category->depth);
+        $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
+    }
+
+    protected function sub_test_grade_category_build_path() {
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($grade_category, 'build_path'));
+        $path = grade_category::build_path($grade_category);
+        $this->assertEquals($grade_category->path, $path);
+    }
+
+    protected function sub_test_grade_category_fetch() {
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'fetch'));
+
+        $grade_category = grade_category::fetch(array('id'=>$this->grade_categories[0]->id));
+        $this->assertEquals($this->grade_categories[0]->id, $grade_category->id);
+        $this->assertEquals($this->grade_categories[0]->fullname, $grade_category->fullname);
+    }
+
+    protected function sub_test_grade_category_fetch_all() {
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'fetch_all'));
+
+        $grade_categories = grade_category::fetch_all(array('courseid'=>$this->courseid));
+        $this->assertEquals(count($this->grade_categories), count($grade_categories)-1);
+    }
+
+    protected function sub_test_grade_category_update() {
+        global $DB;
+        $grade_category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($grade_category, 'update'));
+
+        $grade_category->fullname = 'Updated info for this unittest grade_category';
+        $grade_category->path = null; // path must be recalculated if missing
+        $grade_category->depth = null;
+        $grade_category->aggregation = GRADE_AGGREGATE_MAX; // should force regrading
+
+        $grade_item = $grade_category->get_grade_item();
+        $this->assertEquals(0, $grade_item->needsupdate);
+
+        $this->assertTrue($grade_category->update());
+
+        $fullname = $DB->get_field('grade_categories', 'fullname', array('id' => $this->grade_categories[0]->id));
+        $this->assertEquals($grade_category->fullname, $fullname);
+
+        $path = $DB->get_field('grade_categories', 'path', array('id' => $this->grade_categories[0]->id));
+        $this->assertEquals($grade_category->path, $path);
+
+        $depth = $DB->get_field('grade_categories', 'depth', array('id' => $this->grade_categories[0]->id));
+        $this->assertEquals($grade_category->depth, $depth);
+
+        $grade_item = $grade_category->get_grade_item();
+        $this->assertEquals(1, $grade_item->needsupdate);
+    }
+
+    protected function sub_test_grade_category_delete() {
+        global $DB;
+
+        $grade_category = new grade_category($this->grade_categories[50]);
+        $this->assertTrue(method_exists($grade_category, 'delete'));
+
+        $this->assertTrue($grade_category->delete());
+        $this->assertFalse($DB->get_record('grade_categories', array('id' => $grade_category->id)));
+    }
+
+    protected function sub_test_grade_category_insert() {
+        $course_category = grade_category::fetch_course_category($this->courseid);
+
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'insert'));
+
+        $grade_category->fullname    = 'unittestcategory4';
+        $grade_category->courseid    = $this->courseid;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 100;
+        $grade_category->droplow     = 10;
+        $grade_category->hidden      = 0;
+        $grade_category->parent      = $this->grade_categories[1]->id; //sub_test_grade_category_delete() removed the category at 0
+
+        $grade_category->insert();
+
+        $this->assertEquals('/'.$course_category->id.'/'.$this->grade_categories[1]->parent.'/'.$this->grade_categories[1]->id.'/'.$grade_category->id.'/', $grade_category->path);
+        $this->assertEquals(4, $grade_category->depth);
+
+        $last_grade_category = end($this->grade_categories);
+
+        $this->assertFalse(empty($grade_category->grade_item));
+        $this->assertEquals($grade_category->id, $grade_category->grade_item->iteminstance);
+        $this->assertEquals('category', $grade_category->grade_item->itemtype);
+
+        $this->assertEquals($grade_category->id, $last_grade_category->id + 1);
+        $this->assertFalse(empty($grade_category->timecreated));
+        $this->assertFalse(empty($grade_category->timemodified));
+    }
+
+    protected function sub_test_grade_category_qualifies_for_regrading() {
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($grade_category, 'qualifies_for_regrading'));
+        $this->assertFalse($grade_category->qualifies_for_regrading());
+
+        $grade_category->aggregation = GRADE_AGGREGATE_MAX;
+        $this->assertTrue($grade_category->qualifies_for_regrading());
+
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $grade_category->droplow = 99;
+        $this->assertTrue($grade_category->qualifies_for_regrading());
+
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $grade_category->keephigh = 99;
+        $this->assertTrue($grade_category->qualifies_for_regrading());
+    }
+
+    protected function sub_test_grade_category_force_regrading() {
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($grade_category, 'force_regrading'));
+
+        $grade_category->load_grade_item();
+        $this->assertEquals(0, $grade_category->grade_item->needsupdate);
+
+        $grade_category->force_regrading();
+
+        $grade_category->grade_item = null;
+        $grade_category->load_grade_item();
+
+        $this->assertEquals(1, $grade_category->grade_item->needsupdate);
+    }
+
+    /**
+     * Tests the calculation of grades using the various aggregation methods with and without hidden grades
+     * This will not work entirely until MDL-11837 is done
+     * @global type $DB
+     */
+    protected function sub_test_grade_category_generate_grades() {
+        global $DB;
+
+        //inserting some special grade items to make testing the final grade calculation easier
+        $params->courseid = $this->courseid;
+        $params->fullname = 'unittestgradecalccategory';
+        $params->aggregation = GRADE_AGGREGATE_MEAN;
+        $params->aggregateonlygraded = 0;
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+
+        $this->assertTrue(method_exists($grade_category, 'generate_grades'));
+
+        $grade_category->load_grade_item();
+        $cgi = $grade_category->get_grade_item();
+        $cgi->grademin = 0;
+        $cgi->grademax = 20;//3 grade items out of 10 but category is out of 20 to force scaling to occur
+        $cgi->update();
+
+        //3 grade items each with a maximum grade of 10
+        $grade_items = array();
+        for ($i=0; $i<3; $i++) {
+            $grade_items[$i] = new grade_item();
+            $grade_items[$i]->courseid = $this->courseid;
+            $grade_items[$i]->categoryid = $grade_category->id;
+            $grade_items[$i]->itemname = 'manual grade_item '.$i;
+            $grade_items[$i]->itemtype = 'manual';
+            $grade_items[$i]->itemnumber = 0;
+            $grade_items[$i]->needsupdate = false;
+            $grade_items[$i]->gradetype = GRADE_TYPE_VALUE;
+            $grade_items[$i]->grademin = 0;
+            $grade_items[$i]->grademax = 10;
+            $grade_items[$i]->iteminfo = 'Manual grade item used for unit testing';
+            $grade_items[$i]->timecreated = time();
+            $grade_items[$i]->timemodified = time();
+
+            //used as the weight by weighted mean and as extra credit by mean with extra credit
+            //Will be 0, 1 and 2
+            $grade_items[$i]->aggregationcoef = $i;
+
+            $grade_items[$i]->insert();
+        }
+
+        //a grade for each grade item
+        $grade_grades = array();
+        for ($i=0; $i<3; $i++) {
+            $grade_grades[$i] = new grade_grade();
+            $grade_grades[$i]->itemid = $grade_items[$i]->id;
+            $grade_grades[$i]->userid = $this->userid;
+            $grade_grades[$i]->rawgrade = ($i+1)*2;//produce grade grades of 2, 4 and 6
+            $grade_grades[$i]->finalgrade = ($i+1)*2;
+            $grade_grades[$i]->timecreated = time();
+            $grade_grades[$i]->timemodified = time();
+            $grade_grades[$i]->information = '1 of 2 grade_grades';
+            $grade_grades[$i]->informationformat = FORMAT_PLAIN;
+            $grade_grades[$i]->feedback = 'Good, but not good enough..';
+            $grade_grades[$i]->feedbackformat = FORMAT_PLAIN;
+
+            $grade_grades[$i]->insert();
+        }
+
+        //3 grade items with 1 grade_grade each.
+        //grade grades have the values 2, 4 and 6
+
+        //First correct answer is the aggregate with all 3 grades
+        //Second correct answer is with the first grade (value 2) hidden
+
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEDIAN, 'GRADE_AGGREGATE_MEDIAN', 8, 8);
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MAX, 'GRADE_AGGREGATE_MAX', 12, 12);
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MODE, 'GRADE_AGGREGATE_MODE', 12, 12);
+
+        //weighted mean. note grade totals are rounded to an int to prevent rounding discrepancies. correct final grade isnt actually exactly 10
+        //3 items with grades 2, 4 and 6 with weights 0, 1 and 2 and all out of 10. then doubled to be out of 20.
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN, 'GRADE_AGGREGATE_WEIGHTED_MEAN', 10, 10);
+
+        //simple weighted mean
+        //3 items with grades 2, 4 and 6 equally weighted and all out of 10. then doubled to be out of 20.
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN2, 'GRADE_AGGREGATE_WEIGHTED_MEAN2', 8, 10);
+
+        //mean of grades with extra credit
+        //3 items with grades 2, 4 and 6 with extra credit 0, 1 and 2 equally weighted and all out of 10. then doubled to be out of 20.
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_EXTRACREDIT_MEAN, 'GRADE_AGGREGATE_EXTRACREDIT_MEAN', 10, 13);
+
+        //aggregation tests the are affected by a hidden grade currently dont work as we dont store the altered grade in the database
+        //instead an in memory recalculation is done. This should be remedied by MDL-11837
+
+        //fails with 1 grade hidden. still reports 8 as being correct
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEAN, 'GRADE_AGGREGATE_MEAN', 8, 10);
+
+        //fails with 1 grade hidden. still reports 4 as being correct
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MIN, 'GRADE_AGGREGATE_MIN', 4, 8);
+
+        //fails with 1 grade hidden. still reports 12 as being correct
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_SUM, 'GRADE_AGGREGATE_SUM', 12, 10);
+    }
+
+    /**
+     * Test grade category aggregation using the supplied grade objects and aggregation method
+     * @param grade_category $grade_category the category to be tested
+     * @param array $grade_items array of instance of grade_item
+     * @param array $grade_grades array of instances of grade_grade
+     * @param int $aggmethod the aggregation method to apply ie GRADE_AGGREGATE_MEAN
+     * @param string $aggmethodname the name of the aggregation method to apply. Used to display any test failure messages
+     * @param int $correct1 the correct final grade for the category with NO items hidden
+     * @param int $correct2 the correct final grade for the category with the grade at $grade_grades[0] hidden
+     * @return void
+    */
+    protected function helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, $aggmethod, $aggmethodname, $correct1, $correct2) {
+        global $DB;
+
+        $grade_category->aggregation = $aggmethod;
+        $grade_category->update();
+
+        //check grade_item isnt hidden from a previous test
+        $grade_items[0]->set_hidden(0, true);
+        $this->helper_test_grade_aggregation_result($grade_category, $correct1, 'Testing aggregation method('.$aggmethodname.') with no items hidden %s');
+
+        //hide the grade item with grade of 2
+        $grade_items[0]->set_hidden(1, true);
+        $this->helper_test_grade_aggregation_result($grade_category, $correct2, 'Testing aggregation method('.$aggmethodname.') with 1 item hidden %s');
+    }
+
+    /**
+     * Verify the value of the category grade item for $this->userid
+     * @param grade_category $grade_category the category to be tested
+     * @param int $correctgrade the expected grade
+     * @param string msg The message that should be displayed if the correct grade is not found
+     * @return void
+     */
+    protected function helper_test_grade_aggregation_result($grade_category, $correctgrade, $msg) {
+        global $DB;
+
+        $category_grade_item = $grade_category->get_grade_item();
+
+        //this creates all the grade_grades we need
+        grade_regrade_final_grades($this->courseid);
+
+        $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
+        $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
+        $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
+
+        /*
+         * TODO this doesnt work as the grade_grades created by $grade_category->generate_grades(); dont
+         * observe the category's max grade
+        //delete the grade_grades for the category itself and check they get recreated correctly
+        $DB->delete_records('grade_grades', array('itemid'=>$category_grade_item->id));
+        $grade_category->generate_grades();
+
+        $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
+        $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
+        $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
+         *
+         */
+    }
+
+    protected function sub_test_grade_category_aggregate_grades() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'aggregate_grades'));
+        // tested more fully via test_grade_category_generate_grades()
+    }
+
+    protected function sub_test_grade_category_apply_limit_rules() {
+        $items[$this->grade_items[0]->id] = new grade_item($this->grade_items[0], false);
+        $items[$this->grade_items[1]->id] = new grade_item($this->grade_items[1], false);
+        $items[$this->grade_items[2]->id] = new grade_item($this->grade_items[2], false);
+        $items[$this->grade_items[4]->id] = new grade_item($this->grade_items[4], false);
+
+        $category = new grade_category();
+        $category->droplow = 2;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 2);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
+        $this->assertEquals($grades[$this->grade_items[4]->id], 7.3754);
+
+        $category = new grade_category();
+        $category->keephigh = 1;
+        $category->droplow = 0;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 1);
+        $grade = reset($grades);
+        $this->assertEquals(9.4743, $grade);
+
+        $category = new grade_category();
+        $category->droplow     = 2;
+        $category->aggregation = GRADE_AGGREGATE_SUM;
+        $items[$this->grade_items[2]->id]->aggregationcoef = 1;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 2);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
+        $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
+
+        $category = new grade_category();
+        $category->keephigh = 1;
+        $category->droplow = 0;
+        $category->aggregation = GRADE_AGGREGATE_SUM;
+        $items[$this->grade_items[2]->id]->aggregationcoef = 1;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 2);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
+        $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
+    }
+
+    /**
+     * TODO implement
+     */
+    protected function sub_test_grade_category_is_aggregationcoef_used() {
+
+    }
+
+    protected function sub_test_grade_category_fetch_course_tree() {
+        $category = new grade_category();
+        $this->assertTrue(method_exists($category, 'fetch_course_tree'));
+        //TODO: add some tests
+    }
+
+    protected function sub_test_grade_category_get_children() {
+        $course_category = grade_category::fetch_course_category($this->courseid);
+
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_children'));
+
+        $children_array = $category->get_children(0);
+
+        $this->assertTrue(is_array($children_array));
+        $this->assertFalse(empty($children_array[2]));
+        $this->assertFalse(empty($children_array[2]['object']));
+        $this->assertFalse(empty($children_array[2]['children']));
+        $this->assertEquals($this->grade_categories[1]->id, $children_array[2]['object']->id);
+        $this->assertEquals($this->grade_categories[2]->id, $children_array[5]['object']->id);
+        $this->assertEquals($this->grade_items[0]->id, $children_array[2]['children'][3]['object']->id);
+        $this->assertEquals($this->grade_items[1]->id, $children_array[2]['children'][4]['object']->id);
+        $this->assertEquals($this->grade_items[2]->id, $children_array[5]['children'][6]['object']->id);
+    }
+
+    protected function sub_test_grade_category_load_grade_item() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'load_grade_item'));
+        $this->assertEquals(null, $category->grade_item);
+        $category->load_grade_item();
+        $this->assertEquals($this->grade_items[3]->id, $category->grade_item->id);
+    }
+
+    protected function sub_test_grade_category_get_grade_item() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_grade_item'));
+        $grade_item = $category->get_grade_item();
+        $this->assertEquals($this->grade_items[3]->id, $grade_item->id);
+    }
+
+    protected function sub_test_grade_category_load_parent_category() {
+        $category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($category, 'load_parent_category'));
+        $this->assertEquals(null, $category->parent_category);
+        $category->load_parent_category();
+        $this->assertEquals($this->grade_categories[0]->id, $category->parent_category->id);
+    }
+
+    protected function sub_test_grade_category_get_parent_category() {
+        $category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($category, 'get_parent_category'));
+        $parent_category = $category->get_parent_category();
+        $this->assertEquals($this->grade_categories[0]->id, $parent_category->id);
+    }
+
+    protected function sub_test_grade_category_get_name() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_name'));
+        $this->assertEquals($this->grade_categories[0]->fullname, $category->get_name());
+    }
+
+    protected function sub_test_grade_category_set_parent() {
+        $category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($category, 'set_parent'));
+        // TODO: implement detailed tests
+
+        $course_category = grade_category::fetch_course_category($this->courseid);
+        $this->assertTrue($category->set_parent($course_category->id));
+        $this->assertEquals($course_category->id, $category->parent);
+    }
+
+    protected function sub_test_grade_category_get_final() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_final'));
+        $category->load_grade_item();
+        $this->assertEquals($category->get_final(), $category->grade_item->get_final());
+    }
+
+    protected function sub_test_grade_category_get_sortorder() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_sortorder'));
+        $category->load_grade_item();
+        $this->assertEquals($category->get_sortorder(), $category->grade_item->get_sortorder());
+    }
+
+    protected function sub_test_grade_category_set_sortorder() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'set_sortorder'));
+        $category->load_grade_item();
+        $this->assertEquals($category->set_sortorder(10), $category->grade_item->set_sortorder(10));
+    }
+
+    protected function sub_test_grade_category_move_after_sortorder() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'move_after_sortorder'));
+        $category->load_grade_item();
+        $this->assertEquals($category->move_after_sortorder(10), $category->grade_item->move_after_sortorder(10));
+    }
+
+    protected function sub_test_grade_category_is_course_category() {
+        $category = grade_category::fetch_course_category($this->courseid);
+        $this->assertTrue(method_exists($category, 'is_course_category'));
+        $this->assertTrue($category->is_course_category());
+    }
+
+    protected function sub_test_grade_category_fetch_course_category() {
+        $category = new grade_category();
+        $this->assertTrue(method_exists($category, 'fetch_course_category'));
+        $category = grade_category::fetch_course_category($this->courseid);
+        $this->assertTrue(empty($category->parent));
+    }
+    /**
+     * TODO implement
+     */
+    protected function sub_test_grade_category_is_editable() {
+
+    }
+
+    protected function sub_test_grade_category_is_locked() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'is_locked'));
+        $category->load_grade_item();
+        $this->assertEquals($category->is_locked(), $category->grade_item->is_locked());
+    }
+
+    protected function sub_test_grade_category_set_locked() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'set_locked'));
+
+        //will return false as cannot lock a grade that needs updating
+        $this->assertFalse($category->set_locked(1));
+        grade_regrade_final_grades($this->courseid);
+
+        //get the category from the db again
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue($category->set_locked(1));
+    }
+
+    protected function sub_test_grade_category_is_hidden() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'is_hidden'));
+        $category->load_grade_item();
+        $this->assertEquals($category->is_hidden(), $category->grade_item->is_hidden());
+    }
+
+    protected function sub_test_grade_category_set_hidden() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'set_hidden'));
+        $category->set_hidden(1);
+        $category->load_grade_item();
+        $this->assertEquals(true, $category->grade_item->is_hidden());
+    }
+
+    //beware: adding a duplicate course category messes up the data in a way that's hard to recover from
+    protected function sub_test_grade_category_insert_course_category() {
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'insert_course_category'));
+
+        $id = $grade_category->insert_course_category($this->courseid);
+        $this->assertNotNull($id);
+        $this->assertEquals('?', $grade_category->fullname);
+        $this->assertEquals(GRADE_AGGREGATE_WEIGHTED_MEAN2, $grade_category->aggregation);
+        $this->assertEquals("/$id/", $grade_category->path);
+        $this->assertEquals(1, $grade_category->depth);
+        $this->assertNull($grade_category->parent);
+    }
+
+    protected function generate_random_raw_grade($item, $userid) {
+        $grade = new grade_grade();
+        $grade->itemid = $item->id;
+        $grade->userid = $userid;
+        $grade->grademin = 0;
+        $grade->grademax = 1;
+        $valuetype = "grade$item->gradetype";
+        $grade->rawgrade = rand(0, 1000) / 1000;
+        $grade->insert();
+        return $grade->rawgrade;
+    }
+}
diff --git a/lib/grade/tests/grade_grade_test.php b/lib/grade/tests/grade_grade_test.php
new file mode 100644 (file)
index 0000000..56dddc8
--- /dev/null
@@ -0,0 +1,196 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_grade_testcase extends grade_base_testcase {
+
+    public function test_grade_grade() {
+        $this->sub_test_grade_grade_construct();
+        $this->sub_test_grade_grade_insert();
+        $this->sub_test_grade_grade_update();
+        $this->sub_test_grade_grade_fetch();
+        $this->sub_test_grade_grade_fetch_all();
+        $this->sub_test_grade_grade_load_grade_item();
+        $this->sub_test_grade_grade_standardise_score();
+        $this->sub_test_grade_grade_is_locked();
+        $this->sub_test_grade_grade_set_hidden();
+        $this->sub_test_grade_grade_is_hidden();
+    }
+
+    protected function sub_test_grade_grade_construct() {
+        $params = new stdClass();
+
+        $params->itemid = $this->grade_items[0]->id;
+        $params->userid = 1;
+        $params->rawgrade = 88;
+        $params->rawgrademax = 110;
+        $params->rawgrademin = 18;
+
+        $grade_grade = new grade_grade($params, false);
+        $this->assertEquals($params->itemid, $grade_grade->itemid);
+        $this->assertEquals($params->rawgrade, $grade_grade->rawgrade);
+    }
+
+    protected function sub_test_grade_grade_insert() {
+        $grade_grade = new grade_grade();
+        $this->assertTrue(method_exists($grade_grade, 'insert'));
+
+        $grade_grade->itemid = $this->grade_items[0]->id;
+        $grade_grade->userid = 10;
+        $grade_grade->rawgrade = 88;
+        $grade_grade->rawgrademax = 110;
+        $grade_grade->rawgrademin = 18;
+
+        // Check the grade_item's needsupdate variable first
+        $grade_grade->load_grade_item();
+        $this->assertEmpty($grade_grade->grade_item->needsupdate);
+
+        $grade_grade->insert();
+
+        $last_grade_grade = end($this->grade_grades);
+
+        $this->assertEquals($grade_grade->id, $last_grade_grade->id + 1);
+
+        // timecreated will only be set if the grade was submitted by an activity module
+        $this->assertTrue(empty($grade_grade->timecreated));
+        // timemodified will only be set if the grade was submitted by an activity module
+        $this->assertTrue(empty($grade_grade->timemodified));
+
+        //keep our collection the same as is in the database
+        $this->grade_grades[] = $grade_grade;
+    }
+
+    protected function sub_test_grade_grade_update() {
+        $grade_grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade_grade, 'update'));
+    }
+
+    protected function sub_test_grade_grade_fetch() {
+        $grade_grade = new grade_grade();
+        $this->assertTrue(method_exists($grade_grade, 'fetch'));
+
+        $grades = grade_grade::fetch(array('id'=>$this->grade_grades[0]->id));
+        $this->assertEquals($this->grade_grades[0]->id, $grades->id);
+        $this->assertEquals($this->grade_grades[0]->rawgrade, $grades->rawgrade);
+    }
+
+    protected function sub_test_grade_grade_fetch_all() {
+        $grade_grade = new grade_grade();
+        $this->assertTrue(method_exists($grade_grade, 'fetch_all'));
+
+        $grades = grade_grade::fetch_all(array());
+        $this->assertEquals(count($this->grade_grades), count($grades));
+    }
+
+    protected function sub_test_grade_grade_load_grade_item() {
+        $grade_grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade_grade, 'load_grade_item'));
+        $this->assertNull($grade_grade->grade_item);
+        $this->assertNotEmpty($grade_grade->itemid);
+        $this->assertNotNull($grade_grade->load_grade_item());
+        $this->assertNotNull($grade_grade->grade_item);
+        $this->assertEquals($this->grade_items[0]->id, $grade_grade->grade_item->id);
+    }
+
+
+    protected function sub_test_grade_grade_standardise_score() {
+        $this->assertEquals(4, round(grade_grade::standardise_score(6, 0, 7, 0, 5)));
+        $this->assertEquals(40, grade_grade::standardise_score(50, 30, 80, 0, 100));
+    }
+
+
+    /*
+     * Disabling this test: the set_locked() arguments have been modified, rendering these tests useless until they are re-written
+
+    protected function test_grade_grade_set_locked() {
+        $grade_item = new grade_item($this->grade_items[0]);
+        $grade = new grade_grade($grade_item->get_final(1));
+        $this->assertTrue(method_exists($grade, 'set_locked'));
+
+        $this->assertTrue(empty($grade_item->locked));
+        $this->assertTrue(empty($grade->locked));
+
+        $this->assertTrue($grade->set_locked(true));
+        $this->assertFalse(empty($grade->locked));
+        $this->assertTrue($grade->set_locked(false));
+        $this->assertTrue(empty($grade->locked));
+
+        $this->assertTrue($grade_item->set_locked(true, true));
+        $grade = new grade_grade($grade_item->get_final(1));
+
+        $this->assertFalse(empty($grade->locked));
+        $this->assertFalse($grade->set_locked(true, false));
+
+        $this->assertTrue($grade_item->set_locked(true, false));
+        $grade = new grade_grade($grade_item->get_final(1));
+
+        $this->assertTrue($grade->set_locked(true, false));
+    }
+    */
+
+    protected function sub_test_grade_grade_is_locked() {
+        $grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade, 'is_locked'));
+
+        $this->assertFalse($grade->is_locked());
+        $grade->locked = time();
+        $this->assertTrue($grade->is_locked());
+    }
+
+    protected function sub_test_grade_grade_set_hidden() {
+        $grade = new grade_grade($this->grade_grades[0], false);
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade, 'set_hidden'));
+
+        $this->assertEquals(0, $grade_item->hidden);
+        $this->assertEquals(0, $grade->hidden);
+
+        $grade->set_hidden(0);
+        $this->assertEquals(0, $grade->hidden);
+
+        $grade->set_hidden(1);
+        $this->assertEquals(1, $grade->hidden);
+
+        $grade->set_hidden(0);
+        $this->assertEquals(0, $grade->hidden);
+    }
+
+    protected function sub_test_grade_grade_is_hidden() {
+        $grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade, 'is_hidden'));
+
+        $this->assertFalse($grade->is_hidden());
+        $grade->hidden = 1;
+        $this->assertTrue($grade->is_hidden());
+
+        $grade->hidden = time()-666;
+        $this->assertFalse($grade->is_hidden());
+
+        $grade->hidden = time()+666;
+        $this->assertTrue($grade->is_hidden());
+    }
+}
diff --git a/lib/grade/tests/grade_item_test.php b/lib/grade/tests/grade_item_test.php
new file mode 100644 (file)
index 0000000..dab82c2
--- /dev/null
@@ -0,0 +1,547 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+class grade_item_testcase extends grade_base_testcase {
+    public function test_grade_item() {
+        $this->sub_test_grade_item_construct();
+        $this->sub_test_grade_item_insert();
+        $this->sub_test_grade_item_delete();
+        $this->sub_test_grade_item_update();
+        $this->sub_test_grade_item_load_scale();
+        $this->sub_test_grade_item_load_outcome();
+        $this->sub_test_grade_item_qualifies_for_regrading();
+        $this->sub_test_grade_item_force_regrading();
+        $this->sub_test_grade_item_fetch();
+        $this->sub_test_grade_item_fetch_all();
+        $this->sub_test_grade_item_get_all_finals();
+        $this->sub_test_grade_item_get_final();
+        $this->sub_test_grade_item_get_sortorder();
+        $this->sub_test_grade_item_set_sortorder();
+        $this->sub_test_grade_item_move_after_sortorder();
+        $this->sub_test_grade_item_get_name();
+        $this->sub_test_grade_item_set_parent();
+        $this->sub_test_grade_item_get_parent_category();
+        $this->sub_test_grade_item_load_parent_category();
+        $this->sub_test_grade_item_get_item_category();
+        $this->sub_test_grade_item_load_item_category();
+        $this->sub_test_grade_item_regrade_final_grades();
+        $this->sub_test_grade_item_adjust_raw_grade();
+        $this->sub_test_grade_item_set_locked();
+        $this->sub_test_grade_item_is_locked();
+        $this->sub_test_grade_item_set_hidden();
+        $this->sub_test_grade_item_is_hidden();
+        $this->sub_test_grade_item_is_category_item();
+        $this->sub_test_grade_item_is_course_item();
+        $this->sub_test_grade_item_fetch_course_item();
+        $this->sub_test_grade_item_depends_on();
+        $this->sub_test_grade_item_is_calculated();
+        $this->sub_test_grade_item_set_calculation();
+        $this->sub_test_grade_item_get_calculation();
+        $this->sub_test_grade_item_compute();
+    }
+
+    protected function sub_test_grade_item_construct() {
+        $params = new stdClass();
+
+        $params->courseid = $this->courseid;
+        $params->categoryid = $this->grade_categories[1]->id;
+        $params->itemname = 'unittestgradeitem4';
+        $params->itemtype = 'mod';
+        $params->itemmodule = 'database';
+        $params->iteminfo = 'Grade item used for unit testing';
+
+        $grade_item = new grade_item($params, false);
+
+        $this->assertEquals($params->courseid, $grade_item->courseid);
+        $this->assertEquals($params->categoryid, $grade_item->categoryid);
+        $this->assertEquals($params->itemmodule, $grade_item->itemmodule);
+    }
+
+    protected function sub_test_grade_item_insert() {
+        $grade_item = new grade_item();
+        $this->assertTrue(method_exists($grade_item, 'insert'));
+
+        $grade_item->courseid = $this->courseid;
+        $grade_item->categoryid = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitem4';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = 'quiz';
+        $grade_item->iteminfo = 'Grade item used for unit testing';
+
+        $grade_item->insert();
+
+        $last_grade_item = end($this->grade_items);
+
+        $this->assertEquals($grade_item->id, $last_grade_item->id + 1);
+        $this->assertEquals(11, $grade_item->sortorder);
+
+        //keep our reference collection the same as what is in the database
+        $this->grade_items[] = $grade_item;
+    }
+
+    protected function sub_test_grade_item_delete() {
+        global $DB;
+        $grade_item = new grade_item($this->grade_items[7],false);//use a grade item not touched by previous (or future) tests
+        $this->assertTrue(method_exists($grade_item, 'delete'));
+
+        $this->assertTrue($grade_item->delete());
+
+        $this->assertFalse($DB->get_record('grade_items', array('id' => $grade_item->id)));
+
+        //keep our reference collection the same as the database
+        unset($this->grade_items[7]);
+    }
+
+    protected function sub_test_grade_item_update() {
+        global $DB;
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'update'));
+
+        $grade_item->iteminfo = 'Updated info for this unittest grade_item';
+
+        $this->assertTrue($grade_item->update());
+
+        $grade_item->grademin = 14;
+        $this->assertTrue($grade_item->qualifies_for_regrading());
+        $this->assertTrue($grade_item->update());
+
+        $iteminfo = $DB->get_field('grade_items', 'iteminfo', array('id' => $this->grade_items[0]->id));
+        $this->assertEquals($grade_item->iteminfo, $iteminfo);
+    }
+
+    protected function sub_test_grade_item_load_scale() {
+        $grade_item = new grade_item($this->grade_items[2], false);
+        $this->assertTrue(method_exists($grade_item, 'load_scale'));
+        $scale = $grade_item->load_scale();
+        $this->assertFalse(empty($grade_item->scale));
+        $this->assertEquals($scale->id, $this->grade_items[2]->scaleid);
+    }
+
+    protected function sub_test_grade_item_load_outcome() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'load_outcome'));
+        //TODO: add tests
+    }
+
+    protected function sub_test_grade_item_qualifies_for_regrading() {
+        $grade_item = new grade_item($this->grade_items[3], false);//use a grade item not touched by previous tests
+        $this->assertTrue(method_exists($grade_item, 'qualifies_for_regrading'));
+
+        $this->assertFalse($grade_item->qualifies_for_regrading());
+
+        $grade_item->iteminfo = 'Updated info for this unittest grade_item';
+
+        $this->assertFalse($grade_item->qualifies_for_regrading());
+
+        $grade_item->grademin = 14;
+
+        $this->assertTrue($grade_item->qualifies_for_regrading());
+    }
+
+    protected function sub_test_grade_item_force_regrading() {
+        $grade_item = new grade_item($this->grade_items[3], false);//use a grade item not touched by previous tests
+        $this->assertTrue(method_exists($grade_item, 'force_regrading'));
+
+        $this->assertEquals(0, $grade_item->needsupdate);
+
+        $grade_item->force_regrading();
+        $this->assertEquals(1, $grade_item->needsupdate);
+        $grade_item->update_from_db();
+        $this->assertEquals(1, $grade_item->needsupdate);
+    }
+
+    protected function sub_test_grade_item_fetch() {
+        $grade_item = new grade_item();
+        $this->assertTrue(method_exists($grade_item, 'fetch'));
+
+        //not using $this->grade_items[0] as it's iteminfo was modified by sub_test_grade_item_qualifies_for_regrading()
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[1]->id));
+        $this->assertEquals($this->grade_items[1]->id, $grade_item->id);
+        $this->assertEquals($this->grade_items[1]->iteminfo, $grade_item->iteminfo);
+
+        $grade_item = grade_item::fetch(array('itemtype'=>$this->grade_items[1]->itemtype, 'itemmodule'=>$this->grade_items[1]->itemmodule));
+        $this->assertEquals($this->grade_items[1]->id, $grade_item->id);
+        $this->assertEquals($this->grade_items[1]->iteminfo, $grade_item->iteminfo);
+    }
+
+    protected function sub_test_grade_item_fetch_all() {
+        $grade_item = new grade_item();
+        $this->assertTrue(method_exists($grade_item, 'fetch_all'));
+
+        $grade_items = grade_item::fetch_all(array('courseid'=>$this->courseid));
+        $this->assertEquals(count($this->grade_items), count($grade_items)-1);//-1 to account for the course grade item
+    }
+
+    // Retrieve all final scores for a given grade_item.
+    protected function sub_test_grade_item_get_all_finals() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_final'));
+
+        $final_grades = $grade_item->get_final();
+        $this->assertEquals(3, count($final_grades));
+    }
+
+
+    // Retrieve all final scores for a specific userid.
+    protected function sub_test_grade_item_get_final() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_final'));
+        $final_grade = $grade_item->get_final($this->user[1]->id);
+        $this->assertEquals($this->grade_grades[0]->finalgrade, $final_grade->finalgrade);
+    }
+
+    protected function sub_test_grade_item_get_sortorder() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_sortorder'));
+        $sortorder = $grade_item->get_sortorder();
+        $this->assertEquals($this->grade_items[0]->sortorder, $sortorder);
+    }
+
+    protected function sub_test_grade_item_set_sortorder() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'set_sortorder'));
+        $grade_item->set_sortorder(999);
+        $this->assertEquals($grade_item->sortorder, 999);
+    }
+
+    protected function sub_test_grade_item_move_after_sortorder() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'move_after_sortorder'));
+        $grade_item->move_after_sortorder(5);
+        $this->assertEquals($grade_item->sortorder, 6);
+
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[0]->id));
+        $this->assertEquals($grade_item->sortorder, 6);
+
+        $after = grade_item::fetch(array('id'=>$this->grade_items[6]->id));
+        $this->assertEquals($after->sortorder, 8);
+    }
+
+    protected function sub_test_grade_item_get_name() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_name'));
+
+        $name = $grade_item->get_name();
+        $this->assertEquals($this->grade_items[0]->itemname, $name);
+    }
+
+    protected function sub_test_grade_item_set_parent() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'set_parent'));
+
+        $old = $grade_item->get_parent_category();
+        $new = new grade_category($this->grade_categories[3], false);
+        $new_item = $new->get_grade_item();
+
+        $this->assertTrue($grade_item->set_parent($new->id));
+
+        $new_item->update_from_db();
+        $grade_item->update_from_db();
+
+        $this->assertEquals($grade_item->categoryid, $new->id);
+    }
+
+    protected function sub_test_grade_item_get_parent_category() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_parent_category'));
+
+        $category = $grade_item->get_parent_category();
+        $this->assertEquals($this->grade_categories[1]->fullname, $category->fullname);
+    }
+
+    protected function sub_test_grade_item_load_parent_category() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'load_parent_category'));
+
+        $category = $grade_item->load_parent_category();
+        $this->assertEquals($this->grade_categories[1]->fullname, $category->fullname);
+        $this->assertEquals($this->grade_categories[1]->fullname, $grade_item->parent_category->fullname);
+    }
+
+    protected function sub_test_grade_item_get_item_category() {
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $this->assertTrue(method_exists($grade_item, 'get_item_category'));
+
+        $category = $grade_item->get_item_category();
+        $this->assertEquals($this->grade_categories[0]->fullname, $category->fullname);
+    }
+
+    protected function sub_test_grade_item_load_item_category() {
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $this->assertTrue(method_exists($grade_item, 'load_item_category'));
+
+        $category = $grade_item->load_item_category();
+        $this->assertEquals($this->grade_categories[0]->fullname, $category->fullname);
+        $this->assertEquals($this->grade_categories[0]->fullname, $grade_item->item_category->fullname);
+    }
+
+    // Test update of all final grades
+    protected function sub_test_grade_item_regrade_final_grades() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'regrade_final_grades'));
+        $this->assertEquals(true, $grade_item->regrade_final_grades());
+        //TODO: add more tests
+    }
+
+    // Test the adjust_raw_grade method
+    protected function sub_test_grade_item_adjust_raw_grade() {
+        $grade_item = new grade_item($this->grade_items[2], false); // anything but assignment module!
+        $this->assertTrue(method_exists($grade_item, 'adjust_raw_grade'));
+
+        $grade_raw = new stdClass();
+        $grade_raw->rawgrade = 40;
+        $grade_raw->grademax = 100;
+        $grade_raw->grademin = 0;
+
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->multfactor = 1;
+        $grade_item->plusfactor = 0;
+        $grade_item->grademax = 50;
+        $grade_item->grademin = 0;
+
+        $original_grade_raw  = clone($grade_raw);
+        $original_grade_item = clone($grade_item);
+
+        $this->assertEquals(20, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try a larger maximum grade
+        $grade_item->grademax = 150;
+        $grade_item->grademin = 0;
+        $this->assertEquals(60, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try larger minimum grade
+        $grade_item->grademin = 50;
+
+        $this->assertEquals(90, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Rescaling from a small scale (0-50) to a larger scale (0-100)
+        $grade_raw->grademax = 50;
+        $grade_raw->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->grademin = 0;
+
+        $this->assertEquals(80, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Rescaling from a small scale (0-50) to a larger scale with offset (40-100)
+        $grade_item->grademax = 100;
+        $grade_item->grademin = 40;
+
+        $this->assertEquals(88, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try multfactor and plusfactor
+        $grade_raw = clone($original_grade_raw);
+        $grade_item = clone($original_grade_item);
+        $grade_item->multfactor = 1.23;
+        $grade_item->plusfactor = 3;
+
+        $this->assertEquals(27.6, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try multfactor below 0 and a negative plusfactor
+        $grade_raw = clone($original_grade_raw);
+        $grade_item = clone($original_grade_item);
+        $grade_item->multfactor = 0.23;
+        $grade_item->plusfactor = -3;
+
+        $this->assertEquals(round(1.6), round($grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax)));
+    }
+
+    // Test locking of grade items
+    protected function sub_test_grade_item_set_locked() {
+        //getting a grade_item from the DB as set_locked() will fail if the grade items needs to be updated
+        //also needs to have at least one grade_grade or $grade_item->get_final(1) returns null
+        //$grade_item = new grade_item($this->grade_items[8]);
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[8]->id));
+
+        $this->assertTrue(method_exists($grade_item, 'set_locked'));
+
+        $grade_grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+        $this->assertTrue(empty($grade_item->locked));//not locked
+        $this->assertTrue(empty($grade_grade->locked));//not locked
+
+        $this->assertTrue($grade_item->set_locked(true, true, false));
+        $grade_grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+
+        $this->assertFalse(empty($grade_item->locked));//locked
+        $this->assertFalse(empty($grade_grade->locked)); // individual grades should be locked too
+
+        $this->assertTrue($grade_item->set_locked(false, true, false));
+        $grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+
+        $this->assertTrue(empty($grade_item->locked));
+        $this->assertTrue(empty($grade->locked)); // individual grades should be unlocked too
+    }
+
+    protected function sub_test_grade_item_is_locked() {
+        $grade_item = new grade_item($this->grade_items[10], false);
+        $this->assertTrue(method_exists($grade_item, 'is_locked'));
+
+        $this->assertFalse($grade_item->is_locked());
+        $this->assertFalse($grade_item->is_locked($this->user[1]->id));
+        $this->assertTrue($grade_item->set_locked(true, true, false));
+        $this->assertTrue($grade_item->is_locked());
+        $this->assertTrue($grade_item->is_locked($this->user[1]->id));
+    }
+
+    // Test hiding of grade items
+    protected function sub_test_grade_item_set_hidden() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'set_hidden'));
+
+        $grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+        $this->assertEquals(0, $grade_item->hidden);
+        $this->assertEquals(0, $grade->hidden);
+
+        $grade_item->set_hidden(666, true);
+        $grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+
+        $this->assertEquals(666, $grade_item->hidden);
+        $this->assertEquals(666, $grade->hidden);
+    }
+
+    protected function sub_test_grade_item_is_hidden() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'is_hidden'));
+
+        $this->assertFalse($grade_item->is_hidden());
+        $this->assertFalse($grade_item->is_hidden(1));
+
+        $grade_item->set_hidden(1);
+        $this->assertTrue($grade_item->is_hidden());
+        $this->assertTrue($grade_item->is_hidden(1));
+
+        $grade_item->set_hidden(666);
+        $this->assertFalse($grade_item->is_hidden());
+        $this->assertFalse($grade_item->is_hidden(1));
+
+        $grade_item->set_hidden(time()+666);
+        $this->assertTrue($grade_item->is_hidden());
+        $this->assertTrue($grade_item->is_hidden(1));
+    }
+
+    protected function sub_test_grade_item_is_category_item() {
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $this->assertTrue(method_exists($grade_item, 'is_category_item'));
+        $this->assertTrue($grade_item->is_category_item());
+    }
+
+    protected function sub_test_grade_item_is_course_item() {
+        $grade_item = grade_item::fetch_course_item($this->courseid);
+        $this->assertTrue(method_exists($grade_item, 'is_course_item'));
+        $this->assertTrue($grade_item->is_course_item());
+    }
+
+    protected function sub_test_grade_item_fetch_course_item() {
+        $grade_item = grade_item::fetch_course_item($this->courseid);
+        $this->assertTrue(method_exists($grade_item, 'fetch_course_item'));
+        $this->assertEquals($grade_item->itemtype, 'course');
+    }
+
+    protected function sub_test_grade_item_depends_on() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+
+        // calculated grade dependency
+        $deps = $grade_item->depends_on();
+        sort($deps, SORT_NUMERIC); // for comparison
+        $this->assertEquals(array($this->grade_items[0]->id), $deps);
+
+        // simulate depends on returns none when locked
+        $grade_item->locked = time();
+        $grade_item->update();
+        $deps = $grade_item->depends_on();
+        sort($deps, SORT_NUMERIC); // for comparison
+        $this->assertEquals(array(), $deps);
+
+        // category dependency
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $deps = $grade_item->depends_on();
+        sort($deps, SORT_NUMERIC); // for comparison
+        $res = array($this->grade_items[4]->id, $this->grade_items[5]->id);
+        $this->assertEquals($res, $deps);
+    }
+
+    protected function sub_test_grade_item_is_calculated() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+        $this->assertTrue(method_exists($grade_item, 'is_calculated'));
+        $this->assertTrue($grade_item->is_calculated());
+
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertFalse($grade_item->is_calculated());
+    }
+
+    protected function sub_test_grade_item_set_calculation() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+        $this->assertTrue(method_exists($grade_item, 'set_calculation'));
+        $grade_itemsource = new grade_item($this->grade_items[0], false);
+
+        $grade_item->set_calculation('=[['.$grade_itemsource->idnumber.']]');
+
+        $this->assertTrue(!empty($grade_item->needsupdate));
+        $this->assertEquals('=##gi'.$grade_itemsource->id.'##', $grade_item->calculation);
+    }
+
+    protected function sub_test_grade_item_get_calculation() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+        $this->assertTrue(method_exists($grade_item, 'get_calculation'));
+        $grade_itemsource = new grade_item($this->grade_items[0], false);
+
+        $denormalizedformula = str_replace('##gi'.$grade_itemsource->id.'##', '[['.$grade_itemsource->idnumber.']]', $this->grade_items[1]->calculation);
+
+        $formula = $grade_item->get_calculation();
+        $this->assertTrue(!empty($grade_item->needsupdate));
+        $this->assertEquals($denormalizedformula, $formula);
+    }
+
+    public function sub_test_grade_item_compute() {
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[1]->id));
+        $this->assertTrue(method_exists($grade_item, 'compute'));
+
+        //check the grade_grades in the array match those in the DB then delete $this->grade_items[1]'s grade_grades
+        $this->grade_grades[3] = grade_grade::fetch(array('id'=>$this->grade_grades[3]->id));
+        $grade_grade = grade_grade::fetch(array('id'=>$this->grade_grades[3]->id));
+        $grade_grade->delete();
+
+        $this->grade_grades[4] = grade_grade::fetch(array('id'=>$this->grade_grades[4]->id));
+        $grade_grade = grade_grade::fetch(array('id'=>$this->grade_grades[4]->id));
+        $grade_grade->delete();
+
+        $this->grade_grades[5] = grade_grade::fetch(array('id'=>$this->grade_grades[5]->id));
+        $grade_grade = grade_grade::fetch(array('id'=>$this->grade_grades[5]->id));
+        $grade_grade->delete();
+
+        //recalculate the grades (its a calculation so pulls values from other grade_items) and reinsert them
+        $grade_item->compute();
+
+        $grade_grade = grade_grade::fetch(array('userid'=>$this->grade_grades[3]->userid, 'itemid'=>$this->grade_grades[3]->itemid));
+        $this->assertEquals($this->grade_grades[3]->finalgrade, $grade_grade->finalgrade);
+
+        $grade_grade = grade_grade::fetch(array('userid'=>$this->grade_grades[4]->userid, 'itemid'=>$this->grade_grades[4]->itemid));
+        $this->assertEquals($this->grade_grades[4]->finalgrade, $grade_grade->finalgrade);
+
+        $grade_grade = grade_grade::fetch(array('userid'=>$this->grade_grades[5]->userid, 'itemid'=>$this->grade_grades[5]->itemid));
+        $this->assertEquals($this->grade_grades[5]->finalgrade, $grade_grade->finalgrade);
+    }
+}
diff --git a/lib/grade/tests/grade_outcome_test.php b/lib/grade/tests/grade_outcome_test.php
new file mode 100644 (file)
index 0000000..78ea851
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_outcome_testcase extends grade_base_testcase {
+
+    public function test_grade_outcome() {
+        $this->sub_test_grade_outcome_construct();
+        $this->sub_test_grade_outcome_insert();
+        $this->sub_test_grade_outcome_update();
+        $this->sub_test_grade_outcome_delete();
+        //$this->sub_test_grade_outcome_fetch();
+        $this->sub_test_grade_outcome_fetch_all();
+    }
+
+    protected function sub_test_grade_outcome_construct() {
+        $params = new stdClass();
+
+        $params->courseid = $this->courseid;
+        $params->shortname = 'Team work';
+
+        $grade_outcome = new grade_outcome($params, false);
+        $this->assertEquals($params->courseid, $grade_outcome->courseid);
+        $this->assertEquals($params->shortname, $grade_outcome->shortname);
+    }
+
+    protected function sub_test_grade_outcome_insert() {
+        $grade_outcome = new grade_outcome();
+        $this->assertTrue(method_exists($grade_outcome, 'insert'));
+
+        $grade_outcome->courseid = $this->courseid;
+        $grade_outcome->shortname = 'tw';
+        $grade_outcome->fullname = 'Team work';
+
+        $grade_outcome->insert();
+
+        $last_grade_outcome = end($this->grade_outcomes);
+
+        $this->assertEquals($grade_outcome->id, $last_grade_outcome->id + 1);
+        $this->assertFalse(empty($grade_outcome->timecreated));
+        $this->assertFalse(empty($grade_outcome->timemodified));
+    }
+
+    protected function sub_test_grade_outcome_update() {
+        global $DB;
+        $grade_outcome = new grade_outcome($this->grade_outcomes[0], false);
+        $this->assertTrue(method_exists($grade_outcome, 'update'));
+        $grade_outcome->shortname = 'Team work';
+        $this->assertTrue($grade_outcome->update());
+        $shortname = $DB->get_field('grade_outcomes', 'shortname', array('id' => $this->grade_outcomes[0]->id));
+        $this->assertEquals($grade_outcome->shortname, $shortname);
+    }
+
+    protected function sub_test_grade_outcome_delete() {
+        global $DB;
+        $grade_outcome = new grade_outcome($this->grade_outcomes[0], false);
+        $this->assertTrue(method_exists($grade_outcome, 'delete'));
+
+        $this->assertTrue($grade_outcome->delete());
+        $this->assertFalse($DB->get_record('grade_outcomes', array('id' => $grade_outcome->id)));
+    }
+
+    protected function sub_test_grade_outcome_fetch() {
+        $grade_outcome = new grade_outcome();
+        $this->assertTrue(method_exists($grade_outcome, 'fetch'));
+
+        $grade_outcome = grade_outcome::fetch(array('id'=>$this->grade_outcomes[0]->id));
+        $grade_outcome->load_scale();
+        $this->assertEquals($this->grade_outcomes[0]->id, $grade_outcome->id);
+        $this->assertEquals($this->grade_outcomes[0]->shortname, $grade_outcome->shortname);
+
+        $this->assertEquals($this->scale[2]->id, $grade_outcome->scale->id);
+    }
+
+    protected function sub_test_grade_outcome_fetch_all() {
+        $grade_outcome = new grade_outcome();
+        $this->assertTrue(method_exists($grade_outcome, 'fetch_all'));
+
+        $grade_outcomes = grade_outcome::fetch_all(array());
+        $this->assertEquals(count($this->grade_outcomes), count($grade_outcomes));
+    }
+}
diff --git a/lib/grade/tests/grade_scale_test.php b/lib/grade/tests/grade_scale_test.php
new file mode 100644 (file)
index 0000000..f132222
--- /dev/null
@@ -0,0 +1,130 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_scale_testcase extends grade_base_testcase {
+
+    public function test_grade_scale() {
+        $this->sub_test_scale_construct();
+        $this->sub_test_grade_scale_insert();
+        $this->sub_test_grade_scale_update();
+        $this->sub_test_grade_scale_delete();
+        $this->sub_test_grade_scale_fetch();
+        $this->sub_test_scale_load_items();
+        $this->sub_test_scale_compact_items();
+    }
+
+    protected function sub_test_scale_construct() {
+        $params = new stdClass();
+        $params->name        = 'unittestscale3';
+        $params->courseid    = $this->course->id;
+        $params->userid      = $this->userid;
+        $params->scale       = 'Distinction, Very Good, Good, Pass, Fail';
+        $params->description = 'This scale is used to mark standard assignments.';
+        $params->timemodified = time();
+
+        $scale = new grade_scale($params, false);
+
+        $this->assertEquals($params->name, $scale->name);
+        $this->assertEquals($params->scale, $scale->scale);
+        $this->assertEquals($params->description, $scale->description);
+
+    }
+
+    protected function sub_test_grade_scale_insert() {
+        $grade_scale = new grade_scale();
+        $this->assertTrue(method_exists($grade_scale, 'insert'));
+
+        $grade_scale->name        = 'unittestscale3';
+        $grade_scale->courseid    = $this->courseid;
+        $grade_scale->userid      = $this->userid;
+        $grade_scale->scale       = 'Distinction, Very Good, Good, Pass, Fail';
+        $grade_scale->description = 'This scale is used to mark standard assignments.';
+
+        $grade_scale->insert();
+
+        $last_grade_scale = end($this->scale);
+
+        $this->assertEquals($grade_scale->id, $last_grade_scale->id + 1);
+        $this->assertTrue(!empty($grade_scale->timecreated));
+        $this->assertTrue(!empty($grade_scale->timemodified));
+    }
+
+    protected function sub_test_grade_scale_update() {
+        global $DB;
+        $grade_scale = new grade_scale($this->scale[1], false);
+        $this->assertTrue(method_exists($grade_scale, 'update'));
+
+        $grade_scale->name = 'Updated info for this unittest grade_scale';
+        $this->assertTrue($grade_scale->update());
+        $name = $DB->get_field('scale', 'name', array('id' => $this->scale[1]->id));
+        $this->assertEquals($grade_scale->name, $name);
+    }
+
+    protected function sub_test_grade_scale_delete() {
+        global $DB;
+        $grade_scale = new grade_scale($this->scale[4], false);//choose one we're not using elsewhere
+        $this->assertTrue(method_exists($grade_scale, 'delete'));
+
+        $this->assertTrue($grade_scale->delete());
+        $this->assertFalse($DB->get_record('scale', array('id' => $grade_scale->id)));
+
+        //keep the reference collection the same as what is in the database
+        unset($this->scale[4]);
+    }
+
+    protected function sub_test_grade_scale_fetch() {
+        $grade_scale = new grade_scale();
+        $this->assertTrue(method_exists($grade_scale, 'fetch'));
+
+        $grade_scale = grade_scale::fetch(array('id'=>$this->scale[0]->id));
+        $this->assertEquals($this->scale[0]->id, $grade_scale->id);
+        $this->assertEquals($this->scale[0]->name, $grade_scale->name);
+    }
+
+    protected function sub_test_scale_load_items() {
+        $scale = new grade_scale($this->scale[0], false);
+        $this->assertTrue(method_exists($scale, 'load_items'));
+
+        $scale->load_items();
+        $this->assertEquals(7, count($scale->scale_items));
+        $this->assertEquals('Fairly neutral', $scale->scale_items[2]);
+
+    }
+
+    protected function sub_test_scale_compact_items() {
+        $scale = new grade_scale($this->scale[0], false);
+        $this->assertTrue(method_exists($scale, 'compact_items'));
+
+        $scale->load_items();
+        $scale->scale = null;
+        $scale->compact_items();
+
+        // The original string and the new string may have differences in whitespace around the delimiter, and that's OK
+        $this->assertEquals(preg_replace('/\s*,\s*/', ',', $this->scale[0]->scale), $scale->scale);
+    }
+}
index 60def99..43c3e57 100644 (file)
@@ -8734,47 +8734,6 @@ function moodle_major_version($fromdisk = false) {
     }
 }
 
-/**
- * Sets maximum expected time needed for upgrade task.
- * Please always make sure that upgrade will not run longer!
- *
- * The script may be automatically aborted if upgrade times out.
- *
- * @category upgrade
- * @param int $max_execution_time in seconds (can not be less than 60 s)
- * @todo MDL-32293 - Move this function to lib/upgradelib.php
- */
-function upgrade_set_timeout($max_execution_time=300) {
-    global $CFG;
-
-    if (!isset($CFG->upgraderunning) or $CFG->upgraderunning < time()) {
-        $upgraderunning = get_config(null, 'upgraderunning');
-    } else {
-        $upgraderunning = $CFG->upgraderunning;
-    }
-
-    if (!$upgraderunning) {
-        // upgrade not running or aborted
-        print_error('upgradetimedout', 'admin', "$CFG->wwwroot/$CFG->admin/");
-        die;
-    }
-
-    if ($max_execution_time < 60) {
-        // protection against 0 here
-        $max_execution_time = 60;
-    }
-
-    $expected_end = time() + $max_execution_time;
-
-    if ($expected_end < $upgraderunning + 10 and $expected_end > $upgraderunning - 10) {
-        // no need to store new end, it is nearly the same ;-)
-        return;
-    }
-
-    set_time_limit($max_execution_time);
-    set_config('upgraderunning', $expected_end); // keep upgrade locked until this time
-}
-
 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
 
 /**
index 204eb9b..a2974c9 100644 (file)
@@ -1038,7 +1038,7 @@ class global_navigation extends navigation_node {
         } else {
             // The home element should be the site because the root node is my moodle
             $this->rootnodes['home'] = $this->add(get_string('sitehome'), new moodle_url('/'), self::TYPE_SETTING, null, 'home');
-            if ($CFG->defaulthomepage == HOMEPAGE_MY) {
+            if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY)) {
                 // We need to stop automatic redirection
                 $this->rootnodes['home']->action->param('redirect', '0');
             }
index a862c42..280e19f 100644 (file)
@@ -941,6 +941,9 @@ class theme_config {
                     return $imagefile;
                 }
             }
+            if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image")) {
+                return $imagefile;
+            }
             if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image")) {
                 return $imagefile;
             }
@@ -974,6 +977,9 @@ class theme_config {
                     return $imagefile;
                 }
             }
+            if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image")) {
+                return $imagefile;
+            }
             $dir = get_plugin_directory($type, $plugin);
             if ($imagefile = $this->image_exists("$dir/pix/$image")) {
                 return $imagefile;
index 7d7e3e2..f6ac22f 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Prepares PHPUnit environment, it is called automatically.
+ * Prepares PHPUnit environment, the phpunit.xml configuration
+ * must specify this file as bootstrap.
  *
  * Exit codes:
  *  0   - success
  *  1   - general error
- *  130 - coding error
+ *  130 - missing PHPUnit library error
  *  131 - configuration problem
  *  132 - install new test database
- *  133 - drop old data, then install new test database
+ *  133 - drop existing data before installing
  *
  * @package    core
  * @category   phpunit
@@ -36,29 +37,39 @@ error_reporting(E_ALL | E_STRICT);
 ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
+require_once(__DIR__.'/bootstraplib.php');
+
 if (isset($_SERVER['REMOTE_ADDR'])) {
-    phpunit_bootstrap_error('Unit tests can be executed only from command line!', 1);
+    phpunit_bootstrap_error(1, 'Unit tests can be executed only from command line!');
 }
 
 if (defined('PHPUNIT_TEST')) {
-    phpunit_bootstrap_error("PHPUNIT_TEST constant must not be manually defined anywhere!", 130);
+    phpunit_bootstrap_error(1, "PHPUNIT_TEST constant must not be manually defined anywhere!");
 }
 /** PHPUnit testing framework active */
 define('PHPUNIT_TEST', true);
 
 if (!defined('PHPUNIT_UTIL')) {
-    /** Identifies utility scripts */
+    /** Identifies utility scripts - the database does not need to be initialised */
     define('PHPUNIT_UTIL', false);
 }
 
 if (defined('CLI_SCRIPT')) {
-    phpunit_bootstrap_error('CLI_SCRIPT must not be manually defined in any PHPUnit test scripts', 130);
+    phpunit_bootstrap_error(1, 'CLI_SCRIPT must not be manually defined in any PHPUnit test scripts');
 }
 define('CLI_SCRIPT', true);
 
+$phpunitversion = PHPUnit_Runner_Version::id();
+if ($phpunitversion === '@package_version@') {
+    // library checked out from git, let's hope dev knows that 3.6.0 is required
+} else if (version_compare($phpunitversion, '3.6.0', 'lt')) {
+    phpunit_bootstrap_error(129, $phpunitversion);
+}
+unset($phpunitversion);
+
 define('NO_OUTPUT_BUFFERING', true);
 
-// only load CFG from config.php
+// only load CFG from config.php, stop ASAP in lib/setup.php
 define('ABORT_AFTER_CONFIG', true);
 require(__DIR__ . '/../../config.php');
 
@@ -82,37 +93,38 @@ if (isset($CFG->phpunit_directorypermissions)) {
 }
 $CFG->filepermissions = ($CFG->directorypermissions & 0666);
 if (!isset($CFG->phpunit_dataroot)) {
-    phpunit_bootstrap_error('Missing $CFG->phpunit_dataroot in config.php, can not run tests!', 131);
+    phpunit_bootstrap_error(131, 'Missing $CFG->phpunit_dataroot in config.php, can not run tests!');
 }
 if (isset($CFG->dataroot) and $CFG->phpunit_dataroot === $CFG->dataroot) {
-    phpunit_bootstrap_error('$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!');
 }
 if (!file_exists($CFG->phpunit_dataroot)) {
     mkdir($CFG->phpunit_dataroot, $CFG->directorypermissions);
 }
 if (!is_dir($CFG->phpunit_dataroot)) {
-    phpunit_bootstrap_error('$CFG->phpunit_dataroot directory can not be created, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory can not be created, can not run tests!');
 }
+
 if (!is_writable($CFG->phpunit_dataroot)) {
-    // try to fix premissions if possible
+    // try to fix permissions if possible
     if (function_exists('posix_getuid')) {
         $chmod = fileperms($CFG->phpunit_dataroot);
-        if (fileowner($dir) == posix_getuid()) {
+        if (fileowner($CFG->phpunit_dataroot) == posix_getuid()) {
             $chmod = $chmod | 0700;
             chmod($CFG->phpunit_dataroot, $chmod);
         }
     }
     if (!is_writable($CFG->phpunit_dataroot)) {
-        phpunit_bootstrap_error('$CFG->phpunit_dataroot directory is not writable, can not run tests!', 131);
+        phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory is not writable, can not run tests!');
     }
 }
 if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
     if ($dh = opendir($CFG->phpunit_dataroot)) {
         while (($file = readdir($dh)) !== false) {
-            if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_store') {
+            if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_Store') {
                 continue;
             }
-            phpunit_bootstrap_error('$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?', 131);
+            phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?');
         }
         closedir($dh);
         unset($dh);
@@ -123,22 +135,28 @@ if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
     phpunit_bootstrap_initdataroot($CFG->phpunit_dataroot);
 }
 
-
 // verify db prefix
 if (!isset($CFG->phpunit_prefix)) {
-    phpunit_bootstrap_error('Missing $CFG->phpunit_prefix in config.php, can not run tests!', 131);
+    phpunit_bootstrap_error(131, 'Missing $CFG->phpunit_prefix in config.php, can not run tests!');
 }
 if ($CFG->phpunit_prefix === '') {
-    phpunit_bootstrap_error('$CFG->phpunit_prefix can not be empty, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->phpunit_prefix can not be empty, can not run tests!');
 }
 if (isset($CFG->prefix) and $CFG->prefix === $CFG->phpunit_prefix) {
-    phpunit_bootstrap_error('$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!');
 }
 
-// throw away standard CFG settings
-
-$CFG->dataroot = $CFG->phpunit_dataroot;
-$CFG->prefix = $CFG->phpunit_prefix;
+// override CFG settings if necessary and throw away extra CFG settings
+$CFG->dataroot  = $CFG->phpunit_dataroot;
+$CFG->prefix    = $CFG->phpunit_prefix;
+$CFG->dbtype    = isset($CFG->phpunit_dbtype) ? $CFG->phpunit_dbtype : $CFG->dbtype;
+$CFG->dblibrary = isset($CFG->phpunit_dblibrary) ? $CFG->phpunit_dblibrary : $CFG->dblibrary;
+$CFG->dbhost    = isset($CFG->phpunit_dbhost) ? $CFG->phpunit_dbhost : $CFG->dbhost;
+$CFG->dbname    = isset($CFG->phpunit_dbname) ? $CFG->phpunit_dbname : $CFG->dbname;
+$CFG->dbuser    = isset($CFG->phpunit_dbuser) ? $CFG->phpunit_dbuser : $CFG->dbuser;
+$CFG->dbpass    = isset($CFG->phpunit_dbpass) ? $CFG->phpunit_dbpass : $CFG->dbpass;
+$CFG->prefix    = isset($CFG->phpunit_prefix) ? $CFG->phpunit_prefix : $CFG->prefix;
+$CFG->dboptions = isset($CFG->phpunit_dboptions) ? $CFG->phpunit_dboptions : $CFG->dboptions;
 
 $allowed = array('wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
                  'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions');
@@ -161,7 +179,7 @@ $CFG->debug = (E_ALL | E_STRICT); // can not use DEBUG_DEVELOPER yet
 $CFG->debugdisplay = 1;
 error_reporting($CFG->debug);
 ini_set('display_errors', '1');
-ini_set('log_errors', '0');
+ini_set('log_errors', '1');
 
 $CFG->passwordsaltmain = 'phpunit'; // makes login via normal UI impossible
 
@@ -182,54 +200,15 @@ require("$CFG->dirroot/lib/setup.php");
 raise_memory_limit(MEMORY_EXTRA);
 
 if (PHPUNIT_UTIL) {
-    // we are not going to do testing, this is 'true' in utility scripts that init database usually
+    // we are not going to do testing, this is 'true' in utility scripts that only init database
     return;
 }
 
 // is database and dataroot ready for testing?
-$problem = phpunit_util::testing_ready_problem();
-
-if ($problem) {
-    switch ($problem) {
-        case 132:
-            phpunit_bootstrap_error('Database was not initialised to run unit tests, please use "php admin/tool/phpunit/cli/util.php --install"', $problem);
-        case 133:
-            phpunit_bootstrap_error('Database was initialised for different version, please use "php admin/tool/phpunit/cli/util.php --drop; php admin/tool/phpunit/cli/util.php --install"', $problem);
-        default:
-            phpunit_bootstrap_error('Unknown problem initialising test database', $problem);
-    }
+list($errorcode, $message) = phpunit_util::testing_ready_problem();
+if ($errorcode) {
+    phpunit_bootstrap_error($errorcode, $message);
 }
 
-// prepare for the first test run - store fresh globals, reset dataroot, etc.
+// prepare for the first test run - store fresh globals, reset database and dataroot, etc.
 phpunit_util::bootstrap_init();
-
-
-//=========================================================
-
-/**
- * Print error and stop execution
- * @param string $text An error message to display
- * @param int $errorcode The error code (see docblock for detailed list)
- * @return void stops code execution with error code
- */
-function phpunit_bootstrap_error($text, $errorcode = 1) {
-    fwrite(STDERR, $text."\n");
-    exit($errorcode);
-}
-
-/**
- * Mark empty dataroot to be used for testing.
- * @param string $dataroot The dataroot directory
- * @return void
- */
-function phpunit_bootstrap_initdataroot($dataroot) {
-    global $CFG;
-
-    if (!file_exists("$dataroot/phpunittestdir.txt")) {
-        file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!');
-    }
-    chmod("$dataroot/phpunittestdir.txt", $CFG->filepermissions);
-    if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
-        mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions);
-    }
-}
diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php
new file mode 100644 (file)
index 0000000..d87c23f
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * PHPUnit bootstrap function
+ *
+ * Note: these functions must be self contained and must not rely on any library or include
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Print error and stop execution
+ * @param int $errorcode The exit error code
+ * @param string $text An error message to display
+ * @return void stops code execution with error code
+ */
+function phpunit_bootstrap_error($errorcode, $text = '') {
+    switch ($errorcode) {
+        case 0:
+            // this is not an error, just print information and exit
+            break;
+        case 1:
+            $text = 'Error: '.$text;
+            break;
+        case 129:
+            $text = 'Moodle requires PHPUnit 3.6.x, '.$text.' is not compatible';
+            break;
+        case 130:
+            $text = 'Moodle can not find PHPUnit PEAR library or necessary PHPUnit extension';
+            break;
+        case 131:
+            $text = 'Moodle configuration problem: '.$text;
+            break;
+        case 132:
+            $text = "Moodle PHPUnit environment is not initialised, please use:\n php admin/tool/phpunit/cli/util.php --install";
+            break;
+        case 133:
+            $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php admin/tool/phpunit/cli/util.php --drop\n php admin/tool/phpunit/cli/util.php --install";
+            break;
+        case 134:
+            $text = 'Moodle can not create PHPUnit configuration file, please verify dirroot permissions';
+            break;
+        default:
+            $text = empty($text) ? '' : ': '.$text;
+            $text = 'Unknown error '.$errorcode.$text;
+            break;
+    }
+    if (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL) {
+        // do not write to error stream because we need the error message in PHP exec result from web ui
+        echo($text."\n");
+    } else {
+        fwrite(STDERR, $text."\n");
+    }
+    exit($errorcode);
+}
+
+/**
+ * Mark empty dataroot to be used for testing.
+ * @param string $dataroot The dataroot directory
+ * @return void
+ */
+function phpunit_bootstrap_initdataroot($dataroot) {
+    global $CFG;
+    umask(0);
+    if (!file_exists("$dataroot/phpunittestdir.txt")) {
+        file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!');
+    }
+    phpunit_boostrap_fix_file_permissions("$dataroot/phpunittestdir.txt");
+    if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
+        mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions);
+    }
+}
+
+/**
+ * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
+ * @param string $file
+ * @return bool success
+ */
+function phpunit_boostrap_fix_file_permissions($file) {
+    global $CFG;
+
+    $permissions = fileperms($file);
+    if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
+        $permissions = $permissions | $CFG->filepermissions;
+        return chmod($file, $permissions);
+    }
+
+    return true;
+}
index f01893e..18766fe 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+
 /**
  * Data generator for unit tests
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class phpunit_data_generator {
     protected $usercounter = 0;
     protected $categorycount = 0;
     protected $coursecount = 0;
-    protected $blockcount = 0;
-    protected $modulecount = 0;
     protected $scalecount = 0;
+    protected $groupcount = 0;
+    protected $groupingcount = 0;
+
+    /** @var array list of plugin generators */
+    protected $generators = array();
+
+    /** @var array lis of common last names */
+    public $lastnames = array(
+        'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson',
+        'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann',
+        'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová',
+        'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова',
+        '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周',
+        '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤',
+    );
+
+    /** @var array lis of common first names */
+    public $firstnames = array(
+        'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava',
+        'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura',
+        'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína',
+        'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина',
+        '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏',
+        '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽',
+    );
+
+    public $loremipsum = <<<EOD
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla non arcu lacinia neque faucibus fringilla. Vivamus porttitor turpis ac leo. Integer in sapien. Nullam eget nisl. Aliquam erat volutpat. Cras elementum. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Integer malesuada. Nullam lectus justo, vulputate eget mollis sed, tempor sed magna. Mauris elementum mauris vitae tortor. Aliquam erat volutpat.
+Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Pellentesque ipsum. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Aliquam ante. Proin in tellus sit amet nibh dignissim sagittis. Vivamus porttitor turpis ac leo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Aliquam erat volutpat. Nulla est.
+Vivamus luctus egestas leo. Aenean fermentum risus id tortor. Mauris dictum facilisis augue. Aliquam erat volutpat. Aliquam ornare wisi eu metus. Aliquam id dolor. Duis condimentum augue id magna semper rutrum. Donec iaculis gravida nulla. Pellentesque ipsum. Etiam dictum tincidunt diam. Quisque tincidunt scelerisque libero. Etiam egestas wisi a erat.
+Integer lacinia. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris tincidunt sem sed arcu. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam id dolor. Maecenas sollicitudin. Et harum quidem rerum facilis est et expedita distinctio. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Nullam dapibus fermentum ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Pellentesque sapien. Duis risus. Mauris elementum mauris vitae tortor. Suspendisse nisl. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim.
+In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Maecenas lorem. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Curabitur ligula sapien, pulvinar a vestibulum quis, facilisis vel sapien. Nam sed tellus id magna elementum tincidunt. Suspendisse nisl. Vivamus luctus egestas leo. Nulla non arcu lacinia neque faucibus fringilla. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Etiam dictum tincidunt diam. Etiam commodo dui eget wisi. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Duis ante orci, molestie vitae vehicula venenatis, tincidunt ac pede. Pellentesque sapien.
+EOD;
 
     /**
      * To be called from data reset code only,
@@ -45,9 +82,37 @@ class phpunit_data_generator {
         $this->usercounter = 0;
         $this->categorycount = 0;
         $this->coursecount = 0;
-        $this->blockcount = 0;
-        $this->modulecount = 0;
         $this->scalecount = 0;
+
+        foreach($this->generators as $generator) {
+            $generator->reset();
+        }
+    }
+
+    /**
+     * Return generator for given plugin
+     * @param string $component
+     * @return mixed plugin data generator
+     */
+    public function get_plugin_generator($component) {
+        list($type, $plugin) = normalize_component($component);
+
+        if ($type !== 'mod' and $type !== 'block') {
+            throw new coding_exception("Plugin type $type does not support generators yet");
+        }
+
+        $dir = get_plugin_directory($type, $plugin);
+
+        if (!isset($this->generators[$type.'_'.$plugin])) {
+            $lib = "$dir/tests/generator/lib.php";
+            if (!include_once($lib)) {
+                throw new coding_exception("Plugin $component does not support data generator, missing tests/generator/lib");
+            }
+            $classname = $type.'_'.$plugin.'_generator';
+            $this->generators[$type.'_'.$plugin] = new $classname($this);
+        }
+
+        return $this->generators[$type.'_'.$plugin];
     }
 
     /**
@@ -68,11 +133,18 @@ class phpunit_data_generator {
             $record['auth'] = 'manual';
         }
 
-        if (!isset($record['firstname'])) {
+        if (!isset($record['firstname']) and !isset($record['lastname'])) {
+            $country = rand(0, 5);
+            $firstname = rand(0, 4);
+            $lastname = rand(0, 4);
+            $female = rand(0, 1);
+            $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
+            $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
+
+        } else if (!isset($record['firstname'])) {
             $record['firstname'] = 'Firstname'.$i;
-        }
 
-        if (!isset($record['lastname'])) {
+        } else if (!isset($record['lastname'])) {
             $record['lastname'] = 'Lastname'.$i;
         }
 
@@ -80,8 +152,15 @@ class phpunit_data_generator {
             $record['idnumber'] = '';
         }
 
+        if (!isset($record['mnethostid'])) {
+            $record['mnethostid'] = $CFG->mnet_localhost_id;
+        }
+
         if (!isset($record['username'])) {
-            $record['username'] = 'username'.$i;
+            $record['username'] = textlib::strtolower($record['firstname']).textlib::strtolower($record['lastname']);
+            while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
+                $record['username'] = $record['username'].'_'.$i;
+            }
         }
 
         if (!isset($record['password'])) {
@@ -96,10 +175,6 @@ class phpunit_data_generator {
             $record['confirmed'] = 1;
         }
 
-        if (!isset($record['mnethostid'])) {
-            $record['mnethostid'] = $CFG->mnet_localhost_id;
-        }
-
         if (!isset($record['lang'])) {
             $record['lang'] = 'en';
         }
@@ -129,6 +204,7 @@ class phpunit_data_generator {
         }
 
         $userid = $DB->insert_record('user', $record);
+
         if (!$record['deleted']) {
             context_user::instance($userid);
         }
@@ -160,7 +236,7 @@ class phpunit_data_generator {
         }
 
         if (!isset($record['description'])) {
-            $record['description'] = 'Test course category '.$i;
+            $record['description'] = "Test course category $i\n$this->loremipsum";
         }
 
         if (!isset($record['descriptionformat'])) {
@@ -171,7 +247,7 @@ class phpunit_data_generator {
             $record['descriptionformat'] = 0;
         }
 
-        if ($record['parent'] == 0) {
+        if (empty($record['parent'])) {
             $parent = new stdClass();
             $parent->path = '';
             $parent->depth = 0;
@@ -235,7 +311,7 @@ class phpunit_data_generator {
         }
 
         if (!isset($record['description'])) {
-            $record['description'] = 'Test course '.$i;
+            $record['description'] = "Test course $i\n$this->loremipsum";
         }
 
         if (!isset($record['descriptionformat'])) {
@@ -313,108 +389,106 @@ class phpunit_data_generator {
      * @return stdClass block instance record
      */
     public function create_block($blockname, $record=null, array $options=null) {
-        global $DB;
+        $generator = $this->get_plugin_generator('block_'.$blockname);
+        return $generator->create_instance($record, $options);
+    }
 
-        $this->blockcount++;
-        $i = $this->blockcount;
+    /**
+     * Create a test module
+     * @param string $modulename
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record
+     */
+    public function create_module($modulename, $record=null, array $options=null) {
+        $generator = $this->get_plugin_generator('mod_'.$modulename);
+        return $generator->create_instance($record, $options);
+    }
 
-        $record = (array)$record;
+    /**
+     * Create a test group for the specified course
+     *
+     * $record should be either an array or a stdClass containing infomation about the group to create.
+     * At the very least it needs to contain courseid.
+     * Default values are added for name, description, and descriptionformat if they are not present.
+     *
+     * This function calls {@see groups_create_group()} to create the group within the database.
+     *
+     * @param array|stdClass $record
+     * @return stdClass group record
+     */
+    public function create_group($record) {
+        global $DB, $CFG;
 
-        $record['blockname'] = $blockname;
+        require_once($CFG->dirroot . '/group/lib.php');
 
-        //TODO: use block callbacks
+        $this->groupcount++;
+        $i = $this->groupcount;
 
-        if (!isset($record['parentcontextid'])) {
-            $record['parentcontextid'] = context_system::instance()->id;
-        }
+        $record = (array)$record;
 
-        if (!isset($record['showinsubcontexts'])) {
-            $record['showinsubcontexts'] = 1;
+        if (empty($record['courseid'])) {
+            throw new coding_exception('courseid must be present in phpunit_util::create_group() $record');
         }
 
-        if (!isset($record['pagetypepattern'])) {
-            $record['pagetypepattern'] = '';
+        if (!isset($record['name'])) {
+            $record['name'] = 'group-' . $i;
         }
 
-        if (!isset($record['subpagepattern'])) {
-            $record['subpagepattern'] = '';
+        if (!isset($record['description'])) {
+            $record['description'] = "Test Group $i\n{$this->loremipsum}";
         }
 
-        if (!isset($record['defaultweight'])) {
-            $record['defaultweight'] = '';
+        if (!isset($record['descriptionformat'])) {
+            $record['descriptionformat'] = FORMAT_MOODLE;
         }
 
-        $biid = $DB->insert_record('block_instances', $record);
-        context_block::instance($biid);
+        $id = groups_create_group((object)$record);
 
-        return $DB->get_record('block_instances', array('id'=>$biid), '*', MUST_EXIST);
+        return $DB->get_record('groups', array('id'=>$id));
     }
 
     /**
-     * Create a test module
-     * @param string $modulename
+     * Create a test grouping for the specified course
+     *
+     * $record should be either an array or a stdClass containing infomation about the grouping to create.
+     * At the very least it needs to contain courseid.
+     * Default values are added for name, description, and descriptionformat if they are not present.
+     *
+     * This function calls {@see groups_create_grouping()} to create the grouping within the database.
+     *
      * @param array|stdClass $record
-     * @param array $options
-     * @return stdClass activity record
+     * @return stdClass grouping record
      */
-    public function create_module($modulename, $record=null, array $options=null) {
+    public function create_grouping($record) {
         global $DB, $CFG;
-        require_once("$CFG->dirroot/course/lib.php");
 
-        $this->modulecount++;
-        $i = $this->modulecount;
+        require_once($CFG->dirroot . '/group/lib.php');
 
-        $record = (array)$record;
-        $options = (array)$options;
+        $this->groupingcount++;
+        $i = $this->groupingcount;
 
-        if (!isset($record['name'])) {
-            $record['name'] = get_string('pluginname', $modulename).' '.$i;
-        }
+        $record = (array)$record;
 
-        if (!isset($record['intro'])) {
-            $record['intro'] = 'Test module '.$i;
+        if (empty($record['courseid'])) {
+            throw new coding_exception('courseid must be present in phpunit_util::create_grouping() $record');
         }
 
-        if (!isset($record['introformat'])) {
-            $record['introformat'] = FORMAT_MOODLE;
+        if (!isset($record['name'])) {
+            $record['name'] = 'grouping-' . $i;
         }
 
-        if (!isset($options['section'])) {
-            $options['section'] = 1;
+        if (!isset($record['description'])) {
+            $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
         }
 
-        //TODO: use module callbacks
-
-        if ($modulename === 'page') {
-            if (!isset($record['content'])) {
-                $record['content'] = 'Test page content';
-            }
-            if (!isset($record['contentformat'])) {
-                $record['contentformat'] = FORMAT_MOODLE;
-            }
-
-        } else {
-            error('TODO: only mod_page is supported in data generator for now');
+        if (!isset($record['descriptionformat'])) {
+            $record['descriptionformat'] = FORMAT_MOODLE;
         }
 
-        $id = $DB->insert_record($modulename, $record);
-
-        $cm = new stdClass();
-        $cm->course   = $record['course'];
-        $cm->module   = $DB->get_field('modules', 'id', array('name'=>$modulename));
-        $cm->section  = $options['section'];
-        $cm->instance = $id;
-        $cm->id = $DB->insert_record('course_modules', $cm);
-
-        $cm->coursemodule = $cm->id;
-        add_mod_to_section($cm);
-
-        context_module::instance($cm->id);
-
-        $instance = $DB->get_record($modulename, array('id'=>$id), '*', MUST_EXIST);
-        $instance->cmid = $cm->id;
+        $id = groups_create_grouping((object)$record);
 
-        return $instance;
+        return $DB->get_record('groupings', array('id'=>$id));
     }
 
     /**
@@ -468,3 +542,191 @@ class phpunit_data_generator {
         return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
     }
 }
+
+
+/**
+ * Module generator base class.
+ *
+ * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class phpunit_module_generator {
+    /** @var phpunit_data_generator@var  */
+    protected $datagenerator;
+
+    /** @var number of created instances */
+    protected $instancecount = 0;
+
+    public function __construct(phpunit_data_generator $datagenerator) {
+        $this->datagenerator = $datagenerator;
+    }
+
+    /**
+     * To be called from data reset code only,
+     * do not use in tests.
+     * @return void
+     */
+    public function reset() {
+        $this->instancecount = 0;
+    }
+
+    /**
+     * Returns module name
+     * @return string name of module that this class describes
+     * @throws coding_exception if class invalid
+     */
+    public function get_modulename() {
+        $matches = null;
+        if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) {
+            throw new coding_exception('Invalid module generator class name: '.get_class($this));
+        }
+
+        if (empty($matches[1])) {
+            throw new coding_exception('Invalid module generator class name: '.get_class($this));
+        }
+        return $matches[1];
+    }
+
+    /**
+     * Create course module and link it to course
+     * @param stdClass $instance
+     * @param array $options: section, visible
+     * @return stdClass $cm instance
+     */
+    protected function create_course_module(stdClass $instance, array $options) {
+        global $DB, $CFG;
+        require_once("$CFG->dirroot/course/lib.php");
+
+        $modulename = $this->get_modulename();
+
+        $cm = new stdClass();
+        $cm->course             = $instance->course;
+        $cm->module             = $DB->get_field('modules', 'id', array('name'=>$modulename));
+        $cm->instance           = $instance->id;
+        $cm->section            = isset($options['section']) ? $options['section'] : 0;
+        $cm->idnumber           = isset($options['idnumber']) ? $options['idnumber'] : 0;
+        $cm->added              = time();
+
+        $columns = $DB->get_columns('course_modules');
+        foreach ($options as $key=>$value) {
+            if ($key === 'id' or !isset($columns[$key])) {
+                continue;
+            }
+            if (property_exists($cm, $key)) {
+                continue;
+            }
+            $cm->$key = $value;
+        }
+
+        $cm->id = $DB->insert_record('course_modules', $cm);
+        $cm->coursemodule = $cm->id;
+
+        add_mod_to_section($cm);
+
+        $cm = get_coursemodule_from_id($modulename, $cm->id, $cm->course, true, MUST_EXIST);
+
+        context_module::instance($cm->id);
+
+        return $cm;
+    }
+
+    /**
+     * Create a test module
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record
+     */
+    abstract public function create_instance($record = null, array $options = null);
+}
+
+
+/**
+ * Block generator base class.
+ *
+ * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class phpunit_block_generator {
+    /** @var phpunit_data_generator@var  */
+    protected $datagenerator;
+
+    /** @var number of created instances */
+    protected $instancecount = 0;
+
+    public function __construct(phpunit_data_generator $datagenerator) {
+        $this->datagenerator = $datagenerator;
+    }
+
+    /**
+     * To be called from data reset code only,
+     * do not use in tests.
+     * @return void
+     */
+    public function reset() {
+        $this->instancecount = 0;
+    }
+
+    /**
+     * Returns block name
+     * @return string name of block that this class describes
+     * @throws coding_exception if class invalid
+     */
+    public function get_blockname() {
+        $matches = null;
+        if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) {
+            throw new coding_exception('Invalid block generator class name: '.get_class($this));
+        }
+
+        if (empty($matches[1])) {
+            throw new coding_exception('Invalid block generator class name: '.get_class($this));
+        }
+        return $matches[1];
+    }
+
+    /**
+     * Fill in record defaults
+     * @param stdClass $record
+     * @return stdClass
+     */
+    protected function prepare_record(stdClass $record) {
+        $record->blockname = $this->get_blockname();
+        if (!isset($record->parentcontextid)) {
+            $record->parentcontextid = context_system::instance()->id;
+        }
+        if (!isset($record->showinsubcontexts)) {
+            $record->showinsubcontexts = 1;
+        }
+        if (!isset($record->pagetypepattern)) {
+            $record->pagetypepattern = '';
+        }
+        if (!isset($record->subpagepattern)) {
+            $record->subpagepattern = null;
+        }
+        if (!isset($record->defaultregion)) {
+            $record->defaultregion = '';
+        }
+        if (!isset($record->defaultweight)) {
+            $record->defaultweight = '';
+        }
+        if (!isset($record->configdata)) {
+            $record->configdata = null;
+        }
+        return $record;
+    }
+
+    /**
+     * Create a test block
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record
+     */
+    abstract public function create_instance($record = null, array $options = null);
+}
index a1906a9..4a62f4c 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-// necessary when loaded from cli/util.php script
-// If this is missing then PHPUnit is not in your PHP include path. This normally
-// happens if installation didn't complete correctly. Check your environment.
 require_once 'PHPUnit/Autoload.php';
+require_once 'PHPUnit/Extensions/Database/Autoload.php';
 
 
 /**
@@ -44,20 +42,80 @@ class phpunit_util {
     protected static $tabledata = null;
 
     /**
-     * @var array An array of globals cloned from CFG
+     * @var array original structure of all database tables
+     */
+    protected static $tablestructure = null;
+
+    /**
+     * @var array An array of original globals, restored after each test
      */
     protected static $globals = array();
 
     /**
      * @var int last value of db writes counter, used for db resetting
      */
-    protected static $lastdbwrites = null;
+    public static $lastdbwrites = null;
 
     /**
      * @var phpunit_data_generator
      */
     protected static $generator = null;
 
+    /**
+     * @var resource used for prevention of parallel test execution
+     */
+    protected static $lockhandle = null;
+
+    /**
+     * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
+     *
+     * Note: do not call manually!
+     *
+     * @internal
+     * @static
+     * @return void
+     */
+    public static function acquire_test_lock() {
+        global $CFG;
+        if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
+            // dataroot not initialised yet
+            return;
+        }
+        if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) {
+            file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests');
+            phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock");
+        }
+        if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) {
+            $wouldblock = null;
+            $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock);
+            if (!$locked) {
+                if ($wouldblock) {
+                    echo "Waiting for other test execution to complete...\n";
+                }
+                $locked = flock(self::$lockhandle, LOCK_EX);
+            }
+            if (!$locked) {
+                fclose(self::$lockhandle);
+                self::$lockhandle = null;
+            }
+        }
+        register_shutdown_function(array('phpunit_util', 'release_test_lock'));
+    }
+
+    /**
+     * Note: do not call manually!
+     * @internal
+     * @static
+     * @return void
+     */
+    public static function release_test_lock() {
+        if (self::$lockhandle) {
+            flock(self::$lockhandle, LOCK_UN);
+            fclose(self::$lockhandle);
+            self::$lockhandle = null;
+        }
+    }
+
     /**
      * Get data generator
      * @static
@@ -90,118 +148,270 @@ class phpunit_util {
         }
 
         if (!is_array(self::$tabledata)) {
-            phpunit_bootstrap_error('Can not read dataroot/phpunit/tabledata.ser or invalid format!');
+            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
         }
 
         return self::$tabledata;
     }
 
     /**
-     * Reset all database tables to default values.
+     * Returns structure of all tables right after installation.
      * @static
-     * @param bool $logchanges
-     * @param null|PHPUnit_Framework_TestCase $caller
-     * @return bool true if reset done, false if skipped
+     * @return array $table=>$records
+     */
+    public static function get_tablestructure() {
+        global $CFG;
+
+        if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
+            // not initialised yet
+            return array();
+        }
+
+        if (!isset(self::$tablestructure)) {
+            $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
+            self::$tablestructure = unserialize($data);
+        }
+
+        if (!is_array(self::$tablestructure)) {
+            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.');
+        }
+
+        return self::$tablestructure;
+    }
+
+    /**
+     * Returns list of tables that are unmodified and empty.
+     *
+     * @static
+     * @return array of table names, empty if unknown
      */
-    public static function reset_database($logchanges = false, PHPUnit_Framework_TestCase $caller = null) {
+    protected static function guess_unmodified_empty_tables() {
         global $DB;
 
-        if ($logchanges) {
-            if (self::$lastdbwrites != $DB->perf_get_writes()) {
-                if ($caller) {
-                    $where = ' in testcase: '.get_class($caller).'->'.$caller->getName(true);
-                } else {
-                    $where = '';
+        $dbfamily = $DB->get_dbfamily();
+
+        if ($dbfamily === 'mysql') {
+            $empties = array();
+            $prefix = $DB->get_prefix();
+            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                if (!is_null($info->auto_increment)) {
+                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                    if ($info->auto_increment == 1) {
+                        $empties[$table] = $table;
+                    }
                 }
-                error_log('warning: unexpected database modification, resetting DB state'.$where);
             }
+            $rs->close();
+            return $empties;
+
+        } else if ($dbfamily === 'mssql') {
+            $empties = array();
+            $prefix = $DB->get_prefix();
+            $sql = "SELECT t.name
+                      FROM sys.identity_columns i
+                      JOIN sys.tables t ON t.object_id = i.object_id
+                     WHERE t.name LIKE ?
+                       AND i.name = 'id'
+                       AND i.last_value IS NULL";
+            $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                $empties[$table] = $table;
+            }
+            $rs->close();
+            return $empties;
+
+        } else {
+            return array();
         }
+    }
 
-        $tables = $DB->get_tables(false);
-        if (!$tables or empty($tables['config'])) {
-            // not installed yet
+    /**
+     * Reset all database sequences to initial values.
+     *
+     * @static
+     * @param array $empties tables that are known to be unmodified and empty
+     * @return void
+     */
+    public static function reset_all_database_sequences(array $empties = null) {
+        global $DB;
+
+        if (!$data = self::get_tabledata()) {
+            // not initialised yet
+            return;
+        }
+        if (!$structure = self::get_tablestructure()) {
+            // not initialised yet
             return;
         }
 
-        $dbreset = false;
-        if (is_null(self::$lastdbwrites) or self::$lastdbwrites != $DB->perf_get_writes()) {
-            if ($data = self::get_tabledata()) {
-                $trans = $DB->start_delegated_transaction(); // faster and safer
-
-                $resetseq = array();
-                foreach ($data as $table=>$records) {
+        $dbfamily = $DB->get_dbfamily();
+        if ($dbfamily === 'postgres') {
+            $queries = array();
+            $prefix = $DB->get_prefix();
+            foreach ($data as $table=>$records) {
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
                     if (empty($records)) {
-                        if ($DB->count_records($table)) {
-                            $DB->delete_records($table, array());
-                            $resetseq[$table] = $table;
-                        }
-                        continue;
+                        $nextid = 1;
+                    } else {
+                        $lastrecord = end($records);
+                        $nextid = $lastrecord->id + 1;
                     }
+                    $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
+                }
+            }
+            if ($queries) {
+                $DB->change_database_structure(implode(';', $queries));
+            }
 
-                    $firstrecord = reset($records);
-                    if (property_exists($firstrecord, 'id')) {
-                        if ($DB->count_records($table) >= count($records)) {
-                            $currentrecords = $DB->get_records($table, array(), 'id ASC');
-                            $changed = false;
-                            foreach ($records as $id=>$record) {
-                                if (!isset($currentrecords[$id])) {
-                                    $changed = true;
-                                    break;
-                                }
-                                if ((array)$record != (array)$currentrecords[$id]) {
-                                    $changed = true;
-                                    break;
-                                }
-                                unset($currentrecords[$id]);
-                            }
-                            if (!$changed) {
-                                if ($currentrecords) {
-                                    $remainingfirst = reset($currentrecords);
-                                    $lastrecord = end($records);
-                                    if ($remainingfirst->id > $lastrecord->id) {
-                                        $DB->delete_records_select($table, "id >= ?", array($remainingfirst->id));
-                                        $resetseq[$table] = $table;
-                                        continue;
-                                    }
-                                } else {
-                                    continue;
-                                }
-                            }
+        } else if ($dbfamily === 'mysql') {
+            $sequences = array();
+            $prefix = $DB->get_prefix();
+            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                if (!is_null($info->auto_increment)) {
+                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                    $sequences[$table] = $info->auto_increment;
+                }
+            }
+            $rs->close();
+            foreach ($data as $table=>$records) {
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    if (isset($sequences[$table])) {
+                        if (empty($records)) {
+                            $lastid = 0;
+                        } else {
+                            $lastrecord = end($records);
+                            $lastid = $lastrecord->id;
                         }
+                        if ($sequences[$table] != $lastid +1) {
+                            $DB->get_manager()->reset_sequence($table);
+                        }
+
+                    } else {
+                        $DB->get_manager()->reset_sequence($table);
                     }
+                }
+            }
+
+        } else {
+            // note: does mssql and oracle support any kind of faster reset?
+            if (is_null($empties)) {
+                $empties = self::guess_unmodified_empty_tables();
+            }
+            foreach ($data as $table=>$records) {
+                if (isset($empties[$table])) {
+                    continue;
+                }
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    $DB->get_manager()->reset_sequence($table);
+                }
+            }
+        }
+    }
+
+    /**
+     * Reset all database tables to default values.
+     * @static
+     * @return bool true if reset done, false if skipped
+     */
+    public static function reset_database() {
+        global $DB;
+
+        $tables = $DB->get_tables(false);
+        if (!$tables or empty($tables['config'])) {
+            // not installed yet
+            return false;
+        }
+
+        if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
+            return false;
+        }
+        if (!$data = self::get_tabledata()) {
+            // not initialised yet
+            return false;
+        }
+        if (!$structure = self::get_tablestructure()) {
+            // not initialised yet
+            return false;
+        }
+
+        $empties = self::guess_unmodified_empty_tables();
 
+        foreach ($data as $table=>$records) {
+            if (empty($records)) {
+                if (isset($empties[$table])) {
+                    // table was not modified and is empty
+                } else {
                     $DB->delete_records($table, array());
-                    if (property_exists($firstrecord, 'id')) {
-                        $resetseq[$table] = $table;
+                }
+                continue;
+            }
+
+            if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                $currentrecords = $DB->get_records($table, array(), 'id ASC');
+                $changed = false;
+                foreach ($records as $id=>$record) {
+                    if (!isset($currentrecords[$id])) {
+                        $changed = true;
+                        break;
                     }
-                    foreach ($records as $record) {
-                        $DB->import_record($table, $record, false, true);
+                    if ((array)$record != (array)$currentrecords[$id]) {
+                        $changed = true;
+                        break;
                     }
+                    unset($currentrecords[$id]);
                 }
-                // reset all sequences
-                foreach ($resetseq as $table) {
-                    $DB->get_manager()->reset_sequence($table, true);
+                if (!$changed) {
+                    if ($currentrecords) {
+                        $lastrecord = end($records);
+                        $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
+                        continue;
+                    } else {
+                        continue;
+                    }
                 }
+            }
+
+            $DB->delete_records($table, array());
+            foreach ($records as $record) {
+                $DB->import_record($table, $record, false, true);
+            }
+        }
 
-                $trans->allow_commit();
+        // reset all next record ids - aka sequences
+        self::reset_all_database_sequences($empties);
 
-                // remove extra tables
-                foreach ($tables as $tablename) {
-                    if (!isset($data[$tablename])) {
-                        $DB->get_manager()->drop_table(new xmldb_table($tablename));
-                    }
-                }
-                $dbreset = true;
+        // remove extra tables
+        foreach ($tables as $table) {
+            if (!isset($data[$table])) {
+                $DB->get_manager()->drop_table(new xmldb_table($table));
             }
         }
 
         self::$lastdbwrites = $DB->perf_get_writes();
 
-        return $dbreset;
+        return true;
     }
 
     /**
-     * Purge dataroot
+     * Purge dataroot directory
      * @static
      * @return void
      */
@@ -231,58 +441,62 @@ class phpunit_util {
      *
      * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
      *
+     * @static
      * @param bool $logchanges log changes in global state and database in error log
-     * @param PHPUnit_Framework_TestCase $caller caller object, used for logging only
      * @return void
-     * @static
      */
-    public static function reset_all_data($logchanges = false, PHPUnit_Framework_TestCase $caller = null) {
+    public static function reset_all_data($logchanges = false) {
         global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION;
 
-        $dbreset = self::reset_database($logchanges, $caller);
+        // reset global $DB in case somebody mocked it
+        $DB = self::get_global_backup('DB');
+
+        if ($DB->is_transaction_started()) {
+            // we can not reset inside transaction
+            $DB->force_transaction_rollback();
+        }
+
+        $resetdb = self::reset_database();
+        $warnings = array();
 
         if ($logchanges) {
-            if ($caller) {
-                $where = ' in testcase: '.get_class($caller).'->'.$caller->getName(true);
-            } else {
-                $where = '';
+            if ($resetdb) {
+                $warnings[] = 'Warning: unexpected database modification, resetting DB state';
             }
 
             $oldcfg = self::get_global_backup('CFG');
             $oldsite = self::get_global_backup('SITE');
             foreach($CFG as $k=>$v) {
                 if (!property_exists($oldcfg, $k)) {
-                    error_log('warning: unexpected new $CFG->'.$k.' value'.$where);
+                    $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
                 } else if ($oldcfg->$k !== $CFG->$k) {
-                    error_log('warning: unexpected change of $CFG->'.$k.' value'.$where);
+                    $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
                 }
                 unset($oldcfg->$k);
 
             }
             if ($oldcfg) {
                 foreach($oldcfg as $k=>$v) {
-                    error_log('warning: unexpected removal of $CFG->'.$k.$where);
+                    $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
                 }
             }
 
             if ($USER->id != 0) {
-                error_log('warning: unexpected change of $USER'.$where);
+                $warnings[] = 'Warning: unexpected change of $USER';
             }
 
             if ($COURSE->id != $oldsite->id) {
-                error_log('warning: unexpected change of $COURSE'.$where);
+                $warnings[] = 'Warning: unexpected change of $COURSE';
             }
         }
 
-        // restore _SERVER
-        unset($_SERVER['HTTP_USER_AGENT']);
-
-        // restore original config
+        // restore original globals
+        $_SERVER = self::get_global_backup('_SERVER');
         $CFG = self::get_global_backup('CFG');
         $SITE = self::get_global_backup('SITE');
         $COURSE = $SITE;
 
-        // recreate globals
+        // reinitialise following globals
         $OUTPUT = new bootstrap_renderer();
         $PAGE = new moodle_page();
         $FULLME = null;
@@ -291,7 +505,7 @@ class phpunit_util {
         $SESSION = new stdClass();
         $_SESSION['SESSION'] =& $SESSION;
 
-        // set fresh new user
+        // set fresh new not-logged-in user
         $user = new stdClass();
         $user->id = 0;
         $user->mnethostid = $CFG->mnet_localhost_id;
@@ -300,34 +514,47 @@ class phpunit_util {
         // reset all static caches
         accesslib_clear_all_caches(true);
         get_string_manager()->reset_caches();
+        events_get_handlers('reset');
         //TODO: add more resets here and probably refactor them to new core function
 
-        // purge dataroot
+        // purge dataroot directory
         self::reset_dataroot();
 
-        // restore original config once more in case resetting of caches changes CFG
+        // restore original config once more in case resetting of caches changed CFG
         $CFG = self::get_global_backup('CFG');
 
-        // remember db writes
-        self::$lastdbwrites = $DB->perf_get_writes();
-
         // inform data generator
         self::get_data_generator()->reset();
 
         // fix PHP settings
         error_reporting($CFG->debug);
+
+        // verify db writes just in case something goes wrong in reset
+        if (self::$lastdbwrites != $DB->perf_get_writes()) {
+            error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
+            self::$lastdbwrites = $DB->perf_get_writes();
+        }
+
+        if ($warnings) {
+            $warnings = implode("\n", $warnings);
+            trigger_error($warnings, E_USER_WARNING);
+        }
     }
 
     /**
      * Called during bootstrap only!
+     * @internal
      * @static
+     * @return void
      */
     public static function bootstrap_init() {
-        global $CFG, $SITE;
+        global $CFG, $SITE, $DB;
 
         // backup the globals
+        self::$globals['_SERVER'] = $_SERVER;
         self::$globals['CFG'] = clone($CFG);
         self::$globals['SITE'] = clone($SITE);
+        self::$globals['DB'] = $DB;
 
         // refresh data in all tables, clear caches, etc.
         phpunit_util::reset_all_data();
@@ -340,6 +567,11 @@ class phpunit_util {
      * @return mixed
      */
     public static function get_global_backup($name) {
+        if ($name === 'DB') {
+            // no cloning of database object,
+            // we just need the original reference, not original state
+            return self::$globals['DB'];
+        }
         if (isset(self::$globals[$name])) {
             if (is_object(self::$globals[$name])) {
                 $return = clone(self::$globals[$name]);
@@ -384,41 +616,38 @@ class phpunit_util {
      * Is this site initialised to run unit tests?
      *
      * @static
-     * @return int error code, 0 means ok
+     * @return int array errorcode=>message, 0 means ok
      */
     public static function testing_ready_problem() {
-        global $DB, $CFG;
-
-        if (!self::is_test_site()) {
-            return 131;
-        }
+        global $CFG, $DB;
 
-        $tables = $DB->get_tables(true);
+        $tables = $DB->get_tables(false);
 
-        if (!$tables) {
-            return 132;
+        if (!self::is_test_site()) {
+            // dataroot was verified in bootstrap, so it must be DB
+            return array(131, 'Can not use test database, try changing prefix');
         }
 
-        if (!get_config('core', 'phpunittest')) {
-             return 131;
+        if (empty($tables)) {
+            return array(132, '');
         }
 
-        if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
-            return 131;
+        if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
+            return array(133, '');
         }
 
         if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
-            return 131;
+            return array(133, '');
         }
 
         $hash = phpunit_util::get_version_hash();
         $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
 
         if ($hash !== $oldhash) {
-            return 133;
+            return array(133, '');
         }
 
-        return 0;
+        return array(0, '');
     }
 
     /**
@@ -433,13 +662,25 @@ class phpunit_util {
         global $DB, $CFG;
 
         if (!self::is_test_site()) {
-            cli_error('Can not drop non-test sites!!', 131);
+            phpunit_bootstrap_error(131, 'Can not drop non-test site!!');
         }
 
-        // drop dataroot
+        // purge dataroot
         self::reset_dataroot();
         phpunit_bootstrap_initdataroot($CFG->dataroot);
-        remove_dir("$CFG->dataroot/phpunit", true);
+        $keep = array('.', '..', 'lock', 'webrunner.xml');
+        $files = scandir("$CFG->dataroot/phpunit");
+        foreach ($files as $file) {
+            if (in_array($file, $keep)) {
+                continue;
+            }
+            $path = "$CFG->dataroot/phpunit/$file";
+            if (is_dir($path)) {
+                remove_dir($path, false);
+            } else {
+                unlink($path);
+            }
+        }
 
         // drop all tables
         $tables = $DB->get_tables(false);
@@ -466,15 +707,20 @@ class phpunit_util {
         global $DB, $CFG;
 
         if (!self::is_test_site()) {
-            cli_error('Can not install non-test sites!!', 131);
+            phpunit_bootstrap_error(131, 'Can not install on non-test site!!');
         }
 
         if ($DB->get_tables()) {
-            cli_error('Database tables already installed, drop the site first.', 133);
+            list($errorcode, $message) = phpunit_util::testing_ready_problem();
+            if ($errorcode) {
+                phpunit_bootstrap_error(133, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
+            } else {
+                phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
+            }
         }
 
         $options = array();
-        $options['adminpass'] = 'admin'; // removed later
+        $options['adminpass'] = 'admin';
         $options['shortname'] = 'phpunit';
         $options['fullname'] = 'PHPUnit test site';
 
@@ -489,10 +735,12 @@ class phpunit_util {
 
         // store data for all tables
         $data = array();
+        $structure = array();
         $tables = $DB->get_tables();
         foreach ($tables as $table) {
             $columns = $DB->get_columns($table);
-            if (isset($columns['id'])) {
+            $structure[$table] = $columns;
+            if (isset($columns['id']) and $columns['id']->auto_increment) {
                 $data[$table] = $DB->get_records($table, array(), 'id ASC');
             } else {
                 // there should not be many of these
@@ -500,17 +748,21 @@ class phpunit_util {
             }
         }
         $data = serialize($data);
-        @unlink("$CFG->dataroot/phpunit/tabledata.ser");
         file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
+        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
+
+        $structure = serialize($structure);
+        file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
+        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
 
         // hash all plugin versions - helps with very fast detection of db structure changes
         $hash = phpunit_util::get_version_hash();
-        @unlink("$CFG->dataroot/phpunit/versionshash.txt");
         file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
+        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
     }
 
     /**
-     * Calculate unique version hash for all available plugins and core.
+     * Calculate unique version hash for all plugins and core.
      * @static
      * @return string sha1 hash
      */
@@ -555,9 +807,9 @@ class phpunit_util {
     }
 
     /**
-     * Builds /phpunit.xml file using defaults from /phpunit.xml.dist
+     * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
      * @static
-     * @return void
+     * @return bool true means main config file created, false means only dataroot file created
      */
     public static function build_config_file() {
         global $CFG;
@@ -592,8 +844,19 @@ class phpunit_util {
 
         $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
 
-        @unlink("$CFG->dirroot/phpunit.xml");
-        file_put_contents("$CFG->dirroot/phpunit.xml", $data);
+        $result = false;
+        if (is_writable($CFG->dirroot)) {
+            if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
+                phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
+            }
+        }
+        // relink - it seems that xml:base does not work in phpunit xml files, remove this nasty hack if you find a way to set xml base for relative refs
+        $data = str_replace('lib/phpunit/', "$CFG->dirroot/lib/phpunit/", $data);
+        $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|', '<directory suffix="_test.php">'.$CFG->dirroot.'/$1</directory>', $data);
+        file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
+        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
+
+        return (bool)$result;
     }
 }
 
@@ -610,7 +873,7 @@ class phpunit_util {
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class UnitTestCase extends PHPUnit_Framework_TestCase {
+abstract class UnitTestCase extends PHPUnit_Framework_TestCase {
 
     /**
      * @deprecated since 2.3
@@ -619,7 +882,7 @@ class UnitTestCase extends PHPUnit_Framework_TestCase {
      * @return void
      */
     public function expectException($expected, $message = '') {
-        // use phpdocs: @expectedException ExceptionClassName
+        // alternatively use phpdocs: @expectedException ExceptionClassName
         if (!$expected) {
             return;
         }
@@ -719,7 +982,11 @@ class UnitTestCase extends PHPUnit_Framework_TestCase {
      * @return void
      */
     public static function assertIsA($actual, $expected, $message = '') {
-        parent::assertInstanceOf($expected, $actual, $message);
+        if ($expected === 'array') {
+            parent::assertEquals(gettype($actual), 'array', $message);
+        } else {
+            parent::assertInstanceOf($expected, $actual, $message);
+        }
     }
 }
 
@@ -734,12 +1001,12 @@ class UnitTestCase extends PHPUnit_Framework_TestCase {
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class basic_testcase extends PHPUnit_Framework_TestCase {
+abstract class basic_testcase extends PHPUnit_Framework_TestCase {
 
     /**
      * Constructs a test case with the given name.
      *
-     * Note: use setUp() or setUpBeforeClass() in custom test cases.
+     * Note: use setUp() or setUpBeforeClass() in your test cases.
      *
      * @param string $name
      * @param array  $data
@@ -757,9 +1024,23 @@ class basic_testcase extends PHPUnit_Framework_TestCase {
      * Runs the bare test sequence and log any changes in global state or database.
      * @return void
      */
-    public function runBare() {
-        parent::runBare();
-        phpunit_util::reset_all_data(true, $this);
+    final public function runBare() {
+        global $DB;
+
+        try {
+            parent::runBare();
+        } catch (Exception $e) {
+            // cleanup after failed expectation
+            phpunit_util::reset_all_data();
+            throw $e;
+        }
+
+        if ($DB->is_transaction_started()) {
+            phpunit_util::reset_all_data();
+            throw new coding_exception('basic_testcase '.$this->getName().' is not supposed to use database transactions!');
+        }
+
+        phpunit_util::reset_all_data(true);
     }
 }
 
@@ -772,14 +1053,17 @@ class basic_testcase extends PHPUnit_Framework_TestCase {
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class advanced_testcase extends PHPUnit_Framework_TestCase {
+abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
     /** @var bool automatically reset everything? null means log changes */
-    protected $resetAfterTest;
+    private $resetAfterTest;
+
+    /** @var moodle_transaction */
+    private $testdbtransaction;
 
     /**
      * Constructs a test case with the given name.
      *
-     * Note: use setUp() or setUpBeforeClass() in custom test cases.
+     * Note: use setUp() or setUpBeforeClass() in your test cases.
      *
      * @param string $name
      * @param array  $data
@@ -797,21 +1081,168 @@ class advanced_testcase extends PHPUnit_Framework_TestCase {
      * Runs the bare test sequence.
      * @return void
      */
-    public function runBare() {
-        $this->resetAfterTest = null;
+    final public function runBare() {
+        global $DB;
+
+        if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
+            // this happens when previous test does not reset, we can not use transactions
+            $this->testdbtransaction = null;
+
+        } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
+            // database must allow rollback of DDL, so no mysql here
+            $this->testdbtransaction = $DB->start_delegated_transaction();
+        }
 
-        parent::runBare();
+        try {
+            parent::runBare();
+            // set DB reference in case somebody mocked it in test
+            $DB = phpunit_util::get_global_backup('DB');
+        } catch (Exception $e) {
+            // cleanup after failed expectation
+            phpunit_util::reset_all_data();
+            throw $e;
+        }
+
+        if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
+            $this->testdbtransaction = null;
+        }
 
         if ($this->resetAfterTest === true) {
-            self::resetAllData();
+            if ($this->testdbtransaction) {
+                $DB->force_transaction_rollback();
+                phpunit_util::reset_all_database_sequences();
+                phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
+            }
+            phpunit_util::reset_all_data();
+
         } else if ($this->resetAfterTest === false) {
+            if ($this->testdbtransaction) {
+                $this->testdbtransaction->allow_commit();
+            }
             // keep all data untouched for other tests
+
         } else {
             // reset but log what changed
-            phpunit_util::reset_all_data(true, $this);
+            if ($this->testdbtransaction) {
+                try {
+                    $this->testdbtransaction->allow_commit();
+                } catch (dml_transaction_exception $e) {
+                    phpunit_util::reset_all_data();
+                    throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
+                }
+            }
+            phpunit_util::reset_all_data(true);
+        }
+
+        // make sure test did not forget to close transaction
+        if ($DB->is_transaction_started()) {
+            phpunit_util::reset_all_data();
+            if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
+                    or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
+                    or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
+                throw new coding_exception('Test '.$this->getName().' did not close database transaction');
+            }
         }
+    }
 
-        $this->resetAfterTest = null;
+    /**
+     * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
+     *
+     * @param string $xmlFile
+     * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet
+     */
+    protected function createFlatXMLDataSet($xmlFile) {
+        return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile);
+    }
+
+    /**
+     * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
+     *
+     * @param string $xmlFile
+     * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet
+     */
+    protected function createXMLDataSet($xmlFile) {
+        return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile);
+    }
+
+    /**
+     * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
+     *
+     * @param array $files array tablename=>cvsfile
+     * @param string $delimiter
+     * @param string $enclosure
+     * @param string $escape
+     * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet
+     */
+    protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
+        $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape);
+        foreach($files as $table=>$file) {
+            $dataSet->addTable($table, $file);
+        }
+        return $dataSet;
+    }
+
+    /**
+     * Creates new ArrayDataSet from given array
+     *
+     * @param array $data array of tables, first row in each table is columns
+     * @return phpunit_ArrayDataSet
+     */
+    protected function createArrayDataSet(array $data) {
+        return new phpunit_ArrayDataSet($data);
+    }
+
+    /**
+     * Load date into moodle database tables from standard PHPUnit data set.
+     *
+     * Note: it is usually better to use data generators
+     *
+     * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset
+     * @return void
+     */
+    protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) {
+        global $DB;
+
+        $structure = phpunit_util::get_tablestructure();
+
+        foreach($dataset->getTableNames() as $tablename) {
+            $table = $dataset->getTable($tablename);
+            $metadata = $dataset->getTableMetaData($tablename);
+            $columns = $metadata->getColumns();
+
+            $doimport = false;
+            if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
+                $doimport = in_array('id', $columns);
+            }
+
+            for($r=0; $r<$table->getRowCount(); $r++) {
+                $record = $table->getRow($r);
+                if ($doimport) {
+                    $DB->import_record($tablename, $record);
+                } else {
+                    $DB->insert_record($tablename, $record);
+                }
+            }
+            if ($doimport) {
+                $DB->get_manager()->reset_sequence(new xmldb_table($tablename));
+            }
+        }
+    }
+
+    /**
+     * Call this method from test if you want to make sure that
+     * the resetting of database is done the slow way without transaction
+     * rollback.
+     *
+     * This is useful especially when testing stuff that is not compatible with transactions.
+     *
+     * @return void
+     */
+    public function preventResetByRollback() {
+        if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
+            $this->testdbtransaction->allow_commit();
+            $this->testdbtransaction = null;
+        }
     }
 
     /**
@@ -882,10 +1313,10 @@ class advanced_testcase extends PHPUnit_Framework_TestCase {
      * Recursively visit all the files in the source tree. Calls the callback
      * function with the pathname of each file found.
      *
-     * @param $path the folder to start searching from.
-     * @param $callback the method of this class to call with the name of each file found.
-     * @param $fileregexp a regexp used to filter the search (optional).
-     * @param $exclude If true, pathnames that match the regexp will be ignored. If false,
+     * @param string $path the folder to start searching from.
+     * @param string $callback the method of this class to call with the name of each file found.
+     * @param string $fileregexp a regexp used to filter the search (optional).
+     * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
      *     only files that match the regexp will be included. (default false).
      * @param array&nb