package lbms.plugins.mldht.kad; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import lbms.plugins.mldht.DHTConfiguration; import lbms.plugins.mldht.kad.DHT; import lbms.plugins.mldht.kad.DHTLogger; import lbms.plugins.mldht.kad.DHT.DHTtype; import lbms.plugins.mldht.kad.DHT.LogLevel; import lbms.plugins.mldht.kad.DHTStatus; import java.lang.Thread.UncaughtExceptionHandler; import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.junit.Test; public class DHTLifeCycleTest { /** * Test that startup doesn't throw exceptions * * things not covered: * - routing table loading * - delayed tasks * - bootstrap name resolution * - bootstrap ping attempts */ @Test public void testStartup() throws Exception { int port = ThreadLocalRandom.current().nextInt(1024, 65535); CompletableFuture<Boolean> exceptionCanary = new CompletableFuture<>(); ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { exceptionCanary.completeExceptionally(e); } }); return t; } }); DHT dhtInstance = new DHT(DHTtype.IPV4_DHT); // TODO: refactor to per-instance logger DHT.setLogger(new DHTLogger() { @Override public void log(Throwable t, LogLevel l) { // allow bootstrap node resolution to fail in test environment if(t instanceof UnknownHostException) return; exceptionCanary.completeExceptionally(t); } @Override public void log(String message, LogLevel l) { } }); dhtInstance.setScheduler(scheduler); Path storagePath = Paths.get(".", "does", "not", "exist"); dhtInstance.start(new DHTConfiguration() { @Override public boolean noRouterBootstrap() { return true; } @Override public boolean isPersistingID() { return false; } @Override public Path getStoragePath() { return storagePath; } @Override public int getListeningPort() { return port; } @Override public boolean allowMultiHoming() { return false; } }); assertEquals(DHTStatus.Initializing, dhtInstance.getStatus()); // bootstrap is called synchronously during startup and should fall straight through, thus reset the state assertEquals(DHT.BootstrapState.NONE, dhtInstance.bootstrapping.get()); assertEquals(1, dhtInstance.getServerManager().getServerCount()); CompletableFuture<Boolean> wasEmpty = new CompletableFuture<>(); // single-threaded executor -> we can let startup tasks complete and then stop the DHT from the pool itself // thus there should be no pending tasks on the executor scheduler.execute(() -> { dhtInstance.stop(); scheduler.purge(); wasEmpty.complete(scheduler.getQueue().isEmpty()); }); assertTrue("no tasks should remain queued after stop()", wasEmpty.get()); assertEquals(DHTStatus.Stopped, dhtInstance.getStatus()); assertEquals("no messages should have been sent on a bootstrapless startup", 0, dhtInstance.getStats().getNumSentPackets()); scheduler.shutdown(); assertTrue("all tasks terminated", scheduler.awaitTermination(10, TimeUnit.MILLISECONDS)); exceptionCanary.complete(true); // check for async exceptions exceptionCanary.get(); assertFalse("should not create storage path, that's the caller's duty", Files.isDirectory(storagePath)); } }