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