view test/org/tmatesoft/hg/test/TestStatus.java @ 709:497e697636fc

Report merged lines as changed block if possible, not as a sequence of added/deleted blocks. To facilitate access to merge parent lines AddBlock got mergeLineAt() method that reports index of the line in the second parent (if any), while insertedAt() has been changed to report index in the first parent always
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Aug 2013 16:23:27 +0200
parents b286222158be
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.junit.Assert.*;
import static org.tmatesoft.hg.core.HgStatus.Kind.*;
import static org.tmatesoft.hg.repo.HgRepository.TIP;
import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.tmatesoft.hg.core.HgStatus;
import org.tmatesoft.hg.core.HgStatus.Kind;
import org.tmatesoft.hg.core.HgStatusCommand;
import org.tmatesoft.hg.core.HgStatusHandler;
import org.tmatesoft.hg.internal.PathGlobMatcher;
import org.tmatesoft.hg.repo.HgLookup;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgStatusCollector;
import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector;
import org.tmatesoft.hg.util.Path;
import org.tmatesoft.hg.util.Outcome;

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

	@Rule
	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();

	private HgRepository repo;
	private StatusOutputParser statusParser;
	private ExecHelper eh;
	private StatusReporter sr;

	public static void main(String[] args) throws Throwable {
		TestStatus test = new TestStatus();
		test.testLowLevel();
		test.testStatusCommand();
		test.testPerformance();
		test.errorCollector.verify();
		//
		TestStatus t2 = new TestStatus(new HgLookup().detect("/temp/hg/hg4j-merging/hg4j"));
		t2.testDirstateParentOtherThanTipWithUpdate();
		t2.errorCollector.verify();
		TestStatus t3 = new TestStatus(new HgLookup().detect("/temp/hg/cpython"));
		t3.testDirstateParentOtherThanTipNoUpdate();
		t3.errorCollector.verify();
	}

	public TestStatus() throws Exception {
		this(new HgLookup().detectFromWorkingDir());
	}

	private TestStatus(HgRepository hgRepo) {
		repo = hgRepo;
		Assume.assumeTrue(!repo.isInvalid());
		statusParser = new StatusOutputParser();
		eh = new ExecHelper(statusParser, hgRepo.getWorkingDir());
		sr = new StatusReporter(errorCollector, statusParser);
	}

	@Test
	public void testLowLevel() throws Exception {
		final HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(repo);
		statusParser.reset();
		eh.run("hg", "status", "-A");
		HgStatusCollector.Record r = wcc.status(HgRepository.TIP);
		sr.report("hg status -A", r);
		//
		statusParser.reset();
		int revision = 3;
		eh.run("hg", "status", "-A", "--rev", String.valueOf(revision));
		r = wcc.status(revision);
		sr.report("status -A --rev " + revision, r);
		//
		statusParser.reset();
		eh.run("hg", "status", "-A", "--change", String.valueOf(revision));
		r = new HgStatusCollector.Record();
		new HgStatusCollector(repo).change(revision, r);
		sr.report("status -A --change " + revision, r);
		//
		statusParser.reset();
		int rev2 = 80;
		final String range = String.valueOf(revision) + ":" + String.valueOf(rev2);
		eh.run("hg", "status", "-A", "--rev", range);
		r = new HgStatusCollector(repo).status(revision, rev2);
		sr.report("Status -A -rev " + range, r);
	}

	/**
	 * hg up --rev <earlier rev>; hg status
	 * 
	 * To check if HgWorkingCopyStatusCollector respects actual working copy parent (takes from dirstate)
	 * and if status is calculated correctly
	 */
	@Test
	@Ignore("modifies test repository, needs careful configuration")
	public void testDirstateParentOtherThanTipWithUpdate() throws Exception {
		int revToUpdate = 238;
		try {
			eh.run("hg", "up", "--rev", String.valueOf(revToUpdate));
			testDirstateParentOtherThanTipNoUpdate();
		} finally {
			eh.run("hg", "up");
		}
	}

	@Test
	@Ignore("needs configuration as it requires special repository")
	public void testDirstateParentOtherThanTipNoUpdate() throws Exception {
		final HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(repo);
		statusParser.reset();
		//
		eh.run("hg", "status", "-A");
		HgStatusCollector.Record r = wcc.status(HgRepository.TIP);
		sr.report("hg status -A", r);
		//
		statusParser.reset();
		int revision = 3;
		eh.run("hg", "status", "-A", "--rev", String.valueOf(revision));
		r = wcc.status(revision);
		sr.report("status -A --rev " + revision, r);
	}

	@Test
	public void testStatusCommand() throws Exception {
		final HgStatusCommand sc = new HgStatusCommand(repo).all();
		StatusCollector r;
		statusParser.reset();
		eh.run("hg", "status", "-A");
		sc.execute(r = new StatusCollector());
		sr.report("hg status -A", r);
		//
		statusParser.reset();
		int revision = 3;
		eh.run("hg", "status", "-A", "--rev", String.valueOf(revision));
		sc.base(revision).execute(r = new StatusCollector());
		sr.report("status -A --rev " + revision, r);
		//
		statusParser.reset();
		eh.run("hg", "status", "-A", "--change", String.valueOf(revision));
		sc.base(TIP).revision(revision).execute(r = new StatusCollector());
		sr.report("status -A --change " + revision, r);

		// TODO check not -A, but defaults()/custom set of modifications 
	}

	static class StatusCollector implements HgStatusHandler {
		private final Map<Kind, List<Path>> kind2names = new TreeMap<Kind, List<Path>>();
		private final Map<Path, List<Kind>> name2kinds = new TreeMap<Path, List<Kind>>();
		private final Map<Path, Outcome> name2error = new LinkedHashMap<Path, Outcome>();
		private final Map<Path, Path> new2oldName = new LinkedHashMap<Path, Path>();

		public void status(HgStatus s) {
			List<Path> l = kind2names.get(s.getKind());
			if (l == null) {
				kind2names.put(s.getKind(), l = new LinkedList<Path>());
			}
			l.add(s.getPath());
			if (s.isCopy()) {
				new2oldName.put(s.getPath(), s.getOriginalPath());
			}
			//
			List<Kind> k = name2kinds.get(s.getPath());
			if (k == null) {
				name2kinds.put(s.getPath(), k = new LinkedList<Kind>());
			}
			k.add(s.getKind());
		}

		public void error(Path file, Outcome s) {
			name2error.put(file, s);
		}

		public List<Path> get(Kind k) {
			List<Path> rv = kind2names.get(k);
			return rv == null ? Collections.<Path> emptyList() : rv;
		}

		public List<Kind> get(Path p) {
			List<Kind> rv = name2kinds.get(p);
			return rv == null ? Collections.<Kind> emptyList() : rv;
		}

		public Map<Path, Outcome> getErrors() {
			return name2error;
		}

		public HgStatusCollector.Record asStatusRecord() {
			HgStatusCollector.Record rv = new HgStatusCollector.Record();
			for (Path p : get(Modified)) {
				rv.modified(p);
			}
			for (Path p : get(Added)) {
				if (!new2oldName.containsKey(p)) {
					// new files that are result of a copy get reported separately, below
					rv.added(p);
				}
			}
			for (Path p : get(Removed)) {
				rv.removed(p);
			}
			for (Path p : get(Clean)) {
				rv.clean(p);
			}
			for (Path p : get(Ignored)) {
				rv.ignored(p);
			}
			for (Path p : get(Missing)) {
				rv.missing(p);
			}
			for (Path p : get(Unknown)) {
				rv.unknown(p);
			}
			for (Map.Entry<Path, Path> e : new2oldName.entrySet()) {
				rv.copied(e.getValue(), e.getKey());
			}
			return rv;
		}
	}

	/*
	 * status-1/dir/file5 was added in rev 8, scheduled (hg remove file5) for removal, but not yet committed
	 * Erroneously reported extra REMOVED file (the one added and removed in between). Shall not
	 */
	@Test
	public void testRemovedAgainstBaseWithoutIt() throws Exception {
		// check very end of WCStatusCollector, foreach left knownEntry, collect == null || baseRevFiles.contains()
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		StatusCollector sc = new StatusCollector();
		cmd.all().base(7).execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		Path file5 = Path.create("dir/file5");
		// shall not be listed at all
		assertTrue(sc.get(file5).isEmpty());
	}

	/*
	 * status-1/file2 is tracked, but later .hgignore got entry to ignore it, file2 got modified
	 * HG doesn't respect .hgignore for tracked files.
	 * Now reported as ignored and missing(?!).
	 * Shall be reported as modified.
	 */
	@Test
	public void testTrackedModifiedIgnored() throws Exception {
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		StatusCollector sc = new StatusCollector();
		cmd.all().execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		final Path file2 = Path.create("file2");
		assertTrue(sc.get(file2).contains(Modified));
		assertTrue(sc.get(file2).size() == 1);
	}

	/*
	 * status/dir/file4, added in rev 3, has been scheduled for removal (hg remove -Af file4), but still there in the WC.
	 * Shall be reported as Removed, when comparing against rev 3
	 * (despite both rev 3 and WC's parent has file4, there are different paths in the code for wc against parent and wc against rev)
	 */
	@Test
	public void testMarkedRemovedButStillInWC() throws Exception {
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		StatusCollector sc = new StatusCollector();
		cmd.all().execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		Path file4 = Path.create("dir/file4");
		assertTrue(sc.get(file4).contains(Removed));
		assertTrue(sc.get(file4).size() == 1);
		//
		// different code path (collect != null)
		cmd.base(3).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		assertTrue(sc.get(file4).contains(Removed));
		assertTrue(sc.get(file4).size() == 1);
		//
		// wasn't there in rev 2, shall not be reported at all
		cmd.base(2).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		assertTrue(sc.get(file4).isEmpty());
	}

	/*
	 * status-1/dir/file3 tracked, listed in .hgignore since rev 4, removed (hg remove file3) from repo and WC
	 * (but entry in .hgignore left) in revision 5, and new file3 got created in WC.
	 * Shall be reported as ignored when comparing against WC's parent,
	 * and both ignored and removed when comparing against revision 3
	 */
	@Test
	public void testRemovedIgnoredInWC() throws Exception {
		// check branch !known, ignored
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		StatusCollector sc = new StatusCollector();
		cmd.all().execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		final Path file3 = Path.create("dir/file3");
		assertTrue(sc.get(file3).contains(Ignored));
		assertTrue(sc.get(file3).size() == 1);
		//
		cmd.base(3).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		assertTrue(sc.get(file3).contains(Ignored));
		assertTrue(sc.get(file3).contains(Removed));
		assertTrue(sc.get(file3).size() == 2);
		//
		cmd.base(5).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		assertTrue(sc.get(file3).contains(Ignored));
		assertTrue(sc.get(file3).size() == 1);
		//
		cmd.base(0).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		assertTrue(sc.get(file3).contains(Ignored));
		assertTrue(sc.get(file3).size() == 1);

	}

	/*
	 * status/file1 was removed in cset 2. New file with the same name in the WC.
	 * Shall report 2 statuses (as cmdline hg does): unknown and removed when comparing against that revision.
	 */
	@Test
	public void testNewFileWithSameNameAsDeletedOld() throws Exception {
		// check branch !known, !ignored (=> unknown)
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		StatusCollector sc = new StatusCollector();
		cmd.base(1);
		cmd.all().execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		final Path file1 = Path.create("file1");
		assertTrue(sc.get(file1).contains(Unknown));
		assertTrue(sc.get(file1).contains(Removed));
		assertTrue(sc.get(file1).size() == 2);
		// 
		// no file1 in rev 2, shall be reported as unknown only
		cmd.base(2).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		assertTrue(sc.get(file1).contains(Unknown));
		assertTrue(sc.get(file1).size() == 1);
	}

	@Test
	public void testSubTreeStatus() throws Exception {
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		StatusCollector sc = new StatusCollector();
		cmd.match(new PathGlobMatcher("*"));
		cmd.all().execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		/*
		 * C .hgignore
		 * ? file1
		 * M file2
		 * C readme
		 */
		final Path file1 = Path.create("file1");
		assertTrue(sc.get(file1).contains(Unknown));
		assertTrue(sc.get(file1).size() == 1);
		assertTrue(sc.get(Removed).isEmpty());
		assertTrue(sc.get(Clean).size() == 2);
		assertTrue(sc.get(Modified).size() == 1);
		//
		cmd.match(new PathGlobMatcher("dir/*")).execute(sc = new StatusCollector());
		assertTrue(sc.getErrors().isEmpty());
		/*
		 * I dir/file3
		 * R dir/file4
		 * R dir/file5
		 */
		assertTrue(sc.get(Modified).isEmpty());
		assertTrue(sc.get(Added).isEmpty());
		assertTrue(sc.get(Ignored).size() == 1);
		assertTrue(sc.get(Removed).size() == 2);
	}

	@Test
	public void testSpecificFileStatus() throws Exception {
		repo = Configuration.get().find("status-1");
		// files only
		final Path file2 = Path.create("file2");
		final Path file3 = Path.create("dir/file3");
		HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file2, file3);
		HgStatusCollector.Record r = new HgStatusCollector.Record();
		sc.walk(WORKING_COPY, r);
		assertTrue(r.getAdded().isEmpty());
		assertTrue(r.getRemoved().isEmpty());
		assertTrue(r.getUnknown().isEmpty());
		assertTrue(r.getClean().isEmpty());
		assertTrue(r.getMissing().isEmpty());
		assertTrue(r.getCopied().isEmpty());
		assertTrue(r.getIgnored().contains(file3));
		assertTrue(r.getIgnored().size() == 1);
		assertTrue(r.getModified().contains(file2));
		assertTrue(r.getModified().size() == 1);
		// mix files and directories
		final Path readme = Path.create("readme");
		final Path dir = Path.create("dir/");
		sc = HgWorkingCopyStatusCollector.create(repo, readme, dir);
		sc.walk(WORKING_COPY, r = new HgStatusCollector.Record());
		assertTrue(r.getAdded().isEmpty());
		assertTrue(r.getRemoved().size() == 2);
		for (Path p : r.getRemoved()) {
			assertEquals(Path.CompareResult.ImmediateChild, p.compareWith(dir));
		}
		assertTrue(r.getUnknown().isEmpty());
		assertTrue(r.getClean().size() == 1);
		assertTrue(r.getClean().contains(readme));
		assertTrue(r.getMissing().isEmpty());
		assertTrue(r.getCopied().isEmpty());
		assertTrue(r.getIgnored().contains(file3));
		assertTrue(r.getIgnored().size() == 1);
		assertTrue(r.getModified().isEmpty());
	}

	@Test
	public void testSameResultDirectPathVsMatcher() throws Exception {
		repo = Configuration.get().find("status-1");
		final Path file3 = Path.create("dir/file3");
		final Path file5 = Path.create("dir/file5");

		HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file3, file5);
		HgStatusCollector.Record r;
		sc.walk(WORKING_COPY, r = new HgStatusCollector.Record());
		assertTrue(r.getRemoved().contains(file5));
		assertTrue(r.getIgnored().contains(file3));
		//
		// query for the same file, but with
		sc = HgWorkingCopyStatusCollector.create(repo, new PathGlobMatcher(file3.toString(), file5.toString()));
		sc.walk(WORKING_COPY, r = new HgStatusCollector.Record());
		assertTrue(r.getRemoved().contains(file5));
		assertTrue(r.getIgnored().contains(file3));
	}

	@Test
	public void testScopeInHistoricalStatus() throws Exception {
		repo = Configuration.get().find("status-1");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		cmd.base(3).revision(8).all();
		cmd.match(new PathGlobMatcher("dir/*"));
		StatusCollector sc = new StatusCollector();
		cmd.execute(sc);
		assertTrue(sc.getErrors().isEmpty());
		final Path file3 = Path.create("dir/file3");
		final Path file4 = Path.create("dir/file4");
		final Path file5 = Path.create("dir/file5");
		//
		assertTrue(sc.get(file3).contains(Removed));
		assertTrue(sc.get(file3).size() == 1);
		assertTrue(sc.get(Removed).size() == 1);
		//
		assertTrue(sc.get(file4).contains(Clean));
		assertTrue(sc.get(file4).size() == 1);
		assertTrue(sc.get(Clean).size() == 1);
		//
		assertTrue(sc.get(file5).contains(Added));
		assertTrue(sc.get(file5).size() == 1);
		assertTrue(sc.get(Added).size() == 1);

	}

	/**
	 * Issue 22
	 */
	@Test
	public void testOnEmptyRepositoryWithAllFilesDeleted() throws Exception {
		repo = Configuration.get().find("status-2");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		cmd.all();
		StatusCollector sc = new StatusCollector();
		cmd.execute(sc);
		// shall pass without exception
		assertTrue(sc.getErrors().isEmpty());
		for (HgStatus.Kind k : HgStatus.Kind.values()) {
			assertTrue("Kind " + k.name() + " shall be empty", sc.get(k).isEmpty());
		}
	}

	/**
	 * Issue 22, two subsequent commits that remove all repository files, each in a different branch.
	 * Here's excerpt from my RevlogWriter utility:
	 * 
	 * <pre>
	 * 		final List<String> filesList = Collections.singletonList("file1");
	 * //
	 * file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes());
	 * //
	 * ManifestBuilder mb = new ManifestBuilder();
	 * mb.reset().add("file1", file1.getRevision(0));
	 * manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0
	 * final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.<String, String>emptyMap(), filesList, "Add a file");
	 * changelog.writeUncompressed(-1, -1, 0, 0, cset1);
	 * //
	 * // pretend we delete all files in a branch 1
	 * manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1
	 * final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch");
	 *  changelog.writeUncompressed(0, -1, 1, 1, cset2);
	 * //
	 * // pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1)
	 * manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2
	 * final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch");
	 * changelog.writeUncompressed(0, -1, 2, 2, cset3);
	 * </pre>
	 */
	@Test
	public void testOnEmptyRepositoryWithAllFilesDeletedInBranch() throws Exception {
		repo = Configuration.get().find("status-3");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		cmd.all();
		StatusCollector sc = new StatusCollector();
		cmd.execute(sc);
		// shall pass without exception
		assertTrue(sc.getErrors().isEmpty());
		for (HgStatus.Kind k : HgStatus.Kind.values()) {
			assertTrue("Kind " + k.name() + " shall be empty", sc.get(k).isEmpty());
		}
	}

	/**
	 * Issue 23: HgInvalidRevisionException for svn imported repository (changeset 0 references nullid manifest)
	 */
	@Test
	public void testImportedRepoWithOddManifestRevisions() throws Exception {
		repo = Configuration.get().find("status-4");
		HgStatusCommand cmd = new HgStatusCommand(repo);
		cmd.all();
		StatusCollector sc = new StatusCollector();
		cmd.execute(sc);
		// shall pass without exception
		assertTrue(sc.getErrors().isEmpty());
	}

	/**
	 * Issue 24: IllegalArgumentException in FilterDataAccess
	 * There were two related defects in RevlogStream
	 * a) for compressedLen == 0, a byte was read and FilterDataAccess (of length 0, but it didn't help too much) was created - first byte happen to be 0.
	 * Patch was not applied (userDataAccess.isEmpty() check thanks to Issue 22)
	 * b) That FilterDataAccess (with 0 size represents patch more or less relevantly, but didn't represent actual revision) get successfully
	 * reassigned as lastUserData for the next iteration. And at the next step attempt to apply patch recorded in the next revision failed
	 * because baseRevisionData is 0 length FilterDataAccess
	 * 
	 * Same applies for
	 * Issue 25: IOException: Underflow. Rewind past end of the slice in InflaterDataAccess
	 * with the difference in separate .i and .d (thus not 0 but 'x' first byte was read)
	 * 
	 * Sample:
	 * status-5/file1 has 3 revisions, second is zero-length patch:
	 * Index Offset Packed Actual Base Rev
	 * 0: 0 8 7 0
	 * DATA
	 * 1: 8 0 7 0
	 * NO DATA
	 * 2: 8 14 6 0
	 * PATCH
	 */
	@Test
	public void testZeroLengthPatchAgainstNonEmptyBaseRev() throws Exception {
		repo = Configuration.get().find("status-5");
		// pretend we modified files in the working copy
		// for HgWorkingCopyStatusCollector to go and retrieve its content from repository 
		File f1 = new File(repo.getWorkingDir(), "file1");
		f1.setLastModified(System.currentTimeMillis());
		File f3 = new File(repo.getWorkingDir(), "file3");
		f3.setLastModified(System.currentTimeMillis());
		//
		HgStatusCommand cmd = new HgStatusCommand(repo);
		cmd.all();
		StatusCollector sc = new StatusCollector();
		cmd.execute(sc);
		// shall pass without exception
		//
		for (Map.Entry<Path, Outcome> e : sc.getErrors().entrySet()) {
			System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage());
		}
		assertTrue(sc.getErrors().isEmpty());
	}

	/**
	 * Issue 26: UnsupportedOperationException when patching empty base revision
	 * 
	 * Sample:
	 * status-5/file2 has 3 revisions, second is patch (complete revision content in a form of the patch) for empty base revision:
	 * Index Offset Packed Actual Base Rev
	 * 0: 0 0 0 0
	 * NO DATA
	 * 1: 0 20 7 0
	 * PATCH: 0..0, 7:garbage
	 * 2: 20 16 7 0
	 */
	@Test
	public void testPatchZeroLengthBaseRevision() throws Exception {
		repo = Configuration.get().find("status-5");
		// touch the file to force content retrieval
		File f2 = new File(repo.getWorkingDir(), "file2");
		f2.setLastModified(System.currentTimeMillis());
		//
		HgStatusCommand cmd = new HgStatusCommand(repo);
		cmd.all();
		StatusCollector sc = new StatusCollector();
		cmd.execute(sc);
		// shall pass without exception
		//
		for (Map.Entry<Path, Outcome> e : sc.getErrors().entrySet()) {
			System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage());
		}
		assertTrue(sc.getErrors().isEmpty());
	}
	
	@Test
	public void testNestedRepositoriesAreNotWalkedIn() throws Exception {
		repo = Configuration.get().find("status-nested-repo");
		File s2 = new File(repo.getWorkingDir(), "skip/s2/.hg/");
		File s1 = new File(repo.getWorkingDir(), "s1/.hg/");
		File s1b = new File(repo.getWorkingDir(), "s1/b");
		assertTrue("[sanity]", s1.exists() && s1.isDirectory());
		assertTrue("[sanity]", s1b.exists() && s1b.isFile());
		assertTrue("[sanity]", s2.exists() && s2.isDirectory());
		StatusCollector sc = new StatusCollector();
		new HgStatusCommand(repo).all().execute(sc);
		List<Path> ignored = sc.get(Ignored);
		assertEquals(1, ignored.size());
		assertEquals(Path.create("skip/a"), ignored.get(0));
		assertTrue(sc.get(Path.create("s1/b")).isEmpty());
	}
	
	@Test
	public void testDetectRenamesInNonFirstRev() throws Exception {
		repo = Configuration.get().find("log-renames");
		eh.cwd(repo.getWorkingDir());
		final HgStatusCommand cmd = new HgStatusCommand(repo).defaults();
		StatusCollector sc;
		for (int r : new int[] {2,3,4}) {
			statusParser.reset();
			eh.run("hg", "status", "-C", "--change", String.valueOf(r));
			cmd.change(r).execute(sc = new StatusCollector());
			sr.report("hg status -C --change " + r, sc);
		}
		// a and d from r5 are missing in r3
		statusParser.reset();
		eh.run("hg", "status", "-C", "--rev", "3", "--rev", "5");
		cmd.base(3).revision(5).execute(sc = new StatusCollector());
		sr.report("hg status -C 3..5 ", sc);
		//
		// a is c which is initially b
		// d is b which is initially a
		Path fa = Path.create("a"); 
		Path fb = Path.create("b");
		Path fc = Path.create("c");
		Path fd = Path.create("d");
		// neither initial a nor b have isCopy(() == true
		assertFalse("[sanity]", repo.getFileNode(fa).isCopy());
		// check HgStatusCollector
		// originals (base revision) doesn't contain first copy origin (there's no b in r2)
		cmd.base(2).revision(5).execute(sc = new StatusCollector());
		errorCollector.assertEquals(fa, sc.new2oldName.get(fd));
		errorCollector.assertEquals(Collections.singletonList(Removed), sc.get(fc));
		// ensure same result with HgWorkingCopyStatusCollector
		cmd.base(2).revision(WORKING_COPY).execute(sc = new StatusCollector());
		errorCollector.assertEquals(fa, sc.new2oldName.get(fd));
		errorCollector.assertEquals(Collections.singletonList(Removed), sc.get(fc));
		// originals (base revision) does contain first copy origin (b is in r1)
		cmd.base(1).revision(5).execute(sc = new StatusCollector());
		errorCollector.assertEquals(fa, sc.new2oldName.get(fd));
		errorCollector.assertEquals(Collections.singletonList(Removed), sc.get(fb));
	}

	/*
	 * With warm-up of previous tests, 10 runs, time in milliseconds
	 * 'hg status -A': Native client total 953 (95 per run), Java client 94 (9)
	 * 'hg status -A --rev 3:80': Native client total 1828 (182 per run), Java client 235 (23)
	 * 'hg log --debug', 10 runs: Native client total 1766 (176 per run), Java client 78 (7)
	 * 
	 * 18.02.2011
	 * 'hg status -A --rev 3:80', 10 runs: Native client total 2000 (200 per run), Java client 250 (25)
	 * 'hg log --debug', 10 runs: Native client total 2297 (229 per run), Java client 125 (12)
	 * 
	 * 9.3.2011 (DataAccess instead of byte[] in ReflogStream.Inspector
	 * 'hg status -A', 10 runs: Native client total 1516 (151 per run), Java client 219 (21)
	 * 'hg status -A --rev 3:80', 10 runs: Native client total 1875 (187 per run), Java client 3187 (318) (!!! ???)
	 * 'hg log --debug', 10 runs: Native client total 2484 (248 per run), Java client 344 (34)
	 */
	public void testPerformance() throws Exception {
		final int runs = 10;
		final long start1 = System.currentTimeMillis();
		for (int i = 0; i < runs; i++) {
			statusParser.reset();
			eh.run("hg", "status", "-A", "--rev", "3:80");
		}
		final long start2 = System.currentTimeMillis();
		for (int i = 0; i < runs; i++) {
			StatusCollector r = new StatusCollector();
			new HgStatusCommand(repo).all().base(3).revision(80).execute(r);
		}
		final long end = System.currentTimeMillis();
		System.out.printf("'hg status -A --rev 3:80', %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);
	}
	
	static class StatusReporter {
		private final StatusOutputParser statusParser;
		private final ErrorCollectorExt errorCollector;

		public StatusReporter(ErrorCollectorExt ec, StatusOutputParser sp) {
			errorCollector = ec;
			statusParser = sp;
		}
	
		public void report(String what, StatusCollector r) {
			errorCollector.assertTrue(what, r.getErrors().isEmpty());
			report(what, r.asStatusRecord());
		}

		public void report(String what, HgStatusCollector.Record r) {
			reportNotEqual(what + "#MODIFIED", r.getModified(), statusParser.getModified());
			reportNotEqual(what + "#ADDED", r.getAdded(), statusParser.getAdded());
			reportNotEqual(what + "#REMOVED", r.getRemoved(), statusParser.getRemoved());
			reportNotEqual(what + "#CLEAN", r.getClean(), statusParser.getClean());
			reportNotEqual(what + "#IGNORED", r.getIgnored(), statusParser.getIgnored());
			reportNotEqual(what + "#MISSING", r.getMissing(), statusParser.getMissing());
			reportNotEqual(what + "#UNKNOWN", r.getUnknown(), statusParser.getUnknown());
			List<Path> copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet());
			HashMap<Path, String> copyDiff = new HashMap<Path, String>();
			if (copiedKeyDiff.isEmpty()) {
				for (Path jk : r.getCopied().keySet()) {
					Path jv = r.getCopied().get(jk);
					if (statusParser.getCopied().containsKey(jk)) {
						Path cmdv = statusParser.getCopied().get(jk);
						if (!jv.equals(cmdv)) {
							copyDiff.put(jk, jv + " instead of " + cmdv);
						}
					} else {
						copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA");
					}
				}
			}
			errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections.<Path> emptyList()));
			errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections.<Path, String> emptyMap()));
		}

		private <T extends Comparable<? super T>> void reportNotEqual(String what, Collection<T> l1, Collection<T> l2) {
		//		List<T> diff = difference(l1, l2);
		//		errorCollector.checkThat(what, diff, equalTo(Collections.<T>emptyList()));
			ArrayList<T> sl1 = new ArrayList<T>(l1);
			Collections.sort(sl1);
			ArrayList<T> sl2 = new ArrayList<T>(l2);
			Collections.sort(sl2);
			if (!sl1.isEmpty() && !sl2.isEmpty()) {
				what = what + ", diff:" + difference(sl1, sl2);
			}
			errorCollector.checkThat(what, sl1, equalTo(sl2));
		}

		public static <T> List<T> difference(Collection<T> l1, Collection<T> l2) {
			LinkedList<T> result = new LinkedList<T>(l2);
			for (T t : l1) {
				if (l2.contains(t)) {
					result.remove(t);
				} else {
					result.add(t);
				}
			}
			return result;
		}
	}
}