示例#1
0
文件: decorators.py 项目: TC01/cocotb
class RunningCoroutine(object):
    """Per instance wrapper around an function to turn it into a coroutine.

    Provides the following:

        coro.join() creates a Trigger that will fire when this coroutine
        completes.

        coro.kill() will destroy a coroutine instance (and cause any Join
        triggers to fire.
    """
    def __init__(self, inst, parent):
        if hasattr(inst, "__name__"):
            self.__name__ = "%s" % inst.__name__
            self.log = SimLog("cocotb.coroutine.%s" % self.__name__, id(self))
        else:
            self.log = SimLog("cocotb.coroutine.fail")

        if sys.version_info[:2] >= (3, 5) and inspect.iscoroutine(inst):
            self._natively_awaitable = True
            self._coro = inst.__await__()
        else:
            self._natively_awaitable = False
            self._coro = inst
        self._started = False
        self._callbacks = []
        self._parent = parent
        self.__doc__ = parent._func.__doc__
        self.module = parent._func.__module__
        self.funcname = parent._func.__name__
        self._outcome = None

        if not hasattr(self._coro, "send"):
            self.log.error("%s isn't a valid coroutine! Did you use the yield "
                           "keyword?" % self.funcname)
            raise CoroutineComplete()

    @property
    def retval(self):
        if self._outcome is None:
            raise RuntimeError("coroutine is not complete")
        return self._outcome.get()

    @property
    def _finished(self):
        return self._outcome is not None

    def __iter__(self):
        return self

    def __str__(self):
        return str(self.__name__)

    def _advance(self, outcome):
        """
        Advance to the next yield in this coroutine

        :param outcome: The `outcomes.Outcome` object to resume with.
        :returns: The object yielded from the coroutine

        If the coroutine returns or throws an error, self._outcome is set, and
        this throws `CoroutineComplete`.
        """
        try:
            self._started = True
            return outcome.send(self._coro)
        except ReturnValue as e:
            self._outcome = outcomes.Value(e.retval)
            raise CoroutineComplete()
        except StopIteration as e:
            retval = getattr(e, 'value', None)  # for python >=3.3
            self._outcome = outcomes.Value(retval)
            raise CoroutineComplete()
        except BaseException as e:
            self._outcome = outcomes.Error(e)
            raise CoroutineComplete()

    def send(self, value):
        return self._coro.send(value)

    def throw(self, exc):
        return self._coro.throw(exc)

    def close(self):
        return self._coro.close()

    def kill(self):
        """Kill a coroutine."""
        self.log.debug("kill() called on coroutine")
        # todo: probably better to throw an exception for anyone waiting on the coroutine
        self._outcome = outcomes.Value(None)
        cocotb.scheduler.unschedule(self)

    def join(self):
        """Return a trigger that will fire when the wrapped coroutine exits."""
        return Join(self)

    def has_started(self):
        return self._started

    def __nonzero__(self):
        """Provide boolean testing
            if the coroutine has finished return false
            otherwise return true"""
        return not self._finished

    # Needs `yield from` syntax to implement this correctly.
    # Once 2.7 is dropped, this can be run unconditionally
    if sys.version_info >= (3, 3):
        exec_(textwrap.dedent("""
        def __await__(self):
            if self._natively_awaitable:
                # use the native trampoline, which will only hand back nested
                # non-natively awaitable objects
                return (yield from self._coro)
            else:
                # hand the coroutine back to the scheduler trampoline
                return (yield self)
        """))

    __bool__ = __nonzero__

    def sort_name(self):
        if self.stage is None:
            return "%s.%s" % (self.module, self.funcname)
        else:
            return "%s.%d.%s" % (self.module, self.stage, self.funcname)
示例#2
0
class RunningCoroutine(object):
    """Per instance wrapper around an function to turn it into a coroutine


        Provides the following:

            coro.join() creates a Trigger that will fire when this coroutine
            completes

            coro.kill() will destroy a coroutine instance (and cause any Join
            triggers to fire
    """
    def __init__(self, inst, parent):
        if hasattr(inst, "__name__"):
            self.__name__ = "%s" % inst.__name__
            self.log = SimLog("cocotb.coroutine.%s" % self.__name__, id(self))
        else:
            self.log = SimLog("cocotb.coroutine.fail")
        self._coro = inst
        self._started = False
        self._finished = False
        self._callbacks = []
        self._join = _Join(self)
        self._parent = parent
        self.__doc__ = parent._func.__doc__
        self.module = parent._func.__module__
        self.funcname = parent._func.__name__
        self.retval = None

        if not hasattr(self._coro, "send"):
            self.log.error("%s isn't a valid coroutine! Did you use the yield "
                           "keyword?" % self.funcname)
            raise CoroutineComplete(callback=self._finished_cb)

    def __iter__(self):
        return self

    def __str__(self):
        return str(self.__name__)

    def send(self, value):
        try:
            if isinstance(value, ExternalException):
                self.log.debug("Injecting ExternalException(%s)" % (repr(value)))
                return self._coro.throw(value.exception)
            self._started = True
            return self._coro.send(value)
        except TestComplete as e:
            if isinstance(e, TestFailure):
                self.log.warning(str(e))
            raise
        except ExternalException as e:
            self.retval = e
            self._finished = True
            raise CoroutineComplete(callback=self._finished_cb)
        except ReturnValue as e:
            self.retval = e.retval
            self._finished = True
            raise CoroutineComplete(callback=self._finished_cb)
        except StopIteration as e:
            self._finished = True
            self.retval = getattr(e, 'value', None)  # for python >=3.3
            raise CoroutineComplete(callback=self._finished_cb)
        except Exception as e:
            self._finished = True
            raise raise_error(self, "Send raised exception: %s" % (str(e)))

    def throw(self, exc):
        return self._coro.throw(exc)

    def close(self):
        return self._coro.close()

    def kill(self):
        """Kill a coroutine"""
        self.log.debug("kill() called on coroutine")
        cocotb.scheduler.unschedule(self)

    def _finished_cb(self):
        """Called when the coroutine completes.
            Allows us to mark the coroutine as finished so that boolean testing
            works.
            Also call any callbacks, usually the result of coroutine.join()"""
        self._finished = True

    def join(self):
        """Return a trigger that will fire when the wrapped coroutine exits"""
        if self._finished:
            return NullTrigger()
        else:
            return self._join

    def has_started(self):
        return self._started

    def __nonzero__(self):
        """Provide boolean testing
            if the coroutine has finished return false
            otherwise return true"""
        return not self._finished
示例#3
0
# NB this cheekily ensures a singleton since we're replacing the reference
# so that cocotb.scheduler gives you the singleton instance and not the
# scheduler package

# GPI logging instance
# For autodocumentation don't need the extension modules
if "SPHINX_BUILD" not in os.environ:
    import simulator
    logging.basicConfig()
    logging.setLoggerClass(SimBaseLog)
    log = SimLog('cocotb')
    level = os.getenv("COCOTB_LOG_LEVEL", "INFO")
    try:
        _default_log = getattr(logging, level)
    except AttributeError as e:
        log.error("Unable to set loging level to %s" % level)
        _default_log = logging.INFO
    log.setLevel(_default_log)
    loggpi = SimLog('cocotb.gpi')
    # Notify GPI of log level
    simulator.log_level(_default_log)


scheduler = Scheduler()
regression = None

plusargs = {}

# To save typing provide an alias to scheduler.add
fork = scheduler.add
示例#4
0
class Scheduler(object):
    """
    The main scheduler.

    Here we accept callbacks from the simulator and schedule the appropriate
    coroutines.

    A callback fires, causing the `react`_ method to be called, with the
    trigger that caused the callback as the first argument.

    We look up a list of coroutines to schedule (indexed by the trigger) and
    schedule them in turn. NB implementors should not depend on the scheduling
    order!

    Some additional management is required since coroutines can return a list
    of triggers, to be scheduled when any one of the triggers fires.  To
    ensure we don't receive spurious callbacks, we have to un-prime all the
    other triggers when any one fires.

    Due to the simulator nuances and fun with delta delays we have the
    following modes:

    Normal mode
        - Callbacks cause coroutines to be scheduled
        - Any pending writes are cached and do not happen immediately

    ReadOnly mode
        - Corresponds to cbReadOnlySynch (VPI) or vhpiCbLastKnownDeltaCycle
          (VHPI).  In this state we are not allowed to perform writes.

    Write mode
        - Corresponds to cbReadWriteSynch (VPI) or vhpiCbEndOfProcesses (VHPI)
          In this mode we play back all the cached write updates.

    We can legally transition from normal->write by registering a ReadWrite
    callback, however usually once a simulator has entered the ReadOnly phase
    of a given timestep then we must move to a new timestep before performing
    any writes.  The mechanism for moving to a new timestep may not be
    consistent across simulators and therefore we provide an abstraction to
    assist with compatibility.


    Unless a coroutine has explicitly requested to be scheduled in ReadOnly
    mode (for example wanting to sample the finally settled value after all
    delta delays) then it can reasonably be expected to be scheduled during
    "normal mode" i.e. where writes are permitted.
    """

    _MODE_NORMAL = 1  # noqa
    _MODE_READONLY = 2  # noqa
    _MODE_WRITE = 3  # noqa
    _MODE_TERM = 4  # noqa

    # Singleton events, recycled to avoid spurious object creation
    _readonly = ReadOnly()
    _next_timestep = _NextTimeStep()
    _readwrite = _ReadWrite()
    _timer1 = Timer(1)
    _timer0 = Timer(0)

    def __init__(self):

        self.log = SimLog("cocotb.scheduler")
        if _debug:
            self.log.setLevel(logging.DEBUG)

        # A dictionary of pending coroutines for each trigger,
        # indexed by trigger
        self._trigger2coros = collections.defaultdict(list)

        # A dictionary of pending triggers for each coroutine, indexed by coro
        self._coro2triggers = collections.defaultdict(list)

        # Our main state
        self._mode = Scheduler._MODE_NORMAL

        # A dictionary of pending writes
        self._writes = {}

        self._pending_coros = []
        self._pending_callbacks = []
        self._pending_triggers = []

        self._terminate = False
        self._test_result = None
        self._entrypoint = None

        # Select the appropriate scheduling algorithm for this simulator
        self.advance = self.default_scheduling_algorithm

    def default_scheduling_algorithm(self):
        """
        Decide whether we need to schedule our own triggers (if at all) in
        order to progress to the next mode.

        This algorithm has been tested against the following simulators:
            Icarus Verilog
        """
        if not self._terminate and self._writes:

            if self._mode == Scheduler._MODE_NORMAL:
                if not self._readwrite.primed:
                    self._readwrite.prime(self.react)
            elif not self._next_timestep.primed:
                self._next_timestep.prime(self.react)

        elif self._terminate:
            if _debug:
                self.log.debug("Test terminating, scheduling Timer")

            for t in self._trigger2coros:
                t.unprime()

            for t in [
                    self._readwrite, self._readonly, self._next_timestep,
                    self._timer1, self._timer0
            ]:
                if t.primed:
                    t.unprime()

            self._timer1.prime(self.begin_test)
            self._trigger2coros = collections.defaultdict(list)
            self._coro2triggers = collections.defaultdict(list)
            self._terminate = False
            self._mode = Scheduler._MODE_TERM

    def begin_test(self, trigger=None):
        """
        Called to initiate a test.

        Could be called on start-up or from a callback
        """
        if _debug:
            self.log.debug("begin_test called with trigger: %s" %
                           (str(trigger)))
        if _profiling:
            ps = pstats.Stats(_profile).sort_stats('cumulative')
            ps.dump_stats("test_profile.pstat")
            _profile.enable()

        self._mode = Scheduler._MODE_NORMAL
        if trigger is not None:
            trigger.unprime()

        # Issue previous test result, if there is one
        if self._test_result is not None:
            if _debug:
                self.log.debug("Issue test result to regresssion object")
            cocotb.regression.handle_result(self._test_result)
            self._test_result = None
        if self._entrypoint is not None:
            test = self._entrypoint
            self._entrypoint = None
            self.schedule(test)
            self.advance()

        if _profiling:
            _profile.disable()

    def react(self, trigger, depth=0):
        """
        React called when a trigger fires.

        We find any coroutines that are waiting on the particular trigger and
        schedule them.
        """
        if _profiling and not depth:
            _profile.enable()

        # When a trigger fires it is unprimed internally
        if _debug:
            self.log.debug("Trigger fired: %s" % str(trigger))
        # trigger.unprime()

        if self._mode == Scheduler._MODE_TERM:
            if _debug:
                self.log.debug("Ignoring trigger %s since we're terminating" %
                               str(trigger))
            return

        if trigger is self._readonly:
            self._mode = Scheduler._MODE_READONLY
        # Only GPI triggers affect the simulator scheduling mode
        elif isinstance(trigger, GPITrigger):
            self._mode = Scheduler._MODE_NORMAL

        # We're the only source of ReadWrite triggers which are only used for
        # playing back any cached signal updates
        if trigger is self._readwrite:

            if _debug:
                self.log.debug("Writing cached signal updates")

            while self._writes:
                handle, value = self._writes.popitem()
                handle.setimmediatevalue(value)

            self._readwrite.unprime()

            if _profiling:
                _profile.disable()
            return

        # Similarly if we've scheduled our next_timestep on way to readwrite
        if trigger is self._next_timestep:

            if not self._writes:
                self.log.error(
                    "Moved to next timestep without any pending writes!")
            else:
                self.log.debug(
                    "Priming ReadWrite trigger so we can playback writes")
                self._readwrite.prime(self.react)

            if _profiling:
                _profile.disable()
            return

        if trigger not in self._trigger2coros:

            # GPI triggers should only be ever pending if there is an
            # associated coroutine waiting on that trigger, otherwise it would
            # have been unprimed already
            if isinstance(trigger, GPITrigger):
                self.log.critical(
                    "No coroutines waiting on trigger that fired: %s" %
                    str(trigger))

                trigger.log.info("I'm the culprit")
            # For Python triggers this isn't actually an error - we might do
            # event.set() without knowing whether any coroutines are actually
            # waiting on this event, for example
            elif _debug:
                self.log.debug(
                    "No coroutines waiting on trigger that fired: %s" %
                    str(trigger))

            if _profiling:
                _profile.disable()
            return

        # Scheduled coroutines may append to our waiting list so the first
        # thing to do is pop all entries waiting on this trigger.
        scheduling = self._trigger2coros.pop(trigger)

        if _debug:
            debugstr = "\n\t".join([coro.__name__ for coro in scheduling])
            if len(scheduling):
                debugstr = "\n\t" + debugstr
            self.log.debug("%d pending coroutines for event %s%s" %
                           (len(scheduling), str(trigger), debugstr))

        # If the coroutine was waiting on multiple triggers we may be able
        # to unprime the other triggers that didn't fire
        for coro in scheduling:
            for pending in self._coro2triggers[coro]:
                for others in self._trigger2coros[pending]:
                    if others not in scheduling:
                        break
                else:
                    # if pending is not trigger and pending.primed:
                    #     pending.unprime()
                    if pending.primed:
                        pending.unprime()
                    del self._trigger2coros[pending]

        for coro in scheduling:
            self.schedule(coro, trigger=trigger)
            if _debug:
                self.log.debug("Scheduled coroutine %s" % (coro.__name__))

        while self._pending_triggers:
            if _debug:
                self.log.debug("Scheduling pending trigger %s" %
                               (str(self._pending_triggers[0])))
            self.react(self._pending_triggers.pop(0), depth=depth + 1)

        # We only advance for GPI triggers
        if not depth and isinstance(trigger, GPITrigger):
            self.advance()

            if _debug:
                self.log.debug("All coroutines scheduled, handing control back"
                               " to simulator")

            if _profiling:
                _profile.disable()
        return

    def unschedule(self, coro):
        """Unschedule a coroutine.  Unprime any pending triggers"""

        for trigger in self._coro2triggers[coro]:
            if coro in self._trigger2coros[trigger]:
                self._trigger2coros[trigger].remove(coro)
            if not self._trigger2coros[trigger]:
                trigger.unprime()
        del self._coro2triggers[coro]

        if coro._join in self._trigger2coros:
            self._pending_triggers.append(coro._join)

        # Remove references to allow GC to clean up
        del coro._join

    def save_write(self, handle, value):
        if self._mode == Scheduler._MODE_READONLY:
            raise Exception(
                "Write to object {} was scheduled during a read-only sync phase."
                .format(handle._name))
        self._writes[handle] = value

    def _coroutine_yielded(self, coro, triggers):
        """
        Prime the triggers and update our internal mappings
        """
        self._coro2triggers[coro] = triggers

        for trigger in triggers:

            self._trigger2coros[trigger].append(coro)
            if not trigger.primed:
                try:
                    trigger.prime(self.react)
                except Exception as e:
                    # Convert any exceptions into a test result
                    self.finish_test(
                        create_error(
                            self, "Unable to prime trigger %s: %s" %
                            (str(trigger), str(e))))

    def queue(self, coroutine):
        """Queue a coroutine for execution"""
        self._pending_coros.append(coroutine)

    def add(self, coroutine):
        """
        Add a new coroutine.

        Just a wrapper around self.schedule which provides some debug and
        useful error mesages in the event of common gotchas
        """
        if isinstance(coroutine, cocotb.decorators.coroutine):
            self.log.critical(
                "Attempt to schedule a coroutine that hasn't started")
            coroutine.log.error("This is the failing coroutine")
            self.log.warning(
                "Did you forget to add parentheses to the @test decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        elif not isinstance(coroutine, cocotb.decorators.RunningCoroutine):
            self.log.critical(
                "Attempt to add something to the scheduler which isn't a "
                "coroutine")
            self.log.warning("Got: %s (%s)" %
                             (str(type(coroutine)), repr(coroutine)))
            self.log.warning("Did you use the @coroutine decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        if _debug:
            self.log.debug("Adding new coroutine %s" % coroutine.__name__)

        self.schedule(coroutine)
        self.advance()
        return coroutine

    def new_test(self, coroutine):
        self._entrypoint = coroutine

    def schedule(self, coroutine, trigger=None):
        """
        Schedule a coroutine by calling the send method

        Args:
            coroutine (cocotb.decorators.coroutine): The coroutine to schedule

            trigger (cocotb.triggers.Trigger): The trigger that caused this
                                                coroutine to be scheduled
        """
        if hasattr(trigger, "pass_retval"):
            sendval = trigger.retval
            if _debug:
                coroutine.log.debug("Scheduling with ReturnValue(%s)" %
                                    (repr(sendval)))
        else:
            sendval = trigger
            if _debug:
                coroutine.log.debug("Scheduling with %s" % str(trigger))

        try:
            result = coroutine.send(sendval)
            if _debug:
                self.log.debug("Coroutine %s yielded %s (mode %d)" %
                               (coroutine.__name__, str(result), self._mode))

        # TestComplete indication is game over, tidy up
        except TestComplete as test_result:
            # Tag that close down is needed, save the test_result
            # for later use in cleanup handler
            self.log.debug("TestComplete received: %s" %
                           test_result.__class__.__name__)
            self.finish_test(test_result)
            return

        # Normal co-routine completion
        except cocotb.decorators.CoroutineComplete as exc:
            if _debug:
                self.log.debug("Coroutine completed: %s" % str(coroutine))
            self.unschedule(coroutine)
            return

        # Don't handle the result if we're shutting down
        if self._terminate:
            return

        # Queue current routine to schedule when the nested routine exits
        if isinstance(result, cocotb.decorators.RunningCoroutine):
            if _debug:
                self.log.debug("Scheduling nested co-routine: %s" %
                               result.__name__)

            self.queue(result)
            new_trigger = result.join()
            self._coroutine_yielded(coroutine, [new_trigger])

        elif isinstance(result, Trigger):
            self._coroutine_yielded(coroutine, [result])

        elif (isinstance(result, list)
              and not [t for t in result if not isinstance(t, Trigger)]):
            self._coroutine_yielded(coroutine, result)

        else:
            msg = (
                "Coroutine %s yielded something the scheduler can't handle" %
                str(coroutine))
            msg += ("\nGot type: %s repr: %s str: %s" %
                    (type(result), repr(result), str(result)))
            msg += "\nDid you forget to decorate with @cocotb.coroutine?"
            try:
                raise_error(self, msg)
            except Exception as e:
                self.finish_test(e)

        # Handle any newly queued coroutines that need to be scheduled
        while self._pending_coros:
            self.add(self._pending_coros.pop(0))

        while self._pending_callbacks:
            self._pending_callbacks.pop(0)()

    def finish_test(self, test_result):
        """Cache the test result and set the terminate flag"""
        self.log.debug("finish_test called with %s" % (repr(test_result)))
        if not self._terminate:
            self._terminate = True
            self._test_result = test_result
            self.cleanup()

    def finish_scheduler(self, test_result):
        """Directly call into the regression manager and end test
           once we return the sim will close us so no cleanup is needed"""
        self.log.debug("Issue sim closedown result to regresssion object")
        cocotb.regression.handle_result(test_result)

    def cleanup(self):
        """
        Clear up all our state

        Unprime all pending triggers and kill off any coroutines
        """
        for trigger, waiting in self._trigger2coros.items():
            for coro in waiting:
                if _debug:
                    self.log.debug("Killing %s" % str(coro))
                coro.kill()
示例#5
0
class Scheduler(object):
    """
    The main scheduler.

    Here we accept callbacks from the simulator and schedule the appropriate
    coroutines.

    A callback fires, causing the `react`_ method to be called, with the
    trigger that caused the callback as the first argument.

    We look up a list of coroutines to schedule (indexed by the trigger) and
    schedule them in turn. NB implementors should not depend on the scheduling
    order!

    Some additional management is required since coroutines can return a list
    of triggers, to be scheduled when any one of the triggers fires.  To
    ensure we don't receive spurious callbacks, we have to un-prime all the
    other triggers when any one fires.

    Due to the simulator nuances and fun with delta delays we have the
    following modes:

    Normal mode
        - Callbacks cause coroutines to be scheduled
        - Any pending writes are cached and do not happen immediately

    ReadOnly mode
        - Corresponds to cbReadOnlySynch (VPI) or vhpiCbLastKnownDeltaCycle
          (VHPI).  In this state we are not allowed to perform writes.

    Write mode
        - Corresponds to cbReadWriteSynch (VPI) or vhpiCbEndOfProcesses (VHPI)
          In this mode we play back all the cached write updates.

    We can legally transition from normal->write by registering a ReadWrite
    callback, however usually once a simulator has entered the ReadOnly phase
    of a given timestep then we must move to a new timestep before performing
    any writes.  The mechanism for moving to a new timestep may not be
    consistent across simulators and therefore we provide an abstraction to
    assist with compatibility.


    Unless a coroutine has explicitly requested to be scheduled in ReadOnly
    mode (for example wanting to sample the finally settled value after all
    delta delays) then it can reasonably be expected to be scheduled during
    "normal mode" i.e. where writes are permitted.
    """

    _MODE_NORMAL   = 1  # noqa
    _MODE_READONLY = 2  # noqa
    _MODE_WRITE    = 3  # noqa
    _MODE_TERM     = 4  # noqa

    # Singleton events, recycled to avoid spurious object creation
    _readonly = ReadOnly()
    _next_timestep = _NextTimeStep()
    _readwrite = _ReadWrite()
    _timer1 = Timer(1)
    _timer0 = Timer(0)

    def __init__(self):

        self.log = SimLog("cocotb.scheduler")
        if _debug:
            self.log.setLevel(logging.DEBUG)

        # A dictionary of pending coroutines for each trigger,
        # indexed by trigger
        self._trigger2coros = collections.defaultdict(list)

        # A dictionary of pending triggers for each coroutine, indexed by coro
        self._coro2triggers = collections.defaultdict(list)

        # Our main state
        self._mode = Scheduler._MODE_NORMAL

        # A dictionary of pending writes
        self._writes = {}

        self._pending_coros = []
        self._pending_callbacks = []
        self._pending_triggers = []

        self._terminate = False
        self._test_result = None
        self._entrypoint = None

        # Select the appropriate scheduling algorithm for this simulator
        self.advance = self.default_scheduling_algorithm

    def default_scheduling_algorithm(self):
        """
        Decide whether we need to schedule our own triggers (if at all) in
        order to progress to the next mode.

        This algorithm has been tested against the following simulators:
            Icarus Verilog
        """
        if not self._terminate and self._writes:

            if self._mode == Scheduler._MODE_NORMAL:
                if not self._readwrite.primed:
                    self._readwrite.prime(self.react)
            elif not self._next_timestep.primed:
                self._next_timestep.prime(self.react)

        elif self._terminate:
            if _debug:
                self.log.debug("Test terminating, scheduling Timer")

            for t in self._trigger2coros:
                t.unprime()

            for t in [self._readwrite, self._readonly, self._next_timestep,
                      self._timer1, self._timer0]:
                if t.primed:
                    t.unprime()

            self._timer1.prime(self.begin_test)
            self._trigger2coros = collections.defaultdict(list)
            self._coro2triggers = collections.defaultdict(list)
            self._terminate = False
            self._mode = Scheduler._MODE_TERM

    def begin_test(self, trigger=None):
        """
        Called to initiate a test.

        Could be called on start-up or from a callback
        """
        if _debug:
            self.log.debug("begin_test called with trigger: %s" %
                           (str(trigger)))
        if _profiling:
            ps = pstats.Stats(_profile).sort_stats('cumulative')
            ps.dump_stats("test_profile.pstat")
            _profile.enable()

        self._mode = Scheduler._MODE_NORMAL
        if trigger is not None:
            trigger.unprime()

        # Issue previous test result, if there is one
        if self._test_result is not None:
            if _debug:
                self.log.debug("Issue test result to regresssion object")
            cocotb.regression.handle_result(self._test_result)
            self._test_result = None
        if self._entrypoint is not None:
            test = self._entrypoint
            self._entrypoint = None
            self.schedule(test)
            self.advance()

        if _profiling:
            _profile.disable()

    def react(self, trigger, depth=0):
        """
        React called when a trigger fires.

        We find any coroutines that are waiting on the particular trigger and
        schedule them.
        """
        if _profiling and not depth:
            _profile.enable()

        # When a trigger fires it is unprimed internally
        if _debug:
            self.log.debug("Trigger fired: %s" % str(trigger))
        # trigger.unprime()

        if self._mode == Scheduler._MODE_TERM:
            if _debug:
                self.log.debug("Ignoring trigger %s since we're terminating" %
                               str(trigger))
            return

        if trigger is self._readonly:
            self._mode = Scheduler._MODE_READONLY
        # Only GPI triggers affect the simulator scheduling mode
        elif isinstance(trigger, GPITrigger):
            self._mode = Scheduler._MODE_NORMAL

        # We're the only source of ReadWrite triggers which are only used for
        # playing back any cached signal updates
        if trigger is self._readwrite:

            if _debug:
                self.log.debug("Writing cached signal updates")

            while self._writes:
                handle, value = self._writes.popitem()
                handle.setimmediatevalue(value)

            self._readwrite.unprime()

            if _profiling:
                _profile.disable()
            return

        # Similarly if we've scheduled our next_timestep on way to readwrite
        if trigger is self._next_timestep:

            if not self._writes:
                self.log.error(
                    "Moved to next timestep without any pending writes!")
            else:
                self.log.debug(
                    "Priming ReadWrite trigger so we can playback writes")
                self._readwrite.prime(self.react)

            if _profiling:
                _profile.disable()
            return

        if trigger not in self._trigger2coros:

            # GPI triggers should only be ever pending if there is an
            # associated coroutine waiting on that trigger, otherwise it would
            # have been unprimed already
            if isinstance(trigger, GPITrigger):
                self.log.critical(
                    "No coroutines waiting on trigger that fired: %s" %
                    str(trigger))

                trigger.log.info("I'm the culprit")
            # For Python triggers this isn't actually an error - we might do
            # event.set() without knowing whether any coroutines are actually
            # waiting on this event, for example
            elif _debug:
                self.log.debug(
                    "No coroutines waiting on trigger that fired: %s" %
                    str(trigger))

            if _profiling:
                _profile.disable()
            return

        # Scheduled coroutines may append to our waiting list so the first
        # thing to do is pop all entries waiting on this trigger.
        scheduling = self._trigger2coros.pop(trigger)

        if _debug:
            debugstr = "\n\t".join([coro.__name__ for coro in scheduling])
            if len(scheduling):
                debugstr = "\n\t" + debugstr
            self.log.debug("%d pending coroutines for event %s%s" %
                           (len(scheduling), str(trigger), debugstr))

        # If the coroutine was waiting on multiple triggers we may be able
        # to unprime the other triggers that didn't fire
        for coro in scheduling:
            for pending in self._coro2triggers[coro]:
                for others in self._trigger2coros[pending]:
                    if others not in scheduling:
                        break
                else:
                    # if pending is not trigger and pending.primed:
                    #     pending.unprime()
                    if pending.primed:
                        pending.unprime()
                    del self._trigger2coros[pending]

        for coro in scheduling:
            self.schedule(coro, trigger=trigger)
            if _debug:
                self.log.debug("Scheduled coroutine %s" % (coro.__name__))

        while self._pending_triggers:
            if _debug:
                self.log.debug("Scheduling pending trigger %s" %
                               (str(self._pending_triggers[0])))
            self.react(self._pending_triggers.pop(0), depth=depth + 1)

        # We only advance for GPI triggers
        if not depth and isinstance(trigger, GPITrigger):
            self.advance()

            if _debug:
                self.log.debug("All coroutines scheduled, handing control back"
                               " to simulator")

            if _profiling:
                _profile.disable()
        return

    def unschedule(self, coro):
        """Unschedule a coroutine.  Unprime any pending triggers"""

        for trigger in self._coro2triggers[coro]:
            if coro in self._trigger2coros[trigger]:
                self._trigger2coros[trigger].remove(coro)
            if not self._trigger2coros[trigger]:
                trigger.unprime()
        del self._coro2triggers[coro]

        if coro._join in self._trigger2coros:
            self._pending_triggers.append(coro._join)

        # Remove references to allow GC to clean up
        del coro._join

    def save_write(self, handle, value):
        self._writes[handle] = value

    def _coroutine_yielded(self, coro, triggers):
        """
        Prime the triggers and update our internal mappings
        """
        self._coro2triggers[coro] = triggers

        for trigger in triggers:

            self._trigger2coros[trigger].append(coro)
            if not trigger.primed:
                try:
                    trigger.prime(self.react)
                except Exception as e:
                    # Convert any exceptions into a test result
                    self.finish_test(
                        create_error(self, "Unable to prime trigger %s: %s" %
                                     (str(trigger), str(e))))

    def queue(self, coroutine):
        """Queue a coroutine for execution"""
        self._pending_coros.append(coroutine)

    def add(self, coroutine):
        """
        Add a new coroutine.

        Just a wrapper around self.schedule which provides some debug and
        useful error mesages in the event of common gotchas
        """
        if isinstance(coroutine, cocotb.decorators.coroutine):
            self.log.critical(
                "Attempt to schedule a coroutine that hasn't started")
            coroutine.log.error("This is the failing coroutine")
            self.log.warning(
                "Did you forget to add parentheses to the @test decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        elif not isinstance(coroutine, cocotb.decorators.RunningCoroutine):
            self.log.critical(
                "Attempt to add something to the scheduler which isn't a "
                "coroutine")
            self.log.warning(
                "Got: %s (%s)" % (str(type(coroutine)), repr(coroutine)))
            self.log.warning("Did you use the @coroutine decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        if _debug:
            self.log.debug("Adding new coroutine %s" % coroutine.__name__)

        self.schedule(coroutine)
        self.advance()
        return coroutine

    def new_test(self, coroutine):
        self._entrypoint = coroutine

    def schedule(self, coroutine, trigger=None):
        """
        Schedule a coroutine by calling the send method

        Args:
            coroutine (cocotb.decorators.coroutine): The coroutine to schedule

            trigger (cocotb.triggers.Trigger): The trigger that caused this
                                                coroutine to be scheduled
        """
        if hasattr(trigger, "pass_retval"):
            sendval = trigger.retval
            if _debug:
                coroutine.log.debug("Scheduling with ReturnValue(%s)" %
                                    (repr(sendval)))
        else:
            sendval = trigger
            if _debug:
                coroutine.log.debug("Scheduling with %s" % str(trigger))

        try:
            result = coroutine.send(sendval)
            if _debug:
                self.log.debug("Coroutine %s yielded %s (mode %d)" %
                               (coroutine.__name__, str(result), self._mode))

        # TestComplete indication is game over, tidy up
        except TestComplete as test_result:
            # Tag that close down is needed, save the test_result
            # for later use in cleanup handler
            self.log.debug("TestComplete received: %s" % test_result.__class__.__name__)
            self.finish_test(test_result)
            return

        # Normal co-routine completion
        except cocotb.decorators.CoroutineComplete as exc:
            if _debug:
                self.log.debug("Coroutine completed: %s" % str(coroutine))
            self.unschedule(coroutine)
            return

        # Don't handle the result if we're shutting down
        if self._terminate:
            return

        # Queue current routine to schedule when the nested routine exits
        if isinstance(result, cocotb.decorators.RunningCoroutine):
            if _debug:
                self.log.debug("Scheduling nested co-routine: %s" %
                               result.__name__)

            self.queue(result)
            new_trigger = result.join()
            self._coroutine_yielded(coroutine, [new_trigger])

        elif isinstance(result, Trigger):
            self._coroutine_yielded(coroutine, [result])

        elif (isinstance(result, list) and
                not [t for t in result if not isinstance(t, Trigger)]):
            self._coroutine_yielded(coroutine, result)

        else:
            msg = ("Coroutine %s yielded something the scheduler can't handle"
                   % str(coroutine))
            msg += ("\nGot type: %s repr: %s str: %s" %
                    (type(result), repr(result), str(result)))
            msg += "\nDid you forget to decorate with @cocotb.coroutine?"
            try:
                raise_error(self, msg)
            except Exception as e:
                self.finish_test(e)

        # Handle any newly queued coroutines that need to be scheduled
        while self._pending_coros:
            self.add(self._pending_coros.pop(0))

        while self._pending_callbacks:
            self._pending_callbacks.pop(0)()

    def finish_test(self, test_result):
        """Cache the test result and set the terminate flag"""
        self.log.debug("finish_test called with %s" % (repr(test_result)))
        if not self._terminate:
            self._terminate = True
            self._test_result = test_result
            self.cleanup()

    def finish_scheduler(self, test_result):
        """Directly call into the regression manager and end test
           once we return the sim will close us so no cleanup is needed"""
        self.log.debug("Issue sim closedown result to regresssion object")
        cocotb.regression.handle_result(test_result)

    def cleanup(self):
        """
        Clear up all our state

        Unprime all pending triggers and kill off any coroutines
        """
        for trigger, waiting in self._trigger2coros.items():
            for coro in waiting:
                if _debug:
                    self.log.debug("Killing %s" % str(coro))
                coro.kill()
示例#6
0
文件: regression.py 项目: TC01/cocotb
class TestFactory(object):
    """Used to automatically generate tests.

    Assuming we have a common test function that will run a test. This test
    function will take keyword arguments (for example generators for each of
    the input interfaces) and generate tests that call the supplied function.

    This Factory allows us to generate sets of tests based on the different
    permutations of the possible arguments to the test function.

    For example if we have a module that takes backpressure and idles and
    have some packet generation routines ``gen_a`` and ``gen_b``:

    >>> tf = TestFactory(run_test)
    >>> tf.add_option('data_in', [gen_a, gen_b])
    >>> tf.add_option('backpressure', [None, random_backpressure])
    >>> tf.add_option('idles', [None, random_idles])
    >>> tf.generate_tests()

    We would get the following tests:
        * ``gen_a`` with no backpressure and no idles
        * ``gen_a`` with no backpressure and ``random_idles``
        * ``gen_a`` with ``random_backpressure`` and no idles
        * ``gen_a`` with ``random_backpressure`` and ``random_idles``
        * ``gen_b`` with no backpressure and no idles
        * ``gen_b`` with no backpressure and ``random_idles``
        * ``gen_b`` with ``random_backpressure`` and no idles
        * ``gen_b`` with ``random_backpressure`` and ``random_idles``

    The tests are appended to the calling module for auto-discovery.

    Tests are simply named ``test_function_N``. The docstring for the test (hence
    the test description) includes the name and description of each generator.
    """

    def __init__(self, test_function, *args, **kwargs):
        """
        Args:
            test_function (function): the function that executes a test.
                                      Must take 'dut' as the first argument.

            *args: Remaining args are passed directly to the test function.
                   Note that these arguments are not varied. An argument that
                   varies with each test must be a keyword argument to the
                   test function.
            *kwargs: Remaining kwargs are passed directly to the test function.
                   Note that these arguments are not varied. An argument that
                   varies with each test must be a keyword argument to the
                   test function.
        """
        if not isinstance(test_function, cocotb.coroutine):
            raise TypeError("TestFactory requires a cocotb coroutine")
        self.test_function = test_function
        self.name = self.test_function._func.__name__

        self.args = args
        self.kwargs_constant = kwargs
        self.kwargs = {}
        self.log = SimLog("cocotb.regression")

    def add_option(self, name, optionlist):
        """Add a named option to the test.

        Args:
           name (str): Name of the option. Passed to test as a keyword
                          argument.

           optionlist (list): A list of possible options for this test knob.
        """
        self.kwargs[name] = optionlist

    def generate_tests(self, prefix="", postfix=""):
        """
        Generates exhaustive set of tests using the cartesian product of the
        possible keyword arguments.

        The generated tests are appended to the namespace of the calling
        module.

        Args:
            prefix (str):  Text string to append to start of ``test_function`` name
                     when naming generated test cases. This allows reuse of
                     a single ``test_function`` with multiple
                     :class:`TestFactories <.TestFactory>` without name clashes.
            postfix (str): Text string to append to end of ``test_function`` name
                     when naming generated test cases. This allows reuse of
                     a single ``test_function`` with multiple
                     :class:`TestFactories <.TestFactory>` without name clashes.
        """

        frm = inspect.stack()[1]
        mod = inspect.getmodule(frm[0])

        d = self.kwargs

        for index, testoptions in enumerate((
                                            dict(zip(d, v)) for v in
                                            product(*d.values())
                                            )):

            name = "%s%s%s_%03d" % (prefix, self.name, postfix, index + 1)
            doc = "Automatically generated test\n\n"

            for optname, optvalue in testoptions.items():
                if callable(optvalue):
                    if not optvalue.__doc__:
                        desc = "No docstring supplied"
                    else:
                        desc = optvalue.__doc__.split('\n')[0]
                    doc += "\t%s: %s (%s)\n" % (optname, optvalue.__name__,
                                                desc)
                else:
                    doc += "\t%s: %s\n" % (optname, repr(optvalue))

            self.log.debug("Adding generated test \"%s\" to module \"%s\"" %
                             (name, mod.__name__))
            kwargs = {}
            kwargs.update(self.kwargs_constant)
            kwargs.update(testoptions)
            if hasattr(mod, name):
                self.log.error("Overwriting %s in module %s. "
                                 "This causes a previously defined testcase "
                                 "not to be run. Consider setting/changing "
                                 "name_postfix" % (name, mod))
            setattr(mod, name, _create_test(self.test_function, name, doc, mod,
                                            *self.args, **kwargs))
示例#7
0
class RunningCoroutine(object):
    """Per instance wrapper around an function to turn it into a coroutine


        Provides the following:

            coro.join() creates a Trigger that will fire when this coroutine
            completes

            coro.kill() will destroy a coroutine instance (and cause any Join
            triggers to fire
    """
    def __init__(self, inst, parent):
        if hasattr(inst, "__name__"):
            self.__name__ = "%s" % inst.__name__
            self.log = SimLog("cocotb.coroutine.%s" % self.__name__, id(self))
        else:
            self.log = SimLog("cocotb.coroutine.fail")
        self._coro = inst
        self._started = False
        self._finished = False
        self._callbacks = []
        self._join = _Join(self)
        self._parent = parent
        self.__doc__ = parent._func.__doc__
        self.module = parent._func.__module__
        self.funcname = parent._func.__name__
        self.retval = None

        if not hasattr(self._coro, "send"):
            self.log.error("%s isn't a valid coroutine! Did you use the yield "
                           "keyword?" % self.funcname)
            raise CoroutineComplete(callback=self._finished_cb)

    def __iter__(self):
        return self

    def __str__(self):
        return str(self.__name__)

    def send(self, value):
        try:
            if isinstance(value, ExternalException):
                self.log.debug("Injecting ExternalException(%s)" %
                               (repr(value)))
                return self._coro.throw(value.exception)
            self._started = True
            return self._coro.send(value)
        except TestComplete as e:
            if isinstance(e, TestFailure):
                self.log.warning(str(e))
            raise
        except ExternalException as e:
            self.retval = e
            self._finished = True
            raise CoroutineComplete(callback=self._finished_cb)
        except ReturnValue as e:
            self.retval = e.retval
            self._finished = True
            raise CoroutineComplete(callback=self._finished_cb)
        except StopIteration as e:
            self._finished = True
            self.retval = getattr(e, 'value', None)  # for python >=3.3
            raise CoroutineComplete(callback=self._finished_cb)
        except Exception as e:
            self._finished = True
            raise raise_error(self, "Send raised exception: %s" % (str(e)))

    def throw(self, exc):
        return self._coro.throw(exc)

    def close(self):
        return self._coro.close()

    def kill(self):
        """Kill a coroutine"""
        self.log.debug("kill() called on coroutine")
        cocotb.scheduler.unschedule(self)

    def _finished_cb(self):
        """Called when the coroutine completes.
            Allows us to mark the coroutine as finished so that boolean testing
            works.
            Also call any callbacks, usually the result of coroutine.join()"""
        self._finished = True

    def join(self):
        """Return a trigger that will fire when the wrapped coroutine exits"""
        if self._finished:
            return NullTrigger()
        else:
            return self._join

    def has_started(self):
        return self._started

    def __nonzero__(self):
        """Provide boolean testing
            if the coroutine has finished return false
            otherwise return true"""
        return not self._finished

    def sort_name(self):
        if self.stage is None:
            return "%s.%s" % (self.module, self.funcname)
        else:
            return "%s.%d.%s" % (self.module, self.stage, self.funcname)
示例#8
0
class RegressionManager(object):
    """Encapsulates all regression capability into a single place"""

    def __init__(self, dut, modules, tests=None):
        """
        Args:
            modules (list): A list of python module names to run

        Kwargs
        """
        self._queue = []
        self._dut = dut
        self._modules = modules
        self._functions = tests
        self._running_test = None
        self.log = SimLog("cocotb.regression")

    def initialise(self):

        self.ntests = 0
        self.count = 1
        self.skipped = 0
        self.failures = 0
        self.xunit = XUnitReporter()
        self.xunit.add_testsuite(name="all", tests=repr(self.ntests), package="all")

        # Auto discovery
        for module_name in self._modules:
            module = _my_import(module_name)

            if self._functions:

                # Specific functions specified, don't auto discover
                for test in self._functions.rsplit(','):
                    if not hasattr(module, test):
                        raise AttributeError("Test %s doesn't exist in %s" %
                            (test, module_name))

                    self._queue.append(getattr(module, test)(self._dut))
                    self.ntests += 1
                break

            for thing in vars(module).values():
                if hasattr(thing, "im_test"):
                    try:
                        test = thing(self._dut)
                        skip = test.skip
                    except TestError:
                        skip = True
                        self.log.warning("Failed to initialise test %s" % thing.name)

                    if skip:
                        self.log.info("Skipping test %s" % thing.name)
                        self.xunit.add_testcase(name=thing.name, classname=module_name, time="0.0")
                        self.xunit.add_skipped()
                        self.skipped += 1                        
                    else:
                        self._queue.append(test)
                        self.ntests += 1

        self._queue.sort(key=lambda test: "%s.%s" % (test.module, test.funcname))

        for valid_tests in self._queue:
            self.log.info("Found test %s.%s" %
                        (valid_tests.module,
                         valid_tests.funcname))

    def tear_down(self):
        """It's the end of the world as we know it"""
        if self.failures:
            self.log.error("Failed %d out of %d tests (%d skipped)" %
                (self.failures, self.count -1, self.skipped))
        else:
            self.log.info("Passed %d tests (%d skipped)"  %
                (self.count-1, self.skipped))
        self.log.info("Shutting down...")
        self.xunit.write()
        simulator.stop_simulator()


    def next_test(self):
        """Get the next test to run"""
        if not self._queue: return None
        return self._queue.pop(0)


    def handle_result(self, result):
        """Handle a test result

        Dumps result to XML and schedules the next test (if any)

        Args: result (TestComplete exception)
        """
        self.xunit.add_testcase(name =self._running_test.funcname,
                                classname=self._running_test.module,
                                time=repr(time.time() - self._running_test.start_time) )

        if isinstance(result, TestSuccess) and not self._running_test.expect_fail and not self._running_test.expect_error:
            self.log.info("Test Passed: %s" % self._running_test.funcname)

        elif isinstance(result, TestFailure) and self._running_test.expect_fail:
            self.log.info("Test failed as expected: %s (result was %s)" % (
                          self._running_test.funcname, result.__class__.__name__))

        elif isinstance(result, TestSuccess) and self._running_test.expect_error:
            self.log.error("Test passed but we expected an error: %s (result was %s)" % (
                           self._running_test.funcname, result.__class__.__name__))
            self.xunit.add_failure(stdout=repr(str(result)), stderr="\n".join(self._running_test.error_messages))
            self.failures += 1

        elif isinstance(result, TestSuccess):
            self.log.error("Test passed but we expected a failure: %s (result was %s)" % (
                           self._running_test.funcname, result.__class__.__name__))
            self.xunit.add_failure(stdout=repr(str(result)), stderr="\n".join(self._running_test.error_messages))
            self.failures += 1

        elif isinstance(result, TestError) and self._running_test.expect_error:
            self.log.info("Test errored as expected: %s (result was %s)" % (
                          self._running_test.funcname, result.__class__.__name__))

        elif isinstance(result, SimFailure):
            if self._running_test.expect_error:
                self.log.info("Test errored as expected: %s (result was %s)" % (
                              self._running_test.funcname, result.__class__.__name__))
            else:
                self.log.error("Test error has lead to simulator shuttting us down")
                self.failures += 1
                self.tear_down()
                return

        else:
            self.log.error("Test Failed: %s (result was %s)" % (
                        self._running_test.funcname, result.__class__.__name__))
            self.xunit.add_failure(stdout=repr(str(result)), stderr="\n".join(self._running_test.error_messages))
            self.failures += 1

        self.execute()

    def execute(self):
        self._running_test = cocotb.regression.next_test()
        if self._running_test:
            # Want this to stand out a little bit
            self.log.info("%sRunning test %d/%d:%s %s" % (
               ANSI.BLUE_BG +ANSI.BLACK_FG,
                    self.count, self.ntests,
               ANSI.DEFAULT_FG + ANSI.DEFAULT_BG,
                    self._running_test.funcname))
            if self.count is 1:
                test = cocotb.scheduler.add(self._running_test)
            else:
                test = cocotb.scheduler.new_test(self._running_test)
            self.count+=1
        else:
            self.tear_down()
示例#9
0
class Scheduler(object):
    """The main scheduler.

    Here we accept callbacks from the simulator and schedule the appropriate
    coroutines.

    A callback fires, causing the :any:`react` method to be called, with the
    trigger that caused the callback as the first argument.

    We look up a list of coroutines to schedule (indexed by the trigger) and
    schedule them in turn. NB implementors should not depend on the scheduling
    order!

    Some additional management is required since coroutines can return a list
    of triggers, to be scheduled when any one of the triggers fires.  To
    ensure we don't receive spurious callbacks, we have to un-prime all the
    other triggers when any one fires.

    Due to the simulator nuances and fun with delta delays we have the
    following modes:

    Normal mode
        - Callbacks cause coroutines to be scheduled
        - Any pending writes are cached and do not happen immediately

    ReadOnly mode
        - Corresponds to cbReadOnlySynch (VPI) or vhpiCbLastKnownDeltaCycle
          (VHPI).  In this state we are not allowed to perform writes.

    Write mode
        - Corresponds to cbReadWriteSynch (VPI) or vhpiCbEndOfProcesses (VHPI)
          In this mode we play back all the cached write updates.

    We can legally transition from normal->write by registering a ReadWrite
    callback, however usually once a simulator has entered the ReadOnly phase
    of a given timestep then we must move to a new timestep before performing
    any writes.  The mechanism for moving to a new timestep may not be
    consistent across simulators and therefore we provide an abstraction to
    assist with compatibility.


    Unless a coroutine has explicitly requested to be scheduled in ReadOnly
    mode (for example wanting to sample the finally settled value after all
    delta delays) then it can reasonably be expected to be scheduled during
    "normal mode" i.e. where writes are permitted.
    """

    _MODE_NORMAL = 1  # noqa
    _MODE_READONLY = 2  # noqa
    _MODE_WRITE = 3  # noqa
    _MODE_TERM = 4  # noqa

    # Singleton events, recycled to avoid spurious object creation
    _readonly = ReadOnly()
    # TODO[gh-759]: For some reason, the scheduler requires that these triggers
    # are _not_ the same instances used by the tests themselves. This is risky,
    # because it can lead to them overwriting each other's callbacks. We should
    # try to remove this `copy.copy` in future.
    _next_timestep = copy.copy(NextTimeStep())
    _readwrite = copy.copy(ReadWrite())
    _timer1 = Timer(1)
    _timer0 = Timer(0)

    def __init__(self):

        self.log = SimLog("cocotb.scheduler")
        if _debug:
            self.log.setLevel(logging.DEBUG)

        # A dictionary of pending coroutines for each trigger,
        # indexed by trigger
        self._trigger2coros = collections.defaultdict(list)

        # A dictionary of pending triggers for each coroutine, indexed by coro
        self._coro2triggers = collections.defaultdict(list)

        # Our main state
        self._mode = Scheduler._MODE_NORMAL

        # A dictionary of pending writes
        self._writes = {}

        self._pending_coros = []
        self._pending_callbacks = []
        self._pending_triggers = []
        self._pending_threads = []
        self._pending_events = [
        ]  # Events we need to call set on once we've unwound

        self._terminate = False
        self._test_result = None
        self._entrypoint = None
        self._main_thread = threading.current_thread()

        # Select the appropriate scheduling algorithm for this simulator
        self.advance = self.default_scheduling_algorithm

    def default_scheduling_algorithm(self):
        """
        Decide whether we need to schedule our own triggers (if at all) in
        order to progress to the next mode.

        This algorithm has been tested against the following simulators:
            Icarus Verilog
        """
        if not self._terminate and self._writes:

            if self._mode == Scheduler._MODE_NORMAL:
                if not self._readwrite.primed:
                    self._readwrite.prime(self.react)
            elif not self._next_timestep.primed:
                self._next_timestep.prime(self.react)

        elif self._terminate:
            if _debug:
                self.log.debug("Test terminating, scheduling Timer")

            for t in self._trigger2coros:
                t.unprime()

            for t in [
                    self._readwrite, self._readonly, self._next_timestep,
                    self._timer1, self._timer0
            ]:
                if t.primed:
                    t.unprime()

            self._timer1.prime(self.begin_test)
            self._trigger2coros = collections.defaultdict(list)
            self._coro2triggers = collections.defaultdict(list)
            self._terminate = False
            self._mode = Scheduler._MODE_TERM

    def begin_test(self, trigger=None):
        """Called to initiate a test.

        Could be called on start-up or from a callback.
        """
        if _debug:
            self.log.debug("begin_test called with trigger: %s" %
                           (str(trigger)))
        if _profiling:
            ps = pstats.Stats(_profile).sort_stats('cumulative')
            ps.dump_stats("test_profile.pstat")
            ctx = profiling_context()
        else:
            ctx = nullcontext()

        with ctx:
            self._mode = Scheduler._MODE_NORMAL
            if trigger is not None:
                trigger.unprime()

            # Issue previous test result, if there is one
            if self._test_result is not None:
                if _debug:
                    self.log.debug("Issue test result to regression object")
                cocotb.regression.handle_result(self._test_result)
                self._test_result = None
            if self._entrypoint is not None:
                test = self._entrypoint
                self._entrypoint = None
                self.schedule(test)
                self.advance()

    def react(self, trigger, depth=0):
        """React called when a trigger fires.

        We find any coroutines that are waiting on the particular trigger and
        schedule them.
        """
        if _profiling and not depth:
            ctx = profiling_context()
        else:
            ctx = nullcontext()

        with ctx:
            # When a trigger fires it is unprimed internally
            if _debug:
                self.log.debug("Trigger fired: %s" % str(trigger))
            # trigger.unprime()

            if self._mode == Scheduler._MODE_TERM:
                if _debug:
                    self.log.debug(
                        "Ignoring trigger %s since we're terminating" %
                        str(trigger))
                return

            if trigger is self._readonly:
                self._mode = Scheduler._MODE_READONLY
            # Only GPI triggers affect the simulator scheduling mode
            elif isinstance(trigger, GPITrigger):
                self._mode = Scheduler._MODE_NORMAL

            # We're the only source of ReadWrite triggers which are only used for
            # playing back any cached signal updates
            if trigger is self._readwrite:

                if _debug:
                    self.log.debug("Writing cached signal updates")

                while self._writes:
                    handle, value = self._writes.popitem()
                    handle.setimmediatevalue(value)

                self._readwrite.unprime()

                return

            # Similarly if we've scheduled our next_timestep on way to readwrite
            if trigger is self._next_timestep:

                if not self._writes:
                    self.log.error(
                        "Moved to next timestep without any pending writes!")
                else:
                    self.log.debug(
                        "Priming ReadWrite trigger so we can playback writes")
                    self._readwrite.prime(self.react)

                return

            if trigger not in self._trigger2coros:

                # GPI triggers should only be ever pending if there is an
                # associated coroutine waiting on that trigger, otherwise it would
                # have been unprimed already
                if isinstance(trigger, GPITrigger):
                    self.log.critical(
                        "No coroutines waiting on trigger that fired: %s" %
                        str(trigger))

                    trigger.log.info("I'm the culprit")
                # For Python triggers this isn't actually an error - we might do
                # event.set() without knowing whether any coroutines are actually
                # waiting on this event, for example
                elif _debug:
                    self.log.debug(
                        "No coroutines waiting on trigger that fired: %s" %
                        str(trigger))

                return

            # Scheduled coroutines may append to our waiting list so the first
            # thing to do is pop all entries waiting on this trigger.
            scheduling = self._trigger2coros.pop(trigger)

            if _debug:
                debugstr = "\n\t".join([coro.__name__ for coro in scheduling])
                if len(scheduling):
                    debugstr = "\n\t" + debugstr
                self.log.debug("%d pending coroutines for event %s%s" %
                               (len(scheduling), str(trigger), debugstr))

            # This trigger isn't needed any more
            trigger.unprime()

            # If the coroutine was waiting on multiple triggers we may be able
            # to unprime the other triggers that didn't fire
            scheduling_set = set(scheduling)
            other_triggers = {
                t
                for coro in scheduling for t in self._coro2triggers[coro]
            } - {trigger}

            for pending in other_triggers:
                # every coroutine waiting on this trigger is already being woken
                if scheduling_set.issuperset(self._trigger2coros[pending]):
                    if pending.primed:
                        pending.unprime()
                    del self._trigger2coros[pending]

            for coro in scheduling:
                if _debug:
                    self.log.debug("Scheduling coroutine %s" % (coro.__name__))
                self.schedule(coro, trigger=trigger)
                if _debug:
                    self.log.debug("Scheduled coroutine %s" % (coro.__name__))

            if not depth:
                # Schedule may have queued up some events so we'll burn through those
                while self._pending_events:
                    if _debug:
                        self.log.debug("Scheduling pending event %s" %
                                       (str(self._pending_events[0])))
                    self._pending_events.pop(0).set()

            while self._pending_triggers:
                if _debug:
                    self.log.debug("Scheduling pending trigger %s" %
                                   (str(self._pending_triggers[0])))
                self.react(self._pending_triggers.pop(0), depth=depth + 1)

            # We only advance for GPI triggers
            if not depth and isinstance(trigger, GPITrigger):
                self.advance()

                if _debug:
                    self.log.debug(
                        "All coroutines scheduled, handing control back"
                        " to simulator")

    def unschedule(self, coro):
        """Unschedule a coroutine.  Unprime any pending triggers"""

        for trigger in self._coro2triggers[coro]:
            if coro in self._trigger2coros[trigger]:
                self._trigger2coros[trigger].remove(coro)
            if not self._trigger2coros[trigger]:
                trigger.unprime()
                del self._trigger2coros[trigger]
        del self._coro2triggers[coro]

        if Join(coro) in self._trigger2coros:
            self._pending_triggers.append(Join(coro))
        else:
            try:
                # throws an error if the background coroutine errored
                # and no one was monitoring it
                coro.retval
            except Exception as e:
                self._test_result = TestError(
                    "Forked coroutine {} raised exception {}".format(coro, e))
                self._terminate = True

    def save_write(self, handle, value):
        if self._mode == Scheduler._MODE_READONLY:
            raise Exception(
                "Write to object {0} was scheduled during a read-only sync phase."
                .format(handle._name))
        self._writes[handle] = value

    def _coroutine_yielded(self, coro, triggers):
        """Prime the triggers and update our internal mappings."""
        self._coro2triggers[coro] = triggers

        for trigger in triggers:

            self._trigger2coros[trigger].append(coro)
            if not trigger.primed:
                try:
                    trigger.prime(self.react)
                except Exception as e:
                    # Convert any exceptions into a test result
                    self.finish_test(
                        create_error(
                            self, "Unable to prime trigger %s: %s" %
                            (str(trigger), str(e))))

    def queue(self, coroutine):
        """Queue a coroutine for execution"""
        self._pending_coros.append(coroutine)

    def queue_function(self, coroutine):
        """Queue a coroutine for execution and move the containing thread
        so that it does not block execution of the main thread any longer.
        """

        # We should be able to find ourselves inside the _pending_threads list

        for t in self._pending_threads:
            if t.thread == threading.current_thread():
                t.thread_suspend()
                self._pending_coros.append(coroutine)
                return t

    def run_in_executor(self, func, *args, **kwargs):
        """Run the coroutine in a separate execution thread
        and return a yieldable object for the caller.
        """

        # Create a thread
        # Create a trigger that is called as a result of the thread finishing
        # Create an Event object that the caller can yield on
        # Event object set when the thread finishes execution, this blocks the
        #   calling coroutine (but not the thread) until the external completes

        def execute_external(func, _waiter):
            _waiter._outcome = outcomes.capture(func, *args, **kwargs)
            if _debug:
                self.log.debug("Execution of external routine done %s" %
                               threading.current_thread())
            _waiter.thread_done()

        waiter = external_waiter()
        thread = threading.Thread(group=None,
                                  target=execute_external,
                                  name=func.__name__ + "_thread",
                                  args=([func, waiter]),
                                  kwargs={})

        waiter.thread = thread
        self._pending_threads.append(waiter)

        return waiter

    def add(self, coroutine):
        """Add a new coroutine.

        Just a wrapper around self.schedule which provides some debug and
        useful error messages in the event of common gotchas.
        """
        if isinstance(coroutine, cocotb.decorators.coroutine):
            self.log.critical(
                "Attempt to schedule a coroutine that hasn't started")
            coroutine.log.error("This is the failing coroutine")
            self.log.warning(
                "Did you forget to add parentheses to the @test decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        elif not isinstance(coroutine, cocotb.decorators.RunningCoroutine):
            self.log.critical(
                "Attempt to add something to the scheduler which isn't a "
                "coroutine")
            self.log.warning("Got: %s (%s)" %
                             (str(type(coroutine)), repr(coroutine)))
            self.log.warning("Did you use the @coroutine decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        if _debug:
            self.log.debug("Adding new coroutine %s" % coroutine.__name__)

        self.schedule(coroutine)
        self.advance()
        return coroutine

    def new_test(self, coroutine):
        self._entrypoint = coroutine

    def schedule(self, coroutine, trigger=None):
        """Schedule a coroutine by calling the send method.

        Args:
            coroutine (cocotb.decorators.coroutine): The coroutine to schedule.
            trigger (cocotb.triggers.Trigger): The trigger that caused this
                coroutine to be scheduled.
        """
        if trigger is None:
            send_outcome = outcomes.Value(None)
        else:
            send_outcome = trigger._outcome
        if _debug:
            self.log.debug("Scheduling with {}".format(send_outcome))

        try:
            result = coroutine._advance(send_outcome)
            if _debug:
                self.log.debug("Coroutine %s yielded %s (mode %d)" %
                               (coroutine.__name__, str(result), self._mode))

        # TestComplete indication is game over, tidy up
        except TestComplete as test_result:
            # Tag that close down is needed, save the test_result
            # for later use in cleanup handler
            self.log.debug("TestComplete received: %s" %
                           test_result.__class__.__name__)
            self.finish_test(test_result)
            return

        # Normal coroutine completion
        except cocotb.decorators.CoroutineComplete as exc:
            if _debug:
                self.log.debug("Coroutine completed: %s" % str(coroutine))
            self.unschedule(coroutine)
            return

        # Don't handle the result if we're shutting down
        if self._terminate:
            return

        # Queue current routine to schedule when the nested routine exits
        yield_successful = False
        if isinstance(result, cocotb.decorators.RunningCoroutine):

            if not result.has_started():
                self.queue(result)
                if _debug:
                    self.log.debug("Scheduling nested coroutine: %s" %
                                   result.__name__)
            else:
                if _debug:
                    self.log.debug("Joining to already running coroutine: %s" %
                                   result.__name__)

            new_trigger = result.join()
            self._coroutine_yielded(coroutine, [new_trigger])
            yield_successful = True

        elif isinstance(result, Trigger):
            if _debug:
                self.log.debug("%s: is instance of Trigger" % result)
            self._coroutine_yielded(coroutine, [result])
            yield_successful = True

        # If we get a list, make sure it's a list of triggers or coroutines.
        # For every coroutine, replace it with coroutine.join().
        # This could probably be done more elegantly via list comprehension.
        elif isinstance(result, list):
            new_triggers = []
            for listobj in result:
                if isinstance(listobj, Trigger):
                    new_triggers.append(listobj)
                elif isinstance(listobj, cocotb.decorators.RunningCoroutine):
                    if _debug:
                        self.log.debug("Scheduling coroutine in list: %s" %
                                       listobj.__name__)
                    if not listobj.has_started():
                        self.queue(listobj)
                    new_trigger = listobj.join()
                    new_triggers.append(new_trigger)
                else:
                    # If we encounter something not a coroutine or trigger,
                    # set the success flag to False and break out of the loop.
                    yield_successful = False
                    break

            # Make sure the lists are the same size. If they are not, it means
            # it contained something not a trigger/coroutine, so do nothing.
            if len(new_triggers) == len(result):
                self._coroutine_yielded(coroutine, new_triggers)
                yield_successful = True

        # If we didn't successfully yield anything, thrown an error.
        # Do it this way to make the logic in the list case simpler.
        if not yield_successful:
            msg = (
                "Coroutine %s yielded something the scheduler can't handle" %
                str(coroutine))
            msg += ("\nGot type: %s repr: %s str: %s" %
                    (type(result), repr(result), str(result)))
            msg += "\nDid you forget to decorate with @cocotb.coroutine?"
            try:
                raise_error(self, msg)
            except Exception as e:
                self.finish_test(e)

        # We do not return from here until pending threads have completed, but only
        # from the main thread, this seems like it could be problematic in cases
        # where a sim might change what this thread is.
        def unblock_event(ext):
            @cocotb.coroutine
            def wrapper():
                ext.event.set()
                yield PythonTrigger()

        if self._main_thread is threading.current_thread():

            for ext in self._pending_threads:
                ext.thread_start()
                if _debug:
                    self.log.debug("Blocking from %s on %s" %
                                   (threading.current_thread(), ext.thread))
                state = ext.thread_wait()
                if _debug:
                    self.log.debug(
                        "Back from wait on self %s with newstate %d" %
                        (threading.current_thread(), state))
                if state == external_state.EXITED:
                    self._pending_threads.remove(ext)
                    self._pending_events.append(ext.event)

        # Handle any newly queued coroutines that need to be scheduled
        while self._pending_coros:
            self.add(self._pending_coros.pop(0))

        while self._pending_callbacks:
            self._pending_callbacks.pop(0)()

    def finish_test(self, test_result):
        """Cache the test result and set the terminate flag."""
        self.log.debug("finish_test called with %s" % (repr(test_result)))
        if not self._terminate:
            self._terminate = True
            self._test_result = test_result
            self.cleanup()

    def finish_scheduler(self, test_result):
        """Directly call into the regression manager and end test
           once we return the sim will close us so no cleanup is needed.
        """
        self.log.debug("Issue sim closedown result to regression object")
        cocotb.regression.handle_result(test_result)

    def cleanup(self):
        """Clear up all our state.

        Unprime all pending triggers and kill off any coroutines stop all externals.
        """
        for trigger, waiting in dict(self._trigger2coros).items():
            for coro in waiting:
                if _debug:
                    self.log.debug("Killing %s" % str(coro))
                coro.kill()

        if self._main_thread is not threading.current_thread():
            raise Exception("Cleanup() called outside of the main thread")

        for ext in self._pending_threads:
            self.log.warn("Waiting for %s to exit", ext.thread)
示例#10
0
class RegressionManager(object):
    """Encapsulates all regression capability into a single place"""
    def __init__(self, root_name, modules, tests=None, seed=None, hooks=[]):
        """
        Args:
            root_name (str): The name of the root handle.
            modules (list): A list of Python module names to run.
            tests (list, optional): A list of tests to run.
                Defaults to ``None``, meaning all discovered tests will be run.
            seed (int,  optional): The seed for the random number generator to use.
                Defaults to ``None``.
            hooks (list, optional): A list of hook modules to import.
                Defaults to the empty list.
        """
        self._queue = []
        self._root_name = root_name
        self._dut = None
        self._modules = modules
        self._functions = tests
        self._running_test = None
        self._cov = None
        self.log = SimLog("cocotb.regression")
        self._seed = seed
        self._hooks = hooks

    def initialise(self):

        self.start_time = time.time()
        self.test_results = []
        self.ntests = 0
        self.count = 1
        self.skipped = 0
        self.failures = 0

        # Setup XUnit
        ###################

        results_filename = os.getenv('COCOTB_RESULTS_FILE', "results.xml")
        suite_name = os.getenv('RESULT_TESTSUITE', "all")
        package_name = os.getenv('RESULT_TESTPACKAGE', "all")

        self.xunit = XUnitReporter(filename=results_filename)

        self.xunit.add_testsuite(name=suite_name,
                                 tests=repr(self.ntests),
                                 package=package_name)

        if (self._seed is not None):
            self.xunit.add_property(name="random_seed",
                                    value=("%d" % self._seed))

        # Setup Coverage
        ####################

        if coverage is not None:
            self.log.info("Enabling coverage collection of Python code")
            self._cov = coverage.coverage(branch=True, omit=["*cocotb*"])
            self._cov.start()

        # Setup DUT object
        #######################

        handle = simulator.get_root_handle(self._root_name)

        self._dut = cocotb.handle.SimHandle(handle) if handle else None

        if self._dut is None:
            raise AttributeError("Can not find Root Handle (%s)" %
                                 self._root_name)

        # Test Discovery
        ####################

        for module_name in self._modules:
            try:
                self.log.debug("Python Path: " + ",".join(sys.path))
                self.log.debug("PWD: " + os.getcwd())
                module = _my_import(module_name)
            except Exception as E:
                self.log.critical("Failed to import module %s: %s",
                                  module_name, E)
                self.log.info("MODULE variable was \"%s\"",
                              ".".join(self._modules))
                self.log.info("Traceback: ")
                self.log.info(traceback.format_exc())
                raise

            if self._functions:

                # Specific functions specified, don't auto-discover
                for test in self._functions.rsplit(','):
                    try:
                        _test = getattr(module, test)
                    except AttributeError:
                        self.log.error(
                            "Requested test %s wasn't found in module %s",
                            test, module_name)
                        err = AttributeError("Test %s doesn't exist in %s" %
                                             (test, module_name))
                        _py_compat.raise_from(err,
                                              None)  # discard nested traceback

                    if not hasattr(_test, "im_test"):
                        self.log.error(
                            "Requested %s from module %s isn't a cocotb.test decorated coroutine",
                            test, module_name)
                        raise ImportError("Failed to find requested test %s" %
                                          test)
                    self._init_test(_test)

                # only look in first module for all functions and don't complain if all functions are not found
                break

            # auto-discover
            for thing in vars(module).values():
                if hasattr(thing, "im_test"):
                    self._init_test(thing)

        self._queue.sort(key=lambda test: (test.stage, test._id))

        for valid_tests in self._queue:
            self.log.info("Found test %s.%s" %
                          (valid_tests.module, valid_tests.funcname))

        # Process Hooks
        ###################

        for module_name in self._hooks:
            self.log.info("Loading hook from module '" + module_name + "'")
            module = _my_import(module_name)

            for thing in vars(module).values():
                if hasattr(thing, "im_hook"):
                    try:
                        test = thing(self._dut)
                    except Exception:
                        self.log.warning("Failed to initialize hook %s" %
                                         thing.name,
                                         exc_info=True)
                    else:
                        cocotb.scheduler.add(test)

    def tear_down(self):
        # fail remaining tests
        while True:
            test = self.next_test()
            if test is None:
                break
            self.xunit.add_testcase(name=test.funcname,
                                    classname=test.module,
                                    time=repr(0),
                                    sim_time_ns=repr(0),
                                    ratio_time=repr(0))
            result_pass, _ = self._score_test(
                test, cocotb.outcomes.Error(SimFailure()))
            self._store_test_result(test.__module__, test.__name__,
                                    result_pass, 0, 0, 0)
            if not result_pass:
                self.xunit.add_failure()
                self.failures += 1

        # Write out final log messages
        if self.failures:
            self.log.error("Failed %d out of %d tests (%d skipped)" %
                           (self.failures, self.count - 1, self.skipped))
        else:
            self.log.info("Passed %d tests (%d skipped)" %
                          (self.count - 1, self.skipped))
        if len(self.test_results) > 0:
            self._log_test_summary()
        self._log_sim_summary()
        self.log.info("Shutting down...")

        # Generate output reports
        self.xunit.write()
        if self._cov:
            self._cov.stop()
            self.log.info("Writing coverage data")
            self._cov.save()
            self._cov.html_report()

        # Setup simulator finalization
        simulator.stop_simulator()

    def next_test(self):
        """Get the next test to run"""
        if not self._queue:
            return None
        return self._queue.pop(0)

    def handle_result(self, test):
        """Handle a test completing.

        Dump result to XML and schedule the next test (if any). Entered by the scheduler.

        Args:
            test: The test that completed
        """
        assert test is self._running_test

        real_time = time.time() - test.start_time
        sim_time_ns = get_sim_time('ns') - test.start_sim_time
        ratio_time = self._safe_divide(sim_time_ns, real_time)

        self.xunit.add_testcase(name=test.funcname,
                                classname=test.module,
                                time=repr(real_time),
                                sim_time_ns=repr(sim_time_ns),
                                ratio_time=repr(ratio_time))

        # score test
        result_pass, sim_failed = self._score_test(test, test._outcome)

        # stop capturing log output
        cocotb.log.removeHandler(test.handler)

        # Save results
        self._store_test_result(test.__module__, test.__name__, result_pass,
                                sim_time_ns, real_time, ratio_time)
        if not result_pass:
            self.xunit.add_failure()
            self.failures += 1

        # Fail if required
        if sim_failed:
            self.tear_down()
            return

        self.execute()

    def _init_test(self, test_func):
        """
        Initializes a test.

        Records outcome if the initialization fails.
        Records skip if the test is skipped.
        Saves the initialized test if it successfully initializes.
        """
        test_init_outcome = cocotb.outcomes.capture(test_func, self._dut)

        if isinstance(test_init_outcome, cocotb.outcomes.Error):
            self.log.error("Failed to initialize test %s" % test_func.name,
                           exc_info=True)
            self.xunit.add_testcase(name=test_func.name,
                                    classname=test_func.__module__,
                                    time="0.0",
                                    sim_time_ns="0.0",
                                    ratio_time="0.0")
            result_pass, sim_failed = self._score_test(test_func,
                                                       test_init_outcome)
            # Save results
            self._store_test_result(test_func.__module__, test_func.__name__,
                                    result_pass, 0.0, 0.0, 0.0)
            if not result_pass:
                self.xunit.add_failure()
                self.failures += 1
            # Fail if required
            if sim_failed:
                self.tear_down()
                raise SimFailure(
                    "Test initialization caused a simulator failure. Shutting down."
                )

        else:
            test = test_init_outcome.get()
            if test.skip:
                self.log.info("Skipping test %s" % test_func.name)
                self.xunit.add_testcase(name=test_func.name,
                                        classname=test.module,
                                        time="0.0",
                                        sim_time_ns="0.0",
                                        ratio_time="0.0")
                self.xunit.add_skipped()
                self.skipped += 1
                self._store_test_result(test.module, test_func.name, None, 0.0,
                                        0.0, 0.0)
            else:
                self._queue.append(test)
                self.ntests += 1

    def _score_test(self, test, outcome):
        """
        Given a test and the test's outcome, determine if the test met expectations and log pertinent information
        """

        # Helper for logging result
        def _result_was():
            result_was = ("{} (result was {})".format(
                test.__name__, result.__class__.__name__))
            return result_was

        # scoring outcomes
        result_pass = True
        sim_failed = False

        try:
            outcome.get()
        except Exception as e:
            result = remove_traceback_frames(e, ['_score_test', 'get'])
        else:
            result = TestSuccess()

        if (isinstance(result, TestSuccess) and not test.expect_fail
                and not test.expect_error):
            self.log.info("Test Passed: %s" % test.__name__)

        elif (isinstance(result, AssertionError) and test.expect_fail):
            self.log.info("Test failed as expected: " + _result_was())

        elif (isinstance(result, TestSuccess) and test.expect_error):
            self.log.error("Test passed but we expected an error: " +
                           _result_was())
            result_pass = False

        elif isinstance(result, TestSuccess):
            self.log.error("Test passed but we expected a failure: " +
                           _result_was())
            result_pass = False

        elif isinstance(result, SimFailure):
            if isinstance(result, test.expect_error):
                self.log.info("Test errored as expected: " + _result_was())
            else:
                self.log.error(
                    "Test error has lead to simulator shutting us "
                    "down",
                    exc_info=result)
                result_pass = False
            # whether we expected it or not, the simulation has failed unrecoverably
            sim_failed = True

        elif test.expect_error:
            if isinstance(result, test.expect_error):
                self.log.info("Test errored as expected: " + _result_was())
            else:
                self.log.error("Test errored with unexpected type: " +
                               _result_was(),
                               exc_info=result)
                result_pass = False

        else:
            self.log.error("Test Failed: " + _result_was(), exc_info=result)
            result_pass = False

            if _pdb_on_exception:
                pdb.post_mortem(result.__traceback__)

        return result_pass, sim_failed

    def execute(self):
        self._running_test = cocotb.regression_manager.next_test()
        if self._running_test:
            start = ''
            end = ''
            if want_color_output():
                start = ANSI.COLOR_TEST
                end = ANSI.COLOR_DEFAULT
            # Want this to stand out a little bit
            self.log.info("%sRunning test %d/%d:%s %s" %
                          (start, self.count, self.ntests, end,
                           self._running_test.funcname))

            # start capturing log output
            cocotb.log.addHandler(self._running_test.handler)

            cocotb.scheduler.add_test(self._running_test)
            self.count += 1
        else:
            self.tear_down()

    def _log_test_summary(self):
        TEST_FIELD = 'TEST'
        RESULT_FIELD = 'PASS/FAIL'
        SIM_FIELD = 'SIM TIME(NS)'
        REAL_FIELD = 'REAL TIME(S)'
        RATIO_FIELD = 'RATIO(NS/S)'

        TEST_FIELD_LEN = max(
            len(TEST_FIELD),
            len(max([x['test'] for x in self.test_results], key=len)))
        RESULT_FIELD_LEN = len(RESULT_FIELD)
        SIM_FIELD_LEN = len(SIM_FIELD)
        REAL_FIELD_LEN = len(REAL_FIELD)
        RATIO_FIELD_LEN = len(RATIO_FIELD)

        LINE_LEN = 3 + TEST_FIELD_LEN + 2 + RESULT_FIELD_LEN + 2 + SIM_FIELD_LEN + 2 + REAL_FIELD_LEN + 2 + RATIO_FIELD_LEN + 3

        LINE_SEP = "*" * LINE_LEN + "\n"

        summary = ""
        summary += LINE_SEP
        summary += "** {a:<{a_len}}  {b:^{b_len}}  {c:>{c_len}}  {d:>{d_len}}  {e:>{e_len}} **\n".format(
            a=TEST_FIELD,
            a_len=TEST_FIELD_LEN,
            b=RESULT_FIELD,
            b_len=RESULT_FIELD_LEN,
            c=SIM_FIELD,
            c_len=SIM_FIELD_LEN,
            d=REAL_FIELD,
            d_len=REAL_FIELD_LEN,
            e=RATIO_FIELD,
            e_len=RATIO_FIELD_LEN)
        summary += LINE_SEP
        for result in self.test_results:
            hilite = ''

            if result['pass'] is None:
                pass_fail_str = "N/A"
            elif result['pass']:
                pass_fail_str = "PASS"
            else:
                pass_fail_str = "FAIL"
                if want_color_output():
                    hilite = ANSI.COLOR_HILITE_SUMMARY

            summary += "{start}** {a:<{a_len}}  {b:^{b_len}}  {c:>{c_len}.2f}   {d:>{d_len}.2f}   {e:>{e_len}.2f}  **\n".format(
                a=result['test'],
                a_len=TEST_FIELD_LEN,
                b=pass_fail_str,
                b_len=RESULT_FIELD_LEN,
                c=result['sim'],
                c_len=SIM_FIELD_LEN - 1,
                d=result['real'],
                d_len=REAL_FIELD_LEN - 1,
                e=result['ratio'],
                e_len=RATIO_FIELD_LEN - 1,
                start=hilite)
        summary += LINE_SEP

        self.log.info(summary)

    def _log_sim_summary(self):
        real_time = time.time() - self.start_time
        sim_time_ns = get_sim_time('ns')
        ratio_time = self._safe_divide(sim_time_ns, real_time)

        summary = ""

        summary += "*************************************************************************************\n"
        summary += "**                                 ERRORS : {0:<39}**\n".format(
            self.failures)
        summary += "*************************************************************************************\n"
        summary += "**                               SIM TIME : {0:<39}**\n".format(
            '{0:.2f} NS'.format(sim_time_ns))
        summary += "**                              REAL TIME : {0:<39}**\n".format(
            '{0:.2f} S'.format(real_time))
        summary += "**                        SIM / REAL TIME : {0:<39}**\n".format(
            '{0:.2f} NS/S'.format(ratio_time))
        summary += "*************************************************************************************\n"

        self.log.info(summary)

    @staticmethod
    def _safe_divide(a, b):
        try:
            return a / b
        except ZeroDivisionError:
            if a == 0:
                return float('nan')
            else:
                return float('inf')

    def _store_test_result(self, module_name, test_name, result_pass, sim_time,
                           real_time, ratio):
        result = {
            'test': '.'.join([module_name, test_name]),
            'pass': result_pass,
            'sim': sim_time,
            'real': real_time,
            'ratio': ratio
        }
        self.test_results.append(result)
示例#11
0
class RunningCoroutine(object):
    """Per instance wrapper around an function to turn it into a coroutine.

    Provides the following:

        coro.join() creates a Trigger that will fire when this coroutine
        completes.

        coro.kill() will destroy a coroutine instance (and cause any Join
        triggers to fire.
    """
    def __init__(self, inst, parent):
        if hasattr(inst, "__name__"):
            self.__name__ = "%s" % inst.__name__
            self.log = SimLog("cocotb.coroutine.%s" % self.__name__, id(self))
        else:
            self.log = SimLog("cocotb.coroutine.fail")
        self._coro = inst
        self._started = False
        self._callbacks = []
        self._parent = parent
        self.__doc__ = parent._func.__doc__
        self.module = parent._func.__module__
        self.funcname = parent._func.__name__
        self._outcome = None

        if not hasattr(self._coro, "send"):
            self.log.error("%s isn't a valid coroutine! Did you use the yield "
                           "keyword?" % self.funcname)
            raise CoroutineComplete()

    @property
    def retval(self):
        if self._outcome is None:
            raise RuntimeError("coroutine is not complete")
        return self._outcome.get()

    @property
    def _finished(self):
        return self._outcome is not None

    def __iter__(self):
        return self

    def __str__(self):
        return str(self.__name__)

    def _advance(self, outcome):
        """
        Advance to the next yield in this coroutine

        :param outcome: The `outcomes.Outcome` object to resume with.
        :returns: The object yielded from the coroutine

        If the coroutine returns or throws an error, self._outcome is set, and
        this throws `CoroutineComplete`.
        """
        try:
            self._started = True
            return outcome.send(self._coro)
        except ReturnValue as e:
            self._outcome = outcomes.Value(e.retval)
            raise CoroutineComplete()
        except StopIteration as e:
            retval = getattr(e, 'value', None)  # for python >=3.3
            self._outcome = outcomes.Value(retval)
            raise CoroutineComplete()
        except BaseException as e:
            self._outcome = outcomes.Error(e)
            raise CoroutineComplete()

    def send(self, value):
        return self._coro.send(value)

    def throw(self, exc):
        return self._coro.throw(exc)

    def close(self):
        return self._coro.close()

    def kill(self):
        """Kill a coroutine."""
        self.log.debug("kill() called on coroutine")
        # todo: probably better to throw an exception for anyone waiting on the coroutine
        self._outcome = outcomes.Value(None)
        cocotb.scheduler.unschedule(self)

    def join(self):
        """Return a trigger that will fire when the wrapped coroutine exits."""
        return Join(self)

    def has_started(self):
        return self._started

    def __nonzero__(self):
        """Provide boolean testing
            if the coroutine has finished return false
            otherwise return true"""
        return not self._finished

    __bool__ = __nonzero__

    def sort_name(self):
        if self.stage is None:
            return "%s.%s" % (self.module, self.funcname)
        else:
            return "%s.%d.%s" % (self.module, self.stage, self.funcname)
示例#12
0
class RegressionManager(object):
    """Encapsulates all regression capability into a single place"""
    def __init__(self, root_name, modules, tests=None):
        """
        Args:
            modules (list): A list of python module names to run

        Kwargs
        """
        self._queue = []
        self._root_name = root_name
        self._dut = None
        self._modules = modules
        self._functions = tests
        self._running_test = None
        self._cov = None
        self.log = SimLog("cocotb.regression")

    def initialise(self):

        self.ntests = 0
        self.count = 1
        self.skipped = 0
        self.failures = 0
        self.xunit = XUnitReporter()
        self.xunit.add_testsuite(name="all",
                                 tests=repr(self.ntests),
                                 package="all")

        if coverage is not None:
            self.log.info("Enabling coverage collection of Python code")
            self._cov = coverage.coverage(branch=True, omit=["*cocotb*"])
            self._cov.start()

        self._dut = cocotb.handle.SimHandle(
            simulator.get_root_handle(self._root_name))
        if self._dut is None:
            raise AttributeError("Can not find Root Handle (%s)" % root_name)

        # Auto discovery
        for module_name in self._modules:
            module = _my_import(module_name)

            if self._functions:

                # Specific functions specified, don't auto discover
                for test in self._functions.rsplit(','):
                    if not hasattr(module, test):
                        raise AttributeError("Test %s doesn't exist in %s" %
                                             (test, module_name))

                    self._queue.append(getattr(module, test)(self._dut))
                    self.ntests += 1
                break

            for thing in vars(module).values():
                if hasattr(thing, "im_test"):
                    try:
                        test = thing(self._dut)
                        skip = test.skip
                    except TestError:
                        skip = True
                        self.log.warning("Failed to initialise test %s" %
                                         thing.name)

                    if skip:
                        self.log.info("Skipping test %s" % thing.name)
                        self.xunit.add_testcase(name=thing.name,
                                                classname=module_name,
                                                time="0.0")
                        self.xunit.add_skipped()
                        self.skipped += 1
                    else:
                        self._queue.append(test)
                        self.ntests += 1

        self._queue.sort(
            key=lambda test: "%s.%s" % (test.module, test.funcname))

        for valid_tests in self._queue:
            self.log.info("Found test %s.%s" %
                          (valid_tests.module, valid_tests.funcname))

    def tear_down(self):
        """It's the end of the world as we know it"""
        if self.failures:
            self.log.error("Failed %d out of %d tests (%d skipped)" %
                           (self.failures, self.count - 1, self.skipped))
        else:
            self.log.info("Passed %d tests (%d skipped)" %
                          (self.count - 1, self.skipped))
        if self._cov:
            self._cov.stop()
            self.log.info("Writing coverage data")
            self._cov.save()
            self._cov.html_report()
        self.log.info("Shutting down...")
        self.xunit.write()
        simulator.stop_simulator()

    def next_test(self):
        """Get the next test to run"""
        if not self._queue: return None
        return self._queue.pop(0)

    def handle_result(self, result):
        """Handle a test result

        Dumps result to XML and schedules the next test (if any)

        Args: result (TestComplete exception)
        """
        self.xunit.add_testcase(name=self._running_test.funcname,
                                classname=self._running_test.module,
                                time=repr(time.time() -
                                          self._running_test.start_time))

        if isinstance(
                result, TestSuccess
        ) and not self._running_test.expect_fail and not self._running_test.expect_error:
            self.log.info("Test Passed: %s" % self._running_test.funcname)

        elif isinstance(result,
                        TestFailure) and self._running_test.expect_fail:
            self.log.info(
                "Test failed as expected: %s (result was %s)" %
                (self._running_test.funcname, result.__class__.__name__))

        elif isinstance(result,
                        TestSuccess) and self._running_test.expect_error:
            self.log.error(
                "Test passed but we expected an error: %s (result was %s)" %
                (self._running_test.funcname, result.__class__.__name__))
            self.xunit.add_failure(stdout=repr(str(result)),
                                   stderr="\n".join(
                                       self._running_test.error_messages))
            self.failures += 1

        elif isinstance(result, TestSuccess):
            self.log.error(
                "Test passed but we expected a failure: %s (result was %s)" %
                (self._running_test.funcname, result.__class__.__name__))
            self.xunit.add_failure(stdout=repr(str(result)),
                                   stderr="\n".join(
                                       self._running_test.error_messages))
            self.failures += 1

        elif isinstance(result, TestError) and self._running_test.expect_error:
            self.log.info(
                "Test errored as expected: %s (result was %s)" %
                (self._running_test.funcname, result.__class__.__name__))

        elif isinstance(result, SimFailure):
            if self._running_test.expect_error:
                self.log.info(
                    "Test errored as expected: %s (result was %s)" %
                    (self._running_test.funcname, result.__class__.__name__))
            else:
                self.log.error(
                    "Test error has lead to simulator shuttting us down")
                self.failures += 1
                self.tear_down()
                return

        else:
            self.log.error(
                "Test Failed: %s (result was %s)" %
                (self._running_test.funcname, result.__class__.__name__))
            self.xunit.add_failure(stdout=repr(str(result)),
                                   stderr="\n".join(
                                       self._running_test.error_messages))
            self.failures += 1

        self.execute()

    def execute(self):
        self._running_test = cocotb.regression.next_test()
        if self._running_test:
            # Want this to stand out a little bit
            self.log.info("%sRunning test %d/%d:%s %s" %
                          (ANSI.BLUE_BG + ANSI.BLACK_FG, self.count,
                           self.ntests, ANSI.DEFAULT_FG + ANSI.DEFAULT_BG,
                           self._running_test.funcname))
            if self.count is 1:
                test = cocotb.scheduler.add(self._running_test)
            else:
                test = cocotb.scheduler.new_test(self._running_test)
            self.count += 1
        else:
            self.tear_down()
示例#13
0
class RunningCoroutine(object):
    """Per instance wrapper around an function to turn it into a coroutine.

    Provides the following:

        coro.join() creates a Trigger that will fire when this coroutine
        completes.

        coro.kill() will destroy a coroutine instance (and cause any Join
        triggers to fire.
    """
    def __init__(self, inst, parent):
        if hasattr(inst, "__name__"):
            self.__name__ = "%s" % inst.__name__
            self.log = SimLog("cocotb.coroutine.%s" % self.__name__, id(self))
        else:
            self.log = SimLog("cocotb.coroutine.fail")

        if sys.version_info[:2] >= (3, 5) and inspect.iscoroutine(inst):
            self._natively_awaitable = True
            self._coro = inst.__await__()
        else:
            self._natively_awaitable = False
            self._coro = inst
        self._started = False
        self._callbacks = []
        self._parent = parent
        self.__doc__ = parent._func.__doc__
        self.module = parent._func.__module__
        self.funcname = parent._func.__name__
        self._outcome = None

        if not hasattr(self._coro, "send"):
            self.log.error("%s isn't a valid coroutine! Did you use the yield "
                           "keyword?" % self.funcname)
            raise CoroutineComplete()

    @property
    def retval(self):
        if self._outcome is None:
            raise RuntimeError("coroutine is not complete")
        return self._outcome.get()

    @property
    def _finished(self):
        return self._outcome is not None

    def __iter__(self):
        return self

    def __str__(self):
        return str(self.__name__)

    def _advance(self, outcome):
        """
        Advance to the next yield in this coroutine

        :param outcome: The `outcomes.Outcome` object to resume with.
        :returns: The object yielded from the coroutine

        If the coroutine returns or throws an error, self._outcome is set, and
        this throws `CoroutineComplete`.
        """
        try:
            self._started = True
            return outcome.send(self._coro)
        except ReturnValue as e:
            self._outcome = outcomes.Value(e.retval)
            raise CoroutineComplete()
        except StopIteration as e:
            retval = getattr(e, 'value', None)  # for python >=3.3
            self._outcome = outcomes.Value(retval)
            raise CoroutineComplete()
        except BaseException as e:
            self._outcome = outcomes.Error(e)
            raise CoroutineComplete()

    def send(self, value):
        return self._coro.send(value)

    def throw(self, exc):
        return self._coro.throw(exc)

    def close(self):
        return self._coro.close()

    def kill(self):
        """Kill a coroutine."""
        if self._outcome is not None:
            # already finished, nothing to kill
            return

        self.log.debug("kill() called on coroutine")
        # todo: probably better to throw an exception for anyone waiting on the coroutine
        self._outcome = outcomes.Value(None)
        cocotb.scheduler.unschedule(self)

    def join(self):
        """Return a trigger that will fire when the wrapped coroutine exits."""
        return cocotb.triggers.Join(self)

    def has_started(self):
        return self._started

    def __nonzero__(self):
        """Provide boolean testing
            if the coroutine has finished return false
            otherwise return true"""
        return not self._finished

    # Once 2.7 is dropped, this can be run unconditionally
    if sys.version_info >= (3, 3):
        exec_(textwrap.dedent("""
        def __await__(self):
            # It's tempting to use `return (yield from self._coro)` here,
            # which bypasses the scheduler. Unfortunately, this means that
            # we can't keep track of the result or state of the coroutine,
            # things which we expose in our public API. If you want the
            # efficiency of bypassing the scheduler, remove the `@coroutine`
            # decorator from your `async` functions.

            # Hand the coroutine back to the scheduler trampoline.
            return (yield self)
        """))

    __bool__ = __nonzero__

    def sort_name(self):
        if self.stage is None:
            return "%s.%s" % (self.module, self.funcname)
        else:
            return "%s.%d.%s" % (self.module, self.stage, self.funcname)
示例#14
0
class SimHandle(object):

    def __init__(self, handle):
        """
            Args:
                _handle [integer] : vpi/vhpi handle to the simulator object
        """
        self._handle = handle           # handle used for future simulator transactions
        self._sub_handles = {}          # Dictionary of SimHandle objects created by getattr
        self._len = None

        self.name = simulator.get_name_string(self._handle)
        self.fullname = self.name + '(%s)' % simulator.get_type_string(self._handle)
        self.log = SimLog('cocotb.' + self.name)
        self.log.debug("Created!")
        self._r_edge = _RisingEdge(self)
        self._f_edge = _FallingEdge(self)

    def __hash__(self):
        return self._handle

    def __str__(self):
        return "%s @0x%x" % (self.name, self._handle)

    def __getattr__(self, name):
        """ Query the simulator for a object with the specified name
            and cache the result to build a tree
        """
        if name in self._sub_handles:
            return self._sub_handles[name]
        new_handle = simulator.get_handle_by_name(self._handle, name)
        if not new_handle:
            self._raise_testerror("%s contains no object named %s" % (self.name, name))
        self._sub_handles[name] = SimHandle(new_handle)
        return self._sub_handles[name]

    def _raise_testerror(self, msg):
        lastframe = sys._getframe(2)
        if sys.version_info[0] >= 3:
            buff = StringIO()
            traceback.print_stack(lastframe, file=buff)
        else:
            buff_bytes = BytesIO()
            traceback.print_stack(lastframe, file=buff_bytes)
            buff = StringIO(buff_bytes.getvalue().decode("UTF8"))
        self.log.error("%s\n%s" % (msg, buff.getvalue()))
        exception = TestError(msg)
        exception.stderr.write(buff.getvalue())
        buff.close()
        raise exception

    def __setattr__(self, name, value):
        """Provide transparent access to signals"""
        if not name.startswith('_') and not name in ["name", "fullname", "log", "value"] \
                                                     and self.__hasattr__(name):
            getattr(self, name).setcachedvalue(value)
            return
        object.__setattr__(self, name, value)

    def __hasattr__(self, name):
        """
        Since calling hasattr(handle, "something") will print out a
        backtrace to the log since usually attempting to access a
        non-existent member is an error we provide a 'peek function

        We still add the found handle to our dictionary to prevent leaking
        handles.
        """
        if name in self._sub_handles:
            return self._sub_handles[name]
        new_handle = simulator.get_handle_by_name(self._handle, name)
        if new_handle:
            self._sub_handles[name] = SimHandle(new_handle)
        return new_handle

    def __getitem__(self, index):
        if index in self._sub_handles:
            return self._sub_handles[index]
        new_handle = simulator.get_handle_by_index(self._handle, index)
        if not new_handle:
            self._raise_testerror("%s contains no object at index %d" % (self.name, index))
        self._sub_handles[index] = SimHandle(new_handle)
        return self._sub_handles[index]

    def __setitem__(self, index, value):
        """Provide transparent assignment to bit index"""
        self.__getitem__(index).setcachedvalue(value)

    def getvalue(self):
        result = BinaryValue()
        result.binstr = self._get_value_str()
        return result

    def setimmediatevalue(self, value):
        """
        Set the value of the underlying simulation object to value.

        Args:
            value (ctypes.Structure, cocotb.binary.BinaryValue, int)
                The value to drive onto the simulator object

        Raises:
            TypeError

        This operation will fail unless the handle refers to a modifiable
        object eg net, signal or variable.

        We determine the library call to make based on the type of the value

        Assigning integers less than 32-bits is faster
        """
        if isinstance(value, get_python_integer_types()) and value < 0x7fffffff:
            simulator.set_signal_val(self._handle, value)
            return

        if isinstance(value, ctypes.Structure):
            value = BinaryValue(value=cocotb.utils.pack(value), bits=len(self))
        elif isinstance(value, get_python_integer_types()):
            value = BinaryValue(value=value, bits=len(self), bigEndian=False)
        elif not isinstance(value, BinaryValue):
            self.log.critical("Unsupported type for value assignment: %s (%s)" % (type(value), repr(value)))
            raise TypeError("Unable to set simulator value with type %s" % (type(value)))

        simulator.set_signal_val_str(self._handle, value.binstr)

    def setcachedvalue(self, value):
        """Intercept the store of a value and hold in cache.

        This operation is to enable all of the scheduled callbacks to completed
        with the same read data and for the writes to occour on the next
        sim time"""
        cocotb.scheduler.save_write(self, value)

    # We want to maintain compatability with python 2.5 so we can't use @property with a setter
    value = property(getvalue, setcachedvalue, None, "A reference to the value")

    def _get_value_str(self):
        return simulator.get_signal_val(self._handle)

    def __le__(self, value):
        """Overload the less than or equal to operator to
            provide an hdl-like shortcut
                module.signal <= 2
        """
        self.value = value


    def __len__(self):
        """Returns the 'length' of the underlying object.

        For vectors this is the number of bits.

        TODO: Handle other types (loops, generate etc)
        """
        if self._len is None:
            self._len = len(self._get_value_str())
        return self._len


    def __cmp__(self, other):

        # Permits comparison of handles i.e. if clk == dut.clk
        if isinstance(other, SimHandle):
            if self._handle == other._handle: return 0
            return 1

        # Use the comparison method of the other object against our value
        return self.value.__cmp__(other)


    def __iter__(self):
        """Iterates over all known types defined by simulator module"""
        for handle_type in [simulator.MODULE,
                            simulator.PARAMETER,
                            simulator.REG,
                            simulator.NET,
                            simulator.NETARRAY]:
            iterator = simulator.iterate(handle_type, self._handle)
            while True:
                try:
                    thing = simulator.next(iterator)
                except StopIteration:
                    break
                hdl = SimHandle(thing)
                self._sub_handles[hdl.name] = hdl
                yield hdl

    def __int__(self):
        return int(self.value)
示例#15
0
文件: handle.py 项目: icicle99/cocotb
class SimHandleBase(object):
    """
    Base class for all simulation objects.

    We maintain a handle which we can use for GPI calls
    """

    # For backwards compatibility we support a mapping of old member names
    # which may alias with the simulator hierarchy.  In these cases the
    # simulator result takes priority, only falling back to the python member
    # if there is no colliding object in the elaborated design.
    _compat_mapping = {
        "log"               :       "_log",
        "fullname"          :       "_fullname",
        "name"              :       "_name",
        }

    def __init__(self, handle):
        """
        Args:
            handle (integer)    : the GPI handle to the simulator object
        """
        self._handle = handle
        self._len = None
        self._sub_handles = {}  # Dictionary of children
        self._invalid_sub_handles = {} # Dictionary of invalid queries
        self._discovered = False

        self._name = simulator.get_name_string(self._handle)
        self._type = simulator.get_type_string(self._handle)
        self._fullname = self._name + "(%s)" % self._type
        self._log = SimLog("cocotb.%s" % self._name)
        self._log.debug("Created")

    def __hash__(self):
        return self._handle

    def __len__(self):
        """Returns the 'length' of the underlying object.

        For vectors this is the number of bits.
        """
        if self._len is None:
            self._len = simulator.get_num_elems(self._handle)
        return self._len

    def __eq__(self, other):

        # Permits comparison of handles i.e. if clk == dut.clk
        if isinstance(other, SimHandleBase):
            if self._handle == other._handle: return 0
            return 1

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return self._fullname

    def _raise_testerror(self, msg):
        lastframe = sys._getframe(2)
        if sys.version_info[0] >= 3:
            buff = StringIO()
            traceback.print_stack(lastframe, file=buff)
        else:
            buff_bytes = BytesIO()
            traceback.print_stack(lastframe, file=buff_bytes)
            buff = StringIO(buff_bytes.getvalue().decode("UTF8"))
        self._log.error("%s\n%s" % (msg, buff.getvalue()))
        exception = TestError(msg)
        exception.stderr.write(buff.getvalue())
        buff.close()
        raise exception
示例#16
0
文件: __init__.py 项目: TC01/cocotb
# Singleton scheduler instance
# NB this cheekily ensures a singleton since we're replacing the reference
# so that cocotb.scheduler gives you the singleton instance and not the
# scheduler package

# GPI logging instance
if "COCOTB_SIM" in os.environ:
    import simulator
    logging.basicConfig()
    logging.setLoggerClass(SimBaseLog)
    log = SimLog('cocotb')
    level = os.getenv("COCOTB_LOG_LEVEL", "INFO")
    try:
        _default_log = getattr(logging, level)
    except AttributeError as e:
        log.error("Unable to set loging level to %s" % level)
        _default_log = logging.INFO
    log.setLevel(_default_log)
    loggpi = SimLog('cocotb.gpi')
    # Notify GPI of log level
    simulator.log_level(_default_log)

    # If stdout/stderr are not TTYs, Python may not have opened them with line
    # buffering. In that case, try to reopen them with line buffering
    # explicitly enabled. This ensures that prints such as stack traces always
    # appear. Continue silently if this fails.
    try:
        if not sys.stdout.isatty():
            sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
            log.debug("Reopened stdout with line buffering")
        if not sys.stderr.isatty():
示例#17
0
class TestFactory(object):
    """Used to automatically generate tests.

    Assuming we have a common test function that will run a test. This test
    function will take keyword arguments (for example generators for each of
    the input interfaces) and generate tests that call the supplied function.

    This Factory allows us to generate sets of tests based on the different
    permutations of the possible arguments to the test function.

    For example if we have a module that takes backpressure and idles and
    have some packet generation routines ``gen_a`` and ``gen_b``:

    >>> tf = TestFactory(run_test)
    >>> tf.add_option('data_in', [gen_a, gen_b])
    >>> tf.add_option('backpressure', [None, random_backpressure])
    >>> tf.add_option('idles', [None, random_idles])
    >>> tf.generate_tests()

    We would get the following tests:
        * ``gen_a`` with no backpressure and no idles
        * ``gen_a`` with no backpressure and ``random_idles``
        * ``gen_a`` with ``random_backpressure`` and no idles
        * ``gen_a`` with ``random_backpressure`` and ``random_idles``
        * ``gen_b`` with no backpressure and no idles
        * ``gen_b`` with no backpressure and ``random_idles``
        * ``gen_b`` with ``random_backpressure`` and no idles
        * ``gen_b`` with ``random_backpressure`` and ``random_idles``

    The tests are appended to the calling module for auto-discovery.

    Tests are simply named ``test_function_N``. The docstring for the test (hence
    the test description) includes the name and description of each generator.
    """
    def __init__(self, test_function, *args, **kwargs):
        """
        Args:
            test_function (function): the function that executes a test.
                                      Must take 'dut' as the first argument.

            *args: Remaining args are passed directly to the test function.
                   Note that these arguments are not varied. An argument that
                   varies with each test must be a keyword argument to the
                   test function.
            *kwargs: Remaining kwargs are passed directly to the test function.
                   Note that these arguments are not varied. An argument that
                   varies with each test must be a keyword argument to the
                   test function.
        """
        if not isinstance(test_function, cocotb.coroutine):
            raise TypeError("TestFactory requires a cocotb coroutine")
        self.test_function = test_function
        self.name = self.test_function._func.__name__

        self.args = args
        self.kwargs_constant = kwargs
        self.kwargs = {}
        self.log = SimLog("cocotb.regression")

    def add_option(self, name, optionlist):
        """Add a named option to the test.

        Args:
           name (str): Name of the option. Passed to test as a keyword
                          argument.

           optionlist (list): A list of possible options for this test knob.
        """
        self.kwargs[name] = optionlist

    def generate_tests(self, prefix="", postfix=""):
        """
        Generates exhaustive set of tests using the cartesian product of the
        possible keyword arguments.

        The generated tests are appended to the namespace of the calling
        module.

        Args:
            prefix (str):  Text string to append to start of ``test_function`` name
                     when naming generated test cases. This allows reuse of
                     a single ``test_function`` with multiple
                     :class:`TestFactories <.TestFactory>` without name clashes.
            postfix (str): Text string to append to end of ``test_function`` name
                     when naming generated test cases. This allows reuse of
                     a single ``test_function`` with multiple
                     :class:`TestFactories <.TestFactory>` without name clashes.
        """

        frm = inspect.stack()[1]
        mod = inspect.getmodule(frm[0])

        d = self.kwargs

        for index, testoptions in enumerate(
            (dict(zip(d, v)) for v in product(*d.values()))):

            name = "%s%s%s_%03d" % (prefix, self.name, postfix, index + 1)
            doc = "Automatically generated test\n\n"

            for optname, optvalue in testoptions.items():
                if callable(optvalue):
                    if not optvalue.__doc__:
                        desc = "No docstring supplied"
                    else:
                        desc = optvalue.__doc__.split('\n')[0]
                    doc += "\t%s: %s (%s)\n" % (optname, optvalue.__name__,
                                                desc)
                else:
                    doc += "\t%s: %s\n" % (optname, repr(optvalue))

            self.log.debug("Adding generated test \"%s\" to module \"%s\"" %
                           (name, mod.__name__))
            kwargs = {}
            kwargs.update(self.kwargs_constant)
            kwargs.update(testoptions)
            if hasattr(mod, name):
                self.log.error("Overwriting %s in module %s. "
                               "This causes a previously defined testcase "
                               "not to be run. Consider setting/changing "
                               "name_postfix" % (name, mod))
            setattr(
                mod, name,
                _create_test(self.test_function, name, doc, mod, *self.args,
                             **kwargs))
示例#18
0
class RegressionManager(object):
    """Encapsulates all regression capability into a single place"""

    def __init__(self, root_name, modules, tests=None, seed=None, hooks=[]):
        """
        Args:
            modules (list): A list of python module names to run

        Kwargs
        """
        self._queue = []
        self._root_name = root_name
        self._dut = None
        self._modules = modules
        self._functions = tests
        self._running_test = None
        self._cov = None
        self.log = SimLog("cocotb.regression")
        self._seed = seed
        self._hooks = hooks

    def initialise(self):
        try:
            self._initialise()
        except Exception as e:
            import traceback
            self.log.error(traceback.format_exc())
            raise
        
    def _initialise(self):

        self.start_time = time.time()
        self.test_results = []
        self.ntests = 0
        self.count = 1
        self.skipped = 0
        self.failures = 0
        self.xunit = XUnitReporter()

        suite_name = os.getenv('RESULT_TESTSUITE') if os.getenv('RESULT_TESTSUITE') else "all"
        package_name = os.getenv('RESULT_TESTPACKAGE') if os.getenv('RESULT_TESTPACKAGE') else "all"
                
        self.xunit.add_testsuite(name=suite_name, tests=repr(self.ntests),
                                 package=package_name)
        
        if (self._seed is not None):
            self.xunit.add_property(name="random_seed", value=("%d"%self._seed))

        if coverage is not None:
            self.log.info("Enabling coverage collection of Python code")
            self._cov = coverage.coverage(branch=True, omit=["*cocotb*"])
            self._cov.start()

        handle = simulator.get_root_handle(self._root_name)

        self._dut = cocotb.handle.SimHandle(handle) if handle else None

        if self._dut is None:
            raise AttributeError("Can not find Root Handle (%s)" %
                                 self._root_name)

        # Auto discovery
        for module_name in self._modules:
            try:
                self.log.debug("Python Path: " + ",".join(sys.path))
                self.log.debug("PWD: " + os.getcwd())
                module = _my_import(module_name)
            except Exception as E:
                self.log.critical("Failed to import module %s: %s", module_name, E)
                self.log.info("MODULE variable was \"%s\"", ".".join(self._modules))
                self.log.info("Traceback: ")
                self.log.info(traceback.format_exc())
                raise

            if self._functions:

                # Specific functions specified, don't auto discover
                for test in self._functions.rsplit(','):
                    if not hasattr(module, test):
                        raise AttributeError("Test %s doesn't exist in %s" %
                                             (test, module_name))

                    self._queue.append(getattr(module, test)(self._dut))
                    self.ntests += 1
                break

            for thing in vars(module).values():
                if hasattr(thing, "im_test"):
                    try:
                        test = thing(self._dut)
                        skip = test.skip
                    except TestError:
                        skip = True
                        self.log.warning("Failed to initialise test %s" %
                                         thing.name)

                    if skip:
                        self.log.info("Skipping test %s" % thing.name)
                        self.xunit.add_testcase(name=thing.name,
                                                classname=module_name,
                                                time="0.0",
                                                sim_time_ns="0.0",
                                                ratio_time="0.0")
                        self.xunit.add_skipped()
                        self.skipped += 1
                        self._store_test_result(module_name, thing.name, None, 0.0, 0.0, 0.0)
                    else:
                        self._queue.append(test)
                        self.ntests += 1

        self._queue.sort(key=lambda test: "%s.%s" %
                         (test.module, test.funcname))

        for valid_tests in self._queue:
            self.log.info("Found test %s.%s" %
                          (valid_tests.module,
                           valid_tests.funcname))

        for module_name in self._hooks:
            self.log.info("Loading hook from module '"+module_name+"'")
            module = _my_import(module_name)

            for thing in vars(module).values():
                if hasattr(thing, "im_hook"):
                    try:
                        test = thing(self._dut)
                    except TestError:
                        self.log.warning("Failed to initialize hook %s" % thing.name)
                    else:
                        cocotb.scheduler.add(test)


    def tear_down(self):
        """It's the end of the world as we know it"""
        if self.failures:
            self.log.error("Failed %d out of %d tests (%d skipped)" %
                           (self.failures, self.count - 1, self.skipped))
        else:
            self.log.info("Passed %d tests (%d skipped)" %
                          (self.count - 1, self.skipped))
        if self._cov:
            self._cov.stop()
            self.log.info("Writing coverage data")
            self._cov.save()
            self._cov.html_report()
        self._log_test_summary()
        self._log_sim_summary()
        self.log.info("Shutting down...")
        self.xunit.write()
        simulator.stop_simulator()

    def next_test(self):
        """Get the next test to run"""
        if not self._queue:
            return None
        return self._queue.pop(0)

    def _add_failure(self, result):
        self.xunit.add_failure(stdout=repr(str(result)),
                               stderr="\n".join(self._running_test.error_messages),
                               message="Test failed with random_seed={}".format(self._seed))
        self.failures += 1

    def handle_result(self, result):
        """Handle a test result

        Dumps result to XML and schedules the next test (if any)

        Args: result (TestComplete exception)
        """
        real_time   = time.time() - self._running_test.start_time
        sim_time_ns = get_sim_time('ns') - self._running_test.start_sim_time
        ratio_time  = sim_time_ns / real_time
        self.xunit.add_testcase(name=self._running_test.funcname,
                                classname=self._running_test.module,
                                time=repr(real_time),
                                sim_time_ns=repr(sim_time_ns),
                                ratio_time=repr(ratio_time))

        running_test_funcname = self._running_test.funcname

        # Helper for logging result
        def _result_was():
            result_was = ("%s (result was %s)" %
                          (running_test_funcname, result.__class__.__name__))
            return result_was

        result_pass = True

        if (isinstance(result, TestSuccess) and
                not self._running_test.expect_fail and
                not self._running_test.expect_error):
            self.log.info("Test Passed: %s" % running_test_funcname)

        elif (isinstance(result, TestFailure) and
                self._running_test.expect_fail):
            self.log.info("Test failed as expected: " + _result_was())

        elif (isinstance(result, TestSuccess) and
              self._running_test.expect_error):
            self.log.error("Test passed but we expected an error: " +
                           _result_was())
            self._add_failure(result)
            result_pass = False

        elif isinstance(result, TestSuccess):
            self.log.error("Test passed but we expected a failure: " +
                           _result_was())
            self._add_failure(result)
            result_pass = False

        elif isinstance(result, TestError) and self._running_test.expect_error:
            self.log.info("Test errored as expected: " + _result_was())

        elif isinstance(result, SimFailure):
            if self._running_test.expect_error:
                self.log.info("Test errored as expected: " + _result_was())
            else:
                self.log.error("Test error has lead to simulator shuttting us "
                               "down")
                self._add_failure(result)
                self._store_test_result(self._running_test.module, self._running_test.funcname, False, sim_time_ns, real_time, ratio_time)
                self.tear_down()
                return

        else:
            self.log.error("Test Failed: " + _result_was())
            self._add_failure(result)
            result_pass = False

        self._store_test_result(self._running_test.module, self._running_test.funcname, result_pass, sim_time_ns, real_time, ratio_time)

        self.execute()

    def execute(self):
        self._running_test = cocotb.regression.next_test()
        if self._running_test:
            start = ''
            end   = ''
            if self.log.colour:
                start = ANSI.BLUE_BG + ANSI.BLACK_FG
                end   = ANSI.DEFAULT
            # Want this to stand out a little bit
            self.log.info("%sRunning test %d/%d:%s %s" %
                          (start,
                           self.count, self.ntests,
                           end,
                           self._running_test.funcname))
            if self.count is 1:
                test = cocotb.scheduler.add(self._running_test)
            else:
                test = cocotb.scheduler.new_test(self._running_test)
            self.count += 1
        else:
            self.tear_down()

    def _log_test_summary(self):
        TEST_FIELD   = 'TEST'
        RESULT_FIELD = 'PASS/FAIL'
        SIM_FIELD    = 'SIM TIME(NS)'
        REAL_FIELD   = 'REAL TIME(S)'
        RATIO_FIELD  = 'RATIO(NS/S)'

        TEST_FIELD_LEN   = max(len(TEST_FIELD),len(max([x['test'] for x in self.test_results],key=len)))
        RESULT_FIELD_LEN = len(RESULT_FIELD)
        SIM_FIELD_LEN    = len(SIM_FIELD)
        REAL_FIELD_LEN   = len(REAL_FIELD)
        RATIO_FIELD_LEN  = len(RATIO_FIELD)

        LINE_LEN = 3 + TEST_FIELD_LEN + 2 + RESULT_FIELD_LEN + 2 + SIM_FIELD_LEN + 2 + REAL_FIELD_LEN + 2 + RATIO_FIELD_LEN + 3

        LINE_SEP = "*"*LINE_LEN+"\n"

        summary = ""
        summary += LINE_SEP
        summary += "** {a:<{a_len}}  {b:^{b_len}}  {c:>{c_len}}  {d:>{d_len}}  {e:>{e_len}} **\n".format(a=TEST_FIELD,   a_len=TEST_FIELD_LEN,
                                                                                                         b=RESULT_FIELD, b_len=RESULT_FIELD_LEN,
                                                                                                         c=SIM_FIELD,    c_len=SIM_FIELD_LEN,
                                                                                                         d=REAL_FIELD,   d_len=REAL_FIELD_LEN,
                                                                                                         e=RATIO_FIELD,  e_len=RATIO_FIELD_LEN)
        summary += LINE_SEP
        for result in self.test_results:
            hilite = ''

            if result['pass'] is None:
                pass_fail_str = "N/A"
            elif result['pass']:
                pass_fail_str = "PASS"
            else:
                pass_fail_str = "FAIL"
                if self.log.colour:
                    hilite = ANSI.WHITE_FG + ANSI.RED_BG

            summary += "{start}** {a:<{a_len}}  {b:^{b_len}}  {c:>{c_len}.2f}   {d:>{d_len}.2f}   {e:>{e_len}.2f}  **\n".format(a=result['test'],   a_len=TEST_FIELD_LEN,
                                                                                                                                b=pass_fail_str,    b_len=RESULT_FIELD_LEN,
                                                                                                                                c=result['sim'],    c_len=SIM_FIELD_LEN-1,
                                                                                                                                d=result['real'],   d_len=REAL_FIELD_LEN-1,
                                                                                                                                e=result['ratio'],  e_len=RATIO_FIELD_LEN-1,
                                                                                                                                start=hilite)
        summary += LINE_SEP

        self.log.info(summary)

    def _log_sim_summary(self):
        real_time   = time.time() - self.start_time
        sim_time_ns = get_sim_time('ns')
        ratio_time  = sim_time_ns / real_time

        summary = ""

        summary += "*************************************************************************************\n"
        summary += "**                                 ERRORS : {0:<39}**\n".format(self.failures)
        summary += "*************************************************************************************\n"
        summary += "**                               SIM TIME : {0:<39}**\n".format('{0:.2f} NS'.format(sim_time_ns))
        summary += "**                              REAL TIME : {0:<39}**\n".format('{0:.2f} S'.format(real_time))
        summary += "**                        SIM / REAL TIME : {0:<39}**\n".format('{0:.2f} NS/S'.format(ratio_time))
        summary += "*************************************************************************************\n"

        self.log.info(summary)

    def _store_test_result(self, module_name, test_name, result_pass, sim_time, real_time, ratio):
        result = {
            'test'  : '.'.join([module_name, test_name]),
            'pass'  : result_pass,
            'sim'   : sim_time,
            'real'  : real_time,
            'ratio' : ratio}
        self.test_results.append(result)
示例#19
0
class RegressionManager(object):
    """Encapsulates all regression capability into a single place"""
    def __init__(self, root_name, modules, tests=None, seed=None, hooks=[]):
        """
        Args:
            root_name (str): The name of the root handle.
            modules (list): A list of Python module names to run.
            tests (list, optional): A list of tests to run.
                Defaults to ``None``, meaning all discovered tests will be run.
            seed (int,  optional): The seed for the random number generator to use.
                Defaults to ``None``.
            hooks (list, optional): A list of hook modules to import.
                Defaults to the empty list.
        """
        self._queue = []
        self._root_name = root_name
        self._dut = None
        self._modules = modules
        self._functions = tests
        self._running_test = None
        self._cov = None
        self.log = SimLog("cocotb.regression")
        self._seed = seed
        self._hooks = hooks

    def initialise(self):

        self.start_time = time.time()
        self.test_results = []
        self.ntests = 0
        self.count = 1
        self.skipped = 0
        self.failures = 0
        self.xunit = XUnitReporter()

        suite_name = os.getenv('RESULT_TESTSUITE') if os.getenv(
            'RESULT_TESTSUITE') else "all"
        package_name = os.getenv('RESULT_TESTPACKAGE') if os.getenv(
            'RESULT_TESTPACKAGE') else "all"

        self.xunit.add_testsuite(name=suite_name,
                                 tests=repr(self.ntests),
                                 package=package_name)

        if (self._seed is not None):
            self.xunit.add_property(name="random_seed",
                                    value=("%d" % self._seed))

        if coverage is not None:
            self.log.info("Enabling coverage collection of Python code")
            self._cov = coverage.coverage(branch=True, omit=["*cocotb*"])
            self._cov.start()

        handle = simulator.get_root_handle(self._root_name)

        self._dut = cocotb.handle.SimHandle(handle) if handle else None

        if self._dut is None:
            raise AttributeError("Can not find Root Handle (%s)" %
                                 self._root_name)

        # Auto discovery
        for module_name in self._modules:
            try:
                self.log.debug("Python Path: " + ",".join(sys.path))
                self.log.debug("PWD: " + os.getcwd())
                module = _my_import(module_name)
            except Exception as E:
                self.log.critical("Failed to import module %s: %s",
                                  module_name, E)
                self.log.info("MODULE variable was \"%s\"",
                              ".".join(self._modules))
                self.log.info("Traceback: ")
                self.log.info(traceback.format_exc())
                raise

            if self._functions:

                # Specific functions specified, don't auto discover
                for test in self._functions.rsplit(','):
                    if not hasattr(module, test):
                        self.log.error(
                            "Requested test %s wasn't found in module %s",
                            test, module_name)
                        raise AttributeError("Test %s doesn't exist in %s" %
                                             (test, module_name))
                    _test = getattr(module, test)
                    if not hasattr(_test, "im_test"):
                        self.log.error(
                            "Requested %s from module %s isn't a cocotb.test decorated coroutine",
                            test, module_name)
                        raise ImportError("Failed to find requested test %s" %
                                          test)
                    self._queue.append(getattr(module, test)(self._dut))
                    self.ntests += 1
                break

            for thing in vars(module).values():
                if hasattr(thing, "im_test"):
                    try:
                        test = thing(self._dut)
                        skip = test.skip
                    except TestError:
                        skip = True
                        self.log.warning("Failed to initialize test %s" %
                                         thing.name)

                    if skip:
                        self.log.info("Skipping test %s" % thing.name)
                        self.xunit.add_testcase(name=thing.name,
                                                classname=module_name,
                                                time="0.0",
                                                sim_time_ns="0.0",
                                                ratio_time="0.0")
                        self.xunit.add_skipped()
                        self.skipped += 1
                        self._store_test_result(module_name, thing.name, None,
                                                0.0, 0.0, 0.0)
                    else:
                        self._queue.append(test)
                        self.ntests += 1

        self._queue.sort(key=lambda test: test.sort_name())

        for valid_tests in self._queue:
            self.log.info("Found test %s.%s" %
                          (valid_tests.module, valid_tests.funcname))

        for module_name in self._hooks:
            self.log.info("Loading hook from module '" + module_name + "'")
            module = _my_import(module_name)

            for thing in vars(module).values():
                if hasattr(thing, "im_hook"):
                    try:
                        test = thing(self._dut)
                    except TestError:
                        self.log.warning("Failed to initialize hook %s" %
                                         thing.name)
                    else:
                        cocotb.scheduler.add(test)

    def tear_down(self):
        """It's the end of the world as we know it"""
        if self.failures:
            self.log.error("Failed %d out of %d tests (%d skipped)" %
                           (self.failures, self.count - 1, self.skipped))
        else:
            self.log.info("Passed %d tests (%d skipped)" %
                          (self.count - 1, self.skipped))
        if self._cov:
            self._cov.stop()
            self.log.info("Writing coverage data")
            self._cov.save()
            self._cov.html_report()
        if len(self.test_results) > 0:
            self._log_test_summary()
        self._log_sim_summary()
        self.log.info("Shutting down...")
        self.xunit.write()
        simulator.stop_simulator()

    def next_test(self):
        """Get the next test to run"""
        if not self._queue:
            return None
        return self._queue.pop(0)

    def _add_failure(self, result):
        self.xunit.add_failure(
            stdout=repr(str(result)),
            stderr="\n".join(self._running_test.error_messages),
            message="Test failed with random_seed={}".format(self._seed))
        self.failures += 1

    def handle_result(self, result):
        """Handle a test result.

        Dump result to XML and schedule the next test (if any).

        Args:
            result: The sub-exception of TestComplete to raise.
        """
        real_time = time.time() - self._running_test.start_time
        sim_time_ns = get_sim_time('ns') - self._running_test.start_sim_time
        ratio_time = sim_time_ns / real_time
        self.xunit.add_testcase(name=self._running_test.funcname,
                                classname=self._running_test.module,
                                time=repr(real_time),
                                sim_time_ns=repr(sim_time_ns),
                                ratio_time=repr(ratio_time))

        running_test_funcname = self._running_test.funcname

        # Helper for logging result
        def _result_was():
            result_was = ("%s (result was %s)" %
                          (running_test_funcname, result.__class__.__name__))
            return result_was

        result_pass = True

        if (isinstance(result, TestSuccess)
                and not self._running_test.expect_fail
                and not self._running_test.expect_error):
            self.log.info("Test Passed: %s" % running_test_funcname)

        elif (isinstance(result, TestFailure)
              and self._running_test.expect_fail):
            self.log.info("Test failed as expected: " + _result_was())

        elif (isinstance(result, TestSuccess)
              and self._running_test.expect_error):
            self.log.error("Test passed but we expected an error: " +
                           _result_was())
            self._add_failure(result)
            result_pass = False

        elif isinstance(result, TestSuccess):
            self.log.error("Test passed but we expected a failure: " +
                           _result_was())
            self._add_failure(result)
            result_pass = False

        elif isinstance(result, TestError) and self._running_test.expect_error:
            self.log.info("Test errored as expected: " + _result_was())

        elif isinstance(result, SimFailure):
            if self._running_test.expect_error:
                self.log.info("Test errored as expected: " + _result_was())
            else:
                self.log.error("Test error has lead to simulator shutting us "
                               "down")
                self._add_failure(result)
                self._store_test_result(self._running_test.module,
                                        self._running_test.funcname, False,
                                        sim_time_ns, real_time, ratio_time)
                self.tear_down()
                return

        else:
            self.log.error("Test Failed: " + _result_was())
            self._add_failure(result)
            result_pass = False

        self._store_test_result(self._running_test.module,
                                self._running_test.funcname, result_pass,
                                sim_time_ns, real_time, ratio_time)

        self.execute()

    def execute(self):
        self._running_test = cocotb.regression.next_test()
        if self._running_test:
            start = ''
            end = ''
            if self.log.colour:
                start = ANSI.COLOR_TEST
                end = ANSI.COLOR_DEFAULT
            # Want this to stand out a little bit
            self.log.info("%sRunning test %d/%d:%s %s" %
                          (start, self.count, self.ntests, end,
                           self._running_test.funcname))
            if self.count is 1:
                test = cocotb.scheduler.add(self._running_test)
            else:
                test = cocotb.scheduler.new_test(self._running_test)
            self.count += 1
        else:
            self.tear_down()

    def _log_test_summary(self):
        TEST_FIELD = 'TEST'
        RESULT_FIELD = 'PASS/FAIL'
        SIM_FIELD = 'SIM TIME(NS)'
        REAL_FIELD = 'REAL TIME(S)'
        RATIO_FIELD = 'RATIO(NS/S)'

        TEST_FIELD_LEN = max(
            len(TEST_FIELD),
            len(max([x['test'] for x in self.test_results], key=len)))
        RESULT_FIELD_LEN = len(RESULT_FIELD)
        SIM_FIELD_LEN = len(SIM_FIELD)
        REAL_FIELD_LEN = len(REAL_FIELD)
        RATIO_FIELD_LEN = len(RATIO_FIELD)

        LINE_LEN = 3 + TEST_FIELD_LEN + 2 + RESULT_FIELD_LEN + 2 + SIM_FIELD_LEN + 2 + REAL_FIELD_LEN + 2 + RATIO_FIELD_LEN + 3

        LINE_SEP = "*" * LINE_LEN + "\n"

        summary = ""
        summary += LINE_SEP
        summary += "** {a:<{a_len}}  {b:^{b_len}}  {c:>{c_len}}  {d:>{d_len}}  {e:>{e_len}} **\n".format(
            a=TEST_FIELD,
            a_len=TEST_FIELD_LEN,
            b=RESULT_FIELD,
            b_len=RESULT_FIELD_LEN,
            c=SIM_FIELD,
            c_len=SIM_FIELD_LEN,
            d=REAL_FIELD,
            d_len=REAL_FIELD_LEN,
            e=RATIO_FIELD,
            e_len=RATIO_FIELD_LEN)
        summary += LINE_SEP
        for result in self.test_results:
            hilite = ''

            if result['pass'] is None:
                pass_fail_str = "N/A"
            elif result['pass']:
                pass_fail_str = "PASS"
            else:
                pass_fail_str = "FAIL"
                if self.log.colour:
                    hilite = ANSI.COLOR_HILITE_SUMMARY

            summary += "{start}** {a:<{a_len}}  {b:^{b_len}}  {c:>{c_len}.2f}   {d:>{d_len}.2f}   {e:>{e_len}.2f}  **\n".format(
                a=result['test'],
                a_len=TEST_FIELD_LEN,
                b=pass_fail_str,
                b_len=RESULT_FIELD_LEN,
                c=result['sim'],
                c_len=SIM_FIELD_LEN - 1,
                d=result['real'],
                d_len=REAL_FIELD_LEN - 1,
                e=result['ratio'],
                e_len=RATIO_FIELD_LEN - 1,
                start=hilite)
        summary += LINE_SEP

        self.log.info(summary)

    def _log_sim_summary(self):
        real_time = time.time() - self.start_time
        sim_time_ns = get_sim_time('ns')
        ratio_time = sim_time_ns / real_time

        summary = ""

        summary += "*************************************************************************************\n"
        summary += "**                                 ERRORS : {0:<39}**\n".format(
            self.failures)
        summary += "*************************************************************************************\n"
        summary += "**                               SIM TIME : {0:<39}**\n".format(
            '{0:.2f} NS'.format(sim_time_ns))
        summary += "**                              REAL TIME : {0:<39}**\n".format(
            '{0:.2f} S'.format(real_time))
        summary += "**                        SIM / REAL TIME : {0:<39}**\n".format(
            '{0:.2f} NS/S'.format(ratio_time))
        summary += "*************************************************************************************\n"

        self.log.info(summary)

    def _store_test_result(self, module_name, test_name, result_pass, sim_time,
                           real_time, ratio):
        result = {
            'test': '.'.join([module_name, test_name]),
            'pass': result_pass,
            'sim': sim_time,
            'real': real_time,
            'ratio': ratio
        }
        self.test_results.append(result)
示例#20
0
文件: scheduler.py 项目: TC01/cocotb
class Scheduler(object):
    """The main scheduler.

    Here we accept callbacks from the simulator and schedule the appropriate
    coroutines.

    A callback fires, causing the :any:`react` method to be called, with the
    trigger that caused the callback as the first argument.

    We look up a list of coroutines to schedule (indexed by the trigger) and
    schedule them in turn. NB implementors should not depend on the scheduling
    order!

    Some additional management is required since coroutines can return a list
    of triggers, to be scheduled when any one of the triggers fires.  To
    ensure we don't receive spurious callbacks, we have to un-prime all the
    other triggers when any one fires.

    Due to the simulator nuances and fun with delta delays we have the
    following modes:

    Normal mode
        - Callbacks cause coroutines to be scheduled
        - Any pending writes are cached and do not happen immediately

    ReadOnly mode
        - Corresponds to cbReadOnlySynch (VPI) or vhpiCbLastKnownDeltaCycle
          (VHPI).  In this state we are not allowed to perform writes.

    Write mode
        - Corresponds to cbReadWriteSynch (VPI) or vhpiCbEndOfProcesses (VHPI)
          In this mode we play back all the cached write updates.

    We can legally transition from normal->write by registering a ReadWrite
    callback, however usually once a simulator has entered the ReadOnly phase
    of a given timestep then we must move to a new timestep before performing
    any writes.  The mechanism for moving to a new timestep may not be
    consistent across simulators and therefore we provide an abstraction to
    assist with compatibility.


    Unless a coroutine has explicitly requested to be scheduled in ReadOnly
    mode (for example wanting to sample the finally settled value after all
    delta delays) then it can reasonably be expected to be scheduled during
    "normal mode" i.e. where writes are permitted.
    """

    _MODE_NORMAL   = 1  # noqa
    _MODE_READONLY = 2  # noqa
    _MODE_WRITE    = 3  # noqa
    _MODE_TERM     = 4  # noqa

    # Singleton events, recycled to avoid spurious object creation
    _readonly = ReadOnly()
    # TODO[gh-759]: For some reason, the scheduler requires that these triggers
    # are _not_ the same instances used by the tests themselves. This is risky,
    # because it can lead to them overwriting each other's callbacks. We should
    # try to remove this `copy.copy` in future.
    _next_timestep = copy.copy(NextTimeStep())
    _readwrite = copy.copy(ReadWrite())
    _timer1 = Timer(1)
    _timer0 = Timer(0)

    def __init__(self):

        self.log = SimLog("cocotb.scheduler")
        if _debug:
            self.log.setLevel(logging.DEBUG)

        # A dictionary of pending coroutines for each trigger,
        # indexed by trigger
        self._trigger2coros = collections.defaultdict(list)

        # A dictionary of pending triggers for each coroutine, indexed by coro
        self._coro2triggers = collections.defaultdict(list)

        # Our main state
        self._mode = Scheduler._MODE_NORMAL

        # A dictionary of pending writes
        self._writes = {}

        self._pending_coros = []
        self._pending_callbacks = []
        self._pending_triggers = []
        self._pending_threads = []
        self._pending_events = []   # Events we need to call set on once we've unwound

        self._terminate = False
        self._test_result = None
        self._entrypoint = None
        self._main_thread = threading.current_thread()

        # Select the appropriate scheduling algorithm for this simulator
        self.advance = self.default_scheduling_algorithm
        self._is_reacting = False

    def default_scheduling_algorithm(self):
        """
        Decide whether we need to schedule our own triggers (if at all) in
        order to progress to the next mode.

        This algorithm has been tested against the following simulators:
            Icarus Verilog
        """
        if not self._terminate and self._writes:

            if self._mode == Scheduler._MODE_NORMAL:
                if not self._readwrite.primed:
                    self._readwrite.prime(self.react)
            elif not self._next_timestep.primed:
                self._next_timestep.prime(self.react)

        elif self._terminate:
            if _debug:
                self.log.debug("Test terminating, scheduling Timer")

            for t in self._trigger2coros:
                t.unprime()

            for t in [self._readwrite, self._readonly, self._next_timestep,
                      self._timer1, self._timer0]:
                if t.primed:
                    t.unprime()

            self._timer1.prime(self.begin_test)
            self._trigger2coros = collections.defaultdict(list)
            self._coro2triggers = collections.defaultdict(list)
            self._terminate = False
            self._mode = Scheduler._MODE_TERM

    def begin_test(self, trigger=None):
        """Called to initiate a test.

        Could be called on start-up or from a callback.
        """
        if _debug:
            self.log.debug("begin_test called with trigger: %s" %
                           (str(trigger)))
        if _profiling:
            ps = pstats.Stats(_profile).sort_stats('cumulative')
            ps.dump_stats("test_profile.pstat")
            ctx = profiling_context()
        else:
            ctx = nullcontext()

        with ctx:
            self._mode = Scheduler._MODE_NORMAL
            if trigger is not None:
                trigger.unprime()

            # Issue previous test result, if there is one
            if self._test_result is not None:
                if _debug:
                    self.log.debug("Issue test result to regression object")
                cocotb.regression_manager.handle_result(self._test_result)
                self._test_result = None
            if self._entrypoint is not None:
                test = self._entrypoint
                self._entrypoint = None
                self.schedule(test)
                self.advance()

    def react(self, trigger):
        """
        Called when a trigger fires.

        We ensure that we only start the event loop once, rather than
        letting it recurse.
        """
        if self._is_reacting:
            # queue up the trigger, the event loop will get to it
            self._pending_triggers.append(trigger)
            return

        # start the event loop
        self._is_reacting = True
        try:
            self._event_loop(trigger)
        finally:
            self._is_reacting = False


    def _event_loop(self, trigger):
        """
        Run an event loop triggered by the given trigger.

        The loop will keep running until no further triggers fire.

        This should be triggered by only:
        * The beginning of a test, when there is no trigger to react to
        * A GPI trigger
        """
        if _profiling:
            ctx = profiling_context()
        else:
            ctx = nullcontext()

        with ctx:
            # When a trigger fires it is unprimed internally
            if _debug:
                self.log.debug("Trigger fired: %s" % str(trigger))
            # trigger.unprime()

            if self._mode == Scheduler._MODE_TERM:
                if _debug:
                    self.log.debug("Ignoring trigger %s since we're terminating" %
                                   str(trigger))
                return

            if trigger is self._readonly:
                self._mode = Scheduler._MODE_READONLY
            # Only GPI triggers affect the simulator scheduling mode
            elif isinstance(trigger, GPITrigger):
                self._mode = Scheduler._MODE_NORMAL

            # We're the only source of ReadWrite triggers which are only used for
            # playing back any cached signal updates
            if trigger is self._readwrite:

                if _debug:
                    self.log.debug("Writing cached signal updates")

                while self._writes:
                    handle, value = self._writes.popitem()
                    handle.setimmediatevalue(value)

                self._readwrite.unprime()

                return

            # Similarly if we've scheduled our next_timestep on way to readwrite
            if trigger is self._next_timestep:

                if not self._writes:
                    self.log.error(
                        "Moved to next timestep without any pending writes!")
                else:
                    self.log.debug(
                        "Priming ReadWrite trigger so we can playback writes")
                    self._readwrite.prime(self.react)

                return

            # work through triggers one by one
            is_first = True
            self._pending_triggers.append(trigger)
            while self._pending_triggers:
                trigger = self._pending_triggers.pop(0)

                if not is_first and isinstance(trigger, GPITrigger):
                    self.log.warning(
                        "A GPI trigger occurred after entering react - this "
                        "should not happen."
                    )
                    assert False

                # this only exists to enable the warning above
                is_first = False

                if trigger not in self._trigger2coros:

                    # GPI triggers should only be ever pending if there is an
                    # associated coroutine waiting on that trigger, otherwise it would
                    # have been unprimed already
                    if isinstance(trigger, GPITrigger):
                        self.log.critical(
                            "No coroutines waiting on trigger that fired: %s" %
                            str(trigger))

                        trigger.log.info("I'm the culprit")
                    # For Python triggers this isn't actually an error - we might do
                    # event.set() without knowing whether any coroutines are actually
                    # waiting on this event, for example
                    elif _debug:
                        self.log.debug(
                            "No coroutines waiting on trigger that fired: %s" %
                            str(trigger))

                    continue

                # Scheduled coroutines may append to our waiting list so the first
                # thing to do is pop all entries waiting on this trigger.
                scheduling = self._trigger2coros.pop(trigger)

                if _debug:
                    debugstr = "\n\t".join([coro.__name__ for coro in scheduling])
                    if len(scheduling):
                        debugstr = "\n\t" + debugstr
                    self.log.debug("%d pending coroutines for event %s%s" %
                                   (len(scheduling), str(trigger), debugstr))

                # This trigger isn't needed any more
                trigger.unprime()

                # If the coroutine was waiting on multiple triggers we may be able
                # to unprime the other triggers that didn't fire
                scheduling_set = set(scheduling)
                other_triggers = {
                    t
                    for coro in scheduling
                    for t in self._coro2triggers[coro]
                } - {trigger}

                for pending in other_triggers:
                    # every coroutine waiting on this trigger is already being woken
                    if scheduling_set.issuperset(self._trigger2coros[pending]):
                        if pending.primed:
                            pending.unprime()
                        del self._trigger2coros[pending]

                for coro in scheduling:
                    if _debug:
                        self.log.debug("Scheduling coroutine %s" % (coro.__name__))
                    self.schedule(coro, trigger=trigger)
                    if _debug:
                        self.log.debug("Scheduled coroutine %s" % (coro.__name__))

                # Schedule may have queued up some events so we'll burn through those
                while self._pending_events:
                    if _debug:
                        self.log.debug("Scheduling pending event %s" %
                                       (str(self._pending_events[0])))
                    self._pending_events.pop(0).set()

            # no more pending triggers
            self.advance()
            if _debug:
                self.log.debug("All coroutines scheduled, handing control back"
                               " to simulator")


    def unschedule(self, coro):
        """Unschedule a coroutine.  Unprime any pending triggers"""

        for trigger in self._coro2triggers[coro]:
            if coro in self._trigger2coros[trigger]:
                self._trigger2coros[trigger].remove(coro)
            if not self._trigger2coros[trigger]:
                trigger.unprime()
                del self._trigger2coros[trigger]
        del self._coro2triggers[coro]

        if Join(coro) in self._trigger2coros:
            self._pending_triggers.append(Join(coro))
        else:
            try:
                # throws an error if the background coroutine errored
                # and no one was monitoring it
                coro.retval
            except Exception as e:
                self._test_result = TestError(
                    "Forked coroutine {} raised exception {}"
                    .format(coro, e)
                )
                self._terminate = True

    def save_write(self, handle, value):
        if self._mode == Scheduler._MODE_READONLY:
            raise Exception("Write to object {0} was scheduled during a read-only sync phase.".format(handle._name))
        self._writes[handle] = value

    def _coroutine_yielded(self, coro, triggers):
        """Prime the triggers and update our internal mappings."""
        self._coro2triggers[coro] = triggers

        for trigger in triggers:

            self._trigger2coros[trigger].append(coro)
            if not trigger.primed:
                try:
                    trigger.prime(self.react)
                except Exception as e:
                    # Convert any exceptions into a test result
                    self.finish_test(
                        create_error(self, "Unable to prime trigger %s: %s" %
                                     (str(trigger), str(e))))

    def queue(self, coroutine):
        """Queue a coroutine for execution"""
        self._pending_coros.append(coroutine)

    def queue_function(self, coroutine):
        """Queue a coroutine for execution and move the containing thread
        so that it does not block execution of the main thread any longer.
        """

        # We should be able to find ourselves inside the _pending_threads list

        for t in self._pending_threads:
            if t.thread == threading.current_thread():
                t.thread_suspend()
                self._pending_coros.append(coroutine)
                return t


    def run_in_executor(self, func, *args, **kwargs):
        """Run the coroutine in a separate execution thread
        and return a yieldable object for the caller.
        """
        # Create a thread
        # Create a trigger that is called as a result of the thread finishing
        # Create an Event object that the caller can yield on
        # Event object set when the thread finishes execution, this blocks the
        #   calling coroutine (but not the thread) until the external completes

        def execute_external(func, _waiter):
            _waiter._outcome = outcomes.capture(func, *args, **kwargs)
            if _debug:
                self.log.debug("Execution of external routine done %s" % threading.current_thread())
            _waiter.thread_done()

        waiter = external_waiter()
        thread = threading.Thread(group=None, target=execute_external,
                                  name=func.__name__ + "_thread",
                                  args=([func, waiter]), kwargs={})

        waiter.thread = thread;
        self._pending_threads.append(waiter)

        return waiter

    def add(self, coroutine):
        """Add a new coroutine.

        Just a wrapper around self.schedule which provides some debug and
        useful error messages in the event of common gotchas.
        """
        if isinstance(coroutine, cocotb.decorators.coroutine):
            self.log.critical(
                "Attempt to schedule a coroutine that hasn't started")
            coroutine.log.error("This is the failing coroutine")
            self.log.warning(
                "Did you forget to add parentheses to the @test decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        elif not isinstance(coroutine, cocotb.decorators.RunningCoroutine):
            self.log.critical(
                "Attempt to add something to the scheduler which isn't a "
                "coroutine")
            self.log.warning(
                "Got: %s (%s)" % (str(type(coroutine)), repr(coroutine)))
            self.log.warning("Did you use the @coroutine decorator?")
            self._test_result = TestError(
                "Attempt to schedule a coroutine that hasn't started")
            self._terminate = True
            return

        if _debug:
            self.log.debug("Adding new coroutine %s" % coroutine.__name__)

        self.schedule(coroutine)
        self.advance()
        return coroutine

    def new_test(self, coroutine):
        self._entrypoint = coroutine

    def schedule(self, coroutine, trigger=None):
        """Schedule a coroutine by calling the send method.

        Args:
            coroutine (cocotb.decorators.coroutine): The coroutine to schedule.
            trigger (cocotb.triggers.Trigger): The trigger that caused this
                coroutine to be scheduled.
        """
        if trigger is None:
            send_outcome = outcomes.Value(None)
        else:
            send_outcome = trigger._outcome
        if _debug:
            self.log.debug("Scheduling with {}".format(send_outcome))

        try:
            result = coroutine._advance(send_outcome)
            if _debug:
                self.log.debug("Coroutine %s yielded %s (mode %d)" %
                               (coroutine.__name__, str(result), self._mode))

        # TestComplete indication is game over, tidy up
        except TestComplete as test_result:
            # Tag that close down is needed, save the test_result
            # for later use in cleanup handler
            self.log.debug("TestComplete received: %s" % test_result.__class__.__name__)
            self.finish_test(test_result)
            return

        # Normal coroutine completion
        except cocotb.decorators.CoroutineComplete as exc:
            if _debug:
                self.log.debug("Coroutine completed: %s" % str(coroutine))
            self.unschedule(coroutine)
            return

        # Don't handle the result if we're shutting down
        if self._terminate:
            return

        # Queue current routine to schedule when the nested routine exits
        yield_successful = False
        if isinstance(result, cocotb.decorators.RunningCoroutine):

            if not result.has_started():
                self.queue(result)
                if _debug:
                    self.log.debug("Scheduling nested coroutine: %s" %
                                   result.__name__)
            else:
                if _debug:
                    self.log.debug("Joining to already running coroutine: %s" %
                                   result.__name__)

            new_trigger = result.join()
            self._coroutine_yielded(coroutine, [new_trigger])
            yield_successful = True

        elif isinstance(result, Trigger):
            if _debug:
                self.log.debug("%s: is instance of Trigger" % result)
            self._coroutine_yielded(coroutine, [result])
            yield_successful = True

        # If we get a list, make sure it's a list of triggers or coroutines.
        # For every coroutine, replace it with coroutine.join().
        # This could probably be done more elegantly via list comprehension.
        elif isinstance(result, list):
            new_triggers = []
            for listobj in result:
                if isinstance(listobj, Trigger):
                    new_triggers.append(listobj)
                elif isinstance(listobj, cocotb.decorators.RunningCoroutine):
                    if _debug:
                        self.log.debug("Scheduling coroutine in list: %s" %
                                       listobj.__name__)
                    if not listobj.has_started():
                        self.queue(listobj)
                    new_trigger = listobj.join()
                    new_triggers.append(new_trigger)
                else:
                    # If we encounter something not a coroutine or trigger,
                    # set the success flag to False and break out of the loop.
                    yield_successful = False
                    break

            # Make sure the lists are the same size. If they are not, it means
            # it contained something not a trigger/coroutine, so do nothing.
            if len(new_triggers) == len(result):
                self._coroutine_yielded(coroutine, new_triggers)
                yield_successful = True

        # If we didn't successfully yield anything, thrown an error.
        # Do it this way to make the logic in the list case simpler.
        if not yield_successful:
            msg = ("Coroutine %s yielded something the scheduler can't handle"
                   % str(coroutine))
            msg += ("\nGot type: %s repr: %s str: %s" %
                    (type(result), repr(result), str(result)))
            msg += "\nDid you forget to decorate with @cocotb.coroutine?"
            try:
                raise_error(self, msg)
            except Exception as e:
                self.finish_test(e)

        # We do not return from here until pending threads have completed, but only
        # from the main thread, this seems like it could be problematic in cases
        # where a sim might change what this thread is.
        def unblock_event(ext):
            @cocotb.coroutine
            def wrapper():
                ext.event.set()
                yield PythonTrigger()

        if self._main_thread is threading.current_thread():

            for ext in self._pending_threads:
                ext.thread_start()
                if _debug:
                    self.log.debug("Blocking from %s on %s" % (threading.current_thread(), ext.thread))
                state = ext.thread_wait()
                if _debug:
                    self.log.debug("Back from wait on self %s with newstate %d" % (threading.current_thread(), state))
                if state == external_state.EXITED:
                    self._pending_threads.remove(ext)
                    self._pending_events.append(ext.event)

        # Handle any newly queued coroutines that need to be scheduled
        while self._pending_coros:
            self.add(self._pending_coros.pop(0))

        while self._pending_callbacks:
            self._pending_callbacks.pop(0)()


    def finish_test(self, test_result):
        """Cache the test result and set the terminate flag."""
        self.log.debug("finish_test called with %s" % (repr(test_result)))
        if not self._terminate:
            self._terminate = True
            self._test_result = test_result
            self.cleanup()

    def finish_scheduler(self, test_result):
        """Directly call into the regression manager and end test
           once we return the sim will close us so no cleanup is needed.
        """
        self.log.debug("Issue sim closedown result to regression object")
        cocotb.regression_manager.handle_result(test_result)

    def cleanup(self):
        """Clear up all our state.

        Unprime all pending triggers and kill off any coroutines stop all externals.
        """
        for trigger, waiting in dict(self._trigger2coros).items():
            for coro in waiting:
                if _debug:
                    self.log.debug("Killing %s" % str(coro))
                coro.kill()

        if self._main_thread is not threading.current_thread():
            raise Exception("Cleanup() called outside of the main thread")

        for ext in self._pending_threads:
            self.log.warn("Waiting for %s to exit", ext.thread)
示例#21
0
class RunningCoroutine(object):
    """Per instance wrapper around an function to turn it into a coroutine


        Provides the following:

            coro.join() creates a Trigger that will fire when this coroutine completes

            coro.kill() will destroy a coroutine instance (and cause any Join triggers to fire
    """
    def __init__(self, inst, parent):
        self.__name__ = "%s" % inst.__name__
        self.log = SimLog("cocotb.coroutine.%s" % self.__name__, id(self))
        self._coro = inst
        self._finished = False
        self._callbacks = []
        self._parent = parent
        self.__doc__ = parent._func.__doc__
        self.module = parent._func.__module__
        self.funcname = parent._func.__name__
        self.retval = None

        if not hasattr(self._coro, "send"):
            self.log.error("%s isn't a value coroutine! Did you use the yield keyword?"
                % self.__name__)
            raise CoroutineComplete(callback=self._finished_cb)

    def __iter__(self):
        return self

    def __str__(self):
        return str(self.__name__)

    def send(self, value):
        try:
            return self._coro.send(value)
        except TestComplete as e:
            if isinstance(e, TestFailure):
                self.log.warning(str(e))
            else:
                self.log.info(str(e))
            raise
        except ReturnValue as e:
            self.retval = e.retval
            raise CoroutineComplete(callback=self._finished_cb)
        except StopIteration:
            raise CoroutineComplete(callback=self._finished_cb)
        except Exception as e:
            raise_error(self, "Send raised exception: %s" % (str(e)))

    def throw(self, exc):
        return self._coro.throw(exc)

    def close(self):
        return self._coro.close()

    def kill(self):
        """Kill a coroutine"""
        self.log.debug("kill() called on coroutine")
        cocotb.scheduler.schedule_remove(self, self._finished_cb)

    def _finished_cb(self):
        """Called when the coroutine completes.
            Allows us to mark the coroutine as finished so that boolean testing works.
            Also call any callbacks, usually the result of coroutine.join()"""
        self._finished = True
        self.log.debug("Coroutine finished calling pending callbacks (%d pending)" % len(self._callbacks))
        for cb in self._callbacks:
            cb()
        self._callbacks = []

    def join(self):
        """Return a trigger that will fire when the wrapped coroutine exits"""
        return Join(self)

    def __nonzero__(self):
        """Provide boolean testing
            if the coroutine has finished return false
            otherwise return true"""
        return not self._finished