/*
* Copyright 2012 Jason Miller
*
* 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 jj.event;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Singleton;
import jj.event.help.BrokenListener;
import jj.event.help.ChildSub;
import jj.event.help.ConcreteListener;
import jj.event.help.ConcurrentSub;
import jj.event.help.Event;
import jj.event.help.EventSub;
import jj.event.help.IEvent;
import jj.event.help.NoListeners;
import jj.event.help.Sub;
import jj.event.help.UnrelatedIEvent;
import jj.execution.MockTaskRunner;
import jj.execution.TaskRunner;
import jj.util.RandomHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import com.google.inject.AbstractModule;
import com.google.inject.ConfigurationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.spi.Message;
/**
* doesn't really make sense to test the event stuff in isolation,
* since that'll just get mixed up in wild details. test that it
* dispatches as desired and be happy
*
* @author jason
*
*/
@RunWith(MockitoJUnitRunner.class)
public class EventSystemTest {
@Singleton
public static class PublisherChild extends PublisherImpl {
Map<Class<?>, ConcurrentLinkedQueue<Invoker>> listenerMap;
@Override
void listenerMap(Map<Class<?>, ConcurrentLinkedQueue<Invoker>> listenerMap) {
this.listenerMap = listenerMap;
super.listenerMap(listenerMap);
}
}
private Injector injector;
private PublisherChild pub;
private Thread publisherLoop;
@Before
public void before() throws Exception {
injector = Guice.createInjector(new EventModule(), new AbstractModule() {
@Override
protected void configure() {
bind(TaskRunner.class).to(MockTaskRunner.class);
}
});
MockTaskRunner taskRunner = injector.getInstance(MockTaskRunner.class);
pub = injector.getInstance(PublisherChild.class);
publisherLoop = taskRunner.runFirstTaskInDaemon();
}
@After
public void after() {
publisherLoop.interrupt();
}
@Test
public void testListenersAreRequired() throws Exception {
try {
injector.getInstance(NoListeners.class);
fail("should not have succeeded!");
} catch (ConfigurationException ce) {
Collection<Message> c = ce.getErrorMessages();
assertThat(c.size(), is(1));
Message m = c.iterator().next();
assertThat(m.getMessage(), is(NoListeners.class.getName() + " is annotated as a @Subscriber but has no @Listener methods"));
}
}
@Test
public void testBrokenListener() {
injector.getInstance(BrokenListener.class);
boolean worked = false;
try {
pub.publish(new Event());
worked = true;
} catch (AssertionError ae) {
assertThat(ae.getMessage(), is("broken event listener! jj.event.help.BrokenListener.on(jj.event.help.Event)"));
}
assertFalse(worked);
}
@Test
public void testCorrectOperation() throws Exception {
PublisherChild pub = impl();
System.gc();
// it needs some small amount of time
Thread.sleep(100);
// verify the listeners are all unregistered so
// we aren't leaking memory, but we should still have
// sets for the event types
assertThat(pub.listenerMap.size(), is(4));
assertTrue("should have no IEvent listeners", pub.listenerMap.get(IEvent.class).isEmpty());
assertTrue("should have no Event listeners", pub.listenerMap.get(Event.class).isEmpty());
assertTrue("should have no EventSub listeners", pub.listenerMap.get(EventSub.class).isEmpty());
assertTrue("should have no UnrelatedIEvent listeners", pub.listenerMap.get(UnrelatedIEvent.class).isEmpty());
// and publishing should not cause any exceptions at this point
pub.publish(new EventSub());
}
private PublisherChild impl() {
// publishing with nothing listening is fine
pub.publish(new EventSub());
Sub sub = injector.getInstance(Sub.class);
ConcreteListener cl = injector.getInstance(ConcreteListener.class);
// when
pub.publish(new Event());
pub.publish(new Event());
// then
assertThat(sub.heard, is(2));
// given
ChildSub childSub = injector.createChildInjector(new AbstractModule() {
@Override
protected void configure() {
bind(ChildSub.class);
}
}).getInstance(ChildSub.class);
// when
pub.publish(new Event());
// then
assertThat(childSub.heard, is(1));
assertThat(childSub.heard2, is(0));
assertThat(sub.heard, is(3));
// given
Sub sub2 = injector.getInstance(Sub.class);
// when
pub.publish(new EventSub());
pub.publish(new UnrelatedIEvent());
// then
assertThat(childSub.heard, is(2));
assertThat(childSub.heard2, is(1));
assertThat(sub.heard, is(5));
assertThat(sub2.heard, is(2));
assertThat(cl.unrelatedIEventCount, is(1));
assertThat(cl.iEventCount, is(5));
// we should have four listeners registered at this point,
// and after they go out of scope we should have none
assertThat(pub.listenerMap.size(), is(4));
assertThat(pub.listenerMap.get(IEvent.class).size(), is(3));
assertThat(pub.listenerMap.get(Event.class).size(), is(1));
assertThat(pub.listenerMap.get(EventSub.class).size(), is(1));
assertThat(pub.listenerMap.get(UnrelatedIEvent.class).size(), is(1));
// and one little validation of the target method
assertThat(pub.listenerMap.get(IEvent.class).peek().target(), is("jj.event.help.Sub.on(jj.event.help.IEvent)"));
return pub;
}
@Test
public void concurrencyTest() throws Exception {
// thread loops 500-1000 times, on each iteration either
// - publishing an event
// - spawning a subscriber instance, of varying lifetime
// occasionally the test will try to GC
// this needs more assertions, but the basics make sense right now
// we validate that a subscriber taken before any publishing receives
// each event correctly
final int threads = Runtime.getRuntime().availableProcessors();
final ExecutorService executor = Executors.newFixedThreadPool(threads);
final LinkedBlockingQueue<Throwable> throwables = new LinkedBlockingQueue<>(threads);
final CountDownLatch latch = new CountDownLatch(threads);
final ConcurrentSub sub = injector.getInstance(ConcurrentSub.class);
final AtomicInteger countIEvent = new AtomicInteger();
final AtomicInteger countEvent = new AtomicInteger();
final AtomicInteger countEventSub = new AtomicInteger();
try {
for (int t = 0; t < threads; ++t) {
executor.submit(() -> {
try {
int total = RandomHelper.nextInt(500, 1001);
for (int i = 0; i < total; ++i) {
if (RandomHelper.nextInt(300) == 84) {
// 84 was selected at random
// (that's the joke)
System.gc();
}
switch(RandomHelper.nextInt(10)) {
case 0:
case 1:
injector.getInstance(Sub.class);
break;
case 2:
injector.getInstance(ConcurrentSub.class);
break;
case 3:
case 4:
injector.getInstance(ChildSub.class);
case 5:
case 6:
pub.publish(new EventSub());
countIEvent.getAndIncrement();
countEvent.getAndIncrement();
countEventSub.getAndIncrement();
break;
case 7:
case 8:
pub.publish(new Event());
countIEvent.getAndIncrement();
countEvent.getAndIncrement();
break;
case 9:
pub.publish(new UnrelatedIEvent());
countIEvent.getAndIncrement();
break;
default:
throw new AssertionError("you broke something");
}
}
} catch (Throwable e) {
throwables.add(e);
} finally {
latch.countDown();
}
});
}
// if there is only one cpu available, give it extra time cause it will take longer
int seconds = Math.max(threads, 2) * 2;
assertTrue("timed out in " + seconds + " seconds", latch.await(seconds, SECONDS));
if (!throwables.isEmpty()) {
AssertionError error = new AssertionError(throwables.size() + " test failures");
Throwable t;
while ((t = throwables.poll()) != null) {
error.addSuppressed(t);
}
throw error;
}
assertThat(sub.countIEvent.get(), is(countIEvent.get()));
assertThat(sub.countEvent.get(), is(countEvent.get()));
assertThat(sub.countEventSub.get(), is(countEventSub.get()));
} finally {
executor.shutdownNow();
}
}
}