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