1 /* 2 * Copyright (C) 2017 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 android.media.cts.bitstreams; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.tradefed.targetprep.MediaPreparer; 20 import com.android.compatibility.common.util.MetricsReportLog; 21 import com.android.compatibility.common.util.ResultType; 22 import com.android.compatibility.common.util.ResultUnit; 23 import com.android.tradefed.build.IBuildInfo; 24 import com.android.tradefed.config.Option; 25 import com.android.tradefed.config.OptionClass; 26 import com.android.tradefed.device.DeviceNotAvailableException; 27 import com.android.tradefed.device.ITestDevice; 28 import com.android.tradefed.log.LogUtil.CLog; 29 import com.android.tradefed.testtype.IAbi; 30 import com.android.tradefed.testtype.IAbiReceiver; 31 import com.android.tradefed.testtype.IBuildReceiver; 32 import com.android.tradefed.testtype.IDeviceTest; 33 import com.android.tradefed.util.FileUtil; 34 import java.io.ByteArrayOutputStream; 35 import java.io.File; 36 import java.io.FileFilter; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.io.PrintStream; 41 import java.nio.file.Files; 42 import java.util.ArrayDeque; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.Collections; 46 import java.util.Deque; 47 import java.util.HashMap; 48 import java.util.Iterator; 49 import java.util.LinkedHashSet; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Map.Entry; 53 import java.util.Set; 54 import java.util.concurrent.ConcurrentHashMap; 55 import java.util.concurrent.ConcurrentMap; 56 import org.junit.Ignore; 57 import org.junit.Test; 58 import org.xmlpull.v1.XmlPullParser; 59 import org.xmlpull.v1.XmlPullParserException; 60 import org.xmlpull.v1.XmlPullParserFactory; 61 62 /** 63 * Test that verifies video bitstreams decode pixel perfectly 64 */ 65 @OptionClass(alias="media-bitstreams-test") 66 public abstract class MediaBitstreamsTest implements IDeviceTest, IBuildReceiver, IAbiReceiver { 67 68 @Option(name = MediaBitstreams.OPT_HOST_BITSTREAMS_PATH, 69 description = "Absolute path of Ittiam bitstreams (host)", 70 mandatory = true) 71 private File mHostBitstreamsPath = getDefaultBitstreamsDir(); 72 73 @Option(name = MediaBitstreams.OPT_DEVICE_BITSTREAMS_PATH, 74 description = "Absolute path of Ittiam bitstreams (device)") 75 private String mDeviceBitstreamsPath = MediaBitstreams.DEFAULT_DEVICE_BITSTEAMS_PATH; 76 77 @Option(name = MediaBitstreams.OPT_DOWNLOAD_BITSTREAMS, 78 description = "Whether to download the bitstreams files") 79 private boolean mDownloadBitstreams = false; 80 81 @Option(name = MediaBitstreams.OPT_UTILIZATION_RATE, 82 description = "Percentage of external storage space used for test") 83 private int mUtilizationRate = 80; 84 85 @Option(name = MediaBitstreams.OPT_NUM_BATCHES, 86 description = "Number of batches to test;" 87 + " each batch uses external storage up to utilization rate") 88 private int mNumBatches = Integer.MAX_VALUE; 89 90 @Option(name = MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, 91 description = "Whether to debug target device under test") 92 private boolean mDebugTargetDevice = false; 93 94 @Option(name = MediaBitstreams.OPT_BITSTREAMS_PREFIX, 95 description = "Only test bitstreams in this sub-directory") 96 private String mPrefix = ""; 97 98 private String mPath = ""; 99 100 private static ConcurrentMap<String, List<ConformanceEntry>> mResults = new ConcurrentHashMap<>(); 101 102 /** 103 * Which subset of bitstreams to test 104 */ 105 enum BitstreamPackage { 106 STANDARD, 107 FULL, 108 } 109 110 private BitstreamPackage mPackage = BitstreamPackage.FULL; 111 private BitstreamPackage mPackageToRun = BitstreamPackage.STANDARD; 112 113 static class ConformanceEntry { 114 final String mPath, mCodecName, mStatus; 115 ConformanceEntry(String path, String codecName, String status) { 116 mPath = path; 117 mCodecName = codecName; 118 mStatus = status; 119 } 120 @Override 121 public String toString() { 122 return String.format("%s,%s,%s", mPath, mCodecName, mStatus); 123 } 124 } 125 126 /** 127 * A helper to access resources in the build. 128 */ 129 private CompatibilityBuildHelper mBuildHelper; 130 131 private IAbi mAbi; 132 private ITestDevice mDevice; 133 134 static File getDefaultBitstreamsDir() { 135 File mediaDir = MediaPreparer.getDefaultMediaDir(); 136 File[] subDirs = mediaDir.listFiles(new FileFilter() { 137 @Override 138 public boolean accept(File child) { 139 return child.isDirectory(); 140 } 141 }); 142 if (subDirs != null && subDirs.length == 1) { 143 File parent = new File(mediaDir, subDirs[0].getName()); 144 return new File(parent, MediaBitstreams.DEFAULT_HOST_BITSTREAMS_PATH); 145 } else { 146 return new File(MediaBitstreams.DEFAULT_HOST_BITSTREAMS_PATH); 147 } 148 } 149 150 static Collection<Object[]> bitstreams(String prefix, BitstreamPackage packageToRun) { 151 final String dynConfXml = new File("/", MediaBitstreams.DYNAMIC_CONFIG_XML).toString(); 152 try (InputStream is = MediaBitstreamsTest.class.getResourceAsStream(dynConfXml)) { 153 List<Object[]> entries = new ArrayList<>(); 154 XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); 155 parser.setInput(is, null); 156 parser.nextTag(); 157 parser.require(XmlPullParser.START_TAG, null, MediaBitstreams.DYNAMIC_CONFIG); 158 while (parser.next() != XmlPullParser.END_DOCUMENT) { 159 if (parser.getEventType() != XmlPullParser.START_TAG 160 || !MediaBitstreams.DYNAMIC_CONFIG_ENTRY.equals(parser.getName())) { 161 continue; 162 } 163 final String key = MediaBitstreams.DYNAMIC_CONFIG_KEY; 164 String bitstream = parser.getAttributeValue(null, key); 165 if (!bitstream.startsWith(prefix)) { 166 continue; 167 } 168 while (parser.next() != XmlPullParser.END_DOCUMENT) { 169 if (parser.getEventType() != XmlPullParser.START_TAG) { 170 continue; 171 } 172 if (MediaBitstreams.DYNAMIC_CONFIG_VALUE.equals(parser.getName())) { 173 parser.next(); 174 break; 175 } 176 } 177 String format = parser.getText(); 178 String[] kvPairs = format.split(","); 179 BitstreamPackage curPackage = BitstreamPackage.FULL; 180 for (String kvPair : kvPairs) { 181 String[] kv = kvPair.split("="); 182 if (MediaBitstreams.DYNAMIC_CONFIG_PACKAGE.equals(kv[0])) { 183 String packageName = kv[1]; 184 try { 185 curPackage = BitstreamPackage.valueOf(packageName.toUpperCase()); 186 } catch (Exception e) { 187 CLog.w(e); 188 } 189 } 190 } 191 if (curPackage.compareTo(packageToRun) <= 0) { 192 entries.add(new Object[] {prefix, bitstream, curPackage, packageToRun}); 193 } 194 } 195 return entries; 196 } catch (XmlPullParserException | IOException e) { 197 CLog.e(e); 198 return Collections.emptyList(); 199 } 200 } 201 202 public MediaBitstreamsTest(String prefix, String path, BitstreamPackage pkg, BitstreamPackage packageToRun 203 ) { 204 mPrefix = prefix; 205 mPath = path; 206 mPackage = pkg; 207 mPackageToRun = packageToRun; 208 } 209 210 @Override 211 public void setBuild(IBuildInfo buildInfo) { 212 // Get the build, this is used to access the APK. 213 mBuildHelper = new CompatibilityBuildHelper(buildInfo); 214 } 215 216 @Override 217 public void setAbi(IAbi abi) { 218 mAbi = abi; 219 } 220 221 @Override 222 public void setDevice(ITestDevice device) { 223 mDevice = device; 224 } 225 226 @Override 227 public ITestDevice getDevice() { 228 return mDevice; 229 } 230 231 /* 232 * Returns true if all necessary media files exist on the device, and false otherwise. 233 * 234 * This method is exposed for unit testing. 235 */ 236 private boolean bitstreamsExistOnDevice(ITestDevice device) 237 throws DeviceNotAvailableException { 238 return device.doesFileExist(mDeviceBitstreamsPath) 239 && device.isDirectory(mDeviceBitstreamsPath); 240 } 241 242 private String getCurrentMethod() { 243 return Thread.currentThread().getStackTrace()[2].getMethodName(); 244 } 245 246 private MetricsReportLog createReport(String methodName) { 247 String className = MediaBitstreamsTest.class.getCanonicalName(); 248 MetricsReportLog report = new MetricsReportLog( 249 mBuildHelper.getBuildInfo(), mAbi.getName(), 250 String.format("%s#%s", className, methodName), 251 MediaBitstreams.K_MODULE + "." + this.getClass().getSimpleName(), 252 "media_bitstreams_conformance", true); 253 return report; 254 } 255 256 /** 257 * @param method test method name in the form class#method 258 * @param p path to bitstream 259 * @param d decoder name 260 * @param s test status: unsupported, true, false, crash, or timeout. 261 */ 262 private void addConformanceEntry(String method, String p, String d, String s) { 263 MetricsReportLog report = createReport(method); 264 report.addValue(MediaBitstreams.KEY_PATH, p, ResultType.NEUTRAL, ResultUnit.NONE); 265 report.addValue(MediaBitstreams.KEY_CODEC_NAME, d, ResultType.NEUTRAL, ResultUnit.NONE); 266 report.addValue(MediaBitstreams.KEY_STATUS, s, ResultType.NEUTRAL, ResultUnit.NONE); 267 report.submit(); 268 269 ConformanceEntry ce = new ConformanceEntry(p, d, s); 270 mResults.putIfAbsent(p, new ArrayList<>()); 271 mResults.get(p).add(ce); 272 } 273 274 Map<String, String> getArgs() { 275 Map<String, String> args = new HashMap<>(); 276 args.put(MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, Boolean.toString(mDebugTargetDevice)); 277 args.put(MediaBitstreams.OPT_DEVICE_BITSTREAMS_PATH, mDeviceBitstreamsPath); 278 return args; 279 } 280 281 private class ProcessBitstreamsFormats extends ReportProcessor { 282 283 @Override 284 void setUp(ITestDevice device) throws DeviceNotAvailableException { 285 if (mDownloadBitstreams || !bitstreamsExistOnDevice(device)) { 286 device.pushDir(mHostBitstreamsPath, mDeviceBitstreamsPath); 287 } 288 } 289 290 @Override 291 Map<String, String> getArgs() { 292 return MediaBitstreamsTest.this.getArgs(); 293 } 294 295 @Override 296 void process(ITestDevice device, String reportPath) 297 throws DeviceNotAvailableException, IOException { 298 File dynamicConfigFile = mBuildHelper.getTestFile(MediaBitstreams.K_MODULE + ".dynamic"); 299 device.pullFile(reportPath, dynamicConfigFile); 300 CLog.i("Pulled bitstreams formats to %s", dynamicConfigFile.getPath()); 301 } 302 303 } 304 305 private class ProcessBitstreamsValidation extends ReportProcessor { 306 307 Set<String> mBitstreams; 308 Deque<String> mProcessedBitstreams = new ArrayDeque<>(); 309 private final String mMethodName; 310 private final String mBitstreamsListTxt = new File( 311 mDeviceBitstreamsPath, 312 MediaBitstreams.K_BITSTREAMS_LIST_TXT).toString(); 313 private String mLastCrash; 314 315 ProcessBitstreamsValidation(Set<String> bitstreams, String methodName) { 316 mBitstreams = bitstreams; 317 mMethodName = methodName; 318 } 319 320 private String getBitstreamsListString() { 321 OutputStream baos = new ByteArrayOutputStream(); 322 PrintStream ps = new PrintStream(baos, true); 323 try { 324 for (String b : mBitstreams) { 325 ps.println(b); 326 } 327 return baos.toString(); 328 } finally { 329 ps.close(); 330 } 331 } 332 333 private void pushBitstreams(ITestDevice device) 334 throws IOException, DeviceNotAvailableException { 335 File tmp = null; 336 try { 337 CLog.i("Pushing %d bitstream(s) from %s to %s", 338 mBitstreams.size(), 339 mHostBitstreamsPath, 340 mDeviceBitstreamsPath); 341 tmp = Files.createTempDirectory(null).toFile(); 342 for (String b : mBitstreams) { 343 String m = MediaBitstreams.getMd5Path(b); 344 for (String f : new String[] {m, b}) { 345 File tmpf = new File(tmp, f); 346 new File(tmpf.getParent()).mkdirs(); 347 FileUtil.copyFile(new File(mHostBitstreamsPath, f), tmpf); 348 } 349 } 350 device.executeShellCommand(String.format("rm -rf %s", mDeviceBitstreamsPath)); 351 device.pushDir(tmp, mDeviceBitstreamsPath); 352 device.pushString(getBitstreamsListString(), mBitstreamsListTxt); 353 } finally { 354 FileUtil.recursiveDelete(tmp); 355 } 356 } 357 358 @Override 359 void setUp(ITestDevice device) throws DeviceNotAvailableException, IOException { 360 pushBitstreams(device); 361 } 362 363 @Override 364 Map<String, String> getArgs() { 365 Map<String, String> args = MediaBitstreamsTest.this.getArgs(); 366 if (mLastCrash != null) { 367 args.put(MediaBitstreams.OPT_LAST_CRASH, mLastCrash); 368 } 369 return args; 370 } 371 372 private void parse(ITestDevice device, String reportPath) 373 throws DeviceNotAvailableException { 374 String[] lines = getReportLines(device, reportPath); 375 mProcessedBitstreams.clear(); 376 for (int i = 0; i < lines.length;) { 377 378 String path = lines[i++]; 379 mProcessedBitstreams.add(path); 380 String errMsg; 381 382 boolean failedEarly; 383 if (i < lines.length) { 384 failedEarly = Boolean.parseBoolean(lines[i++]); 385 errMsg = failedEarly ? lines[i++] : ""; 386 } else { 387 failedEarly = true; 388 errMsg = MediaBitstreams.K_NATIVE_CRASH; 389 mLastCrash = MediaBitstreams.generateCrashSignature(path, ""); 390 mProcessedBitstreams.removeLast(); 391 } 392 393 if (failedEarly) { 394 addConformanceEntry(mMethodName, path, null, errMsg); 395 continue; 396 } 397 398 int n = Integer.parseInt(lines[i++]); 399 for (int j = 0; j < n && i < lines.length; j++) { 400 String decoderName = lines[i++]; 401 String result; 402 if (i < lines.length) { 403 result = lines[i++]; 404 } else { 405 result = MediaBitstreams.K_NATIVE_CRASH; 406 mLastCrash = MediaBitstreams.generateCrashSignature(path, decoderName); 407 mProcessedBitstreams.removeLast(); 408 } 409 addConformanceEntry(mMethodName, path, decoderName, result); 410 } 411 412 413 } 414 } 415 416 @Override 417 void process(ITestDevice device, String reportPath) 418 throws DeviceNotAvailableException, IOException { 419 parse(device, reportPath); 420 } 421 422 @Override 423 boolean recover(ITestDevice device, String reportPath) 424 throws DeviceNotAvailableException, IOException { 425 try { 426 parse(device, reportPath); 427 mBitstreams.removeAll(mProcessedBitstreams); 428 device.pushString(getBitstreamsListString(), mBitstreamsListTxt); 429 return true; 430 } catch (RuntimeException e) { 431 File hostFile = reportPath == null ? null : device.pullFile(reportPath); 432 CLog.e("Error parsing report; saving report to %s", hostFile); 433 CLog.e(e); 434 return false; 435 } 436 } 437 438 } 439 440 @Ignore 441 @Test 442 public void testGetBitstreamsFormats() throws DeviceNotAvailableException, IOException { 443 ReportProcessor processor = new ProcessBitstreamsFormats(); 444 processor.processDeviceReport( 445 getDevice(), 446 getCurrentMethod(), 447 MediaBitstreams.KEY_BITSTREAMS_FORMATS_XML); 448 } 449 450 @Test 451 public void testBitstreamsConformance() { 452 File bitstreamFile = new File(mHostBitstreamsPath, mPath); 453 if (!bitstreamFile.exists()) { 454 // todo(b/65165250): throw Exception once MediaPreparer can auto-download 455 CLog.w(bitstreamFile + " not found; skipping"); 456 return; 457 } 458 459 if (!mResults.containsKey(mPath)) { 460 try { 461 testBitstreamsConformance(mPrefix); 462 } catch (DeviceNotAvailableException | IOException e) { 463 String curMethod = getCurrentMethod(); 464 addConformanceEntry(curMethod, mPath, MediaBitstreams.K_UNAVAILABLE, e.toString()); 465 } 466 } 467 // todo(robertshih): lookup conformance entry; pass/fail based on lookup result 468 } 469 470 private void testBitstreamsConformance(String prefix) 471 throws DeviceNotAvailableException, IOException { 472 473 ITestDevice device = getDevice(); 474 SupportedBitstreamsProcessor preparer; 475 preparer = new SupportedBitstreamsProcessor(prefix, mDebugTargetDevice); 476 preparer.processDeviceReport( 477 device, 478 MediaBitstreams.K_TEST_GET_SUPPORTED_BITSTREAMS, 479 MediaBitstreams.KEY_SUPPORTED_BITSTREAMS_TXT); 480 Collection<Object[]> bitstreams = bitstreams(mPrefix, mPackageToRun); 481 Set<String> supportedBitstreams = preparer.getSupportedBitstreams(); 482 CLog.i("%d supported bitstreams under %s", supportedBitstreams.size(), prefix); 483 484 int n = 0; 485 long size = 0; 486 long limit = device.getExternalStoreFreeSpace() * mUtilizationRate * 1024 / 100; 487 488 String curMethod = getCurrentMethod(); 489 Set<String> toPush = new LinkedHashSet<>(); 490 Iterator<Object[]> iter = bitstreams.iterator(); 491 492 for (int i = 0; i < bitstreams.size(); i++) { 493 494 if (n >= mNumBatches) { 495 break; 496 } 497 498 String p = (String) iter.next()[1]; 499 Map<String, Boolean> decoderCapabilities; 500 decoderCapabilities = preparer.getDecoderCapabilitiesForPath(p); 501 if (decoderCapabilities.isEmpty()) { 502 addConformanceEntry( 503 curMethod, p, 504 MediaBitstreams.K_UNAVAILABLE, 505 MediaBitstreams.K_UNSUPPORTED); 506 } 507 for (Entry<String, Boolean> entry : decoderCapabilities.entrySet()) { 508 Boolean supported = entry.getValue(); 509 if (supported) { 510 File bitstreamFile = new File(mHostBitstreamsPath, p); 511 String md5Path = MediaBitstreams.getMd5Path(p); 512 File md5File = new File(mHostBitstreamsPath, md5Path); 513 if (md5File.exists() && bitstreamFile.exists() && toPush.add(p)) { 514 size += md5File.length(); 515 size += bitstreamFile.length(); 516 } 517 } else { 518 String d = entry.getKey(); 519 addConformanceEntry(curMethod, p, d, MediaBitstreams.K_UNSUPPORTED); 520 } 521 } 522 523 if (size > limit || i + 1 == bitstreams.size()) { 524 ReportProcessor processor; 525 processor = new ProcessBitstreamsValidation(toPush, curMethod); 526 processor.processDeviceReport( 527 device, 528 curMethod, 529 MediaBitstreams.KEY_BITSTREAMS_VALIDATION_TXT); 530 toPush.clear(); 531 size = 0; 532 n++; 533 } 534 535 } 536 537 } 538 539 540 } 541