Added sysinfo support to log pre and post iteration (currently it logs
the contents of /proc/schedstat). Added client.common_lib.test hooks to
call pre and post iteration. Used the hooks in client.bin.test.runtest()
for client tests. Refactored server.test and added wrappers for the
pre/post iteration logging and used them on the new pre/post iteration
hooks.

For each iteration there will be a
resultsdir/sysinfo/iteration.N/schedstat.after and schedstat.before.

If there are tests that override execute() and implement their own 
iteration logic they need to be changed over to override run_once() 
instead and take advantage of the default execute() that supports the 
pre/post iteration hooks and other things.

Signed-off-by: Mihai Rusu <[email protected]>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@2929 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/client/bin/base_sysinfo.py b/client/bin/base_sysinfo.py
index d84be2e..48bb725 100644
--- a/client/bin/base_sysinfo.py
+++ b/client/bin/base_sysinfo.py
@@ -9,22 +9,31 @@
     "lspci -vvn", "gcc --version", "ld --version", "mount", "hostname",
     "uptime",
     ]
+_DEFAULT_COMMANDS_TO_LOG_BEFORE_ITERATION = []
+_DEFAULT_COMMANDS_TO_LOG_AFTER_ITERATION = []
 
 _DEFAULT_FILES_TO_LOG_PER_TEST = []
 _DEFAULT_FILES_TO_LOG_PER_BOOT = [
     "/proc/pci", "/proc/meminfo", "/proc/slabinfo", "/proc/version",
     "/proc/cpuinfo", "/proc/modules", "/proc/interrupts",
     ]
+_DEFAULT_FILES_TO_LOG_BEFORE_ITERATION = [
+    "/proc/schedstat"
+    ]
+_DEFAULT_FILES_TO_LOG_AFTER_ITERATION = [
+    "/proc/schedstat"
+    ]
 
 
 class loggable(object):
     """ Abstract class for representing all things "loggable" by sysinfo. """
-    def __init__(self, log_in_keyval):
+    def __init__(self, logf, log_in_keyval):
+        self.logf = logf
         self.log_in_keyval = log_in_keyval
 
 
     def readline(self, logdir):
-        path = os.path.join(logdir, self.logfile)
+        path = os.path.join(logdir, self.logf)
         if os.path.exists(path):
             return utils.read_one_line(path)
         else:
@@ -32,19 +41,22 @@
 
 
 class logfile(loggable):
-    def __init__(self, path, log_in_keyval=False):
-        super(logfile, self).__init__(log_in_keyval)
+    def __init__(self, path, logf=None, log_in_keyval=False):
+        if not logf:
+            logf = os.path.basename(path)
+        super(logfile, self).__init__(logf, log_in_keyval)
         self.path = path
-        self.logfile = os.path.basename(self.path)
 
 
     def __repr__(self):
-        return "sysinfo.logfile(%r, %r)" % (self.path, self.log_in_keyval)
+        r = "sysinfo.logfile(%r, %r, %r)"
+        r %= (self.path, self.logf, self.log_in_keyval)
+        return r
 
 
     def __eq__(self, other):
         if isinstance(other, logfile):
-            return self.path == other.path
+            return (self.path, self.logf) == (other.path, other.logf)
         elif isinstance(other, loggable):
             return False
         return NotImplemented
@@ -58,33 +70,31 @@
 
 
     def __hash__(self):
-        return hash(self.path)
+        return hash((self.path, self.logf))
 
 
     def run(self, logdir):
         if os.path.exists(self.path):
-            shutil.copy(self.path, logdir)
+            shutil.copy(self.path, os.path.join(logdir, self.logf))
 
 
 class command(loggable):
-    def __init__(self, cmd, logfile=None, log_in_keyval=False):
-        super(command, self).__init__(log_in_keyval)
+    def __init__(self, cmd, logf=None, log_in_keyval=False):
+        if not logf:
+            logf = cmd.replace(" ", "_")
+        super(command, self).__init__(logf, log_in_keyval)
         self.cmd = cmd
-        if logfile:
-            self.logfile = logfile
-        else:
-            self.logfile = cmd.replace(" ", "_")
 
 
     def __repr__(self):
         r = "sysinfo.command(%r, %r, %r)"
-        r %= (self.cmd, self.logfile, self.log_in_keyval)
+        r %= (self.cmd, self.logf, self.log_in_keyval)
         return r
 
 
     def __eq__(self, other):
         if isinstance(other, command):
-            return (self.cmd, self.logfile) == (other.cmd, other.logfile)
+            return (self.cmd, self.logf) == (other.cmd, other.logf)
         elif isinstance(other, loggable):
             return False
         return NotImplemented
@@ -98,12 +108,12 @@
 
 
     def __hash__(self):
-        return hash((self.cmd, self.logfile))
+        return hash((self.cmd, self.logf))
 
 
     def run(self, logdir):
         stdin = open(os.devnull, "r")
-        stdout = open(os.path.join(logdir, self.logfile), "w")
+        stdout = open(os.path.join(logdir, self.logf), "w")
         stderr = open(os.devnull, "w")
         env = os.environ.copy()
         if "PATH" not in env:
@@ -132,12 +142,30 @@
         for filename in _DEFAULT_FILES_TO_LOG_PER_BOOT:
             self.boot_loggables.add(logfile(filename))
 
+        # pull in the pre test iteration logs to collect
+        self.before_iteration_loggables = set()
+        for cmd in _DEFAULT_COMMANDS_TO_LOG_BEFORE_ITERATION:
+            self.before_iteration_loggables.add(
+                command(cmd, logf=cmd.replace(" ", "_") + '.before'))
+        for fname in _DEFAULT_FILES_TO_LOG_BEFORE_ITERATION:
+            self.before_iteration_loggables.add(
+                logfile(fname, logf=os.path.basename(fname) + '.before'))
+
+        # pull in the post test iteration logs to collect
+        self.after_iteration_loggables = set()
+        for cmd in _DEFAULT_COMMANDS_TO_LOG_AFTER_ITERATION:
+            self.after_iteration_loggables.add(
+                command(cmd, logf=cmd.replace(" ", "_") + '.after'))
+        for fname in _DEFAULT_FILES_TO_LOG_AFTER_ITERATION:
+            self.after_iteration_loggables.add(
+                logfile(fname, logf=os.path.basename(fname) + '.after'))
+
         # add in a couple of extra files and commands we want to grab
-        self.test_loggables.add(command("df -mP", logfile="df"))
-        self.test_loggables.add(command("dmesg -c", logfile="dmesg"))
+        self.test_loggables.add(command("df -mP", logf="df"))
+        self.test_loggables.add(command("dmesg -c", logf="dmesg"))
         self.boot_loggables.add(logfile("/proc/cmdline",
                                              log_in_keyval=True))
-        self.boot_loggables.add(command("uname -a", logfile="uname",
+        self.boot_loggables.add(command("uname -a", logf="uname",
                                              log_in_keyval=True))
 
 
@@ -176,6 +204,15 @@
             return os.path.join(self.sysinfodir, boot_dir)
 
 
+    def _get_iteration_subdir(self, iteration):
+        iter_dir = "iteration.%d" % iteration
+
+        logdir = os.path.join(self.sysinfodir, iter_dir)
+        if not os.path.exists(logdir):
+            os.mkdir(logdir)
+        return logdir
+
+
     @log.log_and_ignore_errors("post-reboot sysinfo error:")
     def log_per_reboot_data(self):
         """ Logging hook called whenever a job starts, and again after
@@ -220,6 +257,28 @@
         test.write_test_keyval(keyval)
 
 
+    @log.log_and_ignore_errors("pre-test siteration sysinfo error:")
+    def log_before_each_iteration(self, test, iteration=None):
+        """ Logging hook called before a test iteration."""
+        if not iteration:
+            iteration = test.iteration
+        logdir = self._get_iteration_subdir(iteration)
+
+        for log in self.before_iteration_loggables:
+            log.run(logdir)
+
+
+    @log.log_and_ignore_errors("post-test siteration sysinfo error:")
+    def log_after_each_iteration(self, test, iteration=None):
+        """ Logging hook called after a test iteration."""
+        if not iteration:
+            iteration = test.iteration
+        logdir = self._get_iteration_subdir(iteration)
+
+        for log in self.after_iteration_loggables:
+            log.run(logdir)
+
+
     def _log_messages(self, logdir):
         """ Log all of the new data in /var/log/messages. """
         try:
@@ -244,7 +303,7 @@
         keyval = {}
         for log in loggables:
             if log.log_in_keyval:
-                keyval["sysinfo-" + log.logfile] = log.readline(logdir)
+                keyval["sysinfo-" + log.logf] = log.readline(logdir)
         return keyval
 
 
diff --git a/client/bin/job.py b/client/bin/job.py
index 4f29652..a2dd876 100755
--- a/client/bin/job.py
+++ b/client/bin/job.py
@@ -1019,7 +1019,7 @@
 
 
     def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
-        self._add_sysinfo_loggable(sysinfo.command(command, logfile),
+        self._add_sysinfo_loggable(sysinfo.command(command, logf=logfile),
                                    on_every_test)
 
 
diff --git a/client/bin/test.py b/client/bin/test.py
index acae8e3..ba51e56 100755
--- a/client/bin/test.py
+++ b/client/bin/test.py
@@ -32,4 +32,6 @@
 def runtest(job, url, tag, args, dargs):
     common_test.runtest(job, url, tag, args, dargs, locals(), globals(),
                         job.sysinfo.log_before_each_test,
-                        job.sysinfo.log_after_each_test)
+                        job.sysinfo.log_after_each_test,
+                        job.sysinfo.log_before_each_iteration,
+                        job.sysinfo.log_after_each_iteration)
diff --git a/client/common_lib/test.py b/client/common_lib/test.py
index 72fd743..25d5aca 100644
--- a/client/common_lib/test.py
+++ b/client/common_lib/test.py
@@ -104,14 +104,23 @@
             utils.drop_caches()
 
 
-    def _call_run_once(self, args, dargs):
+    def _call_run_once(self, before_iteration_hook, after_iteration_hook,
+                       args, dargs):
         self.drop_caches_between_iterations()
+
+        # execute iteration hooks
+        if before_iteration_hook:
+            before_iteration_hook(self)
         self.run_once(*args, **dargs)
+        if after_iteration_hook:
+            after_iteration_hook(self)
+
         self.postprocess_iteration()
 
 
     def execute(self, iterations=None, test_length=None, profile_only=False,
                 _get_time=time.time, postprocess_profiled_run=None,
+                before_iteration_hook=None, after_iteration_hook=None,
                 *args, **dargs):
         """
         This is the basic execute method for the tests inherited from base_test.
@@ -166,7 +175,8 @@
                     logging.info(
                             'Executing iteration %d, time_elapsed %d s',
                             timed_counter, time_elapsed)
-                self._call_run_once(args, dargs)
+                self._call_run_once(before_iteration_hook, after_iteration_hook,
+                                    args, dargs)
                 test_iteration_finish = _get_time()
                 time_elapsed = test_iteration_finish - test_start
             logging.info('Test finished after %d iterations',
@@ -186,10 +196,11 @@
                                    'Number of iterations: %d', iterations)
                 for self.iteration in xrange(1, iterations+1):
                     logging.info('Executing iteration %d of %d',
-                                       self.iteration, iterations)
-                    self._call_run_once(args, dargs)
+                                 self.iteration, iterations)
+                    self._call_run_once(before_iteration_hook,
+                                        after_iteration_hook, args, dargs)
                 logging.info('Test finished after %d iterations.',
-                                   iterations)
+                             iterations)
 
         self.run_once_profiling(postprocess_profiled_run, *args, **dargs)
 
@@ -270,7 +281,7 @@
         self.logger.addHandler(self.test_handler)
 
 
-    def _exec(self, args, dargs):
+    def _exec(self, before_iteration_hook, after_iteration_hook, args, dargs):
 
         self.job.stdout.tee_redirect(os.path.join(self.debugdir, 'stdout'))
         self.job.stderr.tee_redirect(os.path.join(self.debugdir, 'stderr'))
@@ -309,6 +320,12 @@
 
                 # Execute:
                 os.chdir(self.outputdir)
+
+                # insert the iteration hooks arguments to be potentially
+                # passed over to self.execute() if it accepts them
+                dargs['before_iteration_hook'] = before_iteration_hook
+                dargs['after_iteration_hook'] = after_iteration_hook
+
                 if hasattr(self, 'run_once'):
                     p_args, p_dargs = _cherry_pick_args(self.run_once,
                                                         args, dargs)
@@ -493,7 +510,8 @@
 
 def runtest(job, url, tag, args, dargs,
             local_namespace={}, global_namespace={},
-            before_test_hook=None, after_test_hook=None):
+            before_test_hook=None, after_test_hook=None,
+            before_iteration_hook=None, after_iteration_hook=None):
     local_namespace = local_namespace.copy()
     global_namespace = global_namespace.copy()
 
@@ -561,11 +579,14 @@
 
     pwd = os.getcwd()
     os.chdir(outputdir)
+
     try:
         mytest = global_namespace['mytest']
         if before_test_hook:
             before_test_hook(mytest)
-        mytest._exec(args, dargs)
+        # args/dargs are the test arguments as given by the control file so
+        # we cannot put the iteration hooks inside the dictionary
+        mytest._exec(before_iteration_hook, after_iteration_hook, args, dargs)
     finally:
         os.chdir(pwd)
         if after_test_hook:
diff --git a/client/common_lib/test_unittest.py b/client/common_lib/test_unittest.py
index d56a139..bb088ca 100644
--- a/client/common_lib/test_unittest.py
+++ b/client/common_lib/test_unittest.py
@@ -74,26 +74,41 @@
     def setUp(self):
         TestTestCase.setUp(self)
         self.god.stub_function(self.test, 'warmup')
-        self.god.stub_function(self.test, 'drop_caches_between_iterations')
-        self.god.stub_function(self.test, 'run_once')
-        self.god.stub_function(self.test, 'postprocess_iteration')
         self.god.stub_function(self.test, 'run_once_profiling')
         self.god.stub_function(self.test, 'postprocess')
 
         self.test.warmup.expect_call()
 
 
+    def test_call_run_once(self):
+        # setup
+        self.god.stub_function(self.test, 'drop_caches_between_iterations')
+        self.god.stub_function(self.test, 'run_once')
+        self.god.stub_function(self.test, 'postprocess_iteration')
+        before_hook = self.god.create_mock_function('before_hook')
+        after_hook = self.god.create_mock_function('after_hook')
+
+        # tests the test._call_run_once implementation
+        self.test.drop_caches_between_iterations.expect_call()
+        before_hook.expect_call()
+        self.test.run_once.expect_call(1, 2, arg='val')
+        after_hook.expect_call()
+        self.test.postprocess_iteration.expect_call()
+        self.test._call_run_once(before_hook, after_hook, (1, 2),
+                                 {'arg': 'val'})
+
+
+    def _expect_call_run_once(self):
+        self.test._call_run_once.expect_call(None, None, (), {})
+
+
     def test_execute_test_length(self):
         # test that test_length overrides iterations and works.
-        self.test.drop_caches_between_iterations.expect_call()
-        self.test.run_once.expect_call()
-        self.test.postprocess_iteration.expect_call()
-        self.test.drop_caches_between_iterations.expect_call()
-        self.test.run_once.expect_call()
-        self.test.postprocess_iteration.expect_call()
-        self.test.drop_caches_between_iterations.expect_call()
-        self.test.run_once.expect_call()
-        self.test.postprocess_iteration.expect_call()
+        self.god.stub_function(self.test, '_call_run_once')
+
+        self._expect_call_run_once()
+        self._expect_call_run_once()
+        self._expect_call_run_once()
         self.test.run_once_profiling.expect_call(None)
         self.test.postprocess.expect_call()
 
@@ -104,11 +119,11 @@
 
     def test_execute_iterations(self):
         # test that iterations works.
+        self.god.stub_function(self.test, '_call_run_once')
+
         iterations = 2
         for _ in range(iterations):
-            self.test.drop_caches_between_iterations.expect_call()
-            self.test.run_once.expect_call()
-            self.test.postprocess_iteration.expect_call()
+            self._expect_call_run_once()
         self.test.run_once_profiling.expect_call(None)
         self.test.postprocess.expect_call()
 
@@ -139,9 +154,9 @@
 
     def test_execute_postprocess_profiled_false(self):
         # test that postprocess_profiled_run=False works
-        self.test.drop_caches_between_iterations.expect_call()
-        self.test.run_once.expect_call()
-        self.test.postprocess_iteration.expect_call()
+        self.god.stub_function(self.test, '_call_run_once')
+
+        self._expect_call_run_once()
         self.test.run_once_profiling.expect_call(False)
         self.test.postprocess.expect_call()
 
@@ -151,9 +166,9 @@
 
     def test_execute_postprocess_profiled_true(self):
         # test that postprocess_profiled_run=True works
-        self.test.drop_caches_between_iterations.expect_call()
-        self.test.run_once.expect_call()
-        self.test.postprocess_iteration.expect_call()
+        self.god.stub_function(self.test, '_call_run_once')
+
+        self._expect_call_run_once()
         self.test.run_once_profiling.expect_call(True)
         self.test.postprocess.expect_call()
 
diff --git a/server/server_job.py b/server/server_job.py
index 80306af..98e4ed1 100755
--- a/server/server_job.py
+++ b/server/server_job.py
@@ -564,7 +564,7 @@
 
 
     def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
-        self._add_sysinfo_loggable(sysinfo.command(command, logfile),
+        self._add_sysinfo_loggable(sysinfo.command(command, logf=logfile),
                                    on_every_test)
 
 
diff --git a/server/test.py b/server/test.py
index d1eecf9..00e81a7 100755
--- a/server/test.py
+++ b/server/test.py
@@ -12,7 +12,7 @@
     pass
 
 
-_sysinfo_before_script = """\
+_sysinfo_before_test_script = """\
 import pickle
 from autotest_lib.client.bin import test
 mytest = test.test(job, '', %r)
@@ -22,7 +22,7 @@
 job.record('GOOD', '', 'sysinfo.before')
 """
 
-_sysinfo_after_script = """\
+_sysinfo_after_test_script = """\
 import pickle
 from autotest_lib.client.bin import test
 mytest = test.test(job, '', %r)
@@ -34,6 +34,23 @@
 job.record('GOOD', '', 'sysinfo.after')
 """
 
+# this script is ran after _sysinfo_before_test_script and before
+# _sysinfo_after_test_script which means the pickle file exists
+# already and should be dumped with updated state for
+# _sysinfo_after_test_script to pick it up later
+_sysinfo_iteration_script = """\
+import pickle
+from autotest_lib.client.bin import test
+mytest = test.test(job, '', %r)
+sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
+if os.path.exists(sysinfo_pickle):
+    job.sysinfo = pickle.load(open(sysinfo_pickle))
+    job.sysinfo.__init__(job.resultdir)
+job.sysinfo.%s(mytest, iteration=%d)
+pickle.dump(job.sysinfo, open(sysinfo_pickle, 'w'))
+job.record('GOOD', '', 'sysinfo.iteration.%s')
+"""
+
 
 class _sysinfo_logger(object):
     def __init__(self, job):
@@ -53,38 +70,28 @@
         return host, at
 
 
-    @log.log_and_ignore_errors("pre-test server sysinfo error:")
-    def before_hook(self, mytest):
-        host, at = self._install()
-        outputdir = host.get_tmp_dir()
-
-        # run the pre-test sysinfo script
-        at.run(_sysinfo_before_script % outputdir,
-               results_dir=self.job.resultdir)
-
-        # pull back the sysinfo pickle
+    def _pull_pickle(self, host, outputdir):
+        """Pulls from the client the pickle file with the saved sysinfo state.
+        """
         fd, path = tempfile.mkstemp(dir=self.job.tmpdir)
         os.close(fd)
         host.get_file(os.path.join(outputdir, "sysinfo.pickle"), path)
         self.pickle = path
 
 
-    @log.log_and_ignore_errors("post-test server sysinfo error:")
-    def after_hook(self, mytest):
-        host, at = self._install()
-        outputdir = host.get_tmp_dir()
-
-        # push the sysinfo pickle out to the remote machine
+    def _push_pickle(self, host, outputdir):
+        """Pushes the server saved sysinfo pickle file to the client.
+        """
         if self.pickle:
             host.send_file(self.pickle,
                            os.path.join(outputdir, "sysinfo.pickle"))
             os.remove(self.pickle)
             self.pickle = None
 
-        # run the post-test sysinfo script
-        at.run(_sysinfo_after_script % outputdir,
-               results_dir=self.job.resultdir)
 
+    def _pull_sysinfo_keyval(self, host, outputdir, mytest):
+        """Pulls sysinfo and keyval data from the client.
+        """
         # pull the sysinfo data back on to the server
         host.get_file(os.path.join(outputdir, "sysinfo"), mytest.outputdir)
 
@@ -97,10 +104,73 @@
         mytest.write_test_keyval(keyval)
 
 
+    @log.log_and_ignore_errors("pre-test server sysinfo error:")
+    def before_hook(self, mytest):
+        host, at = self._install()
+        outputdir = host.get_tmp_dir()
+
+        # run the pre-test sysinfo script
+        at.run(_sysinfo_before_test_script % outputdir,
+               results_dir=self.job.resultdir)
+
+        self._pull_pickle(host, outputdir)
+
+
+    @log.log_and_ignore_errors("pre-test iteration server sysinfo error:")
+    def before_iteration_hook(self, mytest):
+        host, at = self._install()
+        outputdir = host.get_tmp_dir()
+
+        # this function is called after before_hook() se we have sysinfo state
+        # to push to the server
+        self._push_pickle(host, outputdir);
+        # run the pre-test iteration sysinfo script
+        at.run(_sysinfo_iteration_script %
+               (outputdir, 'log_before_each_iteration', mytest.iteration,
+                'before'),
+               results_dir=self.job.resultdir)
+
+        # get the new sysinfo state from the client
+        self._pull_pickle(host, outputdir)
+
+
+    @log.log_and_ignore_errors("post-test iteration server sysinfo error:")
+    def after_iteration_hook(self, mytest):
+        host, at = self._install()
+        outputdir = host.get_tmp_dir()
+
+        # push latest sysinfo state to the client
+        self._push_pickle(host, outputdir);
+        # run the post-test iteration sysinfo script
+        at.run(_sysinfo_iteration_script %
+               (outputdir, 'log_after_each_iteration', mytest.iteration,
+                'after'),
+               results_dir=self.job.resultdir)
+
+        # get the new sysinfo state from the client
+        self._pull_pickle(host, outputdir)
+        self._pull_sysinfo_keyval(host, outputdir, mytest)
+
+
+    @log.log_and_ignore_errors("post-test server sysinfo error:")
+    def after_hook(self, mytest):
+        host, at = self._install()
+        outputdir = host.get_tmp_dir()
+
+        self._push_pickle(host, outputdir);
+        # run the post-test sysinfo script
+        at.run(_sysinfo_after_test_script % outputdir,
+               results_dir=self.job.resultdir)
+
+        self._pull_sysinfo_keyval(host, outputdir, mytest)
+
+
 def runtest(job, url, tag, args, dargs):
     if not dargs.pop('disable_sysinfo', False):
         logger = _sysinfo_logger(job)
-        logging_args = [logger.before_hook, logger.after_hook]
+        logging_args = [logger.before_hook, logger.after_hook,
+                        logger.before_iteration_hook,
+                        logger.after_iteration_hook]
     else:
         logging_args = [None, None]
     common_test.runtest(job, url, tag, args, dargs, locals(), globals(),