| Index: appengine_apps/chromium_status/appengine_module/chromium_status/commit_queue.py
|
| diff --git a/appengine_apps/chromium_status/appengine_module/chromium_status/commit_queue.py b/appengine_apps/chromium_status/appengine_module/chromium_status/commit_queue.py
|
| deleted file mode 100644
|
| index 1350c461527d22b286051b0873f0452ceb0c9bad..0000000000000000000000000000000000000000
|
| --- a/appengine_apps/chromium_status/appengine_module/chromium_status/commit_queue.py
|
| +++ /dev/null
|
| @@ -1,573 +0,0 @@
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""Commit queue status."""
|
| -
|
| -import cgi
|
| -import datetime
|
| -import json
|
| -import logging
|
| -import re
|
| -import sys
|
| -import urllib2
|
| -
|
| -from google.appengine.api import memcache
|
| -from google.appengine.api import users
|
| -from google.appengine.ext import db
|
| -from google.appengine.ext.db import polymodel
|
| -
|
| -from appengine_module.chromium_status.base_page import BasePage
|
| -from appengine_module.chromium_status import utils
|
| -
|
| -
|
| -TRY_SERVER_MAP = (
|
| - 'SUCCESS', 'WARNINGS', 'FAILURE', 'SKIPPED', 'EXCEPTION', 'RETRY',
|
| -)
|
| -
|
| -# Verification name in commit-queue/verification/*.py. Initialized at the bottom
|
| -# of this file.
|
| -EVENT_MAP = {}
|
| -
|
| -
|
| -class Owner(db.Model):
|
| - """key == email address."""
|
| - email = db.EmailProperty()
|
| -
|
| - @staticmethod
|
| - def to_key(owner): # pragma: no cover
|
| - return '<%s>' % owner
|
| -
|
| -
|
| -class PendingCommit(db.Model):
|
| - """parent is Owner."""
|
| - created = db.DateTimeProperty()
|
| - done = db.BooleanProperty(default=False)
|
| - issue = db.IntegerProperty()
|
| - patchset = db.IntegerProperty()
|
| -
|
| - @staticmethod
|
| - def to_key(issue, patchset, owner): # pragma: no cover
|
| - # TODO(maruel): My bad, shouldn't have put owner in the key.
|
| - return '<%d-%d-%s>' % (issue, patchset, owner)
|
| -
|
| -
|
| -class VerificationEvent(polymodel.PolyModel):
|
| - """parent is PendingCommit."""
|
| - created = db.DateTimeProperty(auto_now_add=True)
|
| - result = db.IntegerProperty()
|
| - timestamp = db.DateTimeProperty()
|
| -
|
| - @property
|
| - def as_html(self):
|
| - raise NotImplementedError()
|
| -
|
| -
|
| -class TryServerEvent(VerificationEvent):
|
| - name = 'try server'
|
| - build = db.IntegerProperty()
|
| - builder = db.StringProperty()
|
| - clobber = db.BooleanProperty()
|
| - job_name = db.StringProperty()
|
| - revision = db.IntegerProperty()
|
| - url = db.StringProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - if self.build is not None:
|
| - out = '<a href="%s">"%s" on %s, build #%s</a>' % (
|
| - cgi.escape(self.url),
|
| - cgi.escape(self.job_name),
|
| - cgi.escape(self.builder),
|
| - cgi.escape(str(self.build)))
|
| - if (self.result is not None and
|
| - 0 <= self.result < len(TRY_SERVER_MAP[self.result])):
|
| - out = '%s - result: %s' % (out, TRY_SERVER_MAP[self.result])
|
| - return out
|
| - else:
|
| - # TODO(maruel): Load the json
|
| - # ('http://build.chromium.org/p/tryserver.chromium/json/builders/%s/'
|
| - # 'pendingBuilds') % self.builder and display the rank.
|
| - return '"%s" on %s (pending)' % (
|
| - cgi.escape(self.job_name),
|
| - cgi.escape(self.builder))
|
| -
|
| - @classmethod
|
| - def to_key(cls, packet): # pragma: no cover
|
| - if not packet.get('builder') or not packet.get('job_name'):
|
| - return None
|
| - return '<%s-%s-%s>' % (
|
| - cls.name, packet['builder'], packet['job_name'])
|
| -
|
| -
|
| -class TryJobRietveldEvent(VerificationEvent):
|
| - """Same thing as TryServerEvent. Should probably be kept in sync with
|
| - TryServerEvent.
|
| -
|
| - It comes from commit-queue/verification/try_job_on_rietveld.py.
|
| - """
|
| - name = 'try job rietveld'
|
| - build = db.IntegerProperty()
|
| - builder = db.StringProperty()
|
| - clobber = db.BooleanProperty()
|
| - job_name = db.StringProperty()
|
| - # TODO(maruel): Transition all revision properties to string, since it could
|
| - # be a hash for git commits.
|
| - revision = db.StringProperty()
|
| - url = db.StringProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - if self.build is not None:
|
| - out = '<a href="%s">"%s" on %s, build #%s</a>' % (
|
| - cgi.escape(self.url),
|
| - cgi.escape(self.job_name),
|
| - cgi.escape(self.builder),
|
| - cgi.escape(str(self.build)))
|
| - if (self.result is not None and
|
| - 0 <= self.result < len(TRY_SERVER_MAP[self.result])):
|
| - out = '%s - result: %s' % (out, TRY_SERVER_MAP[self.result])
|
| - return out
|
| - else:
|
| - # TODO(maruel): Load the json
|
| - # ('http://build.chromium.org/p/tryserver.chromium/json/builders/%s/'
|
| - # 'pendingBuilds') % self.builder and display the rank.
|
| - return '"%s" on %s (pending)' % (
|
| - cgi.escape(self.job_name),
|
| - cgi.escape(self.builder))
|
| -
|
| - @classmethod
|
| - def to_key(cls, packet): # pragma: no cover
|
| - if not packet.get('builder') or not packet.get('job_name'):
|
| - return None
|
| - return '<%s-%s-%s>' % (
|
| - cls.name, packet['builder'], packet['job_name'])
|
| -
|
| -
|
| -class PresubmitEvent(VerificationEvent):
|
| - name = 'presubmit'
|
| - duration = db.FloatProperty()
|
| - output = db.TextProperty()
|
| - timed_out = db.BooleanProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - return '<pre class="output">%s</pre>' % cgi.escape(self.output)
|
| -
|
| - @classmethod
|
| - def to_key(cls, _): # pragma: no cover
|
| - # There shall be only one PresubmitEvent per PendingCommit.
|
| - return '<%s>' % cls.name
|
| -
|
| -
|
| -class CommitEvent(VerificationEvent):
|
| - name = 'commit'
|
| - output = db.TextProperty()
|
| - revision = db.IntegerProperty()
|
| - url = db.StringProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - out = '<pre class="output">%s</pre>' % cgi.escape(self.output)
|
| - if self.url:
|
| - out += '<a href="%s">Revision %s</a>' % (
|
| - cgi.escape(self.url),
|
| - cgi.escape(str(self.revision)))
|
| - elif self.revision:
|
| - out += '<br>Revision %s' % cgi.escape(str(self.revision))
|
| - return out
|
| -
|
| - @classmethod
|
| - def to_key(cls, _): # pragma: no cover
|
| - return '<%s>' % cls.name
|
| -
|
| -
|
| -class InitialEvent(VerificationEvent):
|
| - name = 'initial'
|
| - revision = db.IntegerProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - return 'Looking at new commit, using revision %s' % (
|
| - cgi.escape(str(self.revision)))
|
| -
|
| - @classmethod
|
| - def to_key(cls, _): # pragma: no cover
|
| - return '<%s>' % cls.name
|
| -
|
| -
|
| -class AbortEvent(VerificationEvent):
|
| - name = 'abort'
|
| - output = db.TextProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - return '<pre class="output">%s</pre>' % cgi.escape(self.output)
|
| -
|
| - @classmethod
|
| - def to_key(cls, _): # pragma: no cover
|
| - return '<%s>' % cls.name
|
| -
|
| -
|
| -class WhyNotEvent(VerificationEvent):
|
| - name = 'why not'
|
| - message = db.TextProperty()
|
| -
|
| - @property
|
| - def as_html(self): # pragma: no cover
|
| - return '<pre class="output">%s</pre>' % cgi.escape(self.message)
|
| -
|
| - @classmethod
|
| - def to_key(cls, _): # pragma: no cover
|
| - return '<%s>' % cls.name
|
| -
|
| -
|
| -def get_owner(owner): # pragma: no cover
|
| - """Efficient querying of Owner with memcache."""
|
| - key = Owner.to_key(owner)
|
| - obj = memcache.get(key, namespace='Owner')
|
| - if not obj:
|
| - obj = Owner.get_or_insert(key_name=key, email=owner)
|
| - memcache.set(key, obj, time=60*60, namespace='Owner')
|
| - return obj
|
| -
|
| -
|
| -def get_pending_commit(issue, patchset, owner, timestamp): # pragma: no cover
|
| - """Efficient querying of PendingCommit with memcache."""
|
| - owner_obj = get_owner(owner)
|
| - key = PendingCommit.to_key(issue, patchset, owner)
|
| - obj = memcache.get(key, namespace='PendingCommit')
|
| - if not obj:
|
| - obj = PendingCommit.get_or_insert(
|
| - key_name=key, parent=owner_obj, issue=issue, patchset=patchset,
|
| - owner=owner, created=timestamp)
|
| - memcache.set(key, obj, time=60*60, namespace='PendingCommit')
|
| - return obj
|
| -
|
| -
|
| -class CQBasePage(BasePage):
|
| - """Returns a web page or json data about commit queue state.
|
| -
|
| - Can filter for everyone, a particular user or a particular issue.
|
| -
|
| - Query args:
|
| - - format: can be 'html' or 'json'.
|
| - - limit: maximum number of elements to result. default is 100.
|
| - """
|
| -
|
| - def get(self, *args): # pragma: no cover
|
| - query = self._get_query(*args)
|
| - if not query:
|
| - # The user probably used /me without being logged.
|
| - self.redirect(users.create_login_url(self.request.url))
|
| - return
|
| - out_format = self.request.get('format', 'html')
|
| - if out_format == 'json':
|
| - return self._get_as_json(query)
|
| - else:
|
| - return self._get_as_html(query)
|
| -
|
| - def _get_query(self, owner=None, issue=None,
|
| - patchset=None): # pragma: no cover
|
| - """Returns None on query failure."""
|
| - query = VerificationEvent.all().order('-timestamp')
|
| - ancestor = None
|
| - if owner:
|
| - owner = self._parse_user(owner)
|
| - if not owner:
|
| - return None
|
| - ancestor = db.Key.from_path('Owner', Owner.to_key(owner))
|
| -
|
| - if issue:
|
| - issue = int(issue)
|
| - if patchset:
|
| - patchset = int(patchset)
|
| - pending_key = PendingCommit.to_key(issue, patchset, owner)
|
| - ancestor = db.Key.from_path(
|
| - 'PendingCommit', pending_key, parent=ancestor)
|
| - else:
|
| - # Only show the last object since it's complex to do a OR with multiple
|
| - # ancestors.
|
| - ancestor = db.Query(PendingCommit, keys_only=True).filter(
|
| - 'issue =', issue).ancestor(ancestor).order('-created').get()
|
| -
|
| - if ancestor:
|
| - query.ancestor(ancestor)
|
| - return query
|
| -
|
| - def _get_limit(self): # pragma: no cover
|
| - limit = self.request.get('limit')
|
| - if limit and limit.isdigit():
|
| - limit = int(limit)
|
| - else:
|
| - limit = 100
|
| - return limit
|
| -
|
| - def _get_as_json(self, query): # pragma: no cover
|
| - self.response.headers['Content-Type'] = 'application/json'
|
| - self.response.headers['Access-Control-Allow-Origin'] = '*'
|
| - data = json.dumps([s.AsDict() for s in query.fetch(self._get_limit())])
|
| - callback = self.request.get('callback')
|
| - if callback:
|
| - if re.match(r'^[a-zA-Z$_][a-zA-Z$0-9._]*$', callback):
|
| - data = '%s(%s);' % (callback, data)
|
| - self.response.out.write(data)
|
| -
|
| - def _get_as_html(self, query): # pragma: no cover
|
| - raise NotImplementedError()
|
| -
|
| - def _parse_user(self, user): # pragma: no cover
|
| - user = urllib2.unquote(user.strip('/'))
|
| - if user == 'me':
|
| - if not self.user:
|
| - user = None
|
| - else:
|
| - user = self.user.email()
|
| - return user
|
| -
|
| -
|
| -class OwnerStats(object): # pragma: no cover
|
| - """CQ usage statistics for a single user."""
|
| - def __init__(self, now, owner, last_day, last_week, last_month, forever):
|
| - # Since epoch in float.
|
| - self.now = now
|
| - # User instance.
|
| - self.owner = owner
|
| - assert all(isinstance(i, PendingCommit) for i in last_day)
|
| - self.last_day = last_day
|
| - assert all(isinstance(i, PendingCommit) for i in last_week)
|
| - self.last_week = last_week
|
| - assert isinstance(last_month, int)
|
| - self.last_month = last_month
|
| - assert isinstance(forever, int)
|
| - self.forever = forever
|
| - # Gamify ALL the things!
|
| - self.points = (
|
| - len(self.last_day) * 10 +
|
| - len(self.last_week) * 5 +
|
| - self.last_month * 2 +
|
| - self.forever)
|
| -
|
| -
|
| -class OwnerQuery(object): # pragma: no cover
|
| - def __init__(self, owner_key, now):
|
| - self.owner_key = owner_key
|
| - self.now = now
|
| - since = lambda x: now - datetime.timedelta(days=x)
|
| - self._owner = db.get_async(owner_key)
|
| - self._last_day = self._pendings().filter('created >=', since(1)).run()
|
| - self._last_week = self._pendings().filter(
|
| - 'created >=', since(7)).filter('created <', since(1)).run()
|
| - # These block.
|
| - self.last_month = self._pendings().filter(
|
| - 'created >=', since(30)).count()
|
| - self.forever = self._pendings(keys_only=True).count()
|
| -
|
| - def _pendings(self, **kwargs):
|
| - return PendingCommit.all(**kwargs).ancestor(self.owner_key)
|
| -
|
| - def to_stats(self):
|
| - obj = OwnerStats(
|
| - self.now,
|
| - self._owner.get_result(),
|
| - list(self._last_day),
|
| - list(self._last_week),
|
| - self.last_month,
|
| - self.forever)
|
| - memcache.add(
|
| - self.owner_key.name(), obj, 2*60*60, namespace='cq_owner_stats')
|
| - return obj
|
| -
|
| -
|
| -def to_link(pending): # pragma: no cover
|
| - return '<a href="/cq/%s/%s/%s">%s</a>' % (
|
| - pending.parent_key().name()[1:-1],
|
| - pending.issue,
|
| - pending.patchset,
|
| - pending.issue)
|
| -
|
| -
|
| -def get_owner_stats(owner_key, now): # pragma: no cover
|
| - """Returns an OnwerStats instance for the Owner."""
|
| - obj = memcache.get(owner_key.name(), 'cq_owner_stats')
|
| - if obj:
|
| - return obj
|
| - return OwnerQuery(owner_key, now).to_stats()
|
| -
|
| -
|
| -def monthly_top_contributors(): # pragma: no cover
|
| - """Returns the top monthly contributors as a list of OwnerStats."""
|
| - obj = memcache.get('monthly', 'cq_top')
|
| - if not obj:
|
| - now = datetime.datetime.utcnow()
|
| - last_pendings = PendingCommit.all(
|
| - keys_only=True).order('-created').fetch(1000)
|
| - # Make it use asynchronous queries.
|
| - obj = [
|
| - get_owner_stats(o, now) for o in set(p.parent() for p in last_pendings)
|
| - ]
|
| - memcache.add('monthly', obj, 2*60*60, namespace='cq_top')
|
| - return obj
|
| -
|
| -
|
| -class Summary(CQBasePage): # pragma: no cover
|
| - def _get_as_html(self, _):
|
| - owners = []
|
| - for stats in monthly_top_contributors():
|
| - data = {
|
| - 'email': stats.owner.email,
|
| - 'last_day': ', '.join(to_link(i) for i in stats.last_day),
|
| - 'last_week': ', '.join(to_link(i) for i in stats.last_week),
|
| - 'last_month': stats.last_month,
|
| - 'forever': stats.forever,
|
| - }
|
| - owners.append(data)
|
| - owners.sort(key=lambda x: -x['last_month'])
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Commit queue')
|
| - template_values['data'] = owners
|
| - self.DisplayTemplate('cq_owners.html', template_values, use_cache=True)
|
| -
|
| -
|
| -def ordinal_number(i):
|
| - if (i % 10) == 1 and (i % 100) != 11:
|
| - return '%dst' % i
|
| - elif (i % 10) == 2 and (i % 100) != 12:
|
| - return '%dnd' % i
|
| - elif (i % 10) == 3 and (i % 100) != 13:
|
| - return '%drd' % i
|
| - else:
|
| - return '%dth' % i
|
| -
|
| -
|
| -class TopScore(CQBasePage): # pragma: no cover
|
| - def _get_as_html(self, _):
|
| - owners = [
|
| - {
|
| - 'name': stats.owner.email.split('@', 1)[0].upper(),
|
| - 'points': stats.points,
|
| - }
|
| - for stats in monthly_top_contributors()
|
| - ]
|
| - owners.sort(key=lambda x: -x['points'])
|
| - for i in xrange(len(owners)):
|
| - owners[i]['rank'] = ordinal_number(i + 1)
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Commit queue')
|
| - template_values['data'] = owners
|
| - self.DisplayTemplate('cq_top_score.html', template_values, use_cache=True)
|
| -
|
| -
|
| -class User(CQBasePage): # pragma: no cover
|
| - def _get_as_html(self, query):
|
| - pending_commits_events = {}
|
| - pending_commits = {}
|
| - for event in query.fetch(self._get_limit()):
|
| - # Implicitly find PendingCommit's.
|
| - pending_commit = event.parent()
|
| - if not pending_commit:
|
| - logging.warn('Event %s is corrupted, can\'t find %s' % (
|
| - event.key().id_or_name(), event.parent_key().id_or_name()))
|
| - continue
|
| - pending_commits_events.setdefault(pending_commit.key(), []).append(event)
|
| - pending_commits[pending_commit.key()] = pending_commit
|
| -
|
| - sorted_data = []
|
| - for pending_commit in sorted(
|
| - pending_commits.itervalues(), key=lambda x: x.created, reverse=True):
|
| - sorted_data.append(
|
| - (pending_commit,
|
| - reversed(pending_commits_events[pending_commit.key()])))
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Commit queue')
|
| - template_values['data'] = sorted_data
|
| - self.DisplayTemplate('cq_owner.html', template_values, use_cache=True)
|
| -
|
| -
|
| -class Issue(CQBasePage): # pragma: no cover
|
| - def _get_as_html(self, query):
|
| - pending_commits_events = {}
|
| - pending_commits = {}
|
| - for event in query.fetch(self._get_limit()):
|
| - # Implicitly find PendingCommit's.
|
| - pending_commit = event.parent()
|
| - if not pending_commit:
|
| - logging.warn('Event %s is corrupted, can\'t find %s' % (
|
| - event.key().id_or_name(), event.parent_key().id_or_name()))
|
| - continue
|
| - pending_commits_events.setdefault(pending_commit.key(), []).append(event)
|
| - pending_commits[pending_commit.key()] = pending_commit
|
| -
|
| - sorted_data = []
|
| - for pending_commit in sorted(
|
| - pending_commits.itervalues(), key=lambda x: x.issue):
|
| - sorted_data.append(
|
| - (pending_commit,
|
| - reversed(pending_commits_events[pending_commit.key()])))
|
| - template_values = self.InitializeTemplate(self.APP_NAME + ' Commit queue')
|
| - template_values['data'] = sorted_data
|
| - self.DisplayTemplate('cq_owner.html', template_values, use_cache=True)
|
| -
|
| -
|
| -class Receiver(BasePage): # pragma: no cover
|
| - @utils.requires_write_access
|
| - def post(self):
|
| - def load_values():
|
| - for p in self.request.get_all('p'):
|
| - try:
|
| - yield json.loads(p)
|
| - except ValueError:
|
| - logging.warn('Discarding invalid packet %r' % p)
|
| -
|
| - count = 0
|
| - for packet in load_values():
|
| - cls = EVENT_MAP.get(packet.get('verification'))
|
| - if (not cls or
|
| - not isinstance(packet.get('issue'), int) or
|
| - not isinstance(packet.get('patchset'), int) or
|
| - not packet.get('timestamp') or
|
| - not isinstance(packet.get('owner'), basestring)):
|
| - logging.warning('Ignoring packet %s' % packet)
|
| - continue
|
| -
|
| - payload = packet.get('payload', {})
|
| - # TODO(maruel): Convert the type implicitly, because storing a int into a
|
| - # FloatProperty or a StringProperty will raise a BadValueError.
|
| - values = dict(
|
| - (i, payload[i]) for i in cls.properties()
|
| - if i not in ('_class', 'pending') and i in payload)
|
| - # Inject the timestamp.
|
| - values['timestamp'] = datetime.datetime.utcfromtimestamp(
|
| - packet['timestamp'])
|
| - pending = get_pending_commit(
|
| - packet['issue'], packet['patchset'], packet['owner'],
|
| - values['timestamp'])
|
| -
|
| - logging.debug('New packet %s' % cls.__name__)
|
| - key_name = cls.to_key(values)
|
| - if not key_name:
|
| - continue
|
| -
|
| - # TODO(maruel) Use an async transaction, in batch.
|
| - obj = cls.get_by_key_name(key_name, parent=pending)
|
| - # Compare the timestamps. Events could arrive in the reverse order.
|
| - if not obj or obj.timestamp <= values['timestamp']:
|
| - # This will override the previous obj if it existed.
|
| - cls(parent=pending, key_name=key_name, **values).put()
|
| - count += 1
|
| - elif obj:
|
| - logging.warn('Received object out of order')
|
| -
|
| - # Cache the fact that the change was committed in the PendingCommit.
|
| - if packet['verification'] == 'commit':
|
| - pending.done = True
|
| - pending.put()
|
| -
|
| - self.response.out.write('%d\n' % count)
|
| -
|
| -
|
| -def bootstrap(): # pragma: no cover
|
| - # Used by _parse_packet() to find the right model to use from the
|
| - # 'verification' value of the packet.
|
| - module = sys.modules[__name__]
|
| - for i in dir(module):
|
| - if i.endswith('Event') and i != 'VerificationEvent':
|
| - obj = getattr(module, i)
|
| - EVENT_MAP[obj.name] = obj
|
|
|