@@ -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