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