From 7637cbe0331cc89ffadaf565255ac5f998c5f737 Mon Sep 17 00:00:00 2001
From: Corey Maynard <me@coreymaynard.com>
Date: Thu, 30 Jun 2011 16:40:57 -0400
Subject: [PATCH] Some changes to the way comments are stored to hold a key
 for user or None. Add in the Tests class. Pulls test cases
 from the wiki, but tracks results, tester, and time in
 testing locally

---
 bodhi/models/models.py |  174 ++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 146 insertions(+), 28 deletions(-)

diff --git a/bodhi/models/models.py b/bodhi/models/models.py
index ebd8801..04d8c2b 100644
--- a/bodhi/models/models.py
+++ b/bodhi/models/models.py
@@ -6,7 +6,7 @@ import xmlrpclib
 import transaction
 
 from textwrap import wrap
-from datetime import datetime
+from datetime import datetime, timedelta
 
 from sqlalchemy import create_engine
 from sqlalchemy import Unicode, UnicodeText, PickleType, Integer, Boolean
@@ -26,7 +26,7 @@ from bodhi.exceptions import InvalidRequest, RPMNotFound
 
 log = logging.getLogger(__name__)
 
-config = {} # FIXME!
+config = {'wiki_test_cases': 'https://fedoraproject.org/w/api.php'} # FIXME!
 from bunch import Bunch
 identity = Bunch(current=Bunch(user_name=u'bodhi'))
 
@@ -117,14 +117,14 @@ class KarmaType(DeclEnum):
 #        Column('update_id', Integer, ForeignKey('updates.id')),
 #        Column('build_id', Integer, ForeignKey('builds.id')))
 
-update_bug_table = Table('update_bug_table', metadata,
-        Column('update_id', Integer, ForeignKey('updates.id')),
-        Column('bug_id', Integer, ForeignKey('bugs.id')))
-
 comment_bug_table = Table('comment_bug_table', metadata,
         Column('comment_id', Integer, ForeignKey('comments.id')),
         Column('bug_id', Integer, ForeignKey('bugs.id')))
 
+update_bug_table = Table('update_bug_table', metadata,
+        Column('update_id', Integer, ForeignKey('updates.id')),
+        Column('bug_id', Integer, ForeignKey('bugs.id')))
+
 update_cve_table = Table('update_cve_table', metadata,
         Column('update_id', Integer, ForeignKey('updates.id')),
         Column('cve_id', Integer, ForeignKey('cves.id')))
@@ -154,6 +154,7 @@ class Release(Base):
 class Package(Base):
     __tablename__ = 'packages'
 
+
     id = Column(Integer, primary_key=True)
     name = Column(Unicode(50), unique=True, nullable=False)
     committers = Column(PickleType, default=None)
@@ -188,6 +189,7 @@ class Build(Base):
     update_id = Column(Integer, ForeignKey('updates.id'))
 
     release = relation('Release', backref='builds', lazy=False)
+    tests = relation('Test', backref='build', lazy=False)
 
     def get_latest(self):
         """ Return the path to the last released srpm of this package """
@@ -277,6 +279,54 @@ class Build(Base):
         """ Return the path of this built update """
         return os.path.join(config.get('build_dir'), *get_nvr(self.nvr))
 
+    def get_test_cases(self, limit=10):
+        """Return a list of the Test Cases from the wiki"""
+        if config.get('wiki_test_cases'):
+            from simplemediawiki import MediaWiki
+            wiki = MediaWiki(config.get('wiki_test_cases'))
+            cat_page = 'Category:Package %s test cases' % (self.nvr.split('-')[0])
+
+            limit = 10
+
+            def list_categorymembers(wiki, cat_page, limit=10):
+                # Build query arguments and call wiki
+                query = dict(action='query', list='categorymembers', cmtitle=cat_page)
+                response = wiki.call(query)
+                members = [entry.get('title') for entry in
+                           response.get('query',{}).get('categorymembers',{})
+                           if entry.has_key('title')]
+
+                # Determine whether we need to recurse
+                idx = 0
+                while True:
+                    if idx >= len(members) or limit <= 0:
+                        break
+                    # Recurse?
+                    if members[idx].startswith('Category:') and limit > 0:
+                        members.extend(list_categorymembers(wiki, members[idx], limit-1))
+                        members.remove(members[idx]) # remove Category from list
+                    else:
+                        idx += 1
+
+                return members
+
+            return list_categorymembers(wiki, cat_page)
+
+        return []
+
+    def make_test_cases(self):
+        """Populate the Test model with the test cases pulled from the wiki.
+        Raises a BodhiException if this update already has tests."""
+        from bodhi.exceptions import BodhiException
+        if len(self.tests) == 0:
+            session = DBSession()
+            for test in self.get_test_cases():
+                t = Test(name=test.replace('QA:Testcase ', ''), build=self)
+                session.add(t)
+            session.commit()
+        else:
+            raise BodhiException("Test cases already exist for %s." % (self.nvr))
+
 
 class Update(Base):
     __tablename__ = 'updates'
@@ -352,7 +402,7 @@ class Update(Base):
 
     @property
     def karma_counts(self):
-        """ Return a dictionary of all the counts for each karma type on this update """
+        """Return a dictionary of all the counts for each karma type on this update"""
         types = {}
         for c in self.comments:
             if types.has_key(c.karma.name):
@@ -362,11 +412,11 @@ class Update(Base):
         return types
 
     def karma_by_type(self, type):
-        """ Return the number points awarded to a specific karma type """
-        return self.karma_counts[type] if self.karma_counts.has_key(type) else 0
+        """Return the number points awarded to a specific karma type"""
+        return self.karma_counts[type.name] if self.karma_counts.has_key(type.name) else 0
 
     def get_bugstring(self, show_titles=False):
-        """Return a space-delimited string of bug numbers for this update """
+        """Return a space-delimited string of bug numbers for this update"""
         val = u''
         if show_titles:
             i = 0
@@ -544,14 +594,15 @@ class Update(Base):
         """
         Add a comment to this update about a change in status
         """
+        user = User.query.filter(User.name=='bodhi').one()
         if self.status is UpdateStatus.stable:
             self.comment(u'This update has been pushed to stable',
-                         user=u'bodhi')
+                         user=user)
         elif self.status is UpdateStatus.testing:
             self.comment(u'This update has been pushed to testing',
-                         user=u'bodhi')
+                         user=user)
         elif self.status is UpdateStatus.obsolete:
-            self.comment(u'This update has been obsoleted', user=u'bodhi')
+            self.comment(u'This update has been obsoleted', user=user)
 
     def send_update_notice(self):
         log.debug("Sending update notice for %s" % self.title)
@@ -600,9 +651,9 @@ class Update(Base):
             val += u"  Update ID: %s\n" % self.alias
         val += u"""    Release: %s
      Status: %s
-       Type: %s\n""" % (self.release.long_name, self.status.description, 
+       Type: %s\n""" % (self.release.long_name, self.status.description,
                       self.type.description)
-        
+
         for c in self.comments:
             val += u"    %s: %d\n" % (c.karma.description.title(), self.karma_counts[c.karma.name])
 
@@ -731,7 +782,7 @@ class Update(Base):
         any bugs related to this specific comment, either the registered user
         or anonymous author. """
         if not self.request and (karma == KarmaType.passes or karma == KarmaType.works) and \
-                self.karma_by_type('works') + self.karma_by_type('passes') == 2:
+                self.karma_by_type(KarmaType.works) + self.karma_by_type(KarmaType.passes) == 2:
             log.info("Automatically marking %s as stable" % self.title)
             self.request = UpdateRequest.stable
             self.pushed = False
@@ -740,7 +791,7 @@ class Update(Base):
             mail.send_admin('stablekarma', self)
         if self.status is UpdateStatus.testing and \
                 (karma == KarmaType.fails or karma == KarmaType.new_errors) and \
-                self.karma_by_type('fails') + self.karma_by_type('new_errors') > 0:
+                self.karma_by_type(KarmaType.fails) + self.karma_by_type(KarmaType.new_errors) > 0:
             log.info("Automatically unpushing %s" % self.title)
             self.obsolete()
             mail.send(self.get_maintainers(), 'unstable', self)
@@ -805,8 +856,6 @@ class Update(Base):
         action, changes in the repository will not propagate until the next
         mash takes place.
         """
-        from sqlalchemy.orm.exc import NoResultFound
-
         log.debug("Obsoleting %s" % self.title)
         self.untag()
         self.status = UpdateStatus.obsolete
@@ -846,11 +895,10 @@ class Comment(Base):
     id = Column(Integer, primary_key=True)
     karma = Column(KarmaType.db_type(), default=KarmaType.works,
             nullable=False)
-    
+
     text = Column(UnicodeText)
     timestamp = Column(DateTime, default=datetime.utcnow)
 
-    #One To One
     update_id = Column(Integer, ForeignKey('updates.id'))
     user_id = Column(Integer, ForeignKey('users.id'))
 
@@ -989,6 +1037,42 @@ class Bug(Base):
         return "%s/show_bug.cgi?id=%s" % (config.get('bz_baseurl'), self.bug_id)
 
 
+class Test(Base):
+    """Test names are pulled from the wiki, but results and testers are tracked within
+    Bodhi."""
+    __tablename__ = 'tests'
+
+    id = Column(Integer, primary_key=True)
+
+    name = Column(Unicode(64))
+    results = Column(UnicodeText)
+
+    date_submitted = Column(DateTime, default=datetime.now)
+    date_completed = Column(DateTime)
+
+    #Keys
+    user_id = Column(Integer, ForeignKey('users.id'))
+    build_id = Column(Integer, ForeignKey('builds.id'))
+
+    @property
+    def completion_time(self):
+        """Figure out how long it took for a test to be completed"""
+        try:
+            return self.date_completed - self.date_submitted
+        except:
+            return timedelta(0)
+
+    @property
+    def average_completion(self):
+        """Get the average test time for all instances of a given test in a package"""
+        deltas = []
+        for test in Test.query.filter(Build.package == self.build.package):
+            if test.name == self.name:
+                deltas.append(test.completion_time)
+
+        return sum(deltas, timedelta(0)) / len(deltas)
+
+
 class User(Base):
     __tablename__ = 'users'
 
@@ -999,6 +1083,7 @@ class User(Base):
     # One-to-many relationships
     comments = relation(Comment, backref='user', lazy=False)
     updates = relation(Update, backref='user', lazy=False)
+    tests = relation(Test, backref='user', lazy=False)
 
 
 #class Stack(Base):
@@ -1011,29 +1096,62 @@ class User(Base):
 #    # updates
 
 def populate():
+    import random
     session = DBSession()
+
     user = User(name=u'bodhi')
     session.add(user)
-    release = Release(name=u'F15', long_name=u'Fedora 15', id_prefix=u'FEDORA', dist_tag=u'dist-f15')
+
+    release = Release(name=u'F15', long_name=u'Fedora 15', id_prefix=u'FEDORA',
+            dist_tag=u'dist-f15')
     session.add(release)
-    pkg = Package(name=u'bodhi')
+
+    pkg = Package(name=u'abrt')
     session.add(pkg)
-    build = Build(nvr=u'bodhi-2.0-1.fc15', package=pkg, release=release)
+
+    build = Build(nvr=u'abrt-2.0-1.fc15', package=pkg, release=release)
     session.add(build)
+
+    build2 = Build(nvr=u'firefox-2.0-1.fc15', package=pkg, release=release)
+    session.add(build2)
+
+    build3 = Build(nvr=u'abrt-3.0-1.fc15', package=pkg, release=release)
+    session.add(build3)
+
+    build4 = Build(nvr=u'firefox-3.0-1.fc15', package=pkg, release=release)
+    session.add(build4)
+
     bug = Bug(bug_id=12345)
     bug2 = Bug(bug_id=12346)
     session.add(bug)
     session.add(bug2)
-    update = Update(builds=[build], user=user, notes=u'Useful details!', release=release)
-    update.type = UpdateType.bugfix
-    update.bugs.append(bug)
-    update.bugs.append(bug2)
+
+    update = Update(builds=[build, build2], user=user, notes=u'First Update',
+            type=UpdateType.bugfix, release=release, bugs=[bug, bug2])
     session.add(update)
+
+    update2 = Update(builds=[build3, build4], user=user, notes=u'Second Update',
+            type=UpdateType.bugfix, release=release)
+    session.add(update2)
+
     session.commit()
+
     update.comment('Always look on the bright side of life', KarmaType.passes, None, user)
     update.comment('It turned me into a newt', KarmaType.fails, [bug], user)
     update.comment('I got better', KarmaType.works, [bug, bug2], user)
     update.comment('Run Away!', KarmaType.passes)
+    build.make_test_cases()
+    build2.make_test_cases()
+    build3.make_test_cases()
+    build4.make_test_cases()
+
+    for test in Test.query.all():
+        test.date_completed = test.date_submitted + timedelta(days=random.randint(1,10))
+        test.user = user
+        test.results = "Passed Test"
+        session.add(test)
+
+    session.commit()
 
 def initialize_sql(engine):
     DBSession.configure(bind=engine)
-- 
1.7.5.4

