robotframework: Support method of failing current keyword/test and continuing in python

I have a Python function keyword described as A context manager that runs as a Robot keyword that supports continue on error.

@keyword
def test(): # will show as passed in log
    with keyword("will fail", continue_on_error=True): # will show as failed in log and will fail the test
       BuiltIn().fail("foo")
    logger.info("bar") # will run

But the report looks janky because it doesn’t make test failed and the error needs to be raised in cleanup.

I guess it boils down to: I want a pure python implementation of “Run Keyword And Continue On Failure”

Here’s a rough implementation of keyword:

from contextlib import contextmanager

from robot.libraries.BuiltIn import BuiltIn
from robot.running.statusreporter import StatusReporter

exception_store = None


@contextmanager
def keyword(name, continue_on_error=False):
    """A context manager that runs as a Robot keyword

    continue_on_error: if an error occurs in this keyword, exit and report as failed but continue test
    """
    try:
        with StatusReporter(BuiltIn()._context, Keyword(kwname=name)):
            yield
    except Exception as err:
        if continue_on_error:
            global exception_store
            exception_store = err
            return
        raise err


def cleanup():
    if exception_store:
        raise exception_store

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 17 (17 by maintainers)

Most upvoted comments

@pekkaklarck I think I’ve found a way to get the desired outcome, does this look right to you?

from contextlib import contextmanager
from functools import wraps
from typing import Optional, Iterator

from robot.libraries.BuiltIn import BuiltIn
from robot.api import ContinuableFailure
from robot.result import Keyword as KeywordResult
from robot.running import Keyword
from robot.running.statusreporter import StatusReporter

rf = BuiltIn()


def foo() -> None:
    with keyword("outer"):
        with keyword("inner1"):
            raise ContinuableFailure("hi")
        rf.log("inner1 should fail")

        with keyword("inner2"):
            rf.log("this should be passed")
        rf.log("inner2 should pass")

        with keyword("inner3"):
            raise ContinuableFailure("hi")
        rf.log("inner3 should fail")
    rf.log("outer should fail")
    rf.log("foo should fail")
    rf.log("test should fail")


status: Optional[Exception] = None


@contextmanager
def keyword(name: str) -> Iterator[None]:
    global status
    previous_status = status
    kw = Keyword()
    kw.name = name
    try:
        with StatusReporter(kw, KeywordResult(kwname=name), rf._context):
            yield
            if not previous_status and status:
                raise status
    except Exception as err:
        status = err


def set_up():
    def wrapper(fn):
        @wraps(fn)
        def inner(*args, **kwargs):
            fn(*args, **kwargs)
            global status
            if err := status:
                status = None
                raise err

        return inner

    for l in rf._namespace.libraries:
            for h in l.handlers._normal.values():
                h._method = wrapper(h._method)
*** Settings ***
Suite Setup    set_up
Library    main

*** Test Cases ***
test1
    foo