package eu.hgross.blaubot.test; import net.jodah.concurrentunit.Waiter; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import eu.hgross.blaubot.admin.AbstractAdminMessage; import eu.hgross.blaubot.admin.StringAdminMessage; import eu.hgross.blaubot.core.BlaubotConstants; import eu.hgross.blaubot.core.BlaubotDevice; import eu.hgross.blaubot.core.IActionListener; import eu.hgross.blaubot.messaging.BlaubotChannelManager; import eu.hgross.blaubot.messaging.BlaubotMessage; import eu.hgross.blaubot.messaging.BlaubotMessageManager; import eu.hgross.blaubot.messaging.IBlaubotAdminMessageListener; import eu.hgross.blaubot.messaging.IBlaubotChannel; import eu.hgross.blaubot.messaging.IBlaubotMessageListener; import eu.hgross.blaubot.mock.BlaubotConnectionQueueMock; import eu.hgross.blaubot.test.mockups.ChannelManagerDeviceMockup; /** * A test for channel managers that can be used with any blaubot implementation by providing a * list of BlaubotChannelManager instances for each blaubot instance to the static methods. */ public class ChannelManagerTest { private static final int NUMBER_OF_CLIENTS = 5; /** * Subscriptions are async and therefore we need to give them some time to arrive before we post messages. * This is the time we wait for subscriptions to be done */ private static final long SUBSCRIPTION_SLEEP_TIME = 1000; private static final int ORDER_TEST_NUMBER_OF_MESSAGES = 10; private ChannelManagerDeviceMockup master; private List<ChannelManagerDeviceMockup> clients; @Before public void setUp() { master = new ChannelManagerDeviceMockup("master"); clients = new ArrayList<>(); for(int i=0;i<NUMBER_OF_CLIENTS;i++) { clients.add(new ChannelManagerDeviceMockup("client" + (i+1))); } } @After public void cleanUp() throws InterruptedException { master.channelManager.deactivate(); for(ChannelManagerDeviceMockup device : clients) { device.channelManager.deactivate(); } } /** * Creates a list of connected ChannelManager instances, where index 0 is the master device. * * @return list of channel manages representing devices */ protected List<BlaubotChannelManager> connectNetwork() { master.channelManager.setMaster(true); ArrayList<BlaubotChannelManager> devices = new ArrayList<>(); devices.add(master.channelManager); for(ChannelManagerDeviceMockup client : clients) { client.connectToOtherDevice(master); devices.add(client.channelManager); } return devices; } @Test(timeout = 10000) public void testAdminMessageBroadcast() throws InterruptedException { // Wire the mockups together final List<BlaubotChannelManager> deviceMockups = connectNetwork(); testAdminBMessageBroadcast(deviceMockups); } @Test(timeout = 30000) /** * Tests if the ChannelManager's start/stop methods are idempotent */ public void testStartStopIdempotence() throws InterruptedException { BlaubotConnectionQueueMock mockConnnection = new BlaubotConnectionQueueMock(new BlaubotDevice("Device1")); BlaubotConnectionQueueMock mockConnectionRemoteEndpoint = mockConnnection.getOtherEndpointConnection(new BlaubotDevice("Dev2")); final BlaubotMessageManager mm1 = new BlaubotMessageManager(mockConnnection); final BlaubotMessageManager mm2 = new BlaubotMessageManager(mockConnectionRemoteEndpoint); // send queues final String mm1Msg = "mm1Msg", mm2Msg = "mm2Msg"; final ArrayList<String> mm1ToMm2 = new ArrayList(); final ArrayList<String> mm2ToMm1 = new ArrayList(); // receive queues final BlockingQueue mm1Received = new LinkedBlockingQueue(); final BlockingQueue mm2Received = new LinkedBlockingQueue(); // fill send queues for(int i=0; i<500;i++) { mm1ToMm2.add(mm1Msg + i); mm2ToMm1.add(mm2Msg + i); } // add listeners to receivers final CountDownLatch receivedAllMessagesLatch = new CountDownLatch(mm1ToMm2.size() + mm2ToMm1.size()); mm1.getMessageReceiver().addMessageListener(new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { try { mm1Received.put(new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET)); } catch (InterruptedException e) { e.printStackTrace(); } } }); mm2.getMessageReceiver().addMessageListener(new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { try { mm2Received.put(new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET)); } catch (InterruptedException e) { e.printStackTrace(); } } }); // async start sending the messages from each side final CountDownLatch sendingFinishedLatch = new CountDownLatch(2); new Thread(new Runnable() { @Override public void run() { for (String current : mm1ToMm2) { BlaubotMessage msg = new BlaubotMessage(); msg.setPayload(current.getBytes(BlaubotConstants.STRING_CHARSET)); mm1.getMessageSender().sendMessage(msg); } sendingFinishedLatch.countDown(); } }).start(); new Thread(new Runnable() { @Override public void run() { for (String current : mm2ToMm1) { BlaubotMessage msg = new BlaubotMessage(); msg.setPayload(current.getBytes(BlaubotConstants.STRING_CHARSET)); mm2.getMessageSender().sendMessage(msg); } sendingFinishedLatch.countDown(); } }).start(); // start/stop calls int iterations = 20; // setup callbacks for the deactivations final AtomicInteger finishedCountMm1 = new AtomicInteger(0); final CountDownLatch deactivationListenerLatch = new CountDownLatch(iterations * 2 * 11); final IActionListener actionListener = new IActionListener() { @Override public void onFinished() { finishedCountMm1.incrementAndGet(); deactivationListenerLatch.countDown(); } }; final CountDownLatch threadCountDownLatch = new CountDownLatch(iterations * 2); for(int i=0; i < 20; i++) { for (final BlaubotMessageManager mm : Arrays.asList(mm1, mm2)) { new Thread(new Runnable() { @Override public void run() { mm.activate(); mm.deactivate(actionListener); mm.deactivate(actionListener); mm.deactivate(actionListener); mm.activate(); mm.activate(); mm.activate(); mm.deactivate(actionListener); mm.activate(); mm.activate(); mm.activate(); mm.activate(); mm.activate(); mm.activate(); mm.deactivate(actionListener); mm.deactivate(actionListener); mm.activate(); mm.activate(); mm.activate(); mm.deactivate(actionListener); mm.deactivate(actionListener); mm.deactivate(actionListener); mm.deactivate(actionListener); mm.activate(); mm.activate(); mm.deactivate(actionListener); threadCountDownLatch.countDown(); } }).start(); } } // await finish of sending and activate/deactivate calls sendingFinishedLatch.await(); threadCountDownLatch.await(); boolean deactivationTimedOut = !deactivationListenerLatch.await(10000, TimeUnit.MILLISECONDS); Assert.assertFalse("Not all deactivation listeners were called", deactivationTimedOut); // We should have counted 2*11*iterations deactivation callbacks (11 times called deactivate) Assert.assertEquals("The deactivation listener were not called", 2 * 11 * iterations, finishedCountMm1.get()); } /** * Tests the admin broadcast on the given channelManagers. * * @param deviceMockups the channel managers to test, note that they have to be already connected as one network * and that the first element in the list has to be the master/king * @throws InterruptedException */ public static void testAdminBMessageBroadcast(List<BlaubotChannelManager> deviceMockups) throws InterruptedException { // We create some messages to be send by each of the mock instances as broadcast final ArrayList<BlaubotMessage> messagesToSend = new ArrayList<>(); final String prefix = "_unit_test_count"; for(int i=0; i<10; i++) { messagesToSend.add(new StringAdminMessage(prefix + i).toBlaubotMessage()); } final int messageCount = messagesToSend.size() * deviceMockups.size(); // count of messages that should be received by each participant // register listeners ConcurrentHashMap<BlaubotChannelManager, CountDownLatch> latchesMapping = new ConcurrentHashMap<>(); ConcurrentHashMap<BlaubotChannelManager, List<AbstractAdminMessage>> receivedMessagesMapping = new ConcurrentHashMap<>(); for(BlaubotChannelManager deviceChannelManager : deviceMockups) { final ArrayList<AbstractAdminMessage> receivedMessages = new ArrayList<>(); final CountDownLatch latch = new CountDownLatch(messageCount); latchesMapping.put(deviceChannelManager, latch); receivedMessagesMapping.put(deviceChannelManager, receivedMessages); deviceChannelManager.addAdminMessageListener(new IBlaubotAdminMessageListener() { @Override public void onAdminMessage(AbstractAdminMessage adminMessage) { if(!(adminMessage instanceof StringAdminMessage)) { return; } if (!((StringAdminMessage) adminMessage).getString().startsWith(prefix)) { return; } receivedMessages.add(adminMessage); latch.countDown(); } }); } // for each device, send all messages for(BlaubotChannelManager deviceChannelManager: deviceMockups) { for(BlaubotMessage blaubotMessage : messagesToSend) { deviceChannelManager.broadcastAdminMessage(blaubotMessage); } } // assert results for(BlaubotChannelManager deviceChannelManager : deviceMockups) { CountDownLatch latch = latchesMapping.get(deviceChannelManager); List<AbstractAdminMessage> receivedMessages = receivedMessagesMapping.get(deviceChannelManager); latch.await(); Assert.assertEquals(messageCount, receivedMessages.size()); } } @Test(timeout = 10000) /** * */ public void testSubscriptions() throws InterruptedException { final List<BlaubotChannelManager> deviceMockups = connectNetwork(); testSubscriptions(deviceMockups); } /** * Tests creation, subscribe and unsubscribe * * @param deviceMockups the channel managers to test, note that they have to be already connected as one network * and that the first element in the list has to be the master/king * @throws InterruptedException */ public static void testSubscriptions(List<BlaubotChannelManager> deviceMockups) throws InterruptedException { final short commonChannelNumber = (short) (deviceMockups.size() + 1); // for each client subscribe to a own channel // master = 0, client1 = 1, client2 = 2, ... final Map<Short, IBlaubotChannel> deviceChannelMap = new ConcurrentHashMap<>(); final List<CountDownLatch> deviceChannelLatches = Collections.synchronizedList(new ArrayList<CountDownLatch>()); final List<CountDownLatch> commonChannelLatches = Collections.synchronizedList(new ArrayList<CountDownLatch>()); final List<IBlaubotChannel> allChannels = Collections.synchronizedList(new ArrayList<IBlaubotChannel>()); short i = 0; for(BlaubotChannelManager deviceChannelManager : deviceMockups) { // for each device subscribe to the devices channel and a common one final IBlaubotChannel deviceChannel = deviceChannelManager.createOrGetChannel(i); final IBlaubotChannel commonChannel = deviceChannelManager.createOrGetChannel(commonChannelNumber); allChannels.add(deviceChannel); allChannels.add(commonChannel); deviceChannel.subscribe(); commonChannel.subscribe(); // memorize the channel deviceChannelMap.put(i, deviceChannel); // attach listeners to the channels final CountDownLatch deviceChannelLatch = new CountDownLatch(deviceMockups.size()); final short clientNumber = i; deviceChannel.addMessageListener(new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { /* * we will send strings in the format messageForClient1, messageForClient2, ... * to each deviceChannel from each device, so we await sizeof(client) messages, * for which we create a latch and count that down. */ String msg = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); Assert.assertEquals("A message was dispatched to the wrong or not subscribed channel (device channel " + clientNumber + ")", "messageForClient" + clientNumber, msg); deviceChannelLatch.countDown(); } }); // add latch for this client deviceChannelLatches.add(deviceChannelLatch); final CountDownLatch commonChannelLatch = new CountDownLatch(deviceMockups.size()); commonChannel.addMessageListener(new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { /* * we will send strings in the format commonMessage to this channel from each device, * so we await sizeof(client) messages, for which we create a latch and count that down. */ String msg = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); Assert.assertEquals("A message was dispatched to the wrong or not subscribed channel (common channel)", "commonChannelMessage", msg); commonChannelLatch.countDown(); } }); // add latch commonChannelLatches.add(commonChannelLatch); i++; } // wait some time to let the subscription messages arrive Thread.sleep(SUBSCRIPTION_SLEEP_TIME); // now send the messages from each device to all the available device channels as well as once to the common channel for(BlaubotChannelManager fromDevice : deviceMockups) { // send to common channel final IBlaubotChannel commonChannel = fromDevice.createOrGetChannel(commonChannelNumber); final String commonMsg = "commonChannelMessage"; commonChannel.publish(commonMsg.getBytes(BlaubotConstants.STRING_CHARSET)); // send to each device short receivingDeviceNumber = 0; for(BlaubotChannelManager toDevice : deviceMockups) { final IBlaubotChannel deviceChannel = fromDevice.createOrGetChannel(receivingDeviceNumber); final String deviceMsg = "messageForClient" + receivingDeviceNumber; deviceChannel.publish(deviceMsg.getBytes(BlaubotConstants.STRING_CHARSET)); receivingDeviceNumber++; } } // await latches List<CountDownLatch> allLatches = new ArrayList<>(); allLatches.addAll(commonChannelLatches); allLatches.addAll(deviceChannelLatches); for(CountDownLatch latch : allLatches) { latch.await(); } // if here, we sucessfully received everything as expected after we subscribed. // now unsubscribe all channels and check if nothing comes through by sending unexpected messages to device and common channel // unsubscribe for(IBlaubotChannel channel : allChannels) { channel.unsubscribe(); } // wait some time to let the unsubscribe messages arrive Thread.sleep(SUBSCRIPTION_SLEEP_TIME); final String wrongMsg = "wrongMessageThatShouldNeverReachAListener"; for(BlaubotChannelManager fromDevice : deviceMockups) { // send to common channel final IBlaubotChannel commonChannel = fromDevice.createOrGetChannel(commonChannelNumber); commonChannel.publish(wrongMsg.getBytes(BlaubotConstants.STRING_CHARSET)); // send to each device short receivingDeviceNumber = 0; for(BlaubotChannelManager toDevice : deviceMockups) { final IBlaubotChannel deviceChannel = fromDevice.createOrGetChannel(receivingDeviceNumber); deviceChannel.publish(wrongMsg.getBytes(BlaubotConstants.STRING_CHARSET)); receivingDeviceNumber++; } } // if something goes wrong, the listeners asserts will fail ... // finished } @Test(timeout = 10000) public void testMessageOrder() throws InterruptedException { final List<BlaubotChannelManager> deviceMockups = connectNetwork(); testMessageOrder(deviceMockups); } @Test(timeout = 10000) public void testExcludeSender() throws InterruptedException, TimeoutException { final List<BlaubotChannelManager> deviceMockups = connectNetwork(); testExcludeSender(deviceMockups); } /** * Tests the message send/and receive order * * @param channelManagers the channel managers to test, note that they have to be already connected as one network * and that the first element in the list has to be the master/king * @throws InterruptedException */ public static void testMessageOrder(List<BlaubotChannelManager> channelManagers) throws InterruptedException { // now we send messages with increasing numbers to check if the order is right // every device sends some messages with the format <fromDeviceNumber>;<toDeviceNumber>;<sequenceNumber> // to each other device channel // the listeners on this device channels asserts that sequenceNumber for fromDeviceNumber is greater than the received before // for each client subscribe to a own channel final Map<Short, IBlaubotChannel> deviceChannelMap = new ConcurrentHashMap<>(); final List<CountDownLatch> deviceChannelLatches = Collections.synchronizedList(new ArrayList<CountDownLatch>()); short deviceId = 0; for(BlaubotChannelManager device : channelManagers) { // for each device subscribe to the devices channel and a common one final IBlaubotChannel deviceChannel = device.createOrGetChannel(deviceId); deviceChannel.getChannelConfig().setTransmitReflexiveMessages(true); deviceChannel.subscribe(); // memorize the channel deviceChannelMap.put(deviceId, deviceChannel); // attach listeners to the channels final CountDownLatch deviceChannelLatch = new CountDownLatch(channelManagers.size() * ORDER_TEST_NUMBER_OF_MESSAGES); deviceChannel.addMessageListener(new IBlaubotMessageListener() { private ConcurrentHashMap<Short, Integer> sequenceCounter = new ConcurrentHashMap<>(); @Override public void onMessage(BlaubotMessage blaubotMessage) { String msg = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); short senderDeviceNumber = Short.valueOf(msg.split(";")[0]); short receivingDeviceNumber = Short.valueOf(msg.split(";")[1]); int sequenceNumber = Integer.valueOf(msg.split(";")[2]); Integer oldSequenceNumber = sequenceCounter.put(senderDeviceNumber, sequenceNumber); if(oldSequenceNumber != null) { Assert.assertTrue("former sequence number should be smaller than the new sequence number but wasn't: old: " + oldSequenceNumber + " new: " + sequenceNumber + "; message: " + msg, oldSequenceNumber < sequenceNumber); } deviceChannelLatch.countDown(); } }); // add latch for this client deviceChannelLatches.add(deviceChannelLatch); deviceId++; } // wait some time to let the subscription messages arrive Thread.sleep(SUBSCRIPTION_SLEEP_TIME); // now send the messages from each device to all the available device channels short sendingDeviceNumber = 0; for(BlaubotChannelManager fromDevice : channelManagers) { // send to each device short receivingDeviceNumber = 0; for(BlaubotChannelManager toDevice : channelManagers) { final IBlaubotChannel deviceChannel = fromDevice.createOrGetChannel(receivingDeviceNumber); for(int seqNumber=0; seqNumber < ORDER_TEST_NUMBER_OF_MESSAGES; seqNumber += 1) { final String deviceMsg = sendingDeviceNumber + ";" + receivingDeviceNumber + ";" + seqNumber; final boolean published = deviceChannel.publish(deviceMsg.getBytes(BlaubotConstants.STRING_CHARSET), false); Assert.assertTrue(published); } receivingDeviceNumber += 1; } sendingDeviceNumber += 1; } // await latches for(CountDownLatch latch : deviceChannelLatches) { latch.await(); } } /** * Tests if the excludeSender option of a channel's publish method is respected for some specific * channel settings * @param channelManagers the channel managers to test, note that they have to be already connected as one network * and that the first element in the list has to be the master/king */ public static void testExcludeSender(List<BlaubotChannelManager> channelManagers) throws InterruptedException, TimeoutException { /* We will build a network of two, subscribe to one channel number on both nodes and check that they never receive their own message when the excludeSender option is used with the transmitReflexiveMessagesOption=false */ BlaubotChannelManager king = channelManagers.get(0); BlaubotChannelManager anyClient = channelManagers.get(1); // define messages to be sent by king and client final String kingMessage = "SentByKing"; final String clientMessage = "SentByClient"; // we await two messages, so we create a latch for that final Waiter noReflexiveWaiter = new Waiter(); IBlaubotChannel kingChannel = king.createOrGetChannel((short) 1); kingChannel.getChannelConfig().setTransmitReflexiveMessages(false); IBlaubotMessageListener kingMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { // assert that the king never receives the message he sent String msgReceived = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); System.out.println("kng" + msgReceived); noReflexiveWaiter.assertTrue(!msgReceived.equals(kingMessage)); // but make sure we receive the client's message noReflexiveWaiter.assertEquals(msgReceived, clientMessage); noReflexiveWaiter.resume(); } }; kingChannel.subscribe(kingMessageListener); IBlaubotChannel clientChannel = anyClient.createOrGetChannel((short) 1); clientChannel.getChannelConfig().setTransmitReflexiveMessages(false); IBlaubotMessageListener clientMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { // assert that the client never receives the message he sent String msgReceived = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); System.out.println("clnt" + msgReceived); noReflexiveWaiter.assertTrue(!msgReceived.equals(clientMessage)); // but make sure we receive the kings message noReflexiveWaiter.assertEquals(msgReceived, kingMessage); noReflexiveWaiter.resume(); } }; clientChannel.subscribe(clientMessageListener); Thread.sleep(1000); // wait some time for subscriptions to be propagated // send with exclude clientChannel.publish(clientMessage.getBytes(), true); kingChannel.publish(kingMessage.getBytes(), true); // await the latch (we await 2 asserted messages) noReflexiveWaiter.await(10000, 2); // now again, we test it with setTransmitReflexiveMessages set to true kingChannel.getChannelConfig().setTransmitReflexiveMessages(true); clientChannel.getChannelConfig().setTransmitReflexiveMessages(true); // we create new listeners kingChannel.removeMessageListener(kingMessageListener); clientChannel.removeMessageListener(clientMessageListener); final Waiter reflexiveWaiter = new Waiter(); kingMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { // assert that the king never receives the message he sent String msgReceived = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); System.out.println("kng" + msgReceived); reflexiveWaiter.assertTrue(!msgReceived.equals(kingMessage)); // but make sure we receive the client's message reflexiveWaiter.assertEquals(msgReceived, clientMessage); reflexiveWaiter.resume(); } }; clientMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { // assert that the client never receives the message he sent String msgReceived = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); System.out.println("clnt" + msgReceived); reflexiveWaiter.assertTrue(!msgReceived.equals(clientMessage)); // but make sure we receive the kings message reflexiveWaiter.assertEquals(msgReceived, kingMessage); reflexiveWaiter.resume(); } }; kingChannel.subscribe(kingMessageListener); clientChannel.subscribe(clientMessageListener); Thread.sleep(300); // wait some time for subscriptions to be propagated/listeners be wired // we don't need to wait for subscriptions to come through here // send with exclude clientChannel.publish(clientMessage.getBytes(), true); kingChannel.publish(kingMessage.getBytes(), true); // await the latch (we await 2 asserted messages) reflexiveWaiter.await(10000, 2); } }