diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java new file mode 100644 index 000000000..715eac270 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2010, Christian Halstrick + * 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; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.TreeSet; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.FileTreeIteratorWithTimeControl; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; + +public class RacyGitTests extends RepositoryTestCase { + public void testIterator() throws IllegalStateException, IOException, + InterruptedException { + TreeSet modTimes = new TreeSet(); + File lastFile = null; + for (int i = 0; i < 10; i++) { + lastFile = new File(db.getWorkTree(), "0." + i); + lastFile.createNewFile(); + if (i == 5) + fsTick(lastFile); + } + modTimes.add(fsTick(lastFile)); + for (int i = 0; i < 10; i++) { + lastFile = new File(db.getWorkTree(), "1." + i); + lastFile.createNewFile(); + } + modTimes.add(fsTick(lastFile)); + for (int i = 0; i < 10; i++) { + lastFile = new File(db.getWorkTree(), "2." + i); + lastFile.createNewFile(); + if (i % 4 == 0) + fsTick(lastFile); + } + FileTreeIteratorWithTimeControl fileIt = new FileTreeIteratorWithTimeControl( + db, modTimes); + NameConflictTreeWalk tw = new NameConflictTreeWalk(db); + tw.reset(); + tw.addTree(fileIt); + tw.setRecursive(true); + FileTreeIterator t; + long t0 = 0; + for (int i = 0; i < 10; i++) { + assertTrue(tw.next()); + t = tw.getTree(0, FileTreeIterator.class); + if (i == 0) + t0 = t.getEntryLastModified(); + else + assertEquals(t0, t.getEntryLastModified()); + } + long t1 = 0; + for (int i = 0; i < 10; i++) { + assertTrue(tw.next()); + t = tw.getTree(0, FileTreeIterator.class); + if (i == 0) { + t1 = t.getEntryLastModified(); + assertTrue(t1 > t0); + } else + assertEquals(t1, t.getEntryLastModified()); + } + long t2 = 0; + for (int i = 0; i < 10; i++) { + assertTrue(tw.next()); + t = tw.getTree(0, FileTreeIterator.class); + if (i == 0) { + t2 = t.getEntryLastModified(); + assertTrue(t2 > t1); + } else + assertEquals(t2, t.getEntryLastModified()); + } + } + + public void testRacyGitDetection() throws IOException, + IllegalStateException, InterruptedException { + DirCache dc; + TreeSet modTimes = new TreeSet(); + File lastFile; + + // wait to ensure that modtimes of the file doesn't match last index + // file modtime + modTimes.add(fsTick(db.getIndexFile())); + + // create two files + addToWorkDir("a", "a"); + lastFile = addToWorkDir("b", "b"); + + // wait to ensure that file-modTimes and therefore index entry modTime + // doesn't match the modtime of index-file after next persistance + modTimes.add(fsTick(lastFile)); + + // now add both files to the index. No racy git expected + addToIndex(modTimes); + + assertEquals("[[a, modTime(index/file): t0/t0], [b, modTime(index/file): t0/t0]]", indexState(modTimes)); + + // Remember the last modTime of index file. All modifications times of + // further modification are translated to this value so it looks that + // files have been modified in the same time slot as the index file + modTimes.add(Long.valueOf(db.getIndexFile().lastModified())); + + // modify one file + addToWorkDir("a", "a2"); + // now update the index the index. 'a' has to be racily clean -- because + // it's modification time is exactly the same as the previous index file + // mod time. + addToIndex(modTimes); + + dc = db.readDirCache(); + assertTrue(dc.getEntryCount() == 2); + assertTrue(dc.getEntry("a").isSmudged()); + assertFalse(dc.getEntry("b").isSmudged()); + + // although racily clean a should not be reported as beeing dirty + assertEquals("[[a, modTime(index/file): t0/t0, unsmudged], [b, modTime(index/file): t1/t1]]", indexState(modTimes)); + assertEquals("[[a, modTime(index/file): t0/t0, unsmudged], [b, modTime(index/file): t1/t1]]", indexState(modTimes)); + + } + + /** + * Waits until it is guaranteed that the filesystem timer (used e.g. for + * lastModified) has a value greater than the lastmodified time of the given + * file. This is done by touch a file, reading the lastmodified and sleeping + * attribute sleeping + * + * @param lastFile + * @return return the last measured value of the filesystem timer which is + * greater than then the lastmodification time of lastfile. + * @throws InterruptedException + * @throws IOException + */ + public static long fsTick(File lastFile) throws InterruptedException, + IOException { + long sleepTime = 1; + File tmp = File.createTempFile("FileTreeIteratorWithTimeControl", null); + try { + long startTime = (lastFile == null) ? tmp.lastModified() : lastFile + .lastModified(); + long actTime = tmp.lastModified(); + while (actTime <= startTime) { + Thread.sleep(sleepTime); + sleepTime *= 5; + tmp.setLastModified(System.currentTimeMillis()); + actTime = tmp.lastModified(); + } + return actTime; + } finally { + tmp.delete(); + } + } + + private void addToIndex(TreeSet modTimes) + throws FileNotFoundException, IOException { + DirCacheBuilder builder = db.lockDirCache().builder(); + FileTreeIterator fIt = new FileTreeIteratorWithTimeControl( + db, modTimes); + DirCacheEntry dce; + while (!fIt.eof()) { + dce = new DirCacheEntry(fIt.getEntryPathString()); + dce.setFileMode(fIt.getEntryFileMode()); + dce.setLastModified(fIt.getEntryLastModified()); + dce.setLength((int) fIt.getEntryLength()); + dce.setObjectId(fIt.getEntryObjectId()); + builder.add(dce); + fIt.next(1); + } + builder.commit(); + } + + private File addToWorkDir(String path, String content) throws IOException { + File f = new File(db.getWorkTree(), path); + FileOutputStream fos = new FileOutputStream(f); + try { + fos.write(content.getBytes(Constants.CHARACTER_ENCODING)); + return f; + } finally { + fos.close(); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryTestCase.java index e78f8512a..5c175d935 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryTestCase.java @@ -52,9 +52,20 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; import org.eclipse.jgit.storage.file.FileRepository; +import org.eclipse.jgit.treewalk.FileTreeIteratorWithTimeControl; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; /** * Base class for most JGit unit tests. @@ -114,4 +125,77 @@ protected void setUp() throws Exception { db = createWorkRepository(); trash = db.getWorkTree(); } + + public String indexState(TreeSet modTimes) + throws IllegalStateException, MissingObjectException, + IncorrectObjectTypeException, IOException { + DirCache dc = db.readDirCache(); + Map lookup = new HashMap(); + List ret = new ArrayList(dc.getEntryCount()); + NameConflictTreeWalk tw = new NameConflictTreeWalk(db); + tw.reset(); + tw.addTree(new FileTreeIteratorWithTimeControl(db, modTimes)); + tw.addTree(new DirCacheIterator(dc)); + boolean smudgedBefore; + while (tw.next()) { + List entry = new ArrayList(4); + FileTreeIteratorWithTimeControl fIt = tw.getTree(0, + FileTreeIteratorWithTimeControl.class); + DirCacheIterator dcIt = tw.getTree(1, DirCacheIterator.class); + entry.add(tw.getPathString()); + entry.add("modTime(index/file): " + + ((dcIt == null) ? "null" : lookup(Long.valueOf(dcIt + .getDirCacheEntry().getLastModified()), "t%n", + lookup)) + + "/" + + ((fIt == null) ? "null" : lookup( + Long.valueOf(fIt.getEntryLastModified()), "t%n", + lookup))); + smudgedBefore = (dcIt == null) ? false : dcIt.getDirCacheEntry() + .isSmudged(); + if (fIt != null + && dcIt != null + && fIt.isModified(dcIt.getDirCacheEntry(), true, true, + db.getFS())) + entry.add("dirty"); + if (dcIt != null && dcIt.getDirCacheEntry().isSmudged()) + entry.add("smudged"); + else if (smudgedBefore) + entry.add("unsmudged"); + ret.add(entry); + } + return ret.toString(); + } + + /** + * Helper method to map arbitrary objects to user-defined names. This can be + * used create short names for objects to produce small and stable debug + * output. It is guaranteed that when you lookup the same object multiple + * times even with different nameTemplates this method will always return + * the same name which was derived from the first nameTemplate. + * nameTemplates can contain "%n" which will be replaced by a running number + * before used as a name. + * + * @param l + * the object to lookup + * @param nameTemplate + * the name for that object. Can contain "%n" which will be + * replaced by a running number before used as a name. If the + * lookup table already contains the object this parameter will + * be ignored + * @param lookupTable + * a table storing object-name mappings. + * @return a name of that object. Is not guaranteed to be unique. Use + * nameTemplates containing "%n" to always have uniqe names + */ + public static String lookup(Object l, String nameTemplate, + Map lookupTable) { + String name = lookupTable.get(l); + if (name == null) { + name = nameTemplate.replaceAll("%n", + Integer.toString(lookupTable.size())); + lookupTable.put(l, name); + } + return name; + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java new file mode 100644 index 000000000..3bfa4fb57 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2010, Christian Halstrick + * 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.treewalk; + +import java.io.File; +import java.util.TreeSet; + +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; + +/** + * A {@link FileTreeIterator} used in tests which allows to specify explicitly + * what will be returned by {@link #getEntryLastModified()}. This allows to + * write tests where certain files have to have the same modification time. + *

+ * This iterator is configured by a list of strictly increasing long values + * t(0), t(1), ..., t(n). For each file with a modification between t(x) and + * t(x+1) [ t(x) <= time < t(x+1) ] this iterator will report t(x). For files + * with a modification time smaller t(0) a modification time of 0 is returned. + * For files with a modification time greater or equal t(n) t(n) will be + * returned. + *

+ * This class was written especially to test racy-git problems + */ +public class FileTreeIteratorWithTimeControl extends FileTreeIterator { + private TreeSet modTimes = new TreeSet(); + + public FileTreeIteratorWithTimeControl(FileTreeIterator p, Repository repo, + TreeSet modTimes) { + super(p, repo.getWorkTree(), repo.getFS()); + this.modTimes = modTimes; + } + + public FileTreeIteratorWithTimeControl(FileTreeIterator p, File f, FS fs, + TreeSet modTimes) { + super(p, f, fs); + this.modTimes = modTimes; + } + + public FileTreeIteratorWithTimeControl(Repository repo, + TreeSet modTimes) { + super(repo); + this.modTimes = modTimes; + } + + public FileTreeIteratorWithTimeControl(File f, FS fs, + TreeSet modTimes) { + super(f, fs); + this.modTimes = modTimes; + } + + @Override + public AbstractTreeIterator createSubtreeIterator(final ObjectReader reader) { + return new FileTreeIteratorWithTimeControl(this, + ((FileEntry) current()).file, fs, modTimes); + } + + @Override + public long getEntryLastModified() { + Long cutOff = modTimes + .floor(Long.valueOf(super.getEntryLastModified())); + return (cutOff == null) ? 0 : cutOff; + } +}