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