This is an automated email from the ASF dual-hosted git repository. emilles pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push: new 9d0add89a0 GROOVY-11225: `shouldFail` return type 9d0add89a0 is described below commit 9d0add89a04d5761db4e9c280550f0f415434195 Author: Eric Milles <eric.mil...@thomsonreuters.com> AuthorDate: Tue Nov 21 10:26:55 2023 -0600 GROOVY-11225: `shouldFail` return type --- .../src/main/java/groovy/test/GroovyAssert.java | 344 +++++++++++---------- .../groovy/groovy/test/GroovyAssertTest.groovy | 48 +-- .../groovy/groovy/test/GroovyTestCaseTest.groovy | 11 +- 3 files changed, 206 insertions(+), 197 deletions(-) diff --git a/subprojects/groovy-test/src/main/java/groovy/test/GroovyAssert.java b/subprojects/groovy-test/src/main/java/groovy/test/GroovyAssert.java index 8f51433170..3795683efe 100644 --- a/subprojects/groovy-test/src/main/java/groovy/test/GroovyAssert.java +++ b/subprojects/groovy-test/src/main/java/groovy/test/GroovyAssert.java @@ -69,26 +69,27 @@ import static org.codehaus.groovy.runtime.DefaultGroovyMethods.isAtLeast; */ public class GroovyAssert { - private static final Logger log = Logger.getLogger(GroovyAssert.class.getName()); - private static final int MAX_NESTED_EXCEPTIONS = 10; + private static final AtomicInteger counter = new AtomicInteger(0); public static final String TEST_SCRIPT_NAME_PREFIX = "TestScript"; /** - * @return a generic script name to be used by {@code GroovyShell#evaluate} calls. + * @return a generic script name to be used by {@code GroovyShell#evaluate}. */ protected static String genericScriptName() { return TEST_SCRIPT_NAME_PREFIX + (counter.getAndIncrement()) + ".groovy"; } + //-------------------------------------------------------------------------- + /** * Asserts that the script runs without any exceptions * - * @param script the script that should pass without any exception thrown + * @param script the script that should pass without any exception */ - public static void assertScript(final String script) throws Exception { + public static void assertScript(final String script) { assertScript(new GroovyShell(), script); } @@ -96,151 +97,90 @@ public class GroovyAssert { * Asserts that the script runs using the given shell without any exceptions * * @param shell the shell to use to evaluate the script - * @param script the script that should pass without any exception thrown + * @param script the script that should pass without any exception */ public static void assertScript(final GroovyShell shell, final String script) { - shell.evaluate(script, genericScriptName()); - } - - /** - * Asserts that the given code closure fails when it is evaluated - * - * @param code the code expected to fail - * @return the caught exception - */ - public static Throwable shouldFail(Closure code) { - boolean failed = false; - Throwable th = null; - try { - code.call(); - } catch (GroovyRuntimeException gre) { - failed = true; - th = ScriptBytecodeAdapter.unwrap(gre); - } catch (Throwable e) { - failed = true; - th = e; - } - assertTrue("Closure " + code + " should have failed", failed); - return th; + shell.evaluate(script, genericScriptName()); // TODO: unwrap GroovyRuntimeException } - private static void assertTrue(String message, boolean condition) { - if (!condition) { - fail(message); - } - } + //-------------------------------------------------------------------------- - public static void fail(String message) { + public static void fail(final String message) { if (message == null) { throw new AssertionError(); } throw new AssertionError(message); } + //-------------------------------------------------------------------------- + /** - * Asserts that the given code closure fails when it is evaluated - * and that a particular type of exception is thrown. + * Asserts that the given script fails when evaluated. * - * @param clazz the class of the expected exception - * @param code the closure that should fail + * @param script the script expected to fail + * @throws AssertionError if no failure * @return the caught exception */ - public static Throwable shouldFail(Class clazz, Closure code) { - Throwable th = null; - try { - code.call(); - } catch (GroovyRuntimeException gre) { - th = ScriptBytecodeAdapter.unwrap(gre); - } catch (Throwable e) { - th = e; - } - - if (th == null) { - fail("Closure " + code + " should have failed with an exception of type " + clazz.getName()); - } else if (!clazz.isInstance(th)) { - fail("Closure " + code + " should have failed with an exception of type " + clazz.getName() + ", instead got Exception " + th); - } - return th; + public static Throwable shouldFail(final String script) { + return shouldFail(new GroovyShell(), script); } /** - * Asserts that the given code closure fails when it is evaluated - * and that a particular Exception type can be attributed to the cause. - * The expected exception class is compared recursively with any nested - * exceptions using getCause() until either a match is found or no more - * nested exceptions exist. - * <p> - * If a match is found, the matching exception is returned - * otherwise the method will fail. + * Asserts that the given script fails when evaluated using the given shell. * - * @param expectedCause the class of the expected exception - * @param code the closure that should fail - * @return the cause + * @param shell the shell to use to evaluate the script + * @param script the script expected to fail + * @throws AssertionError if no failure + * @return the caught exception */ - public static Throwable shouldFailWithCause(Class expectedCause, Closure code) { - if (expectedCause == null) { - fail("The expectedCause class cannot be null"); - } - Throwable cause = null; - Throwable orig = null; - int level = 0; + public static Throwable shouldFail(final GroovyShell shell, final String script) { + Throwable th = null; try { - code.call(); + shell.evaluate(script, genericScriptName()); } catch (GroovyRuntimeException gre) { - orig = ScriptBytecodeAdapter.unwrap(gre); - cause = orig.getCause(); - } catch (Throwable e) { - orig = e; - cause = orig.getCause(); - } - - if (orig != null && cause == null) { - fail("Closure " + code + " was expected to fail due to a nested cause of type " + expectedCause.getName() + - " but instead got a direct exception of type " + orig.getClass().getName() + " with no nested cause(s). Code under test has a bug or perhaps you meant shouldFail?"); + th = ScriptBytecodeAdapter.unwrap(gre); + } catch (Throwable t) { + th = t; } - while (cause != null && !expectedCause.isInstance(cause) && cause != cause.getCause() && level < MAX_NESTED_EXCEPTIONS) { - cause = cause.getCause(); - level++; + if (th == null) { + fail("Script should have failed"); } - if (orig == null) { - fail("Closure " + code + " should have failed with an exception having a nested cause of type " + expectedCause.getName()); - } else if (cause == null || !expectedCause.isInstance(cause)) { - fail("Closure " + code + " should have failed with an exception having a nested cause of type " + expectedCause.getName() + ", instead found these Exceptions:\n" + buildExceptionList(orig)); - } - return cause; + return th; } /** - * Asserts that the given script fails when it is evaluated - * and that a particular type of exception is thrown. + * Asserts that the given script fails when evaluated and that a particular + * type of exception is thrown. * - * @param clazz the class of the expected exception - * @param script the script that should fail + * @param clazz the class of the expected exception + * @param script the script expected to fail + * @throws AssertionError if no failure * @return the caught exception */ - public static Throwable shouldFail(Class clazz, String script) { + public static <T extends Throwable> T shouldFail(final Class<T> clazz, final String script) { return shouldFail(new GroovyShell(), clazz, script); } /** - * Asserts that the given script fails when it is evaluated using the given shell + * Asserts that the given script fails when evaluated using the given shell * and that a particular type of exception is thrown. * - * @param shell the shell to use to evaluate the script - * @param clazz the class of the expected exception - * @param script the script that should fail + * @param shell the shell to use to evaluate the script + * @param clazz the class of the expected exception + * @param script the script expected to fail + * @throws AssertionError if no failure * @return the caught exception */ - public static Throwable shouldFail(GroovyShell shell, Class clazz, String script) { + public static <T extends Throwable> T shouldFail(final GroovyShell shell, final Class<T> clazz, final String script) { Throwable th = null; try { shell.evaluate(script, genericScriptName()); } catch (GroovyRuntimeException gre) { th = ScriptBytecodeAdapter.unwrap(gre); - } catch (Throwable e) { - th = e; + } catch (Throwable t) { + th = t; } if (th == null) { @@ -248,54 +188,145 @@ public class GroovyAssert { } else if (!clazz.isInstance(th)) { fail("Script should have failed with an exception of type " + clazz.getName() + ", instead got Exception " + th); } - return th; + + @SuppressWarnings("unchecked") + T t = (T) th; + return t; } + // + /** - * Asserts that the given script fails when it is evaluated + * Asserts that the given closure fails when executed. * - * @param script the script expected to fail + * @param code the block expected to fail + * @throws AssertionError if no failure * @return the caught exception */ - public static Throwable shouldFail(String script) { - return shouldFail(new GroovyShell(), script); + public static Throwable shouldFail(final Closure<?> code) { + Throwable th = null; + try { + code.call(); + } catch (GroovyRuntimeException gre) { + th = ScriptBytecodeAdapter.unwrap(gre); + } catch (Throwable t) { + th = t; + } + + if (th == null) { + fail("Closure " + code + " should have failed"); + } + + return th; } /** - * Asserts that the given script fails when it is evaluated using the given shell + * Asserts that the given closure fails when executed and that a particular + * type of exception is thrown. * - * @param shell the shell to use to evaluate the script - * @param script the script expected to fail + * @param type the class of the expected exception + * @param code the block expected to fail + * @throws AssertionError if no failure * @return the caught exception */ - public static Throwable shouldFail(GroovyShell shell, String script) { - boolean failed = false; + public static <T extends Throwable> T shouldFail(final Class<T> type, final Closure<?> code) { Throwable th = null; try { - shell.evaluate(script, genericScriptName()); + code.call(); } catch (GroovyRuntimeException gre) { - failed = true; th = ScriptBytecodeAdapter.unwrap(gre); - } catch (Throwable e) { - failed = true; - th = e; + } catch (Throwable t) { + th = t; } - assertTrue("Script should have failed", failed); - return th; + + if (th == null) { + fail("Closure " + code + " should have failed with an exception of type " + type.getName()); + } else if (!type.isInstance(th)) { + fail("Closure " + code + " should have failed with an exception of type " + type.getName() + ", but got: " + th); + } + + @SuppressWarnings("unchecked") + T t = (T) th; + return t; } /** - * NotYetImplemented Implementation + * Asserts that the given closure fails when executed and that a particular + * exception type can be attributed to the cause. The expected exception is + * compared recursively with any nested exceptions using getCause() until + * either a match is found or no more nested exceptions exist. + * <p> + * If a match is found, the matching exception is returned otherwise the + * method will fail. + * + * @param type the class of the expected nested exception + * @param code the block expected to fail + * @throws AssertionError if no failure + * @return the cause */ - private static final ThreadLocal<Boolean> notYetImplementedFlag = new ThreadLocal<>(); + public static <T extends Throwable> T shouldFailWithCause(final Class<T> type, final Closure<?> code) { + Throwable th = null; + try { + code.call(); + } catch (GroovyRuntimeException gre) { + th = ScriptBytecodeAdapter.unwrap(gre); + } catch (Throwable t) { + th = t; + } + + if (th == null) { + fail("Closure " + code + " should have failed with an exception having a nested cause of type " + type.getName()); + } else if (th.getCause() == null) { + fail("Closure " + code + " should have failed with an exception having a nested cause of type " + type.getName() + + " but instead got a direct exception of type " + th.getClass().getName() + " with no cause. Code under test has a bug or perhaps you meant shouldFail?"); + } + + int level = 0; + while (th != th.getCause() && level < MAX_NESTED_EXCEPTIONS) { + th = th.getCause(); if (th == null || type.isInstance(th)) break; + level += 1; + } + + if (th == null || !type.isInstance(th)) { + fail("Closure " + code + " should have failed with an exception having a nested cause of type " + type.getName() + ", instead found these exceptions:\n" + buildExceptionList(th)); + } + + @SuppressWarnings("unchecked") + T t = (T) th; + return t; + } + + private static String buildExceptionList(Throwable th) { + StringBuilder sb = new StringBuilder(); + int level = 0; + while (th != null) { + if (level > 1) { + for (int i = 0; i < level - 1; i++) sb.append(" "); + } + if (level > 0) sb.append("-> "); + if (level > MAX_NESTED_EXCEPTIONS) { + sb.append("..."); + break; + } + sb.append(th.getClass().getName()).append(": ").append(th.getMessage()).append("\n"); + if (th == th.getCause()) { + break; + } + th = th.getCause(); + level++; + } + return sb.toString(); + } + + //-------------------------------------------------------------------------- /** - * From JUnit. Finds from the call stack the active running JUnit test case + * From JUnit. Finds from the call stack the active running JUnit test case. * * @return the test case method * @throws RuntimeException if no method could be found. */ - private static Method findRunningJUnitTestMethod(Class<?> caller) { + private static Method findRunningJUnitTestMethod(final Class<?> caller) { final Class<?>[] args = new Class<?>[]{}; // search the initial junit test @@ -324,8 +355,8 @@ public class GroovyAssert { */ private static boolean isPublicTestMethod(final Method method) { final String name = method.getName(); - final Class[] parameters = method.getParameterTypes(); - final Class returnType = method.getReturnType(); + final Class<?>[] parameters = method.getParameterTypes(); + final Class<?> returnType = method.getReturnType(); return parameters.length == 0 && (name.startsWith("test") || hasTestAnnotation(method)) @@ -333,7 +364,7 @@ public class GroovyAssert { && Modifier.isPublic(method.getModifiers()); } - private static boolean hasTestAnnotation(Method method) { + private static boolean hasTestAnnotation(final Method method) { for (Annotation annotation : method.getAnnotations()) { if ("org.junit.Test".equals(annotation.annotationType().getName())) { return true; @@ -343,24 +374,18 @@ public class GroovyAssert { } /** - * <p> * Runs the calling JUnit test again and fails only if it unexpectedly runs.<br> * This is helpful for tests that don't currently work but should work one day, - * when the tested functionality has been implemented.<br> - * </p> - * + * when the tested functionality has been implemented. * <p> * The right way to use it for JUnit 3 is: - * * <pre> * public void testXXX() { * if (GroovyAssert.notYetImplemented(this)) return; * ... the real (now failing) unit test * } * </pre> - * - * or for JUnit 4 - * + * or for JUnit 4: * <pre> * @Test * public void XXX() { @@ -368,42 +393,44 @@ public class GroovyAssert { * ... the real (now failing) unit test * } * </pre> - * </p> * - * <p> * Idea copied from HtmlUnit (many thanks to Marc Guillemot). * Future versions maybe available in the JUnit distribution. - * </p> * * @return {@code false} when not itself already in the call stack + * @throws AssertionError if no exception + * @see NotYetImplemented */ - public static boolean notYetImplemented(Object caller) { + public static boolean notYetImplemented(final Object caller) { if (notYetImplementedFlag.get() != null) { return false; } notYetImplementedFlag.set(Boolean.TRUE); - final Method testMethod = findRunningJUnitTestMethod(caller.getClass()); + Logger log = Logger.getLogger(GroovyAssert.class.getName()); + Method testMethod = findRunningJUnitTestMethod(caller.getClass()); try { log.info("Running " + testMethod.getName() + " as not yet implemented"); testMethod.invoke(caller, (Object[]) new Class[]{}); fail(testMethod.getName() + " is marked as not yet implemented but passes unexpectedly"); - } - catch (final Exception e) { + } catch (Exception e) { log.info(testMethod.getName() + " fails which is expected as it is not yet implemented"); // method execution failed, it is really "not yet implemented" - } - finally { + } finally { notYetImplementedFlag.remove(); } return true; } + private static final ThreadLocal<Boolean> notYetImplementedFlag = new ThreadLocal<>(); + + //-------------------------------------------------------------------------- + /** * @return true if the JDK version is at least the version given by specVersion (e.g. "1.8", "9.0") * @since 2.5.7 */ - public static boolean isAtLeastJdk(String specVersion) { + public static boolean isAtLeastJdk(final String specVersion) { boolean result = false; try { result = isAtLeast(new BigDecimal(System.getProperty("java.specification.version")), specVersion); @@ -411,27 +438,4 @@ public class GroovyAssert { } return result; } - - private static String buildExceptionList(Throwable th) { - StringBuilder sb = new StringBuilder(); - int level = 0; - while (th != null) { - if (level > 1) { - for (int i = 0; i < level - 1; i++) sb.append(" "); - } - if (level > 0) sb.append("-> "); - if (level > MAX_NESTED_EXCEPTIONS) { - sb.append("..."); - break; - } - sb.append(th.getClass().getName()).append(": ").append(th.getMessage()).append("\n"); - if (th == th.getCause()) { - break; - } - th = th.getCause(); - level++; - } - return sb.toString(); - } - } diff --git a/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyAssertTest.groovy b/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyAssertTest.groovy index 0c2bfde4bf..0a5df1e128 100644 --- a/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyAssertTest.groovy +++ b/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyAssertTest.groovy @@ -18,10 +18,13 @@ */ package groovy.test +import groovy.transform.PackageScope +import groovy.transform.TypeChecked import org.junit.Test + import static groovy.test.GroovyAssert.* -class GroovyAssertTest { +final class GroovyAssertTest { @Test void assertScriptWithAssertion() { @@ -36,42 +39,41 @@ class GroovyAssertTest { void notYetImplementedStaticMethod() { if (notYetImplemented(this)) return - assert 1 == 2 + throw new AssertionError() } @Test void shouldFailAndReturnException() { def re = shouldFail { throw new RuntimeException('x') } - assert re?.message == 'x' assert re instanceof RuntimeException + assert re.message == 'x' } - @Test + @Test @TypeChecked // GROOVY-11225 void shouldFailCheckExceptionClassAndReturnException() { - def re = shouldFail(RuntimeException) { throw new RuntimeException('x') } - assert re?.message == 'x' - assert re instanceof RuntimeException + RuntimeException re = shouldFail(RuntimeException) { throw new RuntimeException('x') } + assert re.message == 'x' } - @Test + @Test @TypeChecked // GROOVY-11225 void shouldFailCheckingCustomException() { - shouldFail(GroovyAssertDummyException) { + RuntimeException re = shouldFail(GroovyAssertDummyException) { GroovyAssertDummyClass.throwException() } } - @Test + @Test @TypeChecked // GROOVY-11225 void shouldFailWithNestedException() { - def throwable = shouldFail(GroovyAssertDummyException) { - new GroovyAssertDummyClass().throwExceptionWithCause() + GroovyAssertDummyException exception = shouldFail(GroovyAssertDummyException) { + GroovyAssertDummyClass.throwExceptionWithCause() } - assert throwable instanceof GroovyAssertDummyException - assert throwable.cause instanceof NullPointerException + assert exception instanceof GroovyAssertDummyException + assert exception.cause instanceof NullPointerException - throwable = shouldFailWithCause(NullPointerException) { - new GroovyAssertDummyClass().throwExceptionWithCause() + NullPointerException cause = shouldFailWithCause(NullPointerException) { + GroovyAssertDummyClass.throwExceptionWithCause() } - assert throwable instanceof NullPointerException + assert cause instanceof NullPointerException } @Test @@ -81,22 +83,24 @@ class GroovyAssertTest { } } -@groovy.transform.PackageScope class GroovyAssertDummyClass { +@PackageScope class GroovyAssertDummyClass { + static throwException() { throw new GroovyAssertDummyException() } - def throwExceptionWithCause() { + static throwExceptionWithCause() { throw new GroovyAssertDummyException(new NullPointerException()) } } -@groovy.transform.PackageScope class GroovyAssertDummyException extends RuntimeException { +@PackageScope class GroovyAssertDummyException extends RuntimeException { + GroovyAssertDummyException(Throwable cause) { - super(cause); + super((Throwable)cause) } GroovyAssertDummyException() { - super(); + super() } } diff --git a/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyTestCaseTest.groovy b/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyTestCaseTest.groovy index 9c04ba7fa4..25524d50c4 100644 --- a/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyTestCaseTest.groovy +++ b/subprojects/groovy-test/src/test/groovy/groovy/test/GroovyTestCaseTest.groovy @@ -18,12 +18,13 @@ */ package groovy.test +import groovy.transform.PackageScope import junit.framework.AssertionFailedError /** * Testing the notYetImplemented feature of GroovyTestCase. */ -class GroovyTestCaseTest extends GroovyTestCase { +final class GroovyTestCaseTest extends GroovyTestCase { void testNotYetImplementedSubclassUse () { if (notYetImplemented()) return @@ -77,12 +78,12 @@ class GroovyTestCaseTest extends GroovyTestCase { throw new Exception() } } - assert msg.contains("was expected to fail due to a nested cause of type java.lang.Exception but instead got a direct exception of type java.lang.Exception with no nested cause(s).") + assert msg.contains("should have failed with an exception having a nested cause of type java.lang.Exception but instead got a direct exception of type java.lang.Exception with no cause.") assert msg.contains("Code under test has a bug or perhaps you meant shouldFail?") } } -class Foo { +@PackageScope class Foo { def createBar() { throw new MyException(null) } @@ -92,8 +93,8 @@ class Foo { } } -class MyException extends RuntimeException { +@PackageScope class MyException extends RuntimeException { MyException(Throwable cause) { - super((Throwable) cause); + super((Throwable) cause) } }