package games.strategy.engine.message.unifiedmessenger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import games.strategy.debug.ClientLogger; import games.strategy.engine.message.ConnectionLostException; import games.strategy.engine.message.IRemote; import games.strategy.engine.message.MessageContext; import games.strategy.engine.message.RemoteMessenger; import games.strategy.engine.message.RemoteName; import games.strategy.engine.message.RemoteNotFoundException; import games.strategy.engine.message.UnifiedMessengerHub; import games.strategy.net.ClientMessenger; import games.strategy.net.IConnectionChangeListener; import games.strategy.net.INode; import games.strategy.net.IServerMessenger; import games.strategy.net.MacFinder; import games.strategy.net.Node; import games.strategy.net.ServerMessenger; import games.strategy.test.TestUtil; import games.strategy.util.ThreadUtil; public class RemoteMessengerTest { private int serverPort = -1; private IServerMessenger serverMessenger = mock(IServerMessenger.class); private RemoteMessenger remoteMessenger; private UnifiedMessengerHub unifiedMessengerHub; @Before public void setUp() throws Exception { // simple set up for non networked testing final List<IConnectionChangeListener> connectionListeners = new CopyOnWriteArrayList<>(); doAnswer(new Answer<Void>() { @Override public Void answer(final InvocationOnMock invocation) throws Throwable { connectionListeners.add(invocation.getArgument(0)); return null; } }).when(serverMessenger).addConnectionChangeListener(any()); doAnswer(new Answer<Void>() { @Override public Void answer(final InvocationOnMock invocation) throws Throwable { for (final IConnectionChangeListener listener : connectionListeners) { listener.connectionRemoved(invocation.getArgument(0)); } return null; } }).when(serverMessenger).removeConnection(any()); Node dummyNode; try { dummyNode = new Node("dummy", InetAddress.getLocalHost(), 0); } catch (final UnknownHostException e) { ClientLogger.logQuietly(e); throw new IllegalStateException(e); } when(serverMessenger.getLocalNode()).thenReturn(dummyNode); when(serverMessenger.getServerNode()).thenReturn(dummyNode); when(serverMessenger.isServer()).thenReturn(true); remoteMessenger = new RemoteMessenger(new UnifiedMessenger(serverMessenger)); serverPort = TestUtil.getUniquePort(); } @After public void tearDown() throws Exception { serverMessenger = null; remoteMessenger = null; } @Test public void testRegisterUnregister() { final TestRemote testRemote = new TestRemote(); final RemoteName test = new RemoteName(ITestRemote.class, "test"); remoteMessenger.registerRemote(testRemote, test); assertTrue(remoteMessenger.hasLocalImplementor(test)); remoteMessenger.unregisterRemote(test); assertFalse(remoteMessenger.hasLocalImplementor(test)); } @Test public void testMethodCall() { final TestRemote testRemote = new TestRemote(); final RemoteName test = new RemoteName(ITestRemote.class, "test"); remoteMessenger.registerRemote(testRemote, test); final ITestRemote remote = (ITestRemote) remoteMessenger.getRemote(test); assertEquals(2, remote.increment(1)); assertEquals(testRemote.getLastSenderNode(), serverMessenger.getLocalNode()); } @Test public void testExceptionThrownWhenUnregisteredRemote() { final TestRemote testRemote = new TestRemote(); final RemoteName test = new RemoteName(ITestRemote.class, "test"); remoteMessenger.registerRemote(testRemote, test); final ITestRemote remote = (ITestRemote) remoteMessenger.getRemote(test); remoteMessenger.unregisterRemote("test"); try { remote.increment(1); fail("No exception thrown"); } catch (final RemoteNotFoundException rme) { // this is what we expect } } @Test public void testNoRemote() { final RemoteName test = new RemoteName(ITestRemote.class, "test"); try { remoteMessenger.getRemote(test); final ITestRemote remote = (ITestRemote) remoteMessenger.getRemote(test); remote.testVoid(); fail("No exception thrown"); } catch (final RemoteNotFoundException rme) { // this is what we expect } } @Test public void testVoidMethodCall() { final TestRemote testRemote = new TestRemote(); final RemoteName test = new RemoteName(ITestRemote.class, "test"); remoteMessenger.registerRemote(testRemote, test); final ITestRemote remote = (ITestRemote) remoteMessenger.getRemote(test); remote.testVoid(); } @Test public void testException() throws Exception { final TestRemote testRemote = new TestRemote(); final RemoteName test = new RemoteName(ITestRemote.class, "test"); remoteMessenger.registerRemote(testRemote, test); final ITestRemote remote = (ITestRemote) remoteMessenger.getRemote(test); try { remote.throwException(); } catch (final Exception e) { // this is what we want if (e.getMessage().equals(TestRemote.EXCEPTION_STRING)) { return; } throw e; } fail("No exception thrown"); } @Test public void testRemoteCall() throws Exception { final RemoteName test = new RemoteName(ITestRemote.class, "test"); ServerMessenger server = null; ClientMessenger client = null; try { server = new ServerMessenger("server", serverPort); server.setAcceptNewConnections(true); final String mac = MacFinder.getHashedMacAddress(); client = new ClientMessenger("localhost", serverPort, "client", mac); final UnifiedMessenger serverUM = new UnifiedMessenger(server); unifiedMessengerHub = serverUM.getHub(); final RemoteMessenger serverRM = new RemoteMessenger(serverUM); final RemoteMessenger clientRM = new RemoteMessenger(new UnifiedMessenger(client)); // register it on the server final TestRemote testRemote = new TestRemote(); serverRM.registerRemote(testRemote, test); // since the registration must go over a socket // and through a couple threads, wait for the // client to get it int waitCount = 0; while (!unifiedMessengerHub.hasImplementors(test.getName()) && waitCount < 20) { waitCount++; ThreadUtil.sleep(50); } // call it on the client final int rVal = ((ITestRemote) clientRM.getRemote(test)).increment(1); assertEquals(2, rVal); assertEquals(testRemote.getLastSenderNode(), client.getLocalNode()); } finally { shutdownServerAndClient(server, client); } } private static void shutdownServerAndClient(final ServerMessenger server, final ClientMessenger client) { if (server != null) { server.shutDown(); } if (client != null) { client.shutDown(); } } @Test public void testRemoteCall2() throws Exception { final RemoteName test = new RemoteName(ITestRemote.class, "test"); ServerMessenger server = null; ClientMessenger client = null; try { server = new ServerMessenger("server", serverPort); server.setAcceptNewConnections(true); final String mac = MacFinder.getHashedMacAddress(); client = new ClientMessenger("localhost", serverPort, "client", mac); final RemoteMessenger serverRM = new RemoteMessenger(new UnifiedMessenger(server)); final TestRemote testRemote = new TestRemote(); serverRM.registerRemote(testRemote, test); final RemoteMessenger clientRM = new RemoteMessenger(new UnifiedMessenger(client)); // call it on the client // should be no need to wait since the constructor should not // reutrn until the initial state of the messenger is good final int rVal = ((ITestRemote) clientRM.getRemote(test)).increment(1); assertEquals(2, rVal); assertEquals(testRemote.getLastSenderNode(), client.getLocalNode()); } finally { shutdownServerAndClient(server, client); } } @Test public void testShutDownClient() throws Exception { // when the client shutdown, remotes created // on the client should not be visible on server final RemoteName test = new RemoteName(ITestRemote.class, "test"); ServerMessenger server = null; ClientMessenger client = null; try { server = new ServerMessenger("server", serverPort); server.setAcceptNewConnections(true); final String mac = MacFinder.getHashedMacAddress(); client = new ClientMessenger("localhost", serverPort, "client", mac); final UnifiedMessenger serverUM = new UnifiedMessenger(server); final RemoteMessenger clientRM = new RemoteMessenger(new UnifiedMessenger(client)); clientRM.registerRemote(new TestRemote(), test); serverUM.getHub().waitForNodesToImplement(test.getName()); assertTrue(serverUM.getHub().hasImplementors(test.getName())); client.shutDown(); ThreadUtil.sleep(200); assertTrue(!serverUM.getHub().hasImplementors(test.getName())); } finally { shutdownServerAndClient(server, client); } } @Test public void testMethodReturnsOnWait() throws Exception { // when the client shutdown, remotes created // on the client should not be visible on server final RemoteName test = new RemoteName(IFoo.class, "test"); ServerMessenger server = null; ClientMessenger client = null; try { server = new ServerMessenger("server", serverPort); server.setAcceptNewConnections(true); final String mac = MacFinder.getHashedMacAddress(); client = new ClientMessenger("localhost", serverPort, "client", mac); final UnifiedMessenger serverUM = new UnifiedMessenger(server); final RemoteMessenger serverRM = new RemoteMessenger(serverUM); final RemoteMessenger clientRM = new RemoteMessenger(new UnifiedMessenger(client)); final Object lock = new Object(); final AtomicBoolean started = new AtomicBoolean(false); final IFoo foo = new IFoo() { @Override public void foo() { synchronized (lock) { try { started.set(true); lock.wait(); } catch (final InterruptedException e) { // ignore interrupted exception } } } }; clientRM.registerRemote(foo, test); serverUM.getHub().waitForNodesToImplement(test.getName()); assertTrue(serverUM.getHub().hasImplementors(test.getName())); final AtomicReference<ConnectionLostException> rme = new AtomicReference<>(null); final Runnable r = new Runnable() { @Override public void run() { try { final IFoo remoteFoo = (IFoo) serverRM.getRemote(test); remoteFoo.foo(); } catch (final ConnectionLostException e) { rme.set(e); } } }; final Thread t = new Thread(r); t.start(); // wait for the thread to start while (started.get() == false) { ThreadUtil.sleep(1); } ThreadUtil.sleep(20); // TODO: we are getting a RemoteNotFoundException because the client is disconnecting before the invoke goes out // completely // Perhaps this situation should be changed to a ConnectionLostException or something else? client.shutDown(); // when the client shutdowns, this should wake up. // and an error should be thrown // give the thread a chance to execute t.join(200); synchronized (lock) { lock.notifyAll(); } assertNotNull(rme.get()); } finally { shutdownServerAndClient(server, client); } } private interface IFoo extends IRemote { void foo(); } private interface ITestRemote extends IRemote { int increment(int testVal); void testVoid(); void throwException() throws Exception; } private static class TestRemote implements ITestRemote { public static final String EXCEPTION_STRING = "AND GO"; private INode senderNode; @Override public int increment(final int testVal) { senderNode = MessageContext.getSender(); return testVal + 1; } @Override public void testVoid() { senderNode = MessageContext.getSender(); } @Override public void throwException() throws Exception { throw new Exception(EXCEPTION_STRING); } public INode getLastSenderNode() { return senderNode; } } }