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.

LocalDiskRepositoryTestCase.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. /*
  2. * Copyright (C) 2009-2010, Google Inc.
  3. * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  4. * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> and others
  5. *
  6. * This program and the accompanying materials are made available under the
  7. * terms of the Eclipse Distribution License v. 1.0 which is available at
  8. * https://www.eclipse.org/org/documents/edl-v10.php.
  9. *
  10. * SPDX-License-Identifier: BSD-3-Clause
  11. */
  12. package org.eclipse.jgit.junit;
  13. import static java.nio.charset.StandardCharsets.UTF_8;
  14. import static org.junit.Assert.assertFalse;
  15. import static org.junit.Assert.fail;
  16. import java.io.File;
  17. import java.io.IOException;
  18. import java.io.PrintStream;
  19. import java.time.Instant;
  20. import java.util.ArrayList;
  21. import java.util.Collections;
  22. import java.util.HashMap;
  23. import java.util.HashSet;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.TreeSet;
  28. import org.eclipse.jgit.dircache.DirCache;
  29. import org.eclipse.jgit.dircache.DirCacheEntry;
  30. import org.eclipse.jgit.internal.storage.file.FileRepository;
  31. import org.eclipse.jgit.lib.ConfigConstants;
  32. import org.eclipse.jgit.lib.Constants;
  33. import org.eclipse.jgit.lib.ObjectId;
  34. import org.eclipse.jgit.lib.PersonIdent;
  35. import org.eclipse.jgit.lib.Repository;
  36. import org.eclipse.jgit.lib.RepositoryCache;
  37. import org.eclipse.jgit.storage.file.FileBasedConfig;
  38. import org.eclipse.jgit.storage.file.WindowCacheConfig;
  39. import org.eclipse.jgit.util.FS;
  40. import org.eclipse.jgit.util.FileUtils;
  41. import org.eclipse.jgit.util.SystemReader;
  42. import org.junit.After;
  43. import org.junit.Before;
  44. /**
  45. * JUnit TestCase with specialized support for temporary local repository.
  46. * <p>
  47. * A temporary directory is created for each test, allowing each test to use a
  48. * fresh environment. The temporary directory is cleaned up after the test ends.
  49. * <p>
  50. * Callers should not use {@link org.eclipse.jgit.lib.RepositoryCache} from
  51. * within these tests as it may wedge file descriptors open past the end of the
  52. * test.
  53. * <p>
  54. * A system property {@code jgit.junit.usemmap} defines whether memory mapping
  55. * is used. Memory mapping has an effect on the file system, in that memory
  56. * mapped files in Java cannot be deleted as long as the mapped arrays have not
  57. * been reclaimed by the garbage collector. The programmer cannot control this
  58. * with precision, so temporary files may hang around longer than desired during
  59. * a test, or tests may fail altogether if there is insufficient file
  60. * descriptors or address space for the test process.
  61. */
  62. public abstract class LocalDiskRepositoryTestCase {
  63. private static final boolean useMMAP = "true".equals(System
  64. .getProperty("jgit.junit.usemmap"));
  65. /** A fake (but stable) identity for author fields in the test. */
  66. protected PersonIdent author;
  67. /** A fake (but stable) identity for committer fields in the test. */
  68. protected PersonIdent committer;
  69. /**
  70. * A {@link SystemReader} used to coordinate time, envars, etc.
  71. * @since 4.2
  72. */
  73. protected MockSystemReader mockSystemReader;
  74. private final Set<Repository> toClose = new HashSet<>();
  75. private File tmp;
  76. /**
  77. * Setup test
  78. *
  79. * @throws Exception
  80. */
  81. @Before
  82. public void setUp() throws Exception {
  83. tmp = File.createTempFile("jgit_test_", "_tmp");
  84. CleanupThread.deleteOnShutdown(tmp);
  85. if (!tmp.delete() || !tmp.mkdir()) {
  86. throw new IOException("Cannot create " + tmp);
  87. }
  88. mockSystemReader = new MockSystemReader();
  89. SystemReader.setInstance(mockSystemReader);
  90. // Measure timer resolution before the test to avoid time critical tests
  91. // are affected by time needed for measurement.
  92. // The MockSystemReader must be configured first since we need to use
  93. // the same one here
  94. FS.getFileStoreAttributes(tmp.toPath().getParent());
  95. FileBasedConfig jgitConfig = new FileBasedConfig(
  96. new File(tmp, "jgitconfig"), FS.DETECTED);
  97. FileBasedConfig systemConfig = new FileBasedConfig(jgitConfig,
  98. new File(tmp, "systemgitconfig"), FS.DETECTED);
  99. FileBasedConfig userConfig = new FileBasedConfig(systemConfig,
  100. new File(tmp, "usergitconfig"), FS.DETECTED);
  101. // We have to set autoDetach to false for tests, because tests expect to be able
  102. // to clean up by recursively removing the repository, and background GC might be
  103. // in the middle of writing or deleting files, which would disrupt this.
  104. userConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION,
  105. null, ConfigConstants.CONFIG_KEY_AUTODETACH, false);
  106. userConfig.save();
  107. mockSystemReader.setJGitConfig(jgitConfig);
  108. mockSystemReader.setSystemGitConfig(systemConfig);
  109. mockSystemReader.setUserGitConfig(userConfig);
  110. ceilTestDirectories(getCeilings());
  111. author = new PersonIdent("J. Author", "jauthor@example.com");
  112. committer = new PersonIdent("J. Committer", "jcommitter@example.com");
  113. final WindowCacheConfig c = new WindowCacheConfig();
  114. c.setPackedGitLimit(128 * WindowCacheConfig.KB);
  115. c.setPackedGitWindowSize(8 * WindowCacheConfig.KB);
  116. c.setPackedGitMMAP(useMMAP);
  117. c.setDeltaBaseCacheLimit(8 * WindowCacheConfig.KB);
  118. c.install();
  119. }
  120. /**
  121. * Get temporary directory.
  122. *
  123. * @return the temporary directory
  124. */
  125. protected File getTemporaryDirectory() {
  126. return tmp.getAbsoluteFile();
  127. }
  128. /**
  129. * Get list of ceiling directories
  130. *
  131. * @return list of ceiling directories
  132. */
  133. protected List<File> getCeilings() {
  134. return Collections.singletonList(getTemporaryDirectory());
  135. }
  136. private void ceilTestDirectories(List<File> ceilings) {
  137. mockSystemReader.setProperty(Constants.GIT_CEILING_DIRECTORIES_KEY, makePath(ceilings));
  138. }
  139. private static String makePath(List<?> objects) {
  140. final StringBuilder stringBuilder = new StringBuilder();
  141. for (Object object : objects) {
  142. if (stringBuilder.length() > 0)
  143. stringBuilder.append(File.pathSeparatorChar);
  144. stringBuilder.append(object.toString());
  145. }
  146. return stringBuilder.toString();
  147. }
  148. /**
  149. * Tear down the test
  150. *
  151. * @throws Exception
  152. */
  153. @After
  154. public void tearDown() throws Exception {
  155. RepositoryCache.clear();
  156. for (Repository r : toClose)
  157. r.close();
  158. toClose.clear();
  159. // Since memory mapping is controlled by the GC we need to
  160. // tell it this is a good time to clean up and unlock
  161. // memory mapped files.
  162. //
  163. if (useMMAP)
  164. System.gc();
  165. if (tmp != null)
  166. recursiveDelete(tmp, false, true);
  167. if (tmp != null && !tmp.exists())
  168. CleanupThread.removed(tmp);
  169. SystemReader.setInstance(null);
  170. }
  171. /**
  172. * Increment the {@link #author} and {@link #committer} times.
  173. */
  174. protected void tick() {
  175. mockSystemReader.tick(5 * 60);
  176. final long now = mockSystemReader.getCurrentTime();
  177. final int tz = mockSystemReader.getTimezone(now);
  178. author = new PersonIdent(author, now, tz);
  179. committer = new PersonIdent(committer, now, tz);
  180. }
  181. /**
  182. * Recursively delete a directory, failing the test if the delete fails.
  183. *
  184. * @param dir
  185. * the recursively directory to delete, if present.
  186. */
  187. protected void recursiveDelete(File dir) {
  188. recursiveDelete(dir, false, true);
  189. }
  190. private static boolean recursiveDelete(final File dir,
  191. boolean silent, boolean failOnError) {
  192. assert !(silent && failOnError);
  193. int options = FileUtils.RECURSIVE | FileUtils.RETRY
  194. | FileUtils.SKIP_MISSING;
  195. if (silent) {
  196. options |= FileUtils.IGNORE_ERRORS;
  197. }
  198. try {
  199. FileUtils.delete(dir, options);
  200. } catch (IOException e) {
  201. reportDeleteFailure(failOnError, dir, e);
  202. return !failOnError;
  203. }
  204. return true;
  205. }
  206. private static void reportDeleteFailure(boolean failOnError, File f,
  207. Exception cause) {
  208. String severity = failOnError ? "ERROR" : "WARNING";
  209. String msg = severity + ": Failed to delete " + f;
  210. if (failOnError) {
  211. fail(msg);
  212. } else {
  213. System.err.println(msg);
  214. }
  215. cause.printStackTrace(new PrintStream(System.err));
  216. }
  217. /** Constant <code>MOD_TIME=1</code> */
  218. public static final int MOD_TIME = 1;
  219. /** Constant <code>SMUDGE=2</code> */
  220. public static final int SMUDGE = 2;
  221. /** Constant <code>LENGTH=4</code> */
  222. public static final int LENGTH = 4;
  223. /** Constant <code>CONTENT_ID=8</code> */
  224. public static final int CONTENT_ID = 8;
  225. /** Constant <code>CONTENT=16</code> */
  226. public static final int CONTENT = 16;
  227. /** Constant <code>ASSUME_UNCHANGED=32</code> */
  228. public static final int ASSUME_UNCHANGED = 32;
  229. /**
  230. * Represent the state of the index in one String. This representation is
  231. * useful when writing tests which do assertions on the state of the index.
  232. * By default information about path, mode, stage (if different from 0) is
  233. * included. A bitmask controls which additional info about
  234. * modificationTimes, smudge state and length is included.
  235. * <p>
  236. * The format of the returned string is described with this BNF:
  237. *
  238. * <pre>
  239. * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* .
  240. * mode = ", mode:" number .
  241. * stage = ", stage:" number .
  242. * time = ", time:t" timestamp-index .
  243. * smudge = "" | ", smudged" .
  244. * length = ", length:" number .
  245. * sha1 = ", sha1:" hex-sha1 .
  246. * content = ", content:" blob-data .
  247. * </pre>
  248. *
  249. * 'stage' is only presented when the stage is different from 0. All
  250. * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The
  251. * smallest reported time-stamp will be called "t0". This allows to write
  252. * assertions against the string although the concrete value of the time
  253. * stamps is unknown.
  254. *
  255. * @param repo
  256. * the repository the index state should be determined for
  257. * @param includedOptions
  258. * a bitmask constructed out of the constants {@link #MOD_TIME},
  259. * {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and
  260. * {@link #CONTENT} controlling which info is present in the
  261. * resulting string.
  262. * @return a string encoding the index state
  263. * @throws IllegalStateException
  264. * @throws IOException
  265. */
  266. public static String indexState(Repository repo, int includedOptions)
  267. throws IllegalStateException, IOException {
  268. DirCache dc = repo.readDirCache();
  269. StringBuilder sb = new StringBuilder();
  270. TreeSet<Instant> timeStamps = new TreeSet<>();
  271. // iterate once over the dircache just to collect all time stamps
  272. if (0 != (includedOptions & MOD_TIME)) {
  273. for (int i = 0; i < dc.getEntryCount(); ++i) {
  274. timeStamps.add(dc.getEntry(i).getLastModifiedInstant());
  275. }
  276. }
  277. // iterate again, now produce the result string
  278. for (int i=0; i<dc.getEntryCount(); ++i) {
  279. DirCacheEntry entry = dc.getEntry(i);
  280. sb.append("["+entry.getPathString()+", mode:" + entry.getFileMode());
  281. int stage = entry.getStage();
  282. if (stage != 0)
  283. sb.append(", stage:" + stage);
  284. if (0 != (includedOptions & MOD_TIME)) {
  285. sb.append(", time:t"+
  286. timeStamps.headSet(entry.getLastModifiedInstant())
  287. .size());
  288. }
  289. if (0 != (includedOptions & SMUDGE))
  290. if (entry.isSmudged())
  291. sb.append(", smudged");
  292. if (0 != (includedOptions & LENGTH))
  293. sb.append(", length:"
  294. + Integer.toString(entry.getLength()));
  295. if (0 != (includedOptions & CONTENT_ID))
  296. sb.append(", sha1:" + ObjectId.toString(entry.getObjectId()));
  297. if (0 != (includedOptions & CONTENT)) {
  298. sb.append(", content:"
  299. + new String(repo.open(entry.getObjectId(),
  300. Constants.OBJ_BLOB).getCachedBytes(), UTF_8));
  301. }
  302. if (0 != (includedOptions & ASSUME_UNCHANGED))
  303. sb.append(", assume-unchanged:"
  304. + Boolean.toString(entry.isAssumeValid()));
  305. sb.append("]");
  306. }
  307. return sb.toString();
  308. }
  309. /**
  310. * Creates a new empty bare repository.
  311. *
  312. * @return the newly created bare repository, opened for access. The
  313. * repository will not be closed in {@link #tearDown()}; the caller
  314. * is responsible for closing it.
  315. * @throws IOException
  316. * the repository could not be created in the temporary area
  317. */
  318. protected FileRepository createBareRepository() throws IOException {
  319. return createRepository(true /* bare */);
  320. }
  321. /**
  322. * Creates a new empty repository within a new empty working directory.
  323. *
  324. * @return the newly created repository, opened for access. The repository
  325. * will not be closed in {@link #tearDown()}; the caller is
  326. * responsible for closing it.
  327. * @throws IOException
  328. * the repository could not be created in the temporary area
  329. */
  330. protected FileRepository createWorkRepository() throws IOException {
  331. return createRepository(false /* not bare */);
  332. }
  333. /**
  334. * Creates a new empty repository.
  335. *
  336. * @param bare
  337. * true to create a bare repository; false to make a repository
  338. * within its working directory
  339. * @return the newly created repository, opened for access. The repository
  340. * will not be closed in {@link #tearDown()}; the caller is
  341. * responsible for closing it.
  342. * @throws IOException
  343. * the repository could not be created in the temporary area
  344. * @since 5.3
  345. */
  346. protected FileRepository createRepository(boolean bare)
  347. throws IOException {
  348. return createRepository(bare, false /* auto close */);
  349. }
  350. /**
  351. * Creates a new empty repository.
  352. *
  353. * @param bare
  354. * true to create a bare repository; false to make a repository
  355. * within its working directory
  356. * @param autoClose
  357. * auto close the repository in {@link #tearDown()}
  358. * @return the newly created repository, opened for access
  359. * @throws IOException
  360. * the repository could not be created in the temporary area
  361. * @deprecated use {@link #createRepository(boolean)} instead
  362. */
  363. @Deprecated
  364. public FileRepository createRepository(boolean bare, boolean autoClose)
  365. throws IOException {
  366. File gitdir = createUniqueTestGitDir(bare);
  367. FileRepository db = new FileRepository(gitdir);
  368. assertFalse(gitdir.exists());
  369. db.create(bare);
  370. if (autoClose) {
  371. addRepoToClose(db);
  372. }
  373. return db;
  374. }
  375. /**
  376. * Adds a repository to the list of repositories which is closed at the end
  377. * of the tests
  378. *
  379. * @param r
  380. * the repository to be closed
  381. */
  382. public void addRepoToClose(Repository r) {
  383. toClose.add(r);
  384. }
  385. /**
  386. * Creates a unique directory for a test
  387. *
  388. * @param name
  389. * a subdirectory
  390. * @return a unique directory for a test
  391. * @throws IOException
  392. */
  393. protected File createTempDirectory(String name) throws IOException {
  394. File directory = new File(createTempFile(), name);
  395. FileUtils.mkdirs(directory);
  396. return directory.getCanonicalFile();
  397. }
  398. /**
  399. * Creates a new unique directory for a test repository
  400. *
  401. * @param bare
  402. * true for a bare repository; false for a repository with a
  403. * working directory
  404. * @return a unique directory for a test repository
  405. * @throws IOException
  406. */
  407. protected File createUniqueTestGitDir(boolean bare) throws IOException {
  408. String gitdirName = createTempFile().getPath();
  409. if (!bare)
  410. gitdirName += "/";
  411. return new File(gitdirName + Constants.DOT_GIT);
  412. }
  413. /**
  414. * Allocates a new unique file path that does not exist.
  415. * <p>
  416. * Unlike the standard {@code File.createTempFile} the returned path does
  417. * not exist, but may be created by another thread in a race with the
  418. * caller. Good luck.
  419. * <p>
  420. * This method is inherently unsafe due to a race condition between creating
  421. * the name and the first use that reserves it.
  422. *
  423. * @return a unique path that does not exist.
  424. * @throws IOException
  425. */
  426. protected File createTempFile() throws IOException {
  427. File p = File.createTempFile("tmp_", "", tmp);
  428. if (!p.delete()) {
  429. throw new IOException("Cannot obtain unique path " + tmp);
  430. }
  431. return p;
  432. }
  433. /**
  434. * Run a hook script in the repository, returning the exit status.
  435. *
  436. * @param db
  437. * repository the script should see in GIT_DIR environment
  438. * @param hook
  439. * path of the hook script to execute, must be executable file
  440. * type on this platform
  441. * @param args
  442. * arguments to pass to the hook script
  443. * @return exit status code of the invoked hook
  444. * @throws IOException
  445. * the hook could not be executed
  446. * @throws InterruptedException
  447. * the caller was interrupted before the hook completed
  448. */
  449. protected int runHook(final Repository db, final File hook,
  450. final String... args) throws IOException, InterruptedException {
  451. final String[] argv = new String[1 + args.length];
  452. argv[0] = hook.getAbsolutePath();
  453. System.arraycopy(args, 0, argv, 1, args.length);
  454. final Map<String, String> env = cloneEnv();
  455. env.put("GIT_DIR", db.getDirectory().getAbsolutePath());
  456. putPersonIdent(env, "AUTHOR", author);
  457. putPersonIdent(env, "COMMITTER", committer);
  458. final File cwd = db.getWorkTree();
  459. final Process p = Runtime.getRuntime().exec(argv, toEnvArray(env), cwd);
  460. p.getOutputStream().close();
  461. p.getErrorStream().close();
  462. p.getInputStream().close();
  463. return p.waitFor();
  464. }
  465. private static void putPersonIdent(final Map<String, String> env,
  466. final String type, final PersonIdent who) {
  467. final String ident = who.toExternalString();
  468. final String date = ident.substring(ident.indexOf("> ") + 2);
  469. env.put("GIT_" + type + "_NAME", who.getName());
  470. env.put("GIT_" + type + "_EMAIL", who.getEmailAddress());
  471. env.put("GIT_" + type + "_DATE", date);
  472. }
  473. /**
  474. * Create a string to a UTF-8 temporary file and return the path.
  475. *
  476. * @param body
  477. * complete content to write to the file. If the file should end
  478. * with a trailing LF, the string should end with an LF.
  479. * @return path of the temporary file created within the trash area.
  480. * @throws IOException
  481. * the file could not be written.
  482. */
  483. protected File write(String body) throws IOException {
  484. final File f = File.createTempFile("temp", "txt", tmp);
  485. try {
  486. write(f, body);
  487. return f;
  488. } catch (Error | RuntimeException | IOException e) {
  489. f.delete();
  490. throw e;
  491. }
  492. }
  493. /**
  494. * Write a string as a UTF-8 file.
  495. *
  496. * @param f
  497. * file to write the string to. Caller is responsible for making
  498. * sure it is in the trash directory or will otherwise be cleaned
  499. * up at the end of the test. If the parent directory does not
  500. * exist, the missing parent directories are automatically
  501. * created.
  502. * @param body
  503. * content to write to the file.
  504. * @throws IOException
  505. * the file could not be written.
  506. */
  507. protected void write(File f, String body) throws IOException {
  508. JGitTestUtil.write(f, body);
  509. }
  510. /**
  511. * Read a file's content
  512. *
  513. * @param f
  514. * the file
  515. * @return the content of the file
  516. * @throws IOException
  517. */
  518. protected String read(File f) throws IOException {
  519. return JGitTestUtil.read(f);
  520. }
  521. private static String[] toEnvArray(Map<String, String> env) {
  522. final String[] envp = new String[env.size()];
  523. int i = 0;
  524. for (Map.Entry<String, String> e : env.entrySet())
  525. envp[i++] = e.getKey() + "=" + e.getValue();
  526. return envp;
  527. }
  528. private static HashMap<String, String> cloneEnv() {
  529. return new HashMap<>(System.getenv());
  530. }
  531. private static final class CleanupThread extends Thread {
  532. private static final CleanupThread me;
  533. static {
  534. me = new CleanupThread();
  535. Runtime.getRuntime().addShutdownHook(me);
  536. }
  537. static void deleteOnShutdown(File tmp) {
  538. synchronized (me) {
  539. me.toDelete.add(tmp);
  540. }
  541. }
  542. static void removed(File tmp) {
  543. synchronized (me) {
  544. me.toDelete.remove(tmp);
  545. }
  546. }
  547. private final List<File> toDelete = new ArrayList<>();
  548. @Override
  549. public void run() {
  550. // On windows accidentally open files or memory
  551. // mapped regions may prevent files from being deleted.
  552. // Suggesting a GC increases the likelihood that our
  553. // test repositories actually get removed after the
  554. // tests, even in the case of failure.
  555. System.gc();
  556. synchronized (this) {
  557. boolean silent = false;
  558. boolean failOnError = false;
  559. for (File tmp : toDelete)
  560. recursiveDelete(tmp, silent, failOnError);
  561. }
  562. }
  563. }
  564. }