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); } }