From 16cd708835e6d3acf1147191d7c9bf16d6c4e908 Mon Sep 17 00:00:00 2001 From: sam marshall Date: Mon, 5 Aug 2013 18:27:15 +0100 Subject: [PATCH] MDL-38190 (1) Backup/restore: Progress tracking API --- backup/controller/backup_controller.class.php | 28 ++ .../controller/restore_controller.class.php | 28 ++ backup/util/includes/backup_includes.php | 2 + backup/util/includes/restore_includes.php | 2 + backup/util/plan/backup_plan.class.php | 10 + backup/util/plan/base_plan.class.php | 8 + backup/util/plan/base_task.class.php | 10 + backup/util/plan/restore_plan.class.php | 10 + .../plan/tests/fixtures/plan_fixtures.php | 4 + .../core_backup_null_progress.class.php | 28 ++ .../progress/core_backup_progress.class.php | 307 +++++++++++++++ backup/util/progress/tests/progress_test.php | 363 ++++++++++++++++++ 12 files changed, 800 insertions(+) create mode 100644 backup/util/progress/core_backup_null_progress.class.php create mode 100644 backup/util/progress/core_backup_progress.class.php create mode 100644 backup/util/progress/tests/progress_test.php diff --git a/backup/controller/backup_controller.class.php b/backup/controller/backup_controller.class.php index b72c39472f5..b7b87d22cf8 100644 --- a/backup/controller/backup_controller.class.php +++ b/backup/controller/backup_controller.class.php @@ -64,6 +64,11 @@ class backup_controller extends backup implements loggable { protected $destination; // Destination chain object (fs_moodle, fs_os, db, email...) protected $logger; // Logging chain object (moodle, inline, fs, db, syslog) + /** + * @var core_backup_progress Progress reporting object. + */ + protected $progress; + protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses /** @@ -109,6 +114,10 @@ class backup_controller extends backup implements loggable { // Default logger chain (based on interactive/execution) $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid); + // By default there is no progress reporter. Interfaces that wish to + // display progress must set it. + $this->progress = new core_backup_null_progress(); + // Instantiate the output_controller singleton and active it if interactive and inmediate $oc = output_controller::get_instance(); if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) { @@ -302,6 +311,25 @@ class backup_controller extends backup implements loggable { return $this->logger; } + /** + * Gets the progress reporter, which can be used to report progress within + * the backup or restore process. + * + * @return core_backup_progress Progress reporting object + */ + public function get_progress() { + return $this->progress; + } + + /** + * Sets the progress reporter. + * + * @param core_backup_progress $progress Progress reporting object + */ + public function set_progress(core_backup_progress $progress) { + $this->progress = $progress; + } + /** * Executes the backup * @return void Throws and exception of completes diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php index ae3d58a4a34..9d98df6ac62 100644 --- a/backup/controller/restore_controller.class.php +++ b/backup/controller/restore_controller.class.php @@ -57,6 +57,11 @@ class restore_controller extends backup implements loggable { protected $logger; // Logging chain object (moodle, inline, fs, db, syslog) + /** + * @var core_backup_progress Progress reporting object. + */ + protected $progress; + protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses /** @@ -101,6 +106,10 @@ class restore_controller extends backup implements loggable { // Default logger chain (based on interactive/execution) $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid); + // By default there is no progress reporter. Interfaces that wish to + // display progress must set it. + $this->progress = new core_backup_null_progress(); + // Instantiate the output_controller singleton and active it if interactive and inmediate $oc = output_controller::get_instance(); if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) { @@ -300,6 +309,25 @@ class restore_controller extends backup implements loggable { return $this->logger; } + /** + * Gets the progress reporter, which can be used to report progress within + * the backup or restore process. + * + * @return core_backup_progress Progress reporting object + */ + public function get_progress() { + return $this->progress; + } + + /** + * Sets the progress reporter. + * + * @param core_backup_progress $progress Progress reporting object + */ + public function set_progress(core_backup_progress $progress) { + $this->progress = $progress; + } + public function execute_plan() { // Basic/initial prevention against time/memory limits set_time_limit(1 * 60 * 60); // 1 hour for 1 course initially granted diff --git a/backup/util/includes/backup_includes.php b/backup/util/includes/backup_includes.php index 12a3259e9a7..16effa4f263 100644 --- a/backup/util/includes/backup_includes.php +++ b/backup/util/includes/backup_includes.php @@ -71,6 +71,8 @@ require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php'); +require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php'); +require_once($CFG->dirroot . '/backup/util/progress/core_backup_null_progress.class.php'); require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php'); require_once($CFG->dirroot . '/backup/util/settings/base_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/backup_setting.class.php'); diff --git a/backup/util/includes/restore_includes.php b/backup/util/includes/restore_includes.php index ae01911167c..94e6e157500 100644 --- a/backup/util/includes/restore_includes.php +++ b/backup/util/includes/restore_includes.php @@ -60,6 +60,8 @@ require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php'); +require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php'); +require_once($CFG->dirroot . '/backup/util/progress/core_backup_null_progress.class.php'); require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php'); require_once($CFG->dirroot . '/backup/util/factories/restore_factory.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php'); diff --git a/backup/util/plan/backup_plan.class.php b/backup/util/plan/backup_plan.class.php index 43782215e35..3d537d2cf43 100644 --- a/backup/util/plan/backup_plan.class.php +++ b/backup/util/plan/backup_plan.class.php @@ -87,6 +87,16 @@ class backup_plan extends base_plan implements loggable { return $this->controller->get_logger(); } + /** + * Gets the progress reporter, which can be used to report progress within + * the backup or restore process. + * + * @return core_backup_progress Progress reporting object + */ + public function get_progress() { + return $this->controller->get_progress(); + } + public function is_excluding_activities() { return $this->excludingdactivities; } diff --git a/backup/util/plan/base_plan.class.php b/backup/util/plan/base_plan.class.php index d8c4b4d7c98..7b3cab16bc4 100644 --- a/backup/util/plan/base_plan.class.php +++ b/backup/util/plan/base_plan.class.php @@ -164,6 +164,14 @@ abstract class base_plan implements checksumable, executable { } } + /** + * Gets the progress reporter, which can be used to report progress within + * the backup or restore process. + * + * @return core_backup_progress Progress reporting object + */ + public abstract function get_progress(); + /** * Destroy all circular references. It helps PHP 5.2 a lot! */ diff --git a/backup/util/plan/base_task.class.php b/backup/util/plan/base_task.class.php index 440521b54de..523679c7a20 100644 --- a/backup/util/plan/base_task.class.php +++ b/backup/util/plan/base_task.class.php @@ -111,6 +111,16 @@ abstract class base_task implements checksumable, executable, loggable { return $this->plan->get_logger(); } + /** + * Gets the progress reporter, which can be used to report progress within + * the backup or restore process. + * + * @return core_backup_progress Progress reporting object + */ + public function get_progress() { + return $this->plan->get_progress(); + } + public function log($message, $level, $a = null, $depth = null, $display = false) { backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger()); } diff --git a/backup/util/plan/restore_plan.class.php b/backup/util/plan/restore_plan.class.php index 9943e0bd0bc..92aede16bf4 100644 --- a/backup/util/plan/restore_plan.class.php +++ b/backup/util/plan/restore_plan.class.php @@ -94,6 +94,16 @@ class restore_plan extends base_plan implements loggable { return $this->controller->get_logger(); } + /** + * Gets the progress reporter, which can be used to report progress within + * the backup or restore process. + * + * @return core_backup_progress Progress reporting object + */ + public function get_progress() { + return $this->controller->get_progress(); + } + public function get_info() { return $this->controller->get_info(); } diff --git a/backup/util/plan/tests/fixtures/plan_fixtures.php b/backup/util/plan/tests/fixtures/plan_fixtures.php index daf1499ee55..1283e7e5304 100644 --- a/backup/util/plan/tests/fixtures/plan_fixtures.php +++ b/backup/util/plan/tests/fixtures/plan_fixtures.php @@ -34,6 +34,10 @@ require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); class mock_base_plan extends base_plan { public function build() { } + + public function get_progress() { + return null; + } } /** diff --git a/backup/util/progress/core_backup_null_progress.class.php b/backup/util/progress/core_backup_null_progress.class.php new file mode 100644 index 00000000000..3e02369751c --- /dev/null +++ b/backup/util/progress/core_backup_null_progress.class.php @@ -0,0 +1,28 @@ +. + +/** + * Progress handler that ignores progress entirely. + * + * @package core_backup + * @copyright 2013 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_backup_null_progress extends core_backup_progress { + public function update_progress() { + // Do nothing. + } +} diff --git a/backup/util/progress/core_backup_progress.class.php b/backup/util/progress/core_backup_progress.class.php new file mode 100644 index 00000000000..2c092c90fa2 --- /dev/null +++ b/backup/util/progress/core_backup_progress.class.php @@ -0,0 +1,307 @@ +. + +/** + * Base class for handling progress information during a backup and restore. + * + * Subclasses should generally override the current_progress function which + * summarises all progress information. + * + * @package core_backup + * @copyright 2013 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class core_backup_progress { + /** + * @var int Constant indicating that the number of progress calls is unknown. + */ + const INDETERMINATE = -1; + + /** + * @var int The number of seconds that can pass without progress() calls. + */ + const TIME_LIMIT_WITHOUT_PROGRESS = 120; + + /** + * @var int Time of last progress call. + */ + protected $lastprogresstime; + + /** + * @var int Number of progress calls (restricted to ~ 1/second). + */ + protected $count; + + /** + * @var array Array of progress descriptions for each stack level. + */ + protected $descriptions = array(); + + /** + * @var array Array of maximum progress values for each stack level. + */ + protected $maxes = array(); + + /** + * @var array Array of current progress values. + */ + protected $currents = array(); + + /** + * @var int Array of counts within parent progress entry (ignored for first) + */ + protected $parentcounts = array(); + + /** + * Marks the start of an operation that will display progress. + * + * This can be called multiple times for nested progress sections. It must + * be paired with calls to end_progress. + * + * The progress maximum may be INDETERMINATE if the current operation has + * an unknown number of steps. (This is default.) + * + * Calling this function will always result in a new display, so this + * should not be called exceedingly frequently. + * + * When it is complete by calling end_progress, each start_progress section + * automatically adds progress to its parent, as defined by $parentcount. + * + * @param string $description Description to display + * @param int $max Maximum value of progress for this section + * @param int $parentcount How many progress points this section counts for + * @throws coding_exception If max is invalid + */ + public function start_progress($description, $max = self::INDETERMINATE, + $parentcount = 1) { + if ($max != self::INDETERMINATE && $max <= 0) { + throw new coding_exception( + 'start_progress() max value cannot be zero or negative'); + } + if ($parentcount < 1) { + throw new coding_exception( + 'start_progress() parent progress count must be at least 1'); + } + if (!empty($this->descriptions)) { + $prevmax = end($this->maxes); + if ($prevmax !== self::INDETERMINATE) { + $prevcurrent = end($this->currents); + if ($prevcurrent + $parentcount > $prevmax) { + throw new coding_exception( + 'start_progress() parent progress would exceed max'); + } + } + } else { + if ($parentcount != 1) { + throw new coding_exception( + 'start_progress() progress count must be 1 when no parent'); + } + } + $this->descriptions[] = $description; + $this->maxes[] = $max; + $this->currents[] = 0; + $this->parentcounts[] = $parentcount; + $this->update_progress(); + $lastprogresstime = $this->get_time(); + } + + /** + * Marks the end of an operation that will display progress. + * + * This must be paired with each start_progress call. + * + * If there is a parent progress section, its progress will be increased + * automatically to reflect the end of the child section. + * + * @throws coding_exception If progress hasn't been started + */ + public function end_progress() { + if (!count($this->descriptions)) { + throw new coding_exception('end_progress() without start_progress()'); + } + array_pop($this->descriptions); + array_pop($this->maxes); + array_pop($this->currents); + $parentcount = array_pop($this->parentcounts); + if (!empty($this->descriptions)) { + $lastmax = end($this->maxes); + if ($lastmax != self::INDETERMINATE) { + $lastvalue = end($this->currents); + $this->currents[key($this->currents)] = $lastvalue + $parentcount; + } + } + $this->update_progress(); + } + + /** + * Indicates that progress has occurred. + * + * The progress value should indicate the total progress so far, from 0 + * to the value supplied for $max (inclusive) in start_progress. + * + * You do not need to call this function for every value. It is OK to skip + * values. It is also OK to call this function as often as desired; it + * doesn't do anything if called more than once per second. + * + * It must be INDETERMINATE if start_progress was called with $max set to + * INDETERMINATE. Otherwise it must not be indeterminate. + * + * @param int $progress Progress so far + * @throws coding_exception If progress value is invalid + */ + public function progress($progress = self::INDETERMINATE) { + // Ignore too-frequent progress calls (more than once per second). + $now = $this->get_time(); + if ($now === $this->lastprogresstime) { + return; + } + + // Check we are inside a progress section. + $max = end($this->maxes); + if ($max === false) { + throw new coding_exception( + 'progress() without start_progress'); + } + + // Check and apply new progress. + if ($progress === self::INDETERMINATE) { + // Indeterminate progress. + if ($max !== self::INDETERMINATE) { + throw new coding_exception( + 'progress() INDETERMINATE, expecting value'); + } + } else { + // Determinate progress. + $current = end($this->currents); + if ($max === self::INDETERMINATE) { + throw new coding_exception( + 'progress() with value, expecting INDETERMINATE'); + } else if ($progress < 0 || $progress > $max) { + throw new coding_exception( + 'progress() value out of range'); + } else if ($progress < $current) { + throw new coding_Exception( + 'progress() value may not go backwards'); + } + $this->currents[key($this->currents)] = $progress; + } + + // Update progress. + $this->count++; + $this->lastprogresstime = $now; + set_time_limit(self::TIME_LIMIT_WITHOUT_PROGRESS); + $this->update_progress(); + } + + /** + * Gets time (this is provided so that unit tests can override it). + * + * @return int Current system time + */ + protected function get_time() { + return time(); + } + + /** + * Called whenever new progress should be displayed. + */ + protected abstract function update_progress(); + + /** + * @return bool True if currently inside a progress section + */ + public function is_in_progress_section() { + return !empty($this->descriptions); + } + + /** + * @return string Current progress section description + */ + public function get_current_description() { + $description = end($this->descriptions); + if ($description === false) { + throw new coding_exception('Not inside progress section'); + } + return $description; + } + + /** + * Obtains current progress in a way suitable for drawing a progress bar. + * + * Progress is returned as a minimum and maximum value. If there is no + * indeterminate progress, these values will be identical. If there is + * intermediate progress, these values can be different. (For example, if + * the top level progress sections is indeterminate, then the values will + * always be 0.0 and 1.0.) + * + * @return array Minimum and maximum possible progress proportions + */ + public function get_progress_proportion_range() { + // If there is no progress underway, we must have finished. + if (empty($this->currents)) { + return array(1.0, 1.0); + } + $count = count($this->currents); + $min = 0.0; + $max = 1.0; + for ($i = 0; $i < $count; $i++) { + // Get max value at that section - if it's indeterminate we can tell + // no more. + $sectionmax = $this->maxes[$i]; + if ($sectionmax === self::INDETERMINATE) { + return array($min, $max); + } + + // Special case if current value is max (this should only happen + // just before ending a section). + $sectioncurrent = $this->currents[$i]; + if ($sectioncurrent === $sectionmax) { + return array($max, $max); + } + + // Using the current value at that section, we know we are somewhere + // between 'current' and the next 'current' value which depends on + // the parentcount of the nested section (if any). + $newmin = ($sectioncurrent / $sectionmax) * ($max - $min) + $min; + $nextcurrent = $sectioncurrent + 1; + if ($i + 1 < $count) { + $weight = $this->parentcounts[$i + 1]; + $nextcurrent = $sectioncurrent + $weight; + } + $newmax = ($nextcurrent / $sectionmax) * ($max - $min) + $min; + $min = $newmin; + $max = $newmax; + } + + // If there was nothing indeterminate, we use the min value as current. + return array($min, $min); + } + + /** + * Obtains current indeterminate progress in a way suitable for adding to + * the progress display. + * + * This returns the number of indeterminate calls (at any level) during the + * lifetime of this progress reporter, whether or not there is a current + * indeterminate step. (The number will not be ridiculously high because + * progress calls are limited to one per second.) + * + * @return int Number of indeterminate progress calls + */ + public function get_progress_count() { + return $this->count; + } +} diff --git a/backup/util/progress/tests/progress_test.php b/backup/util/progress/tests/progress_test.php new file mode 100644 index 00000000000..3c673f7274f --- /dev/null +++ b/backup/util/progress/tests/progress_test.php @@ -0,0 +1,363 @@ +. + +/** + * Unit tests for the progress classes. + * + * @package core_backup + * @category phpunit + * @copyright 2013 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Include all the needed stuff. +global $CFG; +require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php'); + +/** + * Progress tests. + */ +class backup_progress_testcase extends basic_testcase { + + /** + * Tests for basic use with simple numeric progress. + */ + public function test_basic() { + $progress = new core_backup_mock_progress(); + + // Check values of empty progress things. + $this->assertFalse($progress->is_in_progress_section()); + + // Start progress counting, check basic values and check that update + // gets called. + $progress->start_progress('hello', 10); + $this->assertTrue($progress->was_update_called()); + $this->assertTrue($progress->is_in_progress_section()); + $this->assertEquals('hello', $progress->get_current_description()); + + // Check numeric position and indeterminate count. + $this->assert_min_max(0.0, 0.0, $progress); + $this->assertEquals(0, $progress->get_progress_count()); + + // Make some progress and check that the time limit gets added. + $progress->step_time(); + $progress->progress(2); + $this->assertTrue($progress->was_update_called()); + $this->assertEquals(120, ini_get('max_execution_time')); + + // Check the new value. + $this->assert_min_max(0.2, 0.2, $progress); + + // Do another progress run at same time, it should be ignored. + $progress->progress(3); + $this->assertFalse($progress->was_update_called()); + $this->assert_min_max(0.2, 0.2, $progress); + + // End the section. This should cause an update. + $progress->end_progress(); + $this->assertTrue($progress->was_update_called()); + + // Because there are no sections left open, it thinks we finished. + $this->assert_min_max(1.0, 1.0, $progress); + + // There was 1 progress call. + $this->assertEquals(1, $progress->get_progress_count()); + + // Clear the time limit, otherwise phpunit complains. + set_time_limit(0); + } + + /** + * Tests progress that is nested and/or indeterminate. + */ + public function test_nested() { + // Outer progress goes from 0 to 10. + $progress = new core_backup_mock_progress(); + $progress->start_progress('hello', 10); + + // Get up to 4, check position. + $progress->step_time(); + $progress->progress(4); + $this->assert_min_max(0.4, 0.4, $progress); + $this->assertEquals('hello', $progress->get_current_description()); + + // Now start indeterminate progress. + $progress->start_progress('world'); + $this->assert_min_max(0.4, 0.5, $progress); + $this->assertEquals('world', $progress->get_current_description()); + + // Do some indeterminate progress and count it (once per second). + $progress->step_time(); + $progress->progress(); + $this->assertEquals(2, $progress->get_progress_count()); + $progress->progress(); + $this->assertEquals(2, $progress->get_progress_count()); + $progress->step_time(); + $progress->progress(); + $this->assertEquals(3, $progress->get_progress_count()); + $this->assert_min_max(0.4, 0.5, $progress); + + // Exit the indeterminate section. + $progress->end_progress(); + $this->assert_min_max(0.5, 0.5, $progress); + + $progress->step_time(); + $progress->progress(7); + $this->assert_min_max(0.7, 0.7, $progress); + + // Enter a numbered section (this time with a range of 5). + $progress->start_progress('frogs', 5); + $this->assert_min_max(0.7, 0.7, $progress); + $progress->step_time(); + $progress->progress(1); + $this->assert_min_max(0.72, 0.72, $progress); + $progress->step_time(); + $progress->progress(3); + $this->assert_min_max(0.76, 0.76, $progress); + + // Now enter another indeterminate section. + $progress->start_progress('and'); + $this->assert_min_max(0.76, 0.78, $progress); + + // Make some progress, should increment indeterminate count. + $progress->step_time(); + $progress->progress(); + $this->assertEquals(7, $progress->get_progress_count()); + + // Enter numbered section, won't make any difference to values. + $progress->start_progress('zombies', 2); + $progress->step_time(); + $progress->progress(1); + $this->assert_min_max(0.76, 0.78, $progress); + $this->assertEquals(8, $progress->get_progress_count()); + + // Leaving it will make no difference too. + $progress->end_progress(); + + // Leaving the indeterminate section will though. + $progress->end_progress(); + $this->assert_min_max(0.78, 0.78, $progress); + + // Leave the two numbered sections. + $progress->end_progress(); + $this->assert_min_max(0.8, 0.8, $progress); + $progress->end_progress(); + $this->assertFalse($progress->is_in_progress_section()); + + set_time_limit(0); + } + + /** + * Tests the feature for 'weighting' nested progress. + */ + public function test_nested_weighted() { + $progress = new core_backup_mock_progress(); + $progress->start_progress('', 10); + + // First nested child has 2 units of its own and is worth 1 unit. + $progress->start_progress('', 2); + $progress->step_time(); + $progress->progress(1); + $this->assert_min_max(0.05, 0.05, $progress); + $progress->end_progress(); + $this->assert_min_max(0.1, 0.1, $progress); + + // Next child has 2 units of its own but is worth 3 units. + $progress->start_progress('weighted', 2, 3); + $progress->step_time(); + $progress->progress(1); + $this->assert_min_max(0.25, 0.25, $progress); + $progress->end_progress(); + $this->assert_min_max(0.4, 0.4, $progress); + + // Next indeterminate child is worth 6 units. + $progress->start_progress('', core_backup_progress::INDETERMINATE, 6); + $progress->step_time(); + $progress->progress(); + $this->assert_min_max(0.4, 1.0, $progress); + $progress->end_progress(); + $this->assert_min_max(1.0, 1.0, $progress); + + set_time_limit(0); + } + + /** + * I had some issues with real use in backup/restore, this test is intended + * to be similar. + */ + public function test_realistic() { + $progress = new core_backup_mock_progress(); + $progress->start_progress('parent', 100); + $progress->start_progress('child', 1); + $progress->progress(1); + $this->assert_min_max(0.01, 0.01, $progress); + $progress->end_progress(); + $this->assert_min_max(0.01, 0.01, $progress); + + // Clear the time limit, otherwise phpunit complains. + set_time_limit(0); + } + + /** + * Tests for any exceptions due to invalid calls. + */ + public function test_exceptions() { + $progress = new core_backup_mock_progress(); + + // Check errors when empty. + try { + $progress->progress(); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~without start_progress~', $e->getMessage())); + } + try { + $progress->end_progress(); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~without start_progress~', $e->getMessage())); + } + try { + $progress->get_current_description(); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~Not inside progress~', $e->getMessage())); + } + try { + $progress->start_progress('', 1, 7); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~must be 1~', $e->getMessage())); + } + + // Check invalid start (0). + try { + $progress->start_progress('hello', 0); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~cannot be zero or negative~', $e->getMessage())); + } + + // Indeterminate when value expected. + $progress->start_progress('hello', 10); + try { + $progress->progress(core_backup_progress::INDETERMINATE); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~expecting value~', $e->getMessage())); + } + + // Value when indeterminate expected. + $progress->start_progress('hello'); + try { + $progress->progress(4); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~expecting INDETERMINATE~', $e->getMessage())); + } + + // Illegal values. + $progress->start_progress('hello', 10); + try { + $progress->progress(-2); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~out of range~', $e->getMessage())); + } + try { + $progress->progress(11); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~out of range~', $e->getMessage())); + } + + // You are allowed two with the same value... + $progress->progress(4); + $progress->step_time(); + $progress->progress(4); + $progress->step_time(); + + // ...but not to go backwards. + try { + $progress->progress(3); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~backwards~', $e->getMessage())); + } + + // When you go forward, you can't go further than there is room. + try { + $progress->start_progress('', 1, 7); + $this->fail(); + } catch (coding_exception $e) { + $this->assertEquals(1, preg_match('~would exceed max~', $e->getMessage())); + } + + // Clear the time limit, otherwise phpunit complains. + set_time_limit(0); + } + + /** + * Checks the current progress values are as expected. + * + * @param number $min Expected min progress + * @param number $max Expected max progress + * @param core_backup_mock_progress $progress + */ + private function assert_min_max($min, $max, core_backup_mock_progress $progress) { + $this->assertEquals(array($min, $max), + $progress->get_progress_proportion_range()); + } +} + +/** + * Helper class that records when update_progress is called and allows time + * stepping. + */ +class core_backup_mock_progress extends core_backup_progress { + private $updatecalled = false; + private $time = 1; + + /** + * Checks if update was called since the last call to this function. + * + * @return boolean True if update was called + */ + public function was_update_called() { + if ($this->updatecalled) { + $this->updatecalled = false; + return true; + } + return false; + } + + /** + * Steps the current time by 1 second. + */ + public function step_time() { + $this->time++; + } + + protected function update_progress() { + $this->updatecalled = true; + } + + protected function get_time() { + return $this->time; + } +}