changeset 202:706bcc7cfee4

Basic test for HgIncomingCommand. Fix RepositoryComparator for cases when whole repository is unknown. Respect freshly initialized (empty) repositories in general.
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 26 Apr 2011 02:50:06 +0200
parents a736f42ed75b
children 66fd2c73c56f
files design.txt src/org/tmatesoft/hg/core/HgCloneCommand.java src/org/tmatesoft/hg/core/HgIncomingCommand.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/internal/RepositoryComparator.java src/org/tmatesoft/hg/internal/RevlogStream.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java src/org/tmatesoft/hg/repo/HgRepository.java test/org/tmatesoft/hg/test/Configuration.java test/org/tmatesoft/hg/test/OutputParser.java test/org/tmatesoft/hg/test/TestClone.java test/org/tmatesoft/hg/test/TestHistory.java test/org/tmatesoft/hg/test/TestIncoming.java
diffstat 13 files changed, 304 insertions(+), 46 deletions(-) [+]
line wrap: on
line diff
--- a/design.txt	Thu Apr 21 19:16:45 2011 +0200
+++ b/design.txt	Tue Apr 26 02:50:06 2011 +0200
@@ -118,3 +118,5 @@
 
 ExecHelper('cmd', OutputParser()).run(). StatusOutputParser, LogOutputParser extends OutputParser. construct java result similar to that of cmd, compare results
 
+Need better MethodRule than ErrorCollector for tests run as java app (to print not only MultipleFailureException, but distinct errors)
+Also consider using ExternalResource and TemporaryFolder rules. 
--- a/src/org/tmatesoft/hg/core/HgCloneCommand.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/core/HgCloneCommand.java	Tue Apr 26 02:50:06 2011 +0200
@@ -123,20 +123,17 @@
 		private final ArrayList<Nodeid> revisionSequence = new ArrayList<Nodeid>(); // last visited nodes first
 
 		private final LinkedList<String> fncacheFiles = new LinkedList<String>();
+		private Internals implHelper;
 
 		public WriteDownMate(File destDir) {
 			hgDir = new File(destDir, ".hg");
-			Internals i = new Internals();
-			i.setStorageConfig(1, STORE | FNCACHE | DOTENCODE);
-			storagePathHelper = i.buildDataFilesHelper();
+			implHelper = new Internals();
+			implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE);
+			storagePathHelper = implHelper.buildDataFilesHelper();
 		}
 
 		public void initEmptyRepository() throws IOException {
-			hgDir.mkdir();
-			FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires"));
-			requiresFile.write("revlogv1\nstore\nfncache\ndotencode\n".getBytes());
-			requiresFile.close();
-			new File(hgDir, "store").mkdir(); // with that, hg verify says ok.
+			implHelper.initEmptyRepository(hgDir);
 		}
 
 		public void complete() throws IOException {
--- a/src/org/tmatesoft/hg/core/HgIncomingCommand.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/core/HgIncomingCommand.java	Tue Apr 26 02:50:06 2011 +0200
@@ -18,6 +18,8 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -43,6 +45,7 @@
 
 	private final HgRepository localRepo;
 	private HgRemoteRepository remoteRepo;
+	@SuppressWarnings("unused")
 	private boolean includeSubrepo;
 	private RepositoryComparator comparator;
 	private List<BranchChain> missingBranches;
@@ -105,8 +108,10 @@
 		RepositoryComparator repoCompare = getComparator(context);
 		for (BranchChain bc : getMissingBranches(context)) {
 			List<Nodeid> missing = repoCompare.visitBranches(bc);
-			assert bc.branchRoot.equals(missing.get(0)); 
-			missing.remove(0);
+			HashSet<Nodeid> common = new HashSet<Nodeid>(); // ordering is irrelevant  
+			repoCompare.collectKnownRoots(bc, common);
+			// missing could only start with common elements. Once non-common, rest is just distinct branch revision trails.
+			for (Iterator<Nodeid> it = missing.iterator(); it.hasNext() && common.contains(it.next()); it.remove()) ; 
 			result.addAll(missing);
 		}
 		ArrayList<Nodeid> rv = new ArrayList<Nodeid>(result);
@@ -124,10 +129,10 @@
 			throw new IllegalArgumentException("Delegate can't be null");
 		}
 		final List<Nodeid> common = getCommon(handler);
-		HgBundle changegroup = remoteRepo.getChanges(new LinkedList<Nodeid>(common));
+		HgBundle changegroup = remoteRepo.getChanges(common);
 		try {
 			changegroup.changes(localRepo, new HgChangelog.Inspector() {
-				private int localIndex;
+				private int localIndex = -1; // in case we start with empty repo and localIndex would not get initialized in regular way
 				private final HgChangelog.ParentWalker parentHelper;
 				private final ChangesetTransformer transformer;
 				private final HgChangelog changelog;
@@ -161,7 +166,7 @@
 		}
 		if (comparator == null) {
 			comparator = new RepositoryComparator(getParentHelper(), remoteRepo);
-			comparator.compare(context);
+//			comparator.compare(context); // XXX meanwhile we use distinct path to calculate common  
 		}
 		return comparator;
 	}
@@ -182,11 +187,13 @@
 	}
 
 	private List<Nodeid> getCommon(Object context) throws HgException, CancelledException {
+//		return getComparator(context).getCommon();
 		final LinkedHashSet<Nodeid> common = new LinkedHashSet<Nodeid>();
 		// XXX common can be obtained from repoCompare, but at the moment it would almost duplicate work of calculateMissingBranches
 		// once I refactor latter, common shall be taken from repoCompare.
+		RepositoryComparator repoCompare = getComparator(context);
 		for (BranchChain bc : getMissingBranches(context)) {
-			common.add(bc.branchRoot);
+			repoCompare.collectKnownRoots(bc, common);
 		}
 		return new LinkedList<Nodeid>(common);
 	}
--- a/src/org/tmatesoft/hg/internal/Internals.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Tue Apr 26 02:50:06 2011 +0200
@@ -18,6 +18,9 @@
 
 import static org.tmatesoft.hg.internal.RequiresFile.*;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -85,4 +88,24 @@
 		}
 		return filterFactories;
 	}
+	
+	public void initEmptyRepository(File hgDir) throws IOException {
+		hgDir.mkdir();
+		FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires"));
+		StringBuilder sb = new StringBuilder(40);
+		sb.append("revlogv1\n");
+		if ((requiresFlags & STORE) != 0) {
+			sb.append("store\n");
+		}
+		if ((requiresFlags & FNCACHE) != 0) {
+			sb.append("fncache\n");
+		}
+		if ((requiresFlags & DOTENCODE) != 0) {
+			sb.append("dotencode\n");
+		}
+		requiresFile.write(sb.toString().getBytes());
+		requiresFile.close();
+		new File(hgDir, "store").mkdir(); // with that, hg verify says ok.
+	}
+
 }
--- a/src/org/tmatesoft/hg/internal/RepositoryComparator.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/internal/RepositoryComparator.java	Tue Apr 26 02:50:06 2011 +0200
@@ -27,6 +27,7 @@
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 
 import org.tmatesoft.hg.core.HgBadStateException;
 import org.tmatesoft.hg.core.HgException;
@@ -237,14 +238,23 @@
 				} else {
 					chainElement.branchRoot = rb.root;
 					// dig deeper in the history, if necessary
-					if (!NULL.equals(rb.p1) && !localRepo.knownNode(rb.p1)) {
+					boolean hasP1 = !NULL.equals(rb.p1), hasP2 = !NULL.equals(rb.p2);  
+					if (hasP1 && !localRepo.knownNode(rb.p1)) {
 						toQuery.add(rb.p1);
 						head2chain.put(rb.p1, chainElement.p1 = new BranchChain(rb.p1));
 					}
-					if (!NULL.equals(rb.p2) && !localRepo.knownNode(rb.p2)) {
+					if (hasP2 && !localRepo.knownNode(rb.p2)) {
 						toQuery.add(rb.p2);
 						head2chain.put(rb.p2, chainElement.p2 = new BranchChain(rb.p2));
 					}
+					if (!hasP1 && !hasP2) {
+						// special case, when we do incoming against blank repository, chainElement.branchRoot
+						// is first unknown element (revision 0). We need to add another fake BranchChain
+						// to fill the promise that terminal BranchChain has branchRoot that is known both locally and remotely
+						BranchChain fake = new BranchChain(NULL);
+						fake.branchRoot = NULL;
+						chainElement.p1 = chainElement.p2 = fake;
+					}
 				}
 			}
 		}
@@ -291,6 +301,11 @@
 		return branches2load;
 	}
 
+	// root and head (and all between) are unknown for each chain element but last (terminal), which has known root (revision
+	// known to be locally and at remote server
+	// alternative would be to keep only unknown elements (so that promise of calculateMissingBranches would be 100% true), but that 
+	// seems to complicate the method, while being useful only for the case when we ask incoming for an empty repository (i.e.
+	// where branch chain return all nodes, -1..tip.
 	public static final class BranchChain {
 		// when we construct a chain, we know head which is missing locally, hence init it right away.
 		// as for root (branch unknown start), we might happen to have one locally, and need further digging to find out right branch start  
@@ -307,9 +322,15 @@
 			branchHead = head;
 		}
 		public boolean isTerminal() {
-			return p1 == null || p2 == null;
+			return p1 == null && p2 == null; // either can be null, see comment above. Terminal is only when no way to descent
 		}
 		
+		// true when this BranchChain is a branch that spans up to very start of the repository
+		// Thus, the only common revision is NULL, recorded in a fake BranchChain object shared between p1 and p2
+		/*package-local*/ boolean isRepoStart() {
+			return p1 == p2 && p1 != null && p1.branchHead == p1.branchRoot && NULL.equals(p1.branchHead);
+		}
+
 		@Override
 		public String toString() {
 			return String.format("BranchChain [%s, %s]", branchRoot, branchHead);
@@ -460,7 +481,7 @@
 			return Collections.emptyList();
 		}
 		List<Nodeid> mine = completeBranch(bc.branchRoot, bc.branchHead);
-		if (bc.isTerminal()) {
+		if (bc.isTerminal() || bc.isRepoStart()) {
 			return mine;
 		}
 		List<Nodeid> parentBranch1 = visitBranches(bc.p1);
@@ -495,4 +516,18 @@
 		return rv;
 	}
 
+	public void collectKnownRoots(BranchChain bc, Set<Nodeid> result) {
+		if (bc == null) {
+			return;
+		}
+		if (bc.isTerminal()) {
+			result.add(bc.branchRoot);
+			return;
+		}
+		if (bc.isRepoStart()) {
+			return;
+		}
+		collectKnownRoots(bc.p1, result);
+		collectKnownRoots(bc.p2, result);
+	}
 }
--- a/src/org/tmatesoft/hg/internal/RevlogStream.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/internal/RevlogStream.java	Tue Apr 26 02:50:06 2011 +0200
@@ -319,6 +319,11 @@
 		ArrayList<Integer> resOffsets = new ArrayList<Integer>();
 		DataAccess da = getIndexStream();
 		try {
+			if (da.isEmpty()) {
+				// do not fail with exception if stream is empty, it's likely intentional
+				baseRevisions = new int[0];
+				return;
+			}
 			int versionField = da.readInt();
 			da.readInt(); // just to skip next 4 bytes of offset + flags
 			final int INLINEDATA = 1 << 16;
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Tue Apr 26 02:50:06 2011 +0200
@@ -303,6 +303,9 @@
 	}
 
 	/*
+	 * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when
+	 * no common elements found, which in turn means we need to query changes starting with NULL nodeid.
+	 * 
 	 * WireProtocol wiki: roots = a list of the latest nodes on every service side changeset branch that both the client and server know about.
 	 * 
 	 * Perhaps, shall be named 'changegroup'
@@ -316,9 +319,10 @@
 	 * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat)
 	 */
 	public HgBundle getChanges(List<Nodeid> roots) throws HgException {
-		StringBuilder sb = new StringBuilder(20 + roots.size() * 41);
+		List<Nodeid> _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots;
+		StringBuilder sb = new StringBuilder(20 + _roots.size() * 41);
 		sb.append("roots=");
-		for (Nodeid n : roots) {
+		for (Nodeid n : _roots) {
 			sb.append(n.toString());
 			sb.append('+');
 		}
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Tue Apr 26 02:50:06 2011 +0200
@@ -124,7 +124,7 @@
 	public HgChangelog getChangelog() {
 		if (this.changelog == null) {
 			String storagePath = repoPathHelper.rewrite("00changelog.i");
-			RevlogStream content = resolve(Path.create(storagePath));
+			RevlogStream content = resolve(Path.create(storagePath), true);
 			this.changelog = new HgChangelog(this, content);
 		}
 		return this.changelog;
@@ -132,7 +132,7 @@
 	
 	public HgManifest getManifest() {
 		if (this.manifest == null) {
-			RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")));
+			RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")), true);
 			this.manifest = new HgManifest(this, content);
 		}
 		return this.manifest;
@@ -154,7 +154,7 @@
 	public HgDataFile getFileNode(String path) {
 		String nPath = normalizePath.rewrite(path);
 		String storagePath = dataPathHelper.rewrite(nPath);
-		RevlogStream content = resolve(Path.create(storagePath));
+		RevlogStream content = resolve(Path.create(storagePath), false);
 		Path p = Path.create(nPath);
 		if (content == null) {
 			return new HgDataFile(this, p);
@@ -164,7 +164,7 @@
 
 	public HgDataFile getFileNode(Path path) {
 		String storagePath = dataPathHelper.rewrite(path.toString());
-		RevlogStream content = resolve(Path.create(storagePath));
+		RevlogStream content = resolve(Path.create(storagePath), false);
 		// XXX no content when no file? or HgDataFile.exists() to detect that?
 		if (content == null) {
 			return new HgDataFile(this, path);
@@ -220,7 +220,7 @@
 	 * Perhaps, should be separate interface, like ContentLookup
 	 * path - repository storage path (i.e. one usually with .i or .d)
 	 */
-	/*package-local*/ RevlogStream resolve(Path path) {
+	/*package-local*/ RevlogStream resolve(Path path, boolean shallFakeNonExistent) {
 		final SoftReference<RevlogStream> ref = streamsCache.get(path);
 		RevlogStream cached = ref == null ? null : ref.get();
 		if (cached != null) {
@@ -231,6 +231,16 @@
 			RevlogStream s = new RevlogStream(dataAccess, f);
 			streamsCache.put(path, new SoftReference<RevlogStream>(s));
 			return s;
+		} else {
+			if (shallFakeNonExistent) {
+				try {
+					File fake = File.createTempFile(f.getName(), null);
+					fake.deleteOnExit();
+					return new RevlogStream(dataAccess, fake);
+				} catch (IOException ex) {
+					ex.printStackTrace(); // FIXME report in debug
+				}
+			}
 		}
 		return null; // XXX empty stream instead?
 	}
--- a/test/org/tmatesoft/hg/test/Configuration.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/test/org/tmatesoft/hg/test/Configuration.java	Tue Apr 26 02:50:06 2011 +0200
@@ -19,6 +19,8 @@
 import static org.junit.Assert.*;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -36,6 +38,8 @@
 	private static Configuration inst;
 	private final File root;
 	private final HgLookup lookup;
+	private File tempDir;
+	private List<String> remoteServers;
 	
 	private Configuration(File reposRoot) {
 		root = reposRoot;
@@ -65,7 +69,27 @@
 		return rv;
 	}
 
+	// easy override for manual test runs
+	public void remoteServers(String... keys) {
+		remoteServers = Arrays.asList(keys);
+	}
+
 	public List<HgRemoteRepository> allRemote() throws Exception {
-		return Collections.singletonList(lookup.detectRemote("hg4j-gc", null));
+		if (remoteServers == null) {
+			remoteServers = Collections.singletonList("hg4j-gc"); // just a default
+		}
+		ArrayList<HgRemoteRepository> rv = new ArrayList<HgRemoteRepository>(remoteServers.size());
+		for (String key : remoteServers) {
+			rv.add(lookup.detectRemote(key, null));
+		}
+		return rv;
+	}
+
+	public File getTempDir() {
+		if (tempDir == null) {
+			String td = System.getProperty("hg4j.tests.tmpdir", System.getProperty("java.io.tmpdir"));
+			tempDir = new File(td);
+		}
+		return tempDir;
 	}
 }
--- a/test/org/tmatesoft/hg/test/OutputParser.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/test/org/tmatesoft/hg/test/OutputParser.java	Tue Apr 26 02:50:06 2011 +0200
@@ -24,4 +24,10 @@
 public interface OutputParser {
 
 	public void parse(CharSequence seq);
+
+	public class Stub implements OutputParser {
+		public void parse(CharSequence seq) {
+			// no-op
+		}
+	}
 }
--- a/test/org/tmatesoft/hg/test/TestClone.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/test/org/tmatesoft/hg/test/TestClone.java	Tue Apr 26 02:50:06 2011 +0200
@@ -22,8 +22,7 @@
 import java.util.LinkedList;
 import java.util.List;
 
-import junit.framework.Assert;
-
+import org.hamcrest.CoreMatchers;
 import org.junit.Rule;
 import org.tmatesoft.hg.core.HgCloneCommand;
 import org.tmatesoft.hg.repo.HgRemoteRepository;
@@ -38,9 +37,10 @@
 	@Rule
 	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();
 
-	public static void main(String[] args) throws Exception {
+	public static void main(String[] args) throws Throwable {
 		TestClone t = new TestClone();
 		t.testSimpleClone();
+		t.errorCollector.verify();
 	}
 
 	public TestClone() {
@@ -48,7 +48,7 @@
 	
 	public void testSimpleClone() throws Exception {
 		int x = 0;
-		final File tempDir = new File(System.getProperty("java.io.tmpdir"));
+		final File tempDir = Configuration.get().getTempDir();
 		for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) {
 			HgCloneCommand cmd = new HgCloneCommand();
 			cmd.source(hgRemote);
@@ -63,21 +63,16 @@
 	}
 
 	private void verify(HgRemoteRepository hgRemote, File dest) throws Exception {
-		OutputParser noop = new OutputParser() {
-			public void parse(CharSequence seq) {
-				// no-op
-			}
-		};
-		ExecHelper eh = new ExecHelper(noop, dest);
+		ExecHelper eh = new ExecHelper(new OutputParser.Stub(), dest);
 		eh.run("hg", "verify");
-		Assert.assertEquals(0, eh.getExitValue());
+		errorCollector.checkThat("Verify", eh.getExitValue(), CoreMatchers.equalTo(0));
 		eh.run("hg", "out", hgRemote.getLocation());
-		Assert.assertEquals(1, eh.getExitValue());
+		errorCollector.checkThat("Outgoing", eh.getExitValue(), CoreMatchers.equalTo(1));
 		eh.run("hg", "in", hgRemote.getLocation());
-		Assert.assertEquals(1, eh.getExitValue());
+		errorCollector.checkThat("Incoming", eh.getExitValue(), CoreMatchers.equalTo(1));
 	}
 
-	private static void rmdir(File dest) throws IOException {
+	static void rmdir(File dest) throws IOException {
 		LinkedList<File> queue = new LinkedList<File>();
 		queue.addAll(Arrays.asList(dest.listFiles()));
 		while (!queue.isEmpty()) {
--- a/test/org/tmatesoft/hg/test/TestHistory.java	Thu Apr 21 19:16:45 2011 +0200
+++ b/test/org/tmatesoft/hg/test/TestHistory.java	Tue Apr 26 02:50:06 2011 +0200
@@ -20,6 +20,7 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertTrue;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
@@ -122,23 +123,35 @@
 		}
 	}
 
-	private void report(String what, List<HgChangeset> r, boolean reverseConsoleResults) {
+	private void report(String what, List<HgChangeset> r, boolean reverseConsoleResult) {
 		final List<Record> consoleResult = changelogParser.getResult();
-		if (reverseConsoleResults) {
+		report(what, r, consoleResult, reverseConsoleResult, errorCollector);
+	}
+	
+	static void report(String what, List<HgChangeset> hg4jResult, List<Record> consoleResult, boolean reverseConsoleResult, ErrorCollectorExt errorCollector) {
+		consoleResult = new ArrayList<Record>(consoleResult); // need a copy in case callee would use result again
+		if (reverseConsoleResult) {
 			Collections.reverse(consoleResult);
 		}
+		errorCollector.checkThat(what + ". Number of changeset reported didn't match", consoleResult.size(), equalTo(hg4jResult.size()));
 		Iterator<Record> consoleResultItr = consoleResult.iterator();
-		for (HgChangeset cs : r) {
+		for (HgChangeset cs : hg4jResult) {
+			if (!consoleResultItr.hasNext()) {
+				errorCollector.addError(new AssertionError("Ran out of console results while there are still hg4j results"));
+				break;
+			}
 			Record cr = consoleResultItr.next();
 			int x = cs.getRevision() == cr.changesetIndex ? 0x1 : 0;
 			x |= cs.getDate().equals(cr.date) ? 0x2 : 0;
 			x |= cs.getNodeid().toString().equals(cr.changesetNodeid) ? 0x4 : 0;
 			x |= cs.getUser().equals(cr.user) ? 0x8 : 0;
-			x |= cs.getComment().equals(cr.description) ? 0x10 : 0;
-			errorCollector.checkThat(String.format(what + ". Error in %d hg4j rev comparing to %d cmdline's.", cs.getRevision(), cr.changesetIndex), x, equalTo(0x1f));
+			// need to do trim() on comment because command-line template does, and there are
+			// repositories that have couple of newlines in the end of the comment (e.g. hello sample repo from the book) 
+			x |= cs.getComment().trim().equals(cr.description) ? 0x10 : 0;
+			errorCollector.checkThat(String.format(what + ". Mismatch (0x%x) in %d hg4j rev comparing to %d cmdline's.", x, cs.getRevision(), cr.changesetIndex), x, equalTo(0x1f));
 			consoleResultItr.remove();
 		}
-		errorCollector.checkThat(what + ". Insufficient results from Java ", consoleResultItr.hasNext(), equalTo(false));
+		errorCollector.checkThat(what + ". Unprocessed results in console left (insufficient from hg4j)", consoleResultItr.hasNext(), equalTo(false));
 	}
 
 	public void testPerformance() throws Exception {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/org/tmatesoft/hg/test/TestIncoming.java	Tue Apr 26 02:50:06 2011 +0200
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.tmatesoft.hg.internal.RequiresFile.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.tmatesoft.hg.core.HgChangeset;
+import org.tmatesoft.hg.core.HgIncomingCommand;
+import org.tmatesoft.hg.core.HgLogCommand;
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.Internals;
+import org.tmatesoft.hg.repo.HgLookup;
+import org.tmatesoft.hg.repo.HgRemoteRepository;
+import org.tmatesoft.hg.repo.HgRepository;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class TestIncoming {
+	
+	@Rule
+	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();
+
+	public static void main(String[] args) throws Throwable {
+		Configuration.get().remoteServers("http://localhost:8000/");
+		TestIncoming t = new TestIncoming();
+		t.testSimple();
+		t.errorCollector.verify();
+	}
+
+	public TestIncoming() {
+//		Configuration.get().remoteServers("http://localhost:8000/");
+	}
+
+	@Test
+	public void testSimple() throws Exception {
+		int x = 0;
+		HgLookup lookup = new HgLookup();
+		for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) {
+			File dest = initEmptyTempRepo("test-incoming-" + x++);
+			HgRepository localRepo = lookup.detect(dest);
+			// Idea:
+			// hg in, hg4j in, compare
+			// hg pull total/2
+			// hg in, hg4j in, compare
+			List<Nodeid> incoming = runAndCompareIncoming(localRepo, hgRemote);
+			Assert.assertTrue("Need remote repository of reasonable size to test incoming command for partially filled case", incoming.size() > 5);
+			//
+			Nodeid median = incoming.get(incoming.size() / 2); 
+			System.out.println("About to pull up to revision " + median.shortNotation());
+			new ExecHelper(new OutputParser.Stub(), dest).run("hg", "pull", "-r", median.toString(), hgRemote.getLocation());
+			//
+			// shall re-read repository to pull up new changes 
+			localRepo = lookup.detect(dest);
+			runAndCompareIncoming(localRepo, hgRemote);
+		}
+	}
+	
+	private List<Nodeid> runAndCompareIncoming(HgRepository localRepo, HgRemoteRepository hgRemote) throws Exception {
+		// need new command instance as subsequence exec[Lite|Full] on the same command would yield same result,
+		// regardless of the pull in between.
+		HgIncomingCommand cmd = new HgIncomingCommand(localRepo);
+		cmd.against(hgRemote);
+		HgLogCommand.CollectHandler collector = new HgLogCommand.CollectHandler();
+		LogOutputParser outParser = new LogOutputParser(true);
+		ExecHelper eh = new ExecHelper(outParser, new File(localRepo.getLocation()));
+		cmd.executeFull(collector);
+		eh.run("hg", "incoming", "--debug", hgRemote.getLocation());
+		List<Nodeid> liteResult = cmd.executeLite(null);
+		report(collector, outParser, liteResult);
+		return liteResult;
+	}
+	
+	private void report(HgLogCommand.CollectHandler collector, LogOutputParser outParser, List<Nodeid> liteResult) {
+		TestHistory.report("hg in - against blank repo", collector.getChanges(), outParser.getResult(), false, errorCollector);
+		//
+		ArrayList<Nodeid> expected = new ArrayList<Nodeid>(outParser.getResult().size());
+		for (LogOutputParser.Record r : outParser.getResult()) {
+			Nodeid nid = Nodeid.fromAscii(r.changesetNodeid);
+			expected.add(nid);
+		}
+		checkNodeids("hg vs execLite:", liteResult, expected, errorCollector);
+		//
+		expected = new ArrayList<Nodeid>(outParser.getResult().size());
+		for (HgChangeset cs : collector.getChanges()) {
+			expected.add(cs.getNodeid());
+		}
+		checkNodeids("execFull vs execLite:", liteResult, expected, errorCollector);
+	}
+	
+	static void checkNodeids(String what, List<Nodeid> liteResult, List<Nodeid> expected, ErrorCollectorExt errorCollector) {
+		HashSet<Nodeid> set = new HashSet<Nodeid>(liteResult);
+		for (Nodeid nid : expected) {
+			boolean removed = set.remove(nid);
+			errorCollector.checkThat(what + " Missing " +  nid.shortNotation() + " in HgIncomingCommand.execLite result", removed, equalTo(true));
+		}
+		errorCollector.checkThat(what + " Superfluous cset reported by HgIncomingCommand.execLite", set.isEmpty(), equalTo(true));
+	}
+
+	static File initEmptyTempRepo(String dirName) throws IOException {
+		File dest = new File(Configuration.get().getTempDir(), dirName);
+		if (dest.exists()) {
+			TestClone.rmdir(dest);
+		}
+		dest.mkdirs();
+		Internals implHelper = new Internals();
+		implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE);
+		implHelper.initEmptyRepository(new File(dest, ".hg"));
+		return dest;
+	}
+}