'Automated testing', 'description' => 'Configure the automated testing project issue integration.', 'page callback' => 'drupal_get_form', 'page arguments' => array('pift_admin_settings_form'), 'access arguments' => array('administer projects'), 'file' => 'pift.admin.inc', ); $items['pift/retest/%'] = array( 'title' => 'Request retest of a file or branch', 'page callback' => 'drupal_get_form', 'page arguments' => array('pift_pages_retest_confirm_form', 2), 'access arguments' => array('pift re-test files'), 'file' => 'pift.pages.inc', 'type' => MENU_CALLBACK, ); $items['pift/delete/%'] = array( 'title' => 'Request deletion of a test', 'page callback' => 'drupal_get_form', 'page arguments' => array('pift_pages_delete_test_confirm_form', 2), 'access arguments' => array('pift re-test files'), 'file' => 'pift.pages.inc', 'type' => MENU_CALLBACK, ); // TODO: The plan was to remove the testing-status tab for the D7 Port, and // add it back in with Conduit. However, we need somewhere to store the // 'enable testing' checkbox, so leaving this in temporarily until the Conduit // piece is available. $items['node/%node/qa-settings'] = array( 'title' => t('Automated Testing'), 'access callback' => 'pift_results_visibility', 'access arguments' => array(1), 'page callback' => 'drupal_get_form', 'page arguments' => array('pift_pages_project_issue_settings', 1), 'file' => 'pift.pages.inc', 'weight' => '5', 'type' => MENU_LOCAL_TASK, ); return $items; } /** * Access callback to determine whether the Automated Testing tab is visible. */ function pift_results_visibility($node) { // Only display the tab on projects with releases if (project_node_is_project($node)) { if ($node->field_project_has_releases[$node->language][0]['value']) { // Ensure user has access to enable testing on the node if (user_access('pift enable project testing')) { return TRUE; } } } return FALSE; } /** * Implements hook_permission(). */ function pift_permission() { return array( 'pift re-test files' => array( 'title' => t('pift re-test files'), 'description' => t('Request a file to be re-tested'), ), 'pift enable project testing' => array( 'title' => t('pift enable project testing'), 'description' => t('Enable testing on a project'), ), 'pift access project testing tab' => array( 'title' => t('pift access project testing tab'), 'description' => t('Access the testing tab on projects'), ), ); } /** * Implements hook_theme(). */ function pift_theme() { return array( 'pift_attachments' => array( 'variables' => array( 'files' => array(), 'closed' => FALSE, ), 'file' => 'pift.pages.inc', ), 'pift_auto_followup' => array( 'variables' => array( 'type' => '', 'nid' => 0, 'cid' => 0, 'filename' => '', ), ), ); } /** * Implements hook_init(). */ function pift_init() { drupal_add_css(drupal_get_path('module', 'pift') . '/pift.css'); } /** * Implements hook_cron(). */ function pift_cron() { if (PIFT_DELETE) { // An issue comment or node has been deleted, remove related test entries. pift_test_delete_files(); variable_set('pift_delete', FALSE); } // Check if sending is enabled and that the sending frequency has elapsed. $time = REQUEST_TIME; if (PIFT_FREQUENCY != -1 && $time > PIFT_LAST + PIFT_FREQUENCY) { module_load_include('cron.inc', 'pift'); // Requeue all tests that have passed the re-test interval. // pift_cron_retest(); TODO fix query. // Send a batch of queued tests. pift_cron_queue_batch(); // Retrieve any results that have occured since last cron run. pift_cron_retrieve_results(); // Store current time as last run. variable_set('pift_last', $time); } } /** * Implements hook_versioncontrol_code_arrival(). */ function pift_versioncontrol_code_arrival(VersioncontrolRepository $repository, VersioncontrolEvent $event) { // Ignore events for disabled projects and non-Git repos. if (!$repository instanceof VersioncontrolGitRepository || !pift_project_enabled($repository->project_nid)) { return; } module_load_include('cron.inc', 'pift'); $api_versions = pift_core_api_versions(); $branch_names = array(); foreach ($event as $ref) { if (VERSIONCONTROL_GIT_REFTYPE_BRANCH === $ref->reftype) { $branch_names[] = $ref->refname; } } $rids = pift_cron_get_release($repository->project_nid, $branch_names); foreach ($rids as $rid) { // Ensure that one of the compatibility terms is present on the release node. $release = node_load($rid); $found = FALSE; // TODO: Update loop, since $release->taxonomy will not exist foreach ($api_versions as $api_version) { if (array_key_exists($api_version, $release->taxonomy)) { // Compatible term found, continue processing. $test_id = db_query('SELECT test_id FROM {pift_test} WHERE type = :type AND id = :id', array(':type' => PIFT_TYPE_RELEASE, ':id' => $rid))->fetchField(); // If existing test for release, queue it, otherwise add a new test. if ($test_id) { pift_test_requeue($test_id); } else if ($test_id !== 0) { pift_test_add(PIFT_TYPE_RELEASE, $rid); } break; } } } } /** * Helper fuction to build a list of releases for this project's repository. * * @param $node The project node object * @param $quiet Silence error messages * @return array List of available labels with corresponding release nodes */ function pift_get_releases($node, $quiet = FALSE) { $valid = pift_valid_prefix_list(); $branches = array(); $topbranches = array(); if (project_node_is_project($node)) { $result = db_query("Select b.name from {versioncontrol_release_labels} a join {versioncontrol_labels} b on a.label_id = b.label_id where a.project_nid = :nid", array(':nid' => $node->nid)); foreach ($result as $data) { // Filter out any branches < 6 (not accepted by PIFT) // Move '.x' releases to the top if (in_array(substr($data->name, 0, 3), $valid)) { if (substr($data->name, strlen($data->name) - 2, 2) == '.x') { $topbranches[$data->name] = $data->name; } else { $branches[$data->name] = $data->name; } } } // Sort to get the highest branch on top uasort($branches, 'version_compare'); uasort($topbranches, 'version_compare'); $branches = array_merge($branches, $topbranches); $branches = pift_array_reverse($branches); if (empty($branches) && !$quiet) { drupal_set_message(t('No releases found for the given project.'), 'error', FALSE); } } return $branches; } /** * Returns a listing of valid project release prefixes (ie. 6.x, 7.x, 8.x) */ function pift_valid_prefix_list() { $terms = array(); $tids = variable_get('pift_core', array()); foreach ($tids as $key => $value) { if (!empty($tids[$key])) { $term = taxonomy_term_load($key); $terms[$key] = $term->name; } } return $terms; } /** * Alternative function for the php standard array_reverse() * * As array_reverse() would damage numerical array keys, from * http://drupal.org/node/1074220. Code copied from project_git_instructions * * Borrowed from http://php.net/manual/en/function.array-reverse.php#102492 */ function pift_array_reverse($array) { $array_key = array_keys($array); $array_value = array_values($array); $array_return = array(); for ($i = 1, $size_of_array = sizeof($array_key); $i <= $size_of_array; $i++) { $array_return[$array_key[$size_of_array -$i]] = $array_value[$size_of_array -$i]; } return $array_return; } /** * Implements hook_form_FORM_ID_alter(). */ function pift_form_project_issue_node_form_alter(&$form, $form_state, $form_id) { module_load_include('pages.inc', 'pift'); pift_pages_description_add($form, $form_state, $form_id); } /** * Implements hook_node_view(). * * TODO: This approach is no longer valid after the project* changes. The * new approach will be to implement a field formatter on the field_issue_files * field, and add the pift testing results via this field formatter; which * should be much cleaner than removing and injecting the file attachments * table as was done in D6. */ //function pift_node_view($node, $view_mode = 'full') { // if (pift_node_is_interesting($node)) { // if (!$a3 && pift_project_enabled($node->project_issue['pid'])) { // Full view. // $files = pift_test_get_files_node($node->nid); // $status = $node->project_issue['sid']; // $node->content['pift_files'] = array( // '#value' => '
' . // theme('pift_attachments', array('files' => $files, 'closed' => $status)) . '
', // '#weight' => 50, // ); // unset($node->content['files']); // Remove old attachments table. // } // } //} /** * Implements hook_node_insert(). */ function pift_node_insert($node) { if (pift_node_is_interesting($node)) { if (!empty($node->field_issue_files)) { if (pift_test_check_criteria_issue($node)) { pift_test_add_files($node->field_issue_files); } } } } /** * Implements hook_node_update(). */ function pift_node_update($node) { if (pift_node_is_interesting($node)) { if ($node->field_issue_files != $node->original->field_issue_files) { if (!empty($node->field_issue_files)) { if (pift_test_check_criteria_issue($node)) { pift_test_add_files($node->field_issue_files); } } } } } /** * Implements hook_node_delete(). */ function pift_node_delete($node) { if (pift_node_is_interesting($node)) { // Flag pift that a project or issue node was deleted. if (project_node_is_project($node)) { // Remove this project from the pift_project table db_delete('pift_project')->condition('nid', $node->nid)->execute(); } variable_set('pift_delete', TRUE); } } /** * Check if a node is a project node or issue node with file attached. * * Used in hook_node_*() to filter out irrelevant nodes. */ function pift_node_is_interesting($node) { return project_node_is_project($node) || (project_issue_node_is_issue($node) && !empty($node->field_issue_files)); } /** * Cleanup the inconsistent project_issue property placement. * * TODO: Confirm this approach is no longer valid after the project* changes. * * In order to remove the need to a bunch of conditions all over PIFT, convert * the inconsistent node format to the one used everywhere else. The * inconsistent format is only found during node creation, after a node has * been created and hook_load() is used the properties are prefixed by * project_issue. * * * * @param object $node Node to convert. * @return object Properly formatted node. * @link http://drupal.org/node/519562 */ //function pift_nodeapi_clean($node) { // $node->project_issue = array(); // // $fields = array('pid', 'rid', 'component', 'category', 'priority', 'assigned', 'sid'); // foreach ($fields as $field) { // $node->project_issue[$field] = $node->$field; // } // // return $node; //} /** * Implements hook_comment_view(). * * TODO: This approach is no longer valid after the project* changes. The * new approach will be to implement a field formatter on the field_issue_files * field and comments, and add the pift testing results via this field * formatter; which should be much cleaner than removing and injecting the file * attachments table as was done in D6. */ //function pift_comment_view($comment) { // if ($node = pift_comment_is_interesting($comment)) { // if (!empty($comment->files) && pift_project_enabled($node->project_issue['pid'])) { // // Remove comment_upload attachments table and generate new one. // $comment->comment = preg_replace('/.*?<\/table>/s', '', $comment->comment); // $files = pift_test_get_files_comment($comment->cid); // $status = $node->project_issue['sid']; // $comment->comment .= '
' . // theme('pift_attachments', array('files' => $files, 'closed' => $status)) . '
'; // } // } //} /** * Implements hook_comment_insert(). * * TODO: This approach is no longer valid after the project* changes. Files * and patches are now attached to the node directly. */ //function pift_comment_insert($comment) { // if ($node = pift_comment_is_interesting($comment)) { // if (pift_test_check_criteria_issue($node)) { // if (!empty($comment->files)) { // // Add attachments to this comment to the send queue. // $files = comment_upload_load_files($comment['cid']); // pift_test_add_files($files); // } // // Add previously submitted files if issue state changes. // pift_test_add_previous_files($comment['nid']); // } // } //} /** * Implements hook_comment_delete(). * * TODO: Determine if we want to remove patches when the associated comment is * deleted ... this may now be redundant, since we can simply delete the file * from the field_issue_files field. */ //function pift_comment_delete($comment) { // if (pift_comment_is_interesting($comment)) { // variable_set('pift_delete', TRUE); // } //} /** * Check that comment is attached to a project_issue node. * * TODO: This is only called from three functions, all of which have been * commented out as part of the D7 port. */ //function pift_comment_is_interesting($comment) { // if (($node = node_load($comment->nid)) && $node->type == 'project_issue') { // return $node; // } // return FALSE; //} /** * Theme the auto followup comments. * * @param string $type Type of following, either: 'retest' or 'fail'. * @param integer $nid Node ID containting the failed test. * @param integer $cid Comment ID, if applicable, containing the failed test. * @param string $filename Name of file. * @return string HTML output. */ function theme_pift_auto_followup($variables) { // TODO: Validate whether these variables are still valid after the project* // changes introduced with the D7 port. $type = $variables['type']; $nid = $variables['nid']; $cid = $variables['cid']; $filename = $variables['filename']; $args = array( '@id' => "pift-results-$nid", '@filename' => $filename, ); if ($type == 'retest') { if ($cid) { $comment = comment_load($cid); $args['@cid'] = $comment->cid; $args['@comment'] = $comment->subject; return t('@comment: @filename queued for re-testing.', $args); } return t('@filename queued for re-testing.', $args); } elseif ($type == 'fail') { return t('The last submitted patch, @filename, failed testing.', $args); } return ''; } /** * Get the core compatible API version term IDs. * * @return array Associative array of core compatible API version term IDs. */ function pift_core_api_versions() { return array_filter(variable_get('pift_core', array())); } /** * Load the core release for the given API term ID. * * @param integer $api_tid Drupal core API compatibility term ID, of the * vocabulary defined by _project_release_get_api_vid. * @return Drupal core release NID. * @see _project_release_get_api_vid() */ function pift_core_api_release($api_tid) { static $api_releases = array(); if (!isset($api_branches[$api_tid])) { $api_vocabulary = taxonomy_vocabulary_load(variable_get('project_release_api_vocabulary', '')); $taxonomy_field = 'taxonomy_vocabulary_' . $api_vocabulary->machine_name; $query = new EntityFieldQuery(); $query->entityCondition('entity_type', 'node') ->entityCondition('bundle', 'project_release') // TODO: Drupal.org specific ->fieldCondition('field_release_project', 'target_id', PIFT_PID, '=') ->fieldCondition($taxonomy_field, 'tid', $api_tid) ->propertyOrderBy('nid', 'DESC') ->range(0,1); $result = $query->execute(); if (isset($result['node'])) { $api_releases[$api_tid] = reset(array_keys($result['node'])); } } return $api_releases[$api_tid]; } /** * Return a list of active project release compatibility terms in the system. */ function pift_compatibility_list() { $compatibility_list = array(); $query = new EntityFieldQuery(); $query->entityCondition('entity_type', 'taxonomy_term') ->propertyCondition('vid', variable_get('project_release_api_vocabulary', -1)) ->fieldCondition('field_release_recommended', 'value', '1'); $result = $query->execute(); if (isset($result['taxonomy_term'])) { $term_ids = array_keys($result['taxonomy_term']); $terms = entity_load('taxonomy_term', $term_ids); } foreach ($terms as $key => $term) { $compatibility_list[$key] = $term->name; } return $compatibility_list; }