Section Processors¶
In aetest
, functions and methods scheduled to run immediately before and
after testscript sections are called pre-processors, post-processors,
exception-processors.
These programs possess the ability to process the given section based on its
id, parameters and results, dynamically and directly affecting the outcome of
testing.
Because of this unique inline execution trait, pre/post/exception processors may also be used as means to perform common, routine checks before and after each test section. Here are some possible use cases:
- Pre-Processors
take snapshots of the current test environment information (eg, testbed configuration)
check the test environment and determine if the current section should run or not
- Post-Processors
access/validate the result of the section that just finished execution
check current test environment against previous snapshots (eg, router health-checking)
execute debug commands, collect dump files & etc
- Exception-Processors
take post exception snapshots of the current test environment information (eg, testbed configuration) when exception occurs
execute debug commands, collect dump files & etc when exception occurs
suppress a specified Exception and assign a result to the section.
The usage of pre/post/exception processor feature is entirely optional. They
can access the internals of aetest
Object Model, and can be extremely
powerful.
With great power, comes great responsibilities - use them wisely.
Definition & Arguments¶
Pre/post/exception processors are affixed to each script sections using
@processors
decorator, and providing it lists of objects for each condition.
Syntax
------
@processors(pre = [list of pre-processor objects],
post = [list of post-processor objects],
exception = [list of exception-processor objects])
Processor functions support parameter propagation the same way that test sections do. Parameters from parent objects, or a datafile (eg. the testbed) can be passed to a processor by simply declaring an argument of the same name. Some default parameters available for all processors are:
section
for the Testcase or Test Section the processor is being applied to
processor
for the running processor itself, which has aproperties
attribute, as well as result APIs (eg.processor.failed()
steps
for declaring steps within the processor
Exception processors also have default parameters of exc_type
, exc_value
,
and exc_traceback
for the exception that occurs in the parent section.
# Example
# -------
#
# simple pre/post/exception processor example
from pyats import aetest
# define a function that prints the section's uid
def print_uid(section):
print('current section: ', section.uid)
# define a function that prints the section result
def print_result(section):
print('section result: ', section.result)
# define another function that prints the exception message and suppress the
# exception
def print_exception_message(section, exc_type, exc_value, exc_traceback):
print('exception : ', exc_type, exc_value)
return True
# use the above functions as pre/post/exception processors to a Testcase
# pre-processor : print_uid
# post-processor : print_result
# exception-processor : print_exception_message
@aetest.processors(pre = [print_uid],
post = [print_result],
exception = [print_exception_message])
class Testcase(aetest.Testcase):
@aetest.test
def test(self):
print('running testcase test section')
@aetest.test
def testException(self):
raise Exception('running testcase testException section')
# define a function that fails when a section does not set a parameter, this
# will propagate the failure to the parent section
def fail_if_not_a(processor):
a = processor.parameters.get('a')
if not a:
processor.failed('a was not set to True')
class Testcase2(aetest.Testcase):
# use fail_if_not_a as a post-processor on only this test section
@aetest.processors.post(fail_if_not_a)
@aetest.test
def test(self):
self.parameters['a'] = False
# script output
# - notice that the processors ran immediately before and after the
# testcase execution.
#
# +------------------------------------------------------------------------------+
# | Starting testcase Testcase |
# +------------------------------------------------------------------------------+
# +------------------------------------------------------------------------------+
# -> | Starting PreProcessor-1 'print_uid' |
# +------------------------------------------------------------------------------+
# -> current section: Testcase
# +------------------------------------------------------------------------------+
# | Starting section test |
# +------------------------------------------------------------------------------+
# running testcase test section
# The result of section test is => PASSED
# +------------------------------------------------------------------------------+
# | Starting section testException |
# +------------------------------------------------------------------------------+
# +------------------------------------------------------------------------------+
# -> | Starting ExceptionProcessor-1 'print_exception_message' |
# +------------------------------------------------------------------------------+
# -> exception : <class 'Exception'> running testcase testException section
# -> The result of section testException is => PASSED
# +------------------------------------------------------------------------------+
# -> | Starting PostProcessor-1 'print_result' |
# +------------------------------------------------------------------------------+
# -> section result: passed
# The result of testcase Testcase is => PASSED
# +------------------------------------------------------------------------------+
# | Starting testcase Testcase2 |
# +------------------------------------------------------------------------------+
# +------------------------------------------------------------------------------+
# | Starting section test |
# +------------------------------------------------------------------------------+
# +------------------------------------------------------------------------------+
# -> | Starting PostProcessor-1 'fail_if_not_a' |
# +------------------------------------------------------------------------------+
# The result of PostProcessor-1 'fail_if_not_a' is => FAILED
# The result of section test is => FAILED
# The result of testcase Testcase2 is => FAILED
Since parameters are only passed when there is an argument of the same name, any
functions & methods that require zero arguments to invoke are useable as
pre/post processors. However making use of the arguments to access parameters
provides many options. The section
parameter can be passed as an argument,
which references the current running section object that the processor has been
applied to. This enables the processor function to access within the current
executing object, reference its Test Parameters and act accordingly.
Refer to Object Model for section object details.
Pre/post/exception processors can be applied independently towards both test
containers (CommonSetup
, Testcase
, CommonCleanup
) and test sections
(subsections
, setup
, test
, cleanup
). Each section may receive
an arbitrary number of processor functions, run in the order of appearance.
Note
A section can have multiple processors all trying to set the result through APIs. The final result set for the section will be the ‘rolled-up’ result from all of the processors of that type. (see Object Model for rules about roll-up).
Note
If pre-processors block a section from executing, the post-processors will not be executed.
The decorator @processors
can be used to define both pre-processors,
post-processors and exception-processors at the same time. The following
alternatives allows the definition of one specific type using *varargs
style
input.
Alternative Syntax
------------------
@processor.pre(*list of pre-processors)
@processor.post(*list of post-processors)
@processor.exception(*list of exception-processors)
# Example
# -------
#
# extended pre/post processor/exception examples
from pyats import aetest
# assuming we had a library full of readily defined processor functions
# import all of them for the sake of this example
from pre_processors import *
from post_processors import *
from exception_processors import *
class common_setup(aetest.CommonSetup):
# attach some pre-processors to subsection
# using @aetest.processors.pre(x, y, z, ...) shortcut definition
# this is equivalent to:
# @aetest.processors(pre = [x, y, z, ...])
@aetest.processors.pre(collect_snapshot)
@aetest.subsection
def subsection(self):
pass
# attach multiple processors to testcase
@aetest.processors(pre = [check_environment, collect_snapshot, check_uid],
post = [router_health_check, restore_snapshot],
exception = [unexpected_exception_snapshot])
class Testcase(aetest.Testcase):
# attach some post-processors to test section
# using @aetest.processors.post(x, y, z, ...) shortcut definition
# this is equivalent to:
# @aetest.processors(post = [x, y, z, ...])
@aetest.processors.post(run_debug_commands, check_memory_leak)
@aetest.test
def test(self):
pass
Results¶
Processors also have a result, which can be set in multiple ways. The section
object and the processor
object both have Result APIs which act in
slightly different ways. Any result apis called from the processor
object
behave as expected, setting a result for that processor before moving on with
execution. This result rolls up to the result of the parent section, so failing
a processor will also mark a Testcase as failed. Calling a result api from the
section
object will apply that result to the section directly, instead of the
normal roll up behavior. For pre-processors, this will block the execution
of the section entirely, just setting the result instead. For
post-processors, this can override existing results occurring in that
section.
For example, result apis from either processor
or section
could declare a
section as failed with a post-processor even if it already passed.
def section_failed(section):
section.failed()
class Testcase(aetest.Testcase):
@aetest.processors.post(section_failed)
@aetest.test
def test(self):
pass
# This test section would regularly pass, but the processor will cause
# the result to be Failed
Note
However, only the section
apis could mark a failed section as passed,
since this goes against Results Rollups
There are some other ways to impact the section result with processors.
Pre-processors that return False
, will cause the section to be
Skipped
, pre-processors that have an assertion failure will be
Blocked``and **Exception-processors** that return ``True
will suppress the
exception and prevent an Errored
result.
Context Processors¶
Typical pre/post/exception-processors are just functions with a specific purpose. Context-processors, on the other hand, are similar to Python’s context managers in the sense that they can handle the before, after, and exceptions within a single class.
Tip
think of a context-processor as pre + post + exception processor all-in-one
There are two methods of defining context-processors:
by subclassing from
aetest.processors.bases.BaseContextProcessor
using
@aetest.processors.context
decorator on a generator factory function.
# Example
# -------
#
# simple context processor example
from pyats import aetest
from pyats.aetest.processors.bases import BaseContextProcessor
# define a context processor that:
# - print the section uid before testcase
# - prints the section result after testcase in normal conditions
# - prints the exception when an exception occurs
class ContextProcessor(BaseContextProcessor):
def __enter__(self):
print('current section: ', self.section.uid)
# can also access parameters
testbed = self.parameters.get('testbed')
def __exit__(self, type_, value, traceback):
if type_:
print('An exception occured!')
print('exception : ', exc_type, exc_value)
else:
print('section result: ', self.section.result)
# attach above context processor to a Testcase
@aetest.processors(ContextProcessor)
class Testcase(aetest.Testcase):
@aetest.test
def test(self):
print('running testcase test section')
@aetest.test
def testException(self):
raise Exception('running testcase testException section')
In essense, a basic, class-based context-processor is basically a Python
Context Manager, with its __enter__()
called as the section
pre-processor, and __exit__()
called as the
post+exception-processor. If an exception occurs, __exit__()
is called
after the exception but before the section result is set, just like an
exception-processor. If no exception occurs, __exit__()
is called later
after the exception is set, like a post-processor. Additionally, the result
apis are still available from the class itself. So a call of self.failed()
would be equivalent to processor.failed()
in a pre-processor.
You can also opt to define generator-style context-processors, similar to
Python’s contextlib.contextmanager
functionality:
# Example
# -------
#
# simple context processor example using generator
# (same functionality as above)
from pyats import aetest
@aetest.processors.context
def context_processor(section, processor):
print('current section: ', section.uid)
# accessing parameters
testbed = processor.parameters.get('testbed')
try:
yield
except Exception as e:
print('An exception occurred!')
print('exception : ', exc_type, exc_value)
# we are not raising e, so it will be suppressed
else:
print('section result: ', section.result)
# attach above context processor to a Testcase
@aetest.processors(context_processor)
class Testcase(aetest.Testcase):
@aetest.test
def test(self):
print('running testcase test section')
@aetest.test
def testException(self):
raise Exception('running testcase testException section')
Tip
Generator-style context-processors do not return a boolean value to determine whether or not to suppress an exception. Instead, they suppress exceptions by default and must raise the same exception again in order to let it propagate.
Global Processors¶
In addition to the ability to attach processors to classes & sections, it is also possible to define processors that run globally: before and after each and every defined script section (common setup/cleanup, subsection, testcases, setup/cleanup/tests), or on Exception occurance.
Global processors are no different than the ones affixed to each section using
the @processors
decorator, except that they always run automatically. To
use global processors in your testscript, define a script-level dictionary named
global_processors
with pre
, post
and exception
as the keys, and
the values being a list of processor functions.
Global Processors Syntax
------------------------
global_processors = {
'pre': [list of global pre-processor objects],
'post': [list of global post-processor objects],
'exception': [list of global exception-processor objects],
'context': [list of global context processor classes/functions]
}
# Example
# -------
#
# script using global processors
from pyats import aetest
# define a function that prints the section's uid
def print_uid(section):
print('current section: ', section.uid)
# define a function that prints the section result
def print_result(section):
print('section result: ', section.result)
# define another function that prints the exception message and suppress the
# exception
def print_exception_message(section, exc_type, exc_value, exc_traceback):
print('exception : ', exc_type, exc_value)
return True
# use the above functions global pre/post processors
# global pre-processor : print_uid
# global post-processor : print_result
# global exception-processor : print_exception_message
global_processors = {
'pre': [print_uid,],
'post': [print_result,],
'exception': [print_exception_message,],
}
class Testcase(aetest.Testcase):
@aetest.test
def test(self):
print('running testcase test section')
@aetest.test
def testException(self):
pyATS()
# script output
# - notice that the processors ran immediately before and after each
# section (testcase & test) execution.
# - note that section test result is null - because it hasn't been given
# a result by the executer yet.
#
# +------------------------------------------------------------------------------+
# | Starting testcase Testcase |
# +------------------------------------------------------------------------------+
# -> Running pre-processor: 'print_uid'
# -> current section: Testcase
# +------------------------------------------------------------------------------+
# | Starting section test |
# +------------------------------------------------------------------------------+
# -> Running pre-processor: 'print_uid'
# -> current section: test
# running testcase test section
# -> Running post-processor: 'print_result'
# -> section result: null
# The result of section test is => PASSED
# +------------------------------------------------------------------------------+
# | Starting section testException |
# +------------------------------------------------------------------------------+
# -> Running pre-processor: 'print_uid'
# -> current section: testException
# Running exception-processor: 'print_exception_message'
# exception : NameError name 'pyATS' is not defined
# section result: null
# -> Running post-processor: 'print_result'
# -> section result: null
# The result of section testException is => PASSED
# -> Running post-processor: 'print_result'
# -> section result: passed
# The result of testcase Testcase is => PASSED
Hint
global processors may be extremely useful in cases where you wish to run some functions before and after everything - for example, collecting code coverages (Cflow), and router healths (router health check), etc.
Runtime Behaviors¶
The following rules describes the behavior of pre/post/exception processors when defined.
pre-processors are run immediately before test section execution
post-processors are run immediately after test section execution
exception-processors are run immediately after test section raised Exception
exception-processors will be skipped if there is no Exceptions occurred during test section execution
context-processors run before function-based processors. Eg:
before a section, any attached context-processor
__enter__()
will run before all other pre-processorsafter a section, any attached context-processor’s
__exit__()
will run, before all other exception processors and post-processors
global processors are always run before local processors.
if a processor requires an argument named
section
, the current executing section is provided to that argument value.def processorFunc(section): pass
while executing pre-processor functions or context-processor’s
__enter__()
api, if anyAssertionError
is caught, or if the function returnsFalse
, all remainining pre-processor and context-processors will be skipped, and the test section is skipped over with a result ofSkipped
. All post-processors are also skipped. Otherwise, execution continues as originally scheduled.def preprocessorAssertionError(): # assertion error causes all remaining pre-processors to be skipped # and the test section also receives a result of Skipped assert 'vim' is 'great' def preprocessorReturnFalse() # if a pre-processor returns False, all remaining pre-processors # are skipped, and the test section is skipped also. return False
when returning
False
in pre-processors or context processor’s__enter__()
, an optional reason message may also be returned. This is printed as the reason for skipping the current section in the log file.def preprocessorReturnFalseWithReason() # return false along with a reason (as a tuple) return False, "murphy's law :-("
if any
Exceptions
are caught while executing processor functions, all remaining processors functions are skipped over, and the test section receives a result ofErrored
. If thatException
occured within a pre-processor, the test section is skipped with a result ofErrored
.if a section has any attached exception-processors or context-processors, any unhandled exception will be passed to the processor, with exception type, value and traceback.
in the case of generator-based context processors, the exception will be thrown into the generator using
gen.throw()
mechanism
exception-processors will handle Exception in the following order: global, testcase, local.
if any exception-processors or context-processor
__exit__()
returnsTrue
, the Exception from executed test section will be suppressed.
Note
exception-processors does not support AssertionError raised from Section Steps. Other types of Exception raised from Section Steps would be handled accordingly.
Additional APIs¶
The list of pre/post/exception processors affixed to each test script section can be dynamically accessed and queried during runtime, using the following functions:
processors.get(section, type_, incl_globals=False)
returns the list of pre/post/exception processors affixed to a section object. By default, get only returns the processors applied to that section. Using the
incl_globals = True
argument also includes current known global processors of that type.# Example # ------- # # processors.get function from pyats import aetest # create a global processor global_processors = dict(pre = [lambda: True]) # testcase with two lambda functions as pre-processors @aetest.processors.pre(lambda: True, lambda: True) class Testcase(aetest.Testcase): pass aetest.processors.get(Testcase, type_ = 'pre') # [<function <lambda> at 0xf758e734>, # <function <lambda> at 0xf769e0bc>] aetest.processors.get(Testcase, type_ = 'post') # [] aetest.processors.get(Testcase, type_ = 'exception') # [] aetest.processors.get(Testcase, type_ = 'pre', incl_globals = True) # [<function <lambda> at 0xf756b305> # <function <lambda> at 0xf758e734>, # <function <lambda> at 0xf769e0bc>]
processors.affix(section, context = [], pre = [], post = [], exception = [])
dynamically affix pre/post/exception processors to a given section object. Any previously defined pre/post/exception/context processor functions are overwritten.
# Example # ------- # # processors.add function from pyats import aetest # testcase with two lambda functions as pre-processors (false) @aetest.processors.pre(lambda: False, lambda: False) class Testcase(aetest.Testcase): pass # replace the two functions to Testcase aetest.processors.affix(Testcase, pre = [lambda: True, lambda: True])
processors.add(section, context = [], pre = [], post = [], exception = [])
add more pre/post/exception/context processors to a given section object. This appends to the list of existing processors.
# Example # ------- # # processors.add function from pyats import aetest class Testcase(aetest.Testcase): pass # add two lambda functions to Testcase as post-processors aetest.processors.add(Testcase, post = [lambda: True, lambda: True])
# Example
# -------
#
# using additional pre/post/exception processor APIs
from pyats import aetest
def print_parameters(section):
print(section.parameters)
def print_exception_message(section, exc_type, exc_value, exc_traceback):
print('exception : ', exc_type, exc_value)
return True
class CommonSetup(aetest.CommonSetup):
@aetest.subsection
def subsection(self):
# affix pre-processors and exception-processors to testcase
aetest.processors.affix(Testcase, pre = [print_parameters],
exception = [print_exception_message])
class Testcase(aetest.Testcase):
@aetest.setup
def setup(self):
# check if testcase has processors
for type_ in ('pre', 'post', 'exception'):
if aetest.processors.get(self, type_):
print('Testcase has %s-processors' % type_)
# affix post-processors to test function
aetest.processors.affix(self.test, post = [print_parameters])
# affix exception-processors to testException function
aetest.processors.affix(self.testException,
exception = [print_exception_message])
@aetest.test
def test(self):
pass
@aetest.test
def testException(self):
pyATS()
# the above example probably didn't make much sense.
# the goal is to show you what can be done.
Reporting¶
Processors dy default are not reported as sections of a test. This can be
changed using the configuration option, or by using
the processor.report
decorator on the processor function itself
@aetest.processors.report
def my_post_processor(section):
print(section.result)
Each processor appears as a child section of the Testcase/Test Section it is being applied to, similar to adding another Test Section to a Testcase, or a Step to a Test Section.
Even if reporting for a processor is disabled, any results raised will still be propagated to the parent section, so the processor retains all functionality.