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