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