-
Notifications
You must be signed in to change notification settings - Fork 54
New Mapper Test Driver Architecture Draft
The current test driver/mapper architecture is inflexible in that it combines the aspect of having symbols that are themselves executable, and the principle of a mapper which translates from concrete to abstract and from abstract to concrete. The fact that concrete symbols must be executable ignores the fact that "abstract" and "concrete" are no absolute concepts, but instead very much depend on the perspective. This requirement of "absolutely concrete" symbols also prevents mappers from being composable.
The core of the proposed new architecture is to separate the above two aspects. Therefore, we have an ExecutableInputSUL
, which takes care of executing symbols that are implementations of ExecutableInput
-- and nothing more. On the other hand, we have a Mapper
interface, which takes care of mapping input symbols from (some form of) abstract level to the (resp. some form of) concrete level, and vice versa for output symbols.
We therefore introduce the following new important classes/interfaces:
-
ExecutableInputSUL
/ContextExecutableInputSUL
: used to executeExecutableInput
symbols, with either a nullaryexecute()
method, or a context-specificexecute(C context)
method -
Mapper
: this is the (more general) replacement forDataMapper
; explanation below
Mapper
is the replacement for the DataMapper
interface. It also has four type parameters: AI
(abstract input), AO
(abstract output), CI
(concrete input), CO
(concrete output). Note that CI
now no longer has a type bound.
A mapper implementation is responsible for lowering/lifting input/output symbols to/from the abstract/concrete level. To sum it up in one sentence: The purpose of a Mapper<AI,AO,CI,CO>
is to make a concrete SUL<CI,CO>
appear as an abstract SUL<AI,AO>
.
The lowering/lifting is done by the mapInput
and mapOutput
methods, respectively, that are direct equivalents of the old DataMapper.input
and DataMapper.output
methods.
Furthermore, a mapper is responsible for handling SULException
s that occur during invocations of SUL.step()
. The API for this has been changed to a more convenient form, which is best explained along an example:
Suppose we have a mapper that uses String
s as abstract outputs. We treat SULException
s (or rather their causes) according to the following scheme:
-
IllegalArgumentException
s usually result from locally erroneous parameters of a single invocation, but do not affect subsequent invocations. We therefore want to output "error", but otherwise continue the regular execution. -
NullPointerException
s play a special role. We therefore want to produce a distinct output "npe". Furthermore, we do not continue execution, as it might result from the internal state of our target object being corrupted. -
Exception
s signal (regular) error conditions. We also do not want to continue execution, because we do not now what's going wrong. To signal this abnormal termination, the output "fatal" should be used. - everything else might be anything, from a user-defined subclass of
Throwable
to anOutOfMemoryError
. It usually is not a good idea to handle these, we therefore just pass them on to the next higher level.
In case of NullPointerException
s and IllegalArgumentException
s, we abort the execution on the SUL even though there are still inputs to process. To make clear that the outputs do not correspond to any actual execution, we output "unobserved" for each input that was processed after a NullPointerException
or Exception
was thrown.
The corresponding exception mapper is pretty concise:
@Override
public MappedException<String> mapException(SULException ex) {
Throwable cause = ex.getCause();
if(cause instanceof IllegalArgumentException) {
return MappedException.ignoreAndContinue("error");
}
if(cause instanceof NullPointerException) {
return MappedException.repeatOutput("npe", "unobserved");
}
if(cause instanceof Exception) {
return MappedException.repeatOutput("fatal", "unobserved");
}
// else
return MappedException.pass(cause);
}
The Mappers
class provides some utility operations for Mappers.
-
Mappers.apply
takes aMapper<AI,AO,CI,CO>
and aSUL<CI,CO>
and returns aSUL<AI,AO>
(as we remember, this is the prime purpose of a mapper) -
Mappers.compose
returns a mapper that is the composition of two specified mappers. In simplified notation, for obtaining aMapper<AI,AO,CI,CO>
, the provided mappers must be of typesMapper<AI,AO,CAI,ACO>
for the "outer" andMapper<CAI,ACO,CI,CO>
for the "inner" mapper. (CAI
/ACO
stands for "concrete/abstract input" and "abstract/concrete output", respectively)
The SULException
is now part of the API package. This exception is used exclusively to wrap other exceptions that occur during an execution of a SUL.step()
implementation. This exception is unchecked, so existing code depending on the SUL
interface should not break.
Furthermore, I propose moving the ExecutableInput
and ContextExecutableInput
as well as the respective SUL implementations to the learnlib-core
module.
Hopefully, not much will change. Existing implementations of DataMapper
should be easily able to migrate their code to the new Mapper
interface. The only changes are of method names to more descriptive names, and the exception handling facility, which is more convenient now.
Instantiations of TestDriver
s of form new TestDriver<>(myMapper)
will most likely simply have to be replaced by Mappers.apply(myMapper, new ExecutableInputSUL<>())
.
The adaption to the new mapper architecture for the reflection use case can be seen here. The usage only differs marginally.