Skip to main content

Callbacks

Callbacks allow users to execute arbitrary Python code during test runtime to perform operations unavailable via the user interface. Callbacks can be extremely useful in several ways, such as:

  • Implement advanced response processing rules.
  • Incorporating arbitrary health checks into the test flow.
  • Restoring the target to a known good state before delivering the next test case.
  • Utilizing binary instrumentation frameworks (e.g., QDBI and Frida). For example, instrument client applications to connect to GUARDARA when GUARDARA simulates a server.
  • Perform other external automation during the test flow.

Creating Callbacks

You can assign Callbacks to the Callback actions within Test Flow Templates. Similarly to other assets, Callbacks are managed via the Test Assets page. To create a new Callback, click File > New Callback on the main menu of the Test Assets page.

A Callback is a Python function. The Callback skeleton that users can extend is shown below.

def callback(context):
pass

Context

The context argument of a Callback is an object that exposes several internal methods of the Engine. These are discussed below.

Runtime

MethodDescription
context.runtime.is_analysis()Returns True if the Callback is executed during the automated issue analysis or the baseline collection phases of the testing process. It returns False otherwise.
context.runtime.log(severity, message)The method allows writing messages to the test’s Activity Log. The severity supports the following values: info, warning and error. The message is expected to be a string (Python str).
context.runtime.performance()Returns a dictionary (Python dict) containing performance metrics about the test.

The performance() method returns a dictionary that looks like the one below.

{
'average_iteration_time': 0.091961,
'maximum_iteration_time': 0.203699,
'average_test_cases_per_second': 10.0,
'maximum_test_cases_per_second': 10.0,
'analytics_time': 0.18,
'current_speed': 10.0,
'remaining': 3321.6
}

The above performance statistic, for example, tells us that, in this case, we were executing an average of 10 test cases a second and, with the current speed, it may take approximately an hour for the test run to complete.

You can find an example demonstrating the use of the runtime methods exposed on GitLab.

Session

MethodDescription
context.session.get(variable_id)Obtain the raw and rendered value of a Session Variable as a dictionary (dict) based on the variable's ID.
context.session.set(variable_id, type, value)Set the value of an existing Session Variable or create a new one. The type can be either raw or rendered.

Driver

The driver methods allow sending and receiving data from the target. The following table summarizes the driver methods exposed.

MethodDescription
context.driver.connect(props)Allows connecting to the target using the configured driver.
context.driver.disconnect(props)Allows disconnecting from the target using the configured driver.
context.driver.send(data, props)Allows sending data to the target via the configured driver. The data must be Python bytes, for example: b"hello".
context.driver.recv(props)Allows receiving data from the target via the configured driver. The received data is returned as Python bytes.

The props argument is optional and allows passing configuration options to the driver method. These are the same options generated by the Flow Designer for actions.

History

The history contains information about messages previously sent to the target. In addition, the mutator instance of the previously executed and the following action is also made accessible. The following table summarizes the history methods exposed via the callback context.

The accessible history methods are summarized below.

MethodDescription
context.history.fetch()Returns all items from the history.
context.history.get_last()Returns the details of the last send action from the history.
context.history.get_last_mutator()Returns the last mutator instance.
context.history.get_next_mutator()Returns the next mutator instance.

Fetch Method

The fetch method returns information about the last 20 send and receive actions. The following callback example prints the action history to the console.

def callback(context):
print(context.history.fetch())

An example, redacted output of the fetch method is shown below.

[
{
'iteration': 570,
'timestamp': 1657915040527,
'action_id': 'a4b5f0c5-58ca-4d49-bae7-eb5eff53406d',
'action_type': 'send',
'message': {
'id': '37aae2ed-1f5d-40b4-b380-4f7d4ee23d00',
'name': 'Login'
},
'length': 1644,
'data': [80, 79, 83, 84, 32, 47, 97, 112, ...],
'history': {
'index': 570,
'current_iteration_index': 570,
'mutator_id': 'a4b5f0c5-58ca-4d49-bae7-eb5eff53406d'
},
'fields': ['headers / accept / accept.value']
}, {
'iteration': 570,
'timestamp': 1657915040539,
'action_id': 'd87f71fa-3cca-46c9-a3a8-af5111ccbcaa',
'action_type': 'receive',
'message': {
'id': None,
'name': None
},
'length': 795,
'data': [72, 84, 84, 80, 47, 49, 46, 49, 32, 52, 48, 48, 32, 66, 97, 100, 32, ...]
},
...
]

The table below summarizes the common properties of the actions shown above.

PropertyDescription
iterationThe value of the iteration property is identical for both actions, meaning they were performed as part of the same test case. You can also think about the iteration property as the test case number.
timestampThe timestamp (in milliseconds) when the action was performed.
action_idThe unique ID of the action.
action_typeThe type of the action. In the example shown above we see the details of two actions performed: a send and a receive action.
messageIdentifier of the Message Templates involved in the execution of the action. As can be seen in the example, this information is only available for the send action as that is the only action operating using Message templates.
lengthIn case the action_type is send, it represents the length of the data sent. If the action_type is receive, it is the length of the data received.
dataA list of bytes as decimal values. It is the data sent or received, based on the action_type.

The fields property of the send action is a list of strings. These are the full path of the Fields within the assigned Message template that were tested during the iteration.

The history property of the send action should be ignored as it is for internal use only, and its format and contents may change in the future without notice.

Get Last Send

The get_last method returns the last send action from the history. The following callback example prints the action details to the console.

def callback(context):
print(context.history.get_last())

An example, redacted output of the get_last method is shown below.

{
'iteration': 0,
'timestamp': 1657946903302,
'action_id': 'a4b5f0c5-58ca-4d49-bae7-eb5eff53406d',
'action_type': 'send',
'message': {
'id': '37aae2ed-1f5d-40b4-b380-4f7d4ee23d00',
'name': 'Login'
},
'length': 1422,
'data': [80, 79, 83, 84, 32, ...],
'history': {
'index': 0,
'current_iteration_index': 0,
'mutator_id': 'a4b5f0c5-58ca-4d49-bae7-eb5eff53406d'
},
'fields': []
}

The properties of the action is identical to those documented under the Fetch method.

Mutator

The instance of the test case generation engine (mutator) obtained via get_last_mutator() and get_next_mutator() expose the following methods.

MethodDescription
completed()Returns whether all mutations completed (True) or not (False).
next()Forces the currently tested Field to complete and move on to testing the next Field.
mutate()Triggers one mutation of the current Field(s). This method does not return anything.
render()Returns the rendered Message data. The data returned is a list of rendered Group fields on the root level of the Message. Each item in the list is a string. To get the final data to be sent, the list has to be joined together, e.g.: b"".join(list_items).

As mentioned earlier, the get_next_mutator method allows accessing the test case generation engine of the last send action before the Callback in the Test Flow.

The Callback example below demonstrates how to get the data sent to the target by the most recently executed send action.

def callback(context):
next_mutator = context.history.get_last_mutator()
output = b"".join(next_mutator.render(peek=True))
print("LAST SENT", output)

If we wanted to do the same with the test case generation engine of the following send action, the only change required to the above example is to replace get_next_mutator with get_next_mutator.

The test case generation engine instance exposes the Root group via the message property, for example:

mutator = context.get_last_mutator()
root_group = mutator.message

The root_group in the example above exposes the following methods.

MethodDescription
mutating()Returns the instances of the Fields currently tested as a list.
next()Forces the completion of the currently mutating Fields and moves on to the next Field.
mutate()Triggers one mutation of the current Field. This method does not return anything.
render()Returns the rendered Message data. The data returned is a list of rendered Group fields on the root level of the Message. Each item in the list is of Python bytes. To get the final data to be sent, the list has to be concatenated, e.g.: b"".join(list_items).
search(name)Get Field instance by the name of the Field.
search_by_id(id)Get Field instance by the unique ID of the Field.

An example of getting the rendered value of a specific Field (in this example called "test") handled by the mutator of the last send action is shown below.

def callback(context):
mutator = context.history.get_last_mutator()
field = mutator.message.search("test")
print(field.render(peek=True))

Exceptions

Callbacks can raise exceptions just like any Python code. There is a set of Engine-specific exceptions available via the guardara.sdk.exceptions module that Callbacks can use; these are listed below. Please note that the Engine handles any exception not listed in the table raised as an unexpected error, resulting in the termination of the test.

ExceptionDescription
NextIterationExceptionWhen raised, the Engine ignores any remaining actions and continue from the next iteration.
TerminateExceptionWhen raised, the Engine terminates the test. The exception message is added to the test’s Activity Log.
PatternMatchFailConditionExceptionRaised when the response from the target matches a user-provided regular expression. When raised, The Engine stops test execution.
PatternMatchFailConditionWithReportExceptionSame as above but results in the generation of an issue entry within the test report.
PatternMatchNextConditionExceptionRaised when the response from the target matches a user-provided regular expression. When raised, the Engine ignore any remaining actions and moves to the next Field.
PatternMatchNextConditionWithReportExceptionSame as above but results in the generation of an issue entry within the test report.
PatternMatchSkipConditionExceptionRaised when the response from the target matches a user-provided regular expression. When raised, the engine ignores any remaining actions and moves to the next iteration.
PatternMatchSkipConditionWithReportExceptionSame as above but results in the generation of an issue entry within the test report.
PatternMatchPauseConditionExceptionRaised when the response from the target matches a user-provided regular expression. When raised, the Engine pauses test execution.
PatternMatchPauseConditionWithReportExceptionSame as above but results in the generation of an issue entry within the test report.
PatternMatchReportConditionExceptionRaised when the response from the target matches a user-provided regular expression. When raised, the Engine creates an issue entry within the report that includes the exception message.

For example, the NextIterationException exception can be imported as shown below.

from guardara.sdk.exceptions import NextIterationException

Pattern Match Exceptions

The exceptions whose name starts with PatternMatch should be instantiated with a JSON object, for example:

PatternMatchFailConditionException({
"rule_name": "",
"message": ""
})

The rule_name allows setting an arbitrary text to identify the source of the exception. For example, when using standard response processing rules in GUARDARA, the rule_name is set to the name of the response processing rule that raised the exception.

The message is an arbitrary text to include as the Observation of the finding within the report.