argos83/marvin

View on GitHub
marvin/core/step_runner.py

Summary

Maintainability
A
55 mins
Test Coverage

import inspect
import sys

from marvin.core.status import Status
from marvin.exceptions import ContextSkippedException, ExpectedExceptionNotRaised, StepsFailedInContext
from marvin.report.events import StepStartedEvent, StepEndedEvent, StepSkippedEvent
from marvin.util import compat, NO_EXCEPTION


class StepRunner(object):
    """
    Internal Marvin Step executor. Implement the logic of a step execution flow, signals, events, etc
    """

    def __init__(self, step, args, kwargs):
        self._step = step
        self._args = list(args)  # make args mutable
        self._kwargs = kwargs
        self._execution = None
        self._start_time = None
        self._result = Result(None)
        self._step_exception = NO_EXCEPTION
        self._context_exception = NO_EXCEPTION

    @property
    def result(self):
        return self._result.get()

    def __enter__(self):
        start_event = StepStartedEvent(self._step, self._args, self._kwargs)
        self._start_time = start_event.timestamp
        self._step.publisher.notify(start_event)

        result, self._step_exception = self._do_run()
        self._result.set(result)

        is_exception = self._step_exception != NO_EXCEPTION
        expected = is_exception and self._is_exception_expected(self._step_exception, self._step.expected_exceptions)

        # Decide whether we should yield the result or fail right away
        if is_exception and self._step.safe_exec:
            # TODO: This is more a language constraint than a feature.
            # Alternatively we could make `safely` and `do` to be incompatible.
            # Or find a way to inject an exception to be raised immediately inside the context
            return self._step, self._step_exception

        elif is_exception and not expected:
            self.__exit__(*NO_EXCEPTION)

        return self._step, self.result

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._context_exception = (exc_type, exc_val, exc_tb)
        sub_context_failures = self._any_sub_steps_failed()

        if self._step_exception != NO_EXCEPTION:
            exception = self._step_exception
            status, to_raise = self._when_exception(self._step_exception)
        elif self._context_exception != NO_EXCEPTION:
            exception = self._context_exception
            status, to_raise = self._when_exception(self._context_exception)
        elif sub_context_failures != NO_EXCEPTION:
            exception = sub_context_failures
            status, to_raise = self._when_exception(sub_context_failures)
        else:
            status, to_raise = self._when_no_exception()
            exception = to_raise

        skip_exception = None
        if self._step_exception[0] == ContextSkippedException:
            skip_exception = self._step_exception

        elif self._context_exception[0] == ContextSkippedException:
            skip_exception = self._context_exception

        if skip_exception:
            status = Status.SKIP
            skip_event = StepSkippedEvent(self._step, skip_exception)
            self._step.publisher.notify(skip_event)

        if self._step.shall_pass and status == Status.FAIL:
            status = Status.PASS

        end_event = StepEndedEvent(self._step,
                                   status,
                                   self._result,
                                   self._start_time,
                                   exception)

        self._step.publisher.notify(end_event)

        # Notify the result to the parent context
        self._step.ctx.sub_context_finished(status)

        if to_raise[1] and not self._step.safe_exec:
            raise compat.raise_exc_info(*to_raise)

        return True

    def _do_run(self):
        result = None
        exception = NO_EXCEPTION
        try:
            result = self._step.run(*self._args, **self._kwargs)
        except Exception:
            exception = sys.exc_info()

        return result, exception

    def _any_sub_steps_failed(self):
        failed_subcontexts = self._step.context_summary[Status.FAIL]
        if failed_subcontexts:
            return StepsFailedInContext, StepsFailedInContext(failed_subcontexts), None
        return NO_EXCEPTION

    def _when_no_exception(self):
        if not self._step.expected_exceptions:
            return Status.PASS, NO_EXCEPTION

        to_raise = (ExpectedExceptionNotRaised, ExpectedExceptionNotRaised(self._step.expected_exceptions), None)
        return Status.FAIL, to_raise

    def _when_exception(self, exception):
        was_expected = self._is_exception_expected(exception, self._step.expected_exceptions)
        if self._step.shall_pass or was_expected:
            status = Status.PASS
        else:
            status = Status.FAIL
        if self._step.safe_exec or was_expected:
            to_raise = NO_EXCEPTION
        else:
            to_raise = exception
        return status, to_raise

    def _is_exception_expected(self, exception, expectation):
        exc_type, exc_val, exc_tb = exception
        if isinstance(expectation, list):
            return any(self._is_exception_expected(exception, expect) for expect in expectation)
        if inspect.isclass(expectation):
            return issubclass(expectation, exc_type)
        if callable(expectation):
            return expectation(exc_val)
        return False


class Result(object):
    """
    Wraps a step result so plugins can change its value even if type is immutable
    """
    def __init__(self, result):
        self._result = result

    def get(self):
        """Get the step result object"""
        return self._result

    def set(self, value):
        """Change the step result object"""
        self._result = value