Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Tools / RebaselineLogServer / main.py
blob95c394d9f2092108f4eefcc16c4cb51e277284d7
1 # Copyright (C) 2013 Google Inc. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
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.
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.
29 import datetime
30 import json
31 import logging
32 import sys
33 import webapp2
35 from google.appengine.api import mail
36 from google.appengine.ext import ndb
37 from google.appengine.ext.webapp import template
38 from google.appengine.ext.db import BadRequestError
40 # A simple log server for rebaseline-o-matic.
42 # Accepts updates to the same log entry and shows a simple status page.
43 # Has a special state for the case where there are no NeedsRebaseline
44 # lines in TestExpectations to avoid cluttering the log with useless
45 # entries every 30 seconds.
47 # Other than that, new updatelog calls append to the most recent log
48 # entry until they have the newentry parameter, in which case, it
49 # starts a new log entry.
51 LOG_PARAM = "log"
52 NEW_ENTRY_PARAM = "newentry"
53 NUM_LOGS_PARAM = "numlogs"
54 BEFORE_PARAM = "before"
55 COMMAND_PARAM = "command"
57 SENDER_ADDRESS = "bot@blinkrebaseline.appspotmail.com"
58 REBASELINES_LIST_ADDRESS = "blink-rebaselines@chromium.org"
60 class LogEntry(ndb.Model):
61 content = ndb.TextProperty()
62 date = ndb.DateTimeProperty(auto_now_add=True)
65 class SendMailCommand(object):
66 def execute(self, command_data):
67 if "to" not in command_data or "subject" not in command_data or "body" not in command_data:
68 return "Malformed command JSON"
69 to = command_data["to"]
70 subject = command_data["subject"]
71 body = command_data["body"]
72 htmlbody = command_data["htmlbody"] if "htmlbody" in command_data else body
73 mail.send_mail(sender=SENDER_ADDRESS,
74 to=to,
75 reply_to="@".join(["apavlov", "chromium.org"]),
76 cc=REBASELINES_LIST_ADDRESS,
77 subject="Auto-rebaseline bot: " + subject,
78 body=body,
79 html=htmlbody)
80 return "Sent mail to %s (%s)" % (to, subject)
83 COMMANDS = {
84 "sendmail": SendMailCommand()
88 def logs_query():
89 return LogEntry.query().order(-LogEntry.date)
92 def execute_command(command_data):
93 if not command_data:
94 return "ERROR: No command data"
96 if "name" not in command_data:
97 return "Command is missing a name: %s" % json.dumps(command_data)
99 name = command_data["name"]
100 command = COMMANDS.get(name)
101 if not command:
102 return "Command %s is unknown"
103 try:
104 return command.execute(command_data)
105 except Exception as e:
106 return "Exception caught when executing command '%s'" % name
109 class UpdateLog(webapp2.RequestHandler):
110 def post(self):
111 new_log_data = self.request.POST.get(LOG_PARAM)
113 command_json = self.request.POST.get(COMMAND_PARAM)
114 command_data = json.loads(command_json) if command_json else None
116 command_output = ""
117 if command_data is not None:
118 command_output = "\n" + execute_command(command_data)
119 new_log_data += command_output
121 # This entry is set to on whenever a new auto-rebaseline run is going to
122 # start logging entries. If this is not on, then the log will get appended
123 # to the most recent log entry.
124 new_entry = self.request.POST.get(NEW_ENTRY_PARAM) == "on"
126 out = "Wrote new log entry."
127 if not new_entry:
128 log_entries = logs_query().fetch(1)
129 if log_entries:
130 log_entry = log_entries[0]
131 log_entry.date = datetime.datetime.now()
132 log_entry.content = log_entry.content + "\n" + new_log_data
133 out = "Added to existing log entry."
135 if new_entry or not log_entries:
136 log_entry = LogEntry(content=new_log_data)
138 try:
139 log_entry.put()
140 except BadRequestError:
141 out = "Created new log entry because the previous one exceeded the max length."
142 LogEntry(content=new_log_data, date=datetime.datetime.now()).put()
144 self.response.headers.add_header("Content-Type", "text/plain")
145 self.response.out.write(out + command_output)
148 class UploadForm(webapp2.RequestHandler):
149 def get(self):
150 self.response.out.write(template.render("uploadform.html", {
151 "update_log_url": "/updatelog",
152 "set_no_needs_rebaseline_url": "/noneedsrebaselines",
153 "log_param": LOG_PARAM,
154 "new_entry_param": NEW_ENTRY_PARAM,
155 "command_param": COMMAND_PARAM
159 class ShowLatest(webapp2.RequestHandler):
160 def get(self):
161 query = logs_query()
163 before = self.request.get(BEFORE_PARAM)
164 if before:
165 date = datetime.datetime.strptime(before, "%Y-%m-%dT%H:%M:%SZ")
166 query = query.filter(LogEntry.date < date)
168 num_logs = self.request.get(NUM_LOGS_PARAM)
169 logs = query.fetch(int(num_logs) if num_logs else 3)
171 self.response.out.write(template.render("logs.html", {
172 "logs": logs,
173 "num_logs_param": NUM_LOGS_PARAM,
174 "before_param": BEFORE_PARAM,
178 routes = [
179 ('/uploadform', UploadForm),
180 ('/updatelog', UpdateLog),
181 ('/', ShowLatest),
184 app = webapp2.WSGIApplication(routes, debug=True)