/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2009
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package alma.acs.nc;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.omg.CORBA.portable.IDLEntity;
import alma.ACSErrTypeCommon.wrappers.AcsJCouldntPerformActionEx;
import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx;
import alma.ADMINTEST1.statusBlockEvent1;
import alma.ADMINTEST2.statusBlockEvent2;
import alma.JavaContainerError.wrappers.AcsJContainerServicesEx;
import alma.acs.component.client.ComponentClient;
import alma.acs.exceptions.AcsJException;
import alma.acs.nc.AcsEventPublisher;
import alma.acs.nc.NCSubscriber;
import alma.acs.nc.AcsEventSubscriber.Callback;
import alma.acs.nc.AcsEventSubscriber.GenericCallback;
import alma.acs.nc.NCSubscriber.NoEventReceiverListener;
import alma.acs.util.AcsLocations;
import alma.acsErrTypeLifeCycle.wrappers.AcsJEventSubscriptionEx;
import alma.acsnc.EventDescription;
/**
* Tests for NCSubscriber.
*
* @author rtobar, hsommer
*/
public class NCSubscriberTest extends ComponentClient {
private static String CHANNEL_NAME = "pink-floyd";
/**
* The shared subscriber for the tests.
* For more complete testing we use the actual type, not just AcsEventSubscriber interface,
* and therefore need a cast in {@link #newSharedSubscriber()}.
*/
private NCSubscriber<IDLEntity> m_subscriber;
/**
* We'll use this publisher to publish events of different types
* (statusBlockEvent1 and statusBlockEvent2).
* Thus we cannot parameterize it to any one of these types, but have to use the
* generic base type IDLEntity or Object.
*/
private AcsEventPublisher<IDLEntity> m_publisher;
/**
* Test event types, to be used when calling {@link #publish(int, EventType)}.
*/
private enum EventType {
statusBlock1,
statusBlock2
}
public NCSubscriberTest() throws Exception {
super(null, AcsLocations.figureOutManagerLocation(), NCSubscriberTest.class.getSimpleName());
}
/**
* TODO: Check if this rule and the getMethodName() call in setUp() can be moved up to ComponentClient,
* if that adds a runtime dependency on junit, and how bad that would be.
* Probably we should add a class ComponentClientTestCaseJUnit4 that extends ComponentClient
* and only adds this testname business.
*/
@Rule
public TestName testName = new TestName();
@Before
public void setUp() throws Exception {
String testMethodName = testName.getMethodName();
m_logger.info("----------------- " + testMethodName + " ----------------- ");
m_publisher = getContainerServices().createNotificationChannelPublisher(CHANNEL_NAME, IDLEntity.class);
newSharedSubscriber();
// This is the all-exclusive filter
assertEquals(1, m_subscriber.proxySupplier.get_all_filters().length);
}
@After
public void tearDown() throws Exception {
m_publisher.disconnect();
if (m_subscriber != null && !m_subscriber.isDisconnected()) {
m_subscriber.disconnect();
}
super.tearDown();
}
/**
* Creates an NCSubscriber and stores it in {@link #m_subscriber}.
* If we already had a subscriber, that one gets disconnected first.
*/
private void newSharedSubscriber() throws AcsJContainerServicesEx, AcsJIllegalStateEventEx, AcsJCouldntPerformActionEx {
if (m_subscriber != null && !m_subscriber.isDisconnected()) {
m_subscriber.disconnect();
}
m_subscriber = (NCSubscriber<IDLEntity>)getContainerServices().createNotificationChannelSubscriber(CHANNEL_NAME, IDLEntity.class);
}
/**
*/
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testAddSubscription() throws Exception {
// Invalid receiver (returns null)
try {
m_subscriber.addSubscription(new EventReceiver1() {
public Class<statusBlockEvent1> getEventType() {
return null;
}
});
fail("Event receiver is invalid, as it returns a null event type");
} catch(AcsJEventSubscriptionEx e) { }
// Invalid receiver (returns String.class)
try {
m_subscriber.addSubscription(new Callback() {
public void receive(Object event,
EventDescription eventDescrip) {
}
public Class getEventType() {
return String.class;
}
});
fail("Event receiver is invalid, as it returns a java.lang.String as the event type");
} catch(AcsJEventSubscriptionEx e) { }
// Several receiver subscriptions for the same type overwrite the previous one
m_subscriber.addSubscription(new EventReceiver1());
m_subscriber.addSubscription(new EventReceiver1());
m_subscriber.addSubscription(new EventReceiver1());
m_subscriber.addSubscription(new EventReceiver1());
assertEquals(1, m_subscriber.subscriptionsFilters.size());
assertEquals(1, m_subscriber.getNumberOfReceivers());
assertFalse(m_subscriber.hasGenericReceiver());
assertEquals(2, m_subscriber.proxySupplier.get_all_filters().length);
m_subscriber.addSubscription(new EventReceiver2());
m_subscriber.addSubscription(new EventReceiver2());
m_subscriber.addSubscription(new EventReceiver2());
m_subscriber.addSubscription(new EventReceiver2());
assertEquals(2, m_subscriber.subscriptionsFilters.size());
assertEquals(2, m_subscriber.getNumberOfReceivers());
assertFalse(m_subscriber.hasGenericReceiver());
assertEquals(3, m_subscriber.proxySupplier.get_all_filters().length);
m_subscriber.addGenericSubscription(new GenericEventReceiver());
m_subscriber.addGenericSubscription(new GenericEventReceiver());
m_subscriber.addGenericSubscription(new GenericEventReceiver());
m_subscriber.addGenericSubscription(new GenericEventReceiver());
assertEquals(3, m_subscriber.subscriptionsFilters.size());
assertEquals(2, m_subscriber.getNumberOfReceivers());
assertTrue(m_subscriber.hasGenericReceiver());
assertEquals(4, m_subscriber.proxySupplier.get_all_filters().length);
}
@Test
public void testRemoveSubscription() throws Exception {
// Invalid removals, then subscription //
// event 1
try {
m_subscriber.removeSubscription(statusBlockEvent1.class);
fail("Should fail because we don't have a subscription yet to statusBlockEvent1");
} catch (AcsJEventSubscriptionEx e) { }
m_subscriber.addSubscription(new EventReceiver1());
// event 2
try {
m_subscriber.removeSubscription(statusBlockEvent2.class);
fail("Should fail because we don't have a subscription yet to statusBlockEvent2");
} catch (AcsJEventSubscriptionEx e) { }
m_subscriber.addSubscription(new EventReceiver2());
// generic
try {
m_subscriber.removeGenericSubscription();
fail("Should fail because we don't have a generic subscription");
} catch(AcsJEventSubscriptionEx e) {}
m_subscriber.addGenericSubscription(new GenericEventReceiver());
// Now we can safely remove all subscriptions
m_subscriber.removeGenericSubscription();
m_subscriber.removeSubscription(statusBlockEvent1.class);
m_subscriber.removeSubscription(statusBlockEvent2.class);
assertEquals(0, m_subscriber.subscriptionsFilters.size());
assertEquals(0, m_subscriber.getNumberOfReceivers());
assertFalse(m_subscriber.hasGenericReceiver());
// After we remove all subscriptions, only the all-exclusive filter is set in the server
assertEquals(1, m_subscriber.proxySupplier.get_all_filters().length);
// Trying to remove subscriptions should fail now
try {
m_subscriber.removeSubscription(statusBlockEvent1.class);
fail("Should fail because we don't have a subscription yet to statusBlockEvent1");
} catch (AcsJEventSubscriptionEx e) { }
try {
m_subscriber.removeSubscription(statusBlockEvent2.class);
fail("Should fail because we don't have a subscription yet to statusBlockEvent2");
} catch (AcsJEventSubscriptionEx e) { }
}
@Test
public void testSubscriptionByEventTypeReceiving() throws Exception {
final int nEvents = 10;
// Simple case: 1 receiver per event type, publisher publishes same amount of events for each type
CountDownLatch c1 = new CountDownLatch(nEvents);
CountDownLatch c2 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.addSubscription(new EventReceiver2(c2));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertTrue(c2.await(10, TimeUnit.SECONDS));
// Overriding case: 1 receiver per event type, 2nd receiver is overridden
newSharedSubscriber();
c1 = new CountDownLatch(nEvents);
c2 = new CountDownLatch(nEvents);
CountDownLatch c3 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.addSubscription(new EventReceiver2(c2));
m_subscriber.addSubscription(new EventReceiver2(c3));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertTrue(c3.await(10, TimeUnit.SECONDS));
assertEquals(nEvents, c2.getCount());
// Overriding case 2: 1 receiver per event type, 2nd receiver is overridden two times, but second time is invalid
newSharedSubscriber();
c1 = new CountDownLatch(nEvents);
c2 = new CountDownLatch(nEvents);
c3 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.addSubscription(new EventReceiver2(c2));
try {
m_subscriber.addSubscription(new EventReceiver1(c3) {
public Class<statusBlockEvent1> getEventType() {
return null;
}
});
fail("Event receiver is invalid, as it returns a null event type");
} catch(AcsJEventSubscriptionEx e) { }
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertTrue(c2.await(10, TimeUnit.SECONDS));
assertFalse(c3.await(10, TimeUnit.SECONDS));
assertEquals(nEvents, c3.getCount());
m_subscriber.disconnect();
}
@Test
public void testSubscriptionGenericReceiving() throws Exception {
int nEvents = 10;
// Simple case: just the generic receiver, receiving 2 event types
CountDownLatch c1 = new CountDownLatch(2*nEvents);
m_subscriber.addGenericSubscription(new GenericEventReceiver(c1));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
c1.await(10, TimeUnit.SECONDS);
m_subscriber.disconnect();
assertEquals(0, c1.getCount());
// Overriding case: generic subscriber, then overridden by a different one
newSharedSubscriber();
c1 = new CountDownLatch(2*nEvents);
CountDownLatch c2 = new CountDownLatch(2*nEvents);
m_subscriber.addGenericSubscription(new GenericEventReceiver(c1));
m_subscriber.addGenericSubscription(new GenericEventReceiver(c2));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
c1.await(10, TimeUnit.SECONDS);
c2.await(10, TimeUnit.SECONDS);
m_subscriber.disconnect();
assertEquals(2*nEvents, c1.getCount());
assertEquals(0, c2.getCount());
// Add/remove case: add generic subscription, then remove it, then listen: nothing should arrive
newSharedSubscriber();
c1 = new CountDownLatch(2*nEvents);
m_subscriber.addGenericSubscription(new GenericEventReceiver(c1));
m_subscriber.removeGenericSubscription();
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
c1.await(10, TimeUnit.SECONDS);
m_subscriber.disconnect();
assertEquals(2*nEvents, c1.getCount());
// Mixed case: a generic receiver + receiver for event type 1
newSharedSubscriber();
c1 = new CountDownLatch(2*nEvents);
c2 = new CountDownLatch(nEvents);
m_subscriber.addGenericSubscription(new GenericEventReceiver(c1));
m_subscriber.addSubscription(new EventReceiver2(c2));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock1);
publish(nEvents, EventType.statusBlock2);
c1.await(10, TimeUnit.SECONDS);
c2.await(10, TimeUnit.SECONDS);
m_subscriber.disconnect();
assertEquals(10, c1.getCount()); // we set 2*nEvents as the initial count for c1
assertEquals(0, c2.getCount());
}
/**
* Asserts that only subscribed events arrive at the subscriber client,
* while others get filtered out already on the server.
* See also http://jira.alma.cl/browse/COMP-9076
*/
@Test
public void testServerSideEventTypeFiltering() throws Exception {
final int nEvents = 10;
// Verify that our test finds a filter leak when it exists.
// For that, we use one typed subscription but publish two event types.
CountDownLatch cLeak = new CountDownLatch(nEvents);
newSharedSubscriberWithFilterLeakCheck(null, cLeak);
// Note that calling 'm_subscriber.proxySupplier.remove_all_filters()'
// does not work here. Apparently after adding and removing filters, we do not return
// to the initial "all events pass" behavior.
m_subscriber.addFilter("*");
CountDownLatch c1 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock2); // they should leak
publish(nEvents, EventType.statusBlock1);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertTrue("Expecting a leak in the server-side event filter", cLeak.await(10, TimeUnit.SECONDS));
// Again 1 typed subscription and 2 event types published, but without artificial filter leak
List<String> leakedEvents = new ArrayList<String>();
newSharedSubscriberWithFilterLeakCheck(leakedEvents, null);
c1 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock2); // should be filtered out
publish(nEvents, EventType.statusBlock1);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertThat(leakedEvents, empty());
// add generic subscription
newSharedSubscriberWithFilterLeakCheck(leakedEvents, null);
c1 = new CountDownLatch(nEvents);
CountDownLatch c2 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.addGenericSubscription(new GenericEventReceiver(c2));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock2);
publish(nEvents, EventType.statusBlock1);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertTrue(c2.await(10, TimeUnit.SECONDS));
assertThat(leakedEvents, empty());
// remove generic subscription
newSharedSubscriberWithFilterLeakCheck(leakedEvents, null);
c1 = new CountDownLatch(nEvents);
m_subscriber.addSubscription(new EventReceiver1(c1));
m_subscriber.startReceivingEvents();
publish(nEvents, EventType.statusBlock2); // should be filtered out
publish(nEvents, EventType.statusBlock1);
assertTrue(c1.await(10, TimeUnit.SECONDS));
assertThat(leakedEvents, empty());
}
/**
* Sets up a hacked subscriber that reports stray events (normally they only get logged).
* @see #testServerSideEventTypeFiltering()
*/
private void newSharedSubscriberWithFilterLeakCheck(final List<String> leakedEvents, final CountDownLatch cLeak) throws Exception {
newSharedSubscriber();
NoEventReceiverListener filterLeakListener = new NoEventReceiverListener() {
@Override
public void noEventReceiver(String eventName) {
m_logger.info("Leaked event: " + eventName);
if (leakedEvents != null) {
leakedEvents.add(eventName);
}
if (cLeak != null) {
cLeak.countDown();
}
}
};
m_subscriber.setNoEventReceiverListener(filterLeakListener);
}
@Test
public void testLifecycle() throws Exception {
// We're totally disconnected, try to do illegal stuff
try {
m_subscriber.suspend();
fail("suspend() should fail, as we're not yet connected");
} catch(AcsJIllegalStateEventEx e) { }
try {
m_subscriber.resume();
fail("resume() should fail, as we're not yet connected");
} catch(AcsJIllegalStateEventEx e) { }
// disconnect() should work, since it's supposed to be called even if we never connect.
// After that we need a new subscriber though.
m_subscriber.disconnect();
newSharedSubscriber();
m_subscriber.startReceivingEvents();
assertFalse(m_subscriber.isDisconnected());
// Now we're connected, try to do illegal stuff
try {
m_subscriber.startReceivingEvents();
fail("startReceivingEvents() should fail, as we're already connected");
} catch(AcsJIllegalStateEventEx e) { }
try {
m_subscriber.resume();
fail("resume() should fail, as we're not suspended yet");
} catch(AcsJIllegalStateEventEx e) { }
m_subscriber.suspend();
// Now we're suspended, try to do illegal stuff
try {
m_subscriber.suspend();
fail("suspend() should fail, as we're already suspended");
} catch (AcsJIllegalStateEventEx e) { }
try {
m_subscriber.startReceivingEvents();
fail("startReceivingEvents() should fail, as we're already connected");
} catch (AcsJIllegalStateEventEx e) { }
m_subscriber.resume();
m_subscriber.disconnect();
assertTrue(m_subscriber.isDisconnected());
// Now we're disconnected, try to do illegal stuff
try {
m_subscriber.disconnect();
fail("disconnect() should fail, as we're already disconnected");
} catch (AcsJIllegalStateEventEx e) { }
try {
m_subscriber.suspend();
fail("suspend() should fail, as we're already disconnected");
} catch (AcsJIllegalStateEventEx e) { }
try {
m_subscriber.resume();
fail("resume() should fail, as we're already disconnected");
} catch (AcsJIllegalStateEventEx e) { }
}
/*===================================*/
/* Support methods */
/*===================================*/
private void publish(int nEvents, EventType type) {
IDLEntity event = null;
if( type.equals(EventType.statusBlock1) ) {
event = new statusBlockEvent1();
((statusBlockEvent1)event).counter1 = 0;
((statusBlockEvent1)event).counter2 = 0;
((statusBlockEvent1)event).flipFlop = true;
((statusBlockEvent1)event).onOff = alma.ADMINTEST1.OnOffStates.ON;
}
else if( type.equals(EventType.statusBlock2) ) {
event = new statusBlockEvent2();
((statusBlockEvent2)event).counter1 = 0;
((statusBlockEvent2)event).counter2 = 0;
((statusBlockEvent2)event).flipFlop = true;
((statusBlockEvent2)event).onOff = alma.ADMINTEST2.OnOffStates.ON;
}
try {
for(int i=0; i!=nEvents; i++)
m_publisher.publishEvent(event);
} catch (AcsJException e) {
e.printStackTrace();
}
}
/*===================================*/
/* Support classes */
/*===================================*/
private abstract class EventReceiverWithCounter {
private CountDownLatch m_countDownLatch;
public EventReceiverWithCounter(CountDownLatch c) {
m_countDownLatch = c;
}
public void receive(EventDescription event) {
if( m_countDownLatch != null )
m_countDownLatch.countDown();
}
}
/**
* Receives <code>statusBlockEvent1</code> events.
*/
private class EventReceiver1 extends EventReceiverWithCounter implements Callback<statusBlockEvent1> {
public EventReceiver1() {
super(null);
}
public EventReceiver1(CountDownLatch c) {
super(c);
}
public void receive(statusBlockEvent1 event, EventDescription eventDescrip) {
super.receive(eventDescrip);
}
public Class<statusBlockEvent1> getEventType() {
return statusBlockEvent1.class;
}
}
/**
* Receives <code>statusBlockEvent2</code> events.
*/
private class EventReceiver2 extends EventReceiverWithCounter implements Callback<statusBlockEvent2> {
public EventReceiver2() {
super(null);
}
public EventReceiver2(CountDownLatch c) {
super(c);
}
public void receive(statusBlockEvent2 event, EventDescription eventDescrip) {
super.receive(eventDescrip);
}
public Class<statusBlockEvent2> getEventType() {
return statusBlockEvent2.class;
}
}
private class GenericEventReceiver extends EventReceiverWithCounter implements GenericCallback {
public GenericEventReceiver() {
super(null);
}
public GenericEventReceiver(CountDownLatch c) {
super(c);
}
public void receiveGeneric(Object event, EventDescription eventDescrip) {
super.receive(eventDescrip);
}
}
}