Property Decorator

This section aims to explain how Kea’s property definition decorators are designed and implemented.

Function Description and Function Design

In KeaTest, decorators are used to define properties. The role of decorators is to modify the function itself. In Kea, user initialization, preconditions, and main path functions are all a single function. We use decorators to obtain the function body and mark this function body. Since functions in Python are first-class objects, we can dynamically set attributes on the function object after obtaining the function body through decorators. We set different MARKER attributes based on different decorators. When Kea loads the properties, we read the following data structure and convert it into a format that Kea can easily read and process using the KeaTestElements class: KeaTestElements.

../../../_images/decorators-keaTestElements.png

Transformation from User-defined KeaTest to Runtime KeaTestElements

Definition of Property

The following @rule and @precondition decorators encapsulate a user-defined property in the data structure Rule and mark this property function using RULE_MARKER.

The following is the definition of the Rule data structure. precondition is used to store a function object that calculates a precondition. function is used to store the interaction scenario of this property.

@attr.s(frozen=True)
class Rule:
    # `preconditions` denotes the preconditions annotated with `@precondition`
    preconditions:Callable = attr.ib()

    # `function` denotes the function of @Rule.
    # This function includes the interaction scenario and the assertions (i.e., the postconditions)
    function:Callable = attr.ib()

@rule decorator is used to define a property. Among them, RULE_MARKER is a constant.

Parameters:
  • f: Callable[[Any], None] : An interaction scenario function object

Returns:
  • Callable[[Any], None] : The function object of the parsed Rule after being marked by RULE_MARKER.

def rule() -> Callable:
    def accept(f):
        precondition = getattr(f, PRECONDITIONS_MARKER, ())
        rule = Rule(function=f, preconditions=precondition)

        def rule_wrapper(*args, **kwargs):
            return f(*args, **kwargs)

        setattr(rule_wrapper, RULE_MARKER, rule)
        return rule_wrapper

    return accept

@precondition specifies when a property can be executed. A property can have multiple preconditions, each specified by @precondition. Among them, PRECONDITIONS_MARKER is a constant.

Parameters:
  • precond: Callable[[Any], bool] : A function object that returns a boolean value and has been decorated by @rule.

Returns:
  • Callable[[Any], bool] : The function of the parsed precondition after being marked by RULE_MARKER.

def precondition(precond: Callable[[Any], bool]) -> Callable:
    def accept(f):
        def precondition_wrapper(*args, **kwargs):
            return f(*args, **kwargs)

        rule:"Rule" = getattr(f, RULE_MARKER, None)
        if rule is not None:
            new_rule = rule.evolve(preconditions=rule.preconditions + (precond,))
            setattr(precondition_wrapper, RULE_MARKER, new_rule)
        else:
            setattr(
                precondition_wrapper,
                PRECONDITIONS_MARKER,
                getattr(f, PRECONDITIONS_MARKER, ()) + (precond,),
            )
        return precondition_wrapper

    return accept

Definition of Initialization Function

@initializer defines an initialization function used for application initialization, such as skipping tutorial steps. The following @initializer decorator encapsulates a user-defined property in the data structure Initializer and marks this property function using INITIALIZER_MARKER.

The following is the definition of the Initializer data structure. function is used to store a function object for a series of operations to be executed during initialization.

@attr.s()
class Initializer:
    # `function` denotes the function of `@initializer.
    function:Callable = attr.ib()

@initializer decorator is used to define an initialization function, where INITIALIZER_MARKER is a constant.

Parameters:
  • f: Callable[[Any], None] : An initialization function object that defines the initialization event.

Returns:
  • Callable[[Any], None] : The initialization function object marked by INITIALIZER_MARKER.

def initializer():
    def accept(f):
        def initialize_wrapper(*args, **kwargs):
            return f(*args, **kwargs)

        initializer_func = Initializer(function=f)
        setattr(initialize_wrapper, INITIALIZER_MARKER, initializer_func)
        return initialize_wrapper

    return accept

Definition of Main Path Function

The main path specifies a series of events, executing which from the application’s starting page will lead the application to the initial state of the property (the page satisfying the preconditions). The following @mainPath decorator encapsulates a user-defined property in the data structure MainPath and marks this property function using MAINPATH_MARKER.

The following is the definition of the MainPath data structure. function is used to store the user-defined mainPath function object, and path is a list of detailed path steps obtained after processing the source code of this function, storing the source code of each step in the main path.

@attr.s()
class MainPath:

    # `function` denotes the function of `@mainPath.
    function:Callable = attr.ib()

    # the interaction steps (events) in the main path
    path: List[str] = attr.ib()

@mainPath decorator encapsulates a user-defined property in the data structure MainPath, where MAINPATH_MARKER is a constant.

Parameters:
  • f: Callable[[Any], None] : A function object that defines the main path event.

Returns:
  • Callable[[Any], None] : The initialization function object marked by MAINPATH_MARKER.

def mainPath():
    def accept(f):
        def mainpath_wrapper(*args, **kwargs):
            source_code = inspect.getsource(f)
            code_lines = [line.strip() for line in source_code.splitlines() if line.strip()]
            code_lines = [line for line in code_lines if not line.startswith('def ') and not line.startswith('@') and not line.startswith('#')]
            return code_lines

        main_path = MainPath(function=f, path=mainpath_wrapper())
        setattr(mainpath_wrapper, MAINPATH_MARKER, main_path)
        return mainpath_wrapper

    return accept