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.time.Instant;
61 import java.util.Map;
62 import java.util.concurrent.TimeUnit;
63
64 import org.eclipse.jgit.api.Git;
65 import org.eclipse.jgit.api.errors.GitAPIException;
66 import org.eclipse.jgit.dircache.DirCacheBuilder;
67 import org.eclipse.jgit.dircache.DirCacheCheckout;
68 import org.eclipse.jgit.dircache.DirCacheEntry;
69 import org.eclipse.jgit.internal.storage.file.FileRepository;
70 import org.eclipse.jgit.lib.Constants;
71 import org.eclipse.jgit.lib.FileMode;
72 import org.eclipse.jgit.lib.ObjectId;
73 import org.eclipse.jgit.lib.ObjectInserter;
74 import org.eclipse.jgit.lib.RefUpdate;
75 import org.eclipse.jgit.lib.Repository;
76 import org.eclipse.jgit.revwalk.RevCommit;
77 import org.eclipse.jgit.revwalk.RevWalk;
78 import org.eclipse.jgit.treewalk.FileTreeIterator;
79 import org.eclipse.jgit.util.FS;
80 import org.eclipse.jgit.util.FileUtils;
81 import org.junit.Before;
82
83 /**
84 * Base class for most JGit unit tests.
85 *
86 * Sets up a predefined test repository and has support for creating additional
87 * repositories and destroying them when the tests are finished.
88 */
89 public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase {
90 /**
91 * Copy a file
92 *
93 * @param src
94 * @param dst
95 * @throws IOException
96 */
97 protected static void copyFile(File src, File dst)
98 throws IOException {
99 try (FileInputStream fis = new FileInputStream(src);
100 FileOutputStream fos = new FileOutputStream(dst)) {
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 }
107 }
108
109 /**
110 * Write a trash file
111 *
112 * @param name
113 * @param data
114 * @return the trash file
115 * @throws IOException
116 */
117 protected File writeTrashFile(String name, String data)
118 throws IOException {
119 return JGitTestUtil.writeTrashFile(db, name, data);
120 }
121
122 /**
123 * Create a symbolic link
124 *
125 * @param link
126 * the path of the symbolic link to create
127 * @param target
128 * the target of the symbolic link
129 * @return the path to the symbolic link
130 * @throws Exception
131 * @since 4.2
132 */
133 protected Path writeLink(String link, String target)
134 throws Exception {
135 return JGitTestUtil.writeLink(db, link, target);
136 }
137
138 /**
139 * Write a trash file
140 *
141 * @param subdir
142 * @param name
143 * @param data
144 * @return the trash file
145 * @throws IOException
146 */
147 protected File writeTrashFile(final String subdir, final String name,
148 final String data)
149 throws IOException {
150 return JGitTestUtil.writeTrashFile(db, subdir, name, data);
151 }
152
153 /**
154 * Read content of a file
155 *
156 * @param name
157 * @return the file's content
158 * @throws IOException
159 */
160 protected String read(String name) throws IOException {
161 return JGitTestUtil.read(db, name);
162 }
163
164 /**
165 * Check if file exists
166 *
167 * @param name
168 * file name
169 * @return if the file exists
170 */
171 protected boolean check(String name) {
172 return JGitTestUtil.check(db, name);
173 }
174
175 /**
176 * Delete a trash file
177 *
178 * @param name
179 * file name
180 * @throws IOException
181 */
182 protected void deleteTrashFile(String name) throws IOException {
183 JGitTestUtil.deleteTrashFile(db, name);
184 }
185
186 /**
187 * Check content of a file.
188 *
189 * @param f
190 * @param checkData
191 * expected content
192 * @throws IOException
193 */
194 protected static void checkFile(File f, String checkData)
195 throws IOException {
196 try (Reader r = new InputStreamReader(new FileInputStream(f),
197 UTF_8)) {
198 if (checkData.length() > 0) {
199 char[] data = new char[checkData.length()];
200 assertEquals(data.length, r.read(data));
201 assertEquals(checkData, new String(data));
202 }
203 assertEquals(-1, r.read());
204 }
205 }
206
207 /** Test repository, initialized for this test case. */
208 protected FileRepository db;
209
210 /** Working directory of {@link #db}. */
211 protected File trash;
212
213 /** {@inheritDoc} */
214 @Override
215 @Before
216 public void setUp() throws Exception {
217 super.setUp();
218 db = createWorkRepository();
219 trash = db.getWorkTree();
220 }
221
222 /**
223 * Represent the state of the index in one String. This representation is
224 * useful when writing tests which do assertions on the state of the index.
225 * By default information about path, mode, stage (if different from 0) is
226 * included. A bitmask controls which additional info about
227 * modificationTimes, smudge state and length is included.
228 * <p>
229 * The format of the returned string is described with this BNF:
230 *
231 * <pre>
232 * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* .
233 * mode = ", mode:" number .
234 * stage = ", stage:" number .
235 * time = ", time:t" timestamp-index .
236 * smudge = "" | ", smudged" .
237 * length = ", length:" number .
238 * sha1 = ", sha1:" hex-sha1 .
239 * content = ", content:" blob-data .
240 * </pre>
241 *
242 * 'stage' is only presented when the stage is different from 0. All
243 * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The
244 * smallest reported time-stamp will be called "t0". This allows to write
245 * assertions against the string although the concrete value of the time
246 * stamps is unknown.
247 *
248 * @param includedOptions
249 * a bitmask constructed out of the constants {@link #MOD_TIME},
250 * {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and
251 * {@link #CONTENT} controlling which info is present in the
252 * resulting string.
253 * @return a string encoding the index state
254 * @throws IllegalStateException
255 * @throws IOException
256 */
257 public String indexState(int includedOptions)
258 throws IllegalStateException, IOException {
259 return indexState(db, includedOptions);
260 }
261
262 /**
263 * Resets the index to represent exactly some filesystem content. E.g. the
264 * following call will replace the index with the working tree content:
265 * <p>
266 * <code>resetIndex(new FileSystemIterator(db))</code>
267 * <p>
268 * This method can be used by testcases which first prepare a new commit
269 * somewhere in the filesystem (e.g. in the working-tree) and then want to
270 * have an index which matches their prepared content.
271 *
272 * @param treeItr
273 * a {@link org.eclipse.jgit.treewalk.FileTreeIterator} which
274 * determines which files should go into the new index
275 * @throws FileNotFoundException
276 * @throws IOException
277 */
278 protected void resetIndex(FileTreeIterator treeItr)
279 throws FileNotFoundException, IOException {
280 try (ObjectInserter inserter = db.newObjectInserter()) {
281 DirCacheBuilder builder = db.lockDirCache().builder();
282 DirCacheEntry dce;
283
284 while (!treeItr.eof()) {
285 long len = treeItr.getEntryLength();
286
287 dce = new DirCacheEntry(treeItr.getEntryPathString());
288 dce.setFileMode(treeItr.getEntryFileMode());
289 dce.setLastModified(treeItr.getEntryLastModifiedInstant());
290 dce.setLength((int) len);
291 try (FileInputStream in = new FileInputStream(
292 treeItr.getEntryFile())) {
293 dce.setObjectId(
294 inserter.insert(Constants.OBJ_BLOB, len, in));
295 }
296 builder.add(dce);
297 treeItr.next(1);
298 }
299 builder.commit();
300 inserter.flush();
301 }
302 }
303
304 /**
305 * Helper method to map arbitrary objects to user-defined names. This can be
306 * used create short names for objects to produce small and stable debug
307 * output. It is guaranteed that when you lookup the same object multiple
308 * times even with different nameTemplates this method will always return
309 * the same name which was derived from the first nameTemplate.
310 * nameTemplates can contain "%n" which will be replaced by a running number
311 * before used as a name.
312 *
313 * @param l
314 * the object to lookup
315 * @param lookupTable
316 * a table storing object-name mappings.
317 * @param nameTemplate
318 * the name for that object. Can contain "%n" which will be
319 * replaced by a running number before used as a name. If the
320 * lookup table already contains the object this parameter will
321 * be ignored
322 * @return a name of that object. Is not guaranteed to be unique. Use
323 * nameTemplates containing "%n" to always have unique names
324 */
325 public static String lookup(Object l, String nameTemplate,
326 Map<Object, String> lookupTable) {
327 String name = lookupTable.get(l);
328 if (name == null) {
329 name = nameTemplate.replaceAll("%n",
330 Integer.toString(lookupTable.size()));
331 lookupTable.put(l, name);
332 }
333 return name;
334 }
335
336 /**
337 * Replaces '\' by '/'
338 *
339 * @param str
340 * the string in which backslashes should be replaced
341 * @return the resulting string with slashes
342 * @since 4.2
343 */
344 public static String slashify(String str) {
345 str = str.replace('\\', '/');
346 return str;
347 }
348
349 /**
350 * Waits until it is guaranteed that a subsequent file modification has a
351 * younger modification timestamp than the modification timestamp of the
352 * given file. This is done by touching a temporary file, reading the
353 * lastmodified attribute and, if needed, sleeping. After sleeping this loop
354 * starts again until the filesystem timer has advanced enough. The
355 * temporary file will be created as a sibling of lastFile.
356 *
357 * @param lastFile
358 * the file on which we want to wait until the filesystem timer
359 * has advanced more than the lastmodification timestamp of this
360 * file
361 * @return return the last measured value of the filesystem timer which is
362 * greater than then the lastmodification time of lastfile.
363 * @throws InterruptedException
364 * @throws IOException
365 */
366 public static Instant fsTick(File lastFile)
367 throws InterruptedException,
368 IOException {
369 File tmp;
370 FS fs = FS.DETECTED;
371 if (lastFile == null) {
372 lastFile = tmp = File
373 .createTempFile("fsTickTmpFile", null);
374 } else {
375 if (!fs.exists(lastFile)) {
376 throw new FileNotFoundException(lastFile.getPath());
377 }
378 tmp = File.createTempFile("fsTickTmpFile", null,
379 lastFile.getParentFile());
380 }
381 long res = FS.getFileStoreAttributes(tmp.toPath())
382 .getFsTimestampResolution().toNanos();
383 long sleepTime = res / 10;
384 try {
385 Instant startTime = fs.lastModifiedInstant(lastFile);
386 Instant actTime = fs.lastModifiedInstant(tmp);
387 while (actTime.compareTo(startTime) <= 0) {
388 TimeUnit.NANOSECONDS.sleep(sleepTime);
389 FileUtils.touch(tmp.toPath());
390 actTime = fs.lastModifiedInstant(tmp);
391 }
392 return actTime;
393 } finally {
394 FileUtils.delete(tmp);
395 }
396 }
397
398 /**
399 * Create a branch
400 *
401 * @param objectId
402 * @param branchName
403 * @throws IOException
404 */
405 protected void createBranch(ObjectId objectId, String branchName)
406 throws IOException {
407 RefUpdate updateRef = db.updateRef(branchName);
408 updateRef.setNewObjectId(objectId);
409 updateRef.update();
410 }
411
412 /**
413 * Checkout a branch
414 *
415 * @param branchName
416 * @throws IllegalStateException
417 * @throws IOException
418 */
419 protected void checkoutBranch(String branchName)
420 throws IllegalStateException, IOException {
421 try (RevWalk walk = new RevWalk(db)) {
422 RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD));
423 RevCommit branch = walk.parseCommit(db.resolve(branchName));
424 DirCacheCheckout dco = new DirCacheCheckout(db,
425 head.getTree().getId(), db.lockDirCache(),
426 branch.getTree().getId());
427 dco.setFailOnConflict(true);
428 dco.checkout();
429 }
430 // update the HEAD
431 RefUpdate refUpdate = db.updateRef(Constants.HEAD);
432 refUpdate.setRefLogMessage("checkout: moving to " + branchName, false);
433 refUpdate.link(branchName);
434 }
435
436 /**
437 * Writes a number of files in the working tree. The first content specified
438 * will be written into a file named '0', the second into a file named "1"
439 * and so on. If <code>null</code> is specified as content then this file is
440 * skipped.
441 *
442 * @param ensureDistinctTimestamps
443 * if set to <code>true</code> then between two write operations
444 * this method will wait to ensure that the second file will get
445 * a different lastmodification timestamp than the first file.
446 * @param contents
447 * the contents which should be written into the files
448 * @return the File object associated to the last written file.
449 * @throws IOException
450 * @throws InterruptedException
451 */
452 protected File writeTrashFiles(boolean ensureDistinctTimestamps,
453 String... contents)
454 throws IOException, InterruptedException {
455 File f = null;
456 for (int i = 0; i < contents.length; i++)
457 if (contents[i] != null) {
458 if (ensureDistinctTimestamps && (f != null))
459 fsTick(f);
460 f = writeTrashFile(Integer.toString(i), contents[i]);
461 }
462 return f;
463 }
464
465 /**
466 * Commit a file with the specified contents on the specified branch,
467 * creating the branch if it didn't exist before.
468 * <p>
469 * It switches back to the original branch after the commit if there was
470 * one.
471 *
472 * @param filename
473 * @param contents
474 * @param branch
475 * @return the created commit
476 */
477 protected RevCommit commitFile(String filename, String contents, String branch) {
478 try (Git git = new Git(db)) {
479 Repository repo = git.getRepository();
480 String originalBranch = repo.getFullBranch();
481 boolean empty = repo.resolve(Constants.HEAD) == null;
482 if (!empty) {
483 if (repo.findRef(branch) == null)
484 git.branchCreate().setName(branch).call();
485 git.checkout().setName(branch).call();
486 }
487
488 writeTrashFile(filename, contents);
489 git.add().addFilepattern(filename).call();
490 RevCommit commit = git.commit()
491 .setMessage(branch + ": " + filename).call();
492
493 if (originalBranch != null)
494 git.checkout().setName(originalBranch).call();
495 else if (empty)
496 git.branchCreate().setName(branch).setStartPoint(commit).call();
497
498 return commit;
499 } catch (IOException | GitAPIException e) {
500 throw new RuntimeException(e);
501 }
502 }
503
504 /**
505 * Create <code>DirCacheEntry</code>
506 *
507 * @param path
508 * @param mode
509 * @return the DirCacheEntry
510 */
511 protected DirCacheEntry createEntry(String path, FileMode mode) {
512 return createEntry(path, mode, DirCacheEntry.STAGE_0, path);
513 }
514
515 /**
516 * Create <code>DirCacheEntry</code>
517 *
518 * @param path
519 * @param mode
520 * @param content
521 * @return the DirCacheEntry
522 */
523 protected DirCacheEntry createEntry(final String path, final FileMode mode,
524 final String content) {
525 return createEntry(path, mode, DirCacheEntry.STAGE_0, content);
526 }
527
528 /**
529 * Create <code>DirCacheEntry</code>
530 *
531 * @param path
532 * @param mode
533 * @param stage
534 * @param content
535 * @return the DirCacheEntry
536 */
537 protected DirCacheEntry createEntry(final String path, final FileMode mode,
538 final int stage, final String content) {
539 final DirCacheEntry entry = new DirCacheEntry(path, stage);
540 entry.setFileMode(mode);
541 try (ObjectInserter.Formatter formatter = new ObjectInserter.Formatter()) {
542 entry.setObjectId(formatter.idFor(
543 Constants.OBJ_BLOB, Constants.encode(content)));
544 }
545 return entry;
546 }
547
548 /**
549 * Assert files are equal
550 *
551 * @param expected
552 * @param actual
553 * @throws IOException
554 */
555 public static void assertEqualsFile(File expected, File actual)
556 throws IOException {
557 assertEquals(expected.getCanonicalFile(), actual.getCanonicalFile());
558 }
559 }