You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by kl...@apache.org on 2015/08/21 22:29:34 UTC

[7/9] incubator-geode git commit: Test framework refactoring

Test framework refactoring


Project: http://git-wip-us.apache.org/repos/asf/incubator-geode/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-geode/commit/33d2c1c8
Tree: http://git-wip-us.apache.org/repos/asf/incubator-geode/tree/33d2c1c8
Diff: http://git-wip-us.apache.org/repos/asf/incubator-geode/diff/33d2c1c8

Branch: refs/heads/feature/GEODE-217
Commit: 33d2c1c86e0532f1b913f6728be85fdd2cf68782
Parents: fcd2142
Author: Kirk Lund <kl...@pivotal.io>
Authored: Fri Aug 21 13:17:45 2015 -0700
Committer: Kirk Lund <kl...@pivotal.io>
Committed: Fri Aug 21 13:17:45 2015 -0700

----------------------------------------------------------------------
 .../internal/lang/reflect/ReflectionUtils.java  |  41 +++++
 .../lang/reflect/ReflectionUtilsJUnitTest.java  |  78 +++++++++
 .../test/dunit/DistributedSystemSupport.java    | 102 ++++++++++++
 .../gemfire/test/dunit/NetworkSupport.java      |  23 +++
 .../test/dunit/tests/DUnitTestSuite.java        |  16 ++
 .../tests/DistributedTestNameDUnitTest.java     |  75 +++++++++
 .../gemfire/test/dunit/tests/MyTestSuite.java   |  23 +++
 .../gemfire/test/golden/GoldenTestSuite.java    |  27 ++++
 .../com/gemstone/gemfire/test/junit/Retry.java  |  17 ++
 .../gemfire/test/junit/rules/RetryRule.java     | 161 +++++++++++++++++++
 10 files changed, 563 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/main/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtils.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/main/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtils.java b/gemfire-core/src/main/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtils.java
new file mode 100755
index 0000000..0b5fee2
--- /dev/null
+++ b/gemfire-core/src/main/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtils.java
@@ -0,0 +1,41 @@
+package com.gemstone.gemfire.internal.lang.reflect;
+
+/**
+ * Utility class for helping in various reflection operations. See the 
+ * java.lang.reflect package for the classes that this class utilizes.
+ * 
+ * TODO: centralize methods from these classes to here:
+ * <li>com.gemstone.gemfire.management.internal.cli.util.spring.ReflectionUtils
+ * <li>com.gemstone.gemfire.internal.logging.LogService
+ * <li>com.gemstone.gemfire.internal.tools.gfsh.app.misc.util.ReflectionUtil
+ *  
+ * @author Kirk Lund
+ * @see com.gemstone.gemfire.internal.tools.gfsh.app.misc.util.ReflectionUtil
+ * @see com.gemstone.gemfire.internal.logging.LogService
+ * @see com.gemstone.gemfire.management.internal.cli.util.spring.ReflectionUtils
+ */
+public abstract class ReflectionUtils {
+
+  /**
+   * Gets the class name of the caller in the current stack at the given {@code depth}.
+   *
+   * @param depth a 0-based index in the current stack.
+   * @return a class name
+   */
+  public static String getClassName(final int depth) {
+    return Thread.currentThread().getStackTrace()[depth].getClassName();
+  }
+  
+  public static String getClassName() {
+    return Thread.currentThread().getStackTrace()[2].getClassName();
+  }
+  
+  public static String getMethodName(final int depth) {
+    return Thread.currentThread().getStackTrace()[depth].getMethodName();
+  }
+
+  public static String getMethodName() {
+    return Thread.currentThread().getStackTrace()[2].getMethodName();
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtilsJUnitTest.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtilsJUnitTest.java b/gemfire-core/src/test/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtilsJUnitTest.java
new file mode 100755
index 0000000..346dc75
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/internal/lang/reflect/ReflectionUtilsJUnitTest.java
@@ -0,0 +1,78 @@
+package com.gemstone.gemfire.internal.lang.reflect;
+
+import static com.gemstone.gemfire.internal.lang.reflect.ReflectionUtils.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import com.gemstone.gemfire.test.junit.categories.UnitTest;
+
+/**
+ * Unit tests for the ReflectionUtils class.
+ * 
+ * @author Kirk Lund
+ */
+@Category(UnitTest.class)
+public class ReflectionUtilsJUnitTest {
+
+  @Rule
+  public TestWatcher watchman = new TestWatcher() {
+    @Override
+    protected void starting(final Description description) {
+      testClassName = description.getClassName();
+      testMethodName = description.getMethodName();
+    }
+  };
+  
+  private String testClassName;
+  private String testMethodName;
+  
+  @Test
+  public void getClassNameZeroShouldReturnReflectionUtilsClass() {
+    assertThat(getClassName(0), is(Thread.class.getName()));
+  }
+  
+  @Test
+  public void getClassNameOneShouldReturnReflectionUtilsClass() {
+    assertThat(getClassName(1), is(ReflectionUtils.class.getName()));
+  }
+  
+  @Test
+  public void getClassNameTwoShouldReturnReflectionUtilsClass() {
+    assertThat(getClassName(2), is(getClass().getName()));
+    assertThat(getClassName(2), is(this.testClassName));
+  }
+  
+  @Test
+  public void getClassNameShouldReturnReflectionUtilsClass() {
+    assertThat(getClassName(), is(getClass().getName()));
+    assertThat(getClassName(), is(this.testClassName));
+  }
+  
+  @Test
+  public void getMethodNameZeroShouldReturnGetStackTrace() {
+    assertThat(getMethodName(0), is("getStackTrace"));
+  }
+  
+  @Test
+  public void getMethodNameOneShouldReturnGetMethodName() {
+    assertThat(getMethodName(1), is("getMethodName"));
+  }
+  
+  @Test
+  public void getMethodNameTwoShouldReturnThisMethod() {
+    assertThat(getMethodName(2), is("getMethodNameTwoShouldReturnThisMethod"));
+    assertThat(getMethodName(2), is(this.testMethodName));
+  }
+  
+  @Test
+  public void getMethodNameShouldReturnThisMethod() {
+    assertThat(getMethodName(), is("getMethodNameShouldReturnThisMethod"));
+    assertThat(getMethodName(), is(this.testMethodName));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/DistributedSystemSupport.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/DistributedSystemSupport.java b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/DistributedSystemSupport.java
new file mode 100755
index 0000000..9c52e46
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/DistributedSystemSupport.java
@@ -0,0 +1,102 @@
+package com.gemstone.gemfire.test.dunit;
+
+import static com.gemstone.gemfire.test.dunit.Wait.waitForCriterion;
+
+import java.io.File;
+
+import com.gemstone.gemfire.distributed.DistributedSystem;
+import com.gemstone.gemfire.distributed.internal.InternalDistributedSystem;
+import com.gemstone.gemfire.distributed.internal.membership.jgroup.MembershipManagerHelper;
+import com.gemstone.org.jgroups.Event;
+import com.gemstone.org.jgroups.JChannel;
+import com.gemstone.org.jgroups.stack.Protocol;
+
+public class DistributedSystemSupport {
+
+  protected DistributedSystemSupport() {
+  }
+  
+  /**
+   * Crash the cache in the given VM in such a way that it immediately stops communicating with
+   * peers.  This forces the VM's membership manager to throw a ForcedDisconnectException by
+   * forcibly terminating the JGroups protocol stack with a fake EXIT event.<p>
+   * 
+   * NOTE: if you use this method be sure that you clean up the VM before the end of your
+   * test with disconnectFromDS() or disconnectAllFromDS().
+   */
+  public static boolean crashDistributedSystem(VM vm) { // TODO: move
+    return (Boolean)vm.invoke(new SerializableCallable("crash distributed system") {
+      public Object call() throws Exception {
+        DistributedSystem msys = InternalDistributedSystem.getAnyInstance();
+        crashDistributedSystem(msys);
+        return true;
+      }
+    });
+  }
+  
+  /**
+   * Crash the cache in the given VM in such a way that it immediately stops communicating with
+   * peers.  This forces the VM's membership manager to throw a ForcedDisconnectException by
+   * forcibly terminating the JGroups protocol stack with a fake EXIT event.<p>
+   * 
+   * NOTE: if you use this method be sure that you clean up the VM before the end of your
+   * test with disconnectFromDS() or disconnectAllFromDS().
+   */
+  public static void crashDistributedSystem(final DistributedSystem msys) { // TODO: move
+    MembershipManagerHelper.inhibitForcedDisconnectLogging(true);
+    MembershipManagerHelper.playDead(msys);
+    JChannel c = MembershipManagerHelper.getJChannel(msys);
+    Protocol udp = c.getProtocolStack().findProtocol("UDP");
+    udp.stop();
+    udp.passUp(new Event(Event.EXIT, new RuntimeException("killing member's ds")));
+    try {
+      MembershipManagerHelper.getJChannel(msys).waitForClose();
+    }
+    catch (InterruptedException ie) {
+      Thread.currentThread().interrupt();
+      // attempt rest of work with interrupt bit set
+    }
+    MembershipManagerHelper.inhibitForcedDisconnectLogging(false);
+    WaitCriterion wc = new WaitCriterion() {
+      public boolean done() {
+        return !msys.isConnected();
+      }
+      public String description() {
+        return "waiting for distributed system to finish disconnecting: " + msys;
+      }
+    };
+//    try {
+      waitForCriterion(wc, 10000, 1000, true);
+//    } finally {
+//      dumpMyThreads(getLogWriter());
+//    }
+  }
+
+  /** get the host name to use for a server cache in client/server dunit
+   * testing
+   * @param host
+   * @return the host name
+   */
+  public static String getServerHostName(Host host) {
+    return System.getProperty("gemfire.server-bind-address") != null?
+        System.getProperty("gemfire.server-bind-address")
+        : host.getHostName();
+  }
+
+  /** 
+   * Delete locator state files.  Use this after getting a random port
+   * to ensure that an old locator state file isn't picked up by the
+   * new locator you're starting.
+   * 
+   * @param ports
+   */
+  public static void deleteLocatorStateFile(final int... ports) {
+    for (int i=0; i<ports.length; i++) {
+      final File stateFile = new File("locator"+ports[i]+"state.dat");
+      if (stateFile.exists()) {
+        stateFile.delete();
+      }
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/NetworkSupport.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/NetworkSupport.java b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/NetworkSupport.java
new file mode 100755
index 0000000..f702b4e
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/NetworkSupport.java
@@ -0,0 +1,23 @@
+package com.gemstone.gemfire.test.dunit;
+
+import java.net.UnknownHostException;
+
+import com.gemstone.gemfire.internal.SocketCreator;
+
+public class NetworkSupport {
+  
+  protected NetworkSupport() {
+  }
+
+  /** get the IP literal name for the current host, use this instead of  
+   * "localhost" to avoid IPv6 name resolution bugs in the JDK/machine config.
+   * @return an ip literal, this method honors java.net.preferIPvAddresses
+   */
+  public static String getIPLiteral() { // TODO: move
+    try {
+      return SocketCreator.getLocalHost().getHostAddress();
+    } catch (UnknownHostException e) {
+      throw new Error("problem determining host IP address", e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DUnitTestSuite.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DUnitTestSuite.java b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DUnitTestSuite.java
new file mode 100755
index 0000000..3c9fe83
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DUnitTestSuite.java
@@ -0,0 +1,16 @@
+package com.gemstone.gemfire.test.dunit.tests;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+  BasicDUnitTest.class,
+  DistributedTestNameDUnitTest.class,
+  VMDUnitTest.class,
+})
+/**
+ * Suite of tests for the test.dunit DUnit Test framework.
+ */
+public class DUnitTestSuite {
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DistributedTestNameDUnitTest.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DistributedTestNameDUnitTest.java b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DistributedTestNameDUnitTest.java
new file mode 100755
index 0000000..d6afc02
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/DistributedTestNameDUnitTest.java
@@ -0,0 +1,75 @@
+package com.gemstone.gemfire.test.dunit.tests;
+
+import static com.gemstone.gemfire.test.dunit.Invoke.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import com.gemstone.gemfire.internal.lang.reflect.ReflectionUtils;
+import com.gemstone.gemfire.test.dunit.DistributedTestCase;
+import com.gemstone.gemfire.test.dunit.SerializableRunnable;
+import com.gemstone.gemfire.test.junit.categories.DistributedTest;
+
+/**
+ * Verifies that test name is available and consistent in the controller JVM 
+ * and all 4 dunit JVMs.
+ * 
+ * @author Kirk Lund
+ */
+@Category(DistributedTest.class)
+public class DistributedTestNameDUnitTest extends DistributedTestCase {
+  private static final long serialVersionUID = 1L;
+
+  // TODO: remove transient and fix bug so test FAILs fast
+  
+  @Rule
+  public transient TestWatcher watchman = new TestWatcher() {
+    protected void starting(final Description description) {
+      testClassName = description.getClassName();
+      testMethodName = description.getMethodName();
+    }
+  };
+  
+  private String testClassName;
+  private String testMethodName;
+  
+  @Test
+  public void testNameShouldBeConsistentInAllJVMs() throws Exception {
+    final String methodName = this.testMethodName;
+    
+    // JUnit Rule provides getMethodName in Controller JVM
+    assertThat(getMethodName(), is(methodName));
+    
+    // Controller JVM sets testName = getMethodName in itself and all 4 other JVMs
+    assertThat(getTestName(), is(methodName));
+    
+    invokeInEveryVM(new SerializableRunnable(getMethodName()) {
+      private static final long serialVersionUID = 1L;
+      @Override
+      public void run() {
+        assertThat(getTestName(), is(methodName));
+      }
+    });
+  }
+
+  @Test
+  public void uniqueNameShouldBeConsistentInAllJVMs() throws Exception {
+    //final String uniqueName = testClassName + "_" + testMethodName;
+    final String uniqueName = getClass().getSimpleName() + "_" + testMethodName;
+    
+    assertThat(getUniqueName(), is(uniqueName));
+    
+    invokeInEveryVM(new SerializableRunnable(getMethodName()) {
+      private static final long serialVersionUID = 1L;
+      @Override
+      public void run() {
+        assertThat(getUniqueName(), is(uniqueName));
+      }
+    });
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/MyTestSuite.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/MyTestSuite.java b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/MyTestSuite.java
new file mode 100755
index 0000000..ec90a36
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/test/dunit/tests/MyTestSuite.java
@@ -0,0 +1,23 @@
+package com.gemstone.gemfire.test.dunit.tests;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import com.gemstone.gemfire.distributed.DistributedMemberDUnitTest;
+import com.gemstone.gemfire.distributed.HostedLocatorsDUnitTest;
+import com.gemstone.gemfire.internal.offheap.OutOfOffHeapMemoryDUnitTest;
+import com.gemstone.gemfire.test.catchexception.CatchExceptionExampleDUnitTest;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+  BasicDUnitTest.class,
+  DistributedTestNameDUnitTest.class,
+  VMDUnitTest.class,
+  
+  CatchExceptionExampleDUnitTest.class,
+  DistributedMemberDUnitTest.class,
+  HostedLocatorsDUnitTest.class,
+  OutOfOffHeapMemoryDUnitTest.class,
+})
+public class MyTestSuite {
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-core/src/test/java/com/gemstone/gemfire/test/golden/GoldenTestSuite.java
----------------------------------------------------------------------
diff --git a/gemfire-core/src/test/java/com/gemstone/gemfire/test/golden/GoldenTestSuite.java b/gemfire-core/src/test/java/com/gemstone/gemfire/test/golden/GoldenTestSuite.java
new file mode 100755
index 0000000..ef2686e
--- /dev/null
+++ b/gemfire-core/src/test/java/com/gemstone/gemfire/test/golden/GoldenTestSuite.java
@@ -0,0 +1,27 @@
+package com.gemstone.gemfire.test.golden;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+  FailWithErrorInOutputJUnitTest.class,
+  FailWithExtraLineInOutputJUnitTest.class,
+  FailWithLineMissingFromEndOfOutputJUnitTest.class,
+  FailWithLineMissingFromMiddleOfOutputJUnitTest.class,
+  FailWithLoggerErrorInOutputJUnitTest.class,
+  FailWithLoggerFatalInOutputJUnitTest.class,
+  FailWithLoggerWarnInOutputJUnitTest.class,
+  FailWithSevereInOutputJUnitTest.class,
+  FailWithTimeoutOfWaitForOutputToMatchJUnitTest.class,
+  FailWithWarningInOutputJUnitTest.class,
+  PassJUnitTest.class,
+  PassWithExpectedErrorJUnitTest.class,
+  PassWithExpectedSevereJUnitTest.class,
+  PassWithExpectedWarningJUnitTest.class,
+})
+/**
+ * Suite of tests for the test.golden Golden Test framework classes.
+ */
+public class GoldenTestSuite {
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/Retry.java
----------------------------------------------------------------------
diff --git a/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/Retry.java b/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/Retry.java
new file mode 100755
index 0000000..af1dc2f
--- /dev/null
+++ b/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/Retry.java
@@ -0,0 +1,17 @@
+package com.gemstone.gemfire.test.junit;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Java Annotation used to annotate a test suite class test case method in order to
+ * retry it in case of failure up to the specified maximum attempts.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Retry {
+  
+  public static int DEFAULT = 1;
+  
+  int value() default DEFAULT;
+  
+}

http://git-wip-us.apache.org/repos/asf/incubator-geode/blob/33d2c1c8/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/rules/RetryRule.java
----------------------------------------------------------------------
diff --git a/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/rules/RetryRule.java b/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/rules/RetryRule.java
new file mode 100755
index 0000000..0de55ac
--- /dev/null
+++ b/gemfire-junit/src/test/java/com/gemstone/gemfire/test/junit/rules/RetryRule.java
@@ -0,0 +1,161 @@
+package com.gemstone.gemfire.test.junit.rules;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import com.gemstone.gemfire.test.junit.Retry;
+
+/**
+ * JUnit Rule that enables retrying a failed test up to a maximum number of retries.
+ * </p> 
+ * RetryRule can be used globally for all tests in a test case by specifying a 
+ * retryCount when instantiating it:
+ * <pre>
+ * @Rule
+ * public final RetryRule retryRule = new RetryRule(3);
+ * 
+ * @Test
+ * public void shouldBeRetriedUntilPasses() {
+ *   ...
+ * }
+ * </pre>
+ * </p> 
+ * The above will result in 3 retries for every test in the test case.
+ * </p> 
+ * RetryRule can be used locally for specific tests by annotating the test 
+ * method with @Rule and specifying a retryCount for that test:
+ * <pre>
+ * @Rule
+ * public final RetryRule retryRule = new RetryRule();
+ * 
+ * @Test
+ * @Retry(3)
+ * public void shouldBeRetriedUntilPasses() {
+ *   ...
+ * }
+ * </pre>
+ * </p>
+ * This version of RetryRule will retry a test that fails because of any kind 
+ * of Throwable.
+ */
+public class RetryRule implements TestRule {
+  /**
+   * Enables printing of failures to System.err even if test passes on a retry
+   */
+  private static final boolean LOG = false;
+  
+  private final AbstractRetryRule implementation;
+
+  public RetryRule() {
+    this.implementation = new LocalRetryRule();
+  }
+
+  public RetryRule(final int retryCount) {
+    this.implementation = new GlobalRetryRule(retryCount);
+  }
+
+  @Override
+  public Statement apply(final Statement base, final Description description) {
+    return this.implementation.apply(base, description);
+  }
+
+  protected abstract class AbstractRetryRule implements TestRule {
+    protected AbstractRetryRule() {
+    }
+    protected void evaluate(final Statement base, final Description description, final int retryCount) throws Throwable {
+      if (retryCount == 0) {
+        
+      }
+      Throwable caughtThrowable = null;
+      
+      for (int count = 0; count < retryCount; count++) {
+        try {
+          base.evaluate();
+          return;
+        } catch (Throwable t) {
+          caughtThrowable = t;
+          debug(description.getDisplayName() + ": run " + (count + 1) + " failed");
+        }
+      }
+      
+      debug(description.getDisplayName() + ": giving up after " + retryCount + " failures");
+      throw caughtThrowable;
+    }
+    private void debug(final String message) {
+      if (LOG) {
+        System.err.println(message);
+      }
+    }
+  }
+  
+  /**
+   * Implementation of RetryRule for all test methods in a test case
+   */
+  protected class GlobalRetryRule extends AbstractRetryRule {
+    
+    private final int retryCount;
+
+    protected GlobalRetryRule(final int retryCount) {
+      if (retryCount < 1) {
+        throw new IllegalArgumentException("Retry count must be greater than zero");
+      }
+      this.retryCount = retryCount;
+    }
+    
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+      return new Statement() {
+        @Override
+        public void evaluate() throws Throwable {
+          GlobalRetryRule.this.evaluatePerCase(base, description);
+        }
+      };
+    }
+
+    protected void evaluatePerCase(final Statement base, final Description description) throws Throwable {
+      evaluate(base, description, this.retryCount);
+    }
+  }
+
+  /**
+   * Implementation of RetryRule for test methods annotated with Retry
+   */
+  protected class LocalRetryRule extends AbstractRetryRule {
+    
+    protected LocalRetryRule() {
+    }
+    
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+      return new Statement() {
+        @Override 
+        public void evaluate() throws Throwable {
+          LocalRetryRule.this.evaluatePerTest(base, description);
+        }
+      };
+    }
+
+    protected void evaluatePerTest(final Statement base, final Description description) throws Throwable {
+      if (isTest(description)) {
+        Retry retry = description.getAnnotation(Retry.class);
+        int retryCount = getRetryCount(retry);
+        evaluate(base, description, retryCount);
+      }
+    }
+
+    private int getRetryCount(final Retry retry) {
+      int retryCount = Retry.DEFAULT;
+
+      if (retry != null) {
+        retryCount = retry.value();
+      }
+
+      return retryCount;
+    }
+
+    private boolean isTest(final Description description) {
+      return (description.isSuite() || description.isTest());
+    }
+  }
+}