diff --git a/includes/batch.inc b/includes/batch.inc index c0a7c96e02e8b4d3ba39e4a12d5948355b220448..850eff7103db217c7ce8fdd6390cd837d4f86bc8 100644 --- a/includes/batch.inc +++ b/includes/batch.inc @@ -6,7 +6,7 @@ * @file * Batch processing API for processes to run in multiple HTTP requests. * - * Please note that batches are usually invoked by form submissions, which is + * Note that batches are usually invoked by form submissions, which is * why the core interaction functions of the batch processing API live in * form.inc. * @@ -62,8 +62,10 @@ function _batch_page() { // Add batch-specific CSS. foreach ($batch['sets'] as $batch_set) { - foreach ($batch_set['css'] as $css) { - drupal_add_css($css); + if (isset($batch_set['css'])) { + foreach ($batch_set['css'] as $css) { + drupal_add_css($css); + } } } @@ -252,6 +254,12 @@ function _batch_process() { timer_start('batch_processing'); } + if (empty($current_set['start'])) { + $current_set['start'] = microtime(TRUE); + } + + $queue = _batch_queue($current_set); + while (!$current_set['success']) { // If this is the first time we iterate this batch set in the current // request, we check if it requires an additional file for functions @@ -261,42 +269,49 @@ function _batch_process() { } $task_message = ''; - // We assume a single pass operation and set the completion level to 1 by + // Assume a single pass operation and set the completion level to 1 by // default. $finished = 1; - if ((list($function, $args) = reset($current_set['operations'])) && function_exists($function)) { - // Build the 'context' array, execute the function call, and retrieve the - // user message. + + if ($item = $queue->claimItem()) { + list($function, $args) = $item->data; + + // Build the 'context' array and execute the function call. $batch_context = array( 'sandbox' => &$current_set['sandbox'], 'results' => &$current_set['results'], 'finished' => &$finished, 'message' => &$task_message, ); - // Process the current operation. call_user_func_array($function, array_merge($args, array(&$batch_context))); - } - if ($finished == 1) { - // Make sure this step is not counted twice when computing $current. - $finished = 0; - // Remove the processed operation and clear the sandbox. - array_shift($current_set['operations']); - $current_set['sandbox'] = array(); + if ($finished == 1) { + // Make sure this step is not counted twice when computing $current. + $finished = 0; + // Remove the processed operation and clear the sandbox. + $queue->deleteItem($item); + $current_set['count']--; + $current_set['sandbox'] = array(); + } } // When all operations in the current batch set are completed, browse - // through the remaining sets until we find a set that contains operations. - // Note that _batch_next_set() executes stored form submit handlers in - // remaining batch sets, which can add new sets to the batch. + // through the remaining sets, marking them 'successfully processed' + // along the way, until we find a set that contains operations. + // _batch_next_set() executes form submit handlers stored in 'control' + // sets (see form_execute_handlers()), which can in turn add new sets to + // the batch. $set_changed = FALSE; $old_set = $current_set; - while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) { + while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) { $current_set = &_batch_current_set(); + $current_set['start'] = microtime(TRUE); $set_changed = TRUE; } + // At this point, either $current_set contains operations that need to be // processed or all sets have been completed. + $queue = _batch_queue($current_set); // If we are in progressive mode, break processing after 1 second. if ($batch['progressive'] && timer_read('batch_processing') > 1000) { @@ -312,33 +327,31 @@ function _batch_process() { // Reporting 100% progress will cause the whole batch to be considered // processed. If processing was paused right after moving to a new set, // we have to use the info from the new (unprocessed) set. - if ($set_changed && isset($current_set['operations'])) { + if ($set_changed && isset($current_set['queue'])) { // Processing will continue with a fresh batch set. - $remaining = count($current_set['operations']); + $remaining = $current_set['count']; $total = $current_set['total']; $progress_message = $current_set['init_message']; $task_message = ''; } else { // Processing will continue with the current batch set. - $remaining = count($old_set['operations']); + $remaining = $old_set['count']; $total = $old_set['total']; $progress_message = $old_set['progress_message']; } - $current = $total - $remaining + $finished; + $current = $total - $remaining + $finished; $percentage = _batch_api_percentage($total, $current); - $elapsed = $current_set['elapsed']; - // Estimate remaining with percentage in floating format. - $estimate = $elapsed * ($total - $current) / $current; $values = array( '@remaining' => $remaining, '@total' => $total, '@current' => floor($current), '@percentage' => $percentage, '@elapsed' => format_interval($elapsed / 1000), - '@estimate' => format_interval($estimate / 1000), + // If possible, estimate remaining processing time. + '@estimate' => ($current > 0) ? format_interval(($elapsed * ($total - $current) / $current) / 1000) : '-', ); $message = strtr($progress_message, $values); if (!empty($message)) { @@ -410,7 +423,7 @@ function _batch_next_set() { if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) { // We use our stored copies of $form and $form_state to account for // possible alterations by previous form submit handlers. - $function($batch['form'], $batch['form_state']); + $function($batch['form_state']['complete form'], $batch['form_state']); } return TRUE; } @@ -426,15 +439,16 @@ function _batch_finished() { $batch = &batch_get(); // Execute the 'finished' callbacks for each batch set, if defined. - foreach ($batch['sets'] as $key => $batch_set) { + foreach ($batch['sets'] as $batch_set) { if (isset($batch_set['finished'])) { // Check if the set requires an additional file for function definitions. if (isset($batch_set['file']) && is_file($batch_set['file'])) { include_once DRUPAL_ROOT . '/' . $batch_set['file']; } if (function_exists($batch_set['finished'])) { - // Format the elapsed time when batch complete. - $batch_set['finished']($batch_set['success'], $batch_set['results'], $batch_set['operations'], format_interval($batch_set['elapsed'] / 1000)); + $queue = _batch_queue($batch_set); + $operations = $queue->getAllItems(); + $batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000)); } } } @@ -444,6 +458,11 @@ function _batch_finished() { db_delete('batch') ->condition('bid', $batch['id']) ->execute(); + foreach ($batch['sets'] as $batch_set) { + if ($queue = _batch_queue($batch_set)) { + $queue->deleteQueue(); + } + } } $_batch = $batch; $batch = NULL; diff --git a/includes/batch.queue.inc b/includes/batch.queue.inc new file mode 100644 index 0000000000000000000000000000000000000000..8193280f3cf12e2f5c87159b539e5e9aae210d3d --- /dev/null +++ b/includes/batch.queue.inc @@ -0,0 +1,72 @@ + $this->name))->fetchObject(); + if ($item) { + $item->data = unserialize($item->data); + return $item; + } + return FALSE; + } + + /** + * Retrieve all remaining items in the queue. + * + * This is specific to Batch API and is not part of the DrupalQueueInterface, + */ + public function getAllItems() { + $result = array(); + $items = db_query('SELECT data FROM {queue} q WHERE name = :name ORDER BY item_id ASC', array(':name' => $this->name))->fetchAll(); + foreach ($items as $item) { + $result[] = unserialize($item->data); + } + return $result; + } +} + +/** + * Batch queue implementation used for non-progressive batches. + */ +class BatchMemoryQueue extends MemoryQueue { + + public function claimItem($lease_time = 0) { + if (!empty($this->queue)) { + reset($this->queue); + return current($this->queue); + } + return FALSE; + } + + /** + * Retrieve all remaining items in the queue. + * + * This is specific to Batch API and is not part of the DrupalQueueInterface, + */ + public function getAllItems() { + $result = array(); + foreach ($this->queue as $item) { + $result[] = $item->data; + } + return $result; + } +} diff --git a/includes/form.inc b/includes/form.inc index 753f421e81708a5b33094f2954bde61506854dc1..f9f0a27df42ed766a014cb8f4fa3051655a6a10d 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -634,12 +634,23 @@ function drupal_process_form($form_id, &$form, &$form_state) { // that is already being processed (if a batch operation performs a // drupal_form_submit). if ($batch =& batch_get() && !isset($batch['current_set'])) { - // The batch uses its own copies of $form and $form_state for - // late execution of submit handlers and post-batch redirection. - $batch['form'] = $form; - $batch['form_state'] = $form_state; + // Store $form_state information in the batch definition. + // We need the full $form_state when either: + // - Some submit handlers were saved to be called during batch + // processing. See form_execute_handlers(). + // - The form is multistep. + // In other cases, we only need the information expected by + // drupal_redirect_form(). + if ($batch['has_form_submits'] || !empty($form_state['rebuild']) || !empty($form_state['storage'])) { + $batch['form_state'] = $form_state; + } + else { + $batch['form_state'] = array_intersect_key($form_state, array_flip(array('programmed', 'rebuild', 'storage', 'no_redirect', 'redirect'))); + } + $batch['progressive'] = !$form_state['programmed']; batch_process(); + // Execution continues only for programmatic forms. // For 'regular' forms, we get redirected to the batch processing // page. Form redirection will be handled in _batch_finished(), @@ -1004,14 +1015,15 @@ function form_execute_handlers($type, &$form, &$form_state) { foreach ($handlers as $function) { if (function_exists($function)) { - // Check to see if a previous _submit handler has set a batch, but - // make sure we do not react to a batch that is already being processed - // (for instance if a batch operation performs a drupal_form_submit()). - if ($type == 'submit' && ($batch =& batch_get()) && !isset($batch['current_set'])) { - // Some previous _submit handler has set a batch. We store the call - // in a special 'control' batch set, for execution at the correct - // time during the batch processing workflow. + // Check if a previous _submit handler has set a batch, but make sure we + // do not react to a batch that is already being processed (for instance + // if a batch operation performs a drupal_form_submit()). + if ($type == 'submit' && ($batch =& batch_get()) && !isset($batch['id'])) { + // Some previous submit handler has set a batch. To ensure correct + // execution order, store the call in a special 'control' batch set. + // See _batch_next_set(). $batch['sets'][] = array('form_submit' => $function); + $batch['has_form_submits'] = TRUE; } else { $function($form, $form_state); @@ -3305,22 +3317,25 @@ function _form_set_class(&$element, $class = array()) { function batch_set($batch_definition) { if ($batch_definition) { $batch =& batch_get(); - // Initialize the batch + + // Initialize the batch if needed. if (empty($batch)) { $batch = array( 'sets' => array(), + 'has_form_submits' => FALSE, ); } + // Base and default properties for the batch set. + // Use get_t() to allow batches at install time. + $t = get_t(); $init = array( 'sandbox' => array(), 'results' => array(), 'success' => FALSE, - 'start' => microtime(TRUE), + 'start' => 0, 'elapsed' => 0, ); - // Use get_t() to allow batches at install time. - $t = get_t(); $defaults = array( 'title' => $t('Processing'), 'init_message' => $t('Initializing.'), @@ -3330,20 +3345,29 @@ function batch_set($batch_definition) { ); $batch_set = $init + $batch_definition + $defaults; - // Tweak init_message to avoid the bottom of the page flickering down after init phase. + // Tweak init_message to avoid the bottom of the page flickering down after + // init phase. $batch_set['init_message'] .= '
 '; + + // The non-concurrent workflow of batch execution allows us to save + // numberOfItems() queries by handling our own counter. $batch_set['total'] = count($batch_set['operations']); + $batch_set['count'] = $batch_set['total']; - // If the batch is being processed (meaning we are executing a stored submit handler), - // insert the new set after the current one. - if (isset($batch['current_set'])) { - // array_insert does not exist... - $slice1 = array_slice($batch['sets'], 0, $batch['current_set'] + 1); - $slice2 = array_slice($batch['sets'], $batch['current_set'] + 1); - $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); + // Add the set to the batch. + if (empty($batch['id'])) { + // The batch is not running yet. Simply add the new set. + $batch['sets'][] = $batch_set; } else { - $batch['sets'][] = $batch_set; + // The set is being added while the batch is running. Insert the new set + // right after the current one to ensure execution order, and store its + // operations in a queue. + $index = $batch['current_set'] + 1; + $slice1 = array_slice($batch['sets'], 0, $index); + $slice2 = array_slice($batch['sets'], $index); + $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); + _batch_populate_queue($batch, $index); } } } @@ -3387,11 +3411,28 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd ); $batch += $process_info; - // The batch is now completely built. Allow other modules to make changes to the - // batch so that it is easier to reuse batch processes in other enviroments. + // The batch is now completely built. Allow other modules to make changes + // to the batch so that it is easier to reuse batch processes in other + // enviroments. drupal_alter('batch', $batch); + // Assign an arbitrary id: don't rely on a serial column in the 'batch' + // table, since non-progressive batches skip database storage completely. + $batch['id'] = db_next_id(); + + // Move operations to a job queue. Non-progressive batches will use a + // memory-based queue. + foreach ($batch['sets'] as $key => $batch_set) { + _batch_populate_queue($batch, $key); + } + + // Initiate processing. if ($batch['progressive']) { + // Now that we have a batch id, we can generate the redirection link in + // the generic error message. + $t = get_t(); + $batch['error_message'] = $t('Please continue to the error page', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished'))))); + // Clear the way for the drupal_goto() redirection to the batch processing // page, by saving and unsetting the 'destination', if there is any. if (isset($_GET['destination'])) { @@ -3399,24 +3440,11 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd unset($_GET['destination']); } - // Initiate db storage in order to get a batch id. We have to provide - // at least an empty string for the (not null) 'token' column. - $batch['id'] = db_insert('batch') + // Store the batch. + db_insert('batch') ->fields(array( - 'token' => '', + 'bid' => $batch['id'], 'timestamp' => REQUEST_TIME, - )) - ->execute(); - - // Now that we have a batch id, we can generate the redirection link in - // the generic error message. - $t = get_t(); - $batch['error_message'] = $t('Please continue to the error page', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished'))))); - - // Actually store the batch data and the token generated form the batch id. - db_update('batch') - ->condition('bid', $batch['id']) - ->fields(array( 'token' => drupal_get_token($batch['id']), 'batch' => serialize($batch), )) @@ -3425,6 +3453,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd // Set the batch number in the session to guarantee that it will stay alive. $_SESSION['batches'][$batch['id']] = TRUE; + // Redirect for processing. $function = $batch['redirect_callback']; if (function_exists($function)) { $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id']))); @@ -3453,6 +3482,70 @@ function &batch_get() { return $batch; } +/** + * Populates a job queue with the operations of a batch set. + * + * Depending on whether the batch is progressive or not, the BatchQueue or + * BatchStaticQueue handler classes will be used. + * + * @param $batch + * The batch array. + * @param $set_id + * The id of the set to process. + * @return + * The name and class of the queue are added by reference to the batch set. + */ +function _batch_populate_queue(&$batch, $set_id) { + $batch_set = &$batch['sets'][$set_id]; + + if (isset($batch_set['operations'])) { + $batch_set += array( + 'queue' => array( + 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id, + 'class' => $batch['progressive'] ? 'BatchQueue' : 'BatchMemoryQueue', + ), + ); + + $queue = _batch_queue($batch_set); + $queue->createQueue(); + foreach ($batch_set['operations'] as $operation) { + $queue->createItem($operation); + } + + unset($batch_set['operations']); + } +} + +/** + * Returns a queue object for a batch set. + * + * @param $batch_set + * The batch set. + * @return + * The queue object. + */ +function _batch_queue($batch_set) { + static $queues; + + // The class autoloader is not available when running update.php, so make + // sure the files are manually included. + if (is_null($queues)) { + $queues = array(); + require_once DRUPAL_ROOT . '/modules/system/system.queue.inc'; + require_once DRUPAL_ROOT . '/includes/batch.queue.inc'; + } + + if (isset($batch_set['queue'])) { + $name = $batch_set['queue']['name']; + $class = $batch_set['queue']['class']; + + if (!isset($queues[$class][$name])) { + $queues[$class][$name] = new $class($name); + } + return $queues[$class][$name]; + } +} + /** * @} End of "defgroup batch". */ diff --git a/includes/update.inc b/includes/update.inc index 18dd7a8dfa054e77f77825d4c084eb3e152eb86c..816f32bd0af15be0d7e60792c9fd2bf11d822429 100644 --- a/includes/update.inc +++ b/includes/update.inc @@ -338,6 +338,51 @@ function update_fix_d7_requirements() { db_create_table('date_formats', $schema['date_formats']); db_create_table('date_format_locale', $schema['date_format_locale']); + // Add the queue table. + $schema['queue'] = array( + 'description' => 'Stores items in queues.', + 'fields' => array( + 'item_id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'Primary Key: Unique item ID.', + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The queue name.', + ), + 'data' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'serialize' => TRUE, + 'description' => 'The arbitrary data for the item.', + ), + 'expire' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Timestamp when the claim lease expires on the item.', + ), + 'created' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Timestamp when the item was created.', + ), + ), + 'primary key' => array('item_id'), + 'indexes' => array( + 'name_created' => array('name', 'created'), + 'expire' => array('expire'), + ), + ); + db_create_table('queue', $schema['queue']); + // Add column for locale context. if (db_table_exists('locales_source')) { db_add_field('locales_source', 'context', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', 'description' => 'The context this string applies to.')); diff --git a/modules/simpletest/tests/batch.test b/modules/simpletest/tests/batch.test index 7c6ade42372ec02114e13e731d70f8ce3b93a11f..54b29c8a9a51adf3bdf9dce2432b12beb7a0ae8d 100644 --- a/modules/simpletest/tests/batch.test +++ b/modules/simpletest/tests/batch.test @@ -3,40 +3,286 @@ /** * @file - * Unit tests for the Drupal Batch API. + * Tests for the Batch API. */ /** - * Tests for the batch API progress page theme. + * Tests for the Batch API. */ -class BatchAPIThemeTestCase extends DrupalWebTestCase { +class BatchProcessingTestCase extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => 'Batch API progress page theme', - 'description' => 'Tests that while a progressive batch is running, it correctly uses the theme of the page that started the batch.', + 'name' => 'Batch processing', + 'description' => 'Test batch processing in form and non-form workflow.', 'group' => 'Batch API', ); } function setUp() { - parent::setUp('system_test'); - // Make sure that the page which starts the batch (an administrative page) - // is using a different theme than would normally be used by the batch API. - variable_set('theme_default', 'garland'); - variable_set('admin_theme', 'seven'); + parent::setUp('batch_test'); + } + + /** + * Test batches triggered outside of form submission. + */ + function testBatchNoForm() { + // Displaying the page triggers batch 1. + $this->drupalGet('batch_test/no_form'); + $this->assertBatchMessages($this->_resultMessages(1), t('Batch for step 2 performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), t('Execution order was correct.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + } + + /** + * Test batches defined in a form submit handler. + */ + function testBatchForm() { + // Batch 0: no operation. + $edit = array('batch' => 'batch_0'); + $this->drupalPost('batch_test/simple', $edit, 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_0'), t('Batch with no operation performed successfully.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + + // Batch 1: several simple operations. + $edit = array('batch' => 'batch_1'); + $this->drupalPost('batch_test/simple', $edit, 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_1'), t('Batch with simple operations performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), t('Execution order was correct.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + + // Batch 2: one multistep operation. + $edit = array('batch' => 'batch_2'); + $this->drupalPost('batch_test/simple', $edit, 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_2'), t('Batch with multistep operation performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_2'), t('Execution order was correct.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + + // Batch 3: simple + multistep combined. + $edit = array('batch' => 'batch_3'); + $this->drupalPost('batch_test/simple', $edit, 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_3'), t('Batch with simple and multistep operations performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_3'), t('Execution order was correct.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + + // Batch 4: nested batch. + $edit = array('batch' => 'batch_4'); + $this->drupalPost('batch_test/simple', $edit, 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_4'), t('Nested batch performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_4'), t('Execution order was correct.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + } + + /** + * Test batches defined in a multistep form. + */ + function testBatchFormMultistep() { + $this->drupalGet('batch_test/multistep'); + $this->assertText('step 1', t('Form is displayed in step 1.')); + + // First step triggers batch 1. + $this->drupalPost(NULL, array(), 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_1'), t('Batch for step 1 performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), t('Execution order was correct.')); + $this->assertText('step 2', t('Form is displayed in step 2.')); + + // Second step triggers batch 2. + $this->drupalPost(NULL, array(), 'Submit'); + $this->assertBatchMessages($this->_resultMessages('batch_2'), t('Batch for step 2 performed successfully.')); + $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_2'), t('Execution order was correct.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + } + + /** + * Test batches defined in different submit handlers on the same form. + */ + function testBatchFormMultipleBatches() { + // Batches 1, 2 and 3 are triggered in sequence by different submit + // handlers. Each submit handler modify the submitted 'value'. + $value = rand(0, 255); + $edit = array('value' => $value); + $this->drupalPost('batch_test/chained', $edit, 'Submit'); + // Check that result messages are present and in the correct order. + $this->assertBatchMessages($this->_resultMessages('chained'), t('Batches defined in separate submit handlers performed successfully.')); + // The stack contains execution order of batch callbacks and submit + // hanlders and logging of corresponding $form_state[{values']. + $this->assertEqual(batch_test_stack(), $this->_resultStack('chained', $value), t('Execution order was correct, and $form_state is correctly persisted.')); + $this->assertText('Redirection successful.', t('Redirection after batch execution is correct.')); + } + + /** + * Test batches defined in a programmatically submitted form. + * + * Same as above, but the form is submitted through drupal_form_execute(). + */ + function testBatchFormProgrammatic() { + // Batches 1, 2 and 3 are triggered in sequence by different submit + // handlers. Each submit handler modify the submitted 'value'. + $value = rand(0, 255); + $this->drupalGet('batch_test/programmatic/' . $value); + // Check that result messages are present and in the correct order. + $this->assertBatchMessages($this->_resultMessages('chained'), t('Batches defined in separate submit handlers performed successfully.')); + // The stack contains execution order of batch callbacks and submit + // hanlders and logging of corresponding $form_state[{values']. + $this->assertEqual(batch_test_stack(), $this->_resultStack('chained', $value), t('Execution order was correct, and $form_state is correctly persisted.')); + $this->assertText('Got out of a programmatic batched form.', t('Page execution continues normally.')); + } + + /** + * Test that drupal_form_submit() can run within a batch operation. + */ + function testDrupalFormSubmitInBatch() { + // Displaying the page triggers a batch that programmatically submits a + // form. + $value = rand(0, 255); + $this->drupalGet('batch_test/nested_programmatic/' . $value); + $this->assertEqual(batch_test_stack(), array('mock form submitted with value = ' . $value), t('drupal_form_submit() ran successfully within a batch operation.')); + } + + /** + * Will trigger a pass if the texts were found in order in the raw content. + * + * @param $texts + * Array of raw strings to look for . + * @param $message + * Message to display. + * @return + * TRUE on pass, FALSE on fail. + */ + function assertBatchMessages($texts, $message) { + $pattern = '|' . implode('.*', $texts) .'|s'; + return $this->assertPattern($pattern, $message); + } + + /** + * Helper function: return expected execution stacks for the test batches. + */ + function _resultStack($id, $value = 0) { + $stack = array(); + switch ($id) { + case 'batch_1': + for ($i = 1; $i <= 10; $i++) { + $stack[] = "op 1 id $i"; + } + break; + + case 'batch_2': + for ($i = 1; $i <= 10; $i++) { + $stack[] = "op 2 id $i"; + } + break; + + case 'batch_3': + for ($i = 1; $i <= 5; $i++) { + $stack[] = "op 1 id $i"; + } + for ($i = 1; $i <= 5; $i++) { + $stack[] = "op 2 id $i"; + } + for ($i = 6; $i <= 10; $i++) { + $stack[] = "op 1 id $i"; + } + for ($i = 6; $i <= 10; $i++) { + $stack[] = "op 2 id $i"; + } + break; + + case 'batch_4': + for ($i = 1; $i <= 5; $i++) { + $stack[] = "op 1 id $i"; + } + $stack[] = 'setting up batch 2'; + for ($i = 6; $i <= 10; $i++) { + $stack[] = "op 1 id $i"; + } + $stack = array_merge($stack, $this->_resultStack('batch_2')); + break; + + case 'chained': + $stack[] = 'submit handler 1'; + $stack[] = 'value = ' . $value; + $stack = array_merge($stack, $this->_resultStack('batch_1')); + $stack[] = 'submit handler 2'; + $stack[] = 'value = ' . ($value + 1); + $stack = array_merge($stack, $this->_resultStack('batch_2')); + $stack[] = 'submit handler 3'; + $stack[] = 'value = ' . ($value + 2); + $stack[] = 'submit handler 4'; + $stack[] = 'value = ' . ($value + 3); + $stack = array_merge($stack, $this->_resultStack('batch_3')); + break; + } + return $stack; + } + + /** + * Helper function: return expected result messages for the test batches. + */ + function _resultMessages($id) { + $messages = array(); + + switch ($id) { + case 'batch_0': + $messages[] = 'results for batch 0
none'; + break; + + case 'batch_1': + $messages[] = 'results for batch 1
op 1: processed 10 elements'; + break; + + case 'batch_2': + $messages[] = 'results for batch 2
op 2: processed 10 elements'; + break; + + case 'batch_3': + $messages[] = 'results for batch 3
op 1: processed 10 elements
op 2: processed 10 elements'; + break; + + case 'batch_4': + $messages[] = 'results for batch 4
op 1: processed 10 elements'; + $messages = array_merge($messages, $this->_resultMessages('batch_2')); + break; + + case 'chained': + $messages = array_merge($messages, $this->_resultMessages('batch_1')); + $messages = array_merge($messages, $this->_resultMessages('batch_2')); + $messages = array_merge($messages, $this->_resultMessages('batch_3')); + break; + } + return $messages; + } +} + +/** + * Tests for the Batch API Progress page. + */ +class BatchPageTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Batch progress page', + 'description' => 'Test the content of the progress page.', + 'group' => 'Batch API', + ); + } + + function setUp() { + parent::setUp('batch_test'); } /** * Tests that the batch API progress page uses the correct theme. */ - function testBatchAPIProgressPageTheme() { + function testBatchProgressPageTheme() { + // Make sure that the page which starts the batch (an administrative page) + // is using a different theme than would normally be used by the batch API. + variable_set('theme_default', 'garland'); + variable_set('admin_theme', 'seven'); // Visit an administrative page that runs a test batch, and check that the // theme that was used during batch execution (which the batch callback // function saved as a variable) matches the theme used on the // administrative page. - $this->drupalGet('admin/system-test/batch-theme'); - $batch_theme_used = variable_get('system_test_batch_theme_used', 'garland'); - $this->assertEqual($batch_theme_used, 'seven', t('A progressive batch correctly uses the theme of the page that started the batch.')); + $this->drupalGet('admin/batch_test/test_theme'); + // The stack should contain the name of the the used on the progress page. + $this->assertEqual(batch_test_stack(), array('seven'), t('A progressive batch correctly uses the theme of the page that started the batch.')); } } @@ -44,13 +290,13 @@ class BatchAPIThemeTestCase extends DrupalWebTestCase { * Tests the function _batch_api_percentage() to make sure that the rounding * works properly in all cases. */ -class BatchAPIPercentagesTestCase extends DrupalWebTestCase { +class BatchPercentagesUnitTestCase extends DrupalUnitTestCase { protected $testCases = array(); public static function getInfo() { return array( - 'name' => 'Batch API percentages', - 'description' => 'Tests the handling of percentage rounding in the Drupal batch API. This is critical to Drupal user experience.', + 'name' => 'Batch percentages', + 'description' => 'Unit tests of progress percentage rounding.', 'group' => 'Batch API', ); } @@ -99,10 +345,9 @@ class BatchAPIPercentagesTestCase extends DrupalWebTestCase { } /** - * Test the _batch_api_percentage() function with the data stored in the - * testCases class variable. + * Test the _batch_api_percentage() function. */ - function testBatchAPIPercentages() { + function testBatchPercentages() { require_once DRUPAL_ROOT . '/includes/batch.inc'; foreach ($this->testCases as $expected_result => $arguments) { // PHP sometimes casts numeric strings that are array keys to integers, diff --git a/modules/simpletest/tests/batch_test.callbacks.inc b/modules/simpletest/tests/batch_test.callbacks.inc new file mode 100644 index 0000000000000000000000000000000000000000..5f24757ee667a74e22f1c38a743cced9f8fcd77f --- /dev/null +++ b/modules/simpletest/tests/batch_test.callbacks.inc @@ -0,0 +1,118 @@ + $op_results) { + $messages[] = 'op '. $op . ': processed ' . count($op_results) . ' elements'; + } + } + else { + $messages[] = 'none'; + } + + if (!$success) { + // A fatal error occurred during the processing. + $error_operation = reset($operations); + $messages[] = t('An error occurred while processing @op with arguments:
@args', array('@op' => $error_operation[0], '@args' => print_r($error_operation[1], TRUE))); + } + + drupal_set_message(implode('
', $messages)); +} + +/** + * 'finished' callback for batch 0. + */ +function _batch_test_finished_0($success, $results, $operations) { + _batch_test_finished_helper(0, $success, $results, $operations); +} + +/** + * 'finished' callback for batch 1. + */ +function _batch_test_finished_1($success, $results, $operations) { + _batch_test_finished_helper(1, $success, $results, $operations); +} + +/** + * 'finished' callback for batch 2. + */ +function _batch_test_finished_2($success, $results, $operations) { + _batch_test_finished_helper(2, $success, $results, $operations); +} + +/** + * 'finished' callback for batch 3. + */ +function _batch_test_finished_3($success, $results, $operations) { + _batch_test_finished_helper(3, $success, $results, $operations); +} + +/** + * 'finished' callback for batch 4. + */ +function _batch_test_finished_4($success, $results, $operations) { + _batch_test_finished_helper(4, $success, $results, $operations); +} diff --git a/modules/simpletest/tests/batch_test.info b/modules/simpletest/tests/batch_test.info new file mode 100644 index 0000000000000000000000000000000000000000..432c17ff71d3fd35c9e3de1808d69a964f6f4e54 --- /dev/null +++ b/modules/simpletest/tests/batch_test.info @@ -0,0 +1,9 @@ +; $Id$ +name = "Batch API test" +description = "Support module for Batch API tests." +package = Testing +version = VERSION +core = 7.x +files[] = batch_test.module +files[] = batch_test.callbacks.inc +hidden = TRUE diff --git a/modules/simpletest/tests/batch_test.module b/modules/simpletest/tests/batch_test.module new file mode 100644 index 0000000000000000000000000000000000000000..03938aaa828899457af5365ce8c56d3f15401773 --- /dev/null +++ b/modules/simpletest/tests/batch_test.module @@ -0,0 +1,475 @@ + 'Batch test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('batch_test_simple_form'), + 'access callback' => TRUE, + ); + // Simple form: one submit handler, setting a batch. + $items['batch_test/simple'] = array( + 'title' => 'Simple', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 0, + ); + // Multistep form: two steps, each setting a batch. + $items['batch_test/multistep'] = array( + 'title' => 'Multistep', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('batch_test_multistep_form'), + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + ); + // Chained form: four submit handlers, several of which set a batch. + $items['batch_test/chained'] = array( + 'title' => 'Chained', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('batch_test_chained_form'), + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ); + // Programmatic form: the page submits the 'Chained' form through + // drupal_form_submit(). + $items['batch_test/programmatic'] = array( + 'title' => 'Programmatic', + 'page callback' => 'batch_test_programmatic', + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, + ); + // No form: fire a batch simply by accessing a page. + $items['batch_test/no_form'] = array( + 'title' => 'Simple page', + 'page callback' => 'batch_test_no_form', + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + 'weight' => 4, + ); + // Tests programmatic form submission within a batch operation. + $items['batch_test/nested_programmatic'] = array( + 'title' => 'Nested programmatic', + 'page callback' => 'batch_test_nested_drupal_form_submit', + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + 'weight' => 5, + ); + // Landing page to test redirects. + $items['batch_test/redirect'] = array( + 'title' => 'Redirect', + 'page callback' => 'batch_test_redirect_page', + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + 'weight' => 6, + ); + // + // This item lives under 'admin' so that the page uses the admin theme. + $items['admin/batch_test/test_theme'] = array( + 'page callback' => 'batch_test_theme_batch', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Simple form. + */ +function batch_test_simple_form() { + $form['batch'] = array( + '#type' => 'select', + '#title' => 'Choose batch', + '#options' => array( + 'batch_0' => 'batch 0', + 'batch_1' => 'batch 1', + 'batch_2' => 'batch 2', + 'batch_3' => 'batch 3', + 'batch_4' => 'batch 4', + ), + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + + return $form; +} + +/** + * Submit handler for the simple form. + */ +function batch_test_simple_form_submit($form, &$form_state) { + batch_test_stack(NULL, TRUE); + + $function = '_batch_test_' . $form_state['values']['batch']; + batch_set($function()); + + $form_state['redirect'] = 'batch_test/redirect'; +} + + +/** + * Multistep form. + */ +function batch_test_multistep_form($form, &$form_state) { + if (empty($form_state['storage']['step'])) { + $form_state['storage']['step'] = 1; + } + + $form['step_display'] = array( + '#markup' => 'step ' . $form_state['storage']['step'] . '
', + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + + return $form; +} + +/** + * Submit handler for the multistep form. + */ +function batch_test_multistep_form_submit($form, &$form_state) { + batch_test_stack(NULL, TRUE); + + switch ($form_state['storage']['step']) { + case 1: + batch_set(_batch_test_batch_1()); + break; + case 2: + batch_set(_batch_test_batch_2()); + break; + } + + if ($form_state['storage']['step'] < 2) { + $form_state['storage']['step']++; + $form_state['rebuild'] = TRUE; + } + + // This will only be effective on the last step. + $form_state['redirect'] = 'batch_test/redirect'; +} + +/** + * Form with chained submit callbacks. + */ +function batch_test_chained_form() { + // This value is used to test that $form_state persists through batched + // submit handlers. + $form['value'] = array( + '#type' => 'textfield', + '#title' => 'Value', + '#default_value' => 1, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + $form['#submit'] = array( + 'batch_test_chained_form_submit_1', + 'batch_test_chained_form_submit_2', + 'batch_test_chained_form_submit_3', + 'batch_test_chained_form_submit_4', + ); + + return $form; +} + +/** + * Submit handler #1 for the chained form. + */ +function batch_test_chained_form_submit_1($form, &$form_state) { + batch_test_stack(NULL, TRUE); + + batch_test_stack('submit handler 1'); + batch_test_stack('value = ' . $form_state['values']['value']); + + $form_state['values']['value']++; + batch_set(_batch_test_batch_1()); + + // This redirect should not be taken into account. + $form_state['redirect'] = 'should/be/discarded'; +} + +/** + * Submit handler #2 for the chained form. + */ +function batch_test_chained_form_submit_2($form, &$form_state) { + batch_test_stack('submit handler 2'); + batch_test_stack('value = ' . $form_state['values']['value']); + + $form_state['values']['value']++; + batch_set(_batch_test_batch_2()); + + // This redirect should not be taken into account. + $form_state['redirect'] = 'should/be/discarded'; +} + +/** + * Submit handler #3 for the chained form. + */ +function batch_test_chained_form_submit_3($form, &$form_state) { + batch_test_stack('submit handler 3'); + batch_test_stack('value = ' . $form_state['values']['value']); + + $form_state['values']['value']++; + + // This redirect should not be taken into account. + $form_state['redirect'] = 'should/be/discarded'; +} + +/** + * Submit handler #4 for the chained form. + */ +function batch_test_chained_form_submit_4($form, &$form_state) { + batch_test_stack('submit handler 4'); + batch_test_stack('value = ' . $form_state['values']['value']); + + $form_state['values']['value']++; + batch_set(_batch_test_batch_3()); + + // This is the redirect that should prevail. + $form_state['redirect'] = 'batch_test/redirect'; +} + +/** + * Menu callback: programmatically submits the 'Chained' form. + */ +function batch_test_programmatic($value = 1) { + $form_state = array( + 'values' => array('value' => $value) + ); + drupal_form_submit('batch_test_chained_form', $form_state); + return 'Got out of a programmatic batched form.'; +} + +/** + * Menu callback: programmatically submits a form within a batch. + */ +function batch_test_nested_drupal_form_submit($value = 1) { + // Set the batch and process it. + $batch['operations'] = array( + array('_batch_test_nested_drupal_form_submit_callback', array($value)), + ); + batch_set($batch); + batch_process('batch_test/redirect'); +} + +/** + * Batch operation: submits form_test_mock_form using drupal_form_submit(). + */ +function _batch_test_nested_drupal_form_submit_callback($value) { + $state['values']['test_value'] = $value; + drupal_form_submit('batch_test_mock_form', $state); +} + +/** + * A simple form with a textfield and submit button. + */ +function batch_test_mock_form($form, $form_state) { + $form['test_value'] = array( + '#type' => 'textfield', + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} + +/** + * Submit handler for the batch_test_mock form. + */ +function batch_test_mock_form_submit($form, &$form_state) { + batch_test_stack('mock form submitted with value = ' . $form_state['values']['test_value']); +} + +/** + * Menu callback: fire a batch process without a form submission. + */ +function batch_test_no_form() { + batch_test_stack(NULL, TRUE); + + batch_set(_batch_test_batch_1()); + batch_process('batch_test/redirect'); +} + +/** + * Menu callback: successful redirection. + */ +function batch_test_redirect_page() { + return 'Redirection successful.'; +} + +/** + * Batch 0: no operation. + */ +function _batch_test_batch_0() { + $batch = array( + 'operations' => array(), + 'finished' => '_batch_test_finished_0', + 'file' => drupal_get_path('module', 'batch_test'). '/batch_test.callbacks.inc', + ); + return $batch; +} + +/** + * Batch 1: repeats a simple operation. + * + * Operations: op 1 from 1 to 10. + */ +function _batch_test_batch_1() { + // Ensure the batch takes at least two iterations. + $total = 10; + $sleep = (1000000 / $total) * 2; + + $operations = array(); + for ($i = 1; $i <= $total; $i++) { + $operations[] = array('_batch_test_callback_1', array($i, $sleep)); + } + $batch = array( + 'operations' => $operations, + 'finished' => '_batch_test_finished_1', + 'file' => drupal_get_path('module', 'batch_test'). '/batch_test.callbacks.inc', + ); + return $batch; +} + +/** + * Batch 2: single multistep operation. + * + * Operations: op 2 from 1 to 10. + */ +function _batch_test_batch_2() { + // Ensure the batch takes at least two iterations. + $total = 10; + $sleep = (1000000 / $total) * 2; + + $operations = array( + array('_batch_test_callback_2', array(1, $total, $sleep)), + ); + $batch = array( + 'operations' => $operations, + 'finished' => '_batch_test_finished_2', + 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + ); + return $batch; +} + +/** + * Batch 3: both single and multistep operations. + * + * Operations: + * - op 1 from 1 to 5, + * - op 2 from 1 to 5, + * - op 1 from 6 to 10, + * - op 2 from 6 to 10. + */ +function _batch_test_batch_3() { + // Ensure the batch takes at least two iterations. + $total = 10; + $sleep = (1000000 / $total) * 2; + + $operations = array(); + for ($i = 1; $i <= round($total / 2); $i++) { + $operations[] = array('_batch_test_callback_1', array($i, $sleep)); + } + $operations[] = array('_batch_test_callback_2', array(1, $total / 2, $sleep)); + for ($i = round($total / 2) + 1; $i <= $total; $i++) { + $operations[] = array('_batch_test_callback_1', array($i, $sleep)); + } + $operations[] = array('_batch_test_callback_2', array(6, $total / 2, $sleep)); + $batch = array( + 'operations' => $operations, + 'finished' => '_batch_test_finished_3', + 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + ); + return $batch; +} + +/** + * Batch 4: batch within a batch. + * + * Operations: + * - op 1 from 1 to 5, + * - set batch 2 (op 2 from 1 to 10, should run at the end) + * - op 1 from 6 to 10, + */ +function _batch_test_batch_4() { + // Ensure the batch takes at least two iterations. + $total = 10; + $sleep = (1000000 / $total) * 2; + + $operations = array(); + for ($i = 1; $i <= round($total / 2); $i++) { + $operations[] = array('_batch_test_callback_1', array($i, $sleep)); + } + $operations[] = array('_batch_test_nested_batch_callback', array()); + for ($i = round($total / 2) + 1; $i <= $total; $i++) { + $operations[] = array('_batch_test_callback_1', array($i, $sleep)); + } + $batch = array( + 'operations' => $operations, + 'finished' => '_batch_test_finished_4', + 'file' => drupal_get_path('module', 'batch_test') . '/batch_test.callbacks.inc', + ); + return $batch; +} + +/** + * Menu callback: run a batch for testing theme used on the progress page. + */ +function batch_test_theme_batch() { + batch_test_stack(NULL, TRUE); + $batch = array( + 'operations' => array( + array('_batch_test_theme_callback', array()), + ), + ); + batch_set($batch); + batch_process('batch_test/redirect'); +} + +/** + * Batch callback function for testing the theme used on the progress page. + */ +function _batch_test_theme_callback() { + // Because drupalGet() steps through the full progressive batch before + // returning control to the test function, we cannot test that the correct + // theme is being used on the batch processing page by viewing that page + // directly. Instead, we save the theme being used in a variable here, so + // that it can be loaded and inspected in the thread running the test. + global $theme; + batch_test_stack($theme); +} + +/** + * Helper function: store or retrieve traced execution data. + */ +function batch_test_stack($data = NULL, $reset = FALSE) { + if ($reset) { + variable_del('batch_test_stack'); + } + if (is_null($data)) { + return variable_get('batch_test_stack', array()); + } + $stack = variable_get('batch_test_stack', array()); + $stack[] = $data; + variable_set('batch_test_stack', $stack); +} diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index 10839177757bbf99b757b15c29e20d6322418812..b7dec7b6a95acbe71ff6c6012e0e8d0e5a5f5baa 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -511,45 +511,6 @@ class FormsElementsTableSelectFunctionalTest extends DrupalWebTestCase { } -/** - * Test using drupal_form_submit in a batch. - */ -class FormAPITestCase extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'Drupal Execute and Batch API', - 'description' => 'Tests the compatibility of drupal_form_submit and the Batch API', - 'group' => 'Form API', - ); - } - - /** - * Check that we can run drupal_form_submit during a batch. - */ - function testDrupalFormSubmitInBatch() { - - // Our test is going to modify the following variable. - variable_set('form_test_mock_submit', 'initial_state'); - - // This is a page that sets a batch, which calls drupal_form_submit, which - // modifies the variable we set up above. - $this->drupalGet('form_test/drupal_form_submit_batch_api'); - - // If the drupal_form_submit call executed correctly our test variable will be - // set to 'form_submitted'. - $this->assertEqual('form_submitted', variable_get('form_test_mock_submit', 'initial_state'), t('Check drupal_form_submit called submit handlers when running in a batch')); - - // Clean our variable up. - variable_del('form_test_mock_submit'); - } - - function setUp() { - parent::setUp('form_test'); - } - -} - /** * Test the form storage on a multistep form. * diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index c8f565938361a572a880e9aa39b0481a9773d344..54b6299f8d4b42d7ae3824f85a710aa7f03f8790 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -54,13 +54,6 @@ function form_test_menu() { 'type' => MENU_CALLBACK, ); - $items['form_test/drupal_form_submit_batch_api'] = array( - 'title' => 'BatchAPI Drupal_form_submit tests', - 'page callback' => 'form_test_drupal_form_submit_batch_api', - 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, - ); - $items['form_test/form-storage'] = array( 'title' => 'Form storage test', 'page callback' => 'drupal_get_form', @@ -375,67 +368,6 @@ function _form_test_tableselect_js_select_form($form, $form_state, $action) { return _form_test_tableselect_form_builder($form, $form_state, $options); } -/** - * Page callback for the batch/drupal_form_submit interaction test. - * - * When called without any arguments we set up a batch that calls - * form_test_batch_callback. That function will submit a form using - * drupal_form_submit using the values specified in this function. - * - * The form's field test_value begins at 'initial_value', and is changed - * to 'form_submitted' when the form is submitted successfully. On - * completion this function is passed 'done' to complete the process. - */ -function form_test_drupal_form_submit_batch_api($arg = '') { - // If we're at the end of the batch process, return. - if ($arg == 'done') { - return t('Done'); - } - - // Otherwise set up the batch. - $batch['operations'] = array( - array('form_test_batch_callback', array('form_submitted')), - ); - - // Set the batch and process it. - batch_set($batch); - batch_process('form_test/drupal_form_submit_batch_api/done'); -} - -/** - * Submits form_test_mock_form using drupal_form_submit using the given $value. - */ -function form_test_batch_callback($value) { - $state['values']['test_value'] = $value; - drupal_form_submit('form_test_mock_form', $state); -} - -/** - * A simple form with a textfield and submit button. - */ -function form_test_mock_form($form, $form_state) { - $form['test_value'] = array( - '#type' => 'textfield', - '#default_value' => 'initial_state', - ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Submit'), - ); - - return $form; -} - -/** - * Form submission callback. - * - * Updates the variable 'form_test_mock_submit' to the submitted form value. - */ -function form_test_mock_form_submit($form, &$form_state) { - variable_set('form_test_mock_submit', $form_state['values']['test_value']); -} - /** * A multistep form for testing the form storage. * diff --git a/modules/simpletest/tests/system_test.module b/modules/simpletest/tests/system_test.module index 5c57ee20d9bca8f6e323de422426e3dc49dd9f9e..b37019f8c6a7037006e39a22fd51ee1a83188451 100644 --- a/modules/simpletest/tests/system_test.module +++ b/modules/simpletest/tests/system_test.module @@ -5,11 +5,6 @@ * Implements hook_menu(). */ function system_test_menu() { - $items['admin/system-test/batch-theme'] = array( - 'page callback' => 'system_test_batch_theme', - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); $items['system-test/sleep/%'] = array( 'page callback' => 'system_test_sleep', 'page arguments' => array(2), @@ -102,34 +97,6 @@ function system_test_menu() { return $items; } -/** - * Menu callback; start a new batch for testing the batch progress page theme. - */ -function system_test_batch_theme() { - $batch = array( - 'operations' => array( - array('system_test_batch_theme_callback', array()), - ), - ); - batch_set($batch); - // Force the batch to redirect to some page other than this one (to avoid an - // infinite loop). - batch_process('node'); -} - -/** - * Batch callback function for testing the theme used by a batch. - */ -function system_test_batch_theme_callback() { - // Because drupalGet() steps through the full progressive batch before - // returning control to the test function, we cannot test that the correct - // theme is being used on the batch processing page by viewing that page - // directly. Instead, we save the theme being used in a variable here, so - // that it can be loaded and inspected in the thread running the test. - global $theme; - variable_set('system_test_batch_theme_used', $theme); -} - function system_test_sleep($seconds) { sleep($seconds); } diff --git a/modules/system/system.install b/modules/system/system.install index add4c37c4ea710ae64e34994ee77487d398b6cf4..519cbabda5b795622e944e1126bee09023da078e 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -496,7 +496,9 @@ function system_schema() { 'fields' => array( 'bid' => array( 'description' => 'Primary Key: Unique batch ID.', - 'type' => 'serial', + // This is not a serial column, to allow both progressive and + // non-progressive batches. See batch_process(). + 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, ), @@ -2257,50 +2259,7 @@ function system_update_7021() { * Add the queue tables. */ function system_update_7022() { - $schema['queue'] = array( - 'description' => 'Stores items in queues.', - 'fields' => array( - 'item_id' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary Key: Unique item ID.', - ), - 'name' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The queue name.', - ), - 'data' => array( - 'type' => 'text', - 'not null' => FALSE, - 'size' => 'big', - 'serialize' => TRUE, - 'description' => 'The arbitrary data for the item.', - ), - 'expire' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Timestamp when the claim lease expires on the item.', - ), - 'created' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Timestamp when the item was created.', - ), - ), - 'primary key' => array('item_id'), - 'indexes' => array( - 'name_created' => array('name', 'created'), - 'expire' => array('expire'), - ), - ); - - db_create_table('queue', $schema['queue']); + // Moved to update_fix_d7_requirements(). } /** @@ -2727,6 +2686,13 @@ function system_update_7049() { } } +/** + * Change {batch}.id column from serial to regular int. + */ +function system_update_7050() { + db_change_field('batch', 'bid', 'bid', array('description' => 'Primary Key: Unique batch ID.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE)); +} + /** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. diff --git a/modules/system/system.module b/modules/system/system.module index e0da1720bc0b0d98b70c9bf41d88ab0880b25f0e..6247fcb2f10945de1159e9f68e207cf99cd0b5ed 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -2695,10 +2695,6 @@ function system_cron() { db_delete('flood') ->condition('expiration', REQUEST_TIME, '<') ->execute(); - // Cleanup the batch table. - db_delete('batch') - ->condition('timestamp', REQUEST_TIME - 864000, '<') - ->execute(); // Remove temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. // Use separate placeholders for the status to avoid a bug in some versions @@ -2722,6 +2718,15 @@ function system_cron() { cache_clear_all(NULL, $table); } + // Cleanup the batch table and the queue for failed batches. + db_delete('batch') + ->condition('timestamp', REQUEST_TIME - 864000, '<') + ->execute(); + db_delete('queue') + ->condition('created', REQUEST_TIME - 864000, '<') + ->condition('name', 'drupal_batch:%', 'LIKE') + ->execute(); + // Reset expired items in the default queue implementation table. If that's // not used, this will simply be a no-op. db_update('queue') diff --git a/modules/system/system.queue.inc b/modules/system/system.queue.inc index 5174a0a6521a3c287c3233933bd4172bbdd63848..bac1ff26e6e493e138260092c86dfc57604982b7 100644 --- a/modules/system/system.queue.inc +++ b/modules/system/system.queue.inc @@ -253,6 +253,79 @@ public function deleteQueue() { } } +/** + * Static queue implementation. + * + * This allows "undelayed" variants of processes relying on the Queue + * interface. The queue data resides in memory. It should only be used for + * items that will be queued and dequeued within a given page request. + */ +class MemoryQueue implements DrupalQueueInterface { + /** + * The queue data. + * + * @var array + */ + protected $queue; + + /** + * Counter for item ids. + * + * @var int + */ + protected $id_sequence; + + public function __construct($name) { + $this->queue = array(); + $this->id_sequence = 0; + } + + public function createItem($data) { + $item = new stdClass(); + $item->item_id = $this->id_sequence++; + $item->data = $data; + $item->created = time(); + $item->expire = 0; + $this->queue[$item->item_id] = $item; + } + + public function numberOfItems() { + return count($this->queue); + } + + public function claimItem($lease_time = 30) { + foreach ($this->queue as $key => $item) { + if ($item->expire == 0) { + $item->expire = time() + $lease_time; + $this->queue[$key] = $item; + return $item; + } + } + return FALSE; + } + + public function deleteItem($item) { + unset($this->queue[$item->item_id]); + } + + public function releaseItem($item) { + if (isset($this->queue[$item->item_id]) && $this->queue[$item->item_id]->expire != 0) { + $this->queue[$item->item_id]->expire = 0; + return TRUE; + } + return FALSE; + } + + public function createQueue() { + // Nothing needed here. + } + + public function deleteQueue() { + $this->queue = array(); + $this->id_sequence = 0; + } +} + /** * @} End of "defgroup queue". */