]> source.dussan.org Git - jgit.git/commitdiff
Additional unit tests for the GC 83/7783/3
authorSasa Zivkov <sasa.zivkov@sap.com>
Sun, 16 Sep 2012 21:57:18 +0000 (23:57 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Sun, 16 Sep 2012 21:57:18 +0000 (23:57 +0200)
Change-Id: Id5b578f7040c6c896ab9386a6b5ed62b0f495ed5
Signed-off-by: Sasa Zivkov <sasa.zivkov@sap.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/GCTest.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java [new file with mode: 0644]

index b36b5c761f3e087cb5352d478c6aa671201c1fa3..cc4852a2bd1243a6a1cb74526ecf449bfe85fc31 100644 (file)
@@ -549,6 +549,23 @@ public class TestRepository<R extends Repository> {
                return new BranchBuilder(ref);
        }
 
+       /**
+        * Tag an object using a lightweight tag.
+        *
+        * @param name
+        *            the tag name. The /refs/tags/ prefix will be added if the name
+        *            doesn't start with it
+        * @param obj
+        *            the object to tag
+        * @return the tagged object
+        * @throws Exception
+        */
+       public ObjectId lightweightTag(String name, ObjectId obj) throws Exception {
+               if (!name.startsWith(Constants.R_TAGS))
+                       name = Constants.R_TAGS + name;
+               return update(name, obj);
+       }
+
        /**
         * Run consistency checks against the object database.
         * <p>
@@ -733,6 +750,8 @@ public class TestRepository<R extends Repository> {
 
                private final DirCache tree = DirCache.newInCore();
 
+               private ObjectId topLevelTree;
+
                private final List<RevCommit> parents = new ArrayList<RevCommit>(2);
 
                private int tick = 1;
@@ -787,6 +806,11 @@ public class TestRepository<R extends Repository> {
                        return this;
                }
 
+               public CommitBuilder setTopLevelTree(ObjectId treeId) {
+                       topLevelTree = treeId;
+                       return this;
+               }
+
                public CommitBuilder add(String path, String content) throws Exception {
                        return add(path, blob(content));
                }
@@ -840,7 +864,10 @@ public class TestRepository<R extends Repository> {
 
                                ObjectId commitId;
                                try {
-                                       c.setTreeId(tree.writeTree(inserter));
+                                       if (topLevelTree != null)
+                                               c.setTreeId(topLevelTree);
+                                       else
+                                               c.setTreeId(tree.writeTree(inserter));
                                        commitId = inserter.insert(c);
                                        inserter.flush();
                                } finally {
index b2a79274ac786d4aa19e2f367426c6b4d01eb1a4..17c918ba76d7d7cf729b3cdf5f0b41655ac87aa0 100644 (file)
@@ -44,19 +44,40 @@ package org.eclipse.jgit.storage.file;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.util.Collection;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Iterator;
-
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.junit.TestRepository.CommitBuilder;
+import org.eclipse.jgit.lib.EmptyProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.lib.Ref.Storage;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.storage.file.GC.RepoStatistics;
 import org.eclipse.jgit.storage.file.PackIndex.MutableEntry;
 import org.eclipse.jgit.util.FileUtils;
@@ -86,6 +107,310 @@ public class GCTest extends LocalDiskRepositoryTestCase {
                super.tearDown();
        }
 
+       // GC.packRefs tests
+
+       @Test
+       public void packRefs_looseRefPacked() throws Exception {
+               RevBlob a = tr.blob("a");
+               tr.lightweightTag("t", a);
+
+               gc.packRefs();
+               assertSame(repo.getRef("t").getStorage(), Storage.PACKED);
+       }
+
+       @Test
+       public void concurrentPackRefs_onlyOneWritesPackedRefs() throws Exception {
+               RevBlob a = tr.blob("a");
+               tr.lightweightTag("t", a);
+
+               final CyclicBarrier syncPoint = new CyclicBarrier(2);
+
+               Callable<Integer> packRefs = new Callable<Integer>() {
+
+                       /** @return 0 for success, 1 in case of error when writing pack */
+                       public Integer call() throws Exception {
+                               syncPoint.await();
+                               try {
+                                       gc.packRefs();
+                                       return 0;
+                               } catch (IOException e) {
+                                       return 1;
+                               }
+                       }
+               };
+               ExecutorService pool = Executors.newFixedThreadPool(2);
+               try {
+                       Future<Integer> p1 = pool.submit(packRefs);
+                       Future<Integer> p2 = pool.submit(packRefs);
+                       assertTrue(p1.get() + p2.get() == 1);
+               } finally {
+                       pool.shutdown();
+                       pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+               }
+       }
+
+       @Test
+       public void packRefsWhileRefLocked_refNotPackedNoError()
+                       throws Exception {
+               RevBlob a = tr.blob("a");
+               tr.lightweightTag("t1", a);
+               tr.lightweightTag("t2", a);
+               LockFile refLock = new LockFile(new File(repo.getDirectory(),
+                               "refs/tags/t1"), repo.getFS());
+               try {
+                       refLock.lock();
+                       gc.packRefs();
+               } finally {
+                       refLock.unlock();
+               }
+
+               assertSame(repo.getRef("refs/tags/t1").getStorage(), Storage.LOOSE);
+               assertSame(repo.getRef("refs/tags/t2").getStorage(), Storage.PACKED);
+       }
+
+       @Test
+       public void packRefsWhileRefUpdated_refUpdateSucceeds()
+                       throws Exception {
+               RevBlob a = tr.blob("a");
+               tr.lightweightTag("t", a);
+               final RevBlob b = tr.blob("b");
+
+               final CyclicBarrier refUpdateLockedRef = new CyclicBarrier(2);
+               final CyclicBarrier packRefsDone = new CyclicBarrier(2);
+               ExecutorService pool = Executors.newFixedThreadPool(2);
+               try {
+                       Future<Result> result = pool.submit(new Callable<Result>() {
+
+                               public Result call() throws Exception {
+                                       RefUpdate update = new RefDirectoryUpdate(
+                                                       (RefDirectory) repo.getRefDatabase(),
+                                                       repo.getRef("refs/tags/t")) {
+                                               @Override
+                                               public boolean isForceUpdate() {
+                                                       try {
+                                                               refUpdateLockedRef.await();
+                                                               packRefsDone.await();
+                                                       } catch (InterruptedException e) {
+                                                               Thread.currentThread().interrupt();
+                                                       } catch (BrokenBarrierException e) {
+                                                               Thread.currentThread().interrupt();
+                                                       }
+                                                       return super.isForceUpdate();
+                                               }
+                                       };
+                                       update.setForceUpdate(true);
+                                       update.setNewObjectId(b);
+                                       return update.update();
+                               }
+                       });
+
+                       pool.submit(new Callable<Void>() {
+                               public Void call() throws Exception {
+                                       refUpdateLockedRef.await();
+                                       gc.packRefs();
+                                       packRefsDone.await();
+                                       return null;
+                               }
+                       });
+
+                       assertSame(result.get(), Result.FORCED);
+
+               } finally {
+                       pool.shutdownNow();
+                       pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+               }
+
+               assertEquals(repo.getRef("refs/tags/t").getObjectId(), b);
+       }
+
+       // GC.repack tests
+
+       @Test
+       public void repackEmptyRepo_noPackCreated() throws IOException {
+               gc.repack();
+               assertEquals(0, repo.getObjectDatabase().getPacks().size());
+       }
+
+       @Test
+       public void concurrentRepack() throws Exception {
+               final CyclicBarrier syncPoint = new CyclicBarrier(2);
+
+               class DoRepack extends EmptyProgressMonitor implements
+                               Callable<Integer> {
+
+                       public void beginTask(String title, int totalWork) {
+                               if (title.equals(JGitText.get().writingObjects)) {
+                                       try {
+                                               syncPoint.await();
+                                       } catch (InterruptedException e) {
+                                               Thread.currentThread().interrupt();
+                                       } catch (BrokenBarrierException ignored) {
+                                               //
+                                       }
+                               }
+                       }
+
+                       /** @return 0 for success, 1 in case of error when writing pack */
+                       public Integer call() throws Exception {
+                               try {
+                                       gc.setProgressMonitor(this);
+                                       gc.repack();
+                                       return 0;
+                               } catch (IOException e) {
+                                       // leave the syncPoint in broken state so any awaiting
+                                       // threads and any threads that call await in the future get
+                                       // the BrokenBarrierException
+                                       Thread.currentThread().interrupt();
+                                       try {
+                                               syncPoint.await();
+                                       } catch (InterruptedException ignored) {
+                                               //
+                                       }
+                                       return 1;
+                               }
+                       }
+               }
+
+               RevBlob a = tr.blob("a");
+               tr.lightweightTag("t", a);
+
+               ExecutorService pool = Executors.newFixedThreadPool(2);
+               try {
+                       DoRepack repack1 = new DoRepack();
+                       DoRepack repack2 = new DoRepack();
+                       Future<Integer> result1 = pool.submit(repack1);
+                       Future<Integer> result2 = pool.submit(repack2);
+                       assertTrue(result1.get() + result2.get() == 0);
+               } finally {
+                       pool.shutdown();
+                       pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+               }
+       }
+
+       // GC.prune tests
+
+       @Test
+       public void nonReferencedNonExpiredObject_notPruned() throws Exception {
+               long start = now();
+
+               fsTick();
+               RevBlob a = tr.blob("a");
+               long delta = now() - start;
+               gc.setExpireAgeMillis(delta);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertTrue(repo.hasObject(a));
+       }
+
+       @Test
+       public void nonReferencedExpiredObject_pruned() throws Exception {
+               RevBlob a = tr.blob("a");
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertFalse(repo.hasObject(a));
+       }
+
+       @Test
+       public void nonReferencedExpiredObjectTree_pruned() throws Exception {
+               RevBlob a = tr.blob("a");
+               RevTree t = tr.tree(tr.file("a", a));
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertFalse(repo.hasObject(t));
+               assertFalse(repo.hasObject(a));
+       }
+
+       @Test
+       public void nonReferencedObjects_onlyExpiredPruned() throws Exception {
+               RevBlob a = tr.blob("a");
+
+               fsTick();
+               long start = now();
+
+               fsTick();
+               RevBlob b = tr.blob("b");
+               gc.setExpireAgeMillis(now() - start);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertFalse(repo.hasObject(a));
+               assertTrue(repo.hasObject(b));
+       }
+
+       @Test
+       public void lightweightTag_objectNotPruned() throws Exception {
+               RevBlob a = tr.blob("a");
+               tr.lightweightTag("t", a);
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertTrue(repo.hasObject(a));
+       }
+
+       @Test
+       public void annotatedTag_objectNotPruned() throws Exception {
+               RevBlob a = tr.blob("a");
+               RevTag t = tr.tag("t", a); // this doesn't create the refs/tags/t ref
+               tr.lightweightTag("t", t);
+
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertTrue(repo.hasObject(t));
+               assertTrue(repo.hasObject(a));
+       }
+
+       @Test
+       public void branch_historyNotPruned() throws Exception {
+               RevCommit tip = commitChain(10);
+               tr.branch("b").update(tip);
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               do {
+                       assertTrue(repo.hasObject(tip));
+                       tr.parseBody(tip);
+                       RevTree t = tip.getTree();
+                       assertTrue(repo.hasObject(t));
+                       assertTrue(repo.hasObject(tr.get(t, "a")));
+                       tip = tip.getParentCount() > 0 ? tip.getParent(0) : null;
+               } while (tip != null);
+       }
+
+       @Test
+       public void deleteBranch_historyPruned() throws Exception {
+               RevCommit tip = commitChain(10);
+               tr.branch("b").update(tip);
+               RefUpdate update = repo.updateRef("refs/heads/b");
+               update.setForceUpdate(true);
+               update.delete();
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertTrue(gc.getStatistics().numberOfLooseObjects == 0);
+       }
+
+       @Test
+       public void deleteMergedBranch_historyNotPruned() throws Exception {
+               RevCommit parent = tr.commit().create();
+               RevCommit b1Tip = tr.branch("b1").commit().parent(parent).add("x", "x")
+                               .create();
+               RevCommit b2Tip = tr.branch("b2").commit().parent(parent).add("y", "y")
+                               .create();
+
+               // merge b1Tip and b2Tip and update refs/heads/b1 to the merge commit
+               Merger merger = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
+               merger.merge(b1Tip, b2Tip);
+               CommitBuilder cb = tr.commit();
+               cb.parent(b1Tip).parent(b2Tip);
+               cb.setTopLevelTree(merger.getResultTreeId());
+               RevCommit mergeCommit = cb.create();
+               RefUpdate u = repo.updateRef("refs/heads/b1");
+               u.setNewObjectId(mergeCommit);
+               u.update();
+
+               RefUpdate update = repo.updateRef("refs/heads/b2");
+               update.setForceUpdate(true);
+               update.delete();
+
+               gc.setExpireAgeMillis(0);
+               gc.prune(Collections.<ObjectId> emptySet());
+               assertTrue(repo.hasObject(b2Tip));
+       }
+
        @Test
        public void testPackAllObjectsInOnePack() throws Exception {
                tr.branch("refs/heads/master").commit().add("A", "A").add("B", "B")
@@ -345,4 +670,41 @@ public class GCTest extends LocalDiskRepositoryTestCase {
                stats = gc.getStatistics();
                assertEquals(8, stats.numberOfLooseObjects);
        }
+
+       /**
+        * Create a chain of commits of given depth.
+        * <p>
+        * Each commit contains one file named "a" containing the index of the
+        * commit in the chain as its content. The created commit chain is
+        * referenced from any ref.
+        * <p>
+        * A chain of depth = N will create 3*N objects in Gits object database. For
+        * each depth level three objects are created: the commit object, the
+        * top-level tree object and a blob for the content of the file "a".
+        *
+        * @param depth
+        *            the depth of the commit chain.
+        * @return the commit that is the tip of the commit chain
+        * @throws Exception
+        */
+       private RevCommit commitChain(int depth) throws Exception {
+               if (depth <= 0)
+                       throw new IllegalArgumentException("Chain depth must be > 0");
+               CommitBuilder cb = tr.commit();
+               RevCommit tip;
+               do {
+                       --depth;
+                       tip = cb.add("a", "" + depth).message("" + depth).create();
+                       cb = cb.child();
+               } while (depth > 0);
+               return tip;
+       }
+
+       private static long now() {
+               return System.currentTimeMillis();
+       }
+
+       private static void fsTick() throws InterruptedException, IOException {
+               RepositoryTestCase.fsTick(null);
+       }
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java
new file mode 100644 (file)
index 0000000..eabcbbf
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012, Sasa Zivkov <sasa.zivkov@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib;
+
+/**
+ * A convenient base class which provides empty method bodies for all
+ * ProgressMonitor methods.
+ * <p>
+ * Could be used in scenarios when only some of the progress notifications are
+ * important and others can be ignored.
+ */
+public abstract class EmptyProgressMonitor implements ProgressMonitor {
+
+       public void start(int totalTasks) {
+               // empty
+       }
+
+       public void beginTask(String title, int totalWork) {
+               // empty
+       }
+
+       public void update(int completed) {
+               // empty
+       }
+
+       public void endTask() {
+               // empty
+       }
+
+       public boolean isCancelled() {
+               return false;
+       }
+
+}