1 /*
2 * Copyright (C) 2009, Google Inc.
3 * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
4 * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
5 * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com>
6 * and other copyright owners as documented in the project's IP log.
7 *
8 * This program and the accompanying materials are made available
9 * under the terms of the Eclipse Distribution License v1.0 which
10 * accompanies this distribution, is reproduced below, and is
11 * available at http://www.eclipse.org/org/documents/edl-v10.php
12 *
13 * All rights reserved.
14 *
15 * Redistribution and use in source and binary forms, with or
16 * without modification, are permitted provided that the following
17 * conditions are met:
18 *
19 * - Redistributions of source code must retain the above copyright
20 * notice, this list of conditions and the following disclaimer.
21 *
22 * - Redistributions in binary form must reproduce the above
23 * copyright notice, this list of conditions and the following
24 * disclaimer in the documentation and/or other materials provided
25 * with the distribution.
26 *
27 * - Neither the name of the Eclipse Foundation, Inc. nor the
28 * names of its contributors may be used to endorse or promote
29 * products derived from this software without specific prior
30 * written permission.
31 *
32 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
33 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
34 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
36 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
37 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
39 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
40 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
41 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
42 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
43 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 */
46
47 package org.eclipse.jgit.junit;
48
49 import static java.nio.charset.StandardCharsets.UTF_8;
50 import static org.junit.Assert.assertEquals;
51
52 import java.io.File;
53 import java.io.FileInputStream;
54 import java.io.FileNotFoundException;
55 import java.io.FileOutputStream;
56 import java.io.IOException;
57 import java.io.InputStreamReader;
58 import java.io.Reader;
59 import java.nio.file.Path;
60 import java.util.Map;
61
62 import org.eclipse.jgit.api.Git;
63 import org.eclipse.jgit.api.errors.GitAPIException;
64 import org.eclipse.jgit.dircache.DirCacheBuilder;
65 import org.eclipse.jgit.dircache.DirCacheCheckout;
66 import org.eclipse.jgit.dircache.DirCacheEntry;
67 import org.eclipse.jgit.internal.storage.file.FileRepository;
68 import org.eclipse.jgit.lib.Constants;
69 import org.eclipse.jgit.lib.FileMode;
70 import org.eclipse.jgit.lib.ObjectId;
71 import org.eclipse.jgit.lib.ObjectInserter;
72 import org.eclipse.jgit.lib.RefUpdate;
73 import org.eclipse.jgit.lib.Repository;
74 import org.eclipse.jgit.revwalk.RevCommit;
75 import org.eclipse.jgit.revwalk.RevWalk;
76 import org.eclipse.jgit.treewalk.FileTreeIterator;
77 import org.eclipse.jgit.util.FS;
78 import org.eclipse.jgit.util.FileUtils;
79 import org.junit.Before;
80
81 /**
82 * Base class for most JGit unit tests.
83 *
84 * Sets up a predefined test repository and has support for creating additional
85 * repositories and destroying them when the tests are finished.
86 */
87 public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase {
88 /**
89 * Copy a file
90 *
91 * @param src
92 * @param dst
93 * @throws IOException
94 */
95 protected static void copyFile(final File src, final File dst)
96 throws IOException {
97 final FileInputStream fis = new FileInputStream(src);
98 try {
99 final FileOutputStream fos = new FileOutputStream(dst);
100 try {
101 final byte[] buf = new byte[4096];
102 int r;
103 while ((r = fis.read(buf)) > 0) {
104 fos.write(buf, 0, r);
105 }
106 } finally {
107 fos.close();
108 }
109 } finally {
110 fis.close();
111 }
112 }
113
114 /**
115 * Write a trash file
116 *
117 * @param name
118 * @param data
119 * @return the trash file
120 * @throws IOException
121 */
122 protected File writeTrashFile(final String name, final String data)
123 throws IOException {
124 return JGitTestUtil.writeTrashFile(db, name, data);
125 }
126
127 /**
128 * Create a symbolic link
129 *
130 * @param link
131 * the path of the symbolic link to create
132 * @param target
133 * the target of the symbolic link
134 * @return the path to the symbolic link
135 * @throws Exception
136 * @since 4.2
137 */
138 protected Path writeLink(final String link, final String target)
139 throws Exception {
140 return JGitTestUtil.writeLink(db, link, target);
141 }
142
143 /**
144 * Write a trash file
145 *
146 * @param subdir
147 * @param name
148 * @param data
149 * @return the trash file
150 * @throws IOException
151 */
152 protected File writeTrashFile(final String subdir, final String name,
153 final String data)
154 throws IOException {
155 return JGitTestUtil.writeTrashFile(db, subdir, name, data);
156 }
157
158 /**
159 * Read content of a file
160 *
161 * @param name
162 * @return the file's content
163 * @throws IOException
164 */
165 protected String read(final String name) throws IOException {
166 return JGitTestUtil.read(db, name);
167 }
168
169 /**
170 * Check if file exists
171 *
172 * @param name
173 * file name
174 * @return if the file exists
175 */
176 protected boolean check(final String name) {
177 return JGitTestUtil.check(db, name);
178 }
179
180 /**
181 * Delete a trash file
182 *
183 * @param name
184 * file name
185 * @throws IOException
186 */
187 protected void deleteTrashFile(final String name) throws IOException {
188 JGitTestUtil.deleteTrashFile(db, name);
189 }
190
191 /**
192 * Check content of a file.
193 *
194 * @param f
195 * @param checkData
196 * expected content
197 * @throws IOException
198 */
199 protected static void checkFile(File f, final String checkData)
200 throws IOException {
201 try (Reader r = new InputStreamReader(new FileInputStream(f),
202 UTF_8)) {
203 if (checkData.length() > 0) {
204 char[] data = new char[checkData.length()];
205 assertEquals(data.length, r.read(data));
206 assertEquals(checkData, new String(data));
207 }
208 assertEquals(-1, r.read());
209 }
210 }
211
212 /** Test repository, initialized for this test case. */
213 protected FileRepository db;
214
215 /** Working directory of {@link #db}. */
216 protected File trash;
217
218 /** {@inheritDoc} */
219 @Override
220 @Before
221 public void setUp() throws Exception {
222 super.setUp();
223 db = createWorkRepository();
224 trash = db.getWorkTree();
225 }
226
227 /**
228 * Represent the state of the index in one String. This representation is
229 * useful when writing tests which do assertions on the state of the index.
230 * By default information about path, mode, stage (if different from 0) is
231 * included. A bitmask controls which additional info about
232 * modificationTimes, smudge state and length is included.
233 * <p>
234 * The format of the returned string is described with this BNF:
235 *
236 * <pre>
237 * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* .
238 * mode = ", mode:" number .
239 * stage = ", stage:" number .
240 * time = ", time:t" timestamp-index .
241 * smudge = "" | ", smudged" .
242 * length = ", length:" number .
243 * sha1 = ", sha1:" hex-sha1 .
244 * content = ", content:" blob-data .
245 * </pre>
246 *
247 * 'stage' is only presented when the stage is different from 0. All
248 * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The
249 * smallest reported time-stamp will be called "t0". This allows to write
250 * assertions against the string although the concrete value of the time
251 * stamps is unknown.
252 *
253 * @param includedOptions
254 * a bitmask constructed out of the constants {@link #MOD_TIME},
255 * {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and
256 * {@link #CONTENT} controlling which info is present in the
257 * resulting string.
258 * @return a string encoding the index state
259 * @throws IllegalStateException
260 * @throws IOException
261 */
262 public String indexState(int includedOptions)
263 throws IllegalStateException, IOException {
264 return indexState(db, includedOptions);
265 }
266
267 /**
268 * Resets the index to represent exactly some filesystem content. E.g. the
269 * following call will replace the index with the working tree content:
270 * <p>
271 * <code>resetIndex(new FileSystemIterator(db))</code>
272 * <p>
273 * This method can be used by testcases which first prepare a new commit
274 * somewhere in the filesystem (e.g. in the working-tree) and then want to
275 * have an index which matches their prepared content.
276 *
277 * @param treeItr
278 * a {@link org.eclipse.jgit.treewalk.FileTreeIterator} which
279 * determines which files should go into the new index
280 * @throws FileNotFoundException
281 * @throws IOException
282 */
283 protected void resetIndex(FileTreeIterator treeItr)
284 throws FileNotFoundException, IOException {
285 try (ObjectInserter inserter = db.newObjectInserter()) {
286 DirCacheBuilder builder = db.lockDirCache().builder();
287 DirCacheEntry dce;
288
289 while (!treeItr.eof()) {
290 long len = treeItr.getEntryLength();
291
292 dce = new DirCacheEntry(treeItr.getEntryPathString());
293 dce.setFileMode(treeItr.getEntryFileMode());
294 dce.setLastModified(treeItr.getEntryLastModified());
295 dce.setLength((int) len);
296 FileInputStream in = new FileInputStream(
297 treeItr.getEntryFile());
298 dce.setObjectId(inserter.insert(Constants.OBJ_BLOB, len, in));
299 in.close();
300 builder.add(dce);
301 treeItr.next(1);
302 }
303 builder.commit();
304 inserter.flush();
305 }
306 }
307
308 /**
309 * Helper method to map arbitrary objects to user-defined names. This can be
310 * used create short names for objects to produce small and stable debug
311 * output. It is guaranteed that when you lookup the same object multiple
312 * times even with different nameTemplates this method will always return
313 * the same name which was derived from the first nameTemplate.
314 * nameTemplates can contain "%n" which will be replaced by a running number
315 * before used as a name.
316 *
317 * @param l
318 * the object to lookup
319 * @param lookupTable
320 * a table storing object-name mappings.
321 * @param nameTemplate
322 * the name for that object. Can contain "%n" which will be
323 * replaced by a running number before used as a name. If the
324 * lookup table already contains the object this parameter will
325 * be ignored
326 * @return a name of that object. Is not guaranteed to be unique. Use
327 * nameTemplates containing "%n" to always have unique names
328 */
329 public static String lookup(Object l, String nameTemplate,
330 Map<Object, String> lookupTable) {
331 String name = lookupTable.get(l);
332 if (name == null) {
333 name = nameTemplate.replaceAll("%n",
334 Integer.toString(lookupTable.size()));
335 lookupTable.put(l, name);
336 }
337 return name;
338 }
339
340 /**
341 * Replaces '\' by '/'
342 *
343 * @param str
344 * the string in which backslashes should be replaced
345 * @return the resulting string with slashes
346 * @since 4.2
347 */
348 public static String slashify(String str) {
349 str = str.replace('\\', '/');
350 return str;
351 }
352
353 /**
354 * Waits until it is guaranteed that a subsequent file modification has a
355 * younger modification timestamp than the modification timestamp of the
356 * given file. This is done by touching a temporary file, reading the
357 * lastmodified attribute and, if needed, sleeping. After sleeping this loop
358 * starts again until the filesystem timer has advanced enough.
359 *
360 * @param lastFile
361 * the file on which we want to wait until the filesystem timer
362 * has advanced more than the lastmodification timestamp of this
363 * file
364 * @return return the last measured value of the filesystem timer which is
365 * greater than then the lastmodification time of lastfile.
366 * @throws InterruptedException
367 * @throws IOException
368 */
369 public static long fsTick(File lastFile) throws InterruptedException,
370 IOException {
371 long sleepTime = 64;
372 FS fs = FS.DETECTED;
373 if (lastFile != null && !fs.exists(lastFile))
374 throw new FileNotFoundException(lastFile.getPath());
375 File tmp = File.createTempFile("FileTreeIteratorWithTimeControl", null);
376 try {
377 long startTime = (lastFile == null) ? fs.lastModified(tmp) : fs
378 .lastModified(lastFile);
379 long actTime = fs.lastModified(tmp);
380 while (actTime <= startTime) {
381 Thread.sleep(sleepTime);
382 sleepTime *= 2;
383 FileOutputStream fos = new FileOutputStream(tmp);
384 fos.close();
385 actTime = fs.lastModified(tmp);
386 }
387 return actTime;
388 } finally {
389 FileUtils.delete(tmp);
390 }
391 }
392
393 /**
394 * Create a branch
395 *
396 * @param objectId
397 * @param branchName
398 * @throws IOException
399 */
400 protected void createBranch(ObjectId objectId, String branchName)
401 throws IOException {
402 RefUpdate updateRef = db.updateRef(branchName);
403 updateRef.setNewObjectId(objectId);
404 updateRef.update();
405 }
406
407 /**
408 * Checkout a branch
409 *
410 * @param branchName
411 * @throws IllegalStateException
412 * @throws IOException
413 */
414 protected void checkoutBranch(String branchName)
415 throws IllegalStateException, IOException {
416 try (RevWalk walk = new RevWalk(db)) {
417 RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD));
418 RevCommit branch = walk.parseCommit(db.resolve(branchName));
419 DirCacheCheckout dco = new DirCacheCheckout(db,
420 head.getTree().getId(), db.lockDirCache(),
421 branch.getTree().getId());
422 dco.setFailOnConflict(true);
423 dco.checkout();
424 }
425 // update the HEAD
426 RefUpdate refUpdate = db.updateRef(Constants.HEAD);
427 refUpdate.setRefLogMessage("checkout: moving to " + branchName, false);
428 refUpdate.link(branchName);
429 }
430
431 /**
432 * Writes a number of files in the working tree. The first content specified
433 * will be written into a file named '0', the second into a file named "1"
434 * and so on. If <code>null</code> is specified as content then this file is
435 * skipped.
436 *
437 * @param ensureDistinctTimestamps
438 * if set to <code>true</code> then between two write operations
439 * this method will wait to ensure that the second file will get
440 * a different lastmodification timestamp than the first file.
441 * @param contents
442 * the contents which should be written into the files
443 * @return the File object associated to the last written file.
444 * @throws IOException
445 * @throws InterruptedException
446 */
447 protected File writeTrashFiles(boolean ensureDistinctTimestamps,
448 String... contents)
449 throws IOException, InterruptedException {
450 File f = null;
451 for (int i = 0; i < contents.length; i++)
452 if (contents[i] != null) {
453 if (ensureDistinctTimestamps && (f != null))
454 fsTick(f);
455 f = writeTrashFile(Integer.toString(i), contents[i]);
456 }
457 return f;
458 }
459
460 /**
461 * Commit a file with the specified contents on the specified branch,
462 * creating the branch if it didn't exist before.
463 * <p>
464 * It switches back to the original branch after the commit if there was
465 * one.
466 *
467 * @param filename
468 * @param contents
469 * @param branch
470 * @return the created commit
471 */
472 protected RevCommit commitFile(String filename, String contents, String branch) {
473 try (Git git = new Git(db)) {
474 Repository repo = git.getRepository();
475 String originalBranch = repo.getFullBranch();
476 boolean empty = repo.resolve(Constants.HEAD) == null;
477 if (!empty) {
478 if (repo.findRef(branch) == null)
479 git.branchCreate().setName(branch).call();
480 git.checkout().setName(branch).call();
481 }
482
483 writeTrashFile(filename, contents);
484 git.add().addFilepattern(filename).call();
485 RevCommit commit = git.commit()
486 .setMessage(branch + ": " + filename).call();
487
488 if (originalBranch != null)
489 git.checkout().setName(originalBranch).call();
490 else if (empty)
491 git.branchCreate().setName(branch).setStartPoint(commit).call();
492
493 return commit;
494 } catch (IOException e) {
495 throw new RuntimeException(e);
496 } catch (GitAPIException e) {
497 throw new RuntimeException(e);
498 }
499 }
500
501 /**
502 * Create <code>DirCacheEntry</code>
503 *
504 * @param path
505 * @param mode
506 * @return the DirCacheEntry
507 */
508 protected DirCacheEntry createEntry(final String path, final FileMode mode) {
509 return createEntry(path, mode, DirCacheEntry.STAGE_0, path);
510 }
511
512 /**
513 * Create <code>DirCacheEntry</code>
514 *
515 * @param path
516 * @param mode
517 * @param content
518 * @return the DirCacheEntry
519 */
520 protected DirCacheEntry createEntry(final String path, final FileMode mode,
521 final String content) {
522 return createEntry(path, mode, DirCacheEntry.STAGE_0, content);
523 }
524
525 /**
526 * Create <code>DirCacheEntry</code>
527 *
528 * @param path
529 * @param mode
530 * @param stage
531 * @param content
532 * @return the DirCacheEntry
533 */
534 protected DirCacheEntry createEntry(final String path, final FileMode mode,
535 final int stage, final String content) {
536 final DirCacheEntry entry = new DirCacheEntry(path, stage);
537 entry.setFileMode(mode);
538 try (ObjectInserter.Formatter formatter = new ObjectInserter.Formatter()) {
539 entry.setObjectId(formatter.idFor(
540 Constants.OBJ_BLOB, Constants.encode(content)));
541 }
542 return entry;
543 }
544
545 /**
546 * Assert files are equal
547 *
548 * @param expected
549 * @param actual
550 * @throws IOException
551 */
552 public static void assertEqualsFile(File expected, File actual)
553 throws IOException {
554 assertEquals(expected.getCanonicalFile(), actual.getCanonicalFile());
555 }
556 }