ce7f09861f7a8f3d18595a8d7a2291f2a186f30b
[WebKit-https.git] / Tools / QueueStatusServer / handlers / statusbubble.py
1 # Copyright (C) 2009 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6
7 #     * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #     * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #     * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 import operator
30
31 from google.appengine.ext import webapp
32 from google.appengine.ext.webapp import template
33
34 from model.attachment import Attachment
35 from model.activeworkitems import ActiveWorkItems
36 from model.patchlog import PatchLog
37 from model.queues import Queue
38 from model.queuestatus import QueueStatus
39 from model.workitems import WorkItems
40 from sets import Set
41
42 progress_statuses = Set([
43     "Cleaned working directory",
44     "Updated working directory",
45     "Applied patch",
46     "Built patch",
47     "Watchlist applied",
48     "Style checked",
49     "ChangeLog validated",
50     "Built patch",
51     "Able to build without patch",
52     "Passed tests",
53     "Able to pass tests without patch",
54     "Landed patch"
55 ])
56
57 class StatusBubble(webapp.RequestHandler):
58     def _iso_time(self, time):
59         return "[[" + time.isoformat() + "]]"
60
61     # queue_position includes items that are already active, so it's misleading.
62     # For a queue that has 8 bots, being #9 in the queue actually means being #1.
63     def _real_queue_position(self, queue, queue_position):
64         active_work_items = queue.active_work_items().item_ids
65         if active_work_items:
66             return queue_position - len(active_work_items)
67         else:
68             return queue_position
69
70     def _latest_resultative_status(self, statuses):
71         for status in statuses:
72             if not status.message in progress_statuses:
73                 return status
74         return None
75
76     def _build_message_for_provisional_failure(self, queue, attachment, queue_position, statuses):
77         patch_log = PatchLog.lookup_if_exists(attachment.id, queue.name())
78         if not patch_log:
79             return "Internal error. No PatchLog entry in database."
80
81         is_active = attachment.id in queue.active_work_items().item_ids
82         try_count = patch_log.retry_count + (not is_active)  # retry_count is updated when a new attempt starts.
83         latest_resultative_status = self._latest_resultative_status(statuses)
84         tree_is_red = latest_resultative_status.message == "Unable to pass tests without patch (tree is red?)" or latest_resultative_status.message == "Unable to build without patch"
85
86         message = latest_resultative_status.message + "."
87         if is_active:
88             if tree_is_red:
89                 message += "\n\nTrying again now."
90             else:
91                 message += "\n\nThis result is not final, as the issue could be a pre-existing one. Trying to determine that now."
92                 if try_count == 1:
93                     message += "\n\nPreviously completed a round of testing, but couldn't arrive at a definitive conclusion."
94                 elif try_count > 1:
95                     message += "\n\nPreviously completed " + str(try_count) + " rounds of testing, but couldn't arrive at a definitive conclusion."
96         else:
97             real_queue_position = self._real_queue_position(queue, queue_position)
98             if tree_is_red:
99                 message += "\n\nWill try again, currently #" + str(real_queue_position) + " in queue."
100             else:
101                 message += "\n\nThis result is not final, as the issue can be a pre-existing one. "
102                 if try_count == 1:
103                     message += "Completed one round "
104                 else:
105                     message += "Completed " + str(try_count) + " rounds "
106                 message += "of testing trying to determine that, but couldn't arrive at a definitive conclusion yet.\n\nWill try again, currently #" + str(real_queue_position) + " in queue."
107         message += "\n\nPlease click the bubble for detailed results.\n\n" + self._iso_time(statuses[0].date)
108         return message
109
110     def _build_bubble(self, queue, attachment, queue_position):
111         bubble = {
112             "name": queue.short_name().lower(),
113             "attachment_id": attachment.id,
114         }
115         # 10 recent statuses is enough to always include a resultative one, if there were any at all.
116         statuses = QueueStatus.all().filter('queue_name =', queue.name()).filter('active_patch_id =', attachment.id).order('-date').fetch(limit=10)
117         if not statuses:
118             if attachment.id in queue.active_work_items().item_ids:
119                 bubble["state"] = "started"
120                 bubble["details_message"] = "Started processing, no output yet.\n\n" + self._iso_time(queue.active_work_items().time_for_item(attachment.id))
121                 bubble["may_fail_to_apply"] = True
122             else:
123                 real_queue_position = self._real_queue_position(queue, queue_position)
124                 bubble["state"] = "none"
125                 bubble["details_message"] = "Waiting in queue, processing has not started yet.\n\nPosition in queue: " + str(real_queue_position)
126                 bubble["queue_position"] = real_queue_position
127                 bubble["may_fail_to_apply"] = True
128         else:
129             latest_resultative_status = self._latest_resultative_status(statuses)
130             if not latest_resultative_status:
131                 bubble["state"] = "started"
132                 bubble["details_message"] = ("Started processing.\n\nRecent messages:\n\n"
133                     + "\n".join([status.message for status in statuses]) + "\n\n" + self._iso_time(statuses[0].date))
134             elif statuses[0].message == "Pass":
135                 bubble["state"] = "pass"
136                 bubble["details_message"] = "Pass\n\n" + self._iso_time(statuses[0].date)
137             elif statuses[0].message == "Fail":
138                 bubble["state"] = "fail"
139                 bubble["details_message"] = statuses[1].message + "\n\n" + self._iso_time(statuses[0].date)
140             elif statuses[0].message == "Error: " + queue.name() + " did not process patch.":
141                 bubble["state"] = "none"
142                 bubble["details_message"] = "The patch is no longer eligible for processing."
143                 if len(statuses) > 1:
144                     if len(statuses) == 2:
145                         bubble["details_message"] += " One message was logged while the patch was still eligible:\n\n"
146                     else:
147                         bubble["details_message"] += " Some messages were logged while the patch was still eligible:\n\n"
148                     bubble["details_message"] += "\n".join([status.message for status in statuses[1:]]) + "\n\n" + self._iso_time(statuses[0].date)
149                 bubble["may_fail_to_apply"] = True
150             elif statuses[0].message == "Error: " + queue.name() + " unable to apply patch.":
151                 bubble["state"] = "fail"
152                 bubble["details_message"] = statuses[1].message + "\n\n" + self._iso_time(statuses[0].date)
153                 bubble["failed_to_apply"] = True
154             elif statuses[0].message.startswith("Error: "):
155                 bubble["state"] = "error"
156                 bubble["details_message"] = "\n".join([status.message for status in statuses]) + "\n\n" + self._iso_time(statuses[0].date)
157             elif queue_position:
158                 bubble["state"] = "provisional-fail"
159                 bubble["details_message"] = self._build_message_for_provisional_failure(queue, attachment, queue_position, statuses)
160             else:
161                 bubble["state"] = "error"
162                 bubble["details_message"] = ("Internal error. Latest status implies that the patch should be in queue, but it is not. Recent messages:\n\n"
163                     + "\n".join([status.message for status in statuses]) + "\n\n" + self._iso_time(statuses[0].date))
164
165         if "details_message" in bubble:
166             bubble["details_message"] = queue.display_name() + "\n\n" + bubble["details_message"]
167
168         return bubble
169
170     def _should_show_bubble_for(self, attachment, queue):
171          # Any pending queue is shown.
172         if attachment.position_in_queue(queue):
173             return True
174         # EWS queues are also shown when complete.
175         return bool(queue.is_ews() and attachment.status_for_queue(queue))
176
177     def _build_bubbles_for_attachment(self, attachment):
178         show_submit_to_ews = True
179         bubbles = []
180         for queue in Queue.all():
181             if not self._should_show_bubble_for(attachment, queue):
182                 continue
183             queue_position = attachment.position_in_queue(queue)
184             bubble = self._build_bubble(queue, attachment, queue_position)
185             if bubble:
186                 bubbles.append(bubble)
187             # If at least one EWS queue has status, we don't show the submit-to-ews button.
188             if queue.is_ews():
189                 show_submit_to_ews = False
190
191         failed_to_apply = any(map(lambda bubble: "failed_to_apply" in bubble, bubbles))
192         had_output = all(map(lambda bubble: not "may_fail_to_apply" in bubble and not "failed_to_apply" in bubble, bubbles))
193
194         return (bubbles, show_submit_to_ews, failed_to_apply and (not had_output) and (not show_submit_to_ews))
195
196     def get(self, attachment_id_string):
197         attachment_id = int(attachment_id_string)
198         attachment = Attachment(attachment_id)
199         bubbles, show_submit_to_ews, show_failure_to_apply = self._build_bubbles_for_attachment(attachment)
200
201         template_values = {
202             "bubbles": bubbles,
203             "attachment_id": attachment_id,
204             "show_submit_to_ews": show_submit_to_ews,
205             "show_failure_to_apply": show_failure_to_apply,
206         }
207         self.response.out.write(template.render("templates/statusbubble.html", template_values))