Home | History | Annotate | Download | only in tests
      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 com.android.media.tests;
     17 
     18 import com.android.ddmlib.CollectingOutputReceiver;
     19 import com.android.tradefed.config.Option;
     20 import com.android.tradefed.device.DeviceNotAvailableException;
     21 import com.android.tradefed.device.IFileEntry;
     22 import com.android.tradefed.device.ITestDevice;
     23 import com.android.tradefed.log.LogUtil.CLog;
     24 import com.android.tradefed.result.ITestInvocationListener;
     25 import com.android.tradefed.result.TestDescription;
     26 import com.android.tradefed.testtype.IDeviceTest;
     27 import com.android.tradefed.testtype.IRemoteTest;
     28 import com.android.tradefed.util.CommandResult;
     29 import com.android.tradefed.util.CommandStatus;
     30 import com.android.tradefed.util.FileUtil;
     31 import com.android.tradefed.util.RunUtil;
     32 
     33 import java.io.File;
     34 import java.text.ParseException;
     35 import java.text.SimpleDateFormat;
     36 import java.util.Arrays;
     37 import java.util.Date;
     38 import java.util.HashMap;
     39 import java.util.Map;
     40 import java.util.TimeZone;
     41 import java.util.concurrent.TimeUnit;
     42 import java.util.regex.Matcher;
     43 import java.util.regex.Pattern;
     44 
     45 /**
     46  * Tests adb command "screenrecord", i.e. "adb screenrecord [--size] [--bit-rate] [--time-limit]"
     47  *
     48  * <p>The test use the above command to record a video of DUT's screen. It then tries to verify that
     49  * a video was actually recorded and that the video is a valid video file. It currently uses
     50  * 'avprobe' to do the video analysis along with extracting parameters from the adb command's
     51  * output.
     52  */
     53 public class AdbScreenrecordTest implements IDeviceTest, IRemoteTest {
     54 
     55     //===================================================================
     56     // TEST OPTIONS
     57     //===================================================================
     58     @Option(name = "run-key", description = "Run key for the test")
     59     private String mRunKey = "AdbScreenRecord";
     60 
     61     @Option(name = "time-limit", description = "Recording time in seconds", isTimeVal = true)
     62     private long mRecordTimeInSeconds = -1;
     63 
     64     @Option(name = "size", description = "Video Size: 'widthxheight', e.g. '1280x720'")
     65     private String mVideoSize = null;
     66 
     67     @Option(name = "bit-rate", description = "Video bit rate in megabits per second, e.g. 4000000")
     68     private long mBitRate = -1;
     69 
     70     //===================================================================
     71     // CLASS VARIABLES
     72     //===================================================================
     73     private ITestDevice mDevice;
     74     private TestRunHelper mTestRunHelper;
     75 
     76     //===================================================================
     77     // CONSTANTS
     78     //===================================================================
     79     private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
     80     private static final long DEVICE_SYNC_MS = 5 * 60 * 1000; // 5 min
     81     private static final long POLLING_INTERVAL_MS = 5 * 1000; // 5 sec
     82     private static final long CMD_TIMEOUT_MS = 5 * 1000; // 5 sec
     83     private static final String ERR_OPTION_MALFORMED = "Test option %1$s is not correct [%2$s]";
     84     private static final String OPTION_TIME_LIMIT = "--time-limit";
     85     private static final String OPTION_SIZE = "--size";
     86     private static final String OPTION_BITRATE = "--bit-rate";
     87     private static final String RESULT_KEY_RECORDED_FRAMES = "recorded_frames";
     88     private static final String RESULT_KEY_RECORDED_LENGTH = "recorded_length";
     89     private static final String RESULT_KEY_VERIFIED_DURATION = "verified_duration";
     90     private static final String RESULT_KEY_VERIFIED_BITRATE = "verified_bitrate";
     91     private static final String TEST_FILE = "/sdcard/screenrecord_test.mp4";
     92     private static final String AVPROBE_NOT_INSTALLED =
     93             "Program 'avprobe' is not installed on host '%1$s'";
     94     private static final String REGEX_IS_VIDEO_OK =
     95             "Duration: (\\d\\d:\\d\\d:\\d\\d.\\d\\d).+bitrate: (\\d+ .b\\/s)";
     96     private static final String AVPROBE_STR = "avprobe";
     97 
     98     //===================================================================
     99     // ENUMS
    100     //===================================================================
    101     enum HOST_SOFTWARE {
    102         AVPROBE
    103     }
    104 
    105     @Override
    106     public void setDevice(ITestDevice device) {
    107         mDevice = device;
    108     }
    109 
    110     @Override
    111     public ITestDevice getDevice() {
    112         return mDevice;
    113     }
    114 
    115     /** Main test function invoked by test harness */
    116     @Override
    117     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    118         initializeTest(listener);
    119 
    120         CLog.i("Verify required software is installed on host");
    121         verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE.AVPROBE);
    122 
    123         mTestRunHelper.startTest(1);
    124 
    125         Map<String, String> resultsDictionary = new HashMap<String, String>();
    126         try {
    127             CLog.i("Verify that test options are valid");
    128             if (!verifyTestParameters()) {
    129                 return;
    130             }
    131 
    132             // "resultDictionary" can be used to post results to dashboards like BlackBox
    133             resultsDictionary = runTest(resultsDictionary, TEST_TIMEOUT_MS);
    134             final String metricsStr = Arrays.toString(resultsDictionary.entrySet().toArray());
    135             CLog.i("Uploading metrics values:\n" + metricsStr);
    136             mTestRunHelper.endTest(resultsDictionary);
    137         } catch (TestFailureException e) {
    138             CLog.i("TestRunHelper.reportFailure triggered");
    139         } finally {
    140             deleteFileFromDevice(getAbsoluteFilename());
    141         }
    142     }
    143 
    144     /**
    145      * Test code that calls "adb screenrecord" and checks for pass/fail criterias
    146      *
    147      * <p>
    148      *
    149      * <ul>
    150      *   <li>1. Run adb screenrecord command
    151      *   <li>2. Wait until there is a video file; fail if none appears
    152      *   <li>3. Analyze adb output and extract recorded number of frames and video length
    153      *   <li>4. Pull recorded video file off device
    154      *   <li>5. Using avprobe, analyze video file and extract duration and bitrate
    155      *   <li>6. Return extracted results
    156      * </ul>
    157      *
    158      * @throws DeviceNotAvailableException
    159      * @throws TestFailureException
    160      */
    161     private Map<String, String> runTest(Map<String, String> results, final long timeout)
    162             throws DeviceNotAvailableException, TestFailureException {
    163         final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
    164         final String cmd = generateAdbScreenRecordCommand();
    165         final String deviceFileName = getAbsoluteFilename();
    166 
    167         CLog.i("START Execute device shell command: '" + cmd + "'");
    168         getDevice().executeShellCommand(cmd, receiver, timeout, TimeUnit.MILLISECONDS, 3);
    169         String adbOutput = receiver.getOutput();
    170         CLog.i(adbOutput);
    171         CLog.i("END Execute device shell command");
    172 
    173         CLog.i("Wait for recorded file: " + deviceFileName);
    174         if (!waitForFile(getDevice(), timeout, deviceFileName)) {
    175             mTestRunHelper.reportFailure("Recorded test file not found");
    176         }
    177 
    178         CLog.i("Get number of recorded frames and recorded length from adb output");
    179         extractVideoDataFromAdbOutput(adbOutput, results);
    180 
    181         CLog.i("Get duration and bitrate info from video file using '" + AVPROBE_STR + "'");
    182         try {
    183             extractDurationAndBitrateFromVideoFileUsingAvprobe(deviceFileName, results);
    184         } catch (ParseException e) {
    185             throw new RuntimeException(e);
    186         }
    187         deleteFileFromDevice(deviceFileName);
    188         return results;
    189     }
    190 
    191     /** Convert a string on form HH:mm:ss.SS to nearest number of seconds */
    192     private long convertBitrateToKilobits(String bitrate) {
    193         Matcher m = Pattern.compile("(\\d+) (.)b\\/s").matcher(bitrate);
    194         if (!m.matches()) {
    195             return -1;
    196         }
    197 
    198         final String unit = m.group(2).toUpperCase();
    199         long factor = 1;
    200         switch (unit) {
    201             case "K":
    202                 factor = 1;
    203                 break;
    204             case "M":
    205                 factor = 1000;
    206                 break;
    207             case "G":
    208                 factor = 1000000;
    209                 break;
    210         }
    211 
    212         long rate = Long.parseLong(m.group(1));
    213 
    214         return rate * factor;
    215     }
    216 
    217     /**
    218      * Convert a string on form HH:mm:ss.SS to nearest number of seconds
    219      *
    220      * @throws ParseException
    221      */
    222     private long convertDurationToMilliseconds(String duration) throws ParseException {
    223         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS");
    224         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    225         Date convertedDate = sdf.parse("1970-01-01 " + duration);
    226         return convertedDate.getTime();
    227     }
    228 
    229     /**
    230      * Deletes a file off a device
    231      *
    232      * @param deviceFileName - path and filename to file to be deleted
    233      * @throws DeviceNotAvailableException
    234      */
    235     private void deleteFileFromDevice(String deviceFileName) throws DeviceNotAvailableException {
    236         if (deviceFileName == null || deviceFileName.isEmpty()) {
    237             return;
    238         }
    239 
    240         CLog.i("Delete file from device: " + deviceFileName);
    241         getDevice().executeShellCommand("rm -f " + deviceFileName);
    242     }
    243 
    244     /**
    245      * Extracts duration and bitrate data from a video file
    246      *
    247      * @throws DeviceNotAvailableException
    248      * @throws ParseException
    249      * @throws TestFailureException
    250      */
    251     private void extractDurationAndBitrateFromVideoFileUsingAvprobe(
    252             String deviceFileName, Map<String, String> results)
    253             throws DeviceNotAvailableException, ParseException, TestFailureException {
    254         CLog.i("Check if the recorded file has some data in it: " + deviceFileName);
    255         IFileEntry video = getDevice().getFileEntry(deviceFileName);
    256         if (video == null || video.getFileEntry().getSizeValue() < 1) {
    257             mTestRunHelper.reportFailure("Video Entry info failed");
    258         }
    259 
    260         final File recordedVideo = getDevice().pullFile(deviceFileName);
    261         CLog.i("Recorded video file: " + recordedVideo.getAbsolutePath());
    262 
    263         CommandResult result =
    264                 RunUtil.getDefault()
    265                         .runTimedCmd(
    266                                 CMD_TIMEOUT_MS,
    267                                 AVPROBE_STR,
    268                                 "-loglevel",
    269                                 "info",
    270                                 recordedVideo.getAbsolutePath());
    271 
    272         // Remove file from host machine
    273         FileUtil.deleteFile(recordedVideo);
    274 
    275         if (result.getStatus() != CommandStatus.SUCCESS) {
    276             mTestRunHelper.reportFailure(AVPROBE_STR + " command failed");
    277         }
    278 
    279         String data = result.getStderr();
    280         CLog.i("data: " + data);
    281         if (data == null || data.isEmpty()) {
    282             mTestRunHelper.reportFailure(AVPROBE_STR + " output data is empty");
    283         }
    284 
    285         Matcher m = Pattern.compile(REGEX_IS_VIDEO_OK).matcher(data);
    286         if (!m.find()) {
    287             final String errMsg =
    288                     "Video verification failed; no matching verification pattern found";
    289             mTestRunHelper.reportFailure(errMsg);
    290         }
    291 
    292         String duration = m.group(1);
    293         long durationInMilliseconds = convertDurationToMilliseconds(duration);
    294         String bitrate = m.group(2);
    295         long bitrateInKilobits = convertBitrateToKilobits(bitrate);
    296 
    297         results.put(RESULT_KEY_VERIFIED_DURATION, Long.toString(durationInMilliseconds / 1000));
    298         results.put(RESULT_KEY_VERIFIED_BITRATE, Long.toString(bitrateInKilobits));
    299     }
    300 
    301     /** Extracts recorded number of frames and recorded video length from adb output
    302      * @throws TestFailureException */
    303     private boolean extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results)
    304             throws TestFailureException {
    305         final String regEx = "recorded (\\d+) frames in (\\d+) second";
    306         Matcher m = Pattern.compile(regEx).matcher(adbOutput);
    307         if (!m.find()) {
    308             mTestRunHelper.reportFailure("Regular Expression did not find recorded frames");
    309             return false;
    310         }
    311 
    312         int recordedFrames = Integer.parseInt(m.group(1));
    313         int recordedLength = Integer.parseInt(m.group(2));
    314         CLog.i("Recorded frames: " + recordedFrames);
    315         CLog.i("Recorded length: " + recordedLength);
    316         if (recordedFrames <= 0) {
    317             mTestRunHelper.reportFailure("No recorded frames detected");
    318             return false;
    319         }
    320 
    321         results.put(RESULT_KEY_RECORDED_FRAMES, Integer.toString(recordedFrames));
    322         results.put(RESULT_KEY_RECORDED_LENGTH, Integer.toString(recordedLength));
    323         return true;
    324     }
    325 
    326     /** Generates an adb command from passed in test options */
    327     private String generateAdbScreenRecordCommand() {
    328         final String SPACE = " ";
    329         StringBuilder sb = new StringBuilder(128);
    330         sb.append("screenrecord --verbose ").append(getAbsoluteFilename());
    331 
    332         // Add test options if they have been passed in to the test
    333         if (mRecordTimeInSeconds != -1) {
    334             final long timeLimit = TimeUnit.MILLISECONDS.toSeconds(mRecordTimeInSeconds);
    335             sb.append(SPACE).append(OPTION_TIME_LIMIT).append(SPACE).append(timeLimit);
    336         }
    337 
    338         if (mVideoSize != null) {
    339             sb.append(SPACE).append(OPTION_SIZE).append(SPACE).append(mVideoSize);
    340         }
    341 
    342         if (mBitRate != -1) {
    343             sb.append(SPACE).append(OPTION_BITRATE).append(SPACE).append(mBitRate);
    344         }
    345 
    346         return sb.toString();
    347     }
    348 
    349     /** Returns absolute path to device recorded video file */
    350     private String getAbsoluteFilename() {
    351         return TEST_FILE;
    352     }
    353 
    354     /** Performs test initialization steps */
    355     private void initializeTest(ITestInvocationListener listener)
    356             throws UnsupportedOperationException, DeviceNotAvailableException {
    357         TestDescription testId = new TestDescription(getClass().getCanonicalName(), mRunKey);
    358 
    359         // Allocate helpers
    360         mTestRunHelper = new TestRunHelper(listener, testId);
    361 
    362         getDevice().disableKeyguard();
    363         getDevice().waitForDeviceAvailable(DEVICE_SYNC_MS);
    364 
    365         CLog.i("Sync device time to host time");
    366         getDevice().setDate(new Date());
    367     }
    368 
    369     /** Verifies that required software is installed on host machine */
    370     private void verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE software) {
    371         String swName = "";
    372         switch (software) {
    373             case AVPROBE:
    374                 swName = AVPROBE_STR;
    375                 CommandResult result =
    376                         RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT_MS, swName, "-version");
    377                 String output = result.getStdout();
    378                 if (result.getStatus() == CommandStatus.SUCCESS && output.startsWith(swName)) {
    379                     return;
    380                 }
    381                 break;
    382         }
    383 
    384         CLog.i("Program '" + swName + "' not found, report test failure");
    385         String hostname = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT_MS, "hostname").getStdout();
    386 
    387         String err = String.format(AVPROBE_NOT_INSTALLED, (hostname == null) ? "" : hostname);
    388         throw new RuntimeException(err);
    389     }
    390 
    391     /** Verifies that passed in test parameters are legitimate
    392      * @throws TestFailureException */
    393     private boolean verifyTestParameters() throws TestFailureException {
    394         if (mRecordTimeInSeconds != -1 && mRecordTimeInSeconds < 1) {
    395             final String error =
    396                     String.format(ERR_OPTION_MALFORMED, OPTION_TIME_LIMIT, mRecordTimeInSeconds);
    397             mTestRunHelper.reportFailure(error);
    398             return false;
    399         }
    400 
    401         if (mVideoSize != null) {
    402             final String videoSizeRegEx = "\\d+x\\d+";
    403             Matcher m = Pattern.compile(videoSizeRegEx).matcher(mVideoSize);
    404             if (!m.matches()) {
    405                 final String error = String.format(ERR_OPTION_MALFORMED, OPTION_SIZE, mVideoSize);
    406                 mTestRunHelper.reportFailure(error);
    407                 return false;
    408             }
    409         }
    410 
    411         if (mBitRate != -1 && mBitRate < 1) {
    412             final String error = String.format(ERR_OPTION_MALFORMED, OPTION_BITRATE, mBitRate);
    413             mTestRunHelper.reportFailure(error);
    414             return false;
    415         }
    416 
    417         return true;
    418     }
    419 
    420     /** Checks for existence of a file on the device */
    421     private static boolean waitForFile(
    422             ITestDevice device, final long timeout, final String absoluteFilename)
    423             throws DeviceNotAvailableException {
    424         final long checkFileStartTime = System.currentTimeMillis();
    425 
    426         do {
    427             RunUtil.getDefault().sleep(POLLING_INTERVAL_MS);
    428             if (device.doesFileExist(absoluteFilename)) {
    429                 return true;
    430             }
    431         } while (System.currentTimeMillis() - checkFileStartTime < timeout);
    432 
    433         return false;
    434     }
    435 }
    436