Add business logic handling to compatibility infra

bug: 62867056
Test: run gts -j32
Change-Id: I4646a46e0951fbe1147613037b1aca1260ba8dd1
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutor.java b/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutor.java
new file mode 100644
index 0000000..ec940ce
--- /dev/null
+++ b/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutor.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.content.Context;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Execute business logic methods for device side test cases
+ */
+public class BusinessLogicDeviceExecutor extends BusinessLogicExecutor {
+
+    private Context mContext;
+    private Object mTestObj;
+
+    public BusinessLogicDeviceExecutor(Context context, Object testObj) {
+        mContext = context;
+        mTestObj = testObj;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Object getTestObject() {
+        return mTestObj;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ResolvedMethod getResolvedMethod(Class cls, String methodName, String... args)
+            throws ClassNotFoundException {
+        List<Method> nameMatches = getMethodsWithName(cls, methodName);
+        for (Method m : nameMatches) {
+            ResolvedMethod rm = new ResolvedMethod(m);
+            int paramTypesMatched = 0;
+            int argsUsed = 0;
+            Class[] paramTypes = m.getParameterTypes();
+            for (Class paramType : paramTypes) {
+                if (argsUsed == args.length && paramType.equals(String.class)) {
+                    // We've used up all supplied string args, so this method will not match.
+                    // If paramType is the Context class, we can match a paramType without needing
+                    // more string args. similarly, paramType "String[]" can be matched with zero
+                    // string args. If we add support for more paramTypes, this logic may require
+                    // adjustment.
+                    break;
+                }
+                if (paramType.equals(String.class)) {
+                    // Type "String" -- supply the next available arg
+                    rm.addArg(args[argsUsed++]);
+                } else if (Context.class.isAssignableFrom(paramType)) {
+                    // Type "Context" -- supply the context from the test case
+                    rm.addArg(mContext);
+                } else if (paramType.equals(Class.forName(STRING_ARRAY_CLASS))) {
+                    // Type "String[]" (or "String...") -- supply all remaining args
+                    rm.addArg(Arrays.copyOfRange(args, argsUsed, args.length));
+                    argsUsed += (args.length - argsUsed);
+                } else {
+                    break; // Param type is unrecognized, this method will not match.
+                }
+                paramTypesMatched++; // A param type has been matched when reaching this point.
+            }
+            if (paramTypesMatched == paramTypes.length && argsUsed == args.length) {
+                return rm; // Args match, methods match, so return the first method-args pairing.
+            }
+            // Not a match, try args for next method that matches by name.
+        }
+        throw new RuntimeException(String.format(
+                "BusinessLogic: Failed to invoke action method %s with args: %s", methodName,
+                Arrays.toString(args)));
+    }
+}
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java b/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
new file mode 100644
index 0000000..9b3a0b6
--- /dev/null
+++ b/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Device-side base class for tests leveraging the Business Logic service.
+ */
+public class BusinessLogicTestCase {
+
+    /* String marking the beginning of the parameter in a test name */
+    private static final String PARAM_START = "[";
+
+    /* Test name rule that tracks the current test method under execution */
+    @Rule public TestName mTestCase = new TestName();
+
+    private static BusinessLogic mBusinessLogic;
+
+    @BeforeClass
+    public static void prepareBusinessLogic() {
+        File businessLogicFile = new File(BusinessLogic.DEVICE_FILE);
+        mBusinessLogic = BusinessLogicFactory.createFromFile(businessLogicFile);
+    }
+
+    @Before
+    public void executeBusinessLogic() {
+        String methodName = mTestCase.getMethodName();
+        if (methodName.contains(PARAM_START)) {
+            // Strip parameter suffix (e.g. "[0]") from method name
+            methodName = methodName.substring(0, methodName.lastIndexOf(PARAM_START));
+        }
+        String testName = String.format("%s#%s", this.getClass().getName(), methodName);
+        if (mBusinessLogic.hasLogicFor(testName)) {
+            Log.i("Finding business logic for test case: ", testName);
+            BusinessLogicExecutor executor = new BusinessLogicDeviceExecutor(getContext(), this);
+            mBusinessLogic.applyLogicFor(testName, executor);
+        }
+    }
+
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+}
diff --git a/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java b/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
new file mode 100644
index 0000000..578db9c
--- /dev/null
+++ b/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@line BusinessLogicDeviceExecutor}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class BusinessLogicDeviceExecutorTest {
+
+    private static final String THIS_CLASS =
+            "com.android.compatibility.common.util.BusinessLogicDeviceExecutorTest";
+    private static final String METHOD_1 = THIS_CLASS + ".method1";
+    private static final String METHOD_2 = THIS_CLASS + ".method2";
+    private static final String METHOD_3 = THIS_CLASS + ".method3";
+    private static final String METHOD_4 = THIS_CLASS + ".method4";
+    private static final String METHOD_5 = THIS_CLASS + ".method5";
+    private static final String METHOD_6 = THIS_CLASS + ".method6";
+    private static final String METHOD_7 = THIS_CLASS + ".method7";
+    private static final String METHOD_8 = THIS_CLASS + ".method8";
+    private static final String FAKE_METHOD = THIS_CLASS + ".methodDoesntExist";
+    private static final String ARG_STRING_1 = "arg1";
+    private static final String ARG_STRING_2 = "arg2";
+
+    private static final String OTHER_METHOD_1 = THIS_CLASS + "$OtherClass.method1";
+
+    private String mInvoked = null;
+    private Object[] mArgsUsed = null;
+    private Context mContext;
+    private BusinessLogicExecutor mExecutor;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mExecutor = new BusinessLogicDeviceExecutor(mContext, this);
+        // reset the instance variables tracking the method invoked and the args used
+        mInvoked = null;
+        mArgsUsed = null;
+        // reset the OtherClass class variable tracking the method invoked
+        OtherClass.otherInvoked = null;
+    }
+
+    @Test
+    public void testInvokeMethodInThisClass() throws Exception {
+        mExecutor.invokeMethod(METHOD_1);
+        // assert that mInvoked was set for this BusinessLogicDeviceExecutorTest instance
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+    }
+
+    @Test
+    public void testInvokeMethodInOtherClass() throws Exception {
+        mExecutor.invokeMethod(OTHER_METHOD_1);
+        // assert that OtherClass.method1 was invoked, and static field of OtherClass was changed
+        assertEquals("Failed to invoke method in other class", OtherClass.otherInvoked,
+                OTHER_METHOD_1);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_2, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_2);
+        // assert both String arguments were correctly set for method2
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringAndContextArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_3, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_3);
+        // assert that String arg and Context arg were correctly set for method3
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], mContext);
+    }
+
+    @Test
+    public void testInvokeMethodWithContextAndStringArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_4, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_4);
+        // Like testInvokeMethodWithStringAndContextArgs, but flip the args for method4
+        assertEquals("Failed to set first argument", mArgsUsed[0], mContext);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_1);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringArrayArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_5, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_5);
+        // assert both String arguments were correctly set for method5
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithEmptyStringArrayArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_5);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_5);
+        // assert no String arguments were set for method5
+        assertEquals("Incorrectly set args", mArgsUsed.length, 0);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringAndStringArrayArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_6, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_6);
+        // assert both String arguments were correctly set for method6
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithAllArgTypes() throws Exception {
+        mExecutor.invokeMethod(METHOD_7, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_7);
+        // assert all arguments were correctly set for method7
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], mContext);
+        assertEquals("Failed to set third argument", mArgsUsed[2], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeOverloadedMethodOneArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_1, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+        assertEquals("Set wrong number of arguments", mArgsUsed.length, 1);
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+    }
+
+    @Test
+    public void testInvokeOverloadedMethodTwoArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_1, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+        assertEquals("Set wrong number of arguments", mArgsUsed.length, 2);
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeNonExistentMethod() throws Exception {
+        mExecutor.invokeMethod(FAKE_METHOD, ARG_STRING_1, ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodTooManyArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_3, ARG_STRING_1, ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodTooFewArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_2, ARG_STRING_1);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodIncompatibleArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_8, ARG_STRING_1);
+    }
+
+    @Test
+    public void testExecuteConditionCheckReturnValue() throws Exception {
+        assertTrue("Wrong return value",
+                mExecutor.executeCondition(METHOD_2, ARG_STRING_1, ARG_STRING_1));
+        assertFalse("Wrong return value",
+                mExecutor.executeCondition(METHOD_2, ARG_STRING_1, ARG_STRING_2));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testExecuteInvalidCondition() throws Exception {
+        mExecutor.executeCondition(METHOD_1); // method1 does not return type boolean
+    }
+
+    @Test
+    public void testExecuteAction() throws Exception {
+        mExecutor.executeAction(METHOD_2, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_2);
+        // assert both String arguments were correctly set for method2
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    public void method1() {
+        mInvoked = METHOD_1;
+    }
+
+    // overloaded method with one arg
+    public void method1(String arg1) {
+        mInvoked = METHOD_1;
+        mArgsUsed = new Object[]{arg1};
+    }
+
+    // overloaded method with two args
+    public void method1(String arg1, String arg2) {
+        mInvoked = METHOD_1;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    public boolean method2(String arg1, String arg2) {
+        mInvoked = METHOD_2;
+        mArgsUsed = new Object[]{arg1, arg2};
+        return arg1.equals(arg2);
+    }
+
+    public void method3(String arg1, Context arg2) {
+        mInvoked = METHOD_3;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    // Same as method3, but flipped args
+    public void method4(Context arg1, String arg2) {
+        mInvoked = METHOD_4;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    public void method5(String... args) {
+        mInvoked = METHOD_5;
+        mArgsUsed = args;
+    }
+
+    public void method6(String arg1, String... moreArgs) {
+        mInvoked = METHOD_6;
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(arg1);
+        allArgs.addAll(Arrays.asList(moreArgs));
+        mArgsUsed = allArgs.toArray(new String[0]);
+    }
+
+    public void method7(String arg1, Context arg2, String... moreArgs) {
+        mInvoked = METHOD_7;
+        List<Object> allArgs = new ArrayList<>();
+        allArgs.add(arg1);
+        allArgs.add(arg2);
+        allArgs.addAll(Arrays.asList(moreArgs));
+        mArgsUsed = allArgs.toArray(new Object[0]);
+    }
+
+    public void method8(String arg1, Integer arg2) {
+        // This method should never be successfully invoked, since Integer parameter types are
+        // unsupported for the BusinessLogic service
+    }
+
+    public static class OtherClass {
+
+        public static String otherInvoked = null;
+
+        public void method1() {
+            otherInvoked = OTHER_METHOD_1;
+        }
+    }
+}
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java
index 4eb0843..27d3976 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java
@@ -44,6 +44,7 @@
 
     private static final String ROOT_DIR2 = "ROOT_DIR2";
     private static final String DYNAMIC_CONFIG_OVERRIDE_URL = "DYNAMIC_CONFIG_OVERRIDE_URL";
+    private static final String BUSINESS_LOGIC_HOST_FILE = "BUSINESS_LOGIC_HOST_FILE";
     private static final String RETRY_COMMAND_LINE_ARGS = "retry_command_line_args";
     private static final String ALT_HOST_TESTCASE_DIR = "ANDROID_HOST_OUT_TESTCASES";
     private static final String ALT_TARGET_TESTCASE_DIR = "ANDROID_TARGET_OUT_TESTCASES";
@@ -111,6 +112,10 @@
                 configFile.getAbsolutePath());
     }
 
+    public void setBusinessLogicHostFile(File hostFile) {
+        mBuildInfo.addBuildAttribute(BUSINESS_LOGIC_HOST_FILE, hostFile.getAbsolutePath());
+    }
+
     public void setModuleIds(String[] moduleIds) {
         mBuildInfo.addBuildAttribute(MODULE_IDS, String.join(",", moduleIds));
     }
@@ -126,6 +131,10 @@
         return configMap;
     }
 
+    public File getBusinessLogicHostFile() {
+        return new File(mBuildInfo.getBuildAttributes().get(BUSINESS_LOGIC_HOST_FILE));
+    }
+
     /**
      * @return a {@link File} representing the directory holding the Compatibility installation
      * @throws FileNotFoundException if the directory does not exist
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/BusinessLogicPreparer.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/BusinessLogicPreparer.java
new file mode 100644
index 0000000..fded8bf
--- /dev/null
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/BusinessLogicPreparer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.tradefed.targetprep;
+
+import com.android.compatibility.SuiteInfo;
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.util.BusinessLogic;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * Pushes business Logic to the host and the test device, for use by test cases in the test suite.
+ */
+@OptionClass(alias="business-logic-preparer")
+public class BusinessLogicPreparer implements ITargetCleaner {
+
+    private static final String LOG_TAG = BusinessLogicPreparer.class.getSimpleName();
+
+    /* Placeholder in the service URL for the suite to be configured */
+    private static final String SUITE_PLACEHOLDER = "{suite-name}";
+
+    /* String for creating files to store the business logic configuration on the host */
+    private static final String FILE_LOCATION = "business-logic";
+    /* Extension of business logic files */
+    private static final String FILE_EXT = ".bl";
+
+    @Option(name = "business-logic-url", description = "The URL to use when accessing the " +
+            "business logic service, parameters not included", mandatory = true)
+    private String mUrl;
+
+    @Option(name = "business-logic-api-key", description = "The API key to use when accessing " +
+            "the business logic service.", mandatory = true)
+    private String mApiKey;
+
+    @Option(name = "cleanup", description = "Whether to remove config files from the test " +
+            "target after test completion.")
+    private boolean mCleanup = true;
+
+    private String mDeviceFilePushed;
+    private String mHostFilePushed;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, BuildError,
+            DeviceNotAvailableException {
+        // Piece together request URL
+        String requestString = String.format("%s?key=%s", mUrl.replace(SUITE_PLACEHOLDER,
+                SuiteInfo.NAME), mApiKey);
+        // Retrieve business logic string from service
+        String businessLogicString = null;
+        try {
+            URL request = new URL(requestString);
+            businessLogicString = StreamUtil.getStringFromStream(request.openStream());
+        } catch (IOException e) {
+            throw new TargetSetupError(String.format(
+                    "Cannot connect to business logic service for suite %s", SuiteInfo.NAME), e,
+                    device.getDeviceDescriptor());
+        }
+        // Push business logic string to host file
+        try {
+            File hostFile = FileUtil.createTempFile(FILE_LOCATION, FILE_EXT);
+            FileUtil.writeToFile(businessLogicString, hostFile);
+            mHostFilePushed = hostFile.getAbsolutePath();
+            CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
+            buildHelper.setBusinessLogicHostFile(hostFile);
+        } catch (IOException e) {
+            throw new TargetSetupError(String.format(
+                    "Retrieved business logic for suite %s could not be written to host",
+                    SuiteInfo.NAME), device.getDeviceDescriptor());
+        }
+        // Push business logic string to device file
+        removeDeviceFile(device); // remove any existing business logic file from device
+        if (device.pushString(businessLogicString, BusinessLogic.DEVICE_FILE)) {
+            mDeviceFilePushed = BusinessLogic.DEVICE_FILE;
+        } else {
+            throw new TargetSetupError(String.format(
+                    "Retrieved business logic for suite %s could not be written to device %s",
+                    SuiteInfo.NAME, device.getSerialNumber()), device.getDeviceDescriptor());
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+            throws DeviceNotAvailableException {
+        // Clean up host file
+        if (mCleanup) {
+            if (mHostFilePushed != null) {
+                FileUtil.deleteFile(new File(mHostFilePushed));
+            }
+            if (mDeviceFilePushed != null && !(e instanceof DeviceNotAvailableException)) {
+                removeDeviceFile(device);
+            }
+        }
+    }
+
+    /** Remove business logic file from the device */
+    private static void removeDeviceFile(ITestDevice device) throws DeviceNotAvailableException {
+        device.executeShellCommand(String.format("rm -rf %s", BusinessLogic.DEVICE_FILE));
+    }
+}
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/BusinessLogicHostTestBase.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/BusinessLogicHostTestBase.java
new file mode 100644
index 0000000..7d5ab12
--- /dev/null
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/BusinessLogicHostTestBase.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.tradefed.testtype;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.util.BusinessLogic;
+import com.android.compatibility.common.util.BusinessLogicExecutor;
+import com.android.compatibility.common.util.BusinessLogicFactory;
+import com.android.compatibility.common.util.BusinessLogicHostExecutor;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+
+/**
+ * Host-side base class for tests leveraging the Business Logic service.
+ */
+public class BusinessLogicHostTestBase extends CompatibilityHostTestBase {
+
+    /* String marking the beginning of the parameter in a test name */
+    private static final String PARAM_START = "[";
+
+    /* Test name rule that tracks the current test method under execution */
+    @Rule public TestName mTestCase = new TestName();
+
+    private static BusinessLogic mBusinessLogic;
+
+    @Before
+    public void executeBusinessLogic() {
+        // Business logic must be retrieved in this @Before method, since the build info contains
+        // the location of the business logic file and cannot be referenced from a static context
+        if (mBusinessLogic == null) {
+            CompatibilityBuildHelper helper = new CompatibilityBuildHelper(mBuild);
+            File businessLogicFile = helper.getBusinessLogicHostFile();
+            mBusinessLogic = BusinessLogicFactory.createFromFile(businessLogicFile);
+        }
+
+        String methodName = mTestCase.getMethodName();
+        if (methodName.contains(PARAM_START)) {
+            // Strip parameter suffix (e.g. "[0]") from method name
+            methodName = methodName.substring(0, methodName.lastIndexOf(PARAM_START));
+        }
+        String testName = String.format("%s#%s", this.getClass().getName(), methodName);
+        if (mBusinessLogic.hasLogicFor(testName)) {
+            CLog.i("Applying business logic for test case: ", testName);
+            BusinessLogicExecutor executor = new BusinessLogicHostExecutor(getDevice(),
+                    mBuild, this);
+            mBusinessLogic.applyLogicFor(testName, executor);
+        }
+    }
+}
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/BusinessLogicHostExecutor.java b/common/host-side/util/src/com/android/compatibility/common/util/BusinessLogicHostExecutor.java
new file mode 100644
index 0000000..43bdea1
--- /dev/null
+++ b/common/host-side/util/src/com/android/compatibility/common/util/BusinessLogicHostExecutor.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Execute business logic methods for host side test cases
+ */
+public class BusinessLogicHostExecutor extends BusinessLogicExecutor {
+
+    private ITestDevice mDevice;
+    private IBuildInfo mBuild;
+    private Object mTestObj;
+
+    public BusinessLogicHostExecutor(ITestDevice device, IBuildInfo build, Object testObj) {
+        mDevice = device;
+        mBuild = build;
+        mTestObj = testObj;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Object getTestObject() {
+        return mTestObj;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ResolvedMethod getResolvedMethod(Class cls, String methodName, String... args)
+            throws ClassNotFoundException {
+        List<Method> nameMatches = getMethodsWithName(cls, methodName);
+        for (Method m : nameMatches) {
+            ResolvedMethod rm = new ResolvedMethod(m);
+            int paramTypesMatched = 0;
+            int argsUsed = 0;
+            Class[] paramTypes = m.getParameterTypes();
+            for (Class paramType : paramTypes) {
+                if (argsUsed == args.length && paramType.equals(String.class)) {
+                    // We've used up all supplied string args, so this method will not match.
+                    // If paramType is the ITestDevice or IBuildInfo class, we can match a
+                    // paramType without needing more string args. similarly, paramType "String[]"
+                    // can be matched with zero string args. If we add support for more paramTypes,
+                    // this logic may require adjustment.
+                    break;
+                }
+                if (paramType.equals(String.class)) {
+                    // Type "String" -- supply the next available arg
+                    rm.addArg(args[argsUsed++]);
+                } else if (ITestDevice.class.isAssignableFrom(paramType)) {
+                    rm.addArg(mDevice);
+                } else if (IBuildInfo.class.isAssignableFrom(paramType)) {
+                    rm.addArg(mBuild);
+                } else if (paramType.equals(Class.forName(STRING_ARRAY_CLASS))) {
+                    // Type "String[]" (or "String...") -- supply all remaining args
+                    rm.addArg(Arrays.copyOfRange(args, argsUsed, args.length));
+                    argsUsed += (args.length - argsUsed);
+                } else {
+                    break; // Param type is unrecognized, this method will not match.
+                }
+                paramTypesMatched++; // A param type has been matched when reaching this point.
+            }
+            if (paramTypesMatched == paramTypes.length && argsUsed == args.length) {
+                return rm; // Args match, methods match, so return the first method-args pairing.
+            }
+            // Not a match, try args for next method that matches by name.
+        }
+        throw new RuntimeException(String.format(
+                "BusinessLogic: Failed to invoke action method %s with args: %s", methodName,
+                Arrays.toString(args)));
+    }
+}
diff --git a/common/host-side/util/tests/Android.mk b/common/host-side/util/tests/Android.mk
index 338d6c3..5741ec4 100644
--- a/common/host-side/util/tests/Android.mk
+++ b/common/host-side/util/tests/Android.mk
@@ -18,7 +18,7 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_JAVA_LIBRARIES := compatibility-host-util junit-host json-prebuilt tradefed
+LOCAL_JAVA_LIBRARIES := compatibility-host-util easymock junit-host json-prebuilt tradefed
 
 LOCAL_MODULE := compatibility-host-util-tests
 
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java b/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java
new file mode 100644
index 0000000..2940f86
--- /dev/null
+++ b/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@link BusinessLogicHostExecutor}.
+ */
+@RunWith(JUnit4.class)
+public class BusinessLogicHostExecutorTest {
+
+    private static final String THIS_CLASS =
+            "com.android.compatibility.common.util.BusinessLogicHostExecutorTest";
+    private static final String METHOD_1 = THIS_CLASS + ".method1";
+    private static final String METHOD_2 = THIS_CLASS + ".method2";
+    private static final String METHOD_3 = THIS_CLASS + ".method3";
+    private static final String METHOD_4 = THIS_CLASS + ".method4";
+    private static final String METHOD_5 = THIS_CLASS + ".method5";
+    private static final String METHOD_6 = THIS_CLASS + ".method6";
+    private static final String METHOD_7 = THIS_CLASS + ".method7";
+    private static final String METHOD_8 = THIS_CLASS + ".method8";
+    private static final String FAKE_METHOD = THIS_CLASS + ".methodDoesntExist";
+    private static final String ARG_STRING_1 = "arg1";
+    private static final String ARG_STRING_2 = "arg2";
+
+    private static final String OTHER_METHOD_1 = THIS_CLASS + "$OtherClass.method1";
+
+    private String mInvoked = null;
+    private Object[] mArgsUsed = null;
+    private IBuildInfo mMockBuild;
+    private ITestDevice mMockDevice;
+    private BusinessLogicExecutor mExecutor;
+
+    @Before
+    public void setUp() {
+        mMockBuild = EasyMock.createMock(IBuildInfo.class);
+        mMockDevice = EasyMock.createMock(ITestDevice.class);
+        mExecutor = new BusinessLogicHostExecutor(mMockDevice, mMockBuild, this);
+        // reset the instance variables tracking the method invoked and the args used
+        mInvoked = null;
+        mArgsUsed = null;
+        // reset the OtherClass class variable tracking the method invoked
+        OtherClass.otherInvoked = null;
+    }
+
+    @Test
+    public void testInvokeMethodInThisClass() throws Exception {
+        mExecutor.invokeMethod(METHOD_1);
+        // assert that mInvoked was set for this BusinessLogicDeviceExecutorTest instance
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+    }
+
+        @Test
+    public void testInvokeMethodInOtherClass() throws Exception {
+        mExecutor.invokeMethod(OTHER_METHOD_1);
+        // assert that OtherClass.method1 was invoked, and static field of OtherClass was changed
+        assertEquals("Failed to invoke method in other class", OtherClass.otherInvoked,
+                OTHER_METHOD_1);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_2, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_2);
+        // assert both String arguments were correctly set for method2
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringAndDeviceArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_3, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_3);
+        // assert that String arg and ITestDevice arg were correctly set for method3
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], mMockDevice);
+    }
+
+    @Test
+    public void testInvokeMethodWithDeviceAndStringArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_4, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_4);
+        // Like testInvokeMethodWithStringAndDeviceArgs, but flip the args for method4
+        assertEquals("Failed to set first argument", mArgsUsed[0], mMockDevice);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_1);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringArrayArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_5, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_5);
+        // assert both String arguments were correctly set for method5
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithEmptyStringArrayArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_5);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_5);
+        // assert no String arguments were set for method5
+        assertEquals("Incorrectly set args", mArgsUsed.length, 0);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringAndStringArrayArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_6, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_6);
+        // assert both String arguments were correctly set for method6
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithAllArgTypes() throws Exception {
+        mExecutor.invokeMethod(METHOD_7, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_7);
+        // assert all arguments were correctly set for method7
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], mMockBuild);
+        assertEquals("Failed to set second argument", mArgsUsed[2], mMockDevice);
+        assertEquals("Failed to set third argument", mArgsUsed[3], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeOverloadedMethodOneArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_1, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+        assertEquals("Set wrong number of arguments", mArgsUsed.length, 1);
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+    }
+
+    @Test
+    public void testInvokeOverloadedMethodTwoArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_1, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+        assertEquals("Set wrong number of arguments", mArgsUsed.length, 2);
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeNonExistentMethod() throws Exception {
+        mExecutor.invokeMethod(FAKE_METHOD, ARG_STRING_1, ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodTooManyArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_3, ARG_STRING_1, ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodTooFewArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_2, ARG_STRING_1);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodIncompatibleArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_8, ARG_STRING_1);
+    }
+
+    @Test
+    public void testExecuteConditionCheckReturnValue() throws Exception {
+        assertTrue("Wrong return value",
+                mExecutor.executeCondition(METHOD_2, ARG_STRING_1, ARG_STRING_1));
+        assertFalse("Wrong return value",
+                mExecutor.executeCondition(METHOD_2, ARG_STRING_1, ARG_STRING_2));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testExecuteInvalidCondition() throws Exception {
+        mExecutor.executeCondition(METHOD_1); // method1 does not return type boolean
+    }
+
+    @Test
+    public void testExecuteAction() throws Exception {
+        mExecutor.executeAction(METHOD_2, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_2);
+        // assert both String arguments were correctly set for method2
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    public void method1() {
+        mInvoked = METHOD_1;
+    }
+
+    // overloaded method with one arg
+    public void method1(String arg1) {
+        mInvoked = METHOD_1;
+        mArgsUsed = new Object[]{arg1};
+    }
+
+    // overloaded method with two args
+    public void method1(String arg1, String arg2) {
+        mInvoked = METHOD_1;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    public boolean method2(String arg1, String arg2) {
+        mInvoked = METHOD_2;
+        mArgsUsed = new Object[]{arg1, arg2};
+        return arg1.equals(arg2);
+    }
+
+    public void method3(String arg1, ITestDevice arg2) {
+        mInvoked = METHOD_3;
+
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    // Same as method3, but flipped args
+    public void method4(ITestDevice arg1, String arg2) {
+        mInvoked = METHOD_4;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    public void method5(String... args) {
+        mInvoked = METHOD_5;
+        mArgsUsed = args;
+    }
+
+    public void method6(String arg1, String... moreArgs) {
+        mInvoked = METHOD_6;
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(arg1);
+        allArgs.addAll(Arrays.asList(moreArgs));
+        mArgsUsed = allArgs.toArray(new String[0]);
+    }
+
+    public void method7(String arg1, IBuildInfo arg2, ITestDevice arg3, String... moreArgs) {
+        mInvoked = METHOD_7;
+        List<Object> allArgs = new ArrayList<>();
+        allArgs.add(arg1);
+        allArgs.add(arg2);
+        allArgs.add(arg3);
+        allArgs.addAll(Arrays.asList(moreArgs));
+        mArgsUsed = allArgs.toArray(new Object[0]);
+    }
+
+    public void method8(String arg1, Integer arg2) {
+        // This method should never be successfully invoked, since Integer parameter types are
+        // unsupported for the BusinessLogic service
+    }
+
+    public static class OtherClass {
+
+        public static String otherInvoked = null;
+
+        public void method1() {
+            otherInvoked = OTHER_METHOD_1;
+        }
+    }
+}
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java b/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
index a706018..32408e4 100644
--- a/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
+++ b/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
@@ -15,24 +15,21 @@
  */
 package com.android.compatibility.common.util;
 
-import junit.framework.Test;
-import junit.framework.TestSuite;
-
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
 /**
  * A test suite for all host util unit tests.
  * <p/>
  * All tests listed here should be self-contained, and do not require any external dependencies.
  */
-public class HostUnitTests extends TestSuite {
-
-    public HostUnitTests() {
-        super();
-        addTestSuite(DynamicConfigHandlerTest.class);
-        addTestSuite(ModuleResultTest.class);
-        addTestSuite(TestFilterTest.class);
-    }
-
-    public static Test suite() {
-        return new HostUnitTests();
-    }
-}
\ No newline at end of file
+@RunWith(Suite.class)
+@SuiteClasses({
+    BusinessLogicHostExecutorTest.class,
+    DynamicConfigHandlerTest.class,
+    ModuleResultTest.class,
+    TestFilterTest.class,
+})
+public class HostUnitTests {
+    // empty on purpose
+}
diff --git a/common/util/Android.mk b/common/util/Android.mk
index 20bef9d..d5c5f5a 100644
--- a/common/util/Android.mk
+++ b/common/util/Android.mk
@@ -45,6 +45,7 @@
 LOCAL_MODULE := compatibility-common-util-hostsidelib
 
 LOCAL_STATIC_JAVA_LIBRARIES :=  guavalib \
+                                json-prebuilt \
                                 junit-host \
                                 kxml2-2.3.0 \
                                 platform-test-annotations-host
diff --git a/common/util/src/com/android/compatibility/common/util/BusinessLogic.java b/common/util/src/com/android/compatibility/common/util/BusinessLogic.java
new file mode 100644
index 0000000..4b5c5af
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/BusinessLogic.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper and constants accessible to host and device components that enable Business Logic
+ * configuration
+ */
+public class BusinessLogic {
+
+    // Device location to which business logic data is pushed
+    public static final String DEVICE_FILE = "/sdcard/bl";
+
+    /* A map from testcase name to the business logic rules for the test case */
+    protected Map<String, List<BusinessLogicRule>> mRules;
+
+    /**
+     * Determines whether business logic exists for a given test name
+     * @param testName the name of the test case, prefixed by fully qualified class name, then '#'.
+     * For example, "com.android.foo.FooTest#testFoo"
+     * @return whether business logic exists for this test for this suite
+     */
+    public boolean hasLogicFor(String testName) {
+        List<BusinessLogicRule> rules = mRules.get(testName);
+        return rules != null && !rules.isEmpty();
+    }
+
+    /**
+     * Apply business logic for the given test.
+     * @param testName the name of the test case, prefixed by fully qualified class name, then '#'.
+     * For example, "com.android.foo.FooTest#testFoo"
+     * @param executor a {@link BusinessLogicExecutor}
+     */
+    public void applyLogicFor(String testName, BusinessLogicExecutor executor) {
+        List<BusinessLogicRule> rules = mRules.get(testName);
+        if (rules == null || rules.isEmpty()) {
+            return;
+        }
+        for (BusinessLogicRule rule : rules) {
+            // Check conditions
+            if (rule.invokeConditions(executor)) {
+                rule.invokeActions(executor);
+            }
+        }
+    }
+
+    /**
+     * Nested class representing an Business Logic Rule. Stores a collection of conditions
+     * and actions for later invokation.
+     */
+    protected static class BusinessLogicRule {
+
+        /* Stored conditions and actions */
+        protected List<BusinessLogicRuleCondition> mConditions;
+        protected List<BusinessLogicRuleAction> mActions;
+
+        public BusinessLogicRule(List<BusinessLogicRuleCondition> conditions,
+                List<BusinessLogicRuleAction> actions) {
+            mConditions = conditions;
+            mActions = actions;
+        }
+
+        /**
+         * Method that invokes all Business Logic conditions for this rule, and returns true
+         * if all conditions evaluate to true.
+         */
+        public boolean invokeConditions(BusinessLogicExecutor executor) {
+            for (BusinessLogicRuleCondition condition : mConditions) {
+                if (!condition.invoke(executor)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Method that invokes all Business Logic actions for this rule
+         */
+        public void invokeActions(BusinessLogicExecutor executor) {
+            for (BusinessLogicRuleAction action : mActions) {
+                action.invoke(executor);
+            }
+        }
+    }
+
+    /**
+     * Nested class representing an Business Logic Rule Condition. Stores the name of a method
+     * to invoke, as well as String args to use during invokation.
+     */
+    protected static class BusinessLogicRuleCondition {
+
+        /* Stored method name and String args */
+        protected String mMethodName;
+        protected List<String> mMethodArgs;
+
+        public BusinessLogicRuleCondition(String methodName, List<String> methodArgs) {
+            mMethodName = methodName;
+            mMethodArgs = methodArgs;
+        }
+
+        /**
+         * Invoke this Business Logic condition with an executor.
+         */
+        public boolean invoke(BusinessLogicExecutor executor) {
+            return executor.executeCondition(mMethodName,
+                    mMethodArgs.toArray(new String[mMethodArgs.size()]));
+        }
+    }
+
+    /**
+     * Nested class representing an Business Logic Rule Action. Stores the name of a method
+     * to invoke, as well as String args to use during invokation.
+     */
+    protected static class BusinessLogicRuleAction {
+
+        /* Stored method name and String args */
+        protected String mMethodName;
+        protected List<String> mMethodArgs;
+
+        public BusinessLogicRuleAction(String methodName, List<String> methodArgs) {
+            mMethodName = methodName;
+            mMethodArgs = methodArgs;
+        }
+
+        /**
+         * Invoke this Business Logic condition with an executor.
+         */
+        public void invoke(BusinessLogicExecutor executor) {
+            executor.executeAction(mMethodName,
+                    mMethodArgs.toArray(new String[mMethodArgs.size()]));
+        }
+    }
+}
diff --git a/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java b/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
new file mode 100644
index 0000000..e189208
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.compatibility.common.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Resolves methods provided by the BusinessLogicService and invokes them
+ */
+public abstract class BusinessLogicExecutor {
+
+    /** String representations of the String class and String[] class */
+    protected static final String STRING_CLASS = "java.lang.String";
+    protected static final String STRING_ARRAY_CLASS = "[Ljava.lang.String;";
+
+    /**
+     * Execute a business logic condition.
+     * @param method the name of the method to invoke. Must include fully qualified name of the
+     * enclosing class, followed by '.', followed by the name of the method
+     * @param args the string arguments to supply to the method
+     * @return the return value of the method invoked
+     * @throws RuntimeException when failing to resolve or invoke the condition method
+     */
+    public boolean executeCondition(String method, String... args) {
+        try {
+            return (Boolean) invokeMethod(method, args);
+        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
+                InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException(String.format(
+                    "BusinessLogic: Failed to invoke condition method %s with args: %s", method,
+                    Arrays.toString(args)), e);
+        }
+    }
+
+    /**
+     * Execute a business logic action.
+     * @param method the name of the method to invoke. Must include fully qualified name of the
+     * enclosing class, followed by '.', followed by the name of the method
+     * @param args the string arguments to supply to the method
+     * @throws RuntimeException when failing to resolve or invoke the action method
+     */
+    public void executeAction(String method, String... args) {
+        try {
+            invokeMethod(method, args);
+        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
+                InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException(String.format(
+                    "BusinessLogic: Failed to invoke action method %s with args: %s", method,
+                    Arrays.toString(args)), e);
+        }
+    }
+
+    /**
+     * Execute a business logic method.
+     * @param method the name of the method to invoke. Must include fully qualified name of the
+     * enclosing class, followed by '.', followed by the name of the method
+     * @param args the string arguments to supply to the method
+     * @return the return value of the method invoked (type Boolean if method is a condition)
+     * @throws RuntimeException when failing to resolve or invoke the method
+     */
+    protected Object invokeMethod(String method, String... args) throws ClassNotFoundException,
+            IllegalAccessException, InstantiationException, InvocationTargetException,
+            NoSuchMethodException {
+        // Method names served by the BusinessLogic service should assume format
+        // classname.methodName, but also handle format classname#methodName since test names use
+        // this format
+        int index = (method.indexOf('#') == -1) ? method.lastIndexOf('.') : method.indexOf('#');
+        if (index == -1) {
+            throw new RuntimeException(String.format("BusinessLogic: invalid method name "
+                    + "\"%s\". Method string must include fully qualified class name. "
+                    + "For example, \"com.android.packagename.ClassName.methodName\".", method));
+        }
+        String className = method.substring(0, index);
+        Class cls = Class.forName(className);
+        Object obj = cls.getDeclaredConstructor().newInstance();
+        if (getTestObject() != null && cls.isAssignableFrom(getTestObject().getClass())) {
+            // The given method is a member of the test class, use the known test class instance
+            obj = getTestObject();
+        }
+        ResolvedMethod rm = getResolvedMethod(cls, method.substring(index + 1), args);
+        return rm.invoke(obj);
+    }
+
+    /**
+     * Get the test object. This method is left abstract, since non-abstract subclasses will set
+     * the test object in the constructor.
+     * @return the test case instance
+     */
+    protected abstract Object getTestObject();
+
+    /**
+     * Get the method and list of arguments corresponding to the class, method name, and proposed
+     * argument values, in the form of a {@link ResolvedMethod} object. This object stores all
+     * information required to successfully invoke the method. getResolvedMethod is left abstract,
+     * since argument types differ between device-side (e.g. Context) and host-side
+     * (e.g. ITestDevice) implementations of this class.
+     * @param cls the Class to which the method belongs
+     * @param methodName the name of the method to invoke
+     * @param args the string arguments to use when invoking the method
+     * @return a {@link ResolvedMethod}
+     * @throws ClassNotFoundException
+     */
+    protected abstract ResolvedMethod getResolvedMethod(Class cls, String methodName,
+            String... args) throws ClassNotFoundException;
+
+    /**
+     * Retrieve all methods within a class that match a given name
+     * @param cls the class
+     * @param name the method name
+     * @return a list of method objects
+     */
+    protected List<Method> getMethodsWithName(Class cls, String name) {
+        List<Method> methodList = new ArrayList<>();
+        for (Method m : cls.getMethods()) {
+            if (name.equals(m.getName())) {
+                methodList.add(m);
+            }
+        }
+        return methodList;
+    }
+
+    /**
+     * Helper class for storing a method object, and a list of arguments to use when invoking the
+     * method. The class is also equipped with an "invoke" method for convenience.
+     */
+    protected static class ResolvedMethod {
+        private Method mMethod;
+        List<Object> mArgs;
+
+        public ResolvedMethod(Method method) {
+            mMethod = method;
+            mArgs = new ArrayList<>();
+        }
+
+        /** Add an argument to the argument list for this instance */
+        public void addArg(Object arg) {
+            mArgs.add(arg);
+        }
+
+        /** Invoke the stored method with the stored args on a given object */
+        public Object invoke(Object instance) throws IllegalAccessException,
+                InvocationTargetException {
+            return mMethod.invoke(instance, mArgs.toArray());
+        }
+    }
+}
diff --git a/common/util/src/com/android/compatibility/common/util/BusinessLogicFactory.java b/common/util/src/com/android/compatibility/common/util/BusinessLogicFactory.java
new file mode 100644
index 0000000..b21a4ad
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/BusinessLogicFactory.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.android.compatibility.common.util.BusinessLogic.BusinessLogicRule;
+import com.android.compatibility.common.util.BusinessLogic.BusinessLogicRuleAction;
+import com.android.compatibility.common.util.BusinessLogic.BusinessLogicRuleCondition;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+
+/**
+ * Factory for creating a {@link BusinessLogic}
+ */
+public class BusinessLogicFactory {
+
+    // Name of list object storing test-rules pairs
+    private static final String BUSINESS_LOGIC_RULES_LISTS = "businessLogicRulesLists";
+    // Name of test name string
+    private static final String TEST_NAME = "testName";
+    // Name of rules object (one 'rules' object to a single test)
+    private static final String BUSINESS_LOGIC_RULES = "businessLogicRules";
+    // Name of rule conditions array
+    private static final String RULE_CONDITIONS = "ruleConditions";
+    // Name of rule actions array
+    private static final String RULE_ACTIONS = "ruleActions";
+    // Name of method name string
+    private static final String METHOD_NAME = "methodName";
+    // Name of method args array of strings
+    private static final String METHOD_ARGS = "methodArgs";
+
+    /**
+     * Create a BusinessLogic instance from a file of business logic data, formatted in JSON.
+     * This format is identical to that which is received from the Android Partner business logic
+     * service.
+     */
+    public static BusinessLogic createFromFile(File f) {
+        // Populate the map from testname to business rules for this new BusinessLogic instance
+        Map<String, List<BusinessLogicRule>> rulesMap = new HashMap<>();
+        BusinessLogic bl = new BusinessLogic();
+        try {
+            String businessLogicString = readFile(f);
+            JSONObject root = new JSONObject(businessLogicString);
+            JSONArray rulesLists = null;
+            try {
+                rulesLists = root.getJSONArray(BUSINESS_LOGIC_RULES_LISTS);
+            } catch (JSONException e) {
+                bl.mRules = rulesMap;
+                return bl; // no rules defined for this suite, leave internal map empty
+            }
+            for (int i = 0; i < rulesLists.length(); i++) {
+                JSONObject rulesList = rulesLists.getJSONObject(i);
+                String testName = rulesList.getString(TEST_NAME);
+                List<BusinessLogicRule> rules = new ArrayList<>();
+                JSONArray rulesJSONArray = null;
+                try {
+                    rulesJSONArray = rulesList.getJSONArray(BUSINESS_LOGIC_RULES);
+                } catch (JSONException e) {
+                    // no rules defined for this test case
+                    rulesMap.put(testName, rules); // add empty rule list to internal map
+                    continue; // advance to next test case
+                }
+                for (int j = 0; j < rulesJSONArray.length(); j++) {
+                    JSONObject ruleJSONObject = rulesJSONArray.getJSONObject(j);
+                    // Build conditions list
+                    List<BusinessLogicRuleCondition> ruleConditions =
+                            extractRuleConditionList(ruleJSONObject);
+                    // Build actions list
+                    List<BusinessLogicRuleAction> ruleActions =
+                            extractRuleActionList(ruleJSONObject);
+                    rules.add(new BusinessLogicRule(ruleConditions, ruleActions));
+                }
+                rulesMap.put(testName, rules);
+            }
+        } catch (IOException | JSONException e) {
+            throw new RuntimeException("Business Logic failed", e);
+        }
+        // Return business logic
+        bl.mRules = rulesMap;
+        return bl;
+    }
+
+    /* Extract all BusinessLogicRuleConditions from a JSON business logic rule */
+    private static List<BusinessLogicRuleCondition> extractRuleConditionList(
+            JSONObject ruleJSONObject) throws JSONException {
+        List<BusinessLogicRuleCondition> ruleConditions = new ArrayList<>();
+        // All rules require at least one condition, line below throws JSONException if not
+        JSONArray ruleConditionsJSONArray = ruleJSONObject.getJSONArray(RULE_CONDITIONS);
+        for (int i = 0; i < ruleConditionsJSONArray.length(); i++) {
+            JSONObject ruleConditionJSONObject = ruleConditionsJSONArray.getJSONObject(i);
+            String methodName = ruleConditionJSONObject.getString(METHOD_NAME);
+            // Each condition requires at least one arg, line below throws JSONException if not
+            JSONArray methodArgsJSONArray = ruleConditionJSONObject.getJSONArray(METHOD_ARGS);
+            List<String> methodArgs = new ArrayList<>();
+            for (int j = 0; j < methodArgsJSONArray.length(); j++) {
+                methodArgs.add(methodArgsJSONArray.getString(j));
+            }
+            ruleConditions.add(new BusinessLogicRuleCondition(methodName, methodArgs));
+        }
+        return ruleConditions;
+    }
+
+    /* Extract all BusinessLogicRuleActions from a JSON business logic rule */
+    private static List<BusinessLogicRuleAction> extractRuleActionList(JSONObject ruleJSONObject)
+            throws JSONException {
+        List<BusinessLogicRuleAction> ruleActions = new ArrayList<>();
+        // All rules require at least one action, line below throws JSONException if not
+        JSONArray ruleActionsJSONArray = ruleJSONObject.getJSONArray(RULE_ACTIONS);
+        for (int i = 0; i < ruleActionsJSONArray.length(); i++) {
+            JSONObject ruleActionJSONObject = ruleActionsJSONArray.getJSONObject(i);
+            String methodName = ruleActionJSONObject.getString(METHOD_NAME);
+            List<String> methodArgs = new ArrayList<>();
+            JSONArray methodArgsJSONArray = null;
+            try {
+                methodArgsJSONArray = ruleActionJSONObject.getJSONArray(METHOD_ARGS);
+            } catch (JSONException e) {
+                // No method args for this rule action, add rule action with empty args list
+                ruleActions.add(new BusinessLogicRuleAction(methodName, methodArgs));
+                continue;
+            }
+            for (int j = 0; j < methodArgsJSONArray.length(); j++) {
+                methodArgs.add(methodArgsJSONArray.getString(j));
+            }
+            ruleActions.add(new BusinessLogicRuleAction(methodName, methodArgs));
+        }
+        return ruleActions;
+    }
+
+    /* Extract string from file */
+    private static String readFile(File f) throws IOException {
+        StringBuilder sb = new StringBuilder((int) f.length());
+        String lineSeparator = System.getProperty("line.separator");
+        try (Scanner scanner = new Scanner(f)) {
+            while(scanner.hasNextLine()) {
+                sb.append(scanner.nextLine() + lineSeparator);
+            }
+            return sb.toString();
+        }
+    }
+}
diff --git a/common/util/tests/src/com/android/compatibility/common/util/BusinessLogicTest.java b/common/util/tests/src/com/android/compatibility/common/util/BusinessLogicTest.java
new file mode 100644
index 0000000..47ce5da
--- /dev/null
+++ b/common/util/tests/src/com/android/compatibility/common/util/BusinessLogicTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.android.compatibility.common.util.BusinessLogic.BusinessLogicRule;
+import com.android.compatibility.common.util.BusinessLogic.BusinessLogicRuleAction;
+import com.android.compatibility.common.util.BusinessLogic.BusinessLogicRuleCondition;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Unit tests for {@link BusinessLogic}
+ */
+@RunWith(JUnit4.class)
+public class BusinessLogicTest {
+
+    private static final String CORRECT_LOGIC =
+            "{\n" +
+            "  \"name\": \"businessLogic/suites/gts\",\n" +
+            "  \"businessLogicRulesLists\": [\n" +
+            "    {\n" +
+            "      \"testName\": \"testCaseName1\",\n" +
+            "      \"businessLogicRules\": [\n" +
+            "        {\n" +
+            "          \"ruleConditions\": [\n" +
+            "            {\n" +
+            "              \"methodName\": \"conditionMethodName1\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg1\"\n" +
+            "              ]\n" +
+            "            }\n" +
+            "          ],\n" +
+            "          \"ruleActions\": [\n" +
+            "            {\n" +
+            "              \"methodName\": \"actionMethodName1\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg1\",\n" +
+            "                \"arg2\"\n" +
+            "              ]\n" +
+            "            }\n" +
+            "          ]\n" +
+            "        }\n" +
+            "      ]\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"testName\": \"testCaseName2\",\n" +
+            "      \"businessLogicRules\": [\n" +
+            "        {\n" +
+            "          \"ruleConditions\": [\n" +
+            "            {\n" +
+            "              \"methodName\": \"conditionMethodName1\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg1\"\n" +
+            "              ]\n" +
+            "            }\n" +
+            "          ],\n" +
+            "          \"ruleActions\": [\n" +
+            "            {\n" +
+            "              \"methodName\": \"actionMethodName1\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg1\",\n" +
+            "                \"arg2\"\n" +
+            "              ]\n" +
+            "            }\n" +
+            "          ]\n" +
+            "        },\n" +
+            "        {\n" +
+            "          \"ruleConditions\": [\n" +
+            "            {\n" +
+            "              \"methodName\": \"conditionMethodName1\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg1\"\n" +
+            "              ]\n" +
+            "            },\n" +
+            "            {\n" +
+            "              \"methodName\": \"conditionMethodName2\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg2\"\n" +
+            "              ]\n" +
+            "            }\n" +
+            "          ],\n" +
+            "          \"ruleActions\": [\n" +
+            "            {\n" +
+            "              \"methodName\": \"actionMethodName1\",\n" +
+            "              \"methodArgs\": [\n" +
+            "                \"arg1\",\n" +
+            "                \"arg2\"\n" +
+            "              ]\n" +
+            "            },\n" +
+            "            {\n" +
+            "              \"methodName\": \"actionMethodName2\"\n" +
+            "            }\n" +
+            "          ]\n" +
+            "        }\n" +
+            "      ]\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"testName\": \"testCaseName3\"\n" +
+            "    }\n" +
+            "  ]\n" +
+            "}";
+
+    @Test
+    public void testCorrectLogic() throws Exception {
+        File file = createFileFromStr(CORRECT_LOGIC);
+        try {
+            BusinessLogic bl = BusinessLogicFactory.createFromFile(file);
+            assertEquals("Wrong number of business logic rule lists", 3, bl.mRules.size());
+
+            List<BusinessLogicRule> ruleList1 = bl.mRules.get("testCaseName1");
+            assertEquals("Wrong number of rules in first rule list", 1, ruleList1.size());
+            BusinessLogicRule rule1 = ruleList1.get(0);
+            List<BusinessLogicRuleCondition> rule1Conditions = rule1.mConditions;
+            assertEquals("Wrong number of conditions", 1, rule1Conditions.size());
+            BusinessLogicRuleCondition rule1Condition = rule1Conditions.get(0);
+            assertEquals("Wrong method name for business logic rule condition",
+                    "conditionMethodName1", rule1Condition.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule condition", 1,
+                    rule1Condition.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule condition", "arg1",
+                    rule1Condition.mMethodArgs.get(0));
+            List<BusinessLogicRuleAction> rule1Actions = rule1.mActions;
+            assertEquals("Wrong number of actions", 1, rule1Actions.size());
+            BusinessLogicRuleAction rule1Action = rule1Actions.get(0);
+            assertEquals("Wrong method name for business logic rule action",
+                    "actionMethodName1", rule1Action.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule action", 2,
+                    rule1Action.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule action", "arg1",
+                    rule1Action.mMethodArgs.get(0));
+            assertEquals("Wrong arg for business logic rule action", "arg2",
+                    rule1Action.mMethodArgs.get(1));
+
+            List<BusinessLogicRule> ruleList2 = bl.mRules.get("testCaseName2");
+            assertEquals("Wrong number of rules in second rule list", 2, ruleList2.size());
+            BusinessLogicRule rule2 = ruleList2.get(0);
+            List<BusinessLogicRuleCondition> rule2Conditions = rule2.mConditions;
+            assertEquals("Wrong number of conditions", 1, rule2Conditions.size());
+            BusinessLogicRuleCondition rule2Condition = rule2Conditions.get(0);
+            assertEquals("Wrong method name for business logic rule condition",
+                    "conditionMethodName1", rule2Condition.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule condition", 1,
+                    rule2Condition.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule condition", "arg1",
+                    rule2Condition.mMethodArgs.get(0));
+            List<BusinessLogicRuleAction> rule2Actions = rule2.mActions;
+            assertEquals("Wrong number of actions", 1, rule2Actions.size());
+            BusinessLogicRuleAction rule2Action = rule2Actions.get(0);
+            assertEquals("Wrong method name for business logic rule action",
+                    "actionMethodName1", rule2Action.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule action", 2,
+                    rule2Action.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule action", "arg1",
+                    rule2Action.mMethodArgs.get(0));
+            assertEquals("Wrong arg for business logic rule action", "arg2",
+                    rule2Action.mMethodArgs.get(1));
+            BusinessLogicRule rule3 = ruleList2.get(1);
+            List<BusinessLogicRuleCondition> rule3Conditions = rule3.mConditions;
+            assertEquals("Wrong number of conditions", 2, rule3Conditions.size());
+            BusinessLogicRuleCondition rule3Condition1 = rule3Conditions.get(0);
+            assertEquals("Wrong method name for business logic rule condition",
+                    "conditionMethodName1", rule3Condition1.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule condition", 1,
+                    rule3Condition1.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule condition", "arg1",
+                    rule3Condition1.mMethodArgs.get(0));
+            BusinessLogicRuleCondition rule3Condition2 = rule3Conditions.get(1);
+            assertEquals("Wrong method name for business logic rule condition",
+                    "conditionMethodName2", rule3Condition2.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule condition", 1,
+                    rule3Condition2.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule condition", "arg2",
+                    rule3Condition2.mMethodArgs.get(0));
+            List<BusinessLogicRuleAction> rule3Actions = rule3.mActions;
+            assertEquals("Wrong number of actions", 2, rule3Actions.size());
+            BusinessLogicRuleAction rule3Action1 = rule3Actions.get(0);
+            assertEquals("Wrong method name for business logic rule action",
+                    "actionMethodName1", rule3Action1.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule action", 2,
+                    rule3Action1.mMethodArgs.size());
+            assertEquals("Wrong arg for business logic rule action", "arg1",
+                    rule3Action1.mMethodArgs.get(0));
+            assertEquals("Wrong arg for business logic rule action", "arg2",
+                    rule3Action1.mMethodArgs.get(1));
+            BusinessLogicRuleAction rule3Action2 = rule3Actions.get(1);
+            assertEquals("Wrong method name for business logic rule action",
+                    "actionMethodName2", rule3Action2.mMethodName);
+            assertEquals("Wrong arg string count for business logic rule action", 0,
+                    rule3Action2.mMethodArgs.size());
+
+            List<BusinessLogicRule> ruleList3 = bl.mRules.get("testCaseName3");
+            assertEquals("Wrong number of rules in third rule list", 0, ruleList3.size());
+        } finally {
+            FileUtil.deleteFile(file);
+        }
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testLogicWithWrongNodeName() throws Exception {
+        File file = createFileFromStr(CORRECT_LOGIC.replace("testName", "testNam3"));
+        try {
+            BusinessLogic bl = BusinessLogicFactory.createFromFile(file);
+        } finally {
+            FileUtil.deleteFile(file);
+        }
+    }
+
+    private static File createFileFromStr(String blString) throws IOException {
+        File file = File.createTempFile("test", "bl");
+        FileOutputStream stream = new FileOutputStream(file);
+        stream.write(blString.getBytes());
+        stream.flush();
+        stream.close();
+        return file;
+    }
+}
diff --git a/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java b/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
index 9f8235c..6333342 100644
--- a/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
+++ b/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
@@ -15,30 +15,28 @@
  */
 package com.android.compatibility.common.util;
 
-import junit.framework.Test;
-import junit.framework.TestSuite;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
 
 /**
  * A test suite for all util unit tests.
  * <p/>
  * All tests listed here should be self-contained, and do not require any external dependencies.
  */
-public class UnitTests extends TestSuite {
-
-    public UnitTests() {
-        super();
-        addTestSuite(CaseResultTest.class);
-        addTestSuite(DynamicConfigTest.class);
-        addTestSuite(LightInvocationResultTest.class);
-        addTestSuite(MetricsXmlSerializerTest.class);
-        addTestSuite(MultipartFormTest.class);
-        addTestSuite(ReportLogTest.class);
-        addTestSuite(ResultHandlerTest.class);
-        addTestSuite(StatTest.class);
-        addTestSuite(TestResultTest.class);
-    }
-
-    public static Test suite() {
-        return new UnitTests();
-    }
+@RunWith(Suite.class)
+@SuiteClasses({
+    BusinessLogicTest.class,
+    CaseResultTest.class,
+    DynamicConfigTest.class,
+    LightInvocationResultTest.class,
+    MetricsXmlSerializerTest.class,
+    MultipartFormTest.class,
+    ReportLogTest.class,
+    ResultHandlerTest.class,
+    StatTest.class,
+    TestResultTest.class,
+})
+public class UnitTests {
+    // empty on purpose
 }