view test/org/tmatesoft/hg/test/TestHistory.java @ 691:72fc7774b87e

Fix file.isCopy() for blame/annotate. Refactor status and blame to use newly introduced FileHistory helper that builds file rename history
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 02 Aug 2013 23:07:23 +0200
parents 6526d8adbc0f
children
line wrap: on
line source
/*
 * Copyright (c) 2011-2013 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.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld;
import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.tmatesoft.hg.core.HgCallbackTargetException;
import org.tmatesoft.hg.core.HgChangeset;
import org.tmatesoft.hg.core.HgChangesetHandler;
import org.tmatesoft.hg.core.HgChangesetTreeHandler;
import org.tmatesoft.hg.core.HgFileRenameHandlerMixin;
import org.tmatesoft.hg.core.HgFileRevision;
import org.tmatesoft.hg.core.HgIterateDirection;
import org.tmatesoft.hg.core.HgLogCommand;
import org.tmatesoft.hg.core.HgLogCommand.CollectHandler;
import org.tmatesoft.hg.core.Nodeid;
import org.tmatesoft.hg.internal.AdapterPlug;
import org.tmatesoft.hg.repo.HgLookup;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgRuntimeException;
import org.tmatesoft.hg.test.LogOutputParser.Record;
import org.tmatesoft.hg.util.Adaptable;
import org.tmatesoft.hg.util.CancelSupport;
import org.tmatesoft.hg.util.CancelledException;
import org.tmatesoft.hg.util.Pair;
import org.tmatesoft.hg.util.Path;


/**
 *
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public class TestHistory {

	@Rule
	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();

	private HgRepository repo;
	private final ExecHelper eh;
	private LogOutputParser changelogParser;
	
	public static void main(String[] args) throws Throwable {
		TestHistory th = new TestHistory(new HgLookup().detectFromWorkingDir());
		th.testCompleteLog();
		th.testFollowHistory();
		th.errorCollector.verify();
//		th.testPerformance();
		th.testOriginalTestLogRepo();
		th.testUsernames();
		th.testBranches();
		//
		th.errorCollector.verify();
	}
	
	public TestHistory() {
		eh = new ExecHelper(changelogParser = new LogOutputParser(true), null);
	}

	private TestHistory(HgRepository hgRepo) {
		this();
		repo = hgRepo;
		eh.cwd(repo.getWorkingDir());
	}
	
	@Test
	public void testCompleteLog() throws Exception {
		if (repo == null) {
			repo = Configuration.get().own();
			eh.cwd(repo.getWorkingDir());
		}
		changelogParser.reset();
		eh.run("hg", "log", "--debug");
		List<HgChangeset> r = new HgLogCommand(repo).execute();
		report("hg log - COMPLETE REPO HISTORY", r, true);
		
		r = new HgLogCommand(repo).order(NewToOld).execute();
		report("hg log - COMPLETE REPO HISTORY, FROM NEW TO OLD", r, false);
	}
	
	@Test
	public void testFollowHistory() throws Exception {
		if (repo == null) {
			repo = Configuration.get().own();
			eh.cwd(repo.getWorkingDir());
		}
		final Path f = Path.create("cmdline/org/tmatesoft/hg/console/Remote.java");
		assertTrue(repo.getFileNode(f).exists());
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "--follow", f.toString());
		
		CollectWithRenameHandler h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(f, true).execute(h);
		errorCollector.assertEquals(1, h.rh.renames.size());
		HgFileRevision from = h.rh.renames.get(0).first();
		boolean fromMatched = "src/com/tmate/hgkit/console/Remote.java".equals(from.getPath().toString());
		String what = "hg log - FOLLOW FILE HISTORY";
		errorCollector.checkThat(what + "#copyReported ", h.rh.copyReported, is(true));
		errorCollector.checkThat(what + "#copyFromMatched", fromMatched, is(true));
		//
		// cmdline always gives in changesets in order from newest (bigger rev number) to oldest.
		// LogCommand does other way round, from oldest to newest, follewed by revisions of copy source, if any
		// (apparently older than oldest of the copy target). Hence need to sort Java results according to rev numbers
		final LinkedList<HgChangeset> sorted = new LinkedList<HgChangeset>(h.getChanges());
		Collections.sort(sorted, new Comparator<HgChangeset>() {
			public int compare(HgChangeset cs1, HgChangeset cs2) {
				return cs1.getRevisionIndex() < cs2.getRevisionIndex() ? 1 : -1;
			}
		});
		report(what, sorted, false);
	}
	
	@Test
	public void testChangesetTree() throws Exception {
		repo = Configuration.get().find("branches-1");
		final String fname = "file1";
		assertTrue("[sanity]", repo.getFileNode(fname).exists());
		eh.run("hg", "log", "--debug", fname, "--cwd", repo.getLocation());
		
		TreeCollectHandler h = new TreeCollectHandler(false);
		new HgLogCommand(repo).file(fname, false).execute(h);
		// since we use TreeCollectHandler with natural order (older to newer), shall reverse console result in report()
		report("execute with HgChangesetTreeHandler(follow == false)", h.getResult(), true);
	}
	
	/**
	 * Few tests  to check newly introduced followAncestry parameter to HgLogCommand:
	 * followRename: true,	followAncestry: false
	 * followRename: false,	followAncestry: true
	 * followRename: true,	followAncestry: true
	 * Perhaps, shall be merged with {@link #testFollowHistory()}
	 */
	@Test
	public void testFollowRenamesNotAncestry() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname1 = "file1_a";
		final String fname2 = "file1_b";
		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
		// no --follow, but two names we know have been the same file (fname1 renamed to fname2)
		// sequentially gives follow rename semantics without ancestry
		eh.run("hg", "log", "--debug", fname2, fname1, "--cwd", repo.getLocation());
		
		CollectWithRenameHandler h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(fname2, true, false).execute(h);
		errorCollector.assertEquals(1, h.rh.renames.size());
		Pair<HgFileRevision, HgFileRevision> rename = h.rh.renames.get(0);
		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
		// Ensure rename info came in the right moment
		errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size());
		// Command iterates old to new, rename comes after last fname1 revision. Since we don't follow
		// ancestry, it's the very last revision in fname1 history
		String lastRevOfFname1 = "369c0882d477c11424a62eb4b791e86d1d4b6769";
		errorCollector.assertEquals(lastRevOfFname1, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
		report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), true);
		//
		// Direction
		h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(fname2, true, false).order(NewToOld).execute(h);
		// Identical rename shall be reported, at the same moment 
		errorCollector.assertEquals(1, h.rh.renames.size());
		rename = h.rh.renames.get(0);
		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
		errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size());
		// new to old, recently reported would be the very first revision fname2 pops up
		String firstRevOfFname2 = "27e7a69373b74d42e75f3211e56510ff17d01370";
		errorCollector.assertEquals(firstRevOfFname2, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
		report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), false);
		//
		// TreeChangeHandler - in #testChangesetTreeFollowRenamesNotAncestry
	}
	
	@Test
	public void testChangesetTreeFollowRenamesNotAncestry() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname1 = "file1_a";
		final String fname2 = "file1_b";
		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
		// no --follow, but two names we know have been the same file (fname1 renamed to fname2)
		// sequentially gives follow rename semantics without ancestry
		eh.run("hg", "log", "--debug", fname2, fname1, "--cwd", repo.getLocation());
		
		TreeCollectHandler h = new TreeCollectHandler(true);
		RenameCollector rh = new RenameCollector(h);
		// can't check that prev revision is in parent because there are forks in
		// file history (e.g. rev2 and rev3 (that comes next) both have rev0 as their parent
		// and followAncestry is false
		// h.checkPrevInParents = true; 
		new HgLogCommand(repo).file(fname2, true, false).execute(h);
		errorCollector.assertEquals(1, rh.renames.size());
		Pair<HgFileRevision, HgFileRevision> rename = rh.renames.get(0);
		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
		report("HgChangesetTreeHandler(renames: true, ancestry:false)", h.getResult(), false);
		
		// Direction
		h = new TreeCollectHandler(false);
		rh = new RenameCollector(h);
		// h.checkPrevInChildren = true; see above
		new HgLogCommand(repo).file(fname2, true, false).order(NewToOld).execute(h);
		errorCollector.assertEquals(1, rh.renames.size());
		rename = rh.renames.get(0);
		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
		report("HgChangesetTreeHandler(renames: true, ancestry:false)", h.getResult(), false);
	}
		
	@Test
	public void testFollowAncestryNotRenames() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname2 = "file1_b";
		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
		final List<Record> fname2Follow = getAncestryWithoutRenamesFromCmdline(fname2);
		
		CollectWithRenameHandler h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(fname2, false, true).execute(h);
		// renames are reported regardless of followRenames parameter, but 
		// solely based on HgFileRenameHandlerMixin
		errorCollector.assertEquals(1, h.rh.renames.size());
		report("HgChangesetHandler(renames: false, ancestry:true)", h.getChanges(), fname2Follow, true, errorCollector);
		//
		// Direction
		h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(fname2, false, true).order(NewToOld).execute(h);
		report("HgChangesetHandler(renames: false, ancestry:true)", h.getChanges(), fname2Follow, false/*!!!*/, errorCollector);
		//
		// TreeChangeHandler - in #testChangesetTreeFollowAncestryNotRenames
	}

	@Test
	public void testChangesetTreeFollowAncestryNotRenames() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname2 = "file1_b";
		final List<Record> fname2Follow = getAncestryWithoutRenamesFromCmdline(fname2);
		
		TreeCollectHandler h = new TreeCollectHandler(false);
		h.checkPrevInParents = true;
		new HgLogCommand(repo).file(fname2, false, true).execute(h);
		report("HgChangesetTreeHandler(renames: false, ancestry:true)", h.getResult(), fname2Follow, true, errorCollector);
		
		// Direction
		h = new TreeCollectHandler(false);
		h.checkPrevInChildren = true;
		new HgLogCommand(repo).file(fname2, false, true).order(NewToOld).execute(h);
		report("HgChangesetTreeHandler(renames: false, ancestry:true)", h.getResult(), fname2Follow, false, errorCollector);
	}

	
	private List<Record> getAncestryWithoutRenamesFromCmdline(String fname2) throws Exception {
		// to get "followed" history of fname2 only (without fname1 origin),
		// get the complete history and keep there only elements that match fname2 own history 
		eh.run("hg", "log", "--debug", "--follow", fname2, "--cwd", repo.getLocation());
		final List<Record> fname2Follow = new LinkedList<LogOutputParser.Record>(changelogParser.getResult());
		changelogParser.reset();
		eh.run("hg", "log", "--debug", fname2, "--cwd", repo.getLocation());
		// fname2Follow.retainAll(changelogParser.getResult());
		for (Iterator<Record> it = fname2Follow.iterator(); it.hasNext();) {
			Record r = it.next();
			boolean belongsToSoleFname2History = false;
			for (Record d : changelogParser.getResult()) {
				if (d.changesetIndex == r.changesetIndex) {
					assert d.changesetNodeid.equals(r.changesetNodeid) : "[sanity]";
					belongsToSoleFname2History = true;
					break;
				}
			}
			if (!belongsToSoleFname2History) {
				it.remove();
			}
		}
		return fname2Follow;
	}

	/**
	 * output identical to that of "hg log --follow"
	 */
	@Test
	public void testFollowBothRenameAndAncestry() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname1 = "file1_a";
		final String fname2 = "file1_b";
		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
		eh.run("hg", "log", "--debug", "--follow", fname2, "--cwd", repo.getLocation());
		
		CollectWithRenameHandler h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(fname2, true, true).execute(h);
		errorCollector.assertEquals(1, h.rh.renames.size());
		Pair<HgFileRevision, HgFileRevision> rename = h.rh.renames.get(0);
		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
		// Ensure rename info came in the right moment
		errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size());
		String fname1BranchRevision = "6e668ff2940acb250c8627843f8116166fe5d5cd";
		errorCollector.assertEquals(fname1BranchRevision, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
		// finally, match output
		report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), true);
		//
		// Switch direction and compare, order shall match that from console
		h = new CollectWithRenameHandler();
		new HgLogCommand(repo).file(fname2, true, true).order(NewToOld).execute(h);
		// Identical rename event shall be reported
		errorCollector.assertEquals(1, h.rh.renames.size());
		rename = h.rh.renames.get(0);
		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
		// new to old, recently reported would be the very first revision fname2 pops up
		String firstRevOfFname2 = "27e7a69373b74d42e75f3211e56510ff17d01370";
		errorCollector.assertEquals(firstRevOfFname2, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
		report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), false /*do not reorder console results !!!*/);
		//
		// TreeChangeHandler in #testChangesetTreeFollowRenameAndAncestry
	}
	
	@Test
	public void testChangesetTreeFollowRenameAndAncestry() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname = "file1_b";
		assertTrue("[sanity]", repo.getFileNode(fname).exists());
		eh.run("hg", "log", "--debug", "--follow", fname, "--cwd", repo.getLocation());

		TreeCollectHandler h = new TreeCollectHandler(true);
		RenameCollector rh = new RenameCollector(h);
		h.checkPrevInParents = true;
		new HgLogCommand(repo).file(fname, true, true).execute(h);

		assertEquals(1, h.getAdapterUse(HgFileRenameHandlerMixin.class));
		
		report("execute with HgChangesetTreeHandler(follow == true)", h.getResult(), false);
		
		assertEquals(1, rh.renames.size());
		assertEquals(Path.create(fname), rh.renames.get(0).second().getPath());
	}
	
	/**
	 * Ensure {@link HgFileRenameHandlerMixin} is always notified, even
	 * if followRename is false.
	 * Shall check: 
	 *  both {@link HgLogCommand#execute(HgChangesetHandler)} and {@link HgLogCommand#execute(HgChangesetTreeHandler)}
	 *  and for both iteration directions in each case
	 */
	@Test
	public void testRenameHandlerNotifiedEvenIfNotFollowRename() throws Exception {
		repo = Configuration.get().find("log-follow");
		final String fname1 = "file1_a";
		final String fname2 = "file1_b";
		final String fnameNoRename = "file2";
		assertTrue("[sanity]", repo.getFileNode(fnameNoRename).exists());
		
		// first, check that file without renames doesn't report any accidentally
		CollectWithRenameHandler h1 = new CollectWithRenameHandler();
		HgLogCommand cmd = new HgLogCommand(repo).file(fnameNoRename, false, false);
		cmd.execute(h1);
		errorCollector.assertEquals(0, h1.rh.renames.size());
		TreeCollectHandler h2 = new TreeCollectHandler(false);
		RenameCollector rh = new RenameCollector(h2);
		cmd.execute(h2);
		errorCollector.assertEquals(0, rh.renames.size());
		
		// check default iterate direction
		cmd = new HgLogCommand(repo).file(fname2, false, false);
		cmd.execute(h1 = new CollectWithRenameHandler());
		errorCollector.assertEquals(1, h1.rh.renames.size());
		assertRename(fname1, fname2, h1.rh.renames.get(0));
		
		h2 = new TreeCollectHandler(false);
		rh = new RenameCollector(h2);
		cmd.execute(h2);
		errorCollector.assertEquals(1, rh.renames.size());
		assertRename(fname1, fname2, rh.renames.get(0));
		
		eh.run("hg", "log", "--debug", fname2, "--cwd", repo.getLocation());
		report("HgChangesetHandler+RenameHandler with followRenames = false, default iteration order", h1.getChanges(), true);
		report("HgChangesetTreeHandler+RenameHandler with followRenames = false, default iteration order", h2.getResult(), true);
		
		//
		// Now, check that iteration in opposite direction (new to old)
		// still reports renames (and correct revisions, too)
		cmd.order(HgIterateDirection.NewToOld);
		cmd.execute(h1 = new CollectWithRenameHandler());
		errorCollector.assertEquals(1, h1.rh.renames.size());
		assertRename(fname1, fname2, h1.rh.renames.get(0));
		h2 = new TreeCollectHandler(false);
		rh = new RenameCollector(h2);
		cmd.execute(h2);
		errorCollector.assertEquals(1, rh.renames.size());
		assertRename(fname1, fname2, rh.renames.get(0));
		report("HgChangesetHandler+RenameHandler with followRenames = false, new2old iteration order", h1.getChanges(), false);
		report("HgChangesetTreeHandler+RenameHandler with followRenames = false, new2old iteration order", h2.getResult(), false);
	}

	@Test
	public void testFollowMultipleRenames() throws Exception {
		repo = Configuration.get().find("log-renames");
		String fname = "a";
		eh.run("hg", "log", "--debug", "--follow", fname, "--cwd", repo.getLocation());
		HgLogCommand cmd = new HgLogCommand(repo);
		cmd.file(fname, true, true);
		CollectWithRenameHandler h1;
		//
		cmd.order(OldToNew).execute(h1 = new CollectWithRenameHandler());
		errorCollector.assertEquals(2, h1.rh.renames.size());
		report("Follow a->c->b, old2new:", h1.getChanges(), true);
		//
		cmd.order(NewToOld).execute(h1 = new CollectWithRenameHandler());
		errorCollector.assertEquals(2, h1.rh.renames.size());
		report("Follow a->c->b, new2old:", h1.getChanges(), false);
		//
		//
		TreeCollectHandler h2 = new TreeCollectHandler(false);
		RenameCollector rh = new RenameCollector(h2);
		cmd.order(OldToNew).execute(h2);
		errorCollector.assertEquals(2, rh.renames.size());
		report("Tree. Follow a->c->b, old2new:", h2.getResult(), true);
		//
		h2 = new TreeCollectHandler(false);
		rh = new RenameCollector(h2);
		cmd.order(NewToOld).execute(h2);
		errorCollector.assertEquals(2, rh.renames.size());
		report("Tree. Follow a->c->b, new2old:", h2.getResult(), false);
	}
	
	private void assertRename(String fnameFrom, String fnameTo, Pair<HgFileRevision, HgFileRevision> rename) {
		errorCollector.assertEquals(fnameFrom, rename.first().getPath().toString());
		errorCollector.assertEquals(fnameTo, rename.second().getPath().toString());
	}

	/**
	 * @see TestAuxUtilities#testChangelogCancelSupport()
	 */
	@Test
	public void testLogCommandCancelSupport() throws Exception {
		repo  = Configuration.get().find("branches-1"); // any repo with more revisions
		class BaseCancel extends TestAuxUtilities.CancelAtValue implements HgChangesetHandler {
			BaseCancel(int limit) {
				super(limit);
			}
			public void cset(HgChangeset changeset) throws HgCallbackTargetException {
				nextValue(changeset.getRevisionIndex());
			}
		};
		class ImplementsCancel extends BaseCancel implements CancelSupport {
			ImplementsCancel(int limit) {
				super(limit);
			}
			public void checkCancelled() throws CancelledException {
				cancelImpl.checkCancelled();
			}
		};
		class AdaptsToCancel extends BaseCancel implements Adaptable {
			AdaptsToCancel(int limit) {
				super(limit);
			}
			public <T> T getAdapter(Class<T> adapterClass) {
				if (adapterClass == CancelSupport.class) {
					return adapterClass.cast(cancelImpl);
				}
				return null;
			}
		}

		BaseCancel insp = new ImplementsCancel(3);
		try {
			new HgLogCommand(repo).execute(insp);
			errorCollector.fail("CancelSupport as implemented iface");
		} catch (CancelledException ex) {
			errorCollector.assertEquals("CancelSupport as implemented iface", insp.stopValue, insp.lastSeen);
		}
		insp = new AdaptsToCancel(5);
		try {
			new HgLogCommand(repo).execute(insp);
			errorCollector.fail("Adaptable to CancelSupport");
		} catch (CancelledException ex) { 
			errorCollector.assertEquals("Adaptable to CancelSupport", insp.stopValue, insp.lastSeen);
		}
		insp = new BaseCancel(9);
		try {
			new HgLogCommand(repo).set(insp.cancelImpl).execute(insp);
			errorCollector.fail("cmd#set(CancelSupport)");
		} catch (CancelledException e) {
			errorCollector.assertEquals("cmd#set(CancelSupport)", insp.stopValue, insp.lastSeen);
		}
	}

	private void report(String what, List<HgChangeset> r, boolean reverseConsoleResult) {
		final List<Record> consoleResult = changelogParser.getResult();
		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", hg4jResult.size(), equalTo(consoleResult.size()));
		Iterator<Record> consoleResultItr = consoleResult.iterator();
		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();
			// flags, not separate checkThat() because when lists are large, and do not match,
			// number of failures may slow down test process significantly
			int x = cs.getRevisionIndex() == cr.changesetIndex ? 0x1 : 0;
			x |= cs.getDate().toString().equals(cr.date) ? 0x2 : 0;
			x |= cs.getNodeid().toString().equals(cr.changesetNodeid) ? 0x4 : 0;
			x |= cs.getUser().equals(cr.user) ? 0x8 : 0;
			// 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.getRevisionIndex(), cr.changesetIndex), x, equalTo(0x1f));
			consoleResultItr.remove();
		}
		errorCollector.checkThat(what + ". Unprocessed results in console left (insufficient from hg4j)", consoleResultItr.hasNext(), equalTo(false));
	}

	public void testPerformance() throws Exception {
		final int runs = 10;
		final long start1 = System.currentTimeMillis();
		for (int i = 0; i < runs; i++) {
			changelogParser.reset();
			eh.run("hg", "log", "--debug");
		}
		final long start2 = System.currentTimeMillis();
		for (int i = 0; i < runs; i++) {
			new HgLogCommand(repo).execute();
		}
		final long end = System.currentTimeMillis();
		System.out.printf("'hg log --debug', %d runs: Native client total %d (%d per run), Java client %d (%d)\n", runs, start2-start1, (start2-start1)/runs, end-start2, (end-start2)/runs);
	}

	@Test
	public void testOriginalTestLogRepo() throws Exception {
		// tests fro mercurial distribution, test-log.t
		repo = Configuration.get().find("log-1");
		HgLogCommand cmd = new HgLogCommand(repo);
		// funny enough, but hg log -vf a -R c:\temp\hg\test-log\a doesn't work, while --cwd <same> works fine
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "a", "--cwd", repo.getLocation());
		report("log a", cmd.file("a", false).execute(), true);
		//
		changelogParser.reset();
		// fails with Mercurial 2.2.1, @see http://selenic.com/pipermail/mercurial-devel/2012-February/038249.html
		// and http://www.selenic.com/hg/rev/60101427d618?rev=
		// fix for the test (replacement) is available below  
//		eh.run("hg", "log", "--debug", "-f", "a", "--cwd", repo.getLocation());
//		List<HgChangeset> r = cmd.file("a", true).execute();
//		report("log -f a", r, true);

		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-f", "e", "--cwd", repo.getLocation());
		report("log -f e", cmd.file("e", true).execute(), true);
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "dir/b", "--cwd", repo.getLocation());
		report("log dir/b", cmd.file("dir/b", false).execute(), true);
		//
		changelogParser.reset();
//		
//		Commented out for the same reason as above hg log -f a - newly introduced error message in Mercurial 2.2 
//		when files are not part of the parent revision
//		eh.run("hg", "log", "--debug", "-f", "dir/b", "--cwd", repo.getLocation());
//		report("log -f dir/b", cmd.file("dir/b", true).execute(), false /*#1, below*/);
		/*
		 * #1: false works because presently commands dispatches history of the queried file, and then history
		 * of it's origin. With history comprising of renames only, this effectively gives reversed (newest to oldest) 
		 * order of revisions. 
		 */

		// commented tests from above updated to work in 2.2 - update repo to revision where files are present
		eh.run("hg", "update", "-q", "-r", "2", "--cwd", repo.getLocation());
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-f", "a", "--cwd", repo.getLocation());
		List<HgChangeset> r = cmd.file("a", true).execute();
		report("log -f a", r, true);
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-f", "dir/b", "--cwd", repo.getLocation());
		report("log -f dir/b", cmd.file("dir/b", true).execute(), true);
		//
		// get repo back into clear state, up to the tip
		eh.run("hg", "update", "-q", "--cwd", repo.getLocation());
	}

	@Test
	public void testUsernames() throws Exception {
		repo = Configuration.get().find("log-users");
		final String user1 = "User One <user1@example.org>";
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-u", user1, "--cwd", repo.getLocation());
		report("log -u " + user1, new HgLogCommand(repo).user(user1).execute(), true);
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-u", "user1", "-u", "user2", "--cwd", repo.getLocation());
		report("log -u user1 -u user2", new HgLogCommand(repo).user("user1").user("user2").execute(), true);
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-u", "user3", "--cwd", repo.getLocation());
		report("log -u user3", new HgLogCommand(repo).user("user3").execute(), true);
	}

	@Test
	public void testBranches() throws Exception {
		repo = Configuration.get().find("log-branches");
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-b", "default", "--cwd", repo.getLocation());
		report("log -b default" , new HgLogCommand(repo).branch("default").execute(), true);
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-b", "test", "--cwd", repo.getLocation());
		report("log -b test" , new HgLogCommand(repo).branch("test").execute(), true);
		//
		assertTrue("log -b dummy shall yeild empty result", new HgLogCommand(repo).branch("dummy").execute().isEmpty());
		//
		changelogParser.reset();
		eh.run("hg", "log", "--debug", "-b", "default", "-b", "test", "--cwd", repo.getLocation());
		report("log -b default -b test" , new HgLogCommand(repo).branch("default").branch("test").execute(), true);
	}

	////
	
	private final class TreeCollectHandler extends AdapterPlug implements HgChangesetTreeHandler {
		private final LinkedList<HgChangeset> cmdResult = new LinkedList<HgChangeset>();
		private final boolean reverseResult;
		boolean checkPrevInChildren = false; // true when iterating new to old
		boolean checkPrevInParents = false; // true when iterating old to new
		
		public TreeCollectHandler(boolean _reverseResult) {
			this.reverseResult = _reverseResult;
		}

		public List<HgChangeset> getResult() {
			return cmdResult;
		}
		

		public void treeElement(TreeElement entry) throws HgCallbackTargetException, HgRuntimeException {
			// check consistency
			Nodeid cset = entry.changeset().getNodeid();
			errorCollector.assertEquals(entry.changesetRevision(), cset);
			Pair<HgChangeset, HgChangeset> p = entry.parents();
			Pair<HgChangeset, HgChangeset> parents_a = p;
			Pair<Nodeid, Nodeid> parents_b = entry.parentRevisions();
			if (parents_b.first().isNull()) {
				errorCollector.assertTrue(parents_a.first() == null);
			} else {
				errorCollector.assertEquals(parents_b.first(), parents_a.first().getNodeid());
			}
			if (parents_b.second().isNull()) {
				errorCollector.assertTrue(parents_a.second() == null);
			} else {
				errorCollector.assertEquals(parents_b.second(), parents_a.second().getNodeid());
			}
			//
			if (checkPrevInChildren && !cmdResult.isEmpty()) {
				HgChangeset prevChangeset = reverseResult ? cmdResult.getFirst() : cmdResult.getLast();
				String msg = String.format("No parent-child bind between revisions %d and %d", prevChangeset.getRevisionIndex(), entry.changeset().getRevisionIndex());
				errorCollector.assertTrue(msg, entry.children().contains(prevChangeset));
			}
			if (checkPrevInParents && !cmdResult.isEmpty()) {
				HgChangeset prevChangeset = reverseResult ? cmdResult.getFirst() : cmdResult.getLast();
				String msg = String.format("No parent-child bind between revisions %d and %d", prevChangeset.getRevisionIndex(), entry.changeset().getRevisionIndex());
				errorCollector.assertTrue(msg, p.first() == prevChangeset || p.second() == prevChangeset);
			}
			//
			if (reverseResult) {
				cmdResult.addFirst(entry.changeset());
			} else {
				cmdResult.addLast(entry.changeset());
			}
		}
	}

	private static class CollectWithRenameHandler extends CollectHandler implements HgChangesetHandler.WithCopyHistory {
		public final RenameCollector rh = new RenameCollector();
		public List<HgChangeset> lastChangesetReportedAtRename = new LinkedList<HgChangeset>();

		public void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException {
			Assert.assertTrue("Renames couldn't be reported prior to any change", getChanges().size() > 0);
			HgChangeset lastKnown = getChanges().get(getChanges().size() - 1);
			lastChangesetReportedAtRename.add(lastKnown);
			rh.copy(from, to);
		}
	};
	
	private static class RenameCollector implements HgFileRenameHandlerMixin {
		public boolean copyReported = false;
		public List<Pair<HgFileRevision, HgFileRevision>> renames = new LinkedList<Pair<HgFileRevision,HgFileRevision>>();
		
		public RenameCollector() {
		}
		
		public RenameCollector(AdapterPlug ap) {
			ap.attachAdapter(HgFileRenameHandlerMixin.class, this);
		}
		
		public void copy(HgFileRevision from, HgFileRevision to) {
			copyReported = true;
			renames.add(new Pair<HgFileRevision, HgFileRevision>(from, to));
		}
	}
}