diff --git a/includes/batch.inc b/includes/batch.inc index 7011abf..727c625 100644 --- a/includes/batch.inc +++ b/includes/batch.inc @@ -339,6 +339,8 @@ function _batch_process() { $progress_message = $old_set['progress_message']; } + // Total progress is the number of operations that have fully run plus the + // completion level of the current operation. $current = $total - $remaining + $finished; $percentage = _batch_api_percentage($total, $current); $elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0; @@ -373,7 +375,10 @@ function _batch_process() { * @param $total * The total number of operations. * @param $current - * The number of the current operation. + * The number of the current operation. This may be a floating point number + * rather than an integer in the case of a multi-step operation that is not + * yet complete; in that case, the fractional part of $current represents the + * fraction of the operation that has been completed. * @return * The properly formatted percentage, as a string. We output percentages * using the correct number of decimal places so that we never print "100%" @@ -390,7 +395,16 @@ function _batch_api_percentage($total, $current) { // We add a new digit at 200, 2000, etc. (since, for example, 199/200 // would round up to 100% if we didn't). $decimal_places = max(0, floor(log10($total / 2.0)) - 1); - $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places)); + do { + // Calculate the percentage to the specified number of decimal places. + $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places)); + // When $current is an integer, the above calculation will always be + // correct. However, if $current is a floating point number (in the case + // of a multi-step batch operation that is not yet complete), $percentage + // may be erroneously rounded up to 100%. To prevent that, we add one + // more decimal place and try again. + $decimal_places++; + } while ($percentage == '100'); } return $percentage; } diff --git a/modules/simpletest/tests/batch.test b/modules/simpletest/tests/batch.test index d1c0e0b..f668e52 100644 --- a/modules/simpletest/tests/batch.test +++ b/modules/simpletest/tests/batch.test @@ -365,6 +365,19 @@ class BatchPercentagesUnitTestCase extends DrupalUnitTestCase { '99.95' => array('total' => 2000, 'current' => 1999), // 19999/20000 should add yet another digit and go to 99.995%. '99.995' => array('total' => 20000, 'current' => 19999), + // The next five test cases simulate a batch with a single operation + // ('total' equals 1) that takes several steps to complete. Within the + // operation, we imagine that there are 501 items to process, and 100 are + // completed during each step. The percentages we get back should be + // rounded the usual way for the first few passes (i.e., 20%, 40%, etc.), + // but for the last pass through, when 500 out of 501 items have been + // processed, we do not want to round up to 100%, since that would + // erroneously indicate that the processing is complete. + '20' => array('total' => 1, 'current' => 100/501), + '40' => array('total' => 1, 'current' => 200/501), + '60' => array('total' => 1, 'current' => 300/501), + '80' => array('total' => 1, 'current' => 400/501), + '99.8' => array('total' => 1, 'current' => 500/501), ); require_once DRUPAL_ROOT . '/includes/batch.inc'; parent::setUp();