This tutorial guides you through creating a "hello world" Trade Federation (Tradefed or TF) test configuration and gives you a hands-on introduction to the TF framework. Starting from a development environment, you will create a simple configuration and add features.
The tutorial presents the test development process as a set of exercises, each consisting of several steps, that demonstrate how to build and gradually refine your configuration. All sample code you need to complete the test configuration is provided, and the title of each exercise is annotated with a letter describing the roles involved in that step:
- D for Developer
- I for Integrator
- R for Test Runner
After completing the tutorial, you will have a functioning TF configuration and understand many important concepts in the TF framework.
Set up Trade Federation
For details on setting up the TF development environment, see Machine Setup. The rest of this tutorial assumes you have a shell open that has been initialized to the TF environment.
For simplicity, this tutorial illustrates adding a configuration and its classes to the TF framework core library. This can be extended to developing modules outside the source tree by compiling the tradefed JAR, then compiling your modules against that JAR.
Create a test class (D)
Lets create a hello world test that just dumps a message to stdout. A tradefed test generally implements the IRemoteTest interface. Here's an implementation for the HelloWorldTest:
package com.android.tradefed.example; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.invoker.TestInformation; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.ITestInvocationListener; import com.android.tradefed.testtype.IRemoteTest; public class HelloWorldTest implements IRemoteTest { @Override public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException { CLog.i("Hello, TF World!"); } }
Save this sample code to
<tree>/tools/tradefederation/core/src/com/android/tradefed/example/HelloWorldTest.java
and rebuild tradefed from your shell:
m -jN
Note that CLog.i
in the example above is used to direct output to the console. More
information on logging in Trade Federation is described in Logging (D, I, R).
If the build does not succeed, consult Machine Setup to ensure you didn't miss a step.
Create a configuration (I)
Trade Federation tests are made executable by creating a Configuration, an XML file that instructs tradefed on which test (or tests) to run, as well as which other modules to execute and in what order.
Lets create a new Configuration for our HelloWorldTest (note the full class name of the HelloWorldTest):
<configuration description="Runs the hello world test"> <test class="com.android.tradefed.example.HelloWorldTest" /> </configuration>
Save this data to a helloworld.xml
file anywhere on your local
filesystem (e.g. /tmp/helloworld.xml
). TF will parse the
Configuration XML file (aka config), load the specified class using
reflection, instantiate it, cast it to a IRemoteTest
, and call its
run
method.
Run the config (R)
From your shell, launch the tradefed console:
tradefed.sh
Ensure a device is connected to the host machine and is visible to tradefed:
tf> list devices Serial State Product Variant Build Battery 004ad9880810a548 Available mako mako JDQ39 100
Configurations can be executed using the run <config>
console command. Try:
tf> run /tmp/helloworld.xml 05-12 13:19:36 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548 Hello, TF World!
You should see "Hello, TF World!" output on the terminal.
You can confirm that a command is done running by using list invocations
or
l i
in the console prompt, and it should print nothing. If commands are currently
running, they display as follows:
tf >l i Command Id Exec Time Device State 10 0m:00 [876X00GNG] running stub on build(s) 'BuildInfo{bid=0, target=stub, serial=876X00GNG}'
Add the config to the classpath (D, I, R)
For convenience of deployment, you can also bundle configs into the tradefed JARs themselves. Tradefed automatically recognizes all configurations placed in config folders on the classpath.
To illustrate, move the helloworld.xml
file into the tradefed
core library
(<tree>/tools/tradefederation/core/res/config/example/helloworld.xml
).
Rebuild tradefed, restart the tradefed console, then ask tradefed to display the
list of configurations from the classpath:
tf> list configs […] example/helloworld: Runs the hello world test
You can now run the helloworld config using:
tf> run example/helloworld 05-12 13:21:21 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548 Hello, TF World!
Interact with a device (D, R)
So far, our HelloWorldTest isn't doing anything interesting. Tradefed's specialty is running tests using Android devices, so lets add an Android device to the test.
Tests can get a reference to an Android device by using TestInformation
, provided
by the framework when the IRemoteTest#run
method is called.
Let's modify the HelloWorldTest print message to display the serial number of the device:
@Override public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException { CLog.i("Hello, TF World! I have device " + testInfo.getDevice().getSerialNumber()); }
Now rebuild tradefed and check the list of devices:
tradefed.sh
tf> list devices Serial State Product Variant Build Battery 004ad9880810a548 Available mako mako JDQ39 100
Take note of the serial number listed as Available; that is the device that should be allocated to HelloWorld:
tf> run example/helloworld 05-12 13:26:18 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548 Hello, TF World! I have device 004ad9880810a548
You should see the new print message displaying the serial number of the device.
Send test results (D)
IRemoteTest
reports results by calling methods on the
ITestInvocationListener
instance provided to the #run
method. The TF framework itself is
responsible for reporting the start (via
ITestInvocationListener#invocationStarted)
and end (via
ITestInvocationListener#invocationEnded)
of each Invocation.
A test run is a logical collection of tests. To report test results,
IRemoteTest
is responsible for reporting the start of a test run,
the start and end of each test, and the end of the test run.
Here's what the HelloWorldTest implementation might look like with a single failed test result.
@Override public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException { CLog.i("Hello, TF World! I have device " + testInfo.getDevice().getSerialNumber()); TestDescription testId = new TestDescription("com.example.TestClassName", "sampleTest"); listener.testRunStarted("helloworldrun", 1); listener.testStarted(testId); listener.testFailed(testId, "oh noes, test failed"); listener.testEnded(testId, Collections.emptyMap()); listener.testRunEnded(0, Collections.emptyMap()); }
TF includes several IRemoteTest
implementations you can reuse
instead of writing your own from scratch. For example,
InstrumentationTest
can run an Android application's tests remotely on an Android device, parse the
results, and forward those results to the ITestInvocationListener
).
For details, see
Test
Types.
Store test results (I)
The default test listener implementation for a TF config is TextResultReporter, which dumps the results of an invocation to stdout. To illustrate, run the HelloWorldTest config from the previous section:
./tradefed.sh
tf> run example/helloworld 04-29 18:25:55 I/TestInvocation: Invocation was started with cmd: /tmp/helloworld.xml 04-29 18:25:55 I/TestInvocation: Starting invocation for 'stub' with '[ BuildInfo{bid=0, target=stub, serial=876X00GNG} on device '876X00GNG'] 04-29 18:25:55 I/HelloWorldTest: Hello, TF World! I have device 876X00GNG 04-29 18:25:55 I/InvocationToJUnitResultForwarder: Running helloworldrun: 1 tests 04-29 18:25:55 W/InvocationToJUnitResultForwarder: Test com.example.TestClassName#sampleTest failed with stack: oh noes, test failed 04-29 18:25:55 I/InvocationToJUnitResultForwarder: Run ended in 0 ms
To store the results of an invocation elsewhere, such as in a file, specify a
custom ITestInvocationListener
implementation using the
result_reporter
tag in your configuration.
TF also includes the
XmlResultReporter
listener, which writes test results to an XML file in a format similar to that
used by the ant JUnit XML writer. To specify the result_reporter in the
configuration, edit the …/res/config/example/helloworld.xml
config:
<configuration description="Runs the hello world test"> <test class="com.android.tradefed.example.HelloWorldTest" /> <result_reporter class="com.android.tradefed.result.XmlResultReporter" /> </configuration>
Now rebuild tradefed and re-run the hello world sample:
tf> run example/helloworld 05-16 21:07:07 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548 Hello, TF World! I have device 004ad9880810a548 05-16 21:07:07 I/XmlResultReporter: Saved device_logcat log to /tmp/0/inv_2991649128735283633/device_logcat_6999997036887173857.txt 05-16 21:07:07 I/XmlResultReporter: Saved host_log log to /tmp/0/inv_2991649128735283633/host_log_6307746032218561704.txt 05-16 21:07:07 I/XmlResultReporter: XML test result file generated at /tmp/0/inv_2991649128735283633/test_result_536358148261684076.xml. Total tests 1, Failed 1, Error 0
Notice the log message stating that an XML file has been generated; the generated file should look like this:
<?xml version='1.0' encoding='UTF-8' ?> <testsuite name="stub" tests="1" failures="1" errors="0" time="9" timestamp="2011-05-17T04:07:07" hostname="localhost"> <properties /> <testcase name="sampleTest" classname="com.example.TestClassName" time="0"> <failure>oh noes, test failed </failure> </testcase> </testsuite>
You can also write your own custom invocation listeners—they simply need to implement the ITestInvocationListener interface.
Tradefed supports multiple invocation listeners, so you can send test results
to multiple independent destinations. To do this, just specify multiple
<result_reporter>
tags in your config.
Logging facilities (D, I, R)
TF's logging facilities include the ability to:
- Capture logs from the device (aka device logcat)
- Record logs from the Trade Federation framework running on the host machine (aka host log)
The TF framework automatically captures the logcat from the allocated device
and sends it to the invocation listener for processing.
XmlResultReporter
then saves the captured device logcat as a file.
TF host logs are reported using the
CLog wrapper
for the ddmlib Log class. Let's convert the
previous System.out.println
call in HelloWorldTest to a
CLog
call:
@Override public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { CLog.i("Hello, TF World! I have device %s", getDevice().getSerialNumber());
CLog
handles string interpolation directly, similar to
String.format
. When you rebuild and rerun TF, you should see the
log message on stdout:
tf> run example/helloworld … 05-16 21:30:46 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548 …
By default, tradefed
outputs host log
messages to stdout. TF also includes a log implementation that writes
messages to a file:
FileLogger.
To add file logging, add a logger
tag to the config, specifying the
full class name of FileLogger
:
<configuration description="Runs the hello world test"> <test class="com.android.tradefed.example.HelloWorldTest" /> <result_reporter class="com.android.tradefed.result.XmlResultReporter" /> <logger class="com.android.tradefed.log.FileLogger" /> </configuration>
Now, rebuild and run the helloworld example again:
tf >run example/helloworld … 05-16 21:38:21 I/XmlResultReporter: Saved device_logcat log to /tmp/0/inv_6390011618174565918/device_logcat_1302097394309452308.txt 05-16 21:38:21 I/XmlResultReporter: Saved host_log log to /tmp/0/inv_6390011618174565918/host_log_4255420317120216614.txt …
The log message indicates the path of the host log, which, when viewed, should contain your HelloWorldTest log message:
more /tmp/0/inv_6390011618174565918/host_log_4255420317120216614.txt
Example output:
… 05-16 21:38:21 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548
Handling options (D, I, R)
Objects loaded from a TF Configuration (aka Configuration objects)
can also receive data from command line arguments through the use of the
@Option
annotation.
To participate, a Configuration object class applies the @Option
annotation to a member field and provides it a unique name. This enables that
member field value to be populated via a command line option (and also
automatically adds that option to the configuration help system).
Note: Not all field types are supported. For a description of supported types, see OptionSetter.
Let's add an @Option
to HelloWorldTest:
@Option(name="my_option", shortName='m', description="this is the option's help text", // always display this option in the default help text importance=Importance.ALWAYS) private String mMyOption = "thisisthedefault";
Next, let's add a log message to display the value of the option in HelloWorldTest so we can demonstrate it was received correctly:
@Override public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { … CLog.logAndDisplay(LogLevel.INFO, "I received option '%s'", mMyOption);
Finally, rebuild TF and run helloworld; you should see a log message with the
my_option
default value:
tf> run example/helloworld … 05-24 18:30:05 I/HelloWorldTest: I received option 'thisisthedefault'
Pass values from the command line
Pass in a value for my_option
; you should see
my_option
populated with that value:
tf> run example/helloworld --my_option foo … 05-24 18:33:44 I/HelloWorldTest: I received option 'foo'
TF configurations also include a help system, which automatically displays
help text for @Option
fields. Try it now, and you should see the
help text for my_option
:
tf> run example/helloworld --help Printing help for only the important options. To see help for all options, use the --help-all flag cmd_options options: --[no-]help display the help text for the most important/critical options. Default: false. --[no-]help-all display the full help text for all options. Default: false. --[no-]loop keep running continuously. Default: false. test options: -m, --my_option this is the option's help text Default: thisisthedefault. 'file' logger options: --log-level-display the minimum log level to display on stdout. Must be one of verbose, debug, info, warn, error, assert. Default: error.
Note the message about "printing only the important options." To reduce
option help clutter, TF uses the Option#importance
attribute to
determine whether to show a particular @Option
field help text when
--help
is specified. --help-all
always shows help for
all @Option
fields, regardless of importance. For details, see
Option.Importance.
Pass values from a configuration
You can also specify an Option value within the config by adding a
<option name="" value="">
element. Test it using
helloworld.xml
:
<test class="com.android.tradefed.example.HelloWorldTest" > <option name="my_option" value="fromxml" /> </test>
Re-building and running helloworld should now produce this output:
05-24 20:38:25 I/HelloWorldTest: I received option 'fromxml'
The configuration help should also update to indicate the default value of
my_option
:
tf> run example/helloworld --help test options: -m, --my_option this is the option's help text Default: fromxml.
Other configuration objects included in the helloworld config, such as
FileLogger
, also accept options. The option
--log-level-display
is interesting because it filters the logs that
show up on stdout. Earlier in the tutorial, you may have noticed the "Hello, TF
World! I have device …' log message stopped being displayed on stdout after we
switched to using FileLogger
. You can increase the verbosity of
logging to stdout by passing in the --log-level-display
arg.
Try this now, and you should see the 'I have device' log message reappear on stdout, in addition to being logged to a file:
tf> run example/helloworld --log-level-display info … 05-24 18:53:50 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548
That's all, folks!
As a reminder, if you're stuck on something, the Trade Federation source code has a lot of useful information that isn't exposed in the documentation. If all else fails, try asking on the android-platform Google Group, with "Trade Federation" in the message subject.