Test batches will be sent to the client as they are executed by JUnitShell if NoBatchingStrategy (the default) is used. This allows users to use test runners that shard test methods across different clients.

Patch by: jlabanca
Review by: fabbott



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6279 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/junit/BatchingStrategy.java b/user/src/com/google/gwt/junit/BatchingStrategy.java
index 14a5a11..96551e4 100644
--- a/user/src/com/google/gwt/junit/BatchingStrategy.java
+++ b/user/src/com/google/gwt/junit/BatchingStrategy.java
@@ -40,6 +40,21 @@
   public abstract List<TestInfo[]> getTestBlocks(String syntheticModuleName);
 
   /**
+   * Check if this batching strategy only supports execution of a single test at
+   * a time.
+   * 
+   * If this method returns true, test methods will be executed on the client as
+   * they are run by JUnit. If it returns false, test methods will be batched
+   * and sent to the clients in groups. If you are using a test runner that
+   * shards test methods across multiple clients, you should use a strategy that
+   * returns false (such as {@link NoBatchingStrategy}) or all tests will be
+   * executed on all clients.
+   * 
+   * @return true if batches never contain more than one test
+   */
+  public abstract boolean isSingleTestOnly();
+
+  /**
    * Get the set of tests for this module, minus tests that should not be
    * executed.
    * 
@@ -72,6 +87,11 @@
     }
     return testBlocks;
   }
+
+  @Override
+  public boolean isSingleTestOnly() {
+    return true;
+  }
 }
 
 /**
@@ -105,6 +125,11 @@
     }
     return testBlocks;
   }
+
+  @Override
+  public boolean isSingleTestOnly() {
+    return false;
+  }
 }
 
 /**
@@ -121,4 +146,9 @@
     }
     return testBlocks;
   }
+
+  @Override
+  public boolean isSingleTestOnly() {
+    return false;
+  }
 }
diff --git a/user/src/com/google/gwt/junit/CompileStrategy.java b/user/src/com/google/gwt/junit/CompileStrategy.java
index 7b36739..bc3a0e1 100644
--- a/user/src/com/google/gwt/junit/CompileStrategy.java
+++ b/user/src/com/google/gwt/junit/CompileStrategy.java
@@ -45,6 +45,23 @@
   private Set<String> compiledModuleNames = new HashSet<String>();
 
   /**
+   * Maybe add a test block for the currently executed test case.
+   * 
+   * @param testCase the test case being run
+   * @param batchingStrategy the batching strategy
+   */
+  public void maybeAddTestBlockForCurrentTest(GWTTestCase testCase,
+      BatchingStrategy batchingStrategy) {
+    if (batchingStrategy.isSingleTestOnly()) {
+      TestInfo testInfo = new TestInfo(testCase.getSyntheticModuleName(),
+          testCase.getClass().getName(), testCase.getName());
+      List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>(1);
+      testBlocks.add(new TestInfo[] {testInfo});
+      getMessageQueue().addTestBlocks(testBlocks, false);
+    }
+  }
+
+  /**
    * Let the compile strategy compile another module. This is called while
    * {@link JUnitShell} is waiting for the current test to complete.
    * 
@@ -85,6 +102,57 @@
       BatchingStrategy batchingStrategy, TreeLogger treeLogger)
       throws UnableToCompleteException {
 
+    // Let the runstyle compile the module.
+    ModuleDef moduleDef = maybeCompileModuleImpl2(moduleName,
+        syntheticModuleName, strategy, runStyle, treeLogger);
+
+    // Add all test blocks for the module if we haven't seen this module before.
+    if (!compiledModuleNames.contains(syntheticModuleName)) {
+      compiledModuleNames.add(syntheticModuleName);
+      if (!batchingStrategy.isSingleTestOnly()) {
+        // Use >= so we can mock getModuleCount and force isFinalModule to true
+        boolean isFinalModule = compiledModuleNames.size() >= getModuleCount();
+        List<TestInfo[]> testBlocks = batchingStrategy.getTestBlocks(syntheticModuleName);
+        getMessageQueue().addTestBlocks(testBlocks, isFinalModule);
+      }
+    }
+
+    return moduleDef;
+  }
+
+  /**
+   * Visible for testing and mocking.
+   * 
+   * @return the {@link JUnitMessageQueue}
+   */
+  JUnitMessageQueue getMessageQueue() {
+    return JUnitShell.getMessageQueue();
+  }
+
+  /**
+   * Visible for testing and mocking.
+   * 
+   * @return the number of modules to test
+   */
+  int getModuleCount() {
+    return GWTTestCase.getModuleCount();
+  }
+
+  /**
+   * Let the {@link RunStyle} compile the module if needed
+   * 
+   * Visible for testing and mocking.
+   * 
+   * @param moduleName the module name
+   * @param syntheticModuleName the synthetic module name
+   * @param strategy the strategy
+   * @param runStyle the run style
+   * @param treeLogger the logger
+   * @return the {@link ModuleDef} describing the synthetic module
+   */
+  ModuleDef maybeCompileModuleImpl2(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      TreeLogger treeLogger) throws UnableToCompleteException {
     /*
      * Synthesize a synthetic module that derives from the user-specified module
      * but also includes JUnit support.
@@ -104,14 +172,6 @@
 
     runStyle.maybeCompileModule(syntheticModuleName);
 
-    // Add all test blocks for the module if we haven't seen this module before.
-    if (!compiledModuleNames.contains(syntheticModuleName)) {
-      compiledModuleNames.add(syntheticModuleName);
-      boolean isFinalModule = compiledModuleNames.size() == GWTTestCase.getModuleCount();
-      List<TestInfo[]> testBlocks = batchingStrategy.getTestBlocks(syntheticModuleName);
-      JUnitShell.getMessageQueue().addTestBlocks(testBlocks, isFinalModule);
-    }
-
     return moduleDef;
   }
 }
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index e98603d..34f2b9a 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -1005,6 +1005,7 @@
       processTestResult(testCase, testResult, strategy);
       return;
     }
+    compileStrategy.maybeAddTestBlockForCurrentTest(testCase, batchingStrategy);
 
     try {
       if (firstLaunch) {
diff --git a/user/test/com/google/gwt/junit/BatchingStrategyTest.java b/user/test/com/google/gwt/junit/BatchingStrategyTest.java
index 8a44b09..18e1a4c 100644
--- a/user/test/com/google/gwt/junit/BatchingStrategyTest.java
+++ b/user/test/com/google/gwt/junit/BatchingStrategyTest.java
@@ -79,22 +79,28 @@
       FAKE_MODULE_SYNTHETIC_NAME, TestClass2.class.getName(), "testMethod5");
 
   public void testClassBatchingStrategy() {
+    BatchingStrategy strategy = new ClassBatchingStrategy();
+    assertFalse(strategy.isSingleTestOnly());
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
     testBlocks.add(new TestInfo[] {TEST_INFO_0_0, TEST_INFO_0_1});
     testBlocks.add(new TestInfo[] {TEST_INFO_1_2, TEST_INFO_1_3, TEST_INFO_1_4});
     testBlocks.add(new TestInfo[] {TEST_INFO_2_5});
-    testBatchingStrategy(new ClassBatchingStrategy(), testBlocks);
+    testBatchingStrategy(strategy, testBlocks);
   }
 
   public void testModuleBatchingStrategy() {
+    BatchingStrategy strategy = new ModuleBatchingStrategy();
+    assertFalse(strategy.isSingleTestOnly());
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
     testBlocks.add(new TestInfo[] {
         TEST_INFO_0_0, TEST_INFO_0_1, TEST_INFO_1_2, TEST_INFO_1_3,
         TEST_INFO_1_4, TEST_INFO_2_5});
-    testBatchingStrategy(new ModuleBatchingStrategy(), testBlocks);
+    testBatchingStrategy(strategy, testBlocks);
   }
 
   public void testNoBatchingStrategy() {
+    BatchingStrategy strategy = new NoBatchingStrategy();
+    assertTrue(strategy.isSingleTestOnly());
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
     testBlocks.add(new TestInfo[] {TEST_INFO_0_0});
     testBlocks.add(new TestInfo[] {TEST_INFO_0_1});
@@ -102,7 +108,7 @@
     testBlocks.add(new TestInfo[] {TEST_INFO_1_3});
     testBlocks.add(new TestInfo[] {TEST_INFO_1_4});
     testBlocks.add(new TestInfo[] {TEST_INFO_2_5});
-    testBatchingStrategy(new NoBatchingStrategy(), testBlocks);
+    testBatchingStrategy(strategy, testBlocks);
   }
 
   /**
diff --git a/user/test/com/google/gwt/junit/CompileStrategyTest.java b/user/test/com/google/gwt/junit/CompileStrategyTest.java
new file mode 100644
index 0000000..c5c1563
--- /dev/null
+++ b/user/test/com/google/gwt/junit/CompileStrategyTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * 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.google.gwt.junit;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.junit.JUnitShell.Strategy;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests of {@link BatchingStrategy}. This test must run after a
+ * {@link GWTTestCase} to ensure that JUnitShell is already initialized.
+ */
+public class CompileStrategyTest extends TestCase {
+
+  /**
+   * A mock {@link RunStyle} used for testing.
+   */
+  private static class MockRunStyle extends RunStyle {
+
+    public MockRunStyle() {
+      super(null);
+    }
+
+    @Override
+    public boolean isLocal() {
+      return true;
+    }
+
+    @Override
+    public void launchModule(String moduleName) {
+    }
+
+    @Override
+    public void maybeCompileModule(String moduleName) {
+    }
+  }
+
+  /**
+   * A mock {@link JUnitMessageQueue} used for testing.
+   */
+  private static class MockJUnitMessageQueue extends JUnitMessageQueue {
+
+    /**
+     * The test blocks added to the queue.
+     */
+    private List<TestInfo[]> testBlocks;
+
+    /**
+     * Indicates that this is the last test block.
+     */
+    private boolean isLastBlock;
+
+    public MockJUnitMessageQueue() {
+      super(1);
+    }
+
+    @Override
+    void addTestBlocks(List<TestInfo[]> newTestBlocks, boolean isLastBlock) {
+      assertNull(testBlocks);
+      this.testBlocks = newTestBlocks;
+      this.isLastBlock = isLastBlock;
+    }
+
+    void assertIsLastBlock(boolean expected) {
+      assertEquals(expected, isLastBlock);
+    }
+
+    void assertTestBlocks(List<TestInfo[]> expected) {
+      if (expected == null || testBlocks == null) {
+        assertEquals(expected, testBlocks);
+        return;
+      }
+
+      assertEquals(expected.size(), testBlocks.size());
+      for (int i = 0; i < testBlocks.size(); i++) {
+        TestInfo[] actualBlock = testBlocks.get(i);
+        TestInfo[] expectedBlock = expected.get(i);
+        assertEquals(expectedBlock.length, actualBlock.length);
+        for (int j = 0; j < expectedBlock.length; j++) {
+          assertEquals(expectedBlock[j], actualBlock[j]);
+        }
+      }
+    }
+  }
+
+  /**
+   * A mock {@link CompileStrategy} used for testing.
+   */
+  private static class MockCompileStrategy extends CompileStrategy {
+
+    /**
+     * The number of modules to mock.
+     */
+    private int mockModuleCount;
+
+    private MockJUnitMessageQueue messageQueue = new MockJUnitMessageQueue();
+
+    /**
+     * Construct a new {@link MockCompileStrategy}.
+     * 
+     * @param mockModuleCount the number of modules
+     */
+    public MockCompileStrategy(int mockModuleCount) {
+      this.mockModuleCount = mockModuleCount;
+    }
+
+    @Override
+    public ModuleDef maybeCompileModule(String moduleName,
+        String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+        BatchingStrategy batchingStrategy, TreeLogger treeLogger) {
+      fail("This method should not be called.");
+      return null;
+    }
+
+    @Override
+    MockJUnitMessageQueue getMessageQueue() {
+      return messageQueue;
+    }
+
+    @Override
+    int getModuleCount() {
+      return mockModuleCount;
+    }
+
+    @Override
+    ModuleDef maybeCompileModuleImpl2(String moduleName,
+        String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+        TreeLogger treeLogger) {
+      return null;
+    }
+  }
+
+  /**
+   * A mock test case used for testing.
+   */
+  private static class MockGWTTestCase extends GWTTestCase {
+    @Override
+    public String getModuleName() {
+      return "com.google.gwt.junit.JUnit";
+    }
+
+    @Override
+    public String getName() {
+      return "testMethod1";
+    }
+
+    public void testMethod0() {
+    }
+
+    public void testMethod1() {
+    }
+
+    public void testMethod2() {
+    }
+  }
+
+  public void testMaybeAddTestBlockForCurrentTestWithoutBatching() {
+    BatchingStrategy batchingStrategy = new NoBatchingStrategy();
+    assertTrue(batchingStrategy.isSingleTestOnly());
+
+    // Maybe add the current test.
+    GWTTestCase testCase = new MockGWTTestCase();
+    MockCompileStrategy strategy = new MockCompileStrategy(-1);
+    strategy.maybeAddTestBlockForCurrentTest(testCase, batchingStrategy);
+
+    // Generate the expected blocks.
+    TestInfo testInfo = new TestInfo(testCase.getSyntheticModuleName(),
+        testCase.getClass().getName(), testCase.getName());
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    testBlocks.add(new TestInfo[] {testInfo});
+
+    // Verify the test is added to the queue.
+    strategy.getMessageQueue().assertIsLastBlock(false);
+    strategy.getMessageQueue().assertTestBlocks(testBlocks);
+  }
+
+  public void testMaybeAddTestBlockForCurrentTestWithBatching() {
+    BatchingStrategy batchingStrategy = new ModuleBatchingStrategy();
+    assertFalse(batchingStrategy.isSingleTestOnly());
+
+    // Maybe add the current test.
+    GWTTestCase testCase = new MockGWTTestCase();
+    MockCompileStrategy strategy = new MockCompileStrategy(-1);
+    strategy.maybeAddTestBlockForCurrentTest(testCase, batchingStrategy);
+
+    // Verify the test is not added to the queue.
+    strategy.getMessageQueue().assertTestBlocks(null);
+  }
+
+  public void testMaybeCompileModuleImplWithoutBatching() {
+    BatchingStrategy batchingStrategy = new NoBatchingStrategy();
+    assertTrue(batchingStrategy.isSingleTestOnly());
+
+    // Maybe add the current test.
+    RunStyle runStyle = new MockRunStyle();
+    GWTTestCase testCase = new MockGWTTestCase();
+    MockCompileStrategy strategy = new MockCompileStrategy(-1);
+    try {
+      strategy.maybeCompileModuleImpl(testCase.getModuleName(),
+          testCase.getSyntheticModuleName(), testCase.getStrategy(), runStyle,
+          batchingStrategy, TreeLogger.NULL);
+    } catch (UnableToCompleteException e) {
+      fail("Unexpected UnableToCompleteException: " + e.getMessage());
+    }
+
+    // Verify the test block is not added to the queue.
+    strategy.getMessageQueue().assertTestBlocks(null);
+  }
+
+  public void testMaybeCompileModuleImplWithBatchingLastModule() {
+    BatchingStrategy batchingStrategy = new ModuleBatchingStrategy();
+    assertFalse(batchingStrategy.isSingleTestOnly());
+
+    // Maybe add the current test.
+    RunStyle runStyle = new MockRunStyle();
+    GWTTestCase testCase = new MockGWTTestCase();
+    MockCompileStrategy strategy = new MockCompileStrategy(-1);
+    try {
+      strategy.maybeCompileModuleImpl(testCase.getModuleName(),
+          testCase.getSyntheticModuleName(), testCase.getStrategy(), runStyle,
+          batchingStrategy, TreeLogger.NULL);
+    } catch (UnableToCompleteException e) {
+      fail("Unexpected UnableToCompleteException: " + e.getMessage());
+    }
+
+    // Verify the test block is added to the queue.
+    strategy.getMessageQueue().assertIsLastBlock(true);
+    strategy.getMessageQueue().assertTestBlocks(
+        batchingStrategy.getTestBlocks(testCase.getSyntheticModuleName()));
+  }
+
+  public void testMaybeCompileModuleImplWithBatchingNotLastModule() {
+    BatchingStrategy batchingStrategy = new ClassBatchingStrategy();
+    assertFalse(batchingStrategy.isSingleTestOnly());
+
+    // Maybe add the current test.
+    RunStyle runStyle = new MockRunStyle();
+    GWTTestCase testCase = new MockGWTTestCase();
+    MockCompileStrategy strategy = new MockCompileStrategy(1000);
+    try {
+      strategy.maybeCompileModuleImpl(testCase.getModuleName(),
+          testCase.getSyntheticModuleName(), testCase.getStrategy(), runStyle,
+          batchingStrategy, TreeLogger.NULL);
+    } catch (UnableToCompleteException e) {
+      fail("Unexpected UnableToCompleteException: " + e.getMessage());
+    }
+
+    // Verify the test block is added to the queue.
+    strategy.getMessageQueue().assertIsLastBlock(false);
+    strategy.getMessageQueue().assertTestBlocks(
+        batchingStrategy.getTestBlocks(testCase.getSyntheticModuleName()));
+  }
+}
diff --git a/user/test/com/google/gwt/junit/JUnitSuite.java b/user/test/com/google/gwt/junit/JUnitSuite.java
index 40f468e..b7f67ce 100644
--- a/user/test/com/google/gwt/junit/JUnitSuite.java
+++ b/user/test/com/google/gwt/junit/JUnitSuite.java
@@ -25,7 +25,8 @@
  */
 public class JUnitSuite {
   public static Test suite() {
-    GWTTestSuite suite = new GWTTestSuite("Test for suite for com.google.gwt.junit");
+    GWTTestSuite suite = new GWTTestSuite(
+        "Test for suite for com.google.gwt.junit");
 
     // client
     // Suppressed due to flakiness on Linux
@@ -34,6 +35,7 @@
 
     // Must run after a GWTTestCase so JUnitShell is initialized.
     suite.addTestSuite(BatchingStrategyTest.class);
+    suite.addTestSuite(CompileStrategyTest.class);
 
     suite.addTestSuite(FakeMessagesMakerTest.class);
     suite.addTestSuite(JUnitMessageQueueTest.class);