IssueReporter.java revision c43a7c46d8acf98365689d8a69232ca8adad651b
1/* 2 * Copyright (C) 2011 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 17package com.android.cts.tradefed.result; 18 19import com.android.ddmlib.testrunner.TestIdentifier; 20import com.android.tradefed.build.IBuildInfo; 21import com.android.tradefed.config.Option; 22import com.android.tradefed.log.LogUtil.CLog; 23import com.android.tradefed.result.ITestInvocationListener; 24import com.android.tradefed.result.InputStreamSource; 25import com.android.tradefed.result.LogDataType; 26import com.android.tradefed.result.TestSummary; 27 28import java.io.ByteArrayOutputStream; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.OutputStream; 32import java.io.OutputStreamWriter; 33import java.io.PrintWriter; 34import java.net.HttpURLConnection; 35import java.net.URL; 36import java.util.Map; 37import java.util.concurrent.Callable; 38import java.util.concurrent.ExecutorService; 39import java.util.concurrent.Executors; 40import java.util.concurrent.TimeUnit; 41import java.util.zip.GZIPOutputStream; 42 43/** 44 * Class that sends a HTTP POST multipart/form-data request containing details 45 * about a test failure. 46 */ 47public class IssueReporter implements ITestInvocationListener { 48 49 private static final String FORM_DATA_BOUNDARY = "C75I55u3R3p0r73r"; 50 private static final int BUGREPORT_SIZE = 500 * 1024; 51 52 private static final String PRODUCT_NAME_KEY = "buildName"; 53 private static final String BUILD_TYPE_KEY = "build_type"; 54 private static final String BUILD_ID_KEY = "buildID"; 55 56 @Option(name = "issue-server", description = "Server url to post test failures to.") 57 private String mServerUrl; 58 59 private final ExecutorService mReporterService = Executors.newCachedThreadPool(); 60 private Issue mCurrentIssue = new Issue(); 61 62 @Override 63 public void testFailed(TestFailure status, TestIdentifier test, String trace) { 64 mCurrentIssue.mTestName = test.toString(); 65 mCurrentIssue.mStackTrace = trace; 66 } 67 68 @Override 69 public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) { 70 if (dataName.startsWith("bug-")) { 71 try { 72 setBugReport(dataStream); 73 } catch (IOException e) { 74 CLog.e(e); 75 } 76 } 77 } 78 79 /** 80 * Set the bug report for the current test failure. GZip it to save space. 81 * This is only called when the --bugreport option is enabled. 82 */ 83 private void setBugReport(InputStreamSource dataStream) throws IOException { 84 InputStream input = dataStream.createInputStream(); 85 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(BUGREPORT_SIZE); 86 GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput); 87 for (byte[] buffer = new byte[1024]; input.read(buffer) >= 0; ) { 88 gzipOutput.write(buffer); 89 } 90 gzipOutput.close(); 91 92 // Only one bug report can be stored at a time and they are gzipped to 93 // about 0.5 MB so there shoudn't be any memory leak bringing down CTS. 94 mCurrentIssue.mBugReport = byteOutput.toByteArray(); 95 } 96 97 @Override 98 public void testEnded(TestIdentifier test, Map<String, String> testMetrics) { 99 mReporterService.submit(mCurrentIssue); 100 mCurrentIssue = new Issue(); 101 } 102 103 104 @Override 105 public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { 106 setDeviceMetrics(runMetrics); 107 } 108 109 /** Set device information. Populated once when the device info app runs. */ 110 private void setDeviceMetrics(Map<String, String> metrics) { 111 if (metrics.containsKey(BUILD_ID_KEY)) { 112 mCurrentIssue.mBuildId = metrics.get(BUILD_ID_KEY); 113 } 114 if (metrics.containsKey(BUILD_TYPE_KEY)) { 115 mCurrentIssue.mBuildType = metrics.get(BUILD_TYPE_KEY); 116 } 117 if (metrics.containsKey(PRODUCT_NAME_KEY)) { 118 mCurrentIssue.mProductName = metrics.get(PRODUCT_NAME_KEY); 119 } 120 } 121 122 @Override 123 public void invocationEnded(long elapsedTime) { 124 try { 125 mReporterService.shutdown(); 126 if (!mReporterService.awaitTermination(1, TimeUnit.MINUTES)) { 127 CLog.i("Some issues could not be reported..."); 128 } 129 } catch (InterruptedException e) { 130 CLog.e(e); 131 } 132 } 133 134 class Issue implements Callable<Void> { 135 136 private String mBuildId; 137 private String mBuildType; 138 private String mProductName; 139 private String mTestName; 140 private String mStackTrace; 141 private byte[] mBugReport; 142 143 @Override 144 public Void call() throws Exception { 145 if (isEmpty(mServerUrl) 146 || isEmpty(mBuildId) 147 || isEmpty(mBuildType) 148 || isEmpty(mProductName) 149 || isEmpty(mTestName) 150 || isEmpty(mStackTrace)) { 151 return null; 152 } 153 154 HttpURLConnection connection = null; 155 156 try { 157 URL url = new URL(mServerUrl); 158 connection = (HttpURLConnection) url.openConnection(); 159 connection.setRequestMethod("POST"); 160 connection.setDoOutput(true); 161 connection.setRequestProperty("Content-Type", 162 "multipart/form-data; boundary=" + FORM_DATA_BOUNDARY); 163 164 byte[] body = getContentBody(); 165 connection.setRequestProperty("Content-Length", Integer.toString(body.length)); 166 167 OutputStream output = connection.getOutputStream(); 168 output.write(body); 169 output.close(); 170 171 // Open the stream to get a response. Otherwise request will be cancelled. 172 InputStream input = connection.getInputStream(); 173 input.close(); 174 175 } finally { 176 if (connection != null) { 177 connection.disconnect(); 178 } 179 } 180 181 return null; 182 } 183 184 private boolean isEmpty(String value) { 185 return value == null || value.trim().isEmpty(); 186 } 187 188 private byte[] getContentBody() throws IOException { 189 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); 190 PrintWriter writer = new PrintWriter(new OutputStreamWriter(byteOutput)); 191 writer.println(); 192 writeFormField(writer, "productName", mProductName); 193 writeFormField(writer, "buildType", mBuildType); 194 writeFormField(writer, "buildId", mBuildId); 195 writeFormField(writer, "testName", mTestName); 196 writeFormField(writer, "stackTrace", mStackTrace); 197 if (mBugReport != null) { 198 writeFormFileHeader(writer, "bugReport", "bugReport.txt.gz"); 199 writer.flush(); // Must flush here before writing to the byte stream! 200 byteOutput.write(mBugReport); 201 writer.println(); 202 } 203 writer.append("--").append(FORM_DATA_BOUNDARY).println("--"); 204 writer.flush(); 205 writer.close(); 206 return byteOutput.toByteArray(); 207 } 208 209 private void writeFormField(PrintWriter writer, String name, String value) { 210 writer.append("--").println(FORM_DATA_BOUNDARY); 211 writer.append("Content-Disposition: form-data; name=\"").append(name).println("\""); 212 writer.println(); 213 writer.println(value); 214 } 215 216 private void writeFormFileHeader(PrintWriter writer, String name, String fileName) { 217 writer.append("--").println(FORM_DATA_BOUNDARY); 218 writer.append("Content-Disposition: form-data; name=\"").append(name); 219 writer.append("\"; filename=\"").append(fileName).println("\""); 220 writer.println("Content-Type: application/x-gzip"); 221 writer.println("Content-Transfer-Encoding: binary"); 222 writer.println(); 223 } 224 } 225 226 @Override 227 public void invocationStarted(IBuildInfo buildInfo) { 228 } 229 230 @Override 231 public void testRunStarted(String name, int numTests) { 232 } 233 234 @Override 235 public void testStarted(TestIdentifier test) { 236 } 237 238 @Override 239 public void testRunFailed(String arg0) { 240 } 241 242 @Override 243 public void testRunStopped(long elapsedTime) { 244 } 245 246 @Override 247 public void invocationFailed(Throwable cause) { 248 } 249 250 @Override 251 public TestSummary getSummary() { 252 return null; 253 } 254} 255