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