55import java .util .ArrayList ;
66import java .util .List ;
77
8+ import static org .junit .Assert .fail ;
9+
810/**
911 * This class helps coping with assertions in callbacks.
1012 * As AssertionErrors are thrown silently when an assertion fails in a callback,
1113 * this class wraps a CompletionHandler to check if an assert failed and expose the error through {@link AssertCompletionHandler#checkAssertions()}.
1214 */
1315abstract class AssertCompletionHandler implements CompletionHandler {
1416 private AssertionError error ;
15- private final CompletionHandler handler ;
16- private final List < AssertCompletionHandler > innerHandlers = new ArrayList <>() ;
17+ private int invocationCount = 0 ;
18+ private StackTraceElement [] creationStackTrace ;
1719
1820 /**
1921 * Global registry of all created completion handlers. It is cleared when the static `checkAllHandlers()` method is
2022 * called.
2123 */
2224 private static List <AssertCompletionHandler > allHandlers = new ArrayList <>();
2325
24- public AssertCompletionHandler () {
25- this .handler = new CompletionHandler () {
26- @ Override public void requestCompleted (JSONObject content , AlgoliaException error ) {
27- doRequestCompleted (content , error );
26+ // Schedule to check all handlers at JVM shutdown.
27+ // NOTE: This is a safety net in case the programmer forgets to check the handlers manually (hence the warning).
28+ static {
29+ Runtime .getRuntime ().addShutdownHook (new Thread (new Runnable () {
30+ @ Override
31+ public void run () {
32+ if (!allHandlers .isEmpty ()) {
33+ System .err .println ("WARNING: Some handlers have not been checked! Doing it now." );
34+ checkAllHandlers ();
35+ }
2836 }
29- };
37+ }));
38+ }
39+
40+ public AssertCompletionHandler () {
41+ // Capture the stack trace at creation.
42+ // This is used to indicate where the handler was created in case it was never called.
43+ creationStackTrace = Thread .currentThread ().getStackTrace ();
44+
45+ // Add the handler to the global registry.
3046 synchronized (AssertCompletionHandler .class ) {
3147 allHandlers .add (this );
3248 }
3349 }
3450
3551 abstract public void doRequestCompleted (JSONObject content , AlgoliaException error );
3652
37- /**
38- * Add an inner handler to be checked as well in {@link AssertCompletionHandler#checkAssertions()}.
39- */
40- public void addInnerHandler (AssertCompletionHandler handler ) {
41- innerHandlers .add (handler );
42- }
43-
4453 /**
4554 * Fail if the handler encountered at least one AssertionError.
4655 */
4756 public void checkAssertions () {
57+ // NOTE: Throwing an exception with an already populated stack trace maintains this stack trace.
58+ // We use this trick to have a meaningful stack trace displayed.
4859 if (error != null ) {
49- // Throwing the original exception maintains the stack trace... though I am not entirely sure why. =:)
50- // (An alternative would be to chain the exception.)
5160 throw error ;
52- }
53- for (AssertCompletionHandler h : innerHandlers ) {
54- h .checkAssertions ();
61+ } else if (invocationCount == 0 ) {
62+ AssertionError e = new AssertionError ("A completion handler was never called" );
63+ e .setStackTrace (creationStackTrace );
64+ throw e ;
65+ } else if (invocationCount > 1 ) {
66+ AssertionError e = new AssertionError (String .format ("A completion handler was called more than once (%d times)" , invocationCount ));
67+ e .setStackTrace (creationStackTrace );
68+ throw e ;
5569 }
5670 }
5771
@@ -60,15 +74,27 @@ public void checkAssertions() {
6074 * process if this method was never called).
6175 */
6276 public synchronized static void checkAllHandlers () {
77+ // Print all errors.
78+ boolean failed = false ;
6379 for (AssertCompletionHandler handler : allHandlers ) {
64- handler .checkAssertions ();
80+ try {
81+ handler .checkAssertions ();
82+ } catch (AssertionError e ) {
83+ failed = true ;
84+ e .printStackTrace (System .err );
85+ }
6586 }
6687 allHandlers .clear ();
88+ // Cause the test to fail if necessary.
89+ if (failed ) {
90+ fail ("Assertions where caught" );
91+ }
6792 }
6893
6994 @ Override final public void requestCompleted (JSONObject content , AlgoliaException error ) {
95+ invocationCount += 1 ;
7096 try {
71- handler . requestCompleted (content , error );
97+ doRequestCompleted (content , error );
7298 } catch (AssertionError e ) {
7399 this .error = e ;
74100 }
0 commit comments