per-test logfiles

The new --log-dir option creates a directory with one log file for every test
(plus main.log for the oft script). It takes precedence over the --log-file
option if both are specified.

The --log-append option and the ability to pass "-" or "stderr" to --log-file
have been removed.
diff --git a/oft b/oft
index 95496d1..b79d46c 100755
--- a/oft
+++ b/oft
@@ -75,7 +75,7 @@
 
     # Logging options
     "log_file"           : "oft.log",
-    "log_append"         : False,
+    "log_dir"            : None,
     "debug"              : "verbose",
 
     # Test behavior options
@@ -169,10 +169,8 @@
     parser.add_option_group(group)
 
     group = optparse.OptionGroup(parser, "Logging options")
-    group.add_option("--log-file",
-                      help="Name of log file, empty string to log to console (default %default)")
-    group.add_option("--log-append", action="store_true",
-                      help="Do not delete log file if specified")
+    group.add_option("--log-file", help="Name of log file (default %default)")
+    group.add_option("--log-dir", help="Name of log directory")
     dbg_lvl_names = sorted(DEBUG_LEVELS.keys(), key=lambda x: DEBUG_LEVELS[x])
     group.add_option("--debug", choices=dbg_lvl_names,
                       help="Debug lvl: debug, info, warning, error, critical (default %default)")
@@ -220,15 +218,19 @@
     """
     Set up logging based on config
     """
-    _format = "%(asctime)s.%(msecs)03d  %(name)-10s: %(levelname)-8s: %(message)s"
-    _datefmt = "%H:%M:%S"
-    _mode = config["log_append"] and "a" or "w"
-    if config['log_file'] in [ "-", "stderr" ]:
-        config['log_file'] = None
-    logging.basicConfig(filename=config['log_file'],
-                        filemode=_mode,
-                        level=DEBUG_LEVELS[config["debug"]],
-                        format=_format, datefmt=_datefmt)
+
+    logging.getLogger().setLevel(DEBUG_LEVELS[config["debug"]])
+
+    if config["log_dir"] != None:
+        if os.path.exists(config["log_dir"]):
+            import shutil
+            shutil.rmtree(config["log_dir"])
+        os.makedirs(config["log_dir"])
+    else:
+        if os.path.exists(config["log_file"]):
+            os.remove(config["log_file"])
+
+    oftest.open_logfile('main')
 
 def load_test_modules(config):
     """
@@ -501,6 +503,7 @@
 
     logging.info("*** TEST RUN START: " + time.asctime())
     result = unittest.TextTestRunner(verbosity=2).run(suite)
+    oftest.open_logfile('main')
     if oftest.testutils.skipped_test_count > 0:
         ts = " tests"
         if oftest.testutils.skipped_test_count == 1: ts = " test"
diff --git a/src/python/oftest/__init__.py b/src/python/oftest/__init__.py
index 9199b1a..c78bb67 100644
--- a/src/python/oftest/__init__.py
+++ b/src/python/oftest/__init__.py
@@ -1,5 +1,7 @@
 '''Docstring to silence pylint; ignores --ignore option for __init__.py'''
 import sys
+import os
+import logging
 
 # Global config dictionary
 # Populated by oft.
@@ -8,3 +10,31 @@
 # Global DataPlane instance used by all tests.
 # Populated by oft.
 dataplane_instance = None
+
+def open_logfile(name):
+    """
+    (Re)open logfile
+
+    When using a log directory a new logfile is created for each test. The same
+    code is used to implement a single logfile in the absence of --log-dir.
+    """
+
+    _format = "%(asctime)s.%(msecs)03d  %(name)-10s: %(levelname)-8s: %(message)s"
+    _datefmt = "%H:%M:%S"
+
+    if config["log_dir"] != None:
+        filename = os.path.join(config["log_dir"], name) + ".log"
+    else:
+        filename = config["log_file"]
+
+    logger = logging.getLogger()
+
+    # Remove any existing handlers
+    for handler in logger.handlers:
+        logger.removeHandler(handler)
+        handler.close()
+
+    # Add a new handler
+    handler = logging.FileHandler(filename, mode='a')
+    handler.setFormatter(logging.Formatter(_format, _datefmt))
+    logger.addHandler(handler)
diff --git a/src/python/oftest/base_tests.py b/src/python/oftest/base_tests.py
index a1eec43..3b59bc1 100644
--- a/src/python/oftest/base_tests.py
+++ b/src/python/oftest/base_tests.py
@@ -14,13 +14,25 @@
 import oftest.dataplane as dataplane
 import ofp
 
-class SimpleProtocol(unittest.TestCase):
+class BaseTest(unittest.TestCase):
+    def __str__(self):
+        return self.id().replace('.runTest', '')
+
+    def setUp(self):
+        oftest.open_logfile(str(self))
+        logging.info("** START TEST CASE " + str(self))
+
+    def tearDown(self):
+        logging.info("** END TEST CASE " + str(self))
+
+class SimpleProtocol(BaseTest):
     """
     Root class for setting up the controller
     """
 
     def setUp(self):
-        logging.info("** START TEST CASE " + str(self))
+        BaseTest.setUp(self)
+
         self.controller = controller.Controller(
             switch=config["switch_ip"],
             host=config["controller_host"],
@@ -73,10 +85,10 @@
         self.supported_actions = parent.supported_actions
         
     def tearDown(self):
-        logging.info("** END TEST CASE " + str(self))
         self.controller.shutdown()
         self.controller.join()
         del self.controller
+        BaseTest.tearDown(self)
 
     def assertTrue(self, cond, msg):
         if not cond:
@@ -102,20 +114,17 @@
         self.dataplane = parent.dataplane
 
     def tearDown(self):
-        logging.info("Teardown for simple dataplane test")
         SimpleProtocol.tearDown(self)
-        logging.info("Teardown done")
 
-class DataPlaneOnly(unittest.TestCase):
+class DataPlaneOnly(BaseTest):
     """
     Root class that sets up only the dataplane
     """
 
     def setUp(self):
-        logging.info("** START DataPlaneOnly CASE " + str(self))
+        BaseTest.setUp(self)
         self.dataplane = oftest.dataplane_instance
         self.dataplane.flush()
 
     def tearDown(self):
-        logging.info("Teardown for simple dataplane test")
-        logging.info("Teardown done")
+        BaseTest.tearDown(self)