/* * Copyright 2015-2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE 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 3 of the License, or (at your option) any * later version. * * The CCRE 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 the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.cluck; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.NotSerializableException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import ccre.channel.EventOutput; import ccre.log.LogLevel; import ccre.log.VerifyingLogger; import ccre.testing.CountingEventOutput; import ccre.util.Values; @SuppressWarnings("javadoc") public class CluckNodeTest { private static final String ERROR_MESSAGE = "Purposeful failure."; private CluckNode node; @Before public void setUp() throws Exception { node = new CluckNode(); VerifyingLogger.begin(); } @After public void tearDown() throws Exception { VerifyingLogger.checkAndEnd(); node = null; } @Test public void testNotifyNetworkModified() { CountingEventOutput ceo = new CountingEventOutput(); node.addLink(new CluckLink() { @Override public boolean send(String dest, String source, byte[] data) { assertEquals(dest, CluckConstants.BROADCAST_DESTINATION); // note: this is an implementation detail assertEquals(source, "#modsrc"); assertEquals(data.length, 1); assertEquals(data[0], CluckConstants.RMT_NOTIFY); ceo.event(); return true; } }, "example-nm-link"); ceo.ifExpected = true; node.notifyNetworkModified(); ceo.check(); } @Test public void testBroadcast() { VerifyingCluckLink[] vcls = new VerifyingCluckLink[10]; for (int i = 0; i < vcls.length; i++) { vcls[i] = new VerifyingCluckLink(); node.addLink(vcls[i], "example-" + i); } for (byte[] message : Values.getRandomByteses(10, 1, 256)) { for (String source : Values.getRandomStrings(10)) { for (int i = 0; i < vcls.length; i++) { vcls[i].expectedSource = source; vcls[i].expectedMessage = message; vcls[i].expectedDestination = CluckConstants.BROADCAST_DESTINATION; vcls[i].ifExpected = true; } node.broadcast(source, message, null); for (int i = 0; i < vcls.length; i++) { vcls[i].check(); } } } } @Test public void testBroadcastDenies() { VerifyingCluckLink[] vcls = new VerifyingCluckLink[10]; for (int i = 0; i < vcls.length; i++) { vcls[i] = new VerifyingCluckLink(); node.addLink(vcls[i], "example-" + i); } int ti = 0; for (byte[] message : Values.getRandomByteses(11, 1, 256)) { for (String source : Values.getRandomStrings(7)) { int dropi = ti++ % vcls.length; for (int i = 0; i < vcls.length; i++) { vcls[i].expectedSource = source; vcls[i].expectedMessage = message; vcls[i].expectedDestination = CluckConstants.BROADCAST_DESTINATION; vcls[i].ifExpected = dropi != i; } node.broadcast(source, message, vcls[dropi]); for (int i = 0; i < vcls.length; i++) { vcls[i].check(); } } } } @Test public void testTransmitBroadcastDenies() { VerifyingCluckLink[] vcls = new VerifyingCluckLink[10]; for (int i = 0; i < vcls.length; i++) { vcls[i] = new VerifyingCluckLink(); node.addLink(vcls[i], "example-" + i); } int ti = 0; for (byte[] message : Values.getRandomByteses(11, 1, 256)) { for (String source : Values.getRandomStrings(7)) { int dropi = ti++ % vcls.length; for (int i = 0; i < vcls.length; i++) { vcls[i].expectedSource = source; vcls[i].expectedMessage = message; vcls[i].expectedDestination = CluckConstants.BROADCAST_DESTINATION; vcls[i].ifExpected = dropi != i; } node.transmit(CluckConstants.BROADCAST_DESTINATION, source, message, vcls[dropi]); for (int i = 0; i < vcls.length; i++) { vcls[i].check(); } } } } @Test(expected = NullPointerException.class) public void testTransmitNull() { node.transmit("target", "source", null); } @Test public void testBroadcastDetach() { VerifyingCluckLink[] vcls = new VerifyingCluckLink[10]; for (int i = 0; i < vcls.length; i++) { vcls[i] = new VerifyingCluckLink(); node.addLink(vcls[i], "example-" + i); } int ti = 0; for (byte[] message : Values.getRandomByteses(11, 1, 256)) { for (String source : Values.getRandomStrings(7)) { int dropi = ti++ % vcls.length; for (int i = 0; i < vcls.length; i++) { vcls[i].expectedSource = source; vcls[i].expectedMessage = message; vcls[i].expectedDestination = CluckConstants.BROADCAST_DESTINATION; if (vcls[i].keepAlive) { vcls[i].keepAlive = i != dropi; vcls[i].ifExpected = true; } } node.broadcast(source, message, null); for (int i = 0; i < vcls.length; i++) { if (!vcls[i].keepAlive) { assertFalse(node.hasLink("example-" + i)); } else { assertTrue(node.hasLink("example-" + i)); } vcls[i].check(); } } } } @Test public void testBroadcastError() { VerifyingCluckLink[] vcls = new VerifyingCluckLink[50]; for (int i = 0; i < vcls.length; i++) { vcls[i] = new VerifyingCluckLink(); node.addLink(vcls[i], "example-" + i); } CountingEventOutput ceo = new CountingEventOutput(); CluckLink evil = new CluckLink() { @Override public boolean send(String dest, String source, byte[] data) { ceo.event(); throw new RuntimeException(ERROR_MESSAGE); } }; node.addLink(evil, "evil"); for (int c = 0; c < 5; c++) { byte[] message = Values.getRandomBytes(10); for (int i = 0; i < vcls.length; i++) { vcls[i].expectedDestination = CluckConstants.BROADCAST_DESTINATION; vcls[i].expectedMessage = message; vcls[i].ifExpected = true; } ceo.ifExpected = true; VerifyingLogger.configure(LogLevel.SEVERE, "[LOCAL] Error while broadcasting to Cluck link evil", (t) -> t.getClass() == RuntimeException.class && ERROR_MESSAGE.equals(t.getMessage())); node.broadcast(null, message, null); VerifyingLogger.check(); // TODO: test logging ceo.check(); for (int i = 0; i < vcls.length; i++) { vcls[i].check(); } } } @Test(expected = NullPointerException.class) public void testBroadcastNull() { node.broadcast("source", null, new VerifyingCluckLink()); } @Test public void testTransmitDirect() { VerifyingCluckLink vcl = new VerifyingCluckLink(); vcl.expectedSource = "testy testy"; node.addLink(vcl, "example"); for (int i = 0; i < 5; i++) { byte[] message = Values.getRandomBytes(10); vcl.expectedMessage = message; vcl.ifExpected = true; node.transmit("example", "testy testy", message); vcl.check(); } } @Test public void testTransmitUnsub() { VerifyingCluckLink vcl = new VerifyingCluckLink(); vcl.expectedSource = "testy testy"; node.addLink(vcl, "example"); for (int i = 0; i < 6; i++) { byte[] message = Values.getRandomBytes(10); vcl.expectedMessage = message; vcl.ifExpected = i < 5; vcl.keepAlive = i < 4; if (i == 5) { VerifyingLogger.configure(LogLevel.WARNING, "[LOCAL] No link for example(example) from testy testy!"); } node.transmit("example", "testy testy", message); VerifyingLogger.check(); vcl.check(); } } @Test public void testTransmitError() { node.addLink(new CluckLink() { @Override public boolean send(String dest, String source, byte[] data) { throw new RuntimeException(ERROR_MESSAGE); } }, "evil"); VerifyingLogger.configure(LogLevel.SEVERE, "[LOCAL] Error while dispatching to Cluck link evil", (t) -> t.getClass() == RuntimeException.class && ERROR_MESSAGE.equals(t.getMessage())); node.transmit("evil", null, new byte[] { CluckConstants.RMT_NOTIFY }); VerifyingLogger.check(); } @Test public void testTransmitSideChannel() { VerifyingCluckLink vcl = new VerifyingCluckLink(); vcl.expectedSource = "testy testy"; vcl.expectedDestination = "side-channel"; node.addLink(vcl, "example"); for (int i = 0; i < 5; i++) { byte[] message = Values.getRandomBytes(10); vcl.expectedMessage = message; vcl.ifExpected = true; node.transmit("example/side-channel", "testy testy", message); vcl.check(); } } @Test public void testTransmitSideChannelMulti() { VerifyingCluckLink vcl = new VerifyingCluckLink(); vcl.expectedSource = "testy testy"; vcl.expectedDestination = "side-channel/extra/other/hi"; node.addLink(vcl, "example"); for (int i = 0; i < 5; i++) { byte[] message = Values.getRandomBytes(10); vcl.expectedMessage = message; vcl.ifExpected = true; node.transmit("example/side-channel/extra/other/hi", "testy testy", message); vcl.check(); } } @Test public void testTransmitNAKNAK() { // makes sure that we don't get an infinite loop VerifyingLogger.configure(LogLevel.WARNING, "[LOCAL] No link for example(example) from example!"); node.transmit("example", "example", new byte[] { CluckConstants.RMT_NOTIFY }); VerifyingLogger.check(); } @Test public void testTransmitNAK() { VerifyingCluckLink nackTarget = new VerifyingCluckLink(); nackTarget.expectedDestination = null; nackTarget.expectedMessage = new byte[] { CluckConstants.RMT_NEGATIVE_ACK }; nackTarget.expectedSource = "example"; node.addLink(nackTarget, "nacker"); for (int i = 0; i < 10; i++) { nackTarget.ifExpected = true; if (i == 0) { // error deduplication VerifyingLogger.configure(LogLevel.WARNING, "[LOCAL] No link for example(example) from nacker!"); } node.transmit("example", "nacker", new byte[] { CluckConstants.RMT_NOTIFY }); VerifyingLogger.check(); nackTarget.check(); } } @Test public void testTransmitNoNAKNAK() { VerifyingCluckLink nackTarget = new VerifyingCluckLink(); node.addLink(nackTarget, "nacker"); for (int i = 0; i < 10; i++) { nackTarget.ifExpected = false; node.transmit("example", "nacker", new byte[] { CluckConstants.RMT_NEGATIVE_ACK }); nackTarget.check(); } } @Test public void testTransmitNAKSideChannel() { VerifyingCluckLink nackTarget = new VerifyingCluckLink(); nackTarget.expectedDestination = "other-side-channel"; nackTarget.expectedMessage = new byte[] { CluckConstants.RMT_NEGATIVE_ACK }; nackTarget.expectedSource = "example/side-channel"; node.addLink(nackTarget, "nacker"); for (int i = 0; i < 10; i++) { nackTarget.ifExpected = true; if (i == 0) { // error deduplication VerifyingLogger.configure(LogLevel.WARNING, "[LOCAL] No link for example/side-channel(example) from nacker/other-side-channel!"); } node.transmit("example/side-channel", "nacker/other-side-channel", new byte[] { CluckConstants.RMT_NOTIFY }); VerifyingLogger.check(); nackTarget.check(); } } @Test public void testSubscribeToStructureNotifications() { CountingEventOutput ceo = new CountingEventOutput(); node.subscribeToStructureNotifications("receiver-1", ceo); for (int i = 0; i < 10; i++) { ceo.ifExpected = true; node.notifyNetworkModified(); ceo.check(); } for (String source : Values.getRandomStrings(10)) { ceo.ifExpected = true; node.transmit(CluckConstants.BROADCAST_DESTINATION, source, new byte[] { CluckConstants.RMT_NOTIFY }); ceo.check(); } for (String source : Values.getRandomStrings(10)) { ceo.ifExpected = false; node.transmit(CluckConstants.BROADCAST_DESTINATION, source, new byte[] { CluckConstants.RMT_NOTIFY, 0 }); ceo.check(); } for (int i = 0; i < 10; i++) { ceo.ifExpected = false; node.transmit(CluckConstants.BROADCAST_DESTINATION, "#modsrc", new byte[] { CluckConstants.RMT_PING }); ceo.check(); } for (int i = 0; i < 10; i++) { ceo.ifExpected = false; node.transmit(CluckConstants.BROADCAST_DESTINATION, "arbitrary", new byte[] { CluckConstants.RMT_PING }); ceo.check(); } for (int i = 0; i < 10; i++) { ceo.ifExpected = false; node.transmit(CluckConstants.BROADCAST_DESTINATION, "arbitrary", new byte[] { CluckConstants.RMT_BOOLINPUT }); ceo.check(); } for (int i = 0; i < 10; i++) { ceo.ifExpected = false; node.transmit("receiver-1", "arbitrary", new byte[] { CluckConstants.RMT_BOOLINPUT }); ceo.check(); } for (int i = 0; i < 10; i++) { ceo.ifExpected = false; node.transmit("receiver-1", "arbitrary", new byte[] { CluckConstants.RMT_NOTIFY }); ceo.check(); } for (int i = 0; i < 10; i++) { ceo.ifExpected = false; node.transmit("receiver-1", "#modsrc", new byte[] { CluckConstants.RMT_NOTIFY }); ceo.check(); } } @Test(expected = NullPointerException.class) public void testSubscribeToStructureNotificationsNullA() { node.subscribeToStructureNotifications(null, EventOutput.ignored); } @Test(expected = NullPointerException.class) public void testSubscribeToStructureNotificationsNullB() { node.subscribeToStructureNotifications("test", null); } private boolean failInvoke = false; @Test public void testGetLinkName() { failInvoke = false; CluckLink cl = new CluckLink() { @Override public boolean send(String dest, String source, byte[] data) { failInvoke = true; throw new RuntimeException("Should not be invoked!"); } }; node.addLink(new VerifyingCluckLink(), "example-1"); node.addLink(cl, "example-2"); node.addLink(new VerifyingCluckLink(), "example-3"); node.addLink(new VerifyingCluckLink(), "example-4"); node.addLink(new VerifyingCluckLink(), "example-5"); assertEquals(node.getLinkName(cl), "example-2"); assertFalse(failInvoke); } @Test(expected = NullPointerException.class) public void testGetLinkNameNull() { node.getLinkName(null); } @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void testGetLinkNameNonexistent() { thrown.expect(IllegalArgumentException.class); thrown.expectMessage("No such link!"); node.getLinkName(new VerifyingCluckLink()); } @Test public void testHasLink() { for (int i = 0; i < 10; i++) { for (int j = i; j < 10; j++) { assertFalse(node.hasLink("test-" + j)); } node.addLink(new VerifyingCluckLink(), "test-" + i); for (int j = 0; j <= i; j++) { assertTrue(node.hasLink("test-" + j)); } } for (int i = 0; i < 5; i++) { node.removeLink("test-" + i); assertFalse(node.hasLink("test-" + i)); } for (int i = 0; i < 5; i++) { assertFalse(node.hasLink("test-" + i)); } } @Test(expected = NullPointerException.class) public void testHasLinkNull() { node.hasLink(null); } @Test public void testAddLinkHasLink() { assertFalse(node.hasLink("test-1")); node.addLink(new VerifyingCluckLink(), "test-1"); assertTrue(node.hasLink("test-1")); } @Test public void testAddLinkExistent() { node.addLink(new VerifyingCluckLink(), "example-1"); try { node.addLink(new VerifyingCluckLink(), "example-1"); } catch (IllegalStateException ex) { assertTrue(ex.getMessage().contains("already used")); assertTrue(ex.getMessage().contains("example-1")); } } @Test(expected = NullPointerException.class) public void testAddLinkNullA() { node.addLink(null, "example-1"); } @Test(expected = NullPointerException.class) public void testAddLinkNullB() { node.addLink(new VerifyingCluckLink(), null); } @Test(expected = IllegalArgumentException.class) public void testAddLinkSlash() { node.addLink(new VerifyingCluckLink(), "example-1/side-channel"); } @Test public void testAddOrReplaceLinkHasLink() { assertFalse(node.hasLink("test-1")); node.addOrReplaceLink(new VerifyingCluckLink(), "test-1"); assertTrue(node.hasLink("test-1")); } @Test public void testAddOrReplaceLinkExistent() { assertFalse(node.hasLink("example-1")); node.addLink(new VerifyingCluckLink(), "example-1"); assertTrue(node.hasLink("example-1")); VerifyingCluckLink beta = new VerifyingCluckLink(); VerifyingLogger.configure(LogLevel.FINE, "Replaced current link on: example-1"); node.addOrReplaceLink(beta, "example-1"); VerifyingLogger.check(); assertTrue(node.hasLink("example-1")); beta.expectedMessage = new byte[] { CluckConstants.RMT_NOTIFY }; beta.ifExpected = true; node.transmit("example-1", null, new byte[] { CluckConstants.RMT_NOTIFY }); beta.check(); } @Test(expected = NullPointerException.class) public void testAddOrReplaceLinkNullA() { node.addOrReplaceLink(new VerifyingCluckLink(), null); } @Test(expected = NullPointerException.class) public void testAddOrReplaceLinkNullB() { node.addOrReplaceLink(null, "example-1"); } @Test(expected = IllegalArgumentException.class) public void testAddOrReplaceLinkSlash() { node.addOrReplaceLink(new VerifyingCluckLink(), "example-1/side-channel"); } @Test public void testRemoveLink() { assertFalse(node.removeLink("test-1")); assertFalse(node.hasLink("test-1")); VerifyingCluckLink main = new VerifyingCluckLink(); main.expectedDestination = null; main.expectedSource = "bounce"; main.expectedMessage = new byte[] { CluckConstants.RMT_NOTIFY }; node.addLink(main, "test-1"); assertTrue(node.hasLink("test-1")); VerifyingCluckLink bounce = new VerifyingCluckLink(); bounce.expectedDestination = null; bounce.expectedSource = "test-1"; bounce.expectedMessage = new byte[] { CluckConstants.RMT_NEGATIVE_ACK }; node.addLink(bounce, "bounce"); main.ifExpected = true; node.transmit("test-1", "bounce", new byte[] { CluckConstants.RMT_NOTIFY }); main.check(); assertTrue(node.hasLink("test-1")); assertTrue(node.removeLink("test-1")); assertFalse(node.hasLink("test-1")); bounce.ifExpected = true; VerifyingLogger.configure(LogLevel.WARNING, "[LOCAL] No link for test-1(test-1) from bounce!"); node.transmit("test-1", "bounce", new byte[] { CluckConstants.RMT_NOTIFY }); VerifyingLogger.check(); bounce.check(); assertFalse(node.hasLink("test-1")); assertFalse(node.removeLink("test-1")); assertFalse(node.hasLink("test-1")); } @Test(expected = NullPointerException.class) public void testRemoveLinkNull() { node.removeLink(null); } @Test public void testGetRPCManager() { assertTrue(node.getRPCManager() == node.getRPCManager()); } @Test(expected = NotSerializableException.class) public void testNotSerializable() throws IOException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { try (ObjectOutput oos = new ObjectOutputStream(baos)) { oos.writeObject(node); } } } @Test public void testSerializeGlobal() throws IOException, ClassNotFoundException { byte[] bytes; try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { try (ObjectOutput oo = new ObjectOutputStream(baos)) { oo.writeObject(Cluck.getNode()); } bytes = baos.toByteArray(); } try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) { try (ObjectInput oi = new ObjectInputStream(bais)) { assertTrue(Cluck.getNode() == oi.readObject()); } } } }