blob: 8a3fcb158d7f93e161f9eb5143881a1b5e6a8e90 [file] [log] [blame]
/*
* Copyright (C) 2017 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.compatibility.common.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.AssumptionViolatedException;
/**
* Helper and constants accessible to host and device components that enable Business Logic
* configuration
*/
public class BusinessLogic {
// Device location to which business logic data is pushed
public static final String DEVICE_FILE = "/sdcard/bl";
/* A map from testcase name to the business logic rules for the test case */
protected Map<String, List<BusinessLogicRulesList>> mRules;
/* Feature flag determining if device specific tests are executed. */
public boolean mConditionalTestsEnabled;
private AuthenticationStatusEnum mAuthenticationStatus = AuthenticationStatusEnum.UNKNOWN;
// A Date denoting the time of request from the business logic service
protected Date mTimestamp;
/**
* Determines whether business logic exists for a given test name
* @param testName the name of the test case, prefixed by fully qualified class name, then '#'.
* For example, "com.android.foo.FooTest#testFoo"
* @return whether business logic exists for this test for this suite
*/
public boolean hasLogicFor(String testName) {
List<BusinessLogicRulesList> rulesLists = mRules.get(testName);
return rulesLists != null && !rulesLists.isEmpty();
}
/**
* Return whether multiple rule lists exist in the BusinessLogic for this test name.
*/
private boolean hasLogicsFor(String testName) {
List<BusinessLogicRulesList> rulesLists = mRules.get(testName);
return rulesLists != null && rulesLists.size() > 1;
}
/**
* Apply business logic for the given test.
* @param testName the name of the test case, prefixed by fully qualified class name, then '#'.
* For example, "com.android.foo.FooTest#testFoo"
* @param executor a {@link BusinessLogicExecutor}
*/
public void applyLogicFor(String testName, BusinessLogicExecutor executor) {
if (!hasLogicFor(testName)) {
return;
}
if (hasLogicsFor(testName)) {
applyLogicsFor(testName, executor); // handle this special case separately
return;
}
// expecting exactly one rules list at this point
BusinessLogicRulesList rulesList = mRules.get(testName).get(0);
rulesList.invokeRules(executor);
}
/**
* Handle special case in which multiple rule lists exist for the test name provided.
* Execute each rule list in a sandbox and store an exception for each rule list that
* triggers failure or skipping for the test.
* If all rule lists trigger skipping, rethrow AssumptionViolatedException to report a 'skip'
* for the test as a whole.
* If one or more rule lists trigger failure, rethrow RuntimeException with a list containing
* each failure.
*/
private void applyLogicsFor(String testName, BusinessLogicExecutor executor) {
Map<String, RuntimeException> failedMap = new HashMap<>();
Map<String, RuntimeException> skippedMap = new HashMap<>();
List<BusinessLogicRulesList> rulesLists = mRules.get(testName);
for (int index = 0; index < rulesLists.size(); index++) {
BusinessLogicRulesList rulesList = rulesLists.get(index);
String description = cleanDescription(rulesList.getDescription(), index);
try {
rulesList.invokeRules(executor);
} catch (RuntimeException re) {
if (AssumptionViolatedException.class.isInstance(re)) {
skippedMap.put(description, re);
executor.logInfo("Test %s (%s) skipped for reason: %s", testName, description,
re.getMessage());
} else {
failedMap.put(description, re);
}
}
}
if (skippedMap.size() == rulesLists.size()) {
throwAggregatedException(skippedMap, false);
} else if (failedMap.size() > 0) {
throwAggregatedException(failedMap, true);
} // else this test should be reported as a pure pass
}
/**
* Helper to aggregate the messages of many {@link RuntimeException}s, and optionally their
* stack traces, before throwing an exception.
* @param exceptions a map from description strings to exceptions. The descriptive keySet is
* used to differentiate which BusinessLogicRulesList caused which exception
* @param failed whether to trigger failure. When false, throws assumption failure instead, and
* excludes stack traces from the exception message.
*/
private static void throwAggregatedException(Map<String, RuntimeException> exceptions,
boolean failed) {
Set<String> keySet = exceptions.keySet();
String[] descriptions = keySet.toArray(new String[keySet.size()]);
StringBuilder msg = new StringBuilder("");
msg.append(String.format("Test %s for cases: ", (failed) ? "failed" : "skipped"));
msg.append(String.join(", ", descriptions));
msg.append("\nReasons include:");
for (String description : descriptions) {
RuntimeException re = exceptions.get(description);
msg.append(String.format("\nMessage [%s]: %s", description, re.getMessage()));
if (failed) {
StringWriter sw = new StringWriter();
re.printStackTrace(new PrintWriter(sw));
msg.append(String.format("\nStack Trace: %s", sw.toString()));
}
}
if (failed) {
throw new RuntimeException(msg.toString());
} else {
throw new AssumptionViolatedException(msg.toString());
}
}
/**
* Helper method to generate a meaningful description in case the provided description is null
* or empty. In this case, returns a string representation of the index provided.
*/
private String cleanDescription(String description, int index) {
return (description == null || description.length() == 0)
? Integer.toString(index)
: description;
}
public void setAuthenticationStatus(String authenticationStatus) {
try {
mAuthenticationStatus = Enum.valueOf(AuthenticationStatusEnum.class,
authenticationStatus);
} catch (IllegalArgumentException e) {
// Invalid value, set to unknown
mAuthenticationStatus = AuthenticationStatusEnum.UNKNOWN;
}
}
public boolean isAuthorized() {
return AuthenticationStatusEnum.AUTHORIZED.equals(mAuthenticationStatus);
}
public Date getTimestamp() {
return mTimestamp;
}
/**
* Builds a user readable string tha explains the authentication status and the effect on tests
* which require authentication to execute.
*/
public String getAuthenticationStatusMessage() {
switch (mAuthenticationStatus) {
case AUTHORIZED:
return "Authorized";
case NOT_AUTHENTICATED:
return "authorization failed, please ensure the service account key is "
+ "properly installed.";
case NOT_AUTHORIZED:
return "service account is not authorized to access information for this device. "
+ "Please verify device properties are set correctly and account "
+ "permissions are configured to the Business Logic Api.";
case NO_DEVICE_INFO:
return "unable to read device info files. Retry without --skip-device-info flag.";
default:
return "something went wrong, please try again.";
}
}
/**
* A list of BusinessLogicRules, wrapped with an optional description to differentiate rule
* lists that apply to the same test.
*/
protected static class BusinessLogicRulesList {
/* Stored description and rules */
protected List<BusinessLogicRule> mRulesList;
protected String mDescription;
public BusinessLogicRulesList(List<BusinessLogicRule> rulesList) {
mRulesList = rulesList;
}
public BusinessLogicRulesList(List<BusinessLogicRule> rulesList, String description) {
mRulesList = rulesList;
mDescription = description;
}
public String getDescription() {
return mDescription;
}
public List<BusinessLogicRule> getRules() {
return mRulesList;
}
public void invokeRules(BusinessLogicExecutor executor) {
for (BusinessLogicRule rule : mRulesList) {
// Check conditions
if (rule.invokeConditions(executor)) {
rule.invokeActions(executor);
}
}
}
}
/**
* Nested class representing an Business Logic Rule. Stores a collection of conditions
* and actions for later invokation.
*/
protected static class BusinessLogicRule {
/* Stored conditions and actions */
protected List<BusinessLogicRuleCondition> mConditions;
protected List<BusinessLogicRuleAction> mActions;
public BusinessLogicRule(List<BusinessLogicRuleCondition> conditions,
List<BusinessLogicRuleAction> actions) {
mConditions = conditions;
mActions = actions;
}
/**
* Method that invokes all Business Logic conditions for this rule, and returns true
* if all conditions evaluate to true.
*/
public boolean invokeConditions(BusinessLogicExecutor executor) {
for (BusinessLogicRuleCondition condition : mConditions) {
if (!condition.invoke(executor)) {
return false;
}
}
return true;
}
/**
* Method that invokes all Business Logic actions for this rule
*/
public void invokeActions(BusinessLogicExecutor executor) {
for (BusinessLogicRuleAction action : mActions) {
action.invoke(executor);
}
}
}
/**
* Nested class representing an Business Logic Rule Condition. Stores the name of a method
* to invoke, as well as String args to use during invokation.
*/
protected static class BusinessLogicRuleCondition {
/* Stored method name and String args */
protected String mMethodName;
protected List<String> mMethodArgs;
/* Whether or not the boolean result of this condition should be reversed */
protected boolean mNegated;
public BusinessLogicRuleCondition(String methodName, List<String> methodArgs,
boolean negated) {
mMethodName = methodName;
mMethodArgs = methodArgs;
mNegated = negated;
}
/**
* Invoke this Business Logic condition with an executor.
*/
public boolean invoke(BusinessLogicExecutor executor) {
// XOR the negated boolean with the return value of the method
return (mNegated != executor.executeCondition(mMethodName,
mMethodArgs.toArray(new String[mMethodArgs.size()])));
}
}
/**
* Nested class representing an Business Logic Rule Action. Stores the name of a method
* to invoke, as well as String args to use during invokation.
*/
protected static class BusinessLogicRuleAction {
/* Stored method name and String args */
protected String mMethodName;
protected List<String> mMethodArgs;
public BusinessLogicRuleAction(String methodName, List<String> methodArgs) {
mMethodName = methodName;
mMethodArgs = methodArgs;
}
/**
* Invoke this Business Logic action with an executor.
*/
public void invoke(BusinessLogicExecutor executor) {
executor.executeAction(mMethodName,
mMethodArgs.toArray(new String[mMethodArgs.size()]));
}
}
/**
* Nested enum of the possible authentication statuses.
*/
protected enum AuthenticationStatusEnum {
UNKNOWN,
NOT_AUTHENTICATED,
NOT_AUTHORIZED,
AUTHORIZED,
NO_DEVICE_INFO
}
}