>From 15cced9b5c074bb598c63294d0563bbf432cb9cc Mon Sep 17 00:00:00 2001
From: Josef Skladanka <jskladan@redhat.com>
Date: Fri, 9 Jul 2010 12:21:14 +0200
Subject: [PATCH 1/2] autoqa.BaseTest class & ExceptionCatcher decorator

---
 hooks/post-bodhi-update/test_class_template.py |   38 +++++++----
 hooks/post-koji-build/test_class_template.py   |   36 ++++++----
 hooks/post-repo-update/test_class_template.py  |   25 +++++---
 hooks/post-tree-compose/test_class_template.py |   29 ++++++--
 lib/python/decorators.py                       |   85 ++++++++++++++++++++++++
 lib/python/test.py                             |   54 +++++++++++++++
 6 files changed, 221 insertions(+), 46 deletions(-)
 create mode 100644 lib/python/decorators.py
 create mode 100644 lib/python/test.py

diff --git a/hooks/post-bodhi-update/test_class_template.py b/hooks/post-bodhi-update/test_class_template.py
index bd572a1..40d1833 100644
--- a/hooks/post-bodhi-update/test_class_template.py
+++ b/hooks/post-bodhi-update/test_class_template.py
@@ -18,33 +18,41 @@
 # Author: Will Woods <wwoods@redhat.com>
 
 import autoqa.util
-from autotest_lib.client.bin import test, utils
-from autotest_lib.client.bin.test_config import config_loader
+from autoqa.test import BaseTest
+from autoqa.decorators import ExceptionCatcher
+from autotest_lib.client.bin import utils
 
-class post_repo_update_test_class_name(test.test):
+class post_repo_update_test_class_name(BaseTest):
     version = 1 # increment if setup() changes
 
-    def initialize(self, envrs, name, title, updateid, targettag, config):
-        self.config = config_loader(config, self.tmpdir)
+    def setup(self):
+        # this is where you can install required packages and such, e.g.:
+        #utils.system('yum -y install yum-utils')
+        pass
+
+    @ExceptionCatcher("self.initialize_failed")
+    def initialize(self, envrs, name, title, updateid, targettag, config, **kwargs):
+        super(post_repo_update_test_class_name, self).__init__(config) #FIXME: change classname!
         # This is where you can look up more info about the package, e.g.:
         #import autoqa.koji_utils
         #koji = autoqa.koji_utils.SimpleKojiClientSession()
         #self.pkgurls = {}
         #for envr in envrs.split(','):
         #    self.pkgurls[envr] = koji.nvr_to_urls(envr)
-        #URL of logs/results stored on autotest-server
-        self.autotest_url = autoqa.util.make_autotest_url(self.config)
-
-    def setup(self):
-        # this is where you can install required packages and such, e.g.:
-        #utils.system('yum -y install yum-utils')
-        pass
+        #
+        # Use these variables to store information about the test.
+        # They are published by self.postprocess (see autoqa.test.BaseTest)
+        #self.result = None # result value ("PASSED", "FAILED", ...)
+        #self.summary = ""
+        #self.highlights = ""
+        #self.outputs = ""
 
-    def run_once(self, envr, name, title, updateid, targettag, config):
+    @ExceptionCatcher("self.run_once_failed")
+    def run_once(self, envrs, name, title, updateid, targettag, config, **kwargs):
         parentlist = ' '.split(parents)
         cmd = 'test_binary --url %s' % baseurl
         # You can get stuff from the [test] section of autoqa.conf like this
         email = self.config.get('test','result_email')
         if email:
-            cmd += ' --email %s --subject %s' % (email, reponame)
-        self.results = utils.system_output(cmd, retain_output=True)
+            cmd += ' --email %s --subject %s' % (email, name)
+        self.outputs = utils.system_output(cmd, retain_output=True)
diff --git a/hooks/post-koji-build/test_class_template.py b/hooks/post-koji-build/test_class_template.py
index 9f63003..5d3c579 100644
--- a/hooks/post-koji-build/test_class_template.py
+++ b/hooks/post-koji-build/test_class_template.py
@@ -18,31 +18,39 @@
 # Author: Will Woods <wwoods@redhat.com>
 
 import autoqa.util
-from autotest_lib.client.bin import test, utils
-from autotest_lib.client.bin.test_config import config_loader
+from autoqa.test import BaseTest
+from autoqa.decorators import ExceptionCatcher
+from autotest_lib.client.bin import utils
 
-class post_repo_update_test_class_name(test.test):
+class post_repo_update_test_class_name(BaseTest):
     version = 1 # increment if setup() changes
 
-    def initialize(self, envr, name, kojitag, config):
-        self.config = config_loader(config, self.tmpdir)
-        # This is where you can look up more info about the package, e.g.:
-        #import autoqa.koji_utils
-        #koji = autoqa.koji_utils.SimpleKojiClientSession()
-        #self.pkgurls = koji.nvr_to_urls(envr)
-        #URL of logs/results stored on autotest-server
-        self.autotest_url = autoqa.util.make_autotest_url(self.config)
-
     def setup(self):
         # this is where you can install required packages and such, e.g.:
         #utils.system('yum -y install yum-utils')
         pass
 
-    def run_once(self, envr, name, kojitag):
+    @ExceptionCatcher("self.initialize_failed")
+    def initialize(self, envr, name, kojitag, config, **kwargs):
+        super(post_repo_update_test_class_name, self).__init__(config) #FIXME: change classname!
+        # This is where you can look up more info about the package, e.g.:
+        #import autoqa.koji_utils
+        #koji = autoqa.koji_utils.SimpleKojiClientSession()
+        #self.pkgurls = koji.nvr_to_urls(envr)
+        #
+        # Use these variables to store information about the test.
+        # They are published by self.postprocess (see autoqa.test.BaseTest)
+        #self.result = None # result value ("PASSED", "FAILED", ...)
+        #self.summary = ""
+        #self.highlights = ""
+        #self.outputs = ""
+
+    @ExceptionCatcher("self.run_once_failed")
+    def run_once(self, envr, name, kojitag, **kwargs):
         parentlist = ' '.split(parents)
         cmd = 'test_binary --url %s' % baseurl
         # You can get stuff from the [test] section of autoqa.conf like this
         email = self.config.get('test','result_email')
         if email:
-            cmd += ' --email %s --subject %s' % (email, reponame)
-        self.results = utils.system_output(cmd, retain_output=True)
+            cmd += ' --email %s --subject %s' % (email, name)
+        self.outputs = utils.system_output(cmd, retain_output=True)
diff --git a/hooks/post-repo-update/test_class_template.py b/hooks/post-repo-update/test_class_template.py
index 8070b4e..aa8fca4 100644
--- a/hooks/post-repo-update/test_class_template.py
+++ b/hooks/post-repo-update/test_class_template.py
@@ -20,27 +20,34 @@
 # HEY YOU, NOTE: you should change the Copyright / Author.
 
 import autoqa.util
-from autotest_lib.client.bin import test, utils
-from autotest_lib.client.bin.test_config import config_loader
+from autoqa.test import BaseTest
+from autoqa.decorators import ExceptionCatcher
+from autotest_lib.client.bin import utils
 
-class post_repo_update_test_class_name(test.test):
+class post_repo_update_test_class_name(BaseTest):
     version = 1 # increment if setup() changes
 
-    def initialize(self, config):
-        self.config = config_loader(config, self.tmpdir)
-        #URL of logs/results stored on autotest-server
-        self.autotest_url = autoqa.util.make_autotest_url(self.config)
-
     def setup(self):
         # this is where you can install required packages and such, e.g.:
         #utils.system('yum -y install yum-utils')
         pass
 
-    def run_once(self, baseurl, parents, reponame):
+    @ExceptionCatcher("self.initialize_failed")
+    def initialize(self, baseurl, parents,reponame, config, **kwargs):
+        super(post_repo_update_test_class_name, self).__init__(config) #FIXME: change classname!
+        # Use these variables to store information about the test.
+        # They are published by self.postprocess (see autoqa.test.BaseTest)
+        #self.result = None # result value ("PASSED", "FAILED", ...)
+        #self.summary = ""
+        #self.highlights = ""
+        #self.outputs = ""
+
+    @ExceptionCatcher("self.run_once_failed")
+    def run_once(self, baseurl, parents, reponame, **kwargs):
         parentlist = ' '.split(parents)
         cmd = 'test_binary --url %s' % baseurl
         # You can get stuff from the [test] section of autoqa.conf like this
         email = self.config.get('test','result_email')
         if email:
             cmd += ' --email %s --subject %s' % (email, reponame)
-        self.results = utils.system_output(cmd, retain_output=True)
+        self.outputs = utils.system_output(cmd, retain_output=True)
diff --git a/hooks/post-tree-compose/test_class_template.py b/hooks/post-tree-compose/test_class_template.py
index 94a0617..798fd3b 100644
--- a/hooks/post-tree-compose/test_class_template.py
+++ b/hooks/post-tree-compose/test_class_template.py
@@ -20,26 +20,39 @@
 # HEY YOU, NOTE: you should change the Copyright / Author.
 
 import autoqa.util
-from autotest_lib.client.bin import test, utils
-from autotest_lib.client.bin.test_config import config_loader
+from autoqa.test import BaseTest
+from autoqa.decorators import ExceptionCatcher
+from autotest_lib.client.bin import utils
 
-class post_tree_compose_test_class_name(test.test):
+class post_tree_compose_test_class_name(BaseTest):
     version = 1 # increment if setup() changes
 
-    def initialize(self, config):
-        self.config = config_loader(config, self.tmpdir)
-        #URL of logs/results stored on autotest-server
-        self.autotest_url = autoqa.util.make_autotest_url(self.config)
-
     def setup(self):
         # this is where you can install required packages and such, e.g.:
         #utils.system('yum -y install yum-utils')
         pass
 
+    @ExceptionCatcher("self.initialize_failed")
+    def initialize(self, config, **kwargs):
+        super(post_tree_compose_test_class_name, self).__init__(config) #FIXME: change classname!
+        # This is where you can look up more info about the package, e.g.:
+        #import autoqa.koji_utils
+        #koji = autoqa.koji_utils.SimpleKojiClientSession()
+        #self.pkgurls = koji.nvr_to_urls(envr)
+        #
+        # Use these variables to store information about the test.
+        # They are published by self.postprocess (see autoqa.test.BaseTest)
+        #self.result = None # result value ("PASSED", "FAILED", ...)
+        #self.summary = ""
+        #self.highlights = ""
+        #self.outputs = ""
+
+
+    @ExceptionCatcher("self.run_once_failed")
     def run_once(self, baseurl, treename):
         cmd = 'test_binary --url %s' % baseurl
         # You can get stuff from the [test] section of autoqa.conf like this
         email = self.config.get('test','result_email')
         if email:
             cmd += ' --email %s --subject %s' % (email, treename)
-        self.results = utils.system_output(cmd, retain_output=True)
+        self.outputs = utils.system_output(cmd, retain_output=True)
diff --git a/lib/python/decorators.py b/lib/python/decorators.py
new file mode 100644
index 0000000..1a6d5d0
--- /dev/null
+++ b/lib/python/decorators.py
@@ -0,0 +1,85 @@
+class ExceptionCatcher(object):
+    """
+    Because autotest-driven tests immediately end, when exception
+    is raised, use this as a decorator around the run_once function.
+    If an exception is raised in the decorated function, this decorator
+    captures it, then runs the function specified by on_exception, and
+    finally re-raises the exception.
+
+    @ExceptionCatcher(on_exception, *args, **kwargs)
+
+    on_exception parameter can be either pointer to function or string
+    if it's a string, it's evaluated as if it was a name of a class method.
+
+    Example:
+
+        >>> def helper_func():
+        >>>     print "Outside Foo!"
+
+        >>> class Foo(object):
+        >>>     @ExceptionCatcher("self.helper_func") #or "helper_func"
+        >>>     def some_func_1(self):
+        >>>         #calls self.helper_func on exception
+        >>>         raise Exception()
+
+        >>>     @ExceptionCatcher(helper_func) #notice the missing quotes
+        >>>     def some_func_2(self):
+        >>>         #calls the  outside helper_func on exception
+        >>>         raise Exception()
+
+        >>>     def helper_func(self):
+        >>>         print "Inside Foo!"
+
+        >>> x = Foo()
+        >>> x.some_func_1()
+        Inside Foo!
+        Traceback (most recent call last):
+        ...
+        Exception
+
+        >>> x.some_func_2()
+        Outside Foo!
+        Traceback (most recent call last):
+        ...
+        Exception
+    """
+
+    def __init__(self, on_exception, *args, **kwargs):
+        """
+        on_exception - string with class function name, or pointer to function
+        *args, **kwargs - args for the on_exception function
+        """
+        self.oe = on_exception
+        #if the on_exception is string, then it's handled as a
+        #class member function
+        self.oe_is_self_method = type(self.oe) is type("")
+        if self.oe_is_self_method and self.oe.startswith('self.'):
+            self.oe = self.oe[5:]
+        self.oe_a = args
+        self.oe_k = kwargs
+
+    def __call__(self, f):
+        def newf(*args, **kwargs):
+            #if called with arguments, first is stored as
+            #'self' reference, in case it's needed
+            if len(args) > 0:
+                f_self = args[0]
+            exc_info = None
+            try:
+                f_result = f(*args, **kwargs) #call the decorated function
+            except:
+                exc_info = sys.exc_info() #store the exception
+            finally:
+                if exc_info is not None: #if exception happened
+                    #if the 'on_exception' is a name of 'class member function'
+                    #then call the f_self.on_exception(oe_args, oe_kwargs)
+                    if self.oe_is_self_method:
+                        f_self.__getattribute__(self.oe)(*self.oe_a, **self.oe_k)
+                    #'on_exception' is pointer to function
+                    else:
+                        self.oe(*self.oe_a, **self.oe_k)
+                    #re-raise the stored exception
+                    raise exc_info[1], None, exc_info[2]
+                return f_result
+        return newf
+
diff --git a/lib/python/test.py b/lib/python/test.py
new file mode 100644
index 0000000..ce2640b
--- /dev/null
+++ b/lib/python/test.py
@@ -0,0 +1,54 @@
+#
+# Copyright 2010, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Author: Josef Skladanka <jskladan@redhat.com>
+
+from autotest_lib.client.bin import test
+from autotest_lib.client.bin.test_config import config_loader
+
+from decorators import ExceptionCatcher
+from util import make_autotest_url
+
+class BaseTest(test.test, object):
+    version = 1 # increment if setup() changes
+
+    @ExceptionCatcher("self.initialize_failed")
+    def initialize(self, config, **kwargs):
+        self.config = config_loader(config, self.tmpdir)
+        self.autotest_url = make_autotest_url(self.config)
+
+    @ExceptionCatcher("self.run_once_failed")
+    def run_once(self, **kwargs):
+        pass
+
+    def initialize_failed(self):
+        print "Initialize failed"
+        try:
+            self.postprocess()
+        except:
+            pass
+
+    def run_once_failed(self):
+        print "Run_once failed"
+        try:
+            self.postprocess()
+        except:
+            pass
+
+    def postprocess(self):
+        pass
+
-- 
1.6.6.1

