Home | History | Annotate | Download | only in suite
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.tradefed.result.suite;
     17 
     18 import com.android.ddmlib.testrunner.TestResult.TestStatus;
     19 import com.android.tradefed.invoker.IInvocationContext;
     20 import com.android.tradefed.invoker.InvocationContext;
     21 import com.android.tradefed.log.LogUtil.CLog;
     22 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
     23 import com.android.tradefed.result.LogDataType;
     24 import com.android.tradefed.result.LogFile;
     25 import com.android.tradefed.result.TestDescription;
     26 import com.android.tradefed.result.TestResult;
     27 import com.android.tradefed.result.TestRunResult;
     28 import com.android.tradefed.testtype.Abi;
     29 import com.android.tradefed.testtype.IAbi;
     30 import com.android.tradefed.testtype.suite.TestFailureListener;
     31 import com.android.tradefed.util.AbiUtils;
     32 import com.android.tradefed.util.StreamUtil;
     33 
     34 import com.google.common.base.Joiner;
     35 import com.google.common.base.Strings;
     36 
     37 import org.xmlpull.v1.XmlPullParser;
     38 import org.xmlpull.v1.XmlPullParserException;
     39 import org.xmlpull.v1.XmlPullParserFactory;
     40 import org.xmlpull.v1.XmlSerializer;
     41 
     42 import java.io.File;
     43 import java.io.FileOutputStream;
     44 import java.io.FileReader;
     45 import java.io.IOException;
     46 import java.io.OutputStream;
     47 import java.net.InetAddress;
     48 import java.net.UnknownHostException;
     49 import java.text.SimpleDateFormat;
     50 import java.util.ArrayList;
     51 import java.util.Arrays;
     52 import java.util.Collection;
     53 import java.util.Date;
     54 import java.util.HashMap;
     55 import java.util.LinkedHashMap;
     56 import java.util.List;
     57 import java.util.Map;
     58 import java.util.Map.Entry;
     59 
     60 /**
     61  * Utility class to save a suite run as an XML. TODO: Remove all the special Compatibility Test
     62  * format work around to get the same format.
     63  */
     64 public class XmlSuiteResultFormatter implements IFormatterGenerator {
     65 
     66     private static final String ENCODING = "UTF-8";
     67     private static final String TYPE = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer";
     68     public static final String NS = null;
     69 
     70     public static final String TEST_RESULT_FILE_NAME = "test_result_suite.xml";
     71 
     72     // XML constants
     73     private static final String ABI_ATTR = "abi";
     74     private static final String BUGREPORT_TAG = "BugReport";
     75     private static final String BUILD_TAG = "Build";
     76     private static final String CASE_TAG = "TestCase";
     77     private static final String COMMAND_LINE_ARGS = "command_line_args";
     78     private static final String DEVICES_ATTR = "devices";
     79     private static final String DONE_ATTR = "done";
     80     private static final String END_DISPLAY_TIME_ATTR = "end_display";
     81     private static final String END_TIME_ATTR = "end";
     82     private static final String FAILED_ATTR = "failed";
     83     private static final String FAILURE_TAG = "Failure";
     84     private static final String HOST_NAME_ATTR = "host_name";
     85     private static final String JAVA_VENDOR_ATTR = "java_vendor";
     86     private static final String JAVA_VERSION_ATTR = "java_version";
     87     private static final String LOGCAT_TAG = "Logcat";
     88 
     89     private static final String METRIC_TAG = "Metric";
     90     private static final String MESSAGE_ATTR = "message";
     91     private static final String MODULE_TAG = "Module";
     92     private static final String MODULES_DONE_ATTR = "modules_done";
     93     private static final String MODULES_TOTAL_ATTR = "modules_total";
     94     private static final String NAME_ATTR = "name";
     95     private static final String OS_ARCH_ATTR = "os_arch";
     96     private static final String OS_NAME_ATTR = "os_name";
     97     private static final String OS_VERSION_ATTR = "os_version";
     98     private static final String PASS_ATTR = "pass";
     99 
    100     private static final String RESULT_ATTR = "result";
    101     private static final String RESULT_TAG = "Result";
    102     private static final String RUNTIME_ATTR = "runtime";
    103     private static final String SCREENSHOT_TAG = "Screenshot";
    104     private static final String SKIPPED_ATTR = "skipped";
    105     private static final String STACK_TAG = "StackTrace";
    106     private static final String START_DISPLAY_TIME_ATTR = "start_display";
    107     private static final String START_TIME_ATTR = "start";
    108 
    109     private static final String SUMMARY_TAG = "Summary";
    110     private static final String TEST_TAG = "Test";
    111     private static final String TOTAL_TESTS_ATTR = "total_tests";
    112 
    113     private static final String LOG_FILE_NAME_ATTR = "file_name";
    114 
    115     /**
    116      * Allows to add some attributes to the <Result> tag via {@code serializer.attribute}.
    117      *
    118      * @param serializer
    119      * @throws IOException
    120      */
    121     public void addSuiteAttributes(XmlSerializer serializer)
    122             throws IllegalArgumentException, IllegalStateException, IOException {
    123         // Default implementation does nothing
    124     }
    125 
    126     /**
    127      * Reverse operation from {@link #addSuiteAttributes(XmlSerializer)}.
    128      *
    129      * @param parser The parser where to read the attributes from.
    130      * @param context The {@link IInvocationContext} where to put the attributes.
    131      * @throws XmlPullParserException
    132      */
    133     public void parseSuiteAttributes(XmlPullParser parser, IInvocationContext context)
    134             throws XmlPullParserException {
    135         // Default implementation does nothing
    136     }
    137 
    138     /**
    139      * Allows to add some attributes to the <Build> tag via {@code serializer.attribute}.
    140      *
    141      * @param serializer
    142      * @param holder
    143      * @throws IllegalArgumentException
    144      * @throws IllegalStateException
    145      * @throws IOException
    146      */
    147     public void addBuildInfoAttributes(XmlSerializer serializer, SuiteResultHolder holder)
    148             throws IllegalArgumentException, IllegalStateException, IOException {
    149         // Default implementation does nothing
    150     }
    151 
    152     /**
    153      * Reverse operation from {@link #addBuildInfoAttributes(XmlSerializer, SuiteResultHolder)}.
    154      *
    155      * @param parser The parser where to read the attributes from.
    156      * @param context The {@link IInvocationContext} where to put the attributes.
    157      * @throws XmlPullParserException
    158      */
    159     public void parseBuildInfoAttributes(XmlPullParser parser, IInvocationContext context)
    160             throws XmlPullParserException {
    161         // Default implementation does nothing
    162     }
    163 
    164     /**
    165      * Write the invocation results in an xml format.
    166      *
    167      * @param holder a {@link SuiteResultHolder} holding all the info required for the xml
    168      * @param resultDir the result directory {@link File} where to put the results.
    169      * @return a {@link File} pointing to the xml output file.
    170      */
    171     @Override
    172     public File writeResults(SuiteResultHolder holder, File resultDir) throws IOException {
    173         File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
    174         OutputStream stream = new FileOutputStream(resultFile);
    175         XmlSerializer serializer = null;
    176         try {
    177             serializer = XmlPullParserFactory.newInstance(TYPE, null).newSerializer();
    178         } catch (XmlPullParserException e) {
    179             StreamUtil.close(stream);
    180             throw new IOException(e);
    181         }
    182         serializer.setOutput(stream, ENCODING);
    183         serializer.startDocument(ENCODING, false);
    184         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    185         serializer.processingInstruction(
    186                 "xml-stylesheet type=\"text/xsl\" href=\"compatibility_result.xsl\"");
    187         serializer.startTag(NS, RESULT_TAG);
    188         serializer.attribute(NS, START_TIME_ATTR, String.valueOf(holder.startTime));
    189         serializer.attribute(NS, END_TIME_ATTR, String.valueOf(holder.endTime));
    190         serializer.attribute(NS, START_DISPLAY_TIME_ATTR, toReadableDateString(holder.startTime));
    191         serializer.attribute(NS, END_DISPLAY_TIME_ATTR, toReadableDateString(holder.endTime));
    192         serializer.attribute(
    193                 NS,
    194                 COMMAND_LINE_ARGS,
    195                 Strings.nullToEmpty(
    196                         holder.context.getAttributes().getUniqueMap().get(COMMAND_LINE_ARGS)));
    197 
    198         addSuiteAttributes(serializer);
    199 
    200         // Device Info
    201         Map<Integer, List<String>> serialsShards = holder.context.getShardsSerials();
    202         String deviceList = "";
    203         if (serialsShards.isEmpty()) {
    204             deviceList = Joiner.on(",").join(holder.context.getSerials());
    205         } else {
    206             for (List<String> list : serialsShards.values()) {
    207                 deviceList += Joiner.on(",").join(list);
    208             }
    209         }
    210         serializer.attribute(NS, DEVICES_ATTR, deviceList);
    211 
    212         // Host Info
    213         String hostName = "";
    214         try {
    215             hostName = InetAddress.getLocalHost().getHostName();
    216         } catch (UnknownHostException ignored) {
    217         }
    218         serializer.attribute(NS, HOST_NAME_ATTR, hostName);
    219         serializer.attribute(NS, OS_NAME_ATTR, System.getProperty("os.name"));
    220         serializer.attribute(NS, OS_VERSION_ATTR, System.getProperty("os.version"));
    221         serializer.attribute(NS, OS_ARCH_ATTR, System.getProperty("os.arch"));
    222         serializer.attribute(NS, JAVA_VENDOR_ATTR, System.getProperty("java.vendor"));
    223         serializer.attribute(NS, JAVA_VERSION_ATTR, System.getProperty("java.version"));
    224 
    225         // Build Info
    226         serializer.startTag(NS, BUILD_TAG);
    227         for (String key : holder.context.getAttributes().keySet()) {
    228             serializer.attribute(
    229                     NS, key, Joiner.on(",").join(holder.context.getAttributes().get(key)));
    230         }
    231         addBuildInfoAttributes(serializer, holder);
    232         serializer.endTag(NS, BUILD_TAG);
    233 
    234         // Summary
    235         serializer.startTag(NS, SUMMARY_TAG);
    236         serializer.attribute(NS, PASS_ATTR, Long.toString(holder.passedTests));
    237         serializer.attribute(NS, FAILED_ATTR, Long.toString(holder.failedTests));
    238         serializer.attribute(NS, MODULES_DONE_ATTR, Integer.toString(holder.completeModules));
    239         serializer.attribute(NS, MODULES_TOTAL_ATTR, Integer.toString(holder.totalModules));
    240         serializer.endTag(NS, SUMMARY_TAG);
    241 
    242         // Results
    243         for (TestRunResult module : holder.runResults) {
    244             serializer.startTag(NS, MODULE_TAG);
    245             // To be compatible of CTS strip the abi from the module name when available.
    246             if (holder.modulesAbi.get(module.getName()) != null) {
    247                 String moduleAbi = holder.modulesAbi.get(module.getName()).getName();
    248                 String moduleNameStripped = module.getName().replace(moduleAbi + " ", "");
    249                 serializer.attribute(NS, NAME_ATTR, moduleNameStripped);
    250                 serializer.attribute(NS, ABI_ATTR, moduleAbi);
    251             } else {
    252                 serializer.attribute(NS, NAME_ATTR, module.getName());
    253             }
    254             serializer.attribute(NS, RUNTIME_ATTR, String.valueOf(module.getElapsedTime()));
    255             serializer.attribute(NS, DONE_ATTR, Boolean.toString(module.isRunComplete()));
    256             serializer.attribute(
    257                     NS, PASS_ATTR, Integer.toString(module.getNumTestsInState(TestStatus.PASSED)));
    258             serializer.attribute(NS, TOTAL_TESTS_ATTR, Integer.toString(module.getNumTests()));
    259 
    260             serializeTestCases(serializer, module.getTestResults());
    261             serializer.endTag(NS, MODULE_TAG);
    262         }
    263         serializer.endDocument();
    264         return resultFile;
    265     }
    266 
    267     private static void serializeTestCases(
    268             XmlSerializer serializer, Map<TestDescription, TestResult> results)
    269             throws IllegalArgumentException, IllegalStateException, IOException {
    270         // We reformat into the same format as the ResultHandler from CTS to be compatible for now.
    271         Map<String, Map<String, TestResult>> format = new LinkedHashMap<>();
    272         for (Entry<TestDescription, TestResult> cr : results.entrySet()) {
    273             if (format.get(cr.getKey().getClassName()) == null) {
    274                 format.put(cr.getKey().getClassName(), new LinkedHashMap<>());
    275             }
    276             Map<String, TestResult> methodResult = format.get(cr.getKey().getClassName());
    277             methodResult.put(cr.getKey().getTestName(), cr.getValue());
    278         }
    279 
    280         for (String className : format.keySet()) {
    281             serializer.startTag(NS, CASE_TAG);
    282             serializer.attribute(NS, NAME_ATTR, className);
    283             for (Entry<String, TestResult> individualResult : format.get(className).entrySet()) {
    284                 TestStatus status = individualResult.getValue().getStatus();
    285                 if (status == null) {
    286                     continue; // test was not executed, don't report
    287                 }
    288                 serializer.startTag(NS, TEST_TAG);
    289                 serializer.attribute(NS, RESULT_ATTR, getTestStatusCompatibilityString(status));
    290                 serializer.attribute(NS, NAME_ATTR, individualResult.getKey());
    291                 if (TestStatus.IGNORED.equals(status)) {
    292                     serializer.attribute(NS, SKIPPED_ATTR, Boolean.toString(true));
    293                 }
    294 
    295                 handleTestFailure(serializer, individualResult.getValue().getStackTrace());
    296 
    297                 HandleLoggedFiles(serializer, individualResult);
    298 
    299                 for (Entry<String, String> metric :
    300                         individualResult.getValue().getMetrics().entrySet()) {
    301                     serializer.startTag(NS, METRIC_TAG);
    302                     serializer.attribute(NS, metric.getKey(), metric.getValue());
    303                     serializer.endTag(NS, METRIC_TAG);
    304                 }
    305                 serializer.endTag(NS, TEST_TAG);
    306             }
    307             serializer.endTag(NS, CASE_TAG);
    308         }
    309     }
    310 
    311     private static void handleTestFailure(XmlSerializer serializer, String fullStack)
    312             throws IllegalArgumentException, IllegalStateException, IOException {
    313         if (fullStack != null) {
    314             String message;
    315             int index = fullStack.indexOf('\n');
    316             if (index < 0) {
    317                 // Trace is a single line, just set the message to be the same as the stacktrace.
    318                 message = fullStack;
    319             } else {
    320                 message = fullStack.substring(0, index);
    321             }
    322             serializer.startTag(NS, FAILURE_TAG);
    323 
    324             serializer.attribute(NS, MESSAGE_ATTR, message);
    325             serializer.startTag(NS, STACK_TAG);
    326             serializer.text(fullStack);
    327             serializer.endTag(NS, STACK_TAG);
    328 
    329             serializer.endTag(NS, FAILURE_TAG);
    330         }
    331     }
    332 
    333     /** Add files captured by {@link TestFailureListener} on test failures. */
    334     private static void HandleLoggedFiles(
    335             XmlSerializer serializer, Entry<String, TestResult> testResult)
    336             throws IllegalArgumentException, IllegalStateException, IOException {
    337         Map<String, LogFile> loggedFiles = testResult.getValue().getLoggedFiles();
    338         if (loggedFiles == null || loggedFiles.isEmpty()) {
    339             return;
    340         }
    341         for (String key : loggedFiles.keySet()) {
    342             switch (loggedFiles.get(key).getType()) {
    343                 case BUGREPORT:
    344                     serializer.startTag(NS, BUGREPORT_TAG);
    345                     serializer.attribute(NS, LOG_FILE_NAME_ATTR, key);
    346                     serializer.text(loggedFiles.get(key).getUrl());
    347                     serializer.endTag(NS, BUGREPORT_TAG);
    348                     break;
    349                 case LOGCAT:
    350                     serializer.startTag(NS, LOGCAT_TAG);
    351                     serializer.attribute(NS, LOG_FILE_NAME_ATTR, key);
    352                     serializer.text(loggedFiles.get(key).getUrl());
    353                     serializer.endTag(NS, LOGCAT_TAG);
    354                     break;
    355                 case PNG:
    356                 case JPEG:
    357                     serializer.startTag(NS, SCREENSHOT_TAG);
    358                     serializer.attribute(NS, LOG_FILE_NAME_ATTR, key);
    359                     serializer.text(loggedFiles.get(key).getUrl());
    360                     serializer.endTag(NS, SCREENSHOT_TAG);
    361                     break;
    362                 default:
    363                     break;
    364             }
    365         }
    366     }
    367 
    368     /**
    369      * Return the given time as a {@link String} suitable for displaying.
    370      *
    371      * <p>Example: Fri Aug 20 15:13:03 PDT 2010
    372      *
    373      * @param time the epoch time in ms since midnight Jan 1, 1970
    374      */
    375     private static String toReadableDateString(long time) {
    376         SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
    377         return dateFormat.format(new Date(time));
    378     }
    379 
    380     /** Convert our test status to a format compatible with CTS backend. */
    381     private static String getTestStatusCompatibilityString(TestStatus status) {
    382         switch (status) {
    383             case PASSED:
    384                 return "pass";
    385             case FAILURE:
    386                 return "fail";
    387             default:
    388                 return status.toString();
    389         }
    390     }
    391 
    392     /** {@inheritDoc} */
    393     @Override
    394     public SuiteResultHolder parseResults(File resultDir) throws IOException {
    395         File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
    396         if (!resultFile.exists()) {
    397             CLog.d("Could not find %s for loading the results.", resultFile.getAbsolutePath());
    398             return null;
    399         }
    400         SuiteResultHolder invocation = new SuiteResultHolder();
    401         IInvocationContext context = new InvocationContext();
    402         try {
    403             XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
    404             XmlPullParser parser = factory.newPullParser();
    405             parser.setInput(new FileReader(resultFile));
    406 
    407             parser.nextTag();
    408             parser.require(XmlPullParser.START_TAG, NS, RESULT_TAG);
    409             invocation.startTime = (Long.valueOf(parser.getAttributeValue(NS, START_TIME_ATTR)));
    410             invocation.endTime = (Long.valueOf(parser.getAttributeValue(NS, END_TIME_ATTR)));
    411             context.addInvocationAttribute(
    412                     COMMAND_LINE_ARGS, parser.getAttributeValue(NS, COMMAND_LINE_ARGS));
    413             parseSuiteAttributes(parser, context);
    414 
    415             String deviceList = parser.getAttributeValue(NS, DEVICES_ATTR);
    416             int i = 0;
    417             // TODO: Fix to correctly handle the number of device per shard.
    418             for (String device : deviceList.split(",")) {
    419                 context.addSerialsFromShard(i, Arrays.asList(device));
    420                 i++;
    421             }
    422 
    423             parser.nextTag();
    424             parser.require(XmlPullParser.START_TAG, NS, BUILD_TAG);
    425 
    426             for (int index = 0; index < parser.getAttributeCount(); index++) {
    427                 String key = parser.getAttributeName(i);
    428                 String value = parser.getAttributeValue(NS, key);
    429                 // TODO: Handle list of values that are comma separated.
    430                 context.addInvocationAttribute(key, value);
    431             }
    432             parseBuildInfoAttributes(parser, context);
    433 
    434             parser.nextTag();
    435             parser.require(XmlPullParser.END_TAG, NS, BUILD_TAG);
    436 
    437             parser.nextTag();
    438             parser.require(XmlPullParser.START_TAG, NS, SUMMARY_TAG);
    439 
    440             invocation.completeModules =
    441                     Integer.parseInt(parser.getAttributeValue(NS, MODULES_DONE_ATTR));
    442             invocation.totalModules =
    443                     Integer.parseInt(parser.getAttributeValue(NS, MODULES_TOTAL_ATTR));
    444             invocation.passedTests = Integer.parseInt(parser.getAttributeValue(NS, PASS_ATTR));
    445             invocation.failedTests = Integer.parseInt(parser.getAttributeValue(NS, FAILED_ATTR));
    446 
    447             parser.nextTag();
    448             parser.require(XmlPullParser.END_TAG, NS, SUMMARY_TAG);
    449 
    450             Collection<TestRunResult> results = new ArrayList<>();
    451             Map<String, IAbi> moduleAbis = new HashMap<>();
    452             // Module level information parsing
    453             handleModuleLevel(parser, results, moduleAbis);
    454             parser.require(XmlPullParser.END_TAG, NS, RESULT_TAG);
    455             invocation.runResults = results;
    456             invocation.modulesAbi = moduleAbis;
    457         } catch (XmlPullParserException e) {
    458             CLog.e(e);
    459             return null;
    460         }
    461 
    462         invocation.context = context;
    463         return invocation;
    464     }
    465 
    466     /**
    467      * Handle the parsing and replay of all the information inside a module (class, method,
    468      * failures).
    469      */
    470     private void handleModuleLevel(
    471             XmlPullParser parser, Collection<TestRunResult> results, Map<String, IAbi> moduleAbis)
    472             throws IOException, XmlPullParserException {
    473         while (parser.nextTag() == XmlPullParser.START_TAG) {
    474             parser.require(XmlPullParser.START_TAG, NS, MODULE_TAG);
    475             TestRunResult module = new TestRunResult();
    476             results.add(module);
    477             String name = parser.getAttributeValue(NS, NAME_ATTR);
    478             String abi = parser.getAttributeValue(NS, ABI_ATTR);
    479             String moduleId = name;
    480             if (abi != null) {
    481                 moduleId = AbiUtils.createId(abi, name);
    482                 moduleAbis.put(moduleId, new Abi(abi, AbiUtils.getBitness(abi)));
    483             }
    484             long moduleElapsedTime = Long.parseLong(parser.getAttributeValue(NS, RUNTIME_ATTR));
    485             boolean moduleDone = Boolean.parseBoolean(parser.getAttributeValue(NS, DONE_ATTR));
    486             int totalTests = Integer.parseInt(parser.getAttributeValue(NS, TOTAL_TESTS_ATTR));
    487             module.testRunStarted(moduleId, totalTests);
    488             // TestCase level information parsing
    489             while (parser.nextTag() == XmlPullParser.START_TAG) {
    490                 parser.require(XmlPullParser.START_TAG, NS, CASE_TAG);
    491                 String className = parser.getAttributeValue(NS, NAME_ATTR);
    492                 // Test level information parsing
    493                 handleTestCaseLevel(parser, module, className);
    494                 parser.require(XmlPullParser.END_TAG, NS, CASE_TAG);
    495             }
    496             module.testRunEnded(moduleElapsedTime, new HashMap<String, Metric>());
    497             module.setRunComplete(moduleDone);
    498             parser.require(XmlPullParser.END_TAG, NS, MODULE_TAG);
    499         }
    500     }
    501 
    502     /** Parse and replay all the individual test cases level (method) informations. */
    503     private void handleTestCaseLevel(
    504             XmlPullParser parser, TestRunResult currentModule, String className)
    505             throws IOException, XmlPullParserException {
    506         while (parser.nextTag() == XmlPullParser.START_TAG) {
    507             parser.require(XmlPullParser.START_TAG, NS, TEST_TAG);
    508             String methodName = parser.getAttributeValue(NS, NAME_ATTR);
    509             TestDescription description = new TestDescription(className, methodName);
    510             currentModule.testStarted(description);
    511             while (parser.nextTag() == XmlPullParser.START_TAG) { // Failure level
    512                 if (parser.getName().equals(FAILURE_TAG)) {
    513                     String failure = parser.getAttributeValue(NS, MESSAGE_ATTR);
    514                     if (parser.nextTag() == XmlPullParser.START_TAG) {
    515                         parser.require(XmlPullParser.START_TAG, NS, STACK_TAG);
    516                         failure = parser.nextText();
    517                         parser.require(XmlPullParser.END_TAG, NS, STACK_TAG);
    518                         parser.nextTag();
    519                     }
    520                     currentModule.testFailed(description, failure);
    521                     parser.require(XmlPullParser.END_TAG, NS, FAILURE_TAG);
    522                 }
    523                 parseLoggedFiles(parser, currentModule);
    524             }
    525             currentModule.testEnded(description, new HashMap<String, Metric>());
    526             parser.require(XmlPullParser.END_TAG, NS, TEST_TAG);
    527         }
    528     }
    529 
    530     /** Add files captured by {@link TestFailureListener} on test failures. */
    531     private static void parseLoggedFiles(XmlPullParser parser, TestRunResult currentModule)
    532             throws XmlPullParserException, IOException {
    533         if (parser.getName().equals(BUGREPORT_TAG)) {
    534             parseSingleFiles(parser, currentModule, BUGREPORT_TAG, LogDataType.BUGREPORTZ);
    535         } else if (parser.getName().equals(LOGCAT_TAG)) {
    536             parseSingleFiles(parser, currentModule, LOGCAT_TAG, LogDataType.LOGCAT);
    537         } else if (parser.getName().equals(SCREENSHOT_TAG)) {
    538             parseSingleFiles(parser, currentModule, SCREENSHOT_TAG, LogDataType.PNG);
    539         }
    540     }
    541 
    542     private static void parseSingleFiles(
    543             XmlPullParser parser, TestRunResult currentModule, String tagName, LogDataType type)
    544             throws XmlPullParserException, IOException {
    545         String name = parser.getAttributeValue(NS, LOG_FILE_NAME_ATTR);
    546         String logFileUrl = parser.nextText();
    547         currentModule.testLogSaved(name, new LogFile(logFileUrl, logFileUrl, type));
    548         parser.require(XmlPullParser.END_TAG, NS, tagName);
    549     }
    550 }
    551