Fixed incorrect file reference & form declaration typo
[project/project_issue_file_test.git] / pift.module
1 <?php
2
3 /**
4 * @file
5 * Integrates into project to provide an automated testing hub.
6 *
7 * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
8 */
9
10 /*
11 * Variables loaded as constants.
12 */
13 define('PIFT_FREQUENCY', variable_get('pift_frequency', -1));
14 define('PIFT_LAST', variable_get('pift_last', 0));
15 define('PIFT_KEY', variable_get('pift_key', ''));
16 define('PIFT_SERVER', variable_get('pift_server', ''));
17 define('PIFT_DESCRIPTION', variable_get('pift_description', ''));
18 define('PIFT_FOLLOWUP_FAIL', variable_get('pift_followup_fail', 0));
19 define('PIFT_FOLLOWUP_RETEST', variable_get('pift_followup_retest', 0));
20 define('PIFT_REGEX', variable_get('pift_regex', '/(\.diff|\.patch)$/'));
21 define('PIFT_PID', variable_get('pift_pid', 3060)); // TODO: Drupal.org specific, to expediate the D7 port
22 define('PIFT_RETEST', variable_get('pift_retest', 24 * 60 * 60));
23 define('PIFT_DELETE', variable_get('pift_delete', FALSE));
24 define('PIFT_LAST_RETRIEVE', variable_get('pift_last_retrieve', 1));
25
26 /**
27 * Maximum number of items of the same type to transfer in a single XML-RPC
28 * request.
29 */
30 define('PIFT_XMLRPC_MAX', 50);
31
32 /**
33 * Maximum number of batches to send during a single cron run.
34 */
35 define('PIFT_XMLRPC_MAX_BATCHES', 3);
36
37 /*
38 * Test type codes.
39 */
40 define('PIFT_TYPE_RELEASE', 1);
41 define('PIFT_TYPE_FILE', 2);
42
43 /*
44 * Test status codes.
45 */
46 define('PIFT_STATUS_QUEUE', 1);
47 define('PIFT_STATUS_SENT', 2);
48 define('PIFT_STATUS_FAIL', 3);
49 define('PIFT_STATUS_PASS', 4);
50
51 if (!defined('PIFR_RESPONSE_ACCEPTED')) {
52 /*
53 * PIFR XML-RPC response codes.
54 */
55 define('PIFR_RESPONSE_ACCEPTED', 1);
56 define('PIFR_RESPONSE_INVALID_SERVER', 2);
57 define('PIFR_RESPONSE_DENIED', 3);
58
59 /*
60 * Project type codes.
61 */
62 define('PIFR_SERVER_PROJECT_TYPE_CORE', 1);
63 define('PIFR_SERVER_PROJECT_TYPE_MODULE', 2);
64 }
65
66 /*
67 * Load required includes.
68 */
69 module_load_include('project.inc', 'pift');
70 module_load_include('test.inc', 'pift');
71
72 /**
73 * Implements hook_menu().
74 */
75 function pift_menu() {
76 $items = array();
77
78 $items['admin/project/pift'] = array(
79 'title' => 'Automated testing',
80 'description' => 'Configure the automated testing project issue integration.',
81 'page callback' => 'drupal_get_form',
82 'page arguments' => array('pift_admin_settings_form'),
83 'access arguments' => array('administer projects'),
84 'file' => 'pift.admin.inc',
85 );
86 $items['pift/retest/%'] = array(
87 'title' => 'Request retest of a file or branch',
88 'page callback' => 'drupal_get_form',
89 'page arguments' => array('pift_pages_retest_confirm_form', 2),
90 'access arguments' => array('pift re-test files'),
91 'file' => 'pift.pages.inc',
92 'type' => MENU_CALLBACK,
93 );
94 $items['pift/delete/%'] = array(
95 'title' => 'Request deletion of a test',
96 'page callback' => 'drupal_get_form',
97 'page arguments' => array('pift_pages_delete_test_confirm_form', 2),
98 'access arguments' => array('pift re-test files'),
99 'file' => 'pift.pages.inc',
100 'type' => MENU_CALLBACK,
101 );
102
103 // TODO: The plan was to remove the testing-status tab for the D7 Port, and
104 // add it back in with Conduit. However, we need somewhere to store the
105 // 'enable testing' checkbox, so leaving this in temporarily until the Conduit
106 // piece is available.
107 $items['node/%node/qa-settings'] = array(
108 'title' => t('Automated Testing'),
109 'access callback' => 'pift_results_visibility',
110 'access arguments' => array(1),
111 'page callback' => 'drupal_get_form',
112 'page arguments' => array('pift_pages_project_issue_settings', 1),
113 'file' => 'pift.pages.inc',
114 'weight' => '5',
115 'type' => MENU_LOCAL_TASK,
116 );
117 return $items;
118 }
119
120 /**
121 * Access callback to determine whether the Automated Testing tab is visible.
122 */
123 function pift_results_visibility($node) {
124 // Only display the tab on projects with releases
125 if (project_node_is_project($node)) {
126 if ($node->field_project_has_releases[$node->language][0]['value']) {
127 // Ensure user has access to enable testing on the node
128 if (user_access('pift enable project testing')) {
129 return TRUE;
130 }
131 }
132 }
133 return FALSE;
134 }
135
136 /**
137 * Implements hook_permission().
138 */
139 function pift_permission() {
140 return array(
141 'pift re-test files' => array(
142 'title' => t('pift re-test files'),
143 'description' => t('Request a file to be re-tested'),
144 ),
145 'pift enable project testing' => array(
146 'title' => t('pift enable project testing'),
147 'description' => t('Enable testing on a project'),
148 ),
149 'pift access project testing tab' => array(
150 'title' => t('pift access project testing tab'),
151 'description' => t('Access the testing tab on projects'),
152 ),
153 );
154 }
155
156 /**
157 * Implements hook_theme().
158 */
159 function pift_theme() {
160 return array(
161 'pift_attachments' => array(
162 'variables' => array(
163 'files' => array(),
164 'closed' => FALSE,
165 ),
166 'file' => 'pift.pages.inc',
167 ),
168 'pift_auto_followup' => array(
169 'variables' => array(
170 'type' => '',
171 'nid' => 0,
172 'cid' => 0,
173 'filename' => '',
174 ),
175 ),
176 );
177 }
178
179 /**
180 * Implements hook_init().
181 */
182 function pift_init() {
183 drupal_add_css(drupal_get_path('module', 'pift') . '/pift.css');
184 }
185
186 /**
187 * Implements hook_cron().
188 */
189 function pift_cron() {
190 if (PIFT_DELETE) {
191 // An issue comment or node has been deleted, remove related test entries.
192 pift_test_delete_files();
193 variable_set('pift_delete', FALSE);
194 }
195
196 // Check if sending is enabled and that the sending frequency has elapsed.
197 $time = REQUEST_TIME;
198 if (PIFT_FREQUENCY != -1 && $time > PIFT_LAST + PIFT_FREQUENCY) {
199 module_load_include('cron.inc', 'pift');
200
201 // Requeue all tests that have passed the re-test interval.
202 // pift_cron_retest(); TODO fix query.
203
204 // Send a batch of queued tests.
205 pift_cron_queue_batch();
206
207 // Retrieve any results that have occured since last cron run.
208 pift_cron_retrieve_results();
209
210 // Store current time as last run.
211 variable_set('pift_last', $time);
212 }
213 }
214
215 /**
216 * Implements hook_versioncontrol_code_arrival().
217 */
218 function pift_versioncontrol_code_arrival(VersioncontrolRepository $repository, VersioncontrolEvent $event) {
219 // Ignore events for disabled projects and non-Git repos.
220 if (!$repository instanceof VersioncontrolGitRepository || !pift_project_enabled($repository->project_nid)) {
221 return;
222 }
223
224 module_load_include('cron.inc', 'pift');
225 $api_versions = pift_core_api_versions();
226
227 $branch_names = array();
228 foreach ($event as $ref) {
229 if (VERSIONCONTROL_GIT_REFTYPE_BRANCH === $ref->reftype) {
230 $branch_names[] = $ref->refname;
231 }
232 }
233
234 $rids = pift_cron_get_release($repository->project_nid, $branch_names);
235 foreach ($rids as $rid) {
236 // Ensure that one of the compatibility terms is present on the release node.
237 $release = node_load($rid);
238 $found = FALSE;
239 // TODO: Update loop, since $release->taxonomy will not exist
240 foreach ($api_versions as $api_version) {
241 if (array_key_exists($api_version, $release->taxonomy)) {
242 // Compatible term found, continue processing.
243 $test_id = db_query('SELECT test_id
244 FROM {pift_test}
245 WHERE type = :type
246 AND id = :id', array(':type' => PIFT_TYPE_RELEASE, ':id' => $rid))->fetchField();
247
248 // If existing test for release, queue it, otherwise add a new test.
249 if ($test_id) {
250 pift_test_requeue($test_id);
251 }
252 else if ($test_id !== 0) {
253 pift_test_add(PIFT_TYPE_RELEASE, $rid);
254 }
255 break;
256 }
257 }
258 }
259 }
260
261 /**
262 * Helper fuction to build a list of releases for this project's repository.
263 *
264 * @param $node The project node object
265 * @param $quiet Silence error messages
266 * @return array List of available labels with corresponding release nodes
267 */
268 function pift_get_releases($node, $quiet = FALSE) {
269 $valid = pift_valid_prefix_list();
270 $branches = array();
271 $topbranches = array();
272 if (project_node_is_project($node)) {
273 $result = db_query("Select b.name from {versioncontrol_release_labels} a
274 join {versioncontrol_labels} b on a.label_id = b.label_id
275 where a.project_nid = :nid", array(':nid' => $node->nid));
276 foreach ($result as $data) {
277 // Filter out any branches < 6 (not accepted by PIFT)
278 // Move '.x' releases to the top
279 if (in_array(substr($data->name, 0, 3), $valid)) {
280 if (substr($data->name, strlen($data->name) - 2, 2) == '.x') {
281 $topbranches[$data->name] = $data->name;
282 }
283 else {
284 $branches[$data->name] = $data->name;
285 }
286 }
287 }
288 // Sort to get the highest branch on top
289 uasort($branches, 'version_compare');
290 uasort($topbranches, 'version_compare');
291 $branches = array_merge($branches, $topbranches);
292 $branches = pift_array_reverse($branches);
293
294 if (empty($branches) && !$quiet) {
295 drupal_set_message(t('No releases found for the given project.'), 'error', FALSE);
296 }
297 }
298 return $branches;
299 }
300
301 /**
302 * Returns a listing of valid project release prefixes (ie. 6.x, 7.x, 8.x)
303 */
304 function pift_valid_prefix_list() {
305 $terms = array();
306 $tids = variable_get('pift_core', array());
307 foreach ($tids as $key => $value) {
308 if (!empty($tids[$key])) {
309 $term = taxonomy_term_load($key);
310 $terms[$key] = $term->name;
311 }
312 }
313 return $terms;
314 }
315
316
317 /**
318 * Alternative function for the php standard array_reverse()
319 *
320 * As array_reverse() would damage numerical array keys, from
321 * http://drupal.org/node/1074220. Code copied from project_git_instructions
322 *
323 * Borrowed from http://php.net/manual/en/function.array-reverse.php#102492
324 */
325 function pift_array_reverse($array) {
326 $array_key = array_keys($array);
327 $array_value = array_values($array);
328
329 $array_return = array();
330 for ($i = 1, $size_of_array = sizeof($array_key); $i <= $size_of_array; $i++) {
331 $array_return[$array_key[$size_of_array -$i]] = $array_value[$size_of_array -$i];
332 }
333
334 return $array_return;
335 }
336
337 /**
338 * Implements hook_form_FORM_ID_alter().
339 */
340 function pift_form_project_issue_node_form_alter(&$form, $form_state, $form_id) {
341 module_load_include('pages.inc', 'pift');
342 pift_pages_description_add($form, $form_state, $form_id);
343 }
344
345 /**
346 * Implements hook_node_view().
347 *
348 * TODO: This approach is no longer valid after the project* changes. The
349 * new approach will be to implement a field formatter on the field_issue_files
350 * field, and add the pift testing results via this field formatter; which
351 * should be much cleaner than removing and injecting the file attachments
352 * table as was done in D6.
353 */
354 //function pift_node_view($node, $view_mode = 'full') {
355 // if (pift_node_is_interesting($node)) {
356 // if (!$a3 && pift_project_enabled($node->project_issue['pid'])) { // Full view.
357 // $files = pift_test_get_files_node($node->nid);
358 // $status = $node->project_issue['sid'];
359 // $node->content['pift_files'] = array(
360 // '#value' => '<div id="pift-results-' . $node->nid . '">' .
361 // theme('pift_attachments', array('files' => $files, 'closed' => $status)) . '</div>',
362 // '#weight' => 50,
363 // );
364 // unset($node->content['files']); // Remove old attachments table.
365 // }
366 // }
367 //}
368
369 /**
370 * Implements hook_node_insert().
371 */
372 function pift_node_insert($node) {
373 if (pift_node_is_interesting($node)) {
374 if (!empty($node->field_issue_files)) {
375 if (pift_test_check_criteria_issue($node)) {
376 pift_test_add_files($node->field_issue_files);
377 }
378 }
379 }
380 }
381
382 /**
383 * Implements hook_node_update().
384 */
385 function pift_node_update($node) {
386 if (pift_node_is_interesting($node)) {
387 if ($node->field_issue_files != $node->original->field_issue_files) {
388 if (!empty($node->field_issue_files)) {
389 if (pift_test_check_criteria_issue($node)) {
390 pift_test_add_files($node->field_issue_files);
391 }
392 }
393 }
394 }
395 }
396
397 /**
398 * Implements hook_node_delete().
399 */
400 function pift_node_delete($node) {
401 if (pift_node_is_interesting($node)) {
402 // Flag pift that a project or issue node was deleted.
403 if (project_node_is_project($node)) {
404 // Remove this project from the pift_project table
405 db_delete('pift_project')->condition('nid', $node->nid)->execute();
406 }
407 variable_set('pift_delete', TRUE);
408 }
409 }
410
411 /**
412 * Check if a node is a project node or issue node with file attached.
413 *
414 * Used in hook_node_*() to filter out irrelevant nodes.
415 */
416 function pift_node_is_interesting($node) {
417 return project_node_is_project($node) || (project_issue_node_is_issue($node) && !empty($node->field_issue_files));
418 }
419
420 /**
421 * Cleanup the inconsistent project_issue property placement.
422 *
423 * TODO: Confirm this approach is no longer valid after the project* changes.
424 *
425 * In order to remove the need to a bunch of conditions all over PIFT, convert
426 * the inconsistent node format to the one used everywhere else. The
427 * inconsistent format is only found during node creation, after a node has
428 * been created and hook_load() is used the properties are prefixed by
429 * project_issue.
430 *
431 *
432 *
433 * @param object $node Node to convert.
434 * @return object Properly formatted node.
435 * @link http://drupal.org/node/519562
436 */
437 //function pift_nodeapi_clean($node) {
438 // $node->project_issue = array();
439 //
440 // $fields = array('pid', 'rid', 'component', 'category', 'priority', 'assigned', 'sid');
441 // foreach ($fields as $field) {
442 // $node->project_issue[$field] = $node->$field;
443 // }
444 //
445 // return $node;
446 //}
447
448 /**
449 * Implements hook_comment_view().
450 *
451 * TODO: This approach is no longer valid after the project* changes. The
452 * new approach will be to implement a field formatter on the field_issue_files
453 * field and comments, and add the pift testing results via this field
454 * formatter; which should be much cleaner than removing and injecting the file
455 * attachments table as was done in D6.
456 */
457 //function pift_comment_view($comment) {
458 // if ($node = pift_comment_is_interesting($comment)) {
459 // if (!empty($comment->files) && pift_project_enabled($node->project_issue['pid'])) {
460 // // Remove comment_upload attachments table and generate new one.
461 // $comment->comment = preg_replace('/<table class="comment-upload-attachments">.*?<\/table>/s', '', $comment->comment);
462 // $files = pift_test_get_files_comment($comment->cid);
463 // $status = $node->project_issue['sid'];
464 // $comment->comment .= '<div id="pift-results-' . $comment->nid . '-' . $comment->cid . '">' .
465 // theme('pift_attachments', array('files' => $files, 'closed' => $status)) . '</div>';
466 // }
467 // }
468 //}
469
470 /**
471 * Implements hook_comment_insert().
472 *
473 * TODO: This approach is no longer valid after the project* changes. Files
474 * and patches are now attached to the node directly.
475 */
476 //function pift_comment_insert($comment) {
477 // if ($node = pift_comment_is_interesting($comment)) {
478 // if (pift_test_check_criteria_issue($node)) {
479 // if (!empty($comment->files)) {
480 // // Add attachments to this comment to the send queue.
481 // $files = comment_upload_load_files($comment['cid']);
482 // pift_test_add_files($files);
483 // }
484 // // Add previously submitted files if issue state changes.
485 // pift_test_add_previous_files($comment['nid']);
486 // }
487 // }
488 //}
489
490 /**
491 * Implements hook_comment_delete().
492 *
493 * TODO: Determine if we want to remove patches when the associated comment is
494 * deleted ... this may now be redundant, since we can simply delete the file
495 * from the field_issue_files field.
496 */
497 //function pift_comment_delete($comment) {
498 // if (pift_comment_is_interesting($comment)) {
499 // variable_set('pift_delete', TRUE);
500 // }
501 //}
502
503 /**
504 * Check that comment is attached to a project_issue node.
505 *
506 * TODO: This is only called from three functions, all of which have been
507 * commented out as part of the D7 port.
508 */
509 //function pift_comment_is_interesting($comment) {
510 // if (($node = node_load($comment->nid)) && $node->type == 'project_issue') {
511 // return $node;
512 // }
513 // return FALSE;
514 //}
515
516 /**
517 * Theme the auto followup comments.
518 *
519 * @param string $type Type of following, either: 'retest' or 'fail'.
520 * @param integer $nid Node ID containting the failed test.
521 * @param integer $cid Comment ID, if applicable, containing the failed test.
522 * @param string $filename Name of file.
523 * @return string HTML output.
524 */
525 function theme_pift_auto_followup($variables) {
526 // TODO: Validate whether these variables are still valid after the project*
527 // changes introduced with the D7 port.
528 $type = $variables['type'];
529 $nid = $variables['nid'];
530 $cid = $variables['cid'];
531 $filename = $variables['filename'];
532 $args = array(
533 '@id' => "pift-results-$nid",
534 '@filename' => $filename,
535 );
536
537 if ($type == 'retest') {
538 if ($cid) {
539 $comment = comment_load($cid);
540 $args['@cid'] = $comment->cid;
541 $args['@comment'] = $comment->subject;
542 return t('<a href="#comment-@cid">@comment</a>: <a href="#@id">@filename</a> queued for re-testing.', $args);
543 }
544 return t('<a href="#@id">@filename</a> queued for re-testing.', $args);
545 }
546 elseif ($type == 'fail') {
547 return t('The last submitted patch, <a href="#@id">@filename</a>, failed testing.', $args);
548 }
549 return '';
550 }
551
552 /**
553 * Get the core compatible API version term IDs.
554 *
555 * @return array Associative array of core compatible API version term IDs.
556 */
557 function pift_core_api_versions() {
558 return array_filter(variable_get('pift_core', array()));
559 }
560
561 /**
562 * Load the core release for the given API term ID.
563 *
564 * @param integer $api_tid Drupal core API compatibility term ID, of the
565 * vocabulary defined by _project_release_get_api_vid.
566 * @return Drupal core release NID.
567 * @see _project_release_get_api_vid()
568 */
569 function pift_core_api_release($api_tid) {
570 static $api_releases = array();
571 if (!isset($api_branches[$api_tid])) {
572 $api_vocabulary = taxonomy_vocabulary_load(variable_get('project_release_api_vocabulary', ''));
573 $taxonomy_field = 'taxonomy_vocabulary_' . $api_vocabulary->machine_name;
574
575 $query = new EntityFieldQuery();
576 $query->entityCondition('entity_type', 'node')
577 ->entityCondition('bundle', 'project_release') // TODO: Drupal.org specific
578 ->fieldCondition('field_release_project', 'target_id', PIFT_PID, '=')
579 ->fieldCondition($taxonomy_field, 'tid', $api_tid)
580 ->propertyOrderBy('nid', 'DESC')
581 ->range(0,1);
582 $result = $query->execute();
583
584 if (isset($result['node'])) {
585 $api_releases[$api_tid] = reset(array_keys($result['node']));
586 }
587 }
588
589 return $api_releases[$api_tid];
590 }
591
592 /**
593 * Return a list of active project release compatibility terms in the system.
594 */
595 function pift_compatibility_list() {
596 $compatibility_list = array();
597
598 $query = new EntityFieldQuery();
599 $query->entityCondition('entity_type', 'taxonomy_term')
600 ->propertyCondition('vid', variable_get('project_release_api_vocabulary', -1))
601 ->fieldCondition('field_release_recommended', 'value', '1');
602 $result = $query->execute();
603
604 if (isset($result['taxonomy_term'])) {
605 $term_ids = array_keys($result['taxonomy_term']);
606 $terms = entity_load('taxonomy_term', $term_ids);
607 }
608
609 foreach ($terms as $key => $term) {
610 $compatibility_list[$key] = $term->name;
611 }
612
613 return $compatibility_list;
614 }