You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PackFileSnapshotTest.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /*
  2. * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.internal.storage.file;
  44. import static org.junit.Assert.assertEquals;
  45. import static org.junit.Assert.assertFalse;
  46. import static org.junit.Assert.assertTrue;
  47. import static org.junit.Assume.assumeFalse;
  48. import static org.junit.Assume.assumeTrue;
  49. import java.io.File;
  50. import java.io.IOException;
  51. import java.io.OutputStream;
  52. import java.io.Writer;
  53. import java.nio.file.Files;
  54. import java.nio.file.Path;
  55. import java.nio.file.Paths;
  56. import java.nio.file.StandardCopyOption;
  57. import java.nio.file.StandardOpenOption;
  58. //import java.nio.file.attribute.BasicFileAttributes;
  59. import java.text.ParseException;
  60. import java.time.Instant;
  61. import java.util.Collection;
  62. import java.util.Iterator;
  63. import java.util.Random;
  64. import java.util.zip.Deflater;
  65. import org.eclipse.jgit.api.GarbageCollectCommand;
  66. import org.eclipse.jgit.api.Git;
  67. import org.eclipse.jgit.api.errors.AbortedByHookException;
  68. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  69. import org.eclipse.jgit.api.errors.GitAPIException;
  70. import org.eclipse.jgit.api.errors.NoFilepatternException;
  71. import org.eclipse.jgit.api.errors.NoHeadException;
  72. import org.eclipse.jgit.api.errors.NoMessageException;
  73. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  74. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  75. import org.eclipse.jgit.junit.RepositoryTestCase;
  76. import org.eclipse.jgit.lib.AnyObjectId;
  77. import org.eclipse.jgit.lib.ConfigConstants;
  78. import org.eclipse.jgit.lib.ObjectId;
  79. import org.eclipse.jgit.storage.file.FileBasedConfig;
  80. import org.eclipse.jgit.storage.pack.PackConfig;
  81. import org.eclipse.jgit.util.FS;
  82. import org.junit.Test;
  83. public class PackFileSnapshotTest extends RepositoryTestCase {
  84. private static ObjectId unknownID = ObjectId
  85. .fromString("1234567890123456789012345678901234567890");
  86. @Test
  87. public void testSamePackDifferentCompressionDetectChecksumChanged()
  88. throws Exception {
  89. Git git = Git.wrap(db);
  90. File f = writeTrashFile("file", "foobar ");
  91. for (int i = 0; i < 10; i++) {
  92. appendRandomLine(f);
  93. git.add().addFilepattern("file").call();
  94. git.commit().setMessage("message" + i).call();
  95. }
  96. FileBasedConfig c = db.getConfig();
  97. c.setInt(ConfigConstants.CONFIG_GC_SECTION, null,
  98. ConfigConstants.CONFIG_KEY_AUTOPACKLIMIT, 1);
  99. c.save();
  100. Collection<PackFile> packs = gc(Deflater.NO_COMPRESSION);
  101. assertEquals("expected 1 packfile after gc", 1, packs.size());
  102. PackFile p1 = packs.iterator().next();
  103. PackFileSnapshot snapshot = p1.getFileSnapshot();
  104. packs = gc(Deflater.BEST_COMPRESSION);
  105. assertEquals("expected 1 packfile after gc", 1, packs.size());
  106. PackFile p2 = packs.iterator().next();
  107. File pf = p2.getPackFile();
  108. // changing compression level with aggressive gc may change size,
  109. // fileKey (on *nix) and checksum. Hence FileSnapshot.isModified can
  110. // return true already based on size or fileKey.
  111. // So the only thing we can test here is that we ensure that checksum
  112. // also changed when we read it here in this test
  113. assertTrue("expected snapshot to detect modified pack",
  114. snapshot.isModified(pf));
  115. assertTrue("expected checksum changed", snapshot.isChecksumChanged(pf));
  116. }
  117. private void appendRandomLine(File f, int length, Random r)
  118. throws IOException {
  119. try (Writer w = Files.newBufferedWriter(f.toPath(),
  120. StandardOpenOption.APPEND)) {
  121. appendRandomLine(w, length, r);
  122. }
  123. }
  124. private void appendRandomLine(File f) throws IOException {
  125. appendRandomLine(f, 5, new Random());
  126. }
  127. private void appendRandomLine(Writer w, int len, Random r)
  128. throws IOException {
  129. final int c1 = 32; // ' '
  130. int c2 = 126; // '~'
  131. for (int i = 0; i < len; i++) {
  132. w.append((char) (c1 + r.nextInt(1 + c2 - c1)));
  133. }
  134. }
  135. private ObjectId createTestRepo(int testDataSeed, int testDataLength)
  136. throws IOException, GitAPIException, NoFilepatternException,
  137. NoHeadException, NoMessageException, UnmergedPathsException,
  138. ConcurrentRefUpdateException, WrongRepositoryStateException,
  139. AbortedByHookException {
  140. // Create a repo with two commits and one file. Each commit adds
  141. // testDataLength number of bytes. Data are random bytes. Since the
  142. // seed for the random number generator is specified we will get
  143. // the same set of bytes for every run and for every platform
  144. Random r = new Random(testDataSeed);
  145. Git git = Git.wrap(db);
  146. File f = writeTrashFile("file", "foobar ");
  147. appendRandomLine(f, testDataLength, r);
  148. git.add().addFilepattern("file").call();
  149. git.commit().setMessage("message1").call();
  150. appendRandomLine(f, testDataLength, r);
  151. git.add().addFilepattern("file").call();
  152. return git.commit().setMessage("message2").call().getId();
  153. }
  154. // Try repacking so fast that you get two new packs which differ only in
  155. // content/chksum but have same name, size and lastmodified.
  156. // Since this is done with standard gc (which creates new tmp files and
  157. // renames them) the filekeys of the new packfiles differ helping jgit
  158. // to detect the fast modification
  159. @Test
  160. public void testDetectModificationAlthoughSameSizeAndModificationtime()
  161. throws Exception {
  162. int testDataSeed = 1;
  163. int testDataLength = 100;
  164. FileBasedConfig config = db.getConfig();
  165. // don't use mtime of the parent folder to detect pack file
  166. // modification.
  167. config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
  168. ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, false);
  169. config.save();
  170. createTestRepo(testDataSeed, testDataLength);
  171. // repack to create initial packfile
  172. PackFile pf = repackAndCheck(5, null, null, null);
  173. Path packFilePath = pf.getPackFile().toPath();
  174. AnyObjectId chk1 = pf.getPackChecksum();
  175. String name = pf.getPackName();
  176. Long length = Long.valueOf(pf.getPackFile().length());
  177. FS fs = db.getFS();
  178. Instant m1 = fs.lastModifiedInstant(packFilePath);
  179. // Wait for a filesystem timer tick to enhance probability the rest of
  180. // this test is done before the filesystem timer ticks again.
  181. fsTick(packFilePath.toFile());
  182. // Repack to create packfile with same name, length. Lastmodified and
  183. // content and checksum are different since compression level differs
  184. AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
  185. .getPackChecksum();
  186. Instant m2 = fs.lastModifiedInstant(packFilePath);
  187. assumeFalse(m2.equals(m1));
  188. // Repack to create packfile with same name, length. Lastmodified is
  189. // equal to the previous one because we are in the same filesystem timer
  190. // slot. Content and its checksum are different
  191. AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
  192. .getPackChecksum();
  193. Instant m3 = fs.lastModifiedInstant(packFilePath);
  194. // ask for an unknown git object to force jgit to rescan the list of
  195. // available packs. If we would ask for a known objectid then JGit would
  196. // skip searching for new/modified packfiles
  197. db.getObjectDatabase().has(unknownID);
  198. assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
  199. .getPackChecksum());
  200. assumeTrue(m3.equals(m2));
  201. }
  202. // Try repacking so fast that we get two new packs which differ only in
  203. // content and checksum but have same name, size and lastmodified.
  204. // To avoid that JGit detects modification by checking the filekey create
  205. // two new packfiles upfront and create copies of them. Then modify the
  206. // packfiles in-place by opening them for write and then copying the
  207. // content.
  208. @Test
  209. public void testDetectModificationAlthoughSameSizeAndModificationtimeAndFileKey()
  210. throws Exception {
  211. int testDataSeed = 1;
  212. int testDataLength = 100;
  213. FileBasedConfig config = db.getConfig();
  214. config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
  215. ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, false);
  216. config.save();
  217. createTestRepo(testDataSeed, testDataLength);
  218. // Repack to create initial packfile. Make a copy of it
  219. PackFile pf = repackAndCheck(5, null, null, null);
  220. Path packFilePath = pf.getPackFile().toPath();
  221. Path packFileBasePath = packFilePath.resolveSibling(
  222. packFilePath.getFileName().toString().replaceAll(".pack", ""));
  223. AnyObjectId chk1 = pf.getPackChecksum();
  224. String name = pf.getPackName();
  225. Long length = Long.valueOf(pf.getPackFile().length());
  226. copyPack(packFileBasePath, "", ".copy1");
  227. // Repack to create second packfile. Make a copy of it
  228. AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
  229. .getPackChecksum();
  230. copyPack(packFileBasePath, "", ".copy2");
  231. // Repack to create third packfile
  232. AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
  233. .getPackChecksum();
  234. FS fs = db.getFS();
  235. Instant m3 = fs.lastModifiedInstant(packFilePath);
  236. db.getObjectDatabase().has(unknownID);
  237. assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
  238. .getPackChecksum());
  239. // Wait for a filesystem timer tick to enhance probability the rest of
  240. // this test is done before the filesystem timer ticks.
  241. fsTick(packFilePath.toFile());
  242. // Copy copy2 to packfile data to force modification of packfile without
  243. // changing the packfile's filekey.
  244. copyPack(packFileBasePath, ".copy2", "");
  245. Instant m2 = fs.lastModifiedInstant(packFilePath);
  246. assumeFalse(m3.equals(m2));
  247. db.getObjectDatabase().has(unknownID);
  248. assertEquals(chk2, getSinglePack(db.getObjectDatabase().getPacks())
  249. .getPackChecksum());
  250. // Copy copy2 to packfile data to force modification of packfile without
  251. // changing the packfile's filekey.
  252. copyPack(packFileBasePath, ".copy1", "");
  253. Instant m1 = fs.lastModifiedInstant(packFilePath);
  254. assumeTrue(m2.equals(m1));
  255. db.getObjectDatabase().has(unknownID);
  256. assertEquals(chk1, getSinglePack(db.getObjectDatabase().getPacks())
  257. .getPackChecksum());
  258. }
  259. // Copy file from src to dst but avoid creating a new File (with new
  260. // FileKey) if dst already exists
  261. private Path copyFile(Path src, Path dst) throws IOException {
  262. if (Files.exists(dst)) {
  263. dst.toFile().setWritable(true);
  264. try (OutputStream dstOut = Files.newOutputStream(dst)) {
  265. Files.copy(src, dstOut);
  266. return dst;
  267. }
  268. }
  269. return Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
  270. }
  271. private Path copyPack(Path base, String srcSuffix, String dstSuffix)
  272. throws IOException {
  273. copyFile(Paths.get(base + ".idx" + srcSuffix),
  274. Paths.get(base + ".idx" + dstSuffix));
  275. copyFile(Paths.get(base + ".bitmap" + srcSuffix),
  276. Paths.get(base + ".bitmap" + dstSuffix));
  277. return copyFile(Paths.get(base + ".pack" + srcSuffix),
  278. Paths.get(base + ".pack" + dstSuffix));
  279. }
  280. private PackFile repackAndCheck(int compressionLevel, String oldName,
  281. Long oldLength, AnyObjectId oldChkSum)
  282. throws IOException, ParseException {
  283. PackFile p = getSinglePack(gc(compressionLevel));
  284. File pf = p.getPackFile();
  285. // The following two assumptions should not cause the test to fail. If
  286. // on a certain platform we get packfiles (containing the same git
  287. // objects) where the lengths differ or the checksums don't differ we
  288. // just skip this test. A reason for that could be that compression
  289. // works differently or random number generator works differently. Then
  290. // we have to search for more consistent test data or checkin these
  291. // packfiles as test resources
  292. assumeTrue(oldLength == null || pf.length() == oldLength.longValue());
  293. assumeTrue(oldChkSum == null || !p.getPackChecksum().equals(oldChkSum));
  294. assertTrue(oldName == null || p.getPackName().equals(oldName));
  295. return p;
  296. }
  297. private PackFile getSinglePack(Collection<PackFile> packs) {
  298. Iterator<PackFile> pIt = packs.iterator();
  299. PackFile p = pIt.next();
  300. assertFalse(pIt.hasNext());
  301. return p;
  302. }
  303. private Collection<PackFile> gc(int compressionLevel)
  304. throws IOException, ParseException {
  305. GC gc = new GC(db);
  306. PackConfig pc = new PackConfig(db.getConfig());
  307. pc.setCompressionLevel(compressionLevel);
  308. pc.setSinglePack(true);
  309. // --aggressive
  310. pc.setDeltaSearchWindowSize(
  311. GarbageCollectCommand.DEFAULT_GC_AGGRESSIVE_WINDOW);
  312. pc.setMaxDeltaDepth(GarbageCollectCommand.DEFAULT_GC_AGGRESSIVE_DEPTH);
  313. pc.setReuseObjects(false);
  314. gc.setPackConfig(pc);
  315. gc.setExpireAgeMillis(0);
  316. gc.setPackExpireAgeMillis(0);
  317. return gc.gc();
  318. }
  319. }