Run the experiment, using the parameters set using :meth:`set`.
        A "run" consists of calling :meth:`setUp`, :meth:`do`, and :meth:`tearDown`,
        followed by collecting and storing (and returning) the
        experiment's results. If running the experiment raises an
        exception, that will be returned in the metadata along with
        its traceback to help with experiment debugging.

        :returns: a :term:`results dict`

def run( self ):
        """Run the experiment, using the parameters set using :meth:`set`.
        A "run" consists of calling :meth:`setUp`, :meth:`do`, and :meth:`tearDown`,
        followed by collecting and storing (and returning) the
        experiment's results. If running the experiment raises an
        exception, that will be returned in the metadata along with
        its traceback to help with experiment debugging.

        :returns: a :term:`results dict`"""
        
        # perform the experiment protocol
        params = self.parameters()
        self._metadata = dict()
        self._results = None
        res = None
        doneSetupTime = doneExperimentTime = doneTeardownTime = None
        try:
            # do the phases in order, recording the wallclock times at each phase
            startTime = datetime.now()
            self.setUp(params)
            doneSetupTime = datetime.now()
            res = self.do(params)
            doneExperimentTime = datetime.now() 
            self.tearDown()
            doneTeardownTime = datetime.now() 
            
            # record the various timings
            self._metadata[self.START_TIME] = startTime
            self._metadata[self.END_TIME] = doneTeardownTime
            self._metadata[self.ELAPSED_TIME] = (doneTeardownTime - startTime).total_seconds()
            self._metadata[self.SETUP_TIME] = (doneSetupTime - startTime).total_seconds()
            self._metadata[self.EXPERIMENT_TIME] = (doneExperimentTime - doneSetupTime).total_seconds()
            self._metadata[self.TEARDOWN_TIME] = (doneTeardownTime - doneExperimentTime).total_seconds()
            
            # set the success flag
            self._metadata[self.STATUS] = True
        except Exception as e:
            print("Caught exception in experiment: {e}".format(e = e))

            # grab the traceback before we do anything else
            #_, _, tb = sys.exc_info()
            tb = traceback.format_exc()
            
            # decide on the cleanup actions that need doing
            if (doneSetupTime is not None) and (doneExperimentTime is None):
                # we did the setup and then failed in the experiment, so
                # we need to do the teardown
                try:
                    self.tearDown()
                except:
                    pass
                
            # set the failure flag and record the exception
            # (there will be no timing information recorded)
            self._metadata[self.STATUS] = False
            self._metadata[self.EXCEPTION] = e
            self._metadata[self.TRACEBACK] = tb
                
        # report the results
        self._results = res
        return self.report(params,
                           self._metadata,
                           res)