Home | History | Annotate | Download | only in result
      1 /*
      2  * Copyright (C) 2010 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 
     17 package com.android.cts.tradefed.result;
     18 
     19 import com.android.cts.tradefed.build.CtsBuildHelper;
     20 import com.android.cts.tradefed.device.DeviceInfoCollector;
     21 import com.android.cts.tradefed.testtype.CtsTest;
     22 import com.android.cts.tradefed.util.CtsHostStore;
     23 import com.android.ddmlib.Log;
     24 import com.android.ddmlib.Log.LogLevel;
     25 import com.android.ddmlib.testrunner.TestIdentifier;
     26 import com.android.tradefed.build.IBuildInfo;
     27 import com.android.tradefed.build.IFolderBuildInfo;
     28 import com.android.tradefed.config.Option;
     29 import com.android.tradefed.log.LogUtil.CLog;
     30 import com.android.tradefed.result.ITestInvocationListener;
     31 import com.android.tradefed.result.InputStreamSource;
     32 import com.android.tradefed.result.LogDataType;
     33 import com.android.tradefed.result.LogFileSaver;
     34 import com.android.tradefed.result.TestSummary;
     35 import com.android.tradefed.util.FileUtil;
     36 import com.android.tradefed.util.StreamUtil;
     37 
     38 import org.kxml2.io.KXmlSerializer;
     39 
     40 import java.io.File;
     41 import java.io.FileNotFoundException;
     42 import java.io.FileOutputStream;
     43 import java.io.IOException;
     44 import java.io.InputStream;
     45 import java.io.OutputStream;
     46 import java.util.Map;
     47 import java.util.regex.Matcher;
     48 import java.util.regex.Pattern;
     49 
     50 /**
     51  * Writes results to an XML files in the CTS format.
     52  * <p/>
     53  * Collects all test info in memory, then dumps to file when invocation is complete.
     54  * <p/>
     55  * Outputs xml in format governed by the cts_result.xsd
     56  */
     57 public class CtsXmlResultReporter implements ITestInvocationListener {
     58     private static final String LOG_TAG = "CtsXmlResultReporter";
     59 
     60     static final String TEST_RESULT_FILE_NAME = "testResult.xml";
     61     private static final String CTS_RESULT_FILE_VERSION = "4.4";
     62     private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css",
     63         "logo.gif", "newrule-green.png"};
     64 
     65     /** the XML namespace */
     66     static final String ns = null;
     67 
     68     static final String RESULT_TAG = "TestResult";
     69     static final String PLAN_ATTR = "testPlan";
     70     static final String STARTTIME_ATTR = "starttime";
     71 
     72     private static final String REPORT_DIR_NAME = "output-file-path";
     73     @Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " +
     74             "test results and associated logs. If not specified, results will be stored at " +
     75             "<cts root>/repository/results")
     76     protected File mReportDir = null;
     77 
     78     // listen in on the plan option provided to CtsTest
     79     @Option(name = CtsTest.PLAN_OPTION, description = "the test plan to run.")
     80     private String mPlanName = "NA";
     81 
     82     // listen in on the continue-session option provided to CtsTest
     83     @Option(name = CtsTest.CONTINUE_OPTION, description = "the test result session to continue.")
     84     private Integer mContinueSessionId = null;
     85 
     86     @Option(name = "quiet-output", description = "Mute display of test results.")
     87     private boolean mQuietOutput = false;
     88 
     89     @Option(name = "result-server", description = "Server to publish test results.")
     90     private String mResultServer;
     91 
     92     protected IBuildInfo mBuildInfo;
     93     private String mStartTime;
     94     private String mDeviceSerial;
     95     private TestResults mResults = new TestResults();
     96     private TestPackageResult mCurrentPkgResult = null;
     97     private boolean mIsDeviceInfoRun = false;
     98     private ResultReporter mReporter;
     99     private File mLogDir;
    100     private String mSuiteName;
    101 
    102     private static final Pattern mCtsLogPattern = Pattern.compile("(.*)\\+\\+\\+\\+(.*)");
    103 
    104     public void setReportDir(File reportDir) {
    105         mReportDir = reportDir;
    106     }
    107 
    108     /**
    109      * {@inheritDoc}
    110      */
    111     @Override
    112     public void invocationStarted(IBuildInfo buildInfo) {
    113         mBuildInfo = buildInfo;
    114         if (!(buildInfo instanceof IFolderBuildInfo)) {
    115             throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
    116         }
    117         IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
    118         CtsBuildHelper ctsBuildHelper = getBuildHelper(ctsBuild);
    119         mDeviceSerial = buildInfo.getDeviceSerial() == null ? "unknown_device" :
    120             buildInfo.getDeviceSerial();
    121         if (mContinueSessionId != null) {
    122             CLog.d("Continuing session %d", mContinueSessionId);
    123             // reuse existing directory
    124             TestResultRepo resultRepo = new TestResultRepo(ctsBuildHelper.getResultsDir());
    125             mResults = resultRepo.getResult(mContinueSessionId);
    126             if (mResults == null) {
    127                 throw new IllegalArgumentException(String.format("Could not find session %d",
    128                         mContinueSessionId));
    129             }
    130             mPlanName = resultRepo.getSummaries().get(mContinueSessionId).getTestPlan();
    131             mStartTime = resultRepo.getSummaries().get(mContinueSessionId).getStartTime();
    132             mReportDir = resultRepo.getReportDir(mContinueSessionId);
    133         } else {
    134             if (mReportDir == null) {
    135                 mReportDir = ctsBuildHelper.getResultsDir();
    136             }
    137             mReportDir = createUniqueReportDir(mReportDir);
    138 
    139             mStartTime = getTimestamp();
    140             logResult("Created result dir %s", mReportDir.getName());
    141         }
    142         mSuiteName = ctsBuildHelper.getSuiteName();
    143         mReporter = new ResultReporter(mResultServer, mSuiteName);
    144 
    145         // TODO: allow customization of log dir
    146         // create a unique directory for saving logs, with same name as result dir
    147         File rootLogDir = getBuildHelper(ctsBuild).getLogsDir();
    148         mLogDir = new File(rootLogDir, mReportDir.getName());
    149         mLogDir.mkdirs();
    150     }
    151 
    152     /**
    153      * Create a unique directory for saving results.
    154      * <p/>
    155      * Currently using legacy CTS host convention of timestamp directory names. In case of
    156      * collisions, will use {@link FileUtil} to generate unique file name.
    157      * <p/>
    158      * TODO: in future, consider using LogFileSaver to create build-specific directories
    159      *
    160      * @param parentDir the parent folder to create dir in
    161      * @return the created directory
    162      */
    163     private static synchronized File createUniqueReportDir(File parentDir) {
    164         // TODO: in future, consider using LogFileSaver to create build-specific directories
    165 
    166         File reportDir = new File(parentDir, TimeUtil.getResultTimestamp());
    167         if (reportDir.exists()) {
    168             // directory with this timestamp exists already! Choose a unique, although uglier, name
    169             try {
    170                 reportDir = FileUtil.createTempDir(TimeUtil.getResultTimestamp() + "_", parentDir);
    171             } catch (IOException e) {
    172                 CLog.e(e);
    173                 CLog.e("Failed to create result directory %s", reportDir.getAbsolutePath());
    174             }
    175         } else {
    176             if (!reportDir.mkdirs()) {
    177                 // TODO: consider throwing an exception
    178                 CLog.e("mkdirs failed when attempting to create result directory %s",
    179                         reportDir.getAbsolutePath());
    180             }
    181         }
    182         return reportDir;
    183     }
    184 
    185     /**
    186      * Helper method to retrieve the {@link CtsBuildHelper}.
    187      * @param ctsBuild
    188      */
    189     CtsBuildHelper getBuildHelper(IFolderBuildInfo ctsBuild) {
    190         CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
    191         try {
    192             buildHelper.validateStructure();
    193         } catch (FileNotFoundException e) {
    194             // just log an error - it might be expected if we failed to retrieve a build
    195             CLog.e("Invalid CTS build %s", ctsBuild.getRootDir());
    196         }
    197         return buildHelper;
    198     }
    199 
    200     /**
    201      * {@inheritDoc}
    202      */
    203     @Override
    204     public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
    205         try {
    206             File logFile = getLogFileSaver().saveAndZipLogData(dataName, dataType,
    207                     dataStream.createInputStream());
    208             logResult(String.format("Saved log %s", logFile.getName()));
    209         } catch (IOException e) {
    210             CLog.e("Failed to write log for %s", dataName);
    211         }
    212     }
    213 
    214     /**
    215      * Return the {@link LogFileSaver} to use.
    216      * <p/>
    217      * Exposed for unit testing.
    218      */
    219     LogFileSaver getLogFileSaver() {
    220         return new LogFileSaver(mLogDir);
    221     }
    222 
    223     /**
    224      * {@inheritDoc}
    225      */
    226     @Override
    227     public void testRunStarted(String name, int numTests) {
    228         if (mCurrentPkgResult != null && !name.equals(mCurrentPkgResult.getAppPackageName())) {
    229             // display results from previous run
    230             logCompleteRun(mCurrentPkgResult);
    231         }
    232         mIsDeviceInfoRun = name.equals(DeviceInfoCollector.APP_PACKAGE_NAME);
    233         if (mIsDeviceInfoRun) {
    234             logResult("Collecting device info");
    235         } else  {
    236             if (mCurrentPkgResult == null || !name.equals(mCurrentPkgResult.getAppPackageName())) {
    237                 logResult("-----------------------------------------");
    238                 logResult("Test package %s started", name);
    239                 logResult("-----------------------------------------");
    240             }
    241             mCurrentPkgResult = mResults.getOrCreatePackage(name);
    242         }
    243 
    244     }
    245 
    246     /**
    247      * {@inheritDoc}
    248      */
    249     @Override
    250     public void testStarted(TestIdentifier test) {
    251         mCurrentPkgResult.insertTest(test);
    252     }
    253 
    254     /**
    255      * {@inheritDoc}
    256      */
    257     @Override
    258     public void testFailed(TestFailure status, TestIdentifier test, String trace) {
    259         mCurrentPkgResult.reportTestFailure(test, CtsTestStatus.FAIL, trace);
    260     }
    261 
    262     /**
    263      * {@inheritDoc}
    264      */
    265     @Override
    266     public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
    267         collectCtsResults(test, testMetrics);
    268         mCurrentPkgResult.reportTestEnded(test);
    269         Test result = mCurrentPkgResult.findTest(test);
    270         String stack = result.getStackTrace() == null ? "" : "\n" + result.getStackTrace();
    271         logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getResult(),
    272                 stack);
    273     }
    274 
    275     /**
    276      * Collect Cts results for both device and host tests to the package result.
    277      * @param test test ran
    278      * @param testMetrics test metrics which can contain performance result for device tests
    279      */
    280     private void collectCtsResults(TestIdentifier test, Map<String, String> testMetrics) {
    281         // device test can have performance results in testMetrics
    282         String perfResult = CtsReportUtil.getCtsResultFromMetrics(testMetrics);
    283         // host test should be checked in CtsHostStore.
    284         if (perfResult == null) {
    285             perfResult = CtsHostStore.removeCtsResult(mDeviceSerial, test.toString());
    286         }
    287         if (perfResult != null) {
    288             // CTS result is passed in Summary++++Details format.
    289             // Extract Summary and Details, and pass them.
    290             Matcher m = mCtsLogPattern.matcher(perfResult);
    291             if (m.find()) {
    292                 mCurrentPkgResult.reportPerformanceResult(test, CtsTestStatus.PASS, m.group(1),
    293                         m.group(2));
    294             } else {
    295                 logResult("CTS Result unrecognizable:" + perfResult);
    296             }
    297         }
    298     }
    299 
    300     /**
    301      * {@inheritDoc}
    302      */
    303     @Override
    304     public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
    305         if (mIsDeviceInfoRun) {
    306             mResults.populateDeviceInfoMetrics(runMetrics);
    307         } else {
    308             mCurrentPkgResult.populateMetrics(runMetrics);
    309         }
    310     }
    311 
    312     /**
    313      * {@inheritDoc}
    314      */
    315     @Override
    316     public void invocationEnded(long elapsedTime) {
    317         // display the results of the last completed run
    318         if (mCurrentPkgResult != null) {
    319             logCompleteRun(mCurrentPkgResult);
    320         }
    321         if (mReportDir == null || mStartTime == null) {
    322             // invocationStarted must have failed, abort
    323             CLog.w("Unable to create XML report");
    324             return;
    325         }
    326 
    327         File reportFile = getResultFile(mReportDir);
    328         createXmlResult(reportFile, mStartTime, elapsedTime);
    329         copyFormattingFiles(mReportDir);
    330         zipResults(mReportDir);
    331 
    332         try {
    333             mReporter.reportResult(reportFile);
    334         } catch (IOException e) {
    335             CLog.e(e);
    336         }
    337     }
    338 
    339     private void logResult(String format, Object... args) {
    340         if (mQuietOutput) {
    341             CLog.i(format, args);
    342         } else {
    343             Log.logAndDisplay(LogLevel.INFO, mDeviceSerial, String.format(format, args));
    344         }
    345     }
    346 
    347     private void logCompleteRun(TestPackageResult pkgResult) {
    348         if (pkgResult.getAppPackageName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
    349             logResult("Device info collection complete");
    350             return;
    351         }
    352         logResult("%s package complete: Passed %d, Failed %d, Not Executed %d",
    353                 pkgResult.getAppPackageName(), pkgResult.countTests(CtsTestStatus.PASS),
    354                 pkgResult.countTests(CtsTestStatus.FAIL),
    355                 pkgResult.countTests(CtsTestStatus.NOT_EXECUTED));
    356     }
    357 
    358     /**
    359      * Creates a report file and populates it with the report data from the completed tests.
    360      */
    361     private void createXmlResult(File reportFile, String startTimestamp, long elapsedTime) {
    362         String endTime = getTimestamp();
    363         OutputStream stream = null;
    364         try {
    365             stream = createOutputResultStream(reportFile);
    366             KXmlSerializer serializer = new KXmlSerializer();
    367             serializer.setOutput(stream, "UTF-8");
    368             serializer.startDocument("UTF-8", false);
    369             serializer.setFeature(
    370                     "http://xmlpull.org/v1/doc/features.html#indent-output", true);
    371             serializer.processingInstruction("xml-stylesheet type=\"text/xsl\"  " +
    372                     "href=\"cts_result.xsl\"");
    373             serializeResultsDoc(serializer, startTimestamp, endTime);
    374             serializer.endDocument();
    375             String msg = String.format("XML test result file generated at %s. Passed %d, " +
    376                     "Failed %d, Not Executed %d", mReportDir.getName(),
    377                     mResults.countTests(CtsTestStatus.PASS),
    378                     mResults.countTests(CtsTestStatus.FAIL),
    379                     mResults.countTests(CtsTestStatus.NOT_EXECUTED));
    380             logResult(msg);
    381             logResult("Time: %s", TimeUtil.formatElapsedTime(elapsedTime));
    382         } catch (IOException e) {
    383             Log.e(LOG_TAG, "Failed to generate report data");
    384         } finally {
    385             StreamUtil.closeStream(stream);
    386         }
    387     }
    388 
    389     /**
    390      * Output the results XML.
    391      *
    392      * @param serializer the {@link KXmlSerializer} to use
    393      * @param startTime the user-friendly starting time of the test invocation
    394      * @param endTime the user-friendly ending time of the test invocation
    395      * @throws IOException
    396      */
    397     private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime)
    398             throws IOException {
    399         serializer.startTag(ns, RESULT_TAG);
    400         serializer.attribute(ns, PLAN_ATTR, mPlanName);
    401         serializer.attribute(ns, STARTTIME_ATTR, startTime);
    402         serializer.attribute(ns, "endtime", endTime);
    403         serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION);
    404         serializer.attribute(ns, "suite", mSuiteName);
    405 
    406         mResults.serialize(serializer);
    407         // TODO: not sure why, but the serializer doesn't like this statement
    408         //serializer.endTag(ns, RESULT_TAG);
    409     }
    410 
    411     private File getResultFile(File reportDir) {
    412         return new File(reportDir, TEST_RESULT_FILE_NAME);
    413     }
    414 
    415     /**
    416      * Creates the output stream to use for test results. Exposed for mocking.
    417      */
    418     OutputStream createOutputResultStream(File reportFile) throws IOException {
    419         logResult("Created xml report file at file://%s", reportFile.getAbsolutePath());
    420         return new FileOutputStream(reportFile);
    421     }
    422 
    423     /**
    424      * Copy the xml formatting files stored in this jar to the results directory
    425      *
    426      * @param resultsDir
    427      */
    428     private void copyFormattingFiles(File resultsDir) {
    429         for (String resultFileName : CTS_RESULT_RESOURCES) {
    430             InputStream configStream = getClass().getResourceAsStream(String.format("/report/%s",
    431                     resultFileName));
    432             if (configStream != null) {
    433                 File resultFile = new File(resultsDir, resultFileName);
    434                 try {
    435                     FileUtil.writeToFile(configStream, resultFile);
    436                 } catch (IOException e) {
    437                     Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName));
    438                 }
    439             } else {
    440                 Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName));
    441             }
    442         }
    443     }
    444 
    445     /**
    446      * Zip the contents of the given results directory.
    447      *
    448      * @param resultsDir
    449      */
    450     private void zipResults(File resultsDir) {
    451         try {
    452             // create a file in parent directory, with same name as resultsDir
    453             File zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
    454                     resultsDir.getName()));
    455             FileUtil.createZip(resultsDir, zipResultFile);
    456         } catch (IOException e) {
    457             Log.w(LOG_TAG, String.format("Failed to create zip for %s", resultsDir.getName()));
    458         }
    459     }
    460 
    461     /**
    462      * Get a String version of the current time.
    463      * <p/>
    464      * Exposed so unit tests can mock.
    465      */
    466     String getTimestamp() {
    467         return TimeUtil.getTimestamp();
    468     }
    469 
    470     /**
    471      * {@inheritDoc}
    472      */
    473     @Override
    474     public void testRunFailed(String errorMessage) {
    475         // ignore
    476     }
    477 
    478     /**
    479      * {@inheritDoc}
    480      */
    481     @Override
    482     public void testRunStopped(long elapsedTime) {
    483         // ignore
    484     }
    485 
    486     /**
    487      * {@inheritDoc}
    488      */
    489     @Override
    490     public void invocationFailed(Throwable cause) {
    491         // ignore
    492     }
    493 
    494     /**
    495      * {@inheritDoc}
    496      */
    497     @Override
    498     public TestSummary getSummary() {
    499         return null;
    500     }
    501 }
    502