Developing Advanced Eclipse XMF Applications


Summary

This tutorial describes how to write an Eclipse application that integrates a number of XMF technologies with EMF to produce a  general purpose testing framework. The resulting tool uses EMF to represent all data and uses XMF to implement a DSL for a testing language.

Contents

Introduction

This tutorial shows how to develop an Eclipse application that integrates a number of XMF technologies with EMF. EMF is used to represent all models and XMF is used to perform all model execution.

The application is a tool for performing model driven testing of a sustem under test (SUT). The test scripts and test reports are represented as instances of EMF models. XMF is used to design a domain specific language (DSL) for  the testing language, to implement an execution engine for the testing language and to implement code templates that tansform test reports to HTML.

This tutorial is intended to show how XMF and EMF can be combined on Eclipse to produce a complete working application. It assumes that you are familiar with EMF and the basic concepts of XMF and XOCL.

To top.

The Application

Overview

Software systems execute by maintaining an internal state that is queried and modified by system functions that are exposed to the various users of the system. System design usually starts with a simple model of the internal state. Implementation and subsequent modification of the system represents the state information using programming language features that can be very different in structure and volume from the clean information model.

Once a system is implemented it must be tested. Testing involves checking that the system functions return expected information and perform the expected internal state modifications. If the correspondence between the state model and the state implementation is lost then testing must resort to manipulating the programming language constructs directly. This makes the test scripts brittle (they are sensitive to change in the implementation) and unnecessarily complex. Functional testing should test the logical implication of performing system functions and should not need to resort to the implementation detail.

Model Driven Testing is an approach to system testing that allows the test scripts to be constructed using the logical state model. The correspondence between the logical model and the implementation detail is retained, but is defined in one place instead of being spread out amongst the many test cases. Changes in implementation often do not cause any change in the state model (for example a new faster implementation for a table of records is discovered). Therefore, the information model changes much more slowly than its implementation making the test cases much less brittle.

An overview of the architecture of Model Driven Testing is shown in the diagram below:

MDT Architecture

The system under test (SUT) exposes a collection of functions in an interface that is available to clients. The interface is defined against an information model that describes a logical view of the SUT state. A testing framework is used to build and perform a collection of test scripts. The test scripts are defined against the information model.

Typically, a test script will perform a sequence of system functions and then request the current state of the SUT. The state is returned as an instance of the information model. The test script will then interrogate the state to ensure that it contains the expected information.

In another scenario, the testing framework can supply the SUT with an instance of the information model in order to populate the current state.

XMF-OS can be used to develop a Model Driven Testing Framework that uses EMF to model the state information and to provide a simple tree-based user interface. XMF-OS is used to define a testing language. Test scripts written in the testing language can be loaded into the browser and then performed against a SUT.  The rest of this section gives an overview of applying the intended application using a Library example, the remainder of the article will focus on how the application is built.

To top.

Using the application

The application of the testing application will be illustrated using a library example. Assuming a simple information system that implements a Library represented using the following model.

Library EMF model

The model shown above shows a logical view of the Library information. A real Library Information System is likely to use a standard relational database to represent the information. When designing tests for the system it is much easier to think of the logical view rather then the implementation view. So long as the logical view can be translated into the implementation view and vice versa then no information is lost.

Given a logcial view of the information, the first step is to design some test cases. Our testing tool uses a domain specific language (DSL) for testing that makes writing test cases over information models easy. The testing DSL uses the idea of states and actions to construct test scripts. A test script connects to the SUT and sends requests to perform actions. An action is just a system function that is exposed to the testing framework by the SUT. Before and after performing actions, the test script may request the current state of the SUT. The state is returned (possibly by translating the implementation into a logical view) and matched against state patterns. The testing framework checks whether a state pattern is matched and changed the control flow in the test script accordingly.

Here is a very simple test script:

case LibraryTest {

// Define system states that will be used in the
// test scripts....

state hasBook(n)->(b) {

// State hasBook is satisfied when the
// current library state contains a book
// b with the title n. The state receives
// the name n and returns the book b...

obj(root,library.Library) {
books contains b;
}

obj(b,library.Book) {
title = n;
}
}

->

let () = match hasBook("Pride and Prejudice")
in
// If there is a book with the supplied title then
// raise an error...

raise bookExists("Not expecting a book.");

else {
// Do nothing...
}
}
The test case defined above is called LibraryTest. The test case consists of 0 or more state patterns defined before the arrow (->) and a script defined after the arrow. Firstly, consider the script part which defines a check requiring that the current state of the library does not contain a book named Pride and Prejudice. The keyword 'match' is followed by the name of a state pattern and can optionally pass arguments (in this case it passes the name of the book). If the pattern matches, then the action before the 'else' keyword is performed. In this case, since we are expecting the library not to contain the named book, the action raises an error: bookExists("Not expecting a book."). If the state pattern fails to match then the action after the 'else' keyword is performed. In this case, we do nothing.

Now, consider the state pattern named hasBook. A pattern contains a sequence of input parameters (in this case n) and a sequence of output parameters (in this case b). The idea is that a state pattern may be passed information, matches a pattern and then returns information extracted from the match. An SUT will always have a single root class that can be used to navigate via associations to all other classes. In the case of the Library information system, the root class is Library. When matching a state pattern, the variable 'root' is always associated with an instance of the root class. Each object in the state returned by the SUT can be matched using an object pattern of the form:
obj(var,class) {
slotName = slotValue;
slotName contains slotValue;
...
}
where the variable 'var' references the same object throughout the pattern. The state pattern hasBook requires that the current Library state consists of two objects, one of which must be the root. The root must be an instance of the EMF class library.Library and the other must be an instance of the EMF class library.Book. The root object must have a slot called 'books' which must be a collection (actually an EList) that contains the Book object. The Book object must have a title that is the same as the value n passed to the state pattern.

If the hasBook state pattern matches the supplied Library state then the Book instance is passed back as the value of the output parameter.

Here is the complete test case for the library:
case LibraryTest {

// Define system states that will be used in the
// test scripts....

state hasBook(n)->(b) {

// State hasBook is satisfied when the
// current library state contains a book
// b with the title n. The state receives
// the name n and returns the book b...

obj(root,library.Library) {
books contains b;
}

obj(b,library.Book) {
title = n;
}
}

state hasCopy(book,id) -> (copy) {

// State hasCopy is satisfied when the
// current library state has a book with
// the supplied id. the state returns the
// copy...

obj(root,library.Library) {
books contains book;
copies contains copy;
}

obj(copy,library.Copy) {
id = id;
book = book;
}
}

state hasReader(name) -> (reader) {

// State hasReader is satisfied when the
// current library state has a reader with
// the supplied name. The reader is
// returned...

obj(root,library.Library) {
readers contains reader;
}

obj(reader,library.Reader) {
name = name;
}
}

state borrows(reader,copy) -> () {

// State borrows is satisfied when the
// current library state records a borrowing
// event between the supplied reader and the
// supplied book copy. Nothing is returned...

obj(root,library.Library) {
borrowings contains b;
}

obj(b,library.Borrows) {
reader = reader;
copy = copy;
}
}

->

// The following test script is perfomed with respect
// to the declared system states above...

// Firstly, check that there is no book in the
// library with the supplied title...

let () = match hasBook("Pride and Prejudice")
in
// If there is a book with the supplied title then
// raise an error...

raise bookExists("Not expecting a book.");
else {

// Perform the system action that adds a book
// with the given title...

do addBook("Pride and Prejudice");

// Now use the same state check to ensure that
// the library state has been updated correctly...

let (book) = match hasBook("Pride and Prejudice")
in {

// If we get here then the library is OK.
// Now add a copy of the book...

do addCopy("Pride and Prejudice","copy1");

// Check that the library has recorded the copy...

let (copy) = match hasCopy(book,"copy1")
in {

// If we get here then the library has correctly
// recorded the copy of the book. Now add a reader
// with the given name...

do addReader("fred");

// Check that the reader was added...

let (reader) = match hasReader("fred")
in {

// If we get here then the library has a copy of
// the book and the reader is registered. Record
// the reader borrowing the book from the library...

do borrow("fred","copy1");

// Check that the borrowing has taken place...

let () = match borrows(reader,copy)
in {

// If we get here then everything has gone OK and
// we do nothing...
}

// The borrowing event was not recorded...

else raise noBorrow("fred","copy1");
}

// The reader was not registered...

else raise readerNotCreated("fred");
}

// A copy of the book was not recorded...

else raise noCopy("copy1");
}

// No book was added to the library...

else raise noBook("Pride and Prejudice.");
}
}

To use the tool a new Eclipse project "My Tests" is created, and a new Testing Model added to the project.

New test model

A new test suite is added to the model called "Library" via a right click menu on Test Model.  Each test suite may contain any number of test cases. Each test case is written in the testing DSL described above and is loaded from a text file. A test script can be loaded via a menu attached to a test suite in the browser.  The "Library" test script is loaded which consists of four state patterns and a script.

Loading library test case

A test case can be performed with respect to a SUT using a menu on the test case. When the test case is performed, the script is executed causing a sequence of action requests on the SUT. In addition to actions, the state of the SUT is requested at appropriate times and matched against the state patterns. The outcome of the match influences the control flow through the test script. The result of performing a test case is added as a report. Each time a test case is performed, a new report is added to the test case.  

Performing a test case

A test case report consists of a sequence of steps. Each step is either an SUT action, a successful state match or a failed state match. A report can be displayed as a document via a menu.

Report from performing test case

Suppose that the SUT fails to add the reader named fred into the system correctly. When the testing tool requests the system state information model and matches it against the 'hasReader' state pattern, the match will fail and an appropriate exception should be raised. The resulting report shows that the SUT fails to behave as expected:

Report of failing test case


To top.

Implementing the Application

This section describes how the application described in the previous section is implemented using Eclipse, EMF and XMF-OS.

To top.

Test Model

Test models are created as an instance of the following EMF meta-model.

Test case EMF model

The rest of this section describes the classes in this model:
  • NamedElement serves as an abstract super-class for all the elements in the model that have a name.
  • TestModel is used as a container of test suites.
  • TestSuite is a container of test cases.
  • A test case has a number of state patterns, a single action and a collection of test reports. The idea is that a test case defines a script (the action) that performs functions of the SUT and checks the states of the SUT against the syate patterns. Each time the test case is performed, it produces a report that is saved as an instance of the class Report.
  • The class Report is defined here.
  • The class State is defined here.
  • The class Action is an abstract super-class of the things that a test script can do.
  • A Block is a sequence of actions that are performed in turn.
  • The class Let is an abstract super-class of the actions that produce results. A let-action may be used to match against a state pattern or perform an SUT function.
  • A MatchingLet is used to match the current state of the SUT against a named state pattern. At any time the SUT can be asked to produce a description of itself in terms of the information model. This description can be matched against a named state (possibly passing data values into the state). The match will either succeed or fail. If the match succeeds then 0 or more output values are produced. If the match fails then 0 output values are produced. A MatchingLet has a body that is performed if the match succeeds and an otherwise action that is performed if the match fails.
  • A DoingLet performs an SUT function passing 0 or more input values and producing 0 or more output values. A DoingLet is equivalent to a Do, except a Do cannot get values back from the SUT.
  • An exception is raised using the Raise class. The name of an exception is given along with some values.
  • A Do is a request to perform an SUT function.
  • Values may be passed as parameters to various actions. Values are represented as instances of LetArg. A LetArg may be a constant LetConst or a variable reference LetVarRef.
The EMF model is transformed into an EMF editor using standard EMF tools and into a collection of XOCL classes using the XMF translator. Creating a test model directly in terms of the EMF classes would be tedious. XMF is used to define a grammar that can create instances of the XOCL classes that represent the EMF classes in XMF. This allows the EMF instances to be created using a text-based testing DSL as shown here.

To top.

Project Services

When developing an XMF engine for an Eclipse plugin, a key feature is the design of the services-interface. The services are the public interface privided by the XMF application to the Eclipse framework. The services are named operations that are implemented in XOCL and can be called by Java. A typical use of services is to link them to Eclipse menu items so that they can be called via the GUI. The testing framework is to provide the following services:

Services are defined in XMF using a special language construct. The name of the service can then be called from a Java application that runs as an Eclipse plugin in conjunction with an XMF machine that has loaded the service. The rest of this section provides the key steps in defining and linking the services for the testing framework.

Firstly, the services are defined as follows:
@Service newTestSuite(testModel)
let name =
xmf.getString("Name","Name of new Test Suite","")
in testModel.testSuites.add(TestSuite(name))
end
end;

@Service loadTestCase(testSuite)

// Use a try to catch any parsing errors that
// might be raised and then use a dialog to report
// the error...

try
testSuite.loadTestCase(xmf.openFile(".","*.txt",""))
catch(x)
xmf.reportError(x.message)
end
end;

@Service perform(testCase,library)
testCase.perform(library)
end;

@Service report(report)
xmf.browser("Report",report.report())
end;

Now, we need to link the services to menu items. In the plugin generated from the EMF model for the browser-based editor, we add definitions of menu items:
   <extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="testing.TestModel">
<action id="newTestSuite" label="New Test Suite" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>

<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="testing.TestSuite">
<action id="loadTestCase" label="Load Test Case" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>

<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="testing.TestCase">
<action id="perform" label="Perform" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>

<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="reports.Report">
<action id="report" label="Report" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>

Each menu contribution specifies an action id, a label and a class that will have the action when it is selected. The action handler is a sub-class of ActionDelegate (it can be generated automatically for you by the XMF project creation mechanism on Eclipse). The action handler class defines a method run as follows:
public void run(IAction action) {

XMFMachine machine = XMFMachineRegistry.getMachine("testing.machine");

String id = action.getId();
Object o = selection.getFirstElement();
 ClassLoader loader = classLoader();


if (id.equals("newTestSuite"))
machine.sendService("newTestSuite", new Object[] { o },loader);

else if (id.equals("loadTestCase"))
machine.sendService("loadTestCase", new Object[] { o },loader);

else if (id.equals("perform")) {
 Library library = LibraryFactoryImpl.eINSTANCE.createLibrary();

machine.sendService("perform", new Object[] { o, library },loader);

 } else if (id.equals("report"))
machine.sendService("report", new Object[] { o },loader);

else System.out.println("Unknown action id: " + id);
}

Given a definition of the application interface via its services, we will continue with the description of the test framework implementation by describing each service in turn.

To top.

Creating a New Test Suite

The new test suite service, simply creates and adds a new test suite to the supplied test model. It uses a dialog: xmf.getString() to request the name of the new suite from the user.

To top.

Loading a Test Case

A new test case is loaded from a file. The dialog, xmf.openFile() is used to request the path to the file containing the source code for the test case. The source code is loaded via the loadTestCase operation defined for TestSuite:
context TestSuite
@Operation loadTestCase(path:String)

// Use the Grammar::parseFile operation to parse the contents
// of the file, starting at the non-terminal TestCase...

self.getTestCases().add(TestCase.grammar.parseFile(path,"TestCase",Seq{}))
end
The TestCase grammar is defined below (we'll define the State grammar later on):
@Grammar extends State.grammar

TestCase ::=

// A test case is named...

'case' n = Name '{'

// has a sequence of state patterns...

S = State*
'->'

// a test script action...

a = Action
'}'

{ // Create the test script...

let t = TestCase(n,a)
in
// Add the states into the script...

@For s in S do
t.getStates().add(s)
end;
t
end
}.

Action ::=

// An action is one of...

Do // Call an SUT function...
| Block // Perform a sequence of actions...
| Let // Bind some variables...
| Raise. // Raise an exception.

Condition ::= Str.

Do ::=

// Perform a named SUT function...

'do' n = Name A = LetArgs ';' {

// Create the Do...

let d = Do(n)
in
// Add in the argument values...

@For a in A do
d.getArgs().add(a)
end;
d
end
}.

Block ::=

// A block is a sequence of actions...

'{' as = Action* '}'

// Create the block an add in the actions...

{ let b = Block()
in @For a in as do
b.getActions().add(a)
end;
b
end
}.

Let ::=

// All types of let-action bind 0 or more
// arguments. the let-actions differ in terms
// of where the variable values some from...

'let' V = LetVars '=' LetBind^(V).

LetBind(V) ::=

// Either a state match or an SUT function...

MatchingLet^(V)
| DoingLet^(V).

MatchingLet(V) ::=

// Name the state pattern and supply any input
// values..

'match'

n = Name

A = LetArgs

// Specify the action to perform when the
// match is successful...

'in' a = Action

// Specify the action to perform when the
// match fails...

'else' b = Action

{
// Create the matching-let and populate the
// apropriate parts...

let l = MatchingLet(n,a,b)
in @For v in V do
l.getBindings().add(v)
end;
@For a in A do
l.getArgs().add(a)
end;
l
end
}.

DoingLet(V) ::=

// Name the SUT function and supply the values...

'do' n = Name A = LetArgs

// Perform a body for the variables...

'in' a = Action {

// Create the let and populate it...

let l = DoingLet(n,a)
in @For v in V do
l.getBindings().add(v)
end;
@For a in A do
l.getArgs().add(a)
end;
l
end
}.

LetVars ::= '(' LetVarsTail.

LetVarsTail ::=
n = Name ns = (',' Name)* ')' { Seq{n|ns}->collect(n | LetVar(n)) }
| ')' { Seq{} }.

LetArgs ::= '(' LetArgsTail.

LetArgsTail ::=
a = LetArg as = (',' LetArg)* ')' { Seq{a|as} }
| ')' { Seq{} }.

LetArg ::= LetVarRef | LetConst.

LetVarRef ::= n = Name { LetVarRef(n) }.

LetConst ::= LetInt | LetStr.

LetInt ::= i = Int { LetInt(i) }.

LetStr ::= s = Str { LetStr(s) }.

Raise ::= 'raise' n = Name A = LetArgs ';' {
let r = Raise(n)
in @For a in A do
r.getArgs().add(a)
end;
r
end
}.

end

To top.

Performing Test Cases

A test case is performed by supplying it with an SUT state description (in this case the current EMF Library instance) and executing the body of the test case. There are two key aspects to test case execution:  performing the test case actions; and matching SUT states against the state patterns. This section describes how test case actions are performed (making reference to state matching) and the next section defines how states are matched.

A test case is supplied with an SUT state and performs each of the actions with respect to:


The entry point for performing test case actions is defined as follows:
@Operation perform(state)

// This is the entry point from the service...

self.perform(Seq{},state)
end

@Operation perform(env,state)

// Supplied with the variable environment and the SUT
// state. Create a new report, create a state environment
// then call the root action with the arguments...

let report = Report(name,Date())
in self.getReports().add(report);
action.perform(env,self.stateEnv(),state,report,
@Operation(varEnv,state)
varEnv
end)
end
end

@Operation stateEnv()

// Calculate a state environment...

states.asSeq()->iterate(s e = Seq{} | e.bind(s.getName(),s))
end
Each of the action classes provides an implementation of the perform operation. These are given below:


To top.

Matching States

State patterns are used to describe snapshots of an SUT. The pattern consists of a collection of objects. Each object has a type and has slots whose values may be simple values, collections of values or references to other objects. Each slot has a unique identity. A state pattern may contain variables where the variable represents any value.

An SUT state is matched against a state pattern. If the SUT state contains objects of the corresponding types and whose slot values match the those in the pattern then the SUT state matches the pattern. Any variables in the pattern will match any corresponding values in the SUT state and an environment of variable bindings is produced as a side effect of the match.

The EMF state pattern model is shown below:

EMF State Model

The state pattern classes are described below:

  • A State consists of input and output variables and a collection of objects. When a state is matched it is supplied with the input variables. If the pattern matches the SUT state then an environment of variable bindings is produced as a side-effect. The output variables are a sub-set of the variables in the state pattern and are returned to the caller. In order for the state to match, all of the objects in the body of the state pattern must match corresponding objects in the SUT state.
  • An Object has a type (an instance of Path which designates a Java class) and a collection of slots. The name of an object is its identity. Multiple occurrences of the same object identity name must refer to the same object in the SUT state.
  • A Slot has a name and a collection of values. A slot is either a SlotEquals or a SlotContains. The value pattern of a SlotEquals  pattern must match the value in the corresponding SUT slot. The value pattern of a SlotContains pattern must match against one of the elements in the collection found in the SUT slot.
  • Values can be constants (strings, integers or floats), may be sequences (defines with heads and tails) or may be specified as variables. Constants and sequences must match the equivalent SUT element. Variables match any SUT element.

State matching is performed by a match operation defined by State as follows:
    @Operation match(env,succ,fail)

// Supply a collection of objects that must match.
// If all the objects match then invoke the succ continuation
// Otherwise invoke the fail continuation. The supplied
// environment associates variable names with values. The
// initial call to match associates the name 'main' with
// a root SUT element...

self.match(objects.asSeq(),env,succ,fail)
end

@Operation match(objects,env,succ,fail)

// All of the objects must match in order for the
// succ continuation to be called...

@Find(o,objects)

// Get one of the outstanding objects to be matched
// against the SUT...

when env.binds(o.getName())
do
// We have an object pattern o whose name is bound to
// an SUT object in the environment. Match the pattern
// to the SUT object...

o.match(env.lookup(o.getName()),env,

// The success continuation calls match again with
// one less objct pattern to match...

@Operation matchSucc(env,fail)
self.match(objects->excluding(o),env,succ,fail)
end,
fail)
else succ(env,fail)
end
end
Object patterns match SUT objects as follows:
@Operation match(value,env,succ,fail)

// Check that the supplied SUT value is an instance of the
// Java class designated by the path of the object pattern...

if path.getType().isInstance(value)
then self.matchSlots(slots.asSeq(),value,env->bind(name,value),succ,fail)
else fail()
end
end

@Operation matchSlots(slots,value,env,succ,fail)

// All of the slots are tried in turn. Once they have
// all succeeded then the match succeeds...

if slots->isEmpty
then succ(env,fail)
else

// Try a slot...

let slot = slots->head
in
// The SUT value must have the slot...
if value.hasSlot(slot.getName())

then

// Match the slot-pattern value with the SUT object
// value. Pass a success continuation that tries the
// rest of the slots...

slot.match(value.get(slot.getName()),env,
@Operation(env,fail)
self.matchSlots(slots->tail,value,env,succ,fail)
end,
fail)
else fail()
end
end
end
end

SlotEquals just matches the value pattern against the supplied value:
    @Operation match(x,env,succ,fail)
value.match(x,env,succ,fail)
end
SlotContains must try all of the elements in the SUT collection until one successfully matches:
@Operation match(v,env,succ,fail)

// Check that the supplied element is an EList or a sequence of
// elements...

if xmf.javaClass("org.eclipse.emf.common.util.EList").isInstance(v)
then self.matchSequence(v.asSeq(),env,succ,fail)
elseif v.isKindOf(SeqOfElement)
then self.matchSequence(v,env,succ,fail)
else fail()
end
end

@Operation matchSequence(seq,env,succ,fail)

// *ONE* of the elements of the supplied sequence must match
// in order for the success continuation to be invoked...

if seq->isEmpty
then
// No element matched...
fail()

else value.match(seq->head,env,succ,

// If the match ever fails later on then the failure continuation
// will cause another element of the supplied sequence to be
// selected and tried...

@Operation matchSequenceFail()
self.matchSequence(seq->tail,env,succ,fail)
end)
end
end
The constant patterns all have a similar matching process:
 @Operation match(x,env,succ,fail)
if x = value
then succ(env,fail)
else fail()
end
end

To top.

Report Models

When a test case is performed it produces a report that records the action steps (the implementation of perform given above creates and modifies reports). Each report is dated and is added to the test case so that a history of testing is gradually constructed. The EMF report model is shown below:

EMF Report Model
The reporting classes are as follows:

  • A Report consists of a number of steps in order.
  • A Match step records the successful match of a state pattern to an SUT state. The name of the match is that of the state pattern and the ins and outs of the step are the values supplied and produced by the pattern match.
  • An Action step records the invocation of a named SUT function. The arguments supplied to the SUT function are recorded in the step.
  • A MatchFails records the failure of an attempted state pattern match. The name of the state pattern and the supplied arguments are recorded by the step.
  • An Exception step records an exception that was raised by the test script. The name of the exception and its argument values are recorded in the step.

To top.

Producing HTML from Reports

The final testing framework service displays a report in an HTML format. This service uses the code generation facility provided by XMF whereby code templates mix literal HTML output with values generated from the report model. In general, a template has the form:
@HTML(out,0)
some HTML text...
end
where 'out' is an output channel, 0 is supplied as the left-hand indent (in this case it does not matter) and the body of the code template is HTML text. The text in the template body is written to the output channel. Within the body, any XOCL expression within { and } is evaluated. If the expression produces a string then it is written to the output channel. For example,
@HTML(out,0)
<b> { self.name() } </b>
end
embeds the name of the current object.

Furthermore, if [ and ] are encountered within { and } then the text between [ and ] is treated as literal HTML and is written to the output. Therefore,
@HTML(out,0)
<b> { self.name() [ and ] other.name() } </b>
end
joins the two names with the literal text 'and'.

The rest of this section describes the report generation templates for the Report model. The class Report produces HTML as follows:
context Report
@Operation report(out:OutputChannel)
@HTML(out,0)
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<BODY BGCOLOR="#FDF5E6">
<H1 ALIGN=\"CENTER\"> { testCase } </H1>
Generated on { date.toString() }
<BR>

// Set up a table to contain the steps.
// Each step will be a new row in the table...

<TABLE>
{ @For step in self.getSteps() do
step.report(out)
e_nd
}
</TABLE>
</BODY>
</HTML>
end
end
Each step in the report is requested to produce HTML. Note that the @For-loop does not return a string so it produces no output. However, the individual calls of step.report(out) will produce HTML on the supplied output channel.
 A matching step produces a row as follows:
context Match
@Operation report(out:OutputChannel)
@HTML(out,0)
<TR>
<TD> { name } matched. </TD>
<TD> { self.reportValues(ins,out) } </TD>
<TD> { self.reportValues(outs,out) } </TD>
<TD> State </TD>
</TR>
end
end

context Match
  @Operation reportValues(values,out:OutputChannel)
@HTML(out,0)
(
{ @For value in values do
[ { value.value.toString() } ];
if not isLast
then [,]
e_nd
e_nd
}
)
end
end
 The other steps produce HTML in a similar way. The source code for:

Report from performing test case

is as follows:

<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<BODY BGCOLOR="#FDF5E6">
<H1 ALIGN=\"CENTER\"> example1 </H1>
Generated on Thu Dec 13 16:26:13 GMT 2007
<BR>
<TABLE>

hasBook failed to match with args: (
Pride and Predjudice
)

<TR>
<TD> Called addBook </TD>
<TD>
Value(Pride and Predjudice)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>

<TR>
<TD> hasBook matched. </TD>
<TD>
(
Pride and Predjudice
)
</TD>
<TD>
(
Book(Pride and Predjudice)
)
</TD>
<TD> State </TD>
</TR>

<TR>
<TD> Called addCopy </TD>
<TD>
Value(Pride and Predjudice) Value(copy1)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>

<TR>
<TD> hasCopy matched. </TD>
<TD>
(
Book(Pride and Predjudice) , copy1
)
</TD>
<TD>
(
Copy(Book(Pride and Predjudice),copy1)
)
</TD>
<TD> State </TD>
</TR>

<TR>
<TD> Called addReader </TD>
<TD>
Value(fred)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>

<TR>
<TD> hasReader matched. </TD>
<TD>
(
fred
)
</TD>
<TD>
(
Reader(fred)
)
</TD>
<TD> State </TD>
</TR>

<TR>
<TD> Called borrow </TD>
<TD>
Value(fred) Value(copy1)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>

<TR>
<TD> borrows matched. </TD>
<TD>
(
Reader(fred) , Copy(Book(Pride and Predjudice),copy1)
)
</TD>
<TD>
(

)
</TD>
<TD> State </TD>
</TR>

</TABLE>
</BODY>
</HTML>


To top.