diff --git a/admin/tool/phpunit/cli/init.bat b/admin/tool/phpunit/cli/init.bat deleted file mode 100644 index d57029f72f3..00000000000 --- a/admin/tool/phpunit/cli/init.bat +++ /dev/null @@ -1,22 +0,0 @@ -@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 index f091a9f8d36..ca00d564934 100644 --- a/admin/tool/phpunit/cli/init.php +++ b/admin/tool/phpunit/cli/init.php @@ -43,13 +43,13 @@ exec("php util.php --diag", $output, $code); if ($code == 0) { // everything is ready -} else if ($code == 132) { +} else if ($code == PHPUNIT_EXITCODE_INSTALL) { passthru("php util.php --install", $code); if ($code != 0) { exit($code); } -} else if ($code == 133) { +} else if ($code == PHPUNIT_EXITCODE_REINSTALL) { passthru("php util.php --drop", $code); passthru("php util.php --install", $code); if ($code != 0) { diff --git a/admin/tool/phpunit/cli/init.sh b/admin/tool/phpunit/cli/init.sh deleted file mode 100755 index b3616ee722c..00000000000 --- a/admin/tool/phpunit/cli/init.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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 diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index 0ab46c012d9..cced963ecf2 100644 --- a/admin/tool/phpunit/cli/util.php +++ b/admin/tool/phpunit/cli/util.php @@ -17,14 +17,7 @@ /** * PHPUnit related utilities. * - * Exit codes: - * 0 - success - * 1 - general 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 + * Exit codes: {@see phpunit_bootstrap_error()} * * @package tool_phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} @@ -72,12 +65,8 @@ if ($options['phpunitdir']) { } // 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 (!include_once('PHPUnit/Autoload.php')) { + phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING); } if ($options['run']) { @@ -147,7 +136,7 @@ if ($diag) { if (phpunit_util::build_config_file()) { exit(0); } else { - phpunit_bootstrap_error(134); + phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, 'Can not create phpunit.xml configuration file, verify dirroot permissions'); } } else if ($drop) { diff --git a/admin/tool/phpunit/webrunner.php b/admin/tool/phpunit/webrunner.php index ef5168534d3..f697f7ee085 100644 --- a/admin/tool/phpunit/webrunner.php +++ b/admin/tool/phpunit/webrunner.php @@ -67,7 +67,7 @@ if ($execute) { if ($code == 0) { // everything is ready - } else if ($code == 132) { + } else if ($code == PHPUNIT_EXITCODE_INSTALL) { tool_phpunit_header(); echo $OUTPUT->box_start('generalbox'); echo '
';
@@ -87,7 +87,7 @@ if ($execute) {
         echo $OUTPUT->footer();
         die();
 
-    } else if ($code == 133) {
+    } else if ($code == PHPUNIT_EXITCODE_REINSTALL) {
         tool_phpunit_header();
         echo $OUTPUT->box_start('generalbox');
         echo '
';
diff --git a/lib/grade/tests/fixtures/lib.php b/lib/grade/tests/fixtures/lib.php
index 7ca25207d7e..9ebc9b55841 100644
--- a/lib/grade/tests/fixtures/lib.php
+++ b/lib/grade/tests/fixtures/lib.php
@@ -262,6 +262,9 @@ class grade_base_testcase extends advanced_testcase {
     protected function load_grade_items() {
         global $DB;
 
+        // purge all items created by module generators
+        $DB->delete_records('grade_items', array('itemtype'=>'mod'));
+
         $course_category = grade_category::fetch_course_category($this->course->id);
 
         // id = 0
diff --git a/lib/phpunit/bootstrap.php b/lib/phpunit/bootstrap.php
index f6ac22f4bb9..dbf97018b26 100644
--- a/lib/phpunit/bootstrap.php
+++ b/lib/phpunit/bootstrap.php
@@ -18,13 +18,7 @@
  * Prepares PHPUnit environment, the phpunit.xml configuration
  * must specify this file as bootstrap.
  *
- * Exit codes:
- *  0   - success
- *  1   - general error
- *  130 - missing PHPUnit library error
- *  131 - configuration problem
- *  132 - install new test database
- *  133 - drop existing data before installing
+ * Exit codes: {@see phpunit_bootstrap_error()}
  *
  * @package    core
  * @category   phpunit
@@ -63,10 +57,14 @@ $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);
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITWRONG, $phpunitversion);
 }
 unset($phpunitversion);
 
+if (!include_once('PHPUnit/Extensions/Database/Autoload.php')) {
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITEXTMISSING, 'phpunit/DbUnit');
+}
+
 define('NO_OUTPUT_BUFFERING', true);
 
 // only load CFG from config.php, stop ASAP in lib/setup.php
@@ -93,16 +91,16 @@ if (isset($CFG->phpunit_directorypermissions)) {
 }
 $CFG->filepermissions = ($CFG->directorypermissions & 0666);
 if (!isset($CFG->phpunit_dataroot)) {
-    phpunit_bootstrap_error(131, 'Missing $CFG->phpunit_dataroot in config.php, can not run tests!');
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Missing $CFG->phpunit_dataroot in config.php, can not run tests!');
 }
 if (isset($CFG->dataroot) and $CFG->phpunit_dataroot === $CFG->dataroot) {
-    phpunit_bootstrap_error(131, '$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!');
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$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(131, '$CFG->phpunit_dataroot directory can not be created, can not run tests!');
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_dataroot directory can not be created, can not run tests!');
 }
 
 if (!is_writable($CFG->phpunit_dataroot)) {
@@ -115,7 +113,7 @@ if (!is_writable($CFG->phpunit_dataroot)) {
         }
     }
     if (!is_writable($CFG->phpunit_dataroot)) {
-        phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory is not writable, can not run tests!');
+        phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_dataroot directory is not writable, can not run tests!');
     }
 }
 if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
@@ -124,7 +122,7 @@ if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
             if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_Store') {
                 continue;
             }
-            phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?');
+            phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?');
         }
         closedir($dh);
         unset($dh);
@@ -137,13 +135,13 @@ if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
 
 // verify db prefix
 if (!isset($CFG->phpunit_prefix)) {
-    phpunit_bootstrap_error(131, 'Missing $CFG->phpunit_prefix in config.php, can not run tests!');
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Missing $CFG->phpunit_prefix in config.php, can not run tests!');
 }
 if ($CFG->phpunit_prefix === '') {
-    phpunit_bootstrap_error(131, '$CFG->phpunit_prefix can not be empty, can not run tests!');
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_prefix can not be empty, can not run tests!');
 }
 if (isset($CFG->prefix) and $CFG->prefix === $CFG->phpunit_prefix) {
-    phpunit_bootstrap_error(131, '$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!');
+    phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!');
 }
 
 // override CFG settings if necessary and throw away extra CFG settings
diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php
index d87c23f2898..3c191dc5370 100644
--- a/lib/phpunit/bootstraplib.php
+++ b/lib/phpunit/bootstraplib.php
@@ -25,6 +25,14 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+define('PHPUNIT_EXITCODE_PHPUNITMISSING', 129);
+define('PHPUNIT_EXITCODE_PHPUNITWRONG', 130);
+define('PHPUNIT_EXITCODE_PHPUNITEXTMISSING', 131);
+define('PHPUNIT_EXITCODE_CONFIGERROR', 135);
+define('PHPUNIT_EXITCODE_CONFIGWARNING', 136);
+define('PHPUNIT_EXITCODE_INSTALL', 140);
+define('PHPUNIT_EXITCODE_REINSTALL', 141);
+
 /**
  * Print error and stop execution
  * @param int $errorcode The exit error code
@@ -39,35 +47,35 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
         case 1:
             $text = 'Error: '.$text;
             break;
-        case 129:
+        case PHPUNIT_EXITCODE_PHPUNITMISSING:
+            $text = "Moodle can not find PHPUnit PEAR library";
+            break;
+        case PHPUNIT_EXITCODE_PHPUNITWRONG:
             $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';
+        case PHPUNIT_EXITCODE_PHPUNITEXTMISSING:
+            $text = 'Moodle can not find required PHPUnit extension '.$text;
             break;
-        case 131:
-            $text = 'Moodle configuration problem: '.$text;
+        case PHPUNIT_EXITCODE_CONFIGERROR:
+            $text = "Moodle PHPUnit environment configuration error:\n".$text;
             break;
-        case 132:
-            $text = "Moodle PHPUnit environment is not initialised, please use:\n php admin/tool/phpunit/cli/util.php --install";
+        case PHPUNIT_EXITCODE_CONFIGWARNING:
+            $text = "Moodle PHPUnit environment configuration warning:\n".$text;
             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";
+        case PHPUNIT_EXITCODE_INSTALL:
+            $text = "Moodle PHPUnit environment is not initialised, please use:\n php admin/tool/phpunit/cli/init.php";
             break;
-        case 134:
-            $text = 'Moodle can not create PHPUnit configuration file, please verify dirroot permissions';
+        case PHPUNIT_EXITCODE_REINSTALL:
+            $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php admin/tool/phpunit/cli/init.php";
             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");
-    }
+
+    // do not write to error stream because we need the error message in PHP exec result from web ui
+    echo($text."\n");
     exit($errorcode);
 }
 
diff --git a/lib/phpunit/generatorlib.php b/lib/phpunit/generatorlib.php
index 18766fe3e75..a4904a7e2c7 100644
--- a/lib/phpunit/generatorlib.php
+++ b/lib/phpunit/generatorlib.php
@@ -593,20 +593,20 @@ abstract class phpunit_module_generator {
 
     /**
      * Create course module and link it to course
-     * @param stdClass $instance
+     * @param int $courseid
      * @param array $options: section, visible
-     * @return stdClass $cm instance
+     * @return int $cm instance id
      */
-    protected function create_course_module(stdClass $instance, array $options) {
+    protected function precreate_course_module($courseid, array $options) {
         global $DB, $CFG;
         require_once("$CFG->dirroot/course/lib.php");
 
         $modulename = $this->get_modulename();
 
         $cm = new stdClass();
-        $cm->course             = $instance->course;
+        $cm->course             = $courseid;
         $cm->module             = $DB->get_field('modules', 'id', array('name'=>$modulename));
-        $cm->instance           = $instance->id;
+        $cm->instance           = 0;
         $cm->section            = isset($options['section']) ? $options['section'] : 0;
         $cm->idnumber           = isset($options['idnumber']) ? $options['idnumber'] : 0;
         $cm->added              = time();
@@ -627,11 +627,28 @@ abstract class phpunit_module_generator {
 
         add_mod_to_section($cm);
 
-        $cm = get_coursemodule_from_id($modulename, $cm->id, $cm->course, true, MUST_EXIST);
+        return $cm->id;
+    }
 
+    /**
+     * Called after *_add_instance()
+     * @param int $id
+     * @param int $cmid
+     * @return stdClass module instance
+     */
+    protected function post_add_instance($id, $cmid) {
+        global $DB;
+
+        $DB->set_field('course_modules', 'instance', $id, array('id'=>$cmid));
+
+        $instance = $DB->get_record($this->get_modulename(), array('id'=>$id), '*', MUST_EXIST);
+
+        $cm = get_coursemodule_from_id($this->get_modulename(), $cmid, $instance->course, true, MUST_EXIST);
         context_module::instance($cm->id);
 
-        return $cm;
+        $instance->cmid = $cm->id;
+
+        return $instance;
     }
 
     /**
diff --git a/lib/phpunit/lib.php b/lib/phpunit/lib.php
index 4a62f4c9fbc..c984a3c3118 100644
--- a/lib/phpunit/lib.php
+++ b/lib/phpunit/lib.php
@@ -36,34 +36,25 @@ require_once 'PHPUnit/Extensions/Database/Autoload.php';
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class phpunit_util {
-    /**
-     * @var array original content of all database tables
-     */
+    /** @var string current version hash from php files */
+    protected static $versionhash = null;
+
+    /** @var array original content of all database tables*/
     protected static $tabledata = null;
 
-    /**
-     * @var array original structure of all database tables
-     */
+    /** @var array original structure of all database tables */
     protected static $tablestructure = null;
 
-    /**
-     * @var array An array of original globals, restored after each test
-     */
+    /** @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
-     */
+    /** @var int last value of db writes counter, used for db resetting */
     public static $lastdbwrites = null;
 
-    /**
-     * @var phpunit_data_generator
-     */
+    /** @var phpunit_data_generator */
     protected static $generator = null;
 
-    /**
-     * @var resource used for prevention of parallel test execution
-     */
+    /** @var resource used for prevention of parallel test execution */
     protected static $lockhandle = null;
 
     /**
@@ -116,6 +107,30 @@ class phpunit_util {
         }
     }
 
+    /**
+     * Load global $CFG;
+     * @internal
+     * @static
+     * @return void
+     */
+    public static function initialise_cfg() {
+        global $DB;
+        $dbhash = false;
+        try {
+            $dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'));
+        } catch (Exception $e) {
+            // not installed yet
+            initialise_cfg();
+            return;
+        }
+        if ($dbhash !== phpunit_util::get_version_hash()) {
+            // do not set CFG - the only way forward is to drop and reinstall
+            return;
+        }
+        // standard CFG init
+        initialise_cfg();
+    }
+
     /**
      * Get data generator
      * @static
@@ -625,26 +640,31 @@ class phpunit_util {
 
         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');
+            return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
         }
 
         if (empty($tables)) {
-            return array(132, '');
+            return array(PHPUNIT_EXITCODE_INSTALL, '');
         }
 
         if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
-            return array(133, '');
+            return array(PHPUNIT_EXITCODE_REINSTALL, '');
         }
 
         if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
-            return array(133, '');
+            return array(PHPUNIT_EXITCODE_REINSTALL, '');
         }
 
         $hash = phpunit_util::get_version_hash();
         $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
 
         if ($hash !== $oldhash) {
-            return array(133, '');
+            return array(PHPUNIT_EXITCODE_REINSTALL, '');
+        }
+
+        $dbhash = get_config('core', 'phpunittest');
+        if ($hash !== $dbhash) {
+            return array(PHPUNIT_EXITCODE_REINSTALL, '');
         }
 
         return array(0, '');
@@ -662,7 +682,7 @@ class phpunit_util {
         global $DB, $CFG;
 
         if (!self::is_test_site()) {
-            phpunit_bootstrap_error(131, 'Can not drop non-test site!!');
+            phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
         }
 
         // purge dataroot
@@ -707,13 +727,13 @@ class phpunit_util {
         global $DB, $CFG;
 
         if (!self::is_test_site()) {
-            phpunit_bootstrap_error(131, 'Can not install on non-test site!!');
+            phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!');
         }
 
         if ($DB->get_tables()) {
             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');
+                phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
             } else {
                 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
             }
@@ -731,7 +751,8 @@ class phpunit_util {
         update_timezone_records($timezones);
 
         // add test db flag
-        set_config('phpunittest', 'phpunittest');
+        $hash = phpunit_util::get_version_hash();
+        set_config('phpunittest', $hash);
 
         // store data for all tables
         $data = array();
@@ -756,7 +777,6 @@ class phpunit_util {
         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();
         file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
         phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
     }
@@ -769,6 +789,10 @@ class phpunit_util {
     public static function get_version_hash() {
         global $CFG;
 
+        if (self::$versionhash) {
+            return self::$versionhash;
+        }
+
         $versions = array();
 
         // main version first
@@ -801,9 +825,9 @@ class phpunit_util {
             }
         }
 
-        $hash = sha1(serialize($versions));
+        self::$versionhash = sha1(serialize($versions));
 
-        return $hash;
+        return self::$versionhash;
     }
 
     /**
@@ -831,7 +855,7 @@ class phpunit_util {
                 if (!file_exists("$fullplug/tests/")) {
                     continue;
                 }
-                $dir = preg_replace("|$CFG->dirroot/|", '', $fullplug, 1);
+                $dir = substr($fullplug, strlen($CFG->dirroot)+1);
                 $dir .= '/tests';
                 $component = $type.'_'.$plug;
 
@@ -850,9 +874,12 @@ class phpunit_util {
                 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('|([^<]+)|', ''.$CFG->dirroot.'/$1', $data);
+        $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data);
+        $data = preg_replace('|([^<]+)|',
+            ''.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1',
+            $data);
         file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
         phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
 
diff --git a/lib/phpunit/readme.md b/lib/phpunit/readme.md
index 0cf0de37bb7..5af9c06286e 100644
--- a/lib/phpunit/readme.md
+++ b/lib/phpunit/readme.md
@@ -13,7 +13,7 @@ Installation
 1. install PEAR package manager - see [PEAR Manual](http://pear.php.net/manual/en/installation.php)
 2. install PHPUnit package and phpunit/DbUnit extension - see [PHPUnit installation documentation](http://www.phpunit.de/manual/current/en/installation.html)
 3. edit main config.php - add `$CFG->phpunit_prefix` and `$CFG->phpunit_dataroot` - see config-dist.php
-4. execute `admin/tool/phpunit/cli/init.sh` to initialise the test environemnt, repeat it after every upgrade or installation of plugins
+4. execute `php admin/tool/phpunit/cli/init.php` to initialise the test environemnt, repeat it after every upgrade or installation of plugins
 
 
 Test execution
@@ -29,7 +29,7 @@ How to add more tests?
 2. add `local/mytest/tests/my_test.php` file with `local_my_testcase` class that extends `basic_testcase` or `advanced_testcase`
 3. add some test_*() methods
 4. execute your new test case `phpunit local_my_testcase local/mytest/tests/my_test.php`
-5. execute `admin/tool/phpunit/cli/init.sh` to get the plugin tests included in main phpunit.xml configuration file
+5. execute `php admin/tool/phpunit/cli/init.php` to get the plugin tests included in main phpunit.xml configuration file
 
 
 How to convert existing tests?
diff --git a/lib/setup.php b/lib/setup.php
index c2bbb5aa0f6..c8b5626c923 100644
--- a/lib/setup.php
+++ b/lib/setup.php
@@ -479,7 +479,12 @@ if (isset($CFG->debug)) {
 }
 
 // Load up any configuration from the config table
-initialise_cfg();
+
+if (PHPUNIT_TEST) {
+    phpunit_util::initialise_cfg();
+} else {
+    initialise_cfg();
+}
 
 // Verify upgrade is not running unless we are in a script that needs to execute in any case
 if (!defined('NO_UPGRADE_CHECK') and isset($CFG->upgraderunning)) {
diff --git a/lib/tests/completionlib_test.php b/lib/tests/completionlib_test.php
new file mode 100644
index 00000000000..6e81cd710ec
--- /dev/null
+++ b/lib/tests/completionlib_test.php
@@ -0,0 +1,783 @@
+.
+
+/**
+ * Completion tests
+ *
+ * @package    core_completion
+ * @category   phpunit
+ * @copyright  2008 Sam Marshall
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir.'/completionlib.php');
+
+
+class completionlib_testcase extends basic_testcase {
+
+    var $realdb, $realcfg, $realsession, $realuser;
+
+    protected function setUp() {
+        global $DB, $CFG, $SESSION, $USER;
+        parent::setUp();
+
+        $this->realdb = $DB;
+        $this->realcfg = $CFG;
+        $this->realsession = $SESSION;
+        $this->prevuser = $USER;
+
+        $DB =  $this->getMock(get_class($DB));
+        $CFG = clone($this->realcfg);
+        $CFG->prefix = 'test_';
+        $CFG->enablecompletion = COMPLETION_ENABLED;
+        $SESSION = new stdClass();
+        $USER = (object)array('id' =>314159);
+    }
+
+    protected function tearDown() {
+        global $DB,$CFG,$SESSION,$USER;
+        $DB = $this->realdb;
+        $CFG = $this->realcfg;
+        $SESSION = $this->realsession;
+        $USER = $this->prevuser;
+
+        parent::tearDown();
+    }
+
+    function test_is_enabled() {
+        global $CFG;
+
+        // Config alone
+        $CFG->enablecompletion = COMPLETION_DISABLED;
+        $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
+        $CFG->enablecompletion = COMPLETION_ENABLED;
+        $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
+
+        // Course
+        $course = (object)array('id' =>13);
+        $c = new completion_info($course);
+        $course->enablecompletion = COMPLETION_DISABLED;
+        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
+        $course->enablecompletion = COMPLETION_ENABLED;
+        $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
+        $CFG->enablecompletion = COMPLETION_DISABLED;
+        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
+
+        // Course and CM
+        $cm = new stdClass();
+        $cm->completion = COMPLETION_TRACKING_MANUAL;
+        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
+        $CFG->enablecompletion = COMPLETION_ENABLED;
+        $course->enablecompletion = COMPLETION_DISABLED;
+        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
+        $course->enablecompletion = COMPLETION_ENABLED;
+        $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
+        $cm->completion = COMPLETION_TRACKING_NONE;
+        $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
+        $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
+        $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
+    }
+
+    function test_update_state() {
+
+        $c = $this->getMock('completion_info', array('is_enabled','get_data','internal_get_state','internal_set_data'), array((object)array('id'=>42)));
+        $cm = (object)array('id'=>13, 'course'=>42);
+
+        // Not enabled, should do nothing
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(false));
+        $c->update_state($cm);
+
+        // Enabled, but current state is same as possible result, do nothing
+        $current = (object)array('completionstate'=>COMPLETION_COMPLETE);
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, false, 0)
+            ->will($this->returnValue($current));
+        $c->update_state($cm, COMPLETION_COMPLETE);
+
+        // Enabled, but current state is a specific one and new state is just
+        // complete, so do nothing
+        $current->completionstate = COMPLETION_COMPLETE_PASS;
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, false, 0)
+            ->will($this->returnValue($current));
+        $c->update_state($cm, COMPLETION_COMPLETE);
+
+        // Manual, change state (no change)
+        $cm = (object)array('id'=>13,'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
+        $current->completionstate=COMPLETION_COMPLETE;
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, false, 0)
+            ->will($this->returnValue($current));
+        $c->update_state($cm, COMPLETION_COMPLETE);
+
+        // Manual, change state (change)
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, false, 0)
+            ->will($this->returnValue($current));
+        $changed = clone($current);
+        $changed->timemodified = time();
+        $changed->completionstate = COMPLETION_INCOMPLETE;
+        $c->expects($this->at(2))
+            ->method('internal_set_data')
+            ->with($cm, $changed);
+        $c->update_state($cm, COMPLETION_INCOMPLETE);
+
+        // Auto, change state
+        $cm = (object)array('id'=>13,'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
+        $current = (object)array('completionstate'=>COMPLETION_COMPLETE);
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, false, 0)
+            ->will($this->returnValue($current));
+        $c->expects($this->at(2))
+            ->method('internal_get_state')
+            ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
+        $changed = clone($current);
+        $changed->timemodified = time();
+        $changed->completionstate = COMPLETION_COMPLETE_PASS;
+        $c->expects($this->at(3))
+            ->method('internal_set_data')
+            ->with($cm, $changed);
+        $c->update_state($cm, COMPLETION_COMPLETE_PASS);
+    }
+
+    function test_internal_get_state() {
+        global $DB;
+
+        $c = $this->getMock('completion_info', array('internal_get_grade_state'), array((object)array('id'=>42)));
+        $cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null);
+
+        // If view is required, but they haven't viewed it yet
+        $cm->completionview = COMPLETION_VIEW_REQUIRED;
+        $current = (object)array('viewed'=>COMPLETION_NOT_VIEWED);
+        $this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current));
+
+        // OK set view not required
+        $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
+
+        // Test not getting module name
+        $cm->modname='label';
+        $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
+
+        // Test getting module name
+        $cm->module = 13;
+        unset($cm->modname);
+        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
+        $DB->expects($this->once())
+            ->method('get_field')
+            ->with('modules', 'name', array('id'=>13))
+            ->will($this->returnValue('lable'));
+        $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
+
+        // Note: This function is not fully tested (including kind of the main
+        // part) because:
+        // * the grade_item/grade_grade calls are static and can't be mocked
+        // * the plugin_supports call is static and can't be mocked
+    }
+
+    function test_set_module_viewed() {
+
+        $c = $this->getMock('completion_info',
+            array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
+            array((object)array('id'=>42)));
+        $cm = (object)array('id'=>13, 'course'=>42);
+
+        // Not tracking completion, should do nothing
+        $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
+        $c->set_module_viewed($cm);
+
+        // Tracking completion but completion is disabled, should do nothing
+        $cm->completionview = COMPLETION_VIEW_REQUIRED;
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(false));
+        $c->set_module_viewed($cm);
+
+        // Now it's enabled, we expect it to get data. If data already has
+        // viewed, still do nothing
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, 0)
+            ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
+        $c->set_module_viewed($cm);
+
+        // OK finally one that hasn't been viewed, now it should set it viewed
+        // and update state
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('get_data')
+            ->with($cm, 1337)
+            ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
+        $c->expects($this->at(2))
+            ->method('internal_set_data')
+            ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
+        $c->expects($this->at(3))
+            ->method('update_state')
+            ->with($cm, COMPLETION_COMPLETE, 1337);
+        $c->set_module_viewed($cm, 1337);
+    }
+
+    function test_count_user_data() {
+        global $DB;
+
+        $course = (object)array('id'=>13);
+        $cm = (object)array('id'=>42);
+
+        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
+        $DB->expects($this->at(0))
+            ->method('get_field_sql')
+            ->will($this->returnValue(666));
+
+/*
+        $DB->expectOnce('get_field_sql',array(new IgnoreWhitespaceExpectation("SELECT
+    COUNT(1)
+FROM
+    {course_modules_completion}
+WHERE
+    coursemoduleid=? AND completionstate<>0"),array(42)));
+*/
+
+        $c = new completion_info($course);
+        $this->assertEquals(666, $c->count_user_data($cm));
+    }
+
+    function test_delete_all_state() {
+        global $DB, $SESSION;
+
+        $course = (object)array('id'=>13);
+        $cm = (object)array('id'=>42,'course'=>13);
+        $c = new completion_info($course);
+
+        // Check it works ok without data in session
+        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
+        $DB->expects($this->at(0))
+            ->method('delete_records')
+            ->with('course_modules_completion', array('coursemoduleid'=>42))
+            ->will($this->returnValue(true));
+        $c->delete_all_state($cm);
+
+        // Build up a session to check it deletes the right bits from it
+        // (and not other bits)
+        $SESSION->completioncache=array();
+        $SESSION->completioncache[13]=array();
+        $SESSION->completioncache[13][42]='foo';
+        $SESSION->completioncache[13][43]='foo';
+        $SESSION->completioncache[14]=array();
+        $SESSION->completioncache[14][42]='foo';
+        $DB->expects($this->at(0))
+            ->method('delete_records')
+            ->with('course_modules_completion', array('coursemoduleid'=>42))
+            ->will($this->returnValue(true));
+        $c->delete_all_state($cm);
+        $this->assertEquals(array(13=>array(43=>'foo'), 14=>array(42=>'foo')), $SESSION->completioncache);
+    }
+
+    function test_reset_all_state() {
+        global $DB;
+
+        $c = $this->getMock('completion_info',
+            array('delete_all_state', 'get_tracked_users','update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
+            array((object)array('id'=>42)));
+
+        $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
+
+        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
+        $DB->expects($this->at(0))
+            ->method('get_recordset')
+            ->will($this->returnValue(
+                new completion_test_fake_recordset(array((object)array('id'=>1, 'userid'=>100),(object)array('id'=>2, 'userid'=>101)))));
+
+        $c->expects($this->at(0))
+            ->method('delete_all_state')
+            ->with($cm);
+
+        $c->expects($this->at(1))
+            ->method('get_tracked_users')
+            ->will($this->returnValue(array(
+            (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
+            (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'))));
+
+        $c->expects($this->at(2))
+            ->method('update_state')
+            ->with($cm,COMPLETION_UNKNOWN, 100);
+        $c->expects($this->at(3))
+            ->method('update_state')
+            ->with($cm,COMPLETION_UNKNOWN, 101);
+        $c->expects($this->at(4))
+            ->method('update_state')
+            ->with($cm,COMPLETION_UNKNOWN, 201);
+
+        $c->reset_all_state($cm);
+    }
+
+    function test_get_data() {
+        global $DB, $SESSION;
+
+        $c = new completion_info((object)array('id'=>42));
+        $cm = (object)array('id'=>13, 'course'=>42);
+
+        // 1. Not current user, record exists
+        $sillyrecord = (object)array('frog'=>'kermit');
+
+        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
+        $DB->expects($this->at(0))
+            ->method('get_record')
+            ->with('course_modules_completion', array('coursemoduleid'=>13,'userid'=>123))
+            ->will($this->returnValue($sillyrecord));
+        $result = $c->get_data($cm,false,123);
+        $this->assertEquals($sillyrecord, $result);
+        $this->assertTrue(empty($SESSION->completioncache));
+
+        // 2. Not current user, default record, wholecourse (ignored)
+        $DB->expects($this->at(0))
+            ->method('get_record')
+            ->with('course_modules_completion', array('coursemoduleid'=>13,'userid'=>123))
+            ->will($this->returnValue(false));
+        $result=$c->get_data($cm,true,123);
+        $this->assertEquals((object)array(
+            'id'=>'0','coursemoduleid'=>13,'userid'=>123,'completionstate'=>0,
+            'viewed'=>0,'timemodified'=>0),$result);
+        $this->assertTrue(empty($SESSION->completioncache));
+
+        // 3. Current user, single record, not from cache
+        $DB->expects($this->at(0))
+            ->method('get_record')
+            ->with('course_modules_completion', array('coursemoduleid'=>13,'userid'=>314159))
+            ->will($this->returnValue($sillyrecord));
+        $result = $c->get_data($cm);
+        $this->assertEquals($sillyrecord, $result);
+        $this->assertEquals($sillyrecord, $SESSION->completioncache[42][13]);
+        // When checking time(), allow for second overlaps
+        $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
+
+        // 4. Current user, 'whole course', but from cache
+        $result = $c->get_data($cm, true);
+        $this->assertEquals($sillyrecord, $result);
+
+        // 5. Current user, single record, cache expired
+        $SESSION->completioncache[42]['updated']=37; // Quite a long time ago
+        $now = time();
+        $SESSION->completioncache[17]['updated']=$now;
+        $SESSION->completioncache[39]['updated']=72; // Also a long time ago
+        $DB->expects($this->at(0))
+            ->method('get_record')
+            ->with('course_modules_completion', array('coursemoduleid'=>13,'userid'=>314159))
+            ->will($this->returnValue($sillyrecord));
+        $result = $c->get_data($cm, false);
+        $this->assertEquals($sillyrecord, $result);
+
+        // Check that updated value is right, then fudge it to make next compare
+        // work
+        $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
+        $SESSION->completioncache[42]['updated']=$now;
+        // Check things got expired from cache
+        $this->assertEquals(array(42=>array(13=>$sillyrecord, 'updated'=>$now), 17=>array('updated'=>$now)), $SESSION->completioncache);
+
+        // 6. Current user, 'whole course' and record not in cache
+        unset($SESSION->completioncache);
+
+        // Scenario: Completion data exists for one CMid
+        $basicrecord = (object)array('coursemoduleid'=>13);
+        $DB->expects($this->at(0))
+            ->method('get_records_sql')
+            ->will($this->returnValue(array('1'=>$basicrecord)));
+
+/*
+        $DB->expectAt(0,'get_records_sql',array(new IgnoreWhitespaceExpectation("
+SELECT
+    cmc.*
+FROM
+    {course_modules} cm
+    INNER JOIN {course_modules_completion} cmc ON cmc.coursemoduleid=cm.id
+WHERE
+    cm.course=? AND cmc.userid=?"),array(42,314159)));
+*/
+        // There are two CMids in total, the one we had data for and another one
+        $modinfo = new stdClass();
+        $modinfo->cms = array((object)array('id'=>13), (object)array('id'=>14));
+        $result = $c->get_data($cm, true, 0, $modinfo);
+
+        // Check result
+        $this->assertEquals($basicrecord, $result);
+
+        // Check the cache contents
+        $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
+        $SESSION->completioncache[42]['updated'] = $now;
+        $this->assertEquals(array(42=>array(13=>$basicrecord, 14=>(object)array(
+            'id'=>'0', 'coursemoduleid'=>14, 'userid'=>314159, 'completionstate'=>0,
+            'viewed'=>0, 'timemodified'=>0), 'updated'=>$now)), $SESSION->completioncache);
+    }
+
+    function test_internal_set_data() {
+        global $DB, $SESSION;
+
+        $cm = (object)array('course' => 42,'id' => 13);
+        $c = new completion_info((object)array('id' => 42));
+
+        // 1) Test with new data
+        $data = (object)array('id'=>0, 'userid' => 314159, 'coursemoduleid' => 99);
+        $DB->expects($this->at(0))
+            ->method('start_delegated_transaction')
+            ->will($this->returnValue($this->getMock('moodle_transaction', array(), array($DB))));
+
+        $DB->expects($this->at(1))
+            ->method('get_field')
+            ->with('course_modules_completion', 'id', array('coursemoduleid'=>99, 'userid'=>314159))
+            ->will($this->returnValue(false));
+
+        $DB->expects($this->at(2))
+            ->method('insert_record')
+            ->will($this->returnValue(4));
+
+        $c->internal_set_data($cm, $data);
+        $this->assertEquals(4, $data->id);
+        $this->assertEquals(array(42 => array(13 => $data)), $SESSION->completioncache);
+
+        // 2) Test with existing data and for different user (not cached)
+        unset($SESSION->completioncache);
+        $d2 = (object)array('id' => 7, 'userid' => 17, 'coursemoduleid' => 66);
+        $DB->expects($this->at(0))
+            ->method('start_delegated_transaction')
+            ->will($this->returnValue($this->getMock('moodle_transaction', array(), array($DB))));
+        $DB->expects($this->at(1))
+            ->method('update_record')
+            ->with('course_modules_completion', $d2);
+        $c->internal_set_data($cm, $d2);
+        $this->assertFalse(isset($SESSION->completioncache));
+
+        // 3) Test where it THINKS the data is new (from cache) but actually
+        // in the database it has been set since
+        // 1) Test with new data
+        $data = (object)array('id'=>0, 'userid' => 314159, 'coursemoduleid' => 99);
+        $d3 = (object)array('id' => 13, 'userid' => 314159, 'coursemoduleid' => 99);
+        $DB->expects($this->at(0))
+            ->method('start_delegated_transaction')
+            ->will($this->returnValue($this->getMock('moodle_transaction', array(), array($DB))));
+        $DB->expects($this->at(1))
+            ->method('get_field')
+            ->with('course_modules_completion', 'id', array('coursemoduleid' => 99, 'userid' => 314159))
+            ->will($this->returnValue(13));
+        $DB->expects($this->at(2))
+            ->method('update_record')
+            ->with('course_modules_completion', $d3);
+        $c->internal_set_data($cm, $data);
+    }
+
+    function test_get_activities() {
+        global $DB;
+
+        $c = new completion_info((object)array('id'=>42));
+
+        // Try with no activities
+        $DB->expects($this->at(0))
+            ->method('get_records_select')
+            ->with('course_modules', 'course=42 AND completion<>'.COMPLETION_TRACKING_NONE)
+            ->will($this->returnValue(array()));
+        $result = $c->get_activities();
+        $this->assertEquals(array(), $result);
+
+        // Try with an activity (need to fake up modinfo for it as well)
+        $DB->expects($this->at(0))
+            ->method('get_records_select')
+            ->with('course_modules', 'course=42 AND completion<>'.COMPLETION_TRACKING_NONE)
+            ->will($this->returnValue(array(13=>(object)array('id'=>13))));
+        $modinfo = new stdClass;
+        $modinfo->sections = array(array(1, 2, 3), array(12, 13, 14));
+        $modinfo->cms[13] = (object)array('modname'=>'frog', 'name'=>'kermit');
+        $result = $c->get_activities($modinfo);
+        $this->assertEquals(array(13=>(object)array('id'=>13, 'modname'=>'frog', 'name'=>'kermit')), $result);
+    }
+
+    // get_tracked_users() cannot easily be tested because it uses
+    // get_role_users, so skipping that
+
+    function test_get_progress_all() {
+        global $DB;
+
+        $c = $this->getMock('completion_info',
+            array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
+            array((object)array('id'=>42)));
+
+        // 1) Basic usage
+        $c->expects($this->at(0))
+            ->method('get_tracked_users')
+            ->with(false,  array(),  0,  '',  '',  '',  null)
+            ->will($this->returnValue(array(
+                (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
+                (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
+        $DB->expects($this->at(0))
+            ->method('get_in_or_equal')
+            ->with(array(100, 201))
+            ->will($this->returnValue(array(' IN (100, 201)', array())));
+        $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
+        $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
+        $DB->expects($this->at(1))
+            ->method('get_recordset_sql')
+            ->will($this->returnValue(new completion_test_fake_recordset(array($progress1, $progress2))));
+
+/*
+        $DB->expectAt(0, 'get_recordset_sql', array(new IgnoreWhitespaceExpectation("
+SELECT
+    cmc.*
+FROM
+    {course_modules} cm
+    INNER JOIN {course_modules_completion} cmc ON cm.id = cmc.coursemoduleid
+WHERE
+    cm.course = ? AND cmc.userid IN (100, 201)"), array(42)));
+*/
+
+        $this->assertEquals(array(
+                100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
+                    'progress'=>array(13=>$progress1)),
+                201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
+                    'progress'=>array(14=>$progress2)),
+            ), $c->get_progress_all(false));
+
+        // 2) With more than 1, 000 results
+        $tracked = array();
+        $ids = array();
+        $progress = array();
+        for($i = 100;$i<2000;$i++) {
+            $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
+            $ids[] = $i;
+            $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
+            $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
+        }
+        $c->expects($this->at(0))
+            ->method('get_tracked_users')
+            ->with(true,  3,  0,  '',  '',  '',  null)
+            ->will($this->returnValue($tracked));
+        $DB->expects($this->at(0))
+            ->method('get_in_or_equal')
+            ->with(array_slice($ids, 0, 1000))
+            ->will($this->returnValue(array(' IN whatever', array())));
+        $DB->expects($this->at(1))
+            ->method('get_recordset_sql')
+            ->will($this->returnValue(new completion_test_fake_recordset(array_slice($progress, 0, 1000))));
+
+/*
+        $DB->expectAt(1, 'get_recordset_sql', array(new IgnoreWhitespaceExpectation("
+SELECT
+    cmc.*
+FROM
+    {course_modules} cm
+    INNER JOIN {course_modules_completion} cmc ON cm.id = cmc.coursemoduleid
+WHERE
+    cm.course = ? AND cmc.userid IN whatever"), array(42)));
+*/
+
+        $DB->expects($this->at(2))
+            ->method('get_in_or_equal')
+            ->with(array_slice($ids, 1000))
+            ->will($this->returnValue(array(' IN whatever2', array())));
+        $DB->expects($this->at(3))
+            ->method('get_recordset_sql')
+            ->will($this->returnValue(new completion_test_fake_recordset(array_slice($progress, 1000))));
+
+        $result = $c->get_progress_all(true, 3);
+        $resultok = true;
+        $resultok  =  $resultok && ($ids == array_keys($result));
+
+        foreach($result as $userid => $data) {
+            $resultok  =  $resultok && $data->firstname == 'frog';
+            $resultok  =  $resultok && $data->lastname == $userid;
+            $resultok  =  $resultok && $data->id == $userid;
+            $cms = $data->progress;
+            $resultok =  $resultok && (array(13, 14) == array_keys($cms));
+            $resultok =  $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
+            $resultok =  $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
+        }
+        $this->assertTrue($resultok);
+    }
+
+    function test_inform_grade_changed() {
+        $c = $this->getMock('completion_info',
+            array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
+            array((object)array('id'=>42)));
+
+        $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
+        $item = (object)array('itemnumber'=>3,  'gradepass'=>1,  'hidden'=>0);
+        $grade = (object)array('userid'=>31337,  'finalgrade'=>0,  'rawgrade'=>0);
+
+        // Not enabled (should do nothing)
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(false));
+        $c->inform_grade_changed($cm, $item, $grade, false);
+
+        // Enabled but still no grade completion required,  should still do nothing
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->inform_grade_changed($cm, $item, $grade, false);
+
+        // Enabled and completion required but item number is wrong,  does nothing
+        $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->inform_grade_changed($cm, $item, $grade, false);
+
+        // Enabled and completion required and item number right. It is supposed
+        // to call update_state with the new potential state being obtained from
+        // internal_get_grade_state.
+        $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
+        $grade = (object)array('userid'=>31337,  'finalgrade'=>1,  'rawgrade'=>0);
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('update_state')
+            ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
+            ->will($this->returnValue(true));
+        $c->inform_grade_changed($cm, $item, $grade, false);
+
+        // Same as above but marked deleted. It is supposed to call update_state
+        // with new potential state being COMPLETION_INCOMPLETE
+        $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
+        $grade = (object)array('userid'=>31337,  'finalgrade'=>1,  'rawgrade'=>0);
+        $c->expects($this->at(0))
+            ->method('is_enabled')
+            ->with($cm)
+            ->will($this->returnValue(true));
+        $c->expects($this->at(1))
+            ->method('update_state')
+            ->with($cm, COMPLETION_INCOMPLETE, 31337)
+            ->will($this->returnValue(true));
+        $c->inform_grade_changed($cm, $item, $grade, true);
+    }
+
+    function test_internal_get_grade_state() {
+        $item = new stdClass;
+        $grade = new stdClass;
+
+        $item->gradepass = 4;
+        $item->hidden = 0;
+        $grade->rawgrade = 4.0;
+        $grade->finalgrade = null;
+
+        // Grade has pass mark and is not hidden,  user passes
+        $this->assertEquals(
+            COMPLETION_COMPLETE_PASS,
+            completion_info::internal_get_grade_state($item, $grade));
+
+        // Same but user fails
+        $grade->rawgrade = 3.9;
+        $this->assertEquals(
+            COMPLETION_COMPLETE_FAIL,
+            completion_info::internal_get_grade_state($item, $grade));
+
+        // User fails on raw grade but passes on final
+        $grade->finalgrade = 4.0;
+        $this->assertEquals(
+            COMPLETION_COMPLETE_PASS,
+            completion_info::internal_get_grade_state($item, $grade));
+
+        // Item is hidden
+        $item->hidden = 1;
+        $this->assertEquals(
+            COMPLETION_COMPLETE,
+            completion_info::internal_get_grade_state($item, $grade));
+
+        // Item isn't hidden but has no pass mark
+        $item->hidden = 0;
+        $item->gradepass = 0;
+        $this->assertEquals(
+            COMPLETION_COMPLETE,
+            completion_info::internal_get_grade_state($item, $grade));
+    }
+}
+
+
+class completion_test_fake_recordset implements Iterator {
+    var $closed;
+    var $values, $index;
+
+    function __construct($values) {
+        $this->values = $values;
+        $this->index = 0;
+    }
+
+    function current() {
+        return $this->values[$this->index];
+    }
+
+    function key() {
+        return $this->values[$this->index];
+    }
+
+    function next() {
+        $this->index++;
+    }
+
+    function rewind() {
+        $this->index = 0;
+    }
+
+    function valid() {
+        return count($this->values) > $this->index;
+    }
+
+    function close() {
+        $this->closed = true;
+    }
+
+    function was_closed() {
+        return $this->closed;
+    }
+}
diff --git a/mod/assignment/lib.php b/mod/assignment/lib.php
index 12fcf56cf6d..a6c145eb03d 100644
--- a/mod/assignment/lib.php
+++ b/mod/assignment/lib.php
@@ -2708,8 +2708,12 @@ function assignment_update_instance($assignment){
  * Adds an assignment instance
  *
  * This is done by calling the add_instance() method of the assignment type class
+ *
+ * @param stdClass $assignment
+ * @param mod_assignment_mod_form $mform
+ * @return int intance id
  */
-function assignment_add_instance($assignment) {
+function assignment_add_instance($assignment, $mform = null) {
     global $CFG;
 
     $assignment->assignmenttype = clean_param($assignment->assignmenttype, PARAM_PLUGIN);
diff --git a/mod/assignment/tests/generator/lib.php b/mod/assignment/tests/generator/lib.php
index 353d6f92043..ea07132582a 100644
--- a/mod/assignment/tests/generator/lib.php
+++ b/mod/assignment/tests/generator/lib.php
@@ -39,11 +39,11 @@ class mod_assignment_generator extends phpunit_module_generator {
     /**
      * Create new assignment module instance
      * @param array|stdClass $record
-     * @param array $options
+     * @param array $options (mostly course_module properties)
      * @return stdClass activity record with extra cmid field
      */
     public function create_instance($record = null, array $options = null) {
-        global $DB, $CFG;
+        global $CFG;
         require_once("$CFG->dirroot/mod/assignment/locallib.php");
 
         $this->instancecount++;
@@ -52,6 +52,9 @@ class mod_assignment_generator extends phpunit_module_generator {
         $record = (object)(array)$record;
         $options = (array)$options;
 
+        if (empty($record->course)) {
+            throw new coding_exception('module generator requires $record->course');
+        }
         if (!isset($record->name)) {
             $record->name = get_string('pluginname', 'assignment').' '.$i;
         }
@@ -67,15 +70,17 @@ class mod_assignment_generator extends phpunit_module_generator {
         if (!isset($record->grade)) {
             $record->grade = 100;
         }
-        $record->timemodified = time();
+        if (!isset($record->timedue)) {
+            $record->timedue = 0;
+        }
+        if (isset($options['idnumber'])) {
+            $record->cmidnumber = $options['idnumber'];
+        } else {
+            $record->cmidnumber = '';
+        }
 
-        $id = $DB->insert_record('assignment', $record);
-        $instance = $DB->get_record('assignment', array('id'=>$id), '*', MUST_EXIST);
-
-        $cm = $this->create_course_module($instance, $options);
-
-        $instance->cmid = $cm->id;
-
-        return $instance;
+        $record->coursemodule = $this->precreate_course_module($record->course, $options);
+        $id = assignment_add_instance($record, null);
+        return $this->post_add_instance($id, $record->coursemodule);
     }
 }
diff --git a/mod/assignment/tests/generator_test.php b/mod/assignment/tests/generator_test.php
index 635425ffa4d..69e14561793 100644
--- a/mod/assignment/tests/generator_test.php
+++ b/mod/assignment/tests/generator_test.php
@@ -36,28 +36,42 @@ defined('MOODLE_INTERNAL') || die();
  */
 class mod_assignment_generator_testcase extends advanced_testcase {
     public function test_generator() {
-        global $DB, $SITE;
+        global $DB;
 
         $this->resetAfterTest(true);
 
         $this->assertEquals(0, $DB->count_records('assignment'));
 
+        $course = $this->getDataGenerator()->create_course();
+
         /** @var mod_assignment_generator $generator */
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_assignment');
         $this->assertInstanceOf('mod_assignment_generator', $generator);
         $this->assertEquals('assignment', $generator->get_modulename());
 
-        $generator->create_instance(array('course'=>$SITE->id));
-        $generator->create_instance(array('course'=>$SITE->id));
-        $assignment = $generator->create_instance(array('course'=>$SITE->id));
+        $generator->create_instance(array('course'=>$course->id, 'grade'=>0));
+        $generator->create_instance(array('course'=>$course->id, 'grade'=>0));
+        $assignment = $generator->create_instance(array('course'=>$course->id, 'grade'=>100));
         $this->assertEquals(3, $DB->count_records('assignment'));
 
         $cm = get_coursemodule_from_instance('assignment', $assignment->id);
         $this->assertEquals($assignment->id, $cm->instance);
         $this->assertEquals('assignment', $cm->modname);
-        $this->assertEquals($SITE->id, $cm->course);
+        $this->assertEquals($course->id, $cm->course);
 
         $context = context_module::instance($cm->id);
         $this->assertEquals($assignment->cmid, $context->instanceid);
+
+        // test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE!
+        $gitem = $DB->get_record('grade_items', array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule'=>'assignment', 'iteminstance'=>$assignment->id));
+        $this->assertNotEmpty($gitem);
+        $this->assertEquals(100, $gitem->grademax);
+        $this->assertEquals(0, $gitem->grademin);
+        $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
+
+        // test eventslib integration
+        $this->setUser(2); // admin
+        $generator->create_instance(array('course'=>$course->id, 'timedue'=>(time()+60*60+24)));
+        $this->setUser(0);
     }
 }
diff --git a/mod/data/lib.php b/mod/data/lib.php
index cf11a113f61..e6737d9bec8 100644
--- a/mod/data/lib.php
+++ b/mod/data/lib.php
@@ -829,11 +829,11 @@ function data_tags_check($dataid, $template) {
 /**
  * Adds an instance of a data
  *
- * @global object
- * @param object $data
- * @return $int
+ * @param stdClass $data
+ * @param mod_data_mod_form $mform
+ * @return int intance id
  */
-function data_add_instance($data) {
+function data_add_instance($data, $mform = null) {
     global $DB;
 
     if (empty($data->assessed)) {
diff --git a/mod/data/tests/generator/lib.php b/mod/data/tests/generator/lib.php
index d8ce072a062..4ff538d0b2c 100644
--- a/mod/data/tests/generator/lib.php
+++ b/mod/data/tests/generator/lib.php
@@ -39,11 +39,12 @@ class mod_data_generator extends phpunit_module_generator {
     /**
      * Create new data module instance
      * @param array|stdClass $record
-     * @param array $options
+     * @param array $options (mostly course_module properties)
      * @return stdClass activity record with extra cmid field
      */
     public function create_instance($record = null, array $options = null) {
-        global $DB;
+        global $CFG;
+        require_once("$CFG->dirroot/mod/data/locallib.php");
 
         $this->instancecount++;
         $i = $this->instancecount;
@@ -51,6 +52,9 @@ class mod_data_generator extends phpunit_module_generator {
         $record = (object)(array)$record;
         $options = (array)$options;
 
+        if (empty($record->course)) {
+            throw new coding_exception('module generator requires $record->course');
+        }
         if (!isset($record->name)) {
             $record->name = get_string('pluginname', 'data').' '.$i;
         }
@@ -60,15 +64,20 @@ class mod_data_generator extends phpunit_module_generator {
         if (!isset($record->introformat)) {
             $record->introformat = FORMAT_MOODLE;
         }
-        $record->timemodified = time();
+        if (!isset($record->assessed)) {
+            $record->assessed = 0;
+        }
+        if (!isset($record->scale)) {
+            $record->scale = 0;
+        }
+        if (isset($options['idnumber'])) {
+            $record->cmidnumber = $options['idnumber'];
+        } else {
+            $record->cmidnumber = '';
+        }
 
-        $id = $DB->insert_record('data', $record);
-        $instance = $DB->get_record('data', array('id'=>$id), '*', MUST_EXIST);
-
-        $cm = $this->create_course_module($instance, $options);
-
-        $instance->cmid = $cm->id;
-
-        return $instance;
+        $record->coursemodule = $this->precreate_course_module($record->course, $options);
+        $id = data_add_instance($record, null);
+        return $this->post_add_instance($id, $record->coursemodule);
     }
 }
diff --git a/mod/data/tests/generator_test.php b/mod/data/tests/generator_test.php
index 46ac4f86af7..a10d11a0ad5 100644
--- a/mod/data/tests/generator_test.php
+++ b/mod/data/tests/generator_test.php
@@ -36,28 +36,39 @@ defined('MOODLE_INTERNAL') || die();
  */
 class mod_data_generator_testcase extends advanced_testcase {
     public function test_generator() {
-        global $DB, $SITE;
+        global $DB;
 
         $this->resetAfterTest(true);
 
         $this->assertEquals(0, $DB->count_records('data'));
 
+        $course = $this->getDataGenerator()->create_course();
+
         /** @var mod_data_generator $generator */
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
         $this->assertInstanceOf('mod_data_generator', $generator);
         $this->assertEquals('data', $generator->get_modulename());
 
-        $generator->create_instance(array('course'=>$SITE->id));
-        $generator->create_instance(array('course'=>$SITE->id));
-        $data = $generator->create_instance(array('course'=>$SITE->id));
+        $generator->create_instance(array('course'=>$course->id));
+        $generator->create_instance(array('course'=>$course->id));
+        $data = $generator->create_instance(array('course'=>$course->id));
         $this->assertEquals(3, $DB->count_records('data'));
 
         $cm = get_coursemodule_from_instance('data', $data->id);
         $this->assertEquals($data->id, $cm->instance);
         $this->assertEquals('data', $cm->modname);
-        $this->assertEquals($SITE->id, $cm->course);
+        $this->assertEquals($course->id, $cm->course);
 
         $context = context_module::instance($cm->id);
         $this->assertEquals($data->cmid, $context->instanceid);
+
+        // test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE!
+        $data = $generator->create_instance(array('course'=>$course->id, 'assessed'=>1, 'scale'=>100));
+        $gitem = $DB->get_record('grade_items', array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule'=>'data', 'iteminstance'=>$data->id));
+        $this->assertNotEmpty($gitem);
+        $this->assertEquals(100, $gitem->grademax);
+        $this->assertEquals(0, $gitem->grademin);
+        $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
+
     }
 }
diff --git a/mod/forum/lib.php b/mod/forum/lib.php
index eb78a1474bf..96fe4781eb5 100644
--- a/mod/forum/lib.php
+++ b/mod/forum/lib.php
@@ -52,12 +52,11 @@ define('FORUM_TRACKING_ON', 2);
  * will create a new instance and return the id number
  * of the new instance.
  *
- * @global object
- * @global object
- * @param object $forum add forum instance (with magic quotes)
+ * @param stdClass $forum add forum instance
+ * @param mod_forum_mod_form $mform
  * @return int intance id
  */
-function forum_add_instance($forum, $mform) {
+function forum_add_instance($forum, $mform = null) {
     global $CFG, $DB;
 
     $forum->timemodified = time();
diff --git a/mod/forum/tests/generator/lib.php b/mod/forum/tests/generator/lib.php
index b582c51c0ad..369b59349a1 100644
--- a/mod/forum/tests/generator/lib.php
+++ b/mod/forum/tests/generator/lib.php
@@ -43,7 +43,7 @@ class mod_forum_generator extends phpunit_module_generator {
      * @return stdClass activity record with extra cmid field
      */
     public function create_instance($record = null, array $options = null) {
-        global $DB, $CFG;
+        global $CFG;
         require_once("$CFG->dirroot/mod/forum/locallib.php");
 
         $this->instancecount++;
@@ -52,6 +52,9 @@ class mod_forum_generator extends phpunit_module_generator {
         $record = (object)(array)$record;
         $options = (array)$options;
 
+        if (empty($record->course)) {
+            throw new coding_exception('module generator requires $record->course');
+        }
         if (!isset($record->name)) {
             $record->name = get_string('pluginname', 'forum').' '.$i;
         }
@@ -61,15 +64,26 @@ class mod_forum_generator extends phpunit_module_generator {
         if (!isset($record->introformat)) {
             $record->introformat = FORMAT_MOODLE;
         }
-        $record->timemodified = time();
+        if (!isset($record->type)) {
+            $record->type = 'general';
+        }
+        if (!isset($record->assessed)) {
+            $record->assessed = 0;
+        }
+        if (!isset($record->scale)) {
+            $record->scale = 0;
+        }
+        if (!isset($record->forcesubscribe)) {
+            $record->forcesubscribe = FORUM_CHOOSESUBSCRIBE;
+        }
+        if (isset($options['idnumber'])) {
+            $record->cmidnumber = $options['idnumber'];
+        } else {
+            $record->cmidnumber = '';
+        }
 
-        $id = $DB->insert_record('forum', $record);
-        $instance = $DB->get_record('forum', array('id'=>$id), '*', MUST_EXIST);
-
-        $cm = $this->create_course_module($instance, $options);
-
-        $instance->cmid = $cm->id;
-
-        return $instance;
+        $record->coursemodule = $this->precreate_course_module($record->course, $options);
+        $id = forum_add_instance($record, null);
+        return $this->post_add_instance($id, $record->coursemodule);
     }
 }
diff --git a/mod/forum/tests/generator_test.php b/mod/forum/tests/generator_test.php
index a537f27f592..f1843d1c4bc 100644
--- a/mod/forum/tests/generator_test.php
+++ b/mod/forum/tests/generator_test.php
@@ -36,28 +36,38 @@ defined('MOODLE_INTERNAL') || die();
  */
 class mod_forum_generator_testcase extends advanced_testcase {
     public function test_generator() {
-        global $DB, $SITE;
+        global $DB;
 
         $this->resetAfterTest(true);
 
         $this->assertEquals(0, $DB->count_records('forum'));
 
+        $course = $this->getDataGenerator()->create_course();
+
         /** @var mod_forum_generator $generator */
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
         $this->assertInstanceOf('mod_forum_generator', $generator);
         $this->assertEquals('forum', $generator->get_modulename());
 
-        $generator->create_instance(array('course'=>$SITE->id));
-        $generator->create_instance(array('course'=>$SITE->id));
-        $forum = $generator->create_instance(array('course'=>$SITE->id));
+        $generator->create_instance(array('course'=>$course->id));
+        $generator->create_instance(array('course'=>$course->id));
+        $forum = $generator->create_instance(array('course'=>$course->id));
         $this->assertEquals(3, $DB->count_records('forum'));
 
         $cm = get_coursemodule_from_instance('forum', $forum->id);
         $this->assertEquals($forum->id, $cm->instance);
         $this->assertEquals('forum', $cm->modname);
-        $this->assertEquals($SITE->id, $cm->course);
+        $this->assertEquals($course->id, $cm->course);
 
         $context = context_module::instance($cm->id);
         $this->assertEquals($forum->cmid, $context->instanceid);
+
+        // test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE!
+        $forum = $generator->create_instance(array('course'=>$course->id, 'assessed'=>1, 'scale'=>100));
+        $gitem = $DB->get_record('grade_items', array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule'=>'forum', 'iteminstance'=>$forum->id));
+        $this->assertNotEmpty($gitem);
+        $this->assertEquals(100, $gitem->grademax);
+        $this->assertEquals(0, $gitem->grademin);
+        $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
     }
 }
diff --git a/mod/page/lib.php b/mod/page/lib.php
index 0cc7c41e2f4..6d960f8530b 100644
--- a/mod/page/lib.php
+++ b/mod/page/lib.php
@@ -81,16 +81,15 @@ function page_get_post_actions() {
 
 /**
  * Add page instance.
- * @param object $data
- * @param object $mform
+ * @param stdClass $data
+ * @param mod_page_mod_form $mform
  * @return int new page instance id
  */
-function page_add_instance($data, $mform) {
+function page_add_instance($data, $mform = null) {
     global $CFG, $DB;
     require_once("$CFG->libdir/resourcelib.php");
 
-    $cmid        = $data->coursemodule;
-    $draftitemid = $data->page['itemid'];
+    $cmid = $data->coursemodule;
 
     $data->timemodified = time();
     $displayoptions = array();
@@ -102,8 +101,10 @@ function page_add_instance($data, $mform) {
     $displayoptions['printintro']   = $data->printintro;
     $data->displayoptions = serialize($displayoptions);
 
-    $data->content       = $data->page['text'];
-    $data->contentformat = $data->page['format'];
+    if ($mform) {
+        $data->content       = $data->page['text'];
+        $data->contentformat = $data->page['format'];
+    }
 
     $data->id = $DB->insert_record('page', $data);
 
@@ -111,7 +112,8 @@ function page_add_instance($data, $mform) {
     $DB->set_field('course_modules', 'instance', $data->id, array('id'=>$cmid));
     $context = get_context_instance(CONTEXT_MODULE, $cmid);
 
-    if ($draftitemid) {
+    if ($mform and !empty($data->page['itemid'])) {
+        $draftitemid = $data->page['itemid'];
         $data->content = file_save_draft_area_files($draftitemid, $context->id, 'mod_page', 'content', 0, page_get_editor_options($context), $data->content);
         $DB->update_record('page', $data);
     }
diff --git a/mod/page/tests/generator/lib.php b/mod/page/tests/generator/lib.php
index f0a2fe77d5d..d2ae2fa7309 100644
--- a/mod/page/tests/generator/lib.php
+++ b/mod/page/tests/generator/lib.php
@@ -43,7 +43,7 @@ class mod_page_generator extends phpunit_module_generator {
      * @return stdClass activity record with extra cmid field
      */
     public function create_instance($record = null, array $options = null) {
-        global $DB, $CFG;
+        global $CFG;
         require_once("$CFG->dirroot/mod/page/locallib.php");
 
         $this->instancecount++;
@@ -52,6 +52,9 @@ class mod_page_generator extends phpunit_module_generator {
         $record = (object)(array)$record;
         $options = (array)$options;
 
+        if (empty($record->course)) {
+            throw new coding_exception('module generator requires $record->course');
+        }
         if (!isset($record->name)) {
             $record->name = get_string('pluginname', 'page').' '.$i;
         }
@@ -70,15 +73,20 @@ class mod_page_generator extends phpunit_module_generator {
         if (!isset($record->display)) {
             $record->display = RESOURCELIB_DISPLAY_AUTO;
         }
-        $record->timemodified = time();
+        if (isset($options['idnumber'])) {
+            $record->cmidnumber = $options['idnumber'];
+        } else {
+            $record->cmidnumber = '';
+        }
+        if (!isset($record->printheading)) {
+            $record->printheading = 1;
+        }
+        if (!isset($record->printintro)) {
+            $record->printintro = 0;
+        }
 
-        $id = $DB->insert_record('page', $record);
-        $instance = $DB->get_record('page', array('id'=>$id), '*', MUST_EXIST);
-
-        $cm = $this->create_course_module($instance, $options);
-
-        $instance->cmid = $cm->id;
-
-        return $instance;
+        $record->coursemodule = $this->precreate_course_module($record->course, $options);
+        $id = page_add_instance($record, null);
+        return $this->post_add_instance($id, $record->coursemodule);
     }
 }