Skip to content

Commit caca564

Browse files
committed
Exceptions on other threads will be reported in tests
Anything that uses the GRIPModule will be forced to use an event bus that will report exceptions. Also, the thread exceptions will be dumped for other threads that error durring a given test.
1 parent 79bdd9d commit caca564

10 files changed

Lines changed: 269 additions & 39 deletions

File tree

core/src/main/java/edu/wpi/grip/core/GRIPCoreModule.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import edu.wpi.grip.core.sources.MultiImageFileSource;
1717
import edu.wpi.grip.core.util.ExceptionWitness;
1818

19+
import javax.annotation.Nullable;
1920
import java.io.IOException;
2021
import java.util.logging.*;
2122

@@ -25,9 +26,9 @@
2526
*/
2627
@SuppressWarnings("PMD.MoreThanOneLogger")
2728
public class GRIPCoreModule extends AbstractModule {
28-
private final Logger logger = Logger.getLogger(GRIPCoreModule.class.getName());
29+
private static final Logger logger = Logger.getLogger(GRIPCoreModule.class.getName());
2930

30-
private final EventBus eventBus = new EventBus(this::onSubscriberException);
31+
private final EventBus eventBus;
3132

3233
// This is in a static initialization block so that we don't create a ton of
3334
// log files when running tests
@@ -75,7 +76,11 @@ public synchronized void publish(final LogRecord record) {
7576
}
7677
}
7778

79+
/*
80+
* This class should not be used in tests. Use GRIPCoreTestModule for tests.
81+
*/
7882
public GRIPCoreModule() {
83+
this.eventBus = new EventBus(this::onSubscriberException);
7984
// TODO: HACK! Don't assign the global thread handler to an instance method. Creates global state.
8085
Thread.setDefaultUncaughtExceptionHandler(this::onThreadException);
8186
}
@@ -111,7 +116,7 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
111116
install(new FactoryModuleBuilder().build(ExceptionWitness.Factory.class));
112117
}
113118

114-
private void onSubscriberException(Throwable exception, SubscriberExceptionContext context) {
119+
protected void onSubscriberException(Throwable exception, @Nullable SubscriberExceptionContext exceptionContext) {
115120
if (exception instanceof InterruptedException) {
116121
logger.log(Level.FINE, "EventBus Subscriber threw InterruptedException", exception);
117122
Thread.currentThread().interrupt();
@@ -126,7 +131,7 @@ private void onSubscriberException(Throwable exception, SubscriberExceptionConte
126131
* We drop the last throwable because we clearly have a problem beyond our control.
127132
*/
128133
@SuppressWarnings({"PMD.AvoidCatchingThrowable", "PMD.EmptyCatchBlock"})
129-
private void onThreadException(Thread thread, Throwable exception) {
134+
protected void onThreadException(Thread thread, Throwable exception) {
130135
// Don't do anything outside of a try catch block when dealing with thread death
131136
try {
132137
if (exception instanceof Error && !(exception instanceof AssertionError)) {

core/src/main/java/edu/wpi/grip/core/Pipeline.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ public synchronized void moveStep(Step step, int delta) {
300300
public void onConnectionAdded(ConnectionAddedEvent event) {
301301
final Connection connection = event.getConnection();
302302
this.connections.add(connection);
303+
this.eventBus.register(connection);
303304
}
304305

305306
@Subscribe

core/src/test/java/edu/wpi/grip/core/PipelineTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import edu.wpi.grip.core.events.ConnectionRemovedEvent;
88
import edu.wpi.grip.core.events.SourceAddedEvent;
99
import edu.wpi.grip.core.events.SourceRemovedEvent;
10+
import edu.wpi.grip.util.GRIPCoreTestModule;
11+
import org.junit.After;
1012
import org.junit.Before;
1113
import org.junit.Test;
1214

@@ -19,7 +21,7 @@
1921

2022
public class PipelineTest {
2123

22-
private Injector injector;
24+
private GRIPCoreTestModule testModule;
2325
private Step.Factory stepFactory;
2426
private EventBus eventBus;
2527
private Pipeline pipeline;
@@ -38,13 +40,20 @@ public MockConnection(EventBus eventBus, Pipeline pipeline) {
3840

3941
@Before
4042
public void setUp() {
41-
injector = Guice.createInjector(new GRIPCoreModule());
43+
testModule = new GRIPCoreTestModule();
44+
testModule.setUp();
45+
final Injector injector = Guice.createInjector(testModule);
4246
stepFactory = injector.getInstance(Step.Factory.class);
4347
eventBus = injector.getInstance(EventBus.class);
4448
pipeline = injector.getInstance(Pipeline.class);
4549
addition = new AdditionOperation();
4650
}
4751

52+
@After
53+
public void tearDown() {
54+
testModule.tearDown();
55+
}
56+
4857
@Test
4958
public void testAddSource() throws URISyntaxException, IOException {
5059
Source source = new MockSource();

core/src/test/java/edu/wpi/grip/core/serialization/CompatibilityTest.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import com.google.common.eventbus.EventBus;
44
import com.google.inject.Guice;
55
import com.google.inject.Injector;
6-
import edu.wpi.grip.core.GRIPCoreModule;
76
import edu.wpi.grip.core.Pipeline;
87
import edu.wpi.grip.core.operations.Operations;
9-
import edu.wpi.grip.core.settings.ProjectSettings;
108
import edu.wpi.grip.generated.CVOperations;
119
import edu.wpi.grip.util.Files;
10+
import edu.wpi.grip.util.GRIPCoreTestModule;
11+
import org.junit.After;
1212
import org.junit.Before;
1313
import org.junit.Test;
1414

@@ -26,22 +26,19 @@ public class CompatibilityTest {
2626
private static final URI testphotoURI = Files.testphotoURI; //The location of the photo source for the test
2727
private static final URI testprojectURI = Files.testprojectURI; //The location of the save file for the test
2828

29+
private GRIPCoreTestModule testModule;
2930
private Pipeline pipeline;
30-
private Project project;
31-
private ProjectSettings settings;
32-
33-
private EventBus eventBus;
3431

3532
@Before
3633
public void setUp() throws Exception {
37-
34+
testModule = new GRIPCoreTestModule();
35+
testModule.setUp();
3836
//Set up the stuff we need for the core functionality for GRIP
39-
final Injector injector = Guice.createInjector(new GRIPCoreModule());
37+
final Injector injector = Guice.createInjector(testModule);
4038

41-
eventBus = injector.getInstance(EventBus.class);
39+
final EventBus eventBus = injector.getInstance(EventBus.class);
4240
pipeline = injector.getInstance(Pipeline.class);
43-
project = injector.getInstance(Project.class);
44-
settings = injector.getInstance(ProjectSettings.class);
41+
final Project project = injector.getInstance(Project.class);
4542

4643
//Add the operations so that GRIP will recognize them
4744
Operations.addOperations(eventBus);
@@ -73,6 +70,11 @@ public void setUp() throws Exception {
7370
project.open(file);
7471
}
7572

73+
@After
74+
public void tearDown() {
75+
testModule.tearDown();
76+
}
77+
7678
@Test
7779
public void testFoo() throws Exception {
7880
assertEquals("The expected number of steps were not found", 50, pipeline.getSteps().size());

core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import edu.wpi.grip.core.settings.ProjectSettings;
1515
import edu.wpi.grip.core.sources.ImageFileSource;
1616
import edu.wpi.grip.util.Files;
17+
import edu.wpi.grip.util.GRIPCoreTestModule;
18+
import org.junit.After;
1719
import org.junit.Before;
1820
import org.junit.Test;
1921

@@ -27,6 +29,7 @@
2729

2830
public class ProjectTest {
2931

32+
private GRIPCoreTestModule testModule;
3033
private Connection.Factory<Object> connectionFactory;
3134
private ImageFileSource.Factory imageSourceFactory;
3235
private Step.Factory stepFactory;
@@ -40,7 +43,9 @@ public class ProjectTest {
4043

4144
@Before
4245
public void setUp() throws Exception {
43-
final Injector injector = Guice.createInjector(new GRIPCoreModule());
46+
testModule = new GRIPCoreTestModule();
47+
testModule.setUp();
48+
final Injector injector = Guice.createInjector(testModule);
4449
connectionFactory = injector
4550
.getInstance(Key.get(new TypeLiteral<Connection.Factory<Object>>() {
4651
}));
@@ -70,6 +75,11 @@ public void setUp() throws Exception {
7075
eventBus.post(new OperationAddedEvent(opencvAddOperation));
7176
}
7277

78+
@After
79+
public void tearDown() {
80+
testModule.tearDown();
81+
}
82+
7383
private void serializeAndDeserialize() {
7484
final Writer writer = new StringWriter();
7585
project.save(writer);

core/src/test/java/edu/wpi/grip/core/sources/CameraSourceTest.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import com.google.common.util.concurrent.Service;
88
import com.google.inject.Guice;
99
import com.google.inject.Injector;
10-
import edu.wpi.grip.core.GRIPCoreModule;
1110
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
1211
import edu.wpi.grip.core.util.MockExceptionWitness;
12+
import edu.wpi.grip.util.GRIPCoreTestModule;
1313
import org.bytedeco.javacpp.indexer.Indexer;
1414
import org.bytedeco.javacv.Frame;
1515
import org.bytedeco.javacv.FrameGrabber;
@@ -25,10 +25,8 @@
2525
import static org.junit.Assert.*;
2626

2727
public class CameraSourceTest {
28-
private Injector injector;
28+
private GRIPCoreTestModule testModule;
2929
private CameraSource.Factory cameraSourceFactory;
30-
31-
private EventBus eventBus;
3230
private CameraSource cameraSourceWithMockGrabber;
3331
private MockFrameGrabberFactory mockFrameGrabberFactory;
3432

@@ -108,10 +106,12 @@ public FrameGrabber create(String addressProperty) throws MalformedURLException
108106

109107
@Before
110108
public void setUp() throws Exception {
111-
this.injector = Guice.createInjector(new GRIPCoreModule());
109+
this.testModule = new GRIPCoreTestModule();
110+
testModule.setUp();
111+
final Injector injector = Guice.createInjector(testModule);
112112
this.cameraSourceFactory = injector.getInstance(CameraSource.Factory.class);
113113

114-
this.eventBus = new EventBus();
114+
final EventBus eventBus = new EventBus();
115115
class UnhandledExceptionWitness {
116116
@Subscribe
117117
public void onUnexpectedThrowableEvent(UnexpectedThrowableEvent event) {
@@ -120,7 +120,7 @@ public void onUnexpectedThrowableEvent(UnexpectedThrowableEvent event) {
120120
});
121121
}
122122
}
123-
this.eventBus.register(new UnhandledExceptionWitness());
123+
eventBus.register(new UnhandledExceptionWitness());
124124
this.mockFrameGrabberFactory = new MockFrameGrabberFactory();
125125
this.cameraSourceWithMockGrabber = new CameraSource(
126126
eventBus,
@@ -132,6 +132,7 @@ public void onUnexpectedThrowableEvent(UnexpectedThrowableEvent event) {
132132
@After
133133
public void tearDown() throws Exception {
134134
mockFrameGrabberFactory.frameGrabber.release();
135+
testModule.tearDown();
135136
}
136137

137138
@Test(expected = IOException.class)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package edu.wpi.grip.util;
2+
3+
4+
import com.google.common.eventbus.SubscriberExceptionContext;
5+
import edu.wpi.grip.core.GRIPCoreModule;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.concurrent.ConcurrentLinkedQueue;
10+
import java.util.stream.Collectors;
11+
12+
import static com.google.common.base.Preconditions.checkNotNull;
13+
14+
/**
15+
* This GRIPCoreTestModule is designed to be used in tests.
16+
* Any thread that uses the DefaultUncaughtExceptionHandler will have exceptions
17+
* dumped into this class. Exceptions thrown by the event bus will also be
18+
* collected using this handler as well.
19+
* When {@link #tearDown()} is called the exceptions that have been accrued
20+
* during the test will be rethrown. This should be done in a tests
21+
* {@link org.junit.After} annotated method.
22+
* If tear down is not called and another TestThrowablesHandler is created
23+
* this class will throw an assertion error.
24+
* This is done to ensure that exceptions always get dumped for the test
25+
* that has just run.
26+
*/
27+
public class GRIPCoreTestModule extends GRIPCoreModule {
28+
private static volatile boolean instanceAlive = false;
29+
30+
private final ConcurrentLinkedQueue<ThreadThrowablePair> threadExceptions = new ConcurrentLinkedQueue<>();
31+
private final ConcurrentLinkedQueue<SubscriberThrowablePair> subscriberExceptions = new ConcurrentLinkedQueue<>();
32+
private boolean setUp = false;
33+
34+
public GRIPCoreTestModule() {
35+
super();
36+
assert !instanceAlive : "There is a GRIPCoreTestModule that did not have it's `tearDown` method called.";
37+
}
38+
39+
public void setUp() {
40+
instanceAlive = true;
41+
setUp = true;
42+
}
43+
44+
/**
45+
* Rethrows any exceptions that were thrown by other threads or the event bus.
46+
* This must be called in the {@link org.junit.After} method for any test that
47+
* uses this class. If this is not called then the next test that tries
48+
* to use an instance of this class will throw an exception.
49+
*/
50+
public void tearDown() {
51+
try {
52+
final List<Throwable> throwables = new ArrayList<>(2);
53+
if (!threadExceptions.isEmpty()) {
54+
throwables.add(MultiException.create(threadExceptions.stream().map(ThreadThrowablePair::getThrowable).collect(Collectors.toList())));
55+
}
56+
if (!subscriberExceptions.isEmpty()) {
57+
throwables.add(MultiException.create(subscriberExceptions.stream().map(SubscriberThrowablePair::getThrowable).collect(Collectors.toList())));
58+
}
59+
if (!throwables.isEmpty()) {
60+
throw MultiException.create(throwables);
61+
}
62+
} finally {
63+
instanceAlive = false;
64+
}
65+
}
66+
67+
68+
@Override
69+
protected void configure() {
70+
assert setUp : "The GRIPCoreTestModule handler was not set up. Call 'setUp' before passing the injector";
71+
super.configure();
72+
}
73+
74+
@Override
75+
protected void onThreadException(Thread thread, Throwable exception) {
76+
// Do not call super, we don't want the default functionality
77+
threadExceptions.add(new ThreadThrowablePair(thread, exception));
78+
}
79+
80+
@Override
81+
protected void onSubscriberException(Throwable exception, SubscriberExceptionContext exceptionContext) {
82+
// Do not call super, we don't want the default functionality
83+
subscriberExceptions.add(new SubscriberThrowablePair(exception, exceptionContext));
84+
}
85+
86+
private static final class ThreadThrowablePair {
87+
private final Thread thread;
88+
private final Throwable throwable;
89+
90+
private ThreadThrowablePair(Thread thread, Throwable throwable) {
91+
this.thread = checkNotNull(thread, "Thread cannot be null");
92+
this.throwable = checkNotNull(throwable, "Throwable cannot be null");
93+
}
94+
95+
private Throwable getThrowable() {
96+
return throwable;
97+
}
98+
}
99+
100+
private static final class SubscriberThrowablePair {
101+
private final Throwable exception;
102+
private final SubscriberExceptionContext exceptionContext;
103+
104+
private SubscriberThrowablePair(Throwable exception, SubscriberExceptionContext exceptionContext) {
105+
this.exception = checkNotNull(exception, "Throwable cannot be null");
106+
this.exceptionContext = checkNotNull(exceptionContext, "SubscriberExceptionContext cannot be null");
107+
}
108+
109+
private Throwable getThrowable() {
110+
return exception;
111+
}
112+
}
113+
114+
}

0 commit comments

Comments
 (0)