blob: 146f285d6d9bea5236001aefa7d8da2271355203 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.result;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.net.HttpHelper;
import com.android.tradefed.util.net.IHttpHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A result reporter that encode test metrics results and branch, device info into JSON and POST
* into an HTTP service endpoint
*/
@OptionClass(alias = "json-reporter")
public class JsonHttpTestResultReporter extends CollectingTestListener {
/** separator for class name and method name when encoding test identifier */
private final static String SEPARATOR = "#";
private final static String RESULT_SEPARATOR = "##";
/** constants used as keys in JSON results to be posted to remote end */
private final static String KEY_METRICS = "metrics";
private final static String KEY_BRANCH = "branch";
private final static String KEY_BUILD_FLAVOR = "build_flavor";
private final static String KEY_BUILD_ID = "build_id";
private final static String KEY_RESULTS_NAME = "results_name";
private final static String KEY_DEVICE_NAME = "device_name";
private final static String KEY_SDK_RELEASE_NAME = "sdk_release_name";
private final static String DEVICE_NAME_PROPERTY = "ro.product.device";
private final static String SDK_VERSION_PROPERTY = "ro.build.version.sdk";
private final static String BUILD_ID_PROPERTY = "ro.build.id";
private final static String SDK_BUILDID_FORMAT = "API_%s_%c";
/** timeout for HTTP connection to posting endpoint */
private final static int CONNECTION_TIMEOUT_MS = 60 * 1000;
@Option(name = "include-run-name", description = "include test run name in reporting unit")
private boolean mIncludeRunName = false;
@Option(name = "posting-endpoint", description = "url for the HTTP data posting endpoint",
importance = Importance.ALWAYS)
private String mPostingEndpoint;
@Option(name = "disable", description =
"flag to skip reporting of all the results")
private boolean mSkipReporting = false;
@Option(name = "reporting-unit-key-suffix",
description = "suffix to append after the regular reporting unit key")
private String mReportingUnitKeySuffix = null;
@Option(
name = "skip-failed-runs",
description = "flag to skip reporting results from failed runs"
)
private boolean mSkipFailedRuns = false;
@Option(name = "include-device-details", description = "Enabling this flag will parse"
+ " additional device details such as device name, sdk version and build id.")
private boolean mDeviceDetails = false;
@Option(
name = "additional-key-value-pairs",
description = "Map of additional key/value pairs to be added to the results.")
private Map<String, String> mAdditionalKeyValuePairs = new LinkedHashMap<>();
private boolean mHasInvocationFailures = false;
private IInvocationContext mInvocationContext = null;
private String mDeviceName = null;
private String mSdkBuildId = null;
@Override
public void invocationStarted(IInvocationContext context) {
super.invocationStarted(context);
mInvocationContext = context;
if (mDeviceDetails) {
parseAdditionalDeviceDetails(getDevice(context));
}
}
@Override
public void invocationFailed(Throwable cause) {
super.invocationFailed(cause);
mHasInvocationFailures = true;
}
@Override
public void invocationEnded(long elapsedTime) {
super.invocationEnded(elapsedTime);
if (mSkipReporting) {
CLog.d("Skipping reporting because it's disabled.");
} else if (mHasInvocationFailures) {
CLog.d("Skipping reporting beacuse there are invocation failures.");
} else {
try {
postResults(convertMetricsToJson(getMergedTestRunResults()));
} catch (JSONException e) {
CLog.e("JSONException while converting test metrics.");
CLog.e(e);
}
}
}
protected ITestDevice getDevice(IInvocationContext context) {
return context.getDevices().get(0);
}
/**
* Retrieves the device name, sdk version number and the build id from
* the test device.
*
* @param testDevice device to collect the information from.
*/
protected void parseAdditionalDeviceDetails(ITestDevice testDevice) {
try {
// Get the device name.
mDeviceName = testDevice.getProperty(DEVICE_NAME_PROPERTY);
// Get the version name and the first letter of the build id.
// Sample output: API_29_Q
mSdkBuildId = String.format(SDK_BUILDID_FORMAT,
testDevice.getProperty(SDK_VERSION_PROPERTY),
testDevice.getProperty(BUILD_ID_PROPERTY).charAt(0));
} catch (DeviceNotAvailableException e) {
CLog.e("Error in parsing additional additional device info.");
CLog.e(e);
}
}
/**
* Post data to the specified HTTP endpoint
* @param postData data to be posted
*/
protected void postResults(JSONObject postData) {
IHttpHelper helper = new HttpHelper();
OutputStream outputStream = null;
String data = postData.toString();
CLog.d("Attempting to post %s: Data: '%s'", mPostingEndpoint, data);
try {
HttpURLConnection conn = helper.createJsonConnection(
new URL(mPostingEndpoint), "POST");
conn.setConnectTimeout(CONNECTION_TIMEOUT_MS);
conn.setReadTimeout(CONNECTION_TIMEOUT_MS);
outputStream = conn.getOutputStream();
outputStream.write(data.getBytes());
String response = StreamUtil.getStringFromStream(conn.getInputStream()).trim();
int responseCode = conn.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
// log an error but don't do any explicit exceptions if response code is not 2xx
CLog.e("Posting failure. code: %d, response: %s", responseCode, response);
} else {
IBuildInfo buildInfo = mInvocationContext.getBuildInfos().get(0);
CLog.d("Successfully posted results, build: %s, raw data: %s",
buildInfo.getBuildId(), postData);
}
} catch (IOException e) {
CLog.e("IOException occurred while reporting to HTTP endpoint: %s", mPostingEndpoint);
CLog.e(e);
} finally {
StreamUtil.close(outputStream);
}
}
/**
* A util method that converts test metrics and invocation context to json format
*/
JSONObject convertMetricsToJson(Collection<TestRunResult> runResults) throws JSONException {
JSONObject allTestMetrics = new JSONObject();
StringBuffer resultsName = new StringBuffer();
// loops over all test runs
for (TestRunResult runResult : runResults) {
// If the option to skip failed runs is set, skip failed runs.
if (mSkipFailedRuns && runResult.isRunFailure()) {
continue;
}
// Parse run metrics
if (runResult.getRunMetrics().size() > 0) {
JSONObject runResultMetrics = new JSONObject(
getValidMetrics(runResult.getRunMetrics()));
String reportingUnit = runResult.getName();
if (mReportingUnitKeySuffix != null && !mReportingUnitKeySuffix.isEmpty()) {
reportingUnit += mReportingUnitKeySuffix;
}
allTestMetrics.put(reportingUnit, runResultMetrics);
resultsName.append(String.format("%s%s", reportingUnit, RESULT_SEPARATOR));
}
// Parse test metrics
Map<TestDescription, TestResult> testResultMap = runResult.getTestResults();
for (Entry<TestDescription, TestResult> entry : testResultMap.entrySet()) {
TestDescription testDescription = entry.getKey();
TestResult testResult = entry.getValue();
List<String> reportingUnitParts =
Arrays.asList(
testDescription.getClassName(), testDescription.getTestName());
if (mIncludeRunName) {
reportingUnitParts.add(0, runResult.getName());
}
String reportingUnit = String.join(SEPARATOR, reportingUnitParts);
if (mReportingUnitKeySuffix != null && !mReportingUnitKeySuffix.isEmpty()) {
reportingUnit += mReportingUnitKeySuffix;
}
resultsName.append(String.format("%s%s", reportingUnit, RESULT_SEPARATOR));
if (testResult.getMetrics().size() > 0) {
JSONObject testResultMetrics = new JSONObject(
getValidMetrics(testResult.getMetrics()));
allTestMetrics.put(reportingUnit, testResultMetrics);
}
}
}
// get build info, and throw an exception if there are multiple (not supporting multi-device
// result reporting
List<IBuildInfo> buildInfos = mInvocationContext.getBuildInfos();
if (buildInfos.isEmpty()) {
throw new IllegalArgumentException("There is no build info");
}
IBuildInfo buildInfo = buildInfos.get(0);
JSONObject result = new JSONObject();
result.put(KEY_RESULTS_NAME, resultsName);
result.put(KEY_METRICS, allTestMetrics);
result.put(KEY_BRANCH, buildInfo.getBuildBranch());
result.put(KEY_BUILD_FLAVOR, buildInfo.getBuildFlavor());
result.put(KEY_BUILD_ID, buildInfo.getBuildId());
if(mDeviceDetails) {
result.put(KEY_DEVICE_NAME, mDeviceName);
result.put(KEY_SDK_RELEASE_NAME, mSdkBuildId);
}
if (!mAdditionalKeyValuePairs.isEmpty()) {
for (Map.Entry<String, String> pair : mAdditionalKeyValuePairs.entrySet()) {
result.put(pair.getKey(), pair.getValue());
}
}
return result;
}
/**
* Add only the numeric metrics and skip posting the non-numeric metrics.
*
* @param collectedMetrics contains all the metrics.
* @return only the numeric metrics.
*/
public Map<String, String> getValidMetrics(Map<String, String> collectedMetrics) {
Map<String, String> validMetrics = new HashMap<>();
for (Map.Entry<String, String> entry : collectedMetrics.entrySet()) {
try {
Double.parseDouble(entry.getValue());
validMetrics.put(entry.getKey(), entry.getValue());
} catch (Exception e) {
// Skip adding the non numeric metric.
}
}
return validMetrics;
}
}