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