// Copyright (c) 2014 VMware, Inc. // Copyright (c) 2014 Hewlett-Packard Development Company, L.P. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // this regex matches the hash part of review pages var hashRegex = /^\#\/c\/([\d]+)((\/\d+)([.][.](\d+))?)?\/?$/; // this regex matches CI comments var ciRegex = /^(.* CI|Jenkins|Zuul)$/; // this regex matches "Patch set #" var psRegex = /^(Uploaded patch set|Patch Set) (\d+)(:|\.)/; // this regex matches merge failure messages var mergeFailedRegex = /Merge Failed\./; // this regex matches the name of CI systems we trust to report merge failures var trustedCIRegex = /^(OpenStack CI|Jenkins|Zuul)$/; // this regex matches the name+pipeline that we want at the top of the CI list var firstPartyCI = /^(Jenkins|Zuul)/; // this regex matches the pipeline markup var pipelineNameRegex = /Build \w+ \(([-\w]+) pipeline\)/; // The url to full status information on running jobs var zuulStatusURL = 'http://status.openstack.org/zuul'; // The json URL to check for running jobs var zuulStatusJSON = 'https://zuul.openstack.org/status/change/'; // This is a variable to determine if we're in debugging mode, which // lets you globally set it to see what's going on in the flow. var hideci_debug = false; // This is a variable to enable zuul integration, we default it off so // that it creates no additional load, and that it's easy to turn off // the feature. var zuul_inline = false; /** dbg(...) - prints a list of items out to the javascript console.log. This allows us to leave tracing in this file which is a no-op by default, but can be triggered if you enter a javascript console and set hideci_debug = true. */ function dbg () { if (hideci_debug == true) { for (var i = 0; i < arguments.length; i++) { console.log(arguments[i]); } } } function format_time(ms, words) { if (ms == null) { return "unknown"; } var seconds = (+ms)/1000; var minutes = Math.floor(seconds/60); var hours = Math.floor(minutes/60); seconds = Math.floor(seconds % 60); minutes = Math.floor(minutes % 60); r = ''; if (words) { if (hours) { r += hours; r += ' hr '; } r += minutes + ' min'; } else { if (hours < 10) r += '0'; r += hours + ':'; if (minutes < 10) r += '0'; r += minutes + ':'; if (seconds < 10) r += '0'; r += seconds; } return r; } var ci_parse_psnum = function($panel) { var match = psRegex.exec($panel.html()); if (match !== null) { return parseInt(match[2]); } return 0; }; var ci_parse_is_merge_conflict = function($panel) { return (mergeFailedRegex.exec($panel.html()) !== null); }; var ci_find_pipeline = function($panel) { var match = pipelineNameRegex.exec($panel.html()); if (match !== null) { return match[1]; } else { return null; } }; var ci_parse_results = function($panel) { var result_list = []; var test_results = $panel.find("li.comment_test"); var pipeline = null; if (test_results !== null) { test_results.each(function(i, li) { var result = {}; result["name"] = $(li).find("span.comment_test_name").find("a")[0].innerHTML; result["link"] = $(li).find("span.comment_test_name").find("a")[0]; result["result"] = $(li).find("span.comment_test_result")[0]; result_list.push(result); }); } return result_list; }; /*** * function ci_group_by_ci_pipeline - create a group by structure for * iterating on CI's pipelines * * This function takes the full list of comments, the current patch * number, and builds an array of (ci_name_pipelinename, comments array) * tuples. That makes it very easy to process during the display * phase to ensure we only display the latest result for every CI * pipeline. * * Comments that do not have a parsable pipeline (3rd party ci * systems) get collapsed by name, and we specify 'check' for their * pipeline. * **/ var ci_group_by_ci_pipeline = function(current, comments) { var ci_pipelines = []; var ci_pipeline_comments = []; for (var i = 0; i < comments.length; i++) { var comment = comments[i]; if ((comment.psnum != current) || !comment.is_ci || (comment.results.length == 0)) { continue; } var name_pipeline = comment.name; if (comment.pipeline !== null) { name_pipeline += ' ' + comment.pipeline; } var index = ci_pipelines.indexOf(name_pipeline); if (index == -1) { // not found, so create new entries ci_pipelines.push(name_pipeline); ci_pipeline_comments.push([comment]); } else { ci_pipeline_comments[index].push(comment); } } function sort_by_name(a,b) { if (a[0] < b[0]) return -1; else if (a[0] > b[0]) return 1; else return 0; } var results = []; var notfirstparty = []; // we want to separate out first party CI results to always be the // top of the list, and third party CI to come after, so that // hunting for first party CI isn't tough. for (i = 0; i < ci_pipelines.length; i++) { if (firstPartyCI.test(ci_pipelines[i])) { results.push([ci_pipelines[i], ci_pipeline_comments[i]]); } else { notfirstparty.push([ci_pipelines[i], ci_pipeline_comments[i]]); } } notfirstparty.sort(sort_by_name); for (i = 0; i < notfirstparty.length; i++) { results.push(notfirstparty[i]); } return results; }; var ci_parse_comments = function() { var comments = []; $("p").each(function() { var match = psRegex.exec($(this).html()); if (match !== null) { var psnum = parseInt(match[2]); var top = $(this).parent().parent().parent(); // old change screen var name = top.attr("name"); if (!name) { // new change screen name = $(this).parent().parent().parent().children().children()[0].innerHTML; top = $(this).parent().parent().parent().parent(); } var comment = {}; comment.name = name; var date_cell = top.find(".commentPanelDateCell"); if (date_cell.attr("title")) { // old change screen comment.date = date_cell.attr("title"); } else { // new change screen comment.date = $(this).parent().parent().parent().children().children()[2].innerHTML } var comment_panel = $(this).parent(); comment.psnum = psnum; comment.merge_conflict = ci_parse_is_merge_conflict(comment_panel); comment.pipeline = ci_find_pipeline(comment_panel); comment.results = ci_parse_results(comment_panel); comment.is_ci = (ciRegex.exec(comment.name) !== null); comment.is_trusted_ci = (trustedCIRegex.exec(comment.name) !== null); comment.ref = top; dbg("Found comment", comment); comments.push(comment); } }); return comments; }; var ci_latest_patchset = function(comments) { var psnum = 0; for (var i = 0; i < comments.length; i++) { psnum = Math.max(psnum, comments[i].psnum); } return psnum; }; var ci_is_merge_conflict = function(comments) { var latest = ci_latest_patchset(comments); var conflict = false; for (var i = 0; i < comments.length; i++) { var comment = comments[i]; // only if we are actually talking about the latest patch set if (comment.psnum == latest) { if (comment.is_trusted_ci) { conflict = comment.merge_conflict; } } } return conflict; }; var ci_prepare_results_table = function() { // Create a table and insert it after the approval table var table = $("table.test_result_table")[0]; if (!table) { table = document.createElement("table"); $(table).addClass("test_result_table"); $(table).addClass("infoTable").css({"margin-top":"1em", "margin-bottom":"1em"}); var approval_table = $("div.approvalTable"); if (approval_table.length) { var outer_table = document.createElement("table"); $(outer_table).insertBefore(approval_table); var outer_table_row = document.createElement("tr"); $(outer_table).append(outer_table_row); var td = document.createElement("td"); $(outer_table_row).append(td); $(td).css({"vertical-align":"top"}); $(td).append(approval_table); td = document.createElement("td"); $(outer_table_row).append(td); $(td).css({"vertical-align":"top"}); $(td).append(table); } else { var big_table_row = $("div.screen>div>div>table>tbody>tr"); var td = $(big_table_row).children()[1]; $(td).append(table); } } else { $(table).empty(); } return table; }; var ci_display_results = function(comments) { var table = ci_prepare_results_table(); if (ci_is_merge_conflict(comments)) { var mc_header = $("").append($('Patch in Merge Conflict')); mc_header.css('width', '400'); mc_header.css('font-weight', 'bold'); mc_header.css('color', 'red'); mc_header.css('padding-left', '2em'); $(table).append(mc_header); return; } var current = ci_latest_patchset(comments); var ci_pipelines = ci_group_by_ci_pipeline(current, comments); for (var i = 0; i < ci_pipelines.length; i++) { var ci_pipeline_name = ci_pipelines[i][0]; var ci_pipeline_comments = ci_pipelines[i][1]; // the most recent comment on a pipeline var last = ci_pipeline_comments.length - 1; var comment = ci_pipeline_comments[last]; var rechecks = ""; if (last > 0) { rechecks = " (" + last + " rechecks)"; } var header = $("").append($('' + ci_pipeline_name + rechecks + '')); header.append('' + comment.date + ''); $(table).append(header); for (var j = 0; j < comment.results.length; j++) { var result = comment.results[j]; var tr = $(""); tr.append($("").append($(result["link"]).clone())); tr.append($("").append($(result["result"]).clone())); $(table).append(tr); } } }; var set_cookie = function (name, value) { document.cookie = name + "=" + value + "; path=/"; }; var read_cookie = function (name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1, c.length); } if (c.indexOf(nameEQ) == 0) { return c.substring(nameEQ.length, c.length); } } return null; }; var ci_toggle_visibility = function(comments, showOrHide) { if (!comments) { comments = ci_parse_comments(); } $.each(comments, function(i, comment) { if (comment.is_ci) { $(comment.ref).toggle(showOrHide); } }); }; var ci_hide_ci_comments = function(comments) { if (!comments) { comments = ci_parse_comments(); } $.each(comments, function(i, comment) { if (comment.is_ci) { $(comment.ref).hide(); } }); }; var ci_page_loaded = function() { if (hashRegex.test(window.location.hash)) { dbg("Searching for ci results on " + window.location.hash); $("#toggleci").show(); var comments = ci_parse_comments(); ci_display_results(comments); var showOrHide = 'true' == read_cookie('show-ci-comments'); if (!showOrHide) { ci_hide_ci_comments(comments); } if (zuul_inline === true) { ci_zuul_for_change(comments); } } else { $("#toggleci").hide(); } }; var ci_current_change = function() { var change = hashRegex.exec(window.location.hash); if (change.length > 1) { return change[1]; } return null; }; // recursively find the zuul status change, will be much more // efficient once zuul supports since json status. var ci_find_zuul_status = function (data, change_psnum) { var objects = []; for (var i in data) { if (!data.hasOwnProperty(i)) continue; if (typeof data[i] == 'object') { objects = objects.concat(ci_find_zuul_status(data[i], change_psnum)); } else if (i == 'id' && data.id == change_psnum) { objects.push(data); } } return objects; }; var ci_zuul_all_status = function(jobs) { var status = "passing"; for (var i = 0; i < jobs.length; i++) { if (jobs[i].result && jobs[i].result != "SUCCESS") { status = "failing"; break; } } return status; }; var ci_zuul_display_status = function(status) { var zuul_table = $("table.zuul_result_table")[0]; if (!zuul_table) { var test_results = $("table.test_result_table")[0]; zuul_table = document.createElement("table"); $(zuul_table).addClass("zuul_result_table"); $(zuul_table).addClass("infoTable").css({"margin-bottom":"1em"}); if (test_results) { $(test_results).prepend(zuul_table); } } $(zuul_table).empty(); $(zuul_table).show(); $(zuul_table).append("Change currently being tested (full status)"); for (var i = 0; i < status.length; i++) { var item = status[i]; var pipeline = item.jobs[0].pipeline; var passing = (item.failing_reasons && item.failing_reasons.length > 0) ? "failing" : "passing"; var timeleft = item.remaining_time; var row = ""; if (pipeline != null) { row += pipeline + " pipeline: " + passing; row += " (" + format_time(timeleft, false) + " left)"; } else { row += "in between pipelines, status should update shortly"; } row += ""; $(zuul_table).append(row); } }; var ci_zuul_clear_status = function () { var zuul_table = $("table.zuul_result_table")[0]; if (zuul_table) { $(zuul_table).hide(); } }; var ci_zuul_process_changes = function(data, change_psnum) { var zuul_status = ci_find_zuul_status(data, change_psnum); if (zuul_status.length) { ci_zuul_display_status(zuul_status); } else { ci_zuul_clear_status(); } }; var ci_zuul_for_change = function(comments) { if (!comments) { comments = ci_parse_comments(); } var change = ci_current_change(); var psnum = ci_latest_patchset(comments); var change_psnum = change + "," + psnum; // do the loop recursively in ajax (function poll() { $.ajax({ url: zuulStatusJSON + change_psnum, type: "GET", success: function(data) { dbg("Found zuul data for " + change_psnum, data); ci_zuul_process_changes(data, change_psnum); }, dataType: "json", complete: setTimeout(function() { // once we are done with this cycle in the loop we // schedule ourselves again in the future with // setTimeout. However, by the time the function // actually gets called, other things might have // happened, and we may want to just dump the data // instead. // // the UI might have gone hidden (user was bored, // switched to another tab / window). // // the user may have navigated to another review url, // so the data returned is not relevant. // // both cases are recoverable when the user navigates // around, because a new "thread" gets started on // ci_page_load. // // BUG(sdague): there is the possibility that the user // navigates away from a page and back fast enough // that the first "thread" is not dead, and a second // one is started. greghaynes totally said he'd come // up with a way to fix that. if (window.zuul_enable_status_updates == false) { return; } var current = ci_current_change(); if (current && change_psnum.indexOf(current) != 0) { // window url is dead, so don't schedule any more future // updates for this url. return; } poll(); }, 15000), timeout: 5000 }); })(); }; window.onload = function() { var input = document.createElement("input"); input.id = "toggleci"; input.type = "button"; input.className = "gwt-Button"; input.value = "Toggle CI"; input.onclick = function () { // Flip the cookie var showOrHide = 'true' == read_cookie('show-ci-comments'); set_cookie('show-ci-comments', showOrHide ? 'false' : 'true'); // Hide or Show existing comments based on cookie ci_toggle_visibility(null, !showOrHide); }; document.body.appendChild(input); MutationObserver = window.MutationObserver || window.WebKitMutationObserver; var observer = new MutationObserver(function(mutations, observer) { var span = $("span.rpcStatus"); $.each(mutations, function(i, mutation) { if (mutation.target === span[0] && mutation.attributeName === "style" && (!(span.is(":visible:")))) { ci_page_loaded(); } }); }); observer.observe(document, { subtree: true, attributes: true }); $(document).on({ 'show.visibility': function() { window.zuul_enable_status_updates = true; ci_page_loaded(); }, 'hide.visibility': function() { window.zuul_enable_status_updates = false; } }); };