Purpose & status

The censhare Tester allows to run tests scripted in XML and Groovy (and partly Java), simulating a client behaviour. While unit-tests usually concentrate on a single aspect of a product, the censhare Tester is focusing on bigger test scenarios, like load-tests.

What is needed

  • A running censhare server (version 5.x and higher), with an Oracle DB connection and some valid credentials (username/password) to access the server.
  • An XML file that describes the test scenario: what commands to send to the server, and when.
  • Possibly other files that are needed for the test, e.g. a file with assets to import or features to add to the DB.

How to run a test

If the censhare Tester has not been compiled yet, run the following command in a Terminal:

bin/build.sh init release

To run a test, type the following command in the Terminal:

java -jar release/censhare-Tester/censhare-tester.jar my-test-file.xmlCopy

The censhare Tester already comes with a number of tests. You can try the following for instance:

java -DSRV_HOST=localhost -jar release/censhare-Tester/censhare-tester.jar load-test/livequery-many-promises.xmlCopy

If you want to run the test inside eclipse or intelliJ, run the class com.censhare.tester.Tester and give the XML file as a parameter. Refer to your preferred IDE on how to do that.

How to write a test

General structure of a test scenario

A test is described by an XML file with the following structure:

<actions>
    <!-- information about the server: url, user, password. -->
    <server id="server" rmi_url="rmi://dev-load-test:1099/corpus.RMIServer" user="XXX" password="YYY"/>
 
    <!-- actions -->
    <action class="com.censhare.tester.actions.server.load.TouchAssetAction">
        <server idref="server"/>
         <!-- more XML inserted here will provide information to the action on what to do. -->
    </action>
 
    <action class="...">
    </action>
</actions>

  • Line 3, contains the relevant information for the server, i.e. how to access it. The id attribute allows to reference it later in the XML.
  • On lines 6-9, we start an action TouchAssetAction. This action is a Java class of the same name. Line 7 indicates how to connect to the server, by using the information from line 3. The “action” node can contain additional XML nodes which will tell the action what to do.
  • On lines 11-12, more actions are defined.

Parameters

A test can be parametrized by using standard java properties. Each property must be declared inside special <params> element together with default value and option type (types are actually useful only with Groovy scripting). These parameters/properties can be used in a two ways:

  • By plain string replacement of special placeholders in a form @{PROPERTY_NAME}:
    • The replacement is done once, before executing any action
    • The replacement is done in the entire XML, including the attributes
    • This is useful mainly for server declaration
  • As a variable:
    • Useful in groovy action
    • Remember to declare the correct type, unless it is really supposed to be String
    • Shadowed by variable declaration (see later)

<params>
   <param name="SRV_HOST" default="dev-load-test"/>
   <param name="SRV_LOGIN" default="censhare"/>
   <param name="SRV_PSWD" default=""/>
   <param name="PORT_HTTP" default="9000"/>
   <param name="PORT_RMI" default="1099"/>
 
   <param name="TEST_PARAM_QUERY_COUNT" default="10" type="int"/>
   <param name="TEST_PARAM_QLIMIT" default="188" type="int"/>
 </params>
 
 <server id="server" url="http://@{SRV_HOST}:@{PORT_HTTP}/censhare5/client/" user="@{SRV_LOGIN}" password="@{SRV_PSWD}"/>
 <server id="serverrmi" rmi_url="rmi://@{SRV_HOST}:@{PORT_RMI}/corpus.RMIServer" user="@{SRV_LOGIN}" password="@{SRV_PSWD}"/>

Parameters can be set when invoking from the command line:

java -DSRV_HOST=localhost -DTEST_PARAM_LOOP_COUNT=4 -DTEST_PARAM_LOOP_WAIT=1800 -jar release/censhare-Tester/censhare-tester.jar config/cs5-server-load-test/livequery-many-promises.xml

The server node

The server node indicates how to access the server. There are three ways of doing this:

  • RMI access (censhare 4): indicates a configuration name id , a URL rmi_url, and a user and password

    <server id="server1" rmi_url="rmi://dev-load-test:1099/corpus.RMIServer" user="XXX" password="YYY"/>

  • API-Command access (censhare 5): indicate a configuration name id, a URL url, and a user and password

    <server id="server2" url="http://dev-load-test:9000/censhare5/client/" user="XXX" password="YYY"/>

  • Reference to a server configuration: indicates a configuration name idref. The tester will replace the node with the server node that has the same id as given in idref.

    <server idref="server1"/>

Note: Avoid hardcoded hostnames in the server declaration, use parameters instead.

The action node

The action node tells the server which Java class to execute:

<action class="com.censhare.tester.actions.server.load.TouchAssetAction">
    <!-- more XML inserted here will provide information to the action on what to do. -->
</action>

The Java class should sub-class the abstract class com.censhare.tester.actions.AbstractAction, and the executeInternal()(line 4) method should be implemented. The pre- and post- execution methods (line 2-3) can be overwritten as needed. The class also defines some methods to access the XML configuration of the action (lines 11-14), variables (lines 21-24) or the current session (line 17). See the next section below on variables and sessions.

public abstract class AbstractAction implements Action {
    protected void preExecuteInternal() throws Exception {  }
    protected void postExecuteInternal() throws Exception {  }
    protected abstract void executeInternal() throws Exception;
 
   // helper methods
    /** returns the parent directory of the configuration file */
    public File getConfigBase() { ... }
 
   /** returns action node that launched this action. */
    public AXml getConfig() { ... }
 
    /** returns the action node, but where the variables are replaced by their values. */
    public AXml getConfigWithValues() { ... }
 
   /** returns the session in which the current action is running */
    public SessionAction getSessionAction() { ... }
 
    /** Modify or create a variable */
    public void setVariable (String name, Object value) { ... }
 
    /** Returns the value of the variable 'name'. Returns null if no such variable. */
    public Object getVariable (String name) { ... }
}

Additionally class com.censhare.tester.actions.AbstractSshAction establishes an SSH connection in its pre-execution and closes the connection in its post-execution.

Statements

Statements are special actions. There are 3 kinds of statements: SessionAction (to create a session with the server), Loop (to iterate over a set of actions), and VariablesDeclaration. They are defined in the package com.censhare.tester.actions.statements.

SessionAction

This statement is in fact a com.censhare.tester.actions.AbstractAction that is specified as a normal action. It opens a session with the server and keeps it during the execution of the actions. The SessionAction will run its children nodes successively if they are part of: variables, loop, action. Other nodes are ignored (except the server node which holds the url/user/pwd but has no actions). This action needs a server configuration with an API-Command access (see The server node).
Example:

<actions>
  <server id="server" url="http://dev-load-test:9000/censhare5/client/" user="XXX" password="YYY">
  <action class="com.censhare.tester.actions.statements.SessionAction">
    <server idref="server"/>
    <variables> ... </variables>
    <action> ... </action>
    <loop> ... </loop>
    <action> ... </action>
  </action>
</actions>

  • line 2: server configuration
  • line 3: start of a session with the server (referenced in line 4)
  • line 5: declaration of some variables (see below)
  • line 6-8: specify some actions. Line 7 is a loop-action (see below)
  • line 9: end of the session, the connection with the server will be closed.

Variables

The variables node declares a set of global variables that can later be accessed in Java classes or directly in the XML. It is supported by the Java class VariablesDeclaration. Such a node can occur in any action that subclasses AbstractAction, like for instance the SessionAction or Loop statements.

Declaring a variable

Note: This feature is useful only for pure XML tests, without any Groovy.

A variable has a name (string), a type ( long, double, string, list) and a value:

  • long, double and string types: the value is specified as an attribute of the XML node. For long and double, it must be formatted so “new Long/Double(value)” is working in Java.
  • list: the value is specified as the content of the XML node. It is a comma-separated list of values and trimmed with spaces. This means that “my first, my second , term3” will result in “my first” , “my second” , “term3”. It is not possible to have elements with commas inside.

Example: Line 2 defines a long named myVar with an initial value of 10, while lines 3-5 define a list myList with 3 values.

<variables>
    <variable name="myVar" type="long" value="10" />
    <variable name="myList" type="list">
        string1, string2, string3
    </variable>
</variables>

Accessing a variable

You can access the value of a variable in Java by using getVariable(String) (defined in com.censhare.tester.actions.AbstractAction).

Variables are also available in Groovy. Be careful about their type, though - usually they are strings (unlike for parameters, there is no support for declared type). There is, however, hardly any reason to mix these variable and groovy scripting.

public class MyAction extends AbstractAction {
@Override
    protected void executeInternal() throws Exception {
...
Long myVar = getVariable("myVar");
...
}

You can access the value of a variable inside the XML. Use {$varname} to refer to the variable named varname. It works only inside a node’s attributes and it does not work for lists (i.e. the whole list). Be aware: in order for this to work, you need to get the configuration of the action with getConfigWithValues() (defined in com.censhare.tester.actions.AbstractAction)

Examples:

<variables>
    <variable name="myVar" type="long" value="10">
</variables>
<action class="com.censhare.tester.actions.server.load.MyAction">
    <mynode my-attribute="search for id {$myVar}"/>
</action>

public class MyAction extends AbstractAction {
@Override
    protected void executeInternal() throws Exception {
AXml actionXml = getConfigWithValues();
String searchFor = actionXml.find("mynode").getAttr("my-attribute");
// searchFor == "search for id 10"
...
}

Loop

Note: Loops are useful only for pure XML tests. Groovy allows much better flexibility and it should be preferred.

The loop node allows to iterate over a list of actions, in a sequential manner (i.e. like a for-loop), or in parallel. It is supported by the Java class Loop. Such a node can occur in any action that subclasses AbstractAction, like for instance the SessionAction or Loop statements.
The loop has the following attributes (all mandatory unless specified otherwise):

  • loopvar: name of the variable loop, so it can be accessed by the actions of the loop (see Variables).
  • values: the values to iterate over. It can be the name of a variable of type list, or a number in a range of the form “start..end” (inclusive), with start <= end
  • parallel (optional): yes / no . Default to no.
  • delay (optional): delay between two iterations, in milliseconds. For parallel iterations, the loop will wait the given delay before starting the next iteration-thread. Default to 0.

Example 1: Perform the actions 10 times, every 500ms, in a sequential manner.

<loop parallel="no" loopvar="i" values="1..10" delay="500">
    <!-- "i" is a variable that is accessible like any other variable. -->
    <action> ... </action>
    <action> ... </action>
</loop> <!-- "i" is no more accessible -->

Example 2: Perform the actions for each element in a list, and do it in parallel.

<variables>
    <variable name="myterms" type="list">
        term1, term2, term3
    </variable>
</variables>
<loop parallel="yes" loopvar="term" values="myterms">
    <!-- "term" is a variable that contains one element of myterms -->
    <action> ... </action>
    <action> ... </action>
</loop> <!-- "term" is no more accesible -->

API-Command actions

Those actions will work only inside a SessionAction. They are using the API-Command to execute and are available only in censhare 5.

com.censhare.tester.actions.server.load.LiveQueryAction

Creates a LiveQuery. It will be destroyed when the parent SessionAction gets disconnected. It also records all received updates and allows CheckAction to check whether their amount is correct or not.

Example: With the use of a variable (see VariablesDeclaration):

<actions>
    <server id="server" url="http://dev-load-test:9000/censhare5/client/" user="XXX" password="YYY">
    <action class="com.censhare.tester.actions.statements.SessionAction">
        <server idref="server">
        <variables>
            <variable name="search" type="string" value="some text to search" >
        </variables>
        <action class="com.censhare.tester.actions.server.load.LiveQueryAction">
            <query count-rows="false"limit="200" rel-limit="2147483647"  start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
                <condition name="censhare:text.meta" value="{$search}">
            </query>
        </action>
    </action>
</actions>

RMI-oriented actions

Those actions are using the RMI to execute. Some of them will be deprecated once censhare 5 has a stable API-Command. This is a non-exhaustive list, please refer to the Java implementation or the files in the config directory (e.g. config/actions.xml)

com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction

Performs some modifications on assets:

  • Modify assets: search for some assets (via a query), and modify the given feature for all of the returned assets.
  • Delete assets: search for some assets (via a query) and physically delete them.

Nodes inside the action XML:

  • Node query (mandatory): Specifies the assets that will be modified, only one query node can be used per action.
  • Node params (at least 1): Specifies on what to do depending on its attributes:
    • mode (mandatory): modifyFeature, deleteFeature, deleteAssets (last bullet above).
    • featureKey (mandatory if mode != “deleteAssets”): name of the feature to modify/delete. TODO : not all special features are implemented: only asset.description , and features of type String.
    • value (mandatory only for mode=“modifyFeature”): value of the feature. Only strings are handled. Future versions should have a better handling of feature types.
    • append (optional, used only for mode=“modifyFeature”): append to existing value. If equals to date or assetID , will use the current date or the current assetID.

The action will perform the modifications one params-node after the other and then send the updated assets to the server. Exception: with mode=“deleteAssets”, the action will delete the assets immediately and return (ignoring the rest of the ‘params’ nodes).
Sample config (note line 1: we name the server configuration serverrmi in order to make a distinction with possible API-Command access configurations):

<server id="serverrmi" rmi_url="rmi://dev-load-test:1099/corpus.RMIServer" user="XXX" password="YYY">
   ...
   <!-- modify the assets: -->
   <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
       <server idref="serverrmi"/>
       <params mode="modifyFeature" featureKey="loadtest:f1" value="some new value" >
       <query count-rows="true" expand-limit="1000" full="10" limit="10" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
           <condition name="censhare:asset.domain" value="root.*">
       </query>
   </action>
 
   <!-- delete a feature: -->
   <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
       <server idref="serverrmi">
       <params mode="deleteFeature" featureKey="loadtest:f1" limit="10" >
       <query count-rows="true" expand-limit="1000" full="10" limit="10" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
           <and>
               <condition name="loadtest:copied-from" value="notnull">
               <condition name="censhare:asset.domain" value="root.*">
           </and>
       </query>
   </action>
 
   <!-- modify the assets, append the date to the feature -->
   <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
       <server idref="serverrmi">
       <params mode="modifyFeature" featureKey="loadtest:f1" value="Touched by Tester at " append="date" >
       <query count-rows="true" expand-limit="1000" full="10" limit="10" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
           <condition name="censhare:asset.domain" value="root.*">
       </query>
   </action>
 
   <!-- finished, remove the assets: -->
   <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
       <server idref="serverrmi">
       <params mode="deleteAssets">
       <query count-rows="true" expand-limit="1000" full="10" limit="10" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
           <and>
               <condition name="loadtest:copied-from" value="notnull">
           </and>
       </query>
   </action>

Other actions

All classes are in the package:

  • PrintLiveQueryStatisticsAction: Prints the LiveQuery statistics with the logger. You should also call ResetLiveQueryStatisticsAction to have proper stats.
  • ResetLiveQueryStatisticsAction: Resets the LiveQuery statistics.
  • WaitLiveQueriesAreUpToDate: It will ask every 2 seconds for the list of pending queries. Stop if all are UpToDate or Cancelled.
  • TermQueryAction: Execute fulltext queries with a list of terms on the target server. See some sample config in the javadoc.
  • TouchAssetAction: Execute asset updates on a random asset from a query. See some sample config in the javadoc.

More actions can be found in the package com.censhare.tester.actions.server:

  • AdminClientBatchAction
  • AssetImportAction
  • CreateDatabaseAction
  • DropDatabaseAction
  • InstallServerAction
  • MetaImportAction
  • RemoveServerAction
  • ServerTestAction
  • StartServerAction
  • StopServerAction
  • WaitTasksAction

Actions with no connection to the server

Those command don’t use a connection to the server and can be used outside a SessionAction

com.censhare.tester.actions.server.load.Wait

Waits a number of milliseconds. Sample config for waiting 100 ms:

<action class="com.censhare.tester.actions.server.load.WaitAction">
    <pause ms="100"/>
</action>

Check action

This special action allows (in cooperation with other actions) tests to actually test something by checking the expected behaviour. It defines what should be checked at a given place and records the results.

The checks themselves are not actually executed by this action; they must be implemented in some other action, executed somewhere before the check. CheckAction just calls the callbacks of these actions, registers and passes them to its configuration.
A typical example (and at this moment, the only supported check) is LiveQueryAction which counts the received notifications from a promise and allows them to be checked

<action class="com.censhare.tester.actions.statements.CheckAction">
    <liveQuery>
        <!-- each query is filled exactly once, up to the limit -->
        <count kind="ADDITION" sum="@{TEST_PARAM_QLIMIT}"/>
        <!-- and then updated once -->
        <count kind="UPDATE" sum="@{TEST_PARAM_QLIMIT}"/>
    </liveQuery>
</action>

Implementation note: A check implementation should return a list of results (which is then added to the global results), but it can also just throw an AssertionError - a result of type ERROR is automatically added.

The results of all checks are printed at the end of the entire test run, in plaintext form.

Test examples

Creates 100 LiveQueries and perform some database updates. This test concentrates on the time it takes to send many updates.

<actions>
    <server id="server" url="http://dev-load-test:9000/censhare5/client/" user="XXX" password="WWW"/>
    <server id="serverrmi" rmi_url="rmi://dev-load-test:1099/corpus.RMIServer" user="XXX" password="YYY"/>
 
    <!-- setup: add a new feature to the assets (will query on that) -->
    <action class="com.censhare.tester.actions.server.MetaImportAction">
        <server idref="serverrmi"/>
        <import file="load-test-features.xml"/>
    </action>
 
    <!-- setup: the assets we'll be working on. -->
    <action class="com.censhare.tester.actions.server.AssetImportAction">
        <server idref="serverrmi"/>
        <import file="loadtest.censhare-assets"/>
    </action>
 
 
    <action class="com.censhare.tester.actions.server.load.ResetLiveQueryStatisticsAction">
        <server idref="serverrmi"/>
    </action>
 
    <!-- the tests: -->
    <action class="com.censhare.tester.actions.statements.SessionAction" ignore-failure="true">
        <server idref="server"/>
        <variables>
            <variable name="search" type="string" value="manyLiveQueries" />
        </variables>
 
        <!-- live queries will receive empty results at first. -->
        <loop loopvar="i" values="1..100" parallel="yes" delay="100">
            <action class="com.censhare.tester.actions.server.load.LiveQueryAction">
                <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
                    <condition name="censhare:text.meta" value="{$search}"/>
                </query>
            </action>
        </loop>
 
        <!-- Regularly changes a feature: add one, delete one (on all the assets). Updates will be sent to the live queries.-->
        <loop loopvar="i" values="1..100" delay="500">
            <!-- add a feature to the assets so they appear in the query. -->
            <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
                <server idref="serverrmi"/>
                <params mode="modifyFeature" featureKey="censhare:asset.description" value="{$search}" appendValue="{$i}"/>
                <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
                    <condition name="loadtest:copied-from" op="NOTNULL" value=""/>
                </query>
            </action>
 
            <action class="com.censhare.tester.actions.server.load.WaitAction">
                <pause ms="500" />
            </action>
 
            <!-- delete the feature to the assets so they are not part of the results. -->
            <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
                <server idref="serverrmi"/>
                <params mode="deleteFeature" featureKey="censhare:asset.description"/>
                <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
                    <condition name="loadtest:copied-from" op="NOTNULL" value=""/>
                </query>
            </action>
        </loop>
 
        <!-- wait for live queries to be all up-to-date. -->
        <action class="com.censhare.tester.actions.server.load.WaitLiveQueriesAreUpToDate">
            <server idref="serverrmi"/>
        </action>
    </action><!-- closing the sessionAction will cancel the promises and disconnect. -->
 
    <!-- tear-down: delete the assets we've been working on. -->
    <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
        <server idref="serverrmi"/>
        <params mode="deleteAssets"/>
        <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
            <condition name="loadtest:copied-from" op="NOTNULL" value=""/>
        </query>
    </action>
 
    <!-- gather the stats and print them. Print all, but in our case we're interested by the update-time only. -->
    <action class="com.censhare.tester.actions.server.load.PrintLiveQueryStatisticsAction">
        <server idref="serverrmi"/>
    </action>
</actions>

The following test creates a LiveQuery that will receive a long list of documents. The structure is somewhat similar to the previous test.

<actions>
    <server id="server" url="http://dev-load-test:9000/censhare5/client/" user="XXX" password="YYY"/>
    <server id="serverrmi" rmi_url="rmi://dev-load-test:1099/corpus.RMIServer" user="XXX" password="YYY"/>
 
    <!-- setup: add a new feature to the assets (will query on that) -->
    <action class="com.censhare.tester.actions.server.MetaImportAction">
        <server idref="serverrmi"/>
        <import file="load-test-features.xml"/>
    </action>
 
    <!-- setup: the assets we'll be working on. -->
    <action class="com.censhare.tester.actions.server.AssetImportAction">
        <server idref="serverrmi"/>
        <import file="loadtest.censhare-assets"/>
    </action>
 
    <action class="com.censhare.tester.actions.server.load.ResetLiveQueryStatisticsAction">
        <server idref="serverrmi"/>
    </action>
 
    <!-- the tests: -->
    <action class="com.censhare.tester.actions.statements.SessionAction" ignore-failure="true">
        <server idref="server"/>
        <variables>
            <variable name="search" type="string" value="manyLiveQueries" />
        </variables>
 
        <!--get some assets. -->
        <action class="com.censhare.tester.actions.server.load.LiveQueryAction">
            <query count-rows="false" limit="1000" rel-limit="2147483647" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
            </query>
        </action>
 
        <!-- Regularly changes a feature: add one, delete one (on all the assets). Updates will be sent to the live queries.-->
        <loop loopvar="i" values="1..100" delay="500">
            <!-- add a feature to the assets so they appear in the query. -->
            <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
                <server idref="serverrmi"/>
                <params mode="modifyFeature" featureKey="censhare:asset.description" value="{$search}" appendValue="{$i}"/>
                <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
                    <condition name="loadtest:copied-from" op="NOTNULL" value=""/>
                </query>
            </action>
 
            <action class="com.censhare.tester.actions.server.load.WaitAction">
                <pause ms="500" />
            </action>
 
            <!-- delete the feature to the assets so they are not part of the results. -->
            <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
                <server idref="serverrmi"/>
                <params mode="deleteFeature" featureKey="censhare:asset.description"/>
                <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
                    <condition name="loadtest:copied-from" op="NOTNULL" value=""/>
                </query>
            </action>
        </loop>
 
        <!-- wait for live queries to be all up-to-date. -->
        <action class="com.censhare.tester.actions.server.load.WaitLiveQueriesAreUpToDate">
            <server idref="serverrmi"/>
        </action>
    </action><!-- closing the sessionAction will cancel the promises and disconnect. -->
 
    <!-- tear-down: delete the assets we've been working on. -->
    <action class="com.censhare.tester.actions.server.load.ModifyLoadTestAssetAction">
        <server idref="serverrmi"/>
        <params mode="deleteAssets"/>
        <query count-rows="false" expand-limit="1000" full="200" limit="200" rel-limit="2147483647" start-full="0" start-row="0" type="asset" xmlns:corpus="http://www.censhare.com/xml/3.0.0/corpus">
            <condition name="loadtest:copied-from" op="NOTNULL" value=""/>
        </query>
    </action>
 
    <!-- gather the stats and print them. Print all, but in our case we're interested by the update-time only. -->
    <action class="com.censhare.tester.actions.server.load.PrintLiveQueryStatisticsAction">
        <server idref="serverrmi"/>
    </action>
</actions>