View Javadoc
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>
5    * and other copyright owners as documented in the project's IP log.
6    *
7    * This program and the accompanying materials are made available
8    * under the terms of the Eclipse Distribution License v1.0 which
9    * accompanies this distribution, is reproduced below, and is
10   * available at http://www.eclipse.org/org/documents/edl-v10.php
11   *
12   * All rights reserved.
13   *
14   * Redistribution and use in source and binary forms, with or
15   * without modification, are permitted provided that the following
16   * conditions are met:
17   *
18   * - Redistributions of source code must retain the above copyright
19   *   notice, this list of conditions and the following disclaimer.
20   *
21   * - Redistributions in binary form must reproduce the above
22   *   copyright notice, this list of conditions and the following
23   *   disclaimer in the documentation and/or other materials provided
24   *   with the distribution.
25   *
26   * - Neither the name of the Eclipse Foundation, Inc. nor the
27   *   names of its contributors may be used to endorse or promote
28   *   products derived from this software without specific prior
29   *   written permission.
30   *
31   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
32   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
33   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
34   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
36   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
38   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
39   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
40   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
41   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
42   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
43   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44   */
45  
46  package org.eclipse.jgit.junit;
47  
48  import static org.junit.Assert.assertFalse;
49  import static org.junit.Assert.fail;
50  
51  import java.io.File;
52  import java.io.IOException;
53  import java.util.ArrayList;
54  import java.util.Collections;
55  import java.util.HashMap;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.concurrent.TimeUnit;
59  
60  import org.eclipse.jgit.internal.storage.file.FileRepository;
61  import org.eclipse.jgit.lib.Constants;
62  import org.eclipse.jgit.lib.PersonIdent;
63  import org.eclipse.jgit.lib.Repository;
64  import org.eclipse.jgit.lib.RepositoryCache;
65  import org.eclipse.jgit.storage.file.FileBasedConfig;
66  import org.eclipse.jgit.storage.file.WindowCacheConfig;
67  import org.eclipse.jgit.util.FS;
68  import org.eclipse.jgit.util.FileUtils;
69  import org.eclipse.jgit.util.SystemReader;
70  import org.junit.After;
71  import org.junit.Before;
72  
73  /**
74   * JUnit TestCase with specialized support for temporary local repository.
75   * <p>
76   * A temporary directory is created for each test, allowing each test to use a
77   * fresh environment. The temporary directory is cleaned up after the test ends.
78   * <p>
79   * Callers should not use {@link RepositoryCache} from within these tests as it
80   * may wedge file descriptors open past the end of the test.
81   * <p>
82   * A system property {@code jgit.junit.usemmap} defines whether memory mapping
83   * is used. Memory mapping has an effect on the file system, in that memory
84   * mapped files in Java cannot be deleted as long as the mapped arrays have not
85   * been reclaimed by the garbage collector. The programmer cannot control this
86   * with precision, so temporary files may hang around longer than desired during
87   * a test, or tests may fail altogether if there is insufficient file
88   * descriptors or address space for the test process.
89   */
90  public abstract class LocalDiskRepositoryTestCase {
91  	private static final boolean useMMAP = "true".equals(System
92  			.getProperty("jgit.junit.usemmap"));
93  
94  	/** A fake (but stable) identity for author fields in the test. */
95  	protected PersonIdent author;
96  
97  	/** A fake (but stable) identity for committer fields in the test. */
98  	protected PersonIdent committer;
99  
100 	private final List<Repository> toClose = new ArrayList<Repository>();
101 	private File tmp;
102 
103 	private MockSystemReader mockSystemReader;
104 
105 	@Before
106 	public void setUp() throws Exception {
107 		tmp = File.createTempFile("jgit_test_", "_tmp");
108 		CleanupThread.deleteOnShutdown(tmp);
109 		if (!tmp.delete() || !tmp.mkdir())
110 			throw new IOException("Cannot create " + tmp);
111 
112 		mockSystemReader = new MockSystemReader();
113 		mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp,
114 				"usergitconfig"), FS.DETECTED);
115 		ceilTestDirectories(getCeilings());
116 		SystemReader.setInstance(mockSystemReader);
117 
118 		final long now = mockSystemReader.getCurrentTime();
119 		final int tz = mockSystemReader.getTimezone(now);
120 		author = new PersonIdent("J. Author", "jauthor@example.com");
121 		author = new PersonIdent(author, now, tz);
122 
123 		committer = new PersonIdent("J. Committer", "jcommitter@example.com");
124 		committer = new PersonIdent(committer, now, tz);
125 
126 		final WindowCacheConfig c = new WindowCacheConfig();
127 		c.setPackedGitLimit(128 * WindowCacheConfig.KB);
128 		c.setPackedGitWindowSize(8 * WindowCacheConfig.KB);
129 		c.setPackedGitMMAP(useMMAP);
130 		c.setDeltaBaseCacheLimit(8 * WindowCacheConfig.KB);
131 		c.install();
132 	}
133 
134 	protected File getTemporaryDirectory() {
135 		return tmp.getAbsoluteFile();
136 	}
137 
138 	protected List<File> getCeilings() {
139 		return Collections.singletonList(getTemporaryDirectory());
140 	}
141 
142 	private void ceilTestDirectories(List<File> ceilings) {
143 		mockSystemReader.setProperty(Constants.GIT_CEILING_DIRECTORIES_KEY, makePath(ceilings));
144 	}
145 
146 	private static String makePath(List<?> objects) {
147 		final StringBuilder stringBuilder = new StringBuilder();
148 		for (Object object : objects) {
149 			if (stringBuilder.length() > 0)
150 				stringBuilder.append(File.pathSeparatorChar);
151 			stringBuilder.append(object.toString());
152 		}
153 		return stringBuilder.toString();
154 	}
155 
156 	@After
157 	public void tearDown() throws Exception {
158 		RepositoryCache.clear();
159 		for (Repository r : toClose)
160 			r.close();
161 		toClose.clear();
162 
163 		// Since memory mapping is controlled by the GC we need to
164 		// tell it this is a good time to clean up and unlock
165 		// memory mapped files.
166 		//
167 		if (useMMAP)
168 			System.gc();
169 		if (tmp != null)
170 			recursiveDelete(tmp, false, true);
171 		if (tmp != null && !tmp.exists())
172 			CleanupThread.removed(tmp);
173 
174 		SystemReader.setInstance(null);
175 	}
176 
177 	/** Increment the {@link #author} and {@link #committer} times. */
178 	protected void tick() {
179 		final long delta = TimeUnit.MILLISECONDS.convert(5 * 60,
180 				TimeUnit.SECONDS);
181 		final long now = author.getWhen().getTime() + delta;
182 		final int tz = mockSystemReader.getTimezone(now);
183 
184 		author = new PersonIdent(author, now, tz);
185 		committer = new PersonIdent(committer, now, tz);
186 	}
187 
188 	/**
189 	 * Recursively delete a directory, failing the test if the delete fails.
190 	 *
191 	 * @param dir
192 	 *            the recursively directory to delete, if present.
193 	 */
194 	protected void recursiveDelete(final File dir) {
195 		recursiveDelete(dir, false, true);
196 	}
197 
198 	private static boolean recursiveDelete(final File dir,
199 			boolean silent, boolean failOnError) {
200 		assert !(silent && failOnError);
201 		if (!dir.exists())
202 			return silent;
203 		final File[] ls = dir.listFiles();
204 		if (ls != null)
205 			for (int k = 0; k < ls.length; k++) {
206 				final File e = ls[k];
207 				if (e.isDirectory())
208 					silent = recursiveDelete(e, silent, failOnError);
209 				else if (!e.delete()) {
210 					if (!silent)
211 						reportDeleteFailure(failOnError, e);
212 					silent = !failOnError;
213 				}
214 			}
215 		if (!dir.delete()) {
216 			if (!silent)
217 				reportDeleteFailure(failOnError, dir);
218 			silent = !failOnError;
219 		}
220 		return silent;
221 	}
222 
223 	private static void reportDeleteFailure(boolean failOnError, File e) {
224 		String severity = failOnError ? "ERROR" : "WARNING";
225 		String msg = severity + ": Failed to delete " + e;
226 		if (failOnError)
227 			fail(msg);
228 		else
229 			System.err.println(msg);
230 	}
231 
232 	/**
233 	 * Creates a new empty bare repository.
234 	 *
235 	 * @return the newly created repository, opened for access
236 	 * @throws IOException
237 	 *             the repository could not be created in the temporary area
238 	 */
239 	protected FileRepository createBareRepository() throws IOException {
240 		return createRepository(true /* bare */);
241 	}
242 
243 	/**
244 	 * Creates a new empty repository within a new empty working directory.
245 	 *
246 	 * @return the newly created repository, opened for access
247 	 * @throws IOException
248 	 *             the repository could not be created in the temporary area
249 	 */
250 	protected FileRepository createWorkRepository() throws IOException {
251 		return createRepository(false /* not bare */);
252 	}
253 
254 	/**
255 	 * Creates a new empty repository.
256 	 *
257 	 * @param bare
258 	 *            true to create a bare repository; false to make a repository
259 	 *            within its working directory
260 	 * @return the newly created repository, opened for access
261 	 * @throws IOException
262 	 *             the repository could not be created in the temporary area
263 	 */
264 	private FileRepository createRepository(boolean bare) throws IOException {
265 		File gitdir = createUniqueTestGitDir(bare);
266 		FileRepository db = new FileRepository(gitdir);
267 		assertFalse(gitdir.exists());
268 		db.create(bare);
269 		toClose.add(db);
270 		return db;
271 	}
272 
273 	/**
274 	 * Adds a repository to the list of repositories which is closed at the end
275 	 * of the tests
276 	 *
277 	 * @param r
278 	 *            the repository to be closed
279 	 */
280 	public void addRepoToClose(Repository r) {
281 		toClose.add(r);
282 	}
283 
284 	/**
285 	 * Creates a unique directory for a test
286 	 *
287 	 * @param name
288 	 *            a subdirectory
289 	 * @return a unique directory for a test
290 	 * @throws IOException
291 	 */
292 	protected File createTempDirectory(String name) throws IOException {
293 		File directory = new File(createTempFile(), name);
294 		FileUtils.mkdirs(directory);
295 		return directory.getCanonicalFile();
296 	}
297 
298 	/**
299 	 * Creates a new unique directory for a test repository
300 	 *
301 	 * @param bare
302 	 *            true for a bare repository; false for a repository with a
303 	 *            working directory
304 	 * @return a unique directory for a test repository
305 	 * @throws IOException
306 	 */
307 	protected File createUniqueTestGitDir(boolean bare) throws IOException {
308 		String gitdirName = createTempFile().getPath();
309 		if (!bare)
310 			gitdirName += "/";
311 		return new File(gitdirName + Constants.DOT_GIT);
312 	}
313 
314 	/**
315 	 * Allocates a new unique file path that does not exist.
316 	 * <p>
317 	 * Unlike the standard {@code File.createTempFile} the returned path does
318 	 * not exist, but may be created by another thread in a race with the
319 	 * caller. Good luck.
320 	 * <p>
321 	 * This method is inherently unsafe due to a race condition between creating
322 	 * the name and the first use that reserves it.
323 	 *
324 	 * @return a unique path that does not exist.
325 	 * @throws IOException
326 	 */
327 	protected File createTempFile() throws IOException {
328 		File p = File.createTempFile("tmp_", "", tmp);
329 		if (!p.delete()) {
330 			throw new IOException("Cannot obtain unique path " + tmp);
331 		}
332 		return p;
333 	}
334 
335 	/**
336 	 * Run a hook script in the repository, returning the exit status.
337 	 *
338 	 * @param db
339 	 *            repository the script should see in GIT_DIR environment
340 	 * @param hook
341 	 *            path of the hook script to execute, must be executable file
342 	 *            type on this platform
343 	 * @param args
344 	 *            arguments to pass to the hook script
345 	 * @return exit status code of the invoked hook
346 	 * @throws IOException
347 	 *             the hook could not be executed
348 	 * @throws InterruptedException
349 	 *             the caller was interrupted before the hook completed
350 	 */
351 	protected int runHook(final Repository db, final File hook,
352 			final String... args) throws IOException, InterruptedException {
353 		final String[] argv = new String[1 + args.length];
354 		argv[0] = hook.getAbsolutePath();
355 		System.arraycopy(args, 0, argv, 1, args.length);
356 
357 		final Map<String, String> env = cloneEnv();
358 		env.put("GIT_DIR", db.getDirectory().getAbsolutePath());
359 		putPersonIdent(env, "AUTHOR", author);
360 		putPersonIdent(env, "COMMITTER", committer);
361 
362 		final File cwd = db.getWorkTree();
363 		final Process p = Runtime.getRuntime().exec(argv, toEnvArray(env), cwd);
364 		p.getOutputStream().close();
365 		p.getErrorStream().close();
366 		p.getInputStream().close();
367 		return p.waitFor();
368 	}
369 
370 	private static void putPersonIdent(final Map<String, String> env,
371 			final String type, final PersonIdent who) {
372 		final String ident = who.toExternalString();
373 		final String date = ident.substring(ident.indexOf("> ") + 2);
374 		env.put("GIT_" + type + "_NAME", who.getName());
375 		env.put("GIT_" + type + "_EMAIL", who.getEmailAddress());
376 		env.put("GIT_" + type + "_DATE", date);
377 	}
378 
379 	/**
380 	 * Create a string to a UTF-8 temporary file and return the path.
381 	 *
382 	 * @param body
383 	 *            complete content to write to the file. If the file should end
384 	 *            with a trailing LF, the string should end with an LF.
385 	 * @return path of the temporary file created within the trash area.
386 	 * @throws IOException
387 	 *             the file could not be written.
388 	 */
389 	protected File write(final String body) throws IOException {
390 		final File f = File.createTempFile("temp", "txt", tmp);
391 		try {
392 			write(f, body);
393 			return f;
394 		} catch (Error e) {
395 			f.delete();
396 			throw e;
397 		} catch (RuntimeException e) {
398 			f.delete();
399 			throw e;
400 		} catch (IOException e) {
401 			f.delete();
402 			throw e;
403 		}
404 	}
405 
406 	/**
407 	 * Write a string as a UTF-8 file.
408 	 *
409 	 * @param f
410 	 *            file to write the string to. Caller is responsible for making
411 	 *            sure it is in the trash directory or will otherwise be cleaned
412 	 *            up at the end of the test. If the parent directory does not
413 	 *            exist, the missing parent directories are automatically
414 	 *            created.
415 	 * @param body
416 	 *            content to write to the file.
417 	 * @throws IOException
418 	 *             the file could not be written.
419 	 */
420 	protected void write(final File f, final String body) throws IOException {
421 		JGitTestUtil.write(f, body);
422 	}
423 
424 	protected String read(final File f) throws IOException {
425 		return JGitTestUtil.read(f);
426 	}
427 
428 	private static String[] toEnvArray(final Map<String, String> env) {
429 		final String[] envp = new String[env.size()];
430 		int i = 0;
431 		for (Map.Entry<String, String> e : env.entrySet())
432 			envp[i++] = e.getKey() + "=" + e.getValue();
433 		return envp;
434 	}
435 
436 	private static HashMap<String, String> cloneEnv() {
437 		return new HashMap<String, String>(System.getenv());
438 	}
439 
440 	private static final class CleanupThread extends Thread {
441 		private static final CleanupThread me;
442 		static {
443 			me = new CleanupThread();
444 			Runtime.getRuntime().addShutdownHook(me);
445 		}
446 
447 		static void deleteOnShutdown(File tmp) {
448 			synchronized (me) {
449 				me.toDelete.add(tmp);
450 			}
451 		}
452 
453 		static void removed(File tmp) {
454 			synchronized (me) {
455 				me.toDelete.remove(tmp);
456 			}
457 		}
458 
459 		private final List<File> toDelete = new ArrayList<File>();
460 
461 		@Override
462 		public void run() {
463 			// On windows accidentally open files or memory
464 			// mapped regions may prevent files from being deleted.
465 			// Suggesting a GC increases the likelihood that our
466 			// test repositories actually get removed after the
467 			// tests, even in the case of failure.
468 			System.gc();
469 			synchronized (this) {
470 				boolean silent = false;
471 				boolean failOnError = false;
472 				for (File tmp : toDelete)
473 					recursiveDelete(tmp, silent, failOnError);
474 			}
475 		}
476 	}
477 }