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