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 package com.android.tradefed.result;
     17 
     18 import com.android.ddmlib.testrunner.TestIdentifier;
     19 import com.android.ddmlib.testrunner.TestResult;
     20 import com.android.ddmlib.testrunner.TestResult.TestStatus;
     21 import com.android.ddmlib.testrunner.TestRunResult;
     22 import com.android.tradefed.build.IBuildInfo;
     23 import com.android.tradefed.config.Option;
     24 import com.android.tradefed.config.Option.Importance;
     25 import com.android.tradefed.config.OptionClass;
     26 import com.android.tradefed.invoker.IInvocationContext;
     27 import com.android.tradefed.log.LogUtil.CLog;
     28 import com.android.tradefed.targetprep.BuildError;
     29 import com.android.tradefed.util.Email;
     30 import com.android.tradefed.util.IEmail;
     31 import com.android.tradefed.util.IEmail.Message;
     32 import com.android.tradefed.util.StreamUtil;
     33 
     34 import java.io.IOException;
     35 import java.net.InetAddress;
     36 import java.net.UnknownHostException;
     37 import java.util.Collection;
     38 import java.util.HashSet;
     39 import java.util.Iterator;
     40 import java.util.List;
     41 import java.util.Map;
     42 
     43 /**
     44  * A simple result reporter base class that sends emails for test results.<br>
     45  * Subclasses should determine whether an email needs to be sent, and can
     46  * override other behavior.
     47  */
     48 @OptionClass(alias = "email")
     49 public class EmailResultReporter extends CollectingTestListener implements
     50         ITestSummaryListener {
     51     private static final String DEFAULT_SUBJECT_TAG = "Tradefed";
     52     private static final String TEST_FAILURE_STATUS = "FAILED";
     53 
     54     @Option(name = "sender", description = "The envelope-sender address to use for the messages.",
     55             importance = Importance.IF_UNSET)
     56     private String mSender = null;
     57 
     58     @Option(name = "destination", description = "One or more destination addresses.",
     59             importance = Importance.IF_UNSET)
     60     private Collection<String> mDestinations = new HashSet<String>();
     61 
     62     @Option(name = "subject-tag",
     63             description = "The tag to be added to the beginning of the email subject.")
     64     private String mSubjectTag = DEFAULT_SUBJECT_TAG;
     65 
     66     @Option(name = "include-test-failures", description = "If there are some test failures, "
     67             + "this option allows to add them to the email body."
     68             + "To be used with care, as it could be pretty big with traces.")
     69     private boolean mIncludeTestFailures = false;
     70 
     71     private List<TestSummary> mSummaries = null;
     72     private Throwable mInvocationThrowable = null;
     73     private IEmail mMailer;
     74     private boolean mHtml;
     75 
     76     /**
     77      * Create a {@link EmailResultReporter}
     78      */
     79     public EmailResultReporter() {
     80         this(new Email());
     81     }
     82 
     83     /**
     84      * Create a {@link EmailResultReporter} with a custom {@link IEmail} instance to use.
     85      * <p/>
     86      * Exposed for unit testing.
     87      *
     88      * @param mailer the {@link IEmail} instance to use.
     89      */
     90     protected EmailResultReporter(IEmail mailer) {
     91         mMailer = mailer;
     92     }
     93 
     94     /**
     95      * Adds an email destination address.
     96      *
     97      * @param dest
     98      */
     99     public void addDestination(String dest) {
    100         mDestinations.add(dest);
    101     }
    102 
    103 
    104     /**
    105      * {@inheritDoc}
    106      */
    107     @Override
    108     public void putSummary(List<TestSummary> summaries) {
    109         mSummaries = summaries;
    110     }
    111 
    112     /**
    113      * Allow subclasses to get at the summaries we've received
    114      */
    115     protected List<TestSummary> fetchSummaries() {
    116         return mSummaries;
    117     }
    118 
    119     /**
    120      * A method, meant to be overridden, which should do whatever filtering is decided and determine
    121      * whether a notification email should be sent for the test results.  Presumably, would consider
    122      * how many (if any) tests failed, prior failures of the same tests, etc.
    123      *
    124      * @return {@code true} if a notification email should be sent, {@code false} if not
    125      */
    126     protected boolean shouldSendMessage() {
    127         return true;
    128     }
    129 
    130     /**
    131      * A method to generate the subject for email reports. Will not be called if
    132      * {@link #shouldSendMessage()} returns {@code false}.
    133      * <p />
    134      * Sample email subjects:
    135      * <ul>
    136      *   <li>"Tradefed result for powerChromeFullSitesLocal on mantaray-user git_jb-mr1.1-release
    137      *       JDQ39: FAILED"</li>
    138      *   <li>"Tradefed result for Monkey on build 25: FAILED"</li>
    139      * </ul>
    140      *
    141      * @return A {@link String} containing the subject to use for an email
    142      *         report
    143      */
    144     protected String generateEmailSubject() {
    145         final IInvocationContext context = getInvocationContext();
    146         final StringBuilder subj = new StringBuilder(mSubjectTag);
    147 
    148         subj.append(" result for ");
    149 
    150         if (!appendUnlessNull(subj, context.getTestTag())) {
    151             subj.append("(unknown suite)");
    152         }
    153 
    154         subj.append(" on ");
    155         for (IBuildInfo build : context.getBuildInfos()) {
    156             subj.append(build.toString());
    157         }
    158 
    159         subj.append(": ");
    160         subj.append(getInvocationOrTestStatus());
    161         return subj.toString();
    162     }
    163 
    164     /**
    165      * Appends {@code str + " "} to {@code builder} IFF {@code str} is not null.
    166      * @return {@code true} if str is not null, {@code false} if str is null.
    167      */
    168     private boolean appendUnlessNull(StringBuilder builder, String str) {
    169         if (str == null) {
    170             return false;
    171         } else {
    172             builder.append(str);
    173             builder.append(" ");
    174             return true;
    175         }
    176     }
    177 
    178     protected String getInvocationOrTestStatus() {
    179         InvocationStatus invStatus = getInvocationStatus();
    180         // if invocation status is not success, report invocation status
    181         if (!InvocationStatus.SUCCESS.equals(invStatus)) {
    182             // special-case invocation failure and report as "ERROR" to avoid confusion with
    183             // test failures
    184             if (InvocationStatus.FAILED.equals(invStatus)) {
    185                 return "ERROR";
    186             }
    187             return invStatus.toString();
    188         }
    189         if (hasFailedTests()) {
    190             return TEST_FAILURE_STATUS;
    191         }
    192         return invStatus.toString(); // should be success at this point
    193     }
    194 
    195     /**
    196      * Returns the {@link InvocationStatus}
    197      */
    198     protected InvocationStatus getInvocationStatus() {
    199         if (mInvocationThrowable == null) {
    200             return InvocationStatus.SUCCESS;
    201         } else if (mInvocationThrowable instanceof BuildError) {
    202             return InvocationStatus.BUILD_ERROR;
    203         } else {
    204             return InvocationStatus.FAILED;
    205         }
    206     }
    207 
    208     /**
    209      * Returns the {@link Throwable} passed via {@link #invocationFailed(Throwable)}.
    210      */
    211     protected Throwable getInvocationException() {
    212         return mInvocationThrowable;
    213     }
    214 
    215     /**
    216      * A method to generate the body for email reports.  Will not be called if
    217      * {@link #shouldSendMessage()} returns {@code false}.
    218      *
    219      * @return A {@link String} containing the body to use for an email report
    220      */
    221     protected String generateEmailBody() {
    222         StringBuilder bodyBuilder = new StringBuilder();
    223 
    224         for (IBuildInfo build : getInvocationContext().getBuildInfos()) {
    225             bodyBuilder.append(String.format("Device %s:\n", build.getDeviceSerial()));
    226             for (Map.Entry<String, String> buildAttr : build.getBuildAttributes().entrySet()) {
    227                 bodyBuilder.append(buildAttr.getKey());
    228                 bodyBuilder.append(": ");
    229                 bodyBuilder.append(buildAttr.getValue());
    230                 bodyBuilder.append("\n");
    231             }
    232         }
    233         bodyBuilder.append("host: ");
    234         try {
    235             bodyBuilder.append(InetAddress.getLocalHost().getHostName());
    236         } catch (UnknownHostException e) {
    237             bodyBuilder.append("unknown");
    238             CLog.e(e);
    239         }
    240         bodyBuilder.append("\n\n");
    241 
    242         if (mInvocationThrowable != null) {
    243             bodyBuilder.append("Invocation failed: ");
    244             bodyBuilder.append(StreamUtil.getStackTrace(mInvocationThrowable));
    245             bodyBuilder.append("\n");
    246         }
    247         bodyBuilder.append(String.format("Test results:  %d passed, %d failed\n\n",
    248                 getNumTestsInState(TestStatus.PASSED), getNumAllFailedTests()));
    249 
    250         // During a Test Failure, the current run results will not appear in getRunResults()
    251         // This may be fairly big and we are not sure of email body max size, so limiting usage
    252         // with the option.
    253         if (hasFailedTests() && mIncludeTestFailures) {
    254             TestRunResult res = getCurrentRunResults();
    255             for (TestIdentifier tid : res.getTestResults().keySet()) {
    256                 TestResult tr = res.getTestResults().get(tid);
    257                 if (TestStatus.FAILURE.equals(tr.getStatus())) {
    258                     bodyBuilder.append(String.format("Test Identifier: %s\nStack: %s", tid,
    259                             tr.getStackTrace()));
    260                     bodyBuilder.append("\n");
    261                 }
    262             }
    263         }
    264         bodyBuilder.append("\n");
    265 
    266         if (mSummaries != null) {
    267             for (TestSummary summary : mSummaries) {
    268                 bodyBuilder.append("Invocation summary report: ");
    269                 bodyBuilder.append(summary.getSummary().getString());
    270                 if (!summary.getKvEntries().isEmpty()) {
    271                     bodyBuilder.append("\".\nSummary key-value dump:\n");
    272                     bodyBuilder.append(summary.getKvEntries().toString());
    273                 }
    274             }
    275         }
    276         return bodyBuilder.toString();
    277     }
    278 
    279     /**
    280      * A method to set a flag indicating that the email body is in HTML rather than plain text
    281      *
    282      * This method must be called before the email body is generated
    283      *
    284      * @param html true if the body is html
    285      */
    286     protected void setHtml(boolean html) {
    287         mHtml = html;
    288     }
    289 
    290     protected boolean isHtml() {
    291         return mHtml;
    292     }
    293 
    294     @Override
    295     public void invocationFailed(Throwable t) {
    296         mInvocationThrowable = t;
    297     }
    298 
    299     /**
    300      * {@inheritDoc}
    301      */
    302     @Override
    303     public void invocationEnded(long elapsedTime) {
    304         super.invocationEnded(elapsedTime);
    305         if (!shouldSendMessage()) {
    306             return;
    307         }
    308 
    309         if (mDestinations.isEmpty()) {
    310             CLog.i("No destinations set, not sending any emails");
    311             return;
    312         }
    313 
    314         Message msg = new Message();
    315         msg.setSender(mSender);
    316         msg.setSubject(generateEmailSubject());
    317         msg.setBody(generateEmailBody());
    318         msg.setHtml(isHtml());
    319         Iterator<String> toAddress = mDestinations.iterator();
    320         while (toAddress.hasNext()) {
    321             msg.addTo(toAddress.next());
    322         }
    323 
    324         try {
    325             mMailer.send(msg);
    326         } catch (IllegalArgumentException e) {
    327             CLog.e("Failed to send email");
    328             CLog.e(e);
    329         } catch (IOException e) {
    330             CLog.e("Failed to send email");
    331             CLog.e(e);
    332         }
    333     }
    334 }
    335