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