DeqpRunner implements IStrictShardableTest
authorJulien Desprez <jdesprez@google.com>
Fri, 4 Nov 2016 15:04:08 +0000 (15:04 +0000)
committerJulien Desprez <jdesprez@google.com>
Fri, 18 Nov 2016 10:37:48 +0000 (10:37 +0000)
Make runner implements new strict shardable interface.
Also includes some minor clean up of the runner.

Test: build cts, run unit tests,
run cts --compatibility:include-filter CtsDeqpTestCases
with sharding.
Bug: 30449039

Change-Id: I405ccd01cf98a8e08457df63614545dd542ff2c8

.gitignore
android/cts/runner/src/com/drawelements/deqp/runner/BatchRunConfiguration.java [new file with mode: 0644]
android/cts/runner/src/com/drawelements/deqp/runner/DeqpTestRunner.java
android/cts/runner/tests/src/com/drawelements/deqp/runner/DeqpTestRunnerTest.java

index dbd311c..a7a5af1 100644 (file)
@@ -1,6 +1,7 @@
 *~
 *.pyc
 *.user
+*.class
 .*
 !.gitignore
 !.editorconfig
diff --git a/android/cts/runner/src/com/drawelements/deqp/runner/BatchRunConfiguration.java b/android/cts/runner/src/com/drawelements/deqp/runner/BatchRunConfiguration.java
new file mode 100644 (file)
index 0000000..590fbbf
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 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.drawelements.deqp.runner;
+
+/**
+ * Test configuration of dEPQ test instance execution.
+ */
+public class BatchRunConfiguration {
+    public static final String ROTATION_UNSPECIFIED = "unspecified";
+    public static final String ROTATION_PORTRAIT = "0";
+    public static final String ROTATION_LANDSCAPE = "90";
+    public static final String ROTATION_REVERSE_PORTRAIT = "180";
+    public static final String ROTATION_REVERSE_LANDSCAPE = "270";
+
+    private final String mGlConfig;
+    private final String mRotation;
+    private final String mSurfaceType;
+    private final boolean mRequired;
+
+    public BatchRunConfiguration(String glConfig, String rotation, String surfaceType,
+            boolean required) {
+        mGlConfig = glConfig;
+        mRotation = rotation;
+        mSurfaceType = surfaceType;
+        mRequired = required;
+    }
+
+    /**
+     * Get string that uniquely identifies this config
+     */
+    public String getId() {
+        return String.format("{glformat=%s,rotation=%s,surfacetype=%s,required=%b}",
+                mGlConfig, mRotation, mSurfaceType, mRequired);
+    }
+
+    /**
+     * Get the GL config used in this configuration.
+     */
+    public String getGlConfig() {
+        return mGlConfig;
+    }
+
+    /**
+     * Get the screen rotation used in this configuration.
+     */
+    public String getRotation() {
+        return mRotation;
+    }
+
+    /**
+     * Get the surface type used in this configuration.
+     */
+    public String getSurfaceType() {
+        return mSurfaceType;
+    }
+
+    /**
+     * Is this configuration mandatory to support, if target API is supported?
+     */
+    public boolean isRequired() {
+        return mRequired;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) {
+            return false;
+        } else if (!(other instanceof BatchRunConfiguration)) {
+            return false;
+        } else {
+            return getId().equals(((BatchRunConfiguration)other).getId());
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return getId().hashCode();
+    }
+}
index 9050705..776488c 100644 (file)
@@ -23,7 +23,6 @@ import com.android.ddmlib.ShellCommandUnresponsiveException;
 import com.android.ddmlib.TimeoutException;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.build.IFolderBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -33,14 +32,16 @@ import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.testtype.IAbi;
-import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.IShardableTest;
+import com.android.tradefed.testtype.IStrictShardableTest;
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.testtype.StubTest;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunInterruptedException;
@@ -52,8 +53,8 @@ import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.Reader;
-import java.lang.reflect.Method;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -65,9 +66,9 @@ import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Pattern;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 
 /**
  * Test runner for dEQP tests
@@ -77,7 +78,7 @@ import java.util.concurrent.TimeUnit;
 @OptionClass(alias="deqp-test-runner")
 public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
         ITestFilterReceiver, IAbiReceiver, IShardableTest, ITestCollector,
-        IRuntimeHintProvider {
+        IRuntimeHintProvider, IStrictShardableTest {
     private static final String DEQP_ONDEVICE_APK = "com.drawelements.deqp.apk";
     private static final String DEQP_ONDEVICE_PKG = "com.drawelements.deqp";
     private static final String INCOMPLETE_LOG_MESSAGE = "Crash: Incomplete test log";
@@ -245,82 +246,6 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
     }
 
     /**
-     * Test configuration of dEPQ test instance execution.
-     * Exposed for unit testing
-     */
-    public static final class BatchRunConfiguration {
-        public static final String ROTATION_UNSPECIFIED = "unspecified";
-        public static final String ROTATION_PORTRAIT = "0";
-        public static final String ROTATION_LANDSCAPE = "90";
-        public static final String ROTATION_REVERSE_PORTRAIT = "180";
-        public static final String ROTATION_REVERSE_LANDSCAPE = "270";
-
-        private final String mGlConfig;
-        private final String mRotation;
-        private final String mSurfaceType;
-        private final boolean mRequired;
-
-        public BatchRunConfiguration(String glConfig, String rotation, String surfaceType, boolean required) {
-            mGlConfig = glConfig;
-            mRotation = rotation;
-            mSurfaceType = surfaceType;
-            mRequired = required;
-        }
-
-        /**
-         * Get string that uniquely identifies this config
-         */
-        public String getId() {
-            return String.format("{glformat=%s,rotation=%s,surfacetype=%s,required=%b}",
-                    mGlConfig, mRotation, mSurfaceType, mRequired);
-        }
-
-        /**
-         * Get the GL config used in this configuration.
-         */
-        public String getGlConfig() {
-            return mGlConfig;
-        }
-
-        /**
-         * Get the screen rotation used in this configuration.
-         */
-        public String getRotation() {
-            return mRotation;
-        }
-
-        /**
-         * Get the surface type used in this configuration.
-         */
-        public String getSurfaceType() {
-            return mSurfaceType;
-        }
-
-        /**
-         * Is this configuration mandatory to support, if target API is supported?
-         */
-        public boolean isRequired() {
-            return mRequired;
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (other == null) {
-                return false;
-            } else if (!(other instanceof BatchRunConfiguration)) {
-                return false;
-            } else {
-                return getId().equals(((BatchRunConfiguration)other).getId());
-            }
-        }
-
-        @Override
-        public int hashCode() {
-            return getId().hashCode();
-        }
-    }
-
-    /**
      * dEQP test instance listerer and invocation result forwarded
      */
     private class TestInstanceResultListener {
@@ -819,11 +744,9 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
     }
 
     private static class SleepProvider implements ISleepProvider {
+        @Override
         public void sleep(int milliseconds) {
-            try {
-                Thread.sleep(milliseconds);
-            } catch (InterruptedException ex) {
-            }
+            RunUtil.getDefault().sleep(milliseconds);
         }
     }
 
@@ -858,11 +781,10 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
         /**
          * Tries to recover device after abnormal execution termination or link failure.
          *
-         * @param progressedSinceLastCall true if test execution has progressed since last call
          * @throws DeviceNotAvailableException if recovery did not succeed
          */
         public void recoverComLinkKilled() throws DeviceNotAvailableException;
-    };
+    }
 
     /**
      * State machine for execution failure recovery.
@@ -878,7 +800,7 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
             RECOVER, // recover by calling recover()
             REBOOT, // recover by rebooting
             FAIL, // cannot recover
-        };
+        }
 
         private MachineState mState = MachineState.WAIT;
         private ITestDevice mDevice;
@@ -890,6 +812,7 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
         /**
          * {@inheritDoc}
          */
+        @Override
         public void setSleepProvider(ISleepProvider sleepProvider) {
             mSleepProvider = sleepProvider;
         }
@@ -946,7 +869,8 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
                 case FAIL:
                     // Third failure in a row, just fail
                     CLog.w("Cannot recover ADB connection");
-                    throw new DeviceNotAvailableException("failed to connect after reboot");
+                    throw new DeviceNotAvailableException("failed to connect after reboot",
+                            mDevice.getSerialNumber());
             }
         }
 
@@ -1009,7 +933,8 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
                 case FAIL:
                     // Fourth failure in a row, just fail
                     CLog.w("Cannot recover ADB connection");
-                    throw new DeviceNotAvailableException("link killed after reboot");
+                    throw new DeviceNotAvailableException("link killed after reboot",
+                            mDevice.getSerialNumber());
             }
         }
 
@@ -1086,7 +1011,8 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
     }
 
     private static Map<TestIdentifier, Set<BatchRunConfiguration>> generateTestInstances(
-            Reader testlist, String configName, String screenRotation, String surfaceType, boolean required) throws FileNotFoundException {
+            Reader testlist, String configName, String screenRotation, String surfaceType,
+            boolean required) {
         // Note: This is specifically a LinkedHashMap to guarantee that tests are iterated
         // in the insertion order.
         final Map<TestIdentifier, Set<BatchRunConfiguration>> instances = new LinkedHashMap<>();
@@ -1111,11 +1037,18 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
         return instances;
     }
 
-    private Set<BatchRunConfiguration> getTestRunConfigs (TestIdentifier testId) {
+    private Set<BatchRunConfiguration> getTestRunConfigs(TestIdentifier testId) {
         return mTestInstances.get(testId);
     }
 
     /**
+     * Get the test instance of the runner. Exposed for testing.
+     */
+    Map<TestIdentifier, Set<BatchRunConfiguration>> getTestInstance() {
+        return mTestInstances;
+    }
+
+    /**
      * Converts dEQP testcase path to TestIdentifier.
      */
     private static TestIdentifier pathToIdentifier(String testPath) {
@@ -1883,7 +1816,7 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
     /**
      * Get GL major version (based on package name)
      */
-    private int getGlesMajorVersion() throws DeviceNotAvailableException {
+    private int getGlesMajorVersion() {
         if ("dEQP-GLES2".equals(mDeqpPackage)) {
             return 2;
         } else if ("dEQP-GLES3".equals(mDeqpPackage)) {
@@ -1898,7 +1831,7 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
     /**
      * Get GL minor version (based on package name)
      */
-    private int getGlesMinorVersion() throws DeviceNotAvailableException {
+    private int getGlesMinorVersion() {
         if ("dEQP-GLES2".equals(mDeqpPackage)) {
             return 0;
         } else if ("dEQP-GLES3".equals(mDeqpPackage)) {
@@ -1955,7 +1888,7 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
         List<Pattern> includePatterns = getPatternFilters(includeFilters);
         List<Pattern> excludePatterns = getPatternFilters(excludeFilters);
 
-        List<TestIdentifier> testList = new ArrayList(tests.keySet());
+        List<TestIdentifier> testList = new ArrayList<>(tests.keySet());
         for (TestIdentifier test : testList) {
             if (excludeStrings.contains(test.toString())) {
                 tests.remove(test); // remove test if explicitly excluded
@@ -2120,6 +2053,20 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
     }
 
     /**
+     * Helper to update the RuntimeHint of the tests after being sharded.
+     */
+    private void updateRuntimeHint(long originalSize, Collection<IRemoteTest> runners) {
+        if (originalSize > 0) {
+            long fullRuntimeMs = getRuntimeHint();
+            for (IRemoteTest remote: runners) {
+                DeqpTestRunner runner = (DeqpTestRunner)remote;
+                long shardRuntime = (fullRuntimeMs * runner.mTestInstances.size()) / originalSize;
+                runner.mRuntimeHint = shardRuntime;
+            }
+        }
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
@@ -2138,6 +2085,11 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
         Map<TestIdentifier, Set<BatchRunConfiguration>> currentSet = new LinkedHashMap<>();
         Map<TestIdentifier, Set<BatchRunConfiguration>> iterationSet = this.mTestInstances;
 
+        if (iterationSet.keySet().isEmpty()) {
+            CLog.i("Cannot split deqp tests, no tests to run");
+            return null;
+        }
+
         // Go through tests, split
         for (TestIdentifier test: iterationSet.keySet()) {
             currentSet.put(test, iterationSet.get(test));
@@ -2145,23 +2097,58 @@ public class DeqpTestRunner implements IBuildReceiver, IDeviceTest,
                 runners.add(new DeqpTestRunner(this, currentSet));
                 // NOTE: Use linked hash map to keep the insertion order in iteration
                 currentSet = new LinkedHashMap<>();
-             }
+            }
         }
         runners.add(new DeqpTestRunner(this, currentSet));
 
         // Compute new runtime hints
-        long originalSize = iterationSet.size();
-        if (originalSize > 0) {
-            long fullRuntimeMs = getRuntimeHint();
-            for (IRemoteTest remote: runners) {
-                DeqpTestRunner runner = (DeqpTestRunner)remote;
-                long shardRuntime = (fullRuntimeMs * runner.mTestInstances.size()) / originalSize;
-                runner.mRuntimeHint = shardRuntime;
+        updateRuntimeHint(iterationSet.size(), runners);
+        CLog.i("Split deqp tests into %d shards", runners.size());
+        return runners;
+    }
+
+    /**
+     * This sharding should be deterministic for the same input and independent.
+     * Through this API, each shard could be executed on different machine.
+     */
+    @Override
+    public IRemoteTest getTestShard(int shardCount, int shardIndex) {
+        // TODO: refactor getTestshard and split to share some logic.
+        if (mTestInstances == null) {
+            loadTests();
+        }
+
+        List<IRemoteTest> runners = new ArrayList<>();
+        // NOTE: Use linked hash map to keep the insertion order in iteration
+        Map<TestIdentifier, Set<BatchRunConfiguration>> currentSet = new LinkedHashMap<>();
+        Map<TestIdentifier, Set<BatchRunConfiguration>> iterationSet = this.mTestInstances;
+
+        int batchLimit = iterationSet.keySet().size() / shardCount;
+        int i = 1;
+        // Go through tests, split
+        for (TestIdentifier test: iterationSet.keySet()) {
+            currentSet.put(test, iterationSet.get(test));
+            if (currentSet.size() >= batchLimit && i < shardCount) {
+                runners.add(new DeqpTestRunner(this, currentSet));
+                i++;
+                // NOTE: Use linked hash map to keep the insertion order in iteration
+                currentSet = new LinkedHashMap<>();
             }
         }
+        runners.add(new DeqpTestRunner(this, currentSet));
 
-        CLog.i("Split deqp tests into %d shards", runners.size());
-        return runners;
+        // Compute new runtime hints
+        updateRuntimeHint(iterationSet.size(), runners);
+
+        // If too many shards were requested, we complete with placeholder.
+        if (runners.size() < shardCount) {
+            for (int j = runners.size(); j < shardCount; j++) {
+                runners.add(new StubTest());
+            }
+        }
+
+        CLog.i("Split deqp tests into %d shards, return shard: %s", runners.size(), shardIndex);
+        return runners.get(shardIndex);
     }
 
     /**
index 7fd6e2e..7030693 100644 (file)
@@ -18,7 +18,6 @@ package com.drawelements.deqp.runner;
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.IShellOutputReceiver;
-import com.android.ddmlib.ShellCommandUnresponsiveException;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.IFolderBuildInfo;
 import com.android.tradefed.config.ConfigurationException;
@@ -36,10 +35,6 @@ import com.android.tradefed.util.RunInterruptedException;
 
 import junit.framework.TestCase;
 
-import org.easymock.EasyMock;
-import org.easymock.IAnswer;
-import org.easymock.IMocksControl;
-
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.StringReader;
@@ -53,6 +48,10 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.easymock.IMocksControl;
+
 /**
  * Unit tests for {@link DeqpTestRunner}.
  */
@@ -81,43 +80,6 @@ public class DeqpTestRunnerTest extends TestCase {
         DEFAULT_INSTANCE_ARGS.iterator().next().put("surfacetype", "window");
     }
 
-    private static class StubRecovery implements DeqpTestRunner.IRecovery {
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void setSleepProvider(DeqpTestRunner.ISleepProvider sleepProvider) {
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void setDevice(ITestDevice device) {
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void onExecutionProgressed() {
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void recoverConnectionRefused() throws DeviceNotAvailableException {
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void recoverComLinkKilled() throws DeviceNotAvailableException {
-        }
-    };
-
     public static class BuildHelperMock extends CompatibilityBuildHelper {
         public BuildHelperMock(IFolderBuildInfo buildInfo) {
             super(buildInfo);
@@ -734,7 +696,7 @@ public class DeqpTestRunnerTest extends TestCase {
 
         String expectedTrie = "{dEQP-GLES3{pick_me{yes,ok,accepted}}}";
 
-        Set<String> includes = new HashSet();
+        Set<String> includes = new HashSet<>();
         includes.add("dEQP-GLES3.pick_me#*");
         testFiltering(includes, null, allTests, expectedTrie, activeTests);
     }
@@ -761,7 +723,7 @@ public class DeqpTestRunnerTest extends TestCase {
 
         String expectedTrie = "{dEQP-GLES3{pick_me{yes,ok,accepted}}}";
 
-        Set<String> excludes = new HashSet();
+        Set<String> excludes = new HashSet<>();
         excludes.add("dEQP-GLES3.missing#*");
         testFiltering(null, excludes, allTests, expectedTrie, activeTests);
     }
@@ -786,10 +748,10 @@ public class DeqpTestRunnerTest extends TestCase {
 
         String expectedTrie = "{dEQP-GLES3{group2{yes}}}";
 
-        Set<String> includes = new HashSet();
+        Set<String> includes = new HashSet<>();
         includes.add("dEQP-GLES3.group2#*");
 
-        Set<String> excludes = new HashSet();
+        Set<String> excludes = new HashSet<>();
         excludes.add("*foo");
         excludes.add("*thoushallnotpass");
         testFiltering(includes, excludes, allTests, expectedTrie, activeTests);
@@ -812,7 +774,7 @@ public class DeqpTestRunnerTest extends TestCase {
 
         String expectedTrie = "{dEQP-GLES3{group1{mememe,yeah,takeitall},group2{jeba,yes,granted}}}";
 
-        Set<String> includes = new HashSet();
+        Set<String> includes = new HashSet<>();
         includes.add("*");
 
         testFiltering(includes, null, allTests, expectedTrie, allTests);
@@ -835,7 +797,7 @@ public class DeqpTestRunnerTest extends TestCase {
 
         String expectedTrie = "";
 
-        Set<String> excludes = new HashSet();
+        Set<String> excludes = new HashSet<>();
         excludes.add("*");
 
         testFiltering(null, excludes, allTests, expectedTrie, new ArrayList<TestIdentifier>());
@@ -1113,19 +1075,19 @@ public class DeqpTestRunnerTest extends TestCase {
         EasyMock.expect(mockDevice.getProperty("ro.opengles.version"))
                 .andReturn(Integer.toString(version)).atLeastOnce();
 
-        if (!rotation.equals(DeqpTestRunner.BatchRunConfiguration.ROTATION_UNSPECIFIED)) {
+        if (!rotation.equals(BatchRunConfiguration.ROTATION_UNSPECIFIED)) {
             EasyMock.expect(mockDevice.executeShellCommand("pm list features"))
                     .andReturn(featureString);
         }
 
         final boolean isPortraitOrientation =
-                rotation.equals(DeqpTestRunner.BatchRunConfiguration.ROTATION_PORTRAIT) ||
-                rotation.equals(DeqpTestRunner.BatchRunConfiguration.ROTATION_REVERSE_PORTRAIT);
+                rotation.equals(BatchRunConfiguration.ROTATION_PORTRAIT) ||
+                rotation.equals(BatchRunConfiguration.ROTATION_REVERSE_PORTRAIT);
         final boolean isLandscapeOrientation =
-                rotation.equals(DeqpTestRunner.BatchRunConfiguration.ROTATION_LANDSCAPE) ||
-                rotation.equals(DeqpTestRunner.BatchRunConfiguration.ROTATION_REVERSE_LANDSCAPE);
+                rotation.equals(BatchRunConfiguration.ROTATION_LANDSCAPE) ||
+                rotation.equals(BatchRunConfiguration.ROTATION_REVERSE_LANDSCAPE);
         final boolean executable =
-                rotation.equals(DeqpTestRunner.BatchRunConfiguration.ROTATION_UNSPECIFIED) ||
+                rotation.equals(BatchRunConfiguration.ROTATION_UNSPECIFIED) ||
                 (isPortraitOrientation &&
                 featureString.contains(DeqpTestRunner.FEATURE_PORTRAIT)) ||
                 (isLandscapeOrientation &&
@@ -1345,34 +1307,6 @@ public class DeqpTestRunnerTest extends TestCase {
     public void testRun_unsupportedPixelFormat() throws Exception {
         final String pixelFormat = "rgba5658d16m4";
         final TestIdentifier testId = new TestIdentifier("dEQP-GLES3.pixelformat", "test");
-        final String testPath = "dEQP-GLES3.pixelformat.test";
-        final String testTrie = "{dEQP-GLES3{pixelformat{test}}}";
-        final String output = "INSTRUMENTATION_STATUS: dEQP-SessionInfo-Name=releaseName\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=SessionInfo\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-SessionInfo-Value=2014.x\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-SessionInfo-Name=releaseId\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=SessionInfo\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-SessionInfo-Value=0xcafebabe\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-SessionInfo-Name=targetName\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=SessionInfo\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-SessionInfo-Value=android\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=BeginSession\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=BeginTestCase\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-BeginTestCase-TestCasePath=" + testPath + "\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-TestCaseResult-Code=Pass\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-TestCaseResult-Details=Pass\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=TestCaseResult\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=EndTestCase\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_STATUS: dEQP-EventType=EndSession\r\n"
-                + "INSTRUMENTATION_STATUS_CODE: 0\r\n"
-                + "INSTRUMENTATION_CODE: 0\r\n";
 
         ITestDevice mockDevice = EasyMock.createMock(ITestDevice.class);
         ITestInvocationListener mockListener
@@ -1435,7 +1369,7 @@ public class DeqpTestRunnerTest extends TestCase {
         PROGRESS,
         FAIL_CONNECTION_REFUSED,
         FAIL_LINK_KILLED,
-    };
+    }
 
     private void runRecoveryWithPattern(DeqpTestRunner.Recovery recovery, RecoveryEvent[] events)
             throws DeviceNotAvailableException {
@@ -1551,6 +1485,7 @@ public class DeqpTestRunnerTest extends TestCase {
         DeqpTestRunner.Recovery recovery = new DeqpTestRunner.Recovery();
         IMocksControl orderedControl = EasyMock.createStrictControl();
         RecoverableTestDevice mockDevice = orderedControl.createMock(RecoverableTestDevice.class);
+        EasyMock.expect(mockDevice.getSerialNumber()).andStubReturn("SERIAL");
         DeqpTestRunner.ISleepProvider mockSleepProvider =
                 orderedControl.createMock(DeqpTestRunner.ISleepProvider.class);
 
@@ -1897,7 +1832,8 @@ public class DeqpTestRunnerTest extends TestCase {
     public void testSharding_empty() throws Exception {
         DeqpTestRunner runner = buildGlesTestRunner(3, 0, new ArrayList<TestIdentifier>());
         ArrayList<IRemoteTest> shards = (ArrayList<IRemoteTest>)runner.split();
-        // \todo [2015-11-23 kalle] What should the result be? The runner or nothing?
+        // Returns null when cannot be sharded.
+        assertNull(shards);
     }
 
     /**
@@ -2191,6 +2127,56 @@ public class DeqpTestRunnerTest extends TestCase {
                  ((IRuntimeHintProvider)shards.get(1)).getRuntimeHint());
     }
 
+    /**
+     * Test that strict shardable is able to split deterministically the set of tests.
+     */
+    public void testGetTestShard() throws Exception {
+        final int TEST_COUNT = 2237;
+        final int SHARD_COUNT = 4;
+
+        ArrayList<TestIdentifier> testIds = new ArrayList<>(TEST_COUNT);
+        for (int i = 0; i < TEST_COUNT; i++) {
+            testIds.add(new TestIdentifier("dEQP-GLES3.funny.group", String.valueOf(i)));
+        }
+
+        DeqpTestRunner deqpTest = buildGlesTestRunner(3, 0, testIds);
+        OptionSetter setter = new OptionSetter(deqpTest);
+        final long fullRuntimeMs = testIds.size()*100;
+        setter.setOptionValue("runtime-hint", String.valueOf(fullRuntimeMs));
+
+        DeqpTestRunner shard1 = (DeqpTestRunner)deqpTest.getTestShard(SHARD_COUNT, 0);
+        assertEquals(559, shard1.getTestInstance().size());
+        int j = 0;
+        // Ensure numbers, and that order is stable
+        for (TestIdentifier t : shard1.getTestInstance().keySet()) {
+            assertEquals(String.format("dEQP-GLES3.funny.group#%s", j),
+                    String.format("%s#%s", t.getClassName(), t.getTestName()));
+            j++;
+        }
+        DeqpTestRunner shard2 = (DeqpTestRunner)deqpTest.getTestShard(SHARD_COUNT, 1);
+        assertEquals(559, shard2.getTestInstance().size());
+        for (TestIdentifier t : shard2.getTestInstance().keySet()) {
+            assertEquals(String.format("dEQP-GLES3.funny.group#%s", j),
+                    String.format("%s#%s", t.getClassName(), t.getTestName()));
+            j++;
+        }
+        DeqpTestRunner shard3 = (DeqpTestRunner)deqpTest.getTestShard(SHARD_COUNT, 2);
+        assertEquals(559, shard3.getTestInstance().size());
+        for (TestIdentifier t : shard3.getTestInstance().keySet()) {
+            assertEquals(String.format("dEQP-GLES3.funny.group#%s", j),
+                    String.format("%s#%s", t.getClassName(), t.getTestName()));
+            j++;
+        }
+        DeqpTestRunner shard4 = (DeqpTestRunner)deqpTest.getTestShard(SHARD_COUNT, 3);
+        assertEquals(560, shard4.getTestInstance().size());
+        for (TestIdentifier t : shard4.getTestInstance().keySet()) {
+            assertEquals(String.format("dEQP-GLES3.funny.group#%s", j),
+                    String.format("%s#%s", t.getClassName(), t.getTestName()));
+            j++;
+        }
+        assertEquals(TEST_COUNT, j);
+    }
+
     public void testRuntimeHint_optionNotSet() throws Exception {
         final TestIdentifier[] testIds = {
                 new TestIdentifier("dEQP-GLES3.info", "vendor"),