diff --git a/pom.xml b/pom.xml index c0e33c1eb..3a51de059 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,7 @@ UTF-8 + true @@ -173,6 +174,16 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 2.16 + + -Xmx512m + ${maven.test.redirectTestOutputToFile} + + diff --git a/src/main/java/com/turn/ttorrent/cli/TorrentMain.java b/src/main/java/com/turn/ttorrent/cli/TorrentMain.java index bdcb5587c..2155b4963 100644 --- a/src/main/java/com/turn/ttorrent/cli/TorrentMain.java +++ b/src/main/java/com/turn/ttorrent/cli/TorrentMain.java @@ -150,7 +150,7 @@ public static void main(String[] args) { torrent = Torrent.create(source, announceURI, creator); } - torrent.save(fos); + fos.write(torrent.getEncoded()); } else { Torrent.load(new File(filenameValue), true); } diff --git a/src/main/java/com/turn/ttorrent/common/Torrent.java b/src/main/java/com/turn/ttorrent/common/Torrent.java index 5132d4ced..4b5c9e4df 100644 --- a/src/main/java/com/turn/ttorrent/common/Torrent.java +++ b/src/main/java/com/turn/ttorrent/common/Torrent.java @@ -24,7 +24,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.URI; @@ -49,6 +48,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -396,11 +396,11 @@ public boolean isSeeder() { /** * Save this torrent meta-info structure into a .torrent file. * - * @param output The stream to write to. + * @param file The file to write to. * @throws IOException If an I/O error occurs while writing the file. */ - public void save(OutputStream output) throws IOException { - output.write(this.getEncoded()); + public void save(File file) throws IOException { + FileUtils.writeByteArrayToFile(file, this.getEncoded()); } public static byte[] hash(byte[] data) throws NoSuchAlgorithmException { diff --git a/src/main/java/com/turn/ttorrent/tracker/Tracker.java b/src/main/java/com/turn/ttorrent/tracker/Tracker.java index 638fae259..998b89c3d 100644 --- a/src/main/java/com/turn/ttorrent/tracker/Tracker.java +++ b/src/main/java/com/turn/ttorrent/tracker/Tracker.java @@ -22,6 +22,7 @@ import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; +import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Timer; import java.util.TimerTask; @@ -39,7 +40,7 @@ *

* The tracker usually listens on port 6969 (the standard BitTorrent tracker * port). Torrents must be registered directly to this tracker with the - * {@link #announce(TrackedTorrent torrent)} method. + * {@link #announce(Torrent torrent)} method. *

* * @author mpetazzoni @@ -193,7 +194,8 @@ public Collection getTrackedTorrents() { * different from the supplied Torrent object if the tracker already * contained a torrent with the same hash. */ - public synchronized TrackedTorrent announce(TrackedTorrent torrent) { + public synchronized TrackedTorrent announce(Torrent torrent) + throws IOException, NoSuchAlgorithmException { TrackedTorrent existing = this.torrents.get(torrent.getHexInfoHash()); if (existing != null) { @@ -202,10 +204,16 @@ public synchronized TrackedTorrent announce(TrackedTorrent torrent) { return existing; } - this.torrents.put(torrent.getHexInfoHash(), torrent); + final TrackedTorrent result; + if (torrent instanceof TrackedTorrent) { + result = (TrackedTorrent)torrent; + } else { + result = new TrackedTorrent(torrent); + } + this.torrents.put(torrent.getHexInfoHash(), result); logger.info("Registered new torrent for '{}' with hash {}.", torrent.getName(), torrent.getHexInfoHash()); - return torrent; + return result; } /** diff --git a/src/test/java/com/turn/ttorrent/TempFiles.java b/src/test/java/com/turn/ttorrent/TempFiles.java new file mode 100644 index 000000000..fa93ad615 --- /dev/null +++ b/src/test/java/com/turn/ttorrent/TempFiles.java @@ -0,0 +1,137 @@ +package com.turn.ttorrent; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.apache.commons.io.FileUtils; + +public class TempFiles { + + private static final File ourCurrentTempDir = new File(FileUtils.getTempDirectory(), "ttorrent-test"); + + private static final Random ourRandom = new Random(); + + private final File myCurrentTempDir; + private final List myFilesToDelete = new ArrayList(); + private final Thread myShutdownHook; + private volatile boolean myInsideShutdownHook; + + public TempFiles() { + myCurrentTempDir = ourCurrentTempDir; + if (!myCurrentTempDir.isDirectory() && !myCurrentTempDir.mkdirs()) { + + throw new IllegalStateException( + "Temp directory is not a directory, was deleted by some process: " + + myCurrentTempDir.getAbsolutePath()); + } + + myShutdownHook = new Thread(new Runnable() { + public void run() { + myInsideShutdownHook = true; + cleanup(); + } + }); + Runtime.getRuntime().addShutdownHook(myShutdownHook); + } + + private File doCreateTempDir(String prefix, String suffix) + throws IOException { + prefix = prefix == null ? "" : prefix; + suffix = suffix == null ? ".tmp" : suffix; + + do { + int count = ourRandom.nextInt(); + final File f = new File(myCurrentTempDir, prefix + count + suffix); + if (!f.exists() && f.mkdirs()) { + return f.getCanonicalFile(); + } + } while (true); + + } + + private File doCreateTempFile(String prefix, String suffix) + throws IOException { + final File file = doCreateTempDir(prefix, suffix); + file.delete(); + file.createNewFile(); + return file; + } + + public final File createTempFile() throws IOException { + File tempFile = doCreateTempFile("test", null); + registerAsTempFile(tempFile); + return tempFile; + } + + public void registerAsTempFile(final File tempFile) { + myFilesToDelete.add(tempFile); + } + + public final File createTempFile(int size) throws IOException { + File tempFile = createTempFile(); + int bufLen = Math.min(8 * 1024, size); + if (bufLen == 0) return tempFile; + final OutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile)); + try { + byte[] buf = new byte[bufLen]; + ourRandom.nextBytes(buf); + + int numWritten = 0; + for (int i = 0; i < size / buf.length; i++) { + fos.write(buf); + numWritten += buf.length; + } + + if (size > numWritten) { + fos.write(buf, 0, size - numWritten); + } + } finally { + fos.close(); + } + + return tempFile; + } + + /** + * Returns a File object for created temp directory. + * Also stores the value into this object accessed with {@link #getCurrentTempDir()} + * + * @return a File object for created temp directory + * @throws IOException if directory creation fails. + */ + public final File createTempDir() throws IOException { + File f = doCreateTempDir("test", ""); + registerAsTempFile(f); + return f; + } + + /** + * Returns the current directory used by the test or null if no test is running or no directory is created yet. + * + * @return see above + */ + public File getCurrentTempDir() { + return myCurrentTempDir; + } + + public void cleanup() { + try { + for (File file : myFilesToDelete) { + FileUtils.deleteQuietly(file); + } + + myFilesToDelete.clear(); + FileUtils.deleteQuietly(ourCurrentTempDir); + } finally { + if (!myInsideShutdownHook) { + Runtime.getRuntime().removeShutdownHook(myShutdownHook); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/turn/ttorrent/WaitFor.java b/src/test/java/com/turn/ttorrent/WaitFor.java new file mode 100644 index 000000000..71f30d080 --- /dev/null +++ b/src/test/java/com/turn/ttorrent/WaitFor.java @@ -0,0 +1,28 @@ +package com.turn.ttorrent; + +public abstract class WaitFor { + + private static final long DEFAULT_WAIT_TIMEOUT = 40 * 1000; + + private static final long DEFAULT_POLL_INTERVAL = 100; + + protected WaitFor() { + this(DEFAULT_WAIT_TIMEOUT); + } + + protected WaitFor(long timeout) { + this(timeout, DEFAULT_POLL_INTERVAL); + } + + protected WaitFor(long timeout, long interval) { + long started = System.currentTimeMillis(); + try { + while (!condition() && System.currentTimeMillis() - started < timeout) { + Thread.sleep(interval); + } + } catch (InterruptedException ignored) { + } + } + + protected abstract boolean condition(); +} diff --git a/src/test/java/com/turn/ttorrent/common/TorrentTest.java b/src/test/java/com/turn/ttorrent/common/TorrentTest.java new file mode 100644 index 000000000..10df101f0 --- /dev/null +++ b/src/test/java/com/turn/ttorrent/common/TorrentTest.java @@ -0,0 +1,47 @@ +package com.turn.ttorrent.common; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URI; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +@Test +public class TorrentTest { + + private static final Logger logger = + LoggerFactory.getLogger(TorrentTest.class); + + @BeforeMethod(alwaysRun = true) + protected void setUp(Method testMethod) throws Exception { + String testName = testMethod.getDeclaringClass().getSimpleName() + "." + testMethod.getName(); + logger.trace("Test starting: " + testName); + } + + @AfterMethod(alwaysRun = true) + protected void tearDown(Method testMethod) throws Exception { + String testName = testMethod.getDeclaringClass().getSimpleName() + "." + testMethod.getName(); + logger.trace("Test finished: " + testName); + } + + public void test_create_torrent() throws Exception { + URI announceURI = new URI("http://localhost:6969/announce"); + String createdBy = "Test"; + Torrent t = Torrent.create(new File("src/test/resources/parentFiles/file1.jar"), announceURI, createdBy); + assertEquals(createdBy, t.getCreatedBy()); + assertEquals(announceURI, t.getAnnounceList().get(0).get(0)); + } + + public void load_torrent_created_by_utorrent() throws Exception { + Torrent t = Torrent.load(new File("src/test/resources/torrents/file1.jar.torrent")); + assertEquals(new URI("http://localhost:6969/announce"), t.getAnnounceList().get(0).get(0)); + assertEquals("B92D38046C76D73948E14C42DF992CAF25489D08", t.getHexInfoHash()); + assertEquals("uTorrent/3130", t.getCreatedBy()); + } +} diff --git a/src/test/java/com/turn/ttorrent/tracker/TrackerTest.java b/src/test/java/com/turn/ttorrent/tracker/TrackerTest.java new file mode 100644 index 000000000..791c28344 --- /dev/null +++ b/src/test/java/com/turn/ttorrent/tracker/TrackerTest.java @@ -0,0 +1,268 @@ +package com.turn.ttorrent.tracker; + +import com.turn.ttorrent.TempFiles; +import com.turn.ttorrent.WaitFor; +import com.turn.ttorrent.client.Client; +import com.turn.ttorrent.client.SharedTorrent; +import com.turn.ttorrent.common.Torrent; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +@Test +public class TrackerTest { + + private static final Logger logger = + LoggerFactory.getLogger(TrackerTest.class); + + private static final String TEST_RESOURCES = "src/test/resources"; + private static final int TRACKER_PORT = + Integer.getInteger("ttorrent.test.tracker_port", 6969); + + private Tracker tracker; + private TempFiles tempFiles; + private Torrent testTorrent; + private File testFile; + + @BeforeMethod(alwaysRun = true) + protected void setUp(Method testMethod) throws Exception { + String testName = testMethod.getDeclaringClass().getSimpleName() + "." + testMethod.getName(); + logger.trace("Test starting: " + testName); + tempFiles = new TempFiles(); + startTracker(); + testFile = new File(TEST_RESOURCES + "/parentFiles/file1.jar"); + String creator = String.format("%s (ttorrent)", testName); + testTorrent = Torrent.create(testFile, + this.tracker.getAnnounceUrl().toURI(), creator); + } + + @AfterMethod(alwaysRun = true) + protected void tearDown(Method testMethod) throws Exception { + String testName = testMethod.getDeclaringClass().getSimpleName() + "." + testMethod.getName(); + stopTracker(); + tempFiles.cleanup(); + logger.trace("Test finished: " + testName); + } + + public void test_share_and_download() throws Exception { + final TrackedTorrent tt = this.tracker.announce(loadTorrent()); + assertEquals(0, tt.getPeers().size()); + + Client seeder = createClient(completeTorrent()); + + assertEquals(tt.getHexInfoHash(), seeder.getTorrent().getHexInfoHash()); + + final File downloadDir = tempFiles.createTempDir(); + Client leech = createClient(incompleteTorrent(downloadDir)); + + try { + seeder.share(); + + leech.download(); + + waitForFileInDir(downloadDir, testFile.getName()); + assertFilesEqual(testFile, new File(downloadDir, testFile.getName())); + } finally { + leech.stop(true); + seeder.stop(true); + } + } + + public void tracker_accepts_torrent_from_seeder() throws Exception { + final SharedTorrent torrent = completeTorrent(); + tracker.announce(torrent); + Client seeder = createClient(torrent); + + try { + seeder.share(); + + waitForSeeder(seeder.getTorrent().getInfoHash()); + + Collection trackedTorrents = this.tracker.getTrackedTorrents(); + assertEquals(1, trackedTorrents.size()); + + TrackedTorrent trackedTorrent = trackedTorrents.iterator().next(); + Map peers = trackedTorrent.getPeers(); + assertEquals(1, peers.size()); + assertTrue(peers.values().iterator().next().isCompleted()); // seed + assertEquals(1, trackedTorrent.seeders()); + assertEquals(0, trackedTorrent.leechers()); + } finally { + seeder.stop(true); + } + } + + public void tracker_accepts_torrent_from_leech() throws Exception { + + final File downloadDir = tempFiles.createTempDir(); + final SharedTorrent torrent = incompleteTorrent(downloadDir); + tracker.announce(torrent); + Client leech = createClient(torrent); + + try { + leech.download(); + + waitForPeers(1); + + Collection trackedTorrents = this.tracker.getTrackedTorrents(); + assertEquals(1, trackedTorrents.size()); + + TrackedTorrent trackedTorrent = trackedTorrents.iterator().next(); + Map peers = trackedTorrent.getPeers(); + assertEquals(1, peers.size()); + assertFalse(peers.values().iterator().next().isCompleted()); // leech + assertEquals(0, trackedTorrent.seeders()); + assertEquals(1, trackedTorrent.leechers()); + } finally { + leech.stop(true); + } + } + + public void tracker_accepts_torrent_from_seeder_plus_leech() throws Exception { + assertEquals(0, this.tracker.getTrackedTorrents().size()); + + final SharedTorrent completeTorrent = completeTorrent(); + tracker.announce(completeTorrent); + Client seeder = createClient(completeTorrent); + + final File downloadDir = tempFiles.createTempDir(); + final SharedTorrent incompleteTorrent = incompleteTorrent(downloadDir); + Client leech = createClient(incompleteTorrent); + + try { + seeder.share(); + leech.download(); + + waitForFileInDir(downloadDir, testFile.getName()); + } finally { + seeder.stop(true); + leech.stop(true); + } + } + + public void large_file_download() throws Exception { + + File tempFile = tempFiles.createTempFile(201 * 1024 * 1024); + + Torrent torrent = Torrent.create(tempFile, this.tracker.getAnnounceUrl().toURI(), "Test"); + File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent"); + torrent.save(torrentFile); + tracker.announce(torrent); + + Client seeder = createClient(SharedTorrent.fromFile(torrentFile, tempFile.getParentFile())); + + final File downloadDir = tempFiles.createTempDir(); + Client leech = createClient(SharedTorrent.fromFile(torrentFile, downloadDir)); + + try { + seeder.share(); + leech.download(); + + waitForFileInDir(downloadDir, tempFile.getName()); + assertFilesEqual(tempFile, new File(downloadDir, tempFile.getName())); + } finally { + seeder.stop(true); + leech.stop(true); + } + } + + public void test_announce() throws Exception { + assertEquals(0, this.tracker.getTrackedTorrents().size()); + + this.tracker.announce(loadTorrent()); + + assertEquals(1, this.tracker.getTrackedTorrents().size()); + } + + private void waitForSeeder(final byte[] torrentHash) { + new WaitFor() { + @Override + protected boolean condition() { + for (TrackedTorrent tt : TrackerTest.this.tracker.getTrackedTorrents()) { + if (tt.seeders() == 1 && tt.getHexInfoHash().equals(Torrent.byteArrayToHexString(torrentHash))) + return true; + } + + return false; + } + }; + } + + private void waitForPeers(final int numPeers) { + new WaitFor() { + @Override + protected boolean condition() { + for (TrackedTorrent tt : TrackerTest.this.tracker.getTrackedTorrents()) { + if (tt.getPeers().size() == numPeers) return true; + } + + return false; + } + }; + } + + private void waitForFileInDir(final File downloadDir, final String fileName) { + new WaitFor(120 * 1000) { + @Override + protected boolean condition() { + return new File(downloadDir, fileName).isFile(); + } + }; + + assertTrue(new File(downloadDir, fileName).isFile()); + } + + private TrackedTorrent loadTorrent() + throws IOException, NoSuchAlgorithmException { + return new TrackedTorrent(testTorrent); + } + + private void startTracker() throws IOException { + this.tracker = new Tracker(new InetSocketAddress(TRACKER_PORT)); + this.tracker.start(); + } + + private Client createClient(SharedTorrent torrent) + throws IOException, NoSuchAlgorithmException, InterruptedException { + return new Client(InetAddress.getLocalHost(), torrent); + } + + private SharedTorrent completeTorrent() + throws IOException, NoSuchAlgorithmException { + File parentFiles = new File(TEST_RESOURCES + "/parentFiles"); + return new SharedTorrent(testTorrent, parentFiles); + } + + private SharedTorrent incompleteTorrent(File destDir) + throws IOException, NoSuchAlgorithmException { + return new SharedTorrent(testTorrent, destDir); + } + + private void stopTracker() { + this.tracker.stop(); + } + + private void assertFilesEqual(File f1, File f2) throws IOException { + assertEquals(f1.length(), f2.length(), "Files size differs"); + Checksum c1 = FileUtils.checksum(f1, new CRC32()); + Checksum c2 = FileUtils.checksum(f2, new CRC32()); + assertEquals(c1.getValue(), c2.getValue()); + } +} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 000000000..5fe42d854 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,9 @@ +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=DEBUG, A1 + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n diff --git a/src/test/resources/parentFiles/file1.jar b/src/test/resources/parentFiles/file1.jar new file mode 100644 index 000000000..add257700 Binary files /dev/null and b/src/test/resources/parentFiles/file1.jar differ diff --git a/src/test/resources/parentFiles/file2.jar b/src/test/resources/parentFiles/file2.jar new file mode 100644 index 000000000..95d8e2163 Binary files /dev/null and b/src/test/resources/parentFiles/file2.jar differ diff --git a/src/test/resources/torrents/file1.jar.torrent b/src/test/resources/torrents/file1.jar.torrent new file mode 100644 index 000000000..e169744ea Binary files /dev/null and b/src/test/resources/torrents/file1.jar.torrent differ diff --git a/src/test/resources/torrents/file2.jar.torrent b/src/test/resources/torrents/file2.jar.torrent new file mode 100644 index 000000000..85d3a3aee Binary files /dev/null and b/src/test/resources/torrents/file2.jar.torrent differ