Test detecting modified packfiles

Test that JGit detects that packfiles have changed even if they are
repacked multiple times in one tick of the filesystem timer.

Test that this detection works also when repacking doesn't change the
length or the filekey of the packfile. In this case where a modified
file can't be detected by looking at file metadata JGit should still
detect too fast modification by racy git checks and trigger rescanning
the pack list and consequently rereading of packfile content.

Change-Id: I67682cfb807c58afc6de9375224ff7489d6618fb
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Christian Halstrick 2019-05-31 15:02:02 +02:00 committed by Matthias Sohn
parent b2ee9cfbc3
commit 5f8d91fded
1 changed files with 222 additions and 11 deletions

View File

@ -43,28 +43,50 @@
package org.eclipse.jgit.internal.storage.file;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
//import java.nio.file.attribute.BasicFileAttributes;
import java.text.ParseException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Random;
import java.util.zip.Deflater;
import org.eclipse.jgit.api.GarbageCollectCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.AbortedByHookException;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.NoMessageException;
import org.eclipse.jgit.api.errors.UnmergedPathsException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.storage.pack.PackConfig;
import org.junit.Test;
public class PackFileSnapshotTest extends RepositoryTestCase {
private static ObjectId unknownID = ObjectId
.fromString("1234567890123456789012345678901234567890");
@Test
public void testSamePackDifferentCompressionDetectChecksumChanged()
throws Exception {
@ -100,24 +122,213 @@ public void testSamePackDifferentCompressionDetectChecksumChanged()
assertTrue("expected checksum changed", snapshot.isChecksumChanged(pf));
}
private void appendRandomLine(File f) throws IOException {
private void appendRandomLine(File f, int length, Random r)
throws IOException {
try (Writer w = Files.newBufferedWriter(f.toPath(),
StandardOpenOption.APPEND)) {
w.append(randomLine(20));
appendRandomLine(w, length, r);
}
}
private String randomLine(int len) {
final int a = 97; // 'a'
int z = 122; // 'z'
Random random = new Random();
StringBuilder buffer = new StringBuilder(len);
private void appendRandomLine(File f) throws IOException {
appendRandomLine(f, 5, new Random());
}
private void appendRandomLine(Writer w, int len, Random r)
throws IOException {
final int c1 = 32; // ' '
int c2 = 126; // '~'
for (int i = 0; i < len; i++) {
int rnd = a + (int) (random.nextFloat() * (z - a + 1));
buffer.append((char) rnd);
w.append((char) (c1 + r.nextInt(1 + c2 - c1)));
}
buffer.append('\n');
return buffer.toString();
}
private ObjectId createTestRepo(int testDataSeed, int testDataLength)
throws IOException, GitAPIException, NoFilepatternException,
NoHeadException, NoMessageException, UnmergedPathsException,
ConcurrentRefUpdateException, WrongRepositoryStateException,
AbortedByHookException {
// Create a repo with two commits and one file. Each commit adds
// testDataLength number of bytes. Data are random bytes. Since the
// seed for the random number generator is specified we will get
// the same set of bytes for every run and for every platform
Random r = new Random(testDataSeed);
Git git = Git.wrap(db);
File f = writeTrashFile("file", "foobar ");
appendRandomLine(f, testDataLength, r);
git.add().addFilepattern("file").call();
git.commit().setMessage("message1").call();
appendRandomLine(f, testDataLength, r);
git.add().addFilepattern("file").call();
return git.commit().setMessage("message2").call().getId();
}
// Try repacking so fast that you get two new packs which differ only in
// content/chksum but have same name, size and lastmodified.
// Since this is done with standard gc (which creates new tmp files and
// renames them) the filekeys of the new packfiles differ helping jgit
// to detect the fast modification
@Test
public void testDetectModificationAlthoughSameSizeAndModificationtime()
throws Exception {
int testDataSeed = 1;
int testDataLength = 100;
FileBasedConfig config = db.getConfig();
// don't use mtime of the parent folder to detect pack file
// modification.
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, false);
config.save();
createTestRepo(testDataSeed, testDataLength);
// repack to create initial packfile
PackFile pf = repackAndCheck(5, null, null, null);
Path packFilePath = pf.getPackFile().toPath();
AnyObjectId chk1 = pf.getPackChecksum();
String name = pf.getPackName();
Long length = Long.valueOf(pf.getPackFile().length());
long m1 = packFilePath.toFile().lastModified();
// Wait for a filesystem timer tick to enhance probability the rest of
// this test is done before the filesystem timer ticks again.
fsTick(packFilePath.toFile());
// Repack to create packfile with same name, length. Lastmodified and
// content and checksum are different since compression level differs
AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
.getPackChecksum();
long m2 = packFilePath.toFile().lastModified();
assumeFalse(m2 == m1);
// Repack to create packfile with same name, length. Lastmodified is
// equal to the previous one because we are in the same filesystem timer
// slot. Content and its checksum are different
AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
.getPackChecksum();
long m3 = packFilePath.toFile().lastModified();
// ask for an unknown git object to force jgit to rescan the list of
// available packs. If we would ask for a known objectid then JGit would
// skip searching for new/modified packfiles
db.getObjectDatabase().has(unknownID);
assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
.getPackChecksum());
assumeTrue(m3 == m2);
}
// Try repacking so fast that we get two new packs which differ only in
// content and checksum but have same name, size and lastmodified.
// To avoid that JGit detects modification by checking the filekey create
// two new packfiles upfront and create copies of them. Then modify the
// packfiles in-place by opening them for write and then copying the
// content.
@Test
public void testDetectModificationAlthoughSameSizeAndModificationtimeAndFileKey()
throws Exception {
int testDataSeed = 1;
int testDataLength = 100;
FileBasedConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, false);
config.save();
createTestRepo(testDataSeed, testDataLength);
// Repack to create initial packfile. Make a copy of it
PackFile pf = repackAndCheck(5, null, null, null);
Path packFilePath = pf.getPackFile().toPath();
Path packFileBasePath = packFilePath.resolveSibling(
packFilePath.getFileName().toString().replaceAll(".pack", ""));
AnyObjectId chk1 = pf.getPackChecksum();
String name = pf.getPackName();
Long length = Long.valueOf(pf.getPackFile().length());
copyPack(packFileBasePath, "", ".copy1");
// Repack to create second packfile. Make a copy of it
AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
.getPackChecksum();
copyPack(packFileBasePath, "", ".copy2");
// Repack to create third packfile
AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
.getPackChecksum();
long m3 = packFilePath.toFile().lastModified();
db.getObjectDatabase().has(unknownID);
assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
.getPackChecksum());
// Wait for a filesystem timer tick to enhance probability the rest of
// this test is done before the filesystem timer ticks.
fsTick(packFilePath.toFile());
// Copy copy2 to packfile data to force modification of packfile without
// changing the packfile's filekey.
copyPack(packFileBasePath, ".copy2", "");
long m2 = packFilePath.toFile().lastModified();
assumeFalse(m3 == m2);
db.getObjectDatabase().has(unknownID);
assertEquals(chk2, getSinglePack(db.getObjectDatabase().getPacks())
.getPackChecksum());
// Copy copy2 to packfile data to force modification of packfile without
// changing the packfile's filekey.
copyPack(packFileBasePath, ".copy1", "");
long m1 = packFilePath.toFile().lastModified();
assumeTrue(m2 == m1);
db.getObjectDatabase().has(unknownID);
assertEquals(chk1, getSinglePack(db.getObjectDatabase().getPacks())
.getPackChecksum());
}
// Copy file from src to dst but avoid creating a new File (with new
// FileKey) if dst already exists
private Path copyFile(Path src, Path dst) throws IOException {
if (Files.exists(dst)) {
dst.toFile().setWritable(true);
try (OutputStream dstOut = Files.newOutputStream(dst)) {
Files.copy(src, dstOut);
return dst;
}
} else {
return Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
}
}
private Path copyPack(Path base, String srcSuffix, String dstSuffix)
throws IOException {
copyFile(Paths.get(base + ".idx" + srcSuffix),
Paths.get(base + ".idx" + dstSuffix));
copyFile(Paths.get(base + ".bitmap" + srcSuffix),
Paths.get(base + ".bitmap" + dstSuffix));
return copyFile(Paths.get(base + ".pack" + srcSuffix),
Paths.get(base + ".pack" + dstSuffix));
}
private PackFile repackAndCheck(int compressionLevel, String oldName,
Long oldLength, AnyObjectId oldChkSum)
throws IOException, ParseException {
PackFile p = getSinglePack(gc(compressionLevel));
File pf = p.getPackFile();
// The following two assumptions should not cause the test to fail. If
// on a certain platform we get packfiles (containing the same git
// objects) where the lengths differ or the checksums don't differ we
// just skip this test. A reason for that could be that compression
// works differently or random number generator works differently. Then
// we have to search for more consistent test data or checkin these
// packfiles as test resources
assumeTrue(oldLength == null || pf.length() == oldLength.longValue());
assumeTrue(oldChkSum == null || !p.getPackChecksum().equals(oldChkSum));
assertTrue(oldName == null || p.getPackName().equals(oldName));
return p;
}
private PackFile getSinglePack(Collection<PackFile> packs) {
Iterator<PackFile> pIt = packs.iterator();
PackFile p = pIt.next();
assertFalse(pIt.hasNext());
return p;
}
private Collection<PackFile> gc(int compressionLevel)