Home | History | Annotate | Download | only in util
      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