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