1 /* 2 * Copyright (C) 2017 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 17 package com.android.compatibility.common.util; 18 19 import java.io.PrintWriter; 20 import java.io.StringWriter; 21 import java.util.ArrayList; 22 import java.util.Arrays; 23 import java.util.Date; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Set; 28 29 import org.junit.AssumptionViolatedException; 30 31 /** 32 * Helper and constants accessible to host and device components that enable Business Logic 33 * configuration 34 */ 35 public class BusinessLogic { 36 37 // Device location to which business logic data is pushed 38 public static final String DEVICE_FILE = "/sdcard/bl"; 39 40 /* A map from testcase name to the business logic rules for the test case */ 41 protected Map<String, List<BusinessLogicRulesList>> mRules; 42 /* Feature flag determining if device specific tests are executed. */ 43 public boolean mConditionalTestsEnabled; 44 private AuthenticationStatusEnum mAuthenticationStatus = AuthenticationStatusEnum.UNKNOWN; 45 46 // A Date denoting the time of request from the business logic service 47 protected Date mTimestamp; 48 49 // A list of regexes triggering log redaction 50 protected List<String> mRedactionRegexes = new ArrayList<>(); 51 52 /** 53 * Determines whether business logic exists for a given test name 54 * @param testName the name of the test case, prefixed by fully qualified class name, then '#'. 55 * For example, "com.android.foo.FooTest#testFoo" 56 * @return whether business logic exists for this test for this suite 57 */ hasLogicFor(String testName)58 public boolean hasLogicFor(String testName) { 59 List<BusinessLogicRulesList> rulesLists = mRules.get(testName); 60 return rulesLists != null && !rulesLists.isEmpty(); 61 } 62 63 /** 64 * Return whether multiple rule lists exist in the BusinessLogic for this test name. 65 */ hasLogicsFor(String testName)66 private boolean hasLogicsFor(String testName) { 67 List<BusinessLogicRulesList> rulesLists = mRules.get(testName); 68 return rulesLists != null && rulesLists.size() > 1; 69 } 70 71 /** 72 * Apply business logic for the given test. 73 * @param testName the name of the test case, prefixed by fully qualified class name, then '#'. 74 * For example, "com.android.foo.FooTest#testFoo" 75 * @param executor a {@link BusinessLogicExecutor} 76 */ applyLogicFor(String testName, BusinessLogicExecutor executor)77 public void applyLogicFor(String testName, BusinessLogicExecutor executor) { 78 if (!hasLogicFor(testName)) { 79 return; 80 } 81 if (hasLogicsFor(testName)) { 82 applyLogicsFor(testName, executor); // handle this special case separately 83 return; 84 } 85 // expecting exactly one rules list at this point 86 BusinessLogicRulesList rulesList = mRules.get(testName).get(0); 87 rulesList.invokeRules(executor); 88 } 89 90 /** 91 * Handle special case in which multiple rule lists exist for the test name provided. 92 * Execute each rule list in a sandbox and store an exception for each rule list that 93 * triggers failure or skipping for the test. 94 * If all rule lists trigger skipping, rethrow AssumptionViolatedException to report a 'skip' 95 * for the test as a whole. 96 * If one or more rule lists trigger failure, rethrow RuntimeException with a list containing 97 * each failure. 98 */ applyLogicsFor(String testName, BusinessLogicExecutor executor)99 private void applyLogicsFor(String testName, BusinessLogicExecutor executor) { 100 Map<String, RuntimeException> failedMap = new HashMap<>(); 101 Map<String, RuntimeException> skippedMap = new HashMap<>(); 102 List<BusinessLogicRulesList> rulesLists = mRules.get(testName); 103 for (int index = 0; index < rulesLists.size(); index++) { 104 BusinessLogicRulesList rulesList = rulesLists.get(index); 105 String description = cleanDescription(rulesList.getDescription(), index); 106 try { 107 rulesList.invokeRules(executor); 108 } catch (RuntimeException re) { 109 if (AssumptionViolatedException.class.isInstance(re)) { 110 skippedMap.put(description, re); 111 executor.logInfo("Test %s (%s) skipped for reason: %s", testName, description, 112 re.getMessage()); 113 } else { 114 failedMap.put(description, re); 115 } 116 } 117 } 118 if (skippedMap.size() == rulesLists.size()) { 119 throwAggregatedException(skippedMap, false); 120 } else if (failedMap.size() > 0) { 121 throwAggregatedException(failedMap, true); 122 } // else this test should be reported as a pure pass 123 } 124 125 /** 126 * Helper to aggregate the messages of many {@link RuntimeException}s, and optionally their 127 * stack traces, before throwing an exception. 128 * @param exceptions a map from description strings to exceptions. The descriptive keySet is 129 * used to differentiate which BusinessLogicRulesList caused which exception 130 * @param failed whether to trigger failure. When false, throws assumption failure instead, and 131 * excludes stack traces from the exception message. 132 */ throwAggregatedException(Map<String, RuntimeException> exceptions, boolean failed)133 private static void throwAggregatedException(Map<String, RuntimeException> exceptions, 134 boolean failed) { 135 Set<String> keySet = exceptions.keySet(); 136 String[] descriptions = keySet.toArray(new String[keySet.size()]); 137 StringBuilder msg = new StringBuilder(""); 138 msg.append(String.format("Test %s for cases: ", (failed) ? "failed" : "skipped")); 139 msg.append(Arrays.toString(descriptions)); 140 msg.append("\nReasons include:"); 141 for (String description : descriptions) { 142 RuntimeException re = exceptions.get(description); 143 msg.append(String.format("\nMessage [%s]: %s", description, re.getMessage())); 144 if (failed) { 145 StringWriter sw = new StringWriter(); 146 re.printStackTrace(new PrintWriter(sw)); 147 msg.append(String.format("\nStack Trace: %s", sw.toString())); 148 } 149 } 150 if (failed) { 151 throw new RuntimeException(msg.toString()); 152 } else { 153 throw new AssumptionViolatedException(msg.toString()); 154 } 155 } 156 157 /** 158 * Helper method to generate a meaningful description in case the provided description is null 159 * or empty. In this case, returns a string representation of the index provided. 160 */ cleanDescription(String description, int index)161 private String cleanDescription(String description, int index) { 162 return (description == null || description.length() == 0) 163 ? Integer.toString(index) 164 : description; 165 } 166 setAuthenticationStatus(String authenticationStatus)167 public void setAuthenticationStatus(String authenticationStatus) { 168 try { 169 mAuthenticationStatus = Enum.valueOf(AuthenticationStatusEnum.class, 170 authenticationStatus); 171 } catch (IllegalArgumentException e) { 172 // Invalid value, set to unknown 173 mAuthenticationStatus = AuthenticationStatusEnum.UNKNOWN; 174 } 175 } 176 isAuthorized()177 public boolean isAuthorized() { 178 return AuthenticationStatusEnum.AUTHORIZED.equals(mAuthenticationStatus); 179 } 180 getTimestamp()181 public Date getTimestamp() { 182 return mTimestamp; 183 } 184 getRedactionRegexes()185 public List<String> getRedactionRegexes() { 186 return new ArrayList<String>(mRedactionRegexes); 187 } 188 189 /** 190 * Builds a user readable string tha explains the authentication status and the effect on tests 191 * which require authentication to execute. 192 */ getAuthenticationStatusMessage()193 public String getAuthenticationStatusMessage() { 194 switch (mAuthenticationStatus) { 195 case AUTHORIZED: 196 return "Authorized"; 197 case NOT_AUTHENTICATED: 198 return "authorization failed, please ensure the service account key is " 199 + "properly installed."; 200 case NOT_AUTHORIZED: 201 return "service account is not authorized to access information for this device. " 202 + "Please verify device properties are set correctly and account " 203 + "permissions are configured to the Business Logic Api."; 204 case NO_DEVICE_INFO: 205 return "unable to read device info files. Retry without --skip-device-info flag."; 206 default: 207 return "something went wrong, please try again."; 208 } 209 } 210 211 /** 212 * A list of BusinessLogicRules, wrapped with an optional description to differentiate rule 213 * lists that apply to the same test. 214 */ 215 protected static class BusinessLogicRulesList { 216 217 /* Stored description and rules */ 218 protected List<BusinessLogicRule> mRulesList; 219 protected String mDescription; 220 BusinessLogicRulesList(List<BusinessLogicRule> rulesList)221 public BusinessLogicRulesList(List<BusinessLogicRule> rulesList) { 222 mRulesList = rulesList; 223 } 224 BusinessLogicRulesList(List<BusinessLogicRule> rulesList, String description)225 public BusinessLogicRulesList(List<BusinessLogicRule> rulesList, String description) { 226 mRulesList = rulesList; 227 mDescription = description; 228 } 229 getDescription()230 public String getDescription() { 231 return mDescription; 232 } 233 getRules()234 public List<BusinessLogicRule> getRules() { 235 return mRulesList; 236 } 237 invokeRules(BusinessLogicExecutor executor)238 public void invokeRules(BusinessLogicExecutor executor) { 239 for (BusinessLogicRule rule : mRulesList) { 240 // Check conditions 241 if (rule.invokeConditions(executor)) { 242 rule.invokeActions(executor); 243 } 244 } 245 } 246 } 247 248 /** 249 * Nested class representing an Business Logic Rule. Stores a collection of conditions 250 * and actions for later invokation. 251 */ 252 protected static class BusinessLogicRule { 253 254 /* Stored conditions and actions */ 255 protected List<BusinessLogicRuleCondition> mConditions; 256 protected List<BusinessLogicRuleAction> mActions; 257 BusinessLogicRule(List<BusinessLogicRuleCondition> conditions, List<BusinessLogicRuleAction> actions)258 public BusinessLogicRule(List<BusinessLogicRuleCondition> conditions, 259 List<BusinessLogicRuleAction> actions) { 260 mConditions = conditions; 261 mActions = actions; 262 } 263 264 /** 265 * Method that invokes all Business Logic conditions for this rule, and returns true 266 * if all conditions evaluate to true. 267 */ invokeConditions(BusinessLogicExecutor executor)268 public boolean invokeConditions(BusinessLogicExecutor executor) { 269 for (BusinessLogicRuleCondition condition : mConditions) { 270 if (!condition.invoke(executor)) { 271 return false; 272 } 273 } 274 return true; 275 } 276 277 /** 278 * Method that invokes all Business Logic actions for this rule 279 */ invokeActions(BusinessLogicExecutor executor)280 public void invokeActions(BusinessLogicExecutor executor) { 281 for (BusinessLogicRuleAction action : mActions) { 282 action.invoke(executor); 283 } 284 } 285 } 286 287 /** 288 * Nested class representing an Business Logic Rule Condition. Stores the name of a method 289 * to invoke, as well as String args to use during invokation. 290 */ 291 protected static class BusinessLogicRuleCondition { 292 293 /* Stored method name and String args */ 294 protected String mMethodName; 295 protected List<String> mMethodArgs; 296 /* Whether or not the boolean result of this condition should be reversed */ 297 protected boolean mNegated; 298 299 BusinessLogicRuleCondition(String methodName, List<String> methodArgs, boolean negated)300 public BusinessLogicRuleCondition(String methodName, List<String> methodArgs, 301 boolean negated) { 302 mMethodName = methodName; 303 mMethodArgs = methodArgs; 304 mNegated = negated; 305 } 306 307 /** 308 * Invoke this Business Logic condition with an executor. 309 */ invoke(BusinessLogicExecutor executor)310 public boolean invoke(BusinessLogicExecutor executor) { 311 // XOR the negated boolean with the return value of the method 312 return (mNegated != executor.executeCondition(mMethodName, 313 mMethodArgs.toArray(new String[mMethodArgs.size()]))); 314 } 315 } 316 317 /** 318 * Nested class representing an Business Logic Rule Action. Stores the name of a method 319 * to invoke, as well as String args to use during invokation. 320 */ 321 protected static class BusinessLogicRuleAction { 322 323 /* Stored method name and String args */ 324 protected String mMethodName; 325 protected List<String> mMethodArgs; 326 BusinessLogicRuleAction(String methodName, List<String> methodArgs)327 public BusinessLogicRuleAction(String methodName, List<String> methodArgs) { 328 mMethodName = methodName; 329 mMethodArgs = methodArgs; 330 } 331 332 /** 333 * Invoke this Business Logic action with an executor. 334 */ invoke(BusinessLogicExecutor executor)335 public void invoke(BusinessLogicExecutor executor) { 336 executor.executeAction(mMethodName, 337 mMethodArgs.toArray(new String[mMethodArgs.size()])); 338 } 339 } 340 341 /** 342 * Nested enum of the possible authentication statuses. 343 */ 344 protected enum AuthenticationStatusEnum { 345 UNKNOWN, 346 NOT_AUTHENTICATED, 347 NOT_AUTHORIZED, 348 AUTHORIZED, 349 NO_DEVICE_INFO 350 } 351 352 } 353