diff options
Diffstat (limited to 'org.eclipse.jgit.junit/src/org/eclipse/jgit')
14 files changed, 2100 insertions, 676 deletions
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Assert.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Assert.java index 40a05b4b7c..6e0a8c79ab 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Assert.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Assert.java @@ -1,58 +1,46 @@ /* - * Copyright (C) 2012, Robin Rosenberg - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2012, Robin Rosenberg and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; -import static java.lang.Boolean.valueOf; - +/** + * Assertion class + */ public class Assert { + /** + * Assert booleans are equal + * + * @param expect + * expected value + * @param actual + * actual value + */ public static void assertEquals(boolean expect, boolean actual) { - org.junit.Assert.assertEquals(valueOf(expect), valueOf(actual)); + org.junit.Assert.assertEquals(Boolean.valueOf(expect), + Boolean.valueOf(actual)); } + /** + * Assert booleans are equal + * + * @param message + * message + * @param expect + * expected value + * @param actual + * actual value + */ public static void assertEquals(String message, boolean expect, boolean actual) { org.junit.Assert - .assertEquals(message, valueOf(expect), valueOf(actual)); + .assertEquals(message, Boolean.valueOf(expect), + Boolean.valueOf(actual)); } } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/FakeIndexFactory.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/FakeIndexFactory.java new file mode 100644 index 0000000000..eb23bec584 --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/FakeIndexFactory.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2025, Google Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.junit; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toUnmodifiableList; + +import java.io.IOException; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.internal.storage.file.PackIndex.EntriesIterator; +import org.eclipse.jgit.internal.storage.file.PackReverseIndex; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Create indexes with predefined data + * + * @since 7.2 + */ +public class FakeIndexFactory { + + /** + * An object for the fake index + * + * @param name + * a sha1 + * @param offset + * the (fake) position of the object in the pack + */ + public record IndexObject(String name, long offset) { + /** + * Name (sha1) as an objectId + * + * @return name (a sha1) as an objectId. + */ + public ObjectId getObjectId() { + return ObjectId.fromString(name); + } + } + + /** + * Return an index populated with these objects + * + * @param objs + * objects to be indexed + * @return a PackIndex implementation + */ + public static PackIndex indexOf(List<IndexObject> objs) { + return new FakePackIndex(objs); + } + + /** + * Return a reverse pack index with these objects + * + * @param objs + * objects to be indexed + * @return a PackReverseIndex implementation + */ + public static PackReverseIndex reverseIndexOf(List<IndexObject> objs) { + return new FakeReverseIndex(objs); + } + + private FakeIndexFactory() { + } + + private static class FakePackIndex implements PackIndex { + private static final Comparator<IndexObject> SHA1_COMPARATOR = (o1, + o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.name(), + o2.name()); + + private final Map<String, IndexObject> idx; + + private final List<IndexObject> sha1Ordered; + + private final long offset64count; + + FakePackIndex(List<IndexObject> objs) { + sha1Ordered = objs.stream().sorted(SHA1_COMPARATOR) + .collect(toUnmodifiableList()); + idx = objs.stream().collect(toMap(IndexObject::name, identity())); + offset64count = objs.stream() + .filter(o -> o.offset > Integer.MAX_VALUE).count(); + } + + @Override + public Iterator<MutableEntry> iterator() { + return new FakeEntriesIterator(sha1Ordered); + } + + @Override + public long getObjectCount() { + return sha1Ordered.size(); + } + + @Override + public long getOffset64Count() { + return offset64count; + } + + @Override + public ObjectId getObjectId(long nthPosition) { + return ObjectId + .fromString(sha1Ordered.get((int) nthPosition).name()); + } + + @Override + public long getOffset(long nthPosition) { + return sha1Ordered.get((int) nthPosition).offset(); + } + + @Override + public long findOffset(AnyObjectId objId) { + IndexObject o = idx.get(objId.name()); + if (o == null) { + return -1; + } + return o.offset(); + } + + @Override + public int findPosition(AnyObjectId objId) { + IndexObject o = idx.get(objId.name()); + if (o == null) { + return -1; + } + return sha1Ordered.indexOf(o); + } + + @Override + public long findCRC32(AnyObjectId objId) throws MissingObjectException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasCRC32Support() { + return false; + } + + @Override + public void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, + int matchLimit) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getChecksum() { + return new byte[0]; + } + } + + private static class FakeReverseIndex implements PackReverseIndex { + private static final Comparator<IndexObject> OFFSET_COMPARATOR = Comparator + .comparingLong(IndexObject::offset); + + private final List<IndexObject> byOffset; + + private final Map<Long, IndexObject> ridx; + + FakeReverseIndex(List<IndexObject> objs) { + byOffset = objs.stream().sorted(OFFSET_COMPARATOR) + .collect(toUnmodifiableList()); + ridx = byOffset.stream() + .collect(toMap(IndexObject::offset, identity())); + } + + @Override + public void verifyPackChecksum(String packFilePath) { + // Do nothing + } + + @Override + public ObjectId findObject(long offset) { + IndexObject indexObject = ridx.get(offset); + if (indexObject == null) { + return null; + } + return ObjectId.fromString(indexObject.name()); + } + + @Override + public long findNextOffset(long offset, long maxOffset) + throws CorruptObjectException { + IndexObject o = ridx.get(offset); + if (o == null) { + throw new CorruptObjectException("Invalid offset"); //$NON-NLS-1$ + } + int pos = byOffset.indexOf(o); + if (pos == byOffset.size() - 1) { + return maxOffset; + } + return byOffset.get(pos + 1).offset(); + } + + @Override + public int findPosition(long offset) { + IndexObject indexObject = ridx.get(offset); + return byOffset.indexOf(indexObject); + } + + @Override + public ObjectId findObjectByPosition(int nthPosition) { + return byOffset.get(nthPosition).getObjectId(); + } + } + + private static class FakeEntriesIterator extends EntriesIterator { + + private static final byte[] buffer = new byte[Constants.OBJECT_ID_LENGTH]; + + private final Iterator<IndexObject> it; + + FakeEntriesIterator(List<IndexObject> objs) { + super(objs.size()); + it = objs.iterator(); + } + + @Override + protected void readNext() { + IndexObject next = it.next(); + next.getObjectId().copyRawTo(buffer, 0); + setIdBuffer(buffer, 0); + setOffset(next.offset()); + } + } +} diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java index 136c64726f..177d8737cb 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java @@ -1,50 +1,19 @@ /* * Copyright (C) 2008-2009, Google Inc. * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com> - * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -55,6 +24,7 @@ import java.io.Writer; import java.lang.reflect.Method; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Path; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.FileUtils; @@ -63,13 +33,22 @@ import org.eclipse.jgit.util.RawParseUtils; import org.junit.Assert; import org.junit.Test; +/** + * Abstract test util class + */ public abstract class JGitTestUtil { + /** Constant <code>CLASSPATH_TO_RESOURCES="org/eclipse/jgit/test/resources/"</code> */ public static final String CLASSPATH_TO_RESOURCES = "org/eclipse/jgit/test/resources/"; private JGitTestUtil() { throw new UnsupportedOperationException(); } + /** + * Get name of current test by inspecting stack trace + * + * @return the name + */ public static String getName() { GatherStackTrace stack; try { @@ -108,6 +87,14 @@ public abstract class JGitTestUtil { // Thrown above to collect the stack frame. } + /** + * Assert byte arrays are equal + * + * @param exp + * expected value + * @param act + * actual value + */ public static void assertEquals(byte[] exp, byte[] act) { Assert.assertEquals(s(exp), s(act)); } @@ -116,7 +103,14 @@ public abstract class JGitTestUtil { return RawParseUtils.decode(raw); } - public static File getTestResourceFile(final String fileName) { + /** + * Get test resource file. + * + * @param fileName + * file name + * @return the test resource file + */ + public static File getTestResourceFile(String fileName) { if (fileName == null || fileName.length() <= 0) { return null; } @@ -144,23 +138,26 @@ public abstract class JGitTestUtil { } } + /** + * Copy test resource. + * + * @param name + * resource name + * @param dest + * destination file + * @throws IOException + * if an IO error occurred + */ public static void copyTestResource(String name, File dest) throws IOException { URL url = cl().getResource(CLASSPATH_TO_RESOURCES + name); if (url == null) throw new FileNotFoundException(name); - InputStream in = url.openStream(); - try { - FileOutputStream out = new FileOutputStream(dest); - try { - byte[] buf = new byte[4096]; - for (int n; (n = in.read(buf)) > 0;) - out.write(buf, 0, n); - } finally { - out.close(); - } - } finally { - in.close(); + try (InputStream in = url.openStream(); + FileOutputStream out = new FileOutputStream(dest)) { + byte[] buf = new byte[4096]; + for (int n; (n = in.read(buf)) > 0;) + out.write(buf, 0, n); } } @@ -168,6 +165,19 @@ public abstract class JGitTestUtil { return JGitTestUtil.class.getClassLoader(); } + /** + * Write a trash file. + * + * @param db + * the repository + * @param name + * file name + * @param data + * file content + * @return the trash file + * @throws IOException + * if an IO error occurred + */ public static File writeTrashFile(final Repository db, final String name, final String data) throws IOException { File path = new File(db.getWorkTree(), name); @@ -175,6 +185,21 @@ public abstract class JGitTestUtil { return path; } + /** + * Write a trash file. + * + * @param db + * the repository + * @param subdir + * under working tree + * @param name + * file name + * @param data + * file content + * @return the trash file + * @throws IOException + * if an IO error occurred + */ public static File writeTrashFile(final Repository db, final String subdir, final String name, final String data) throws IOException { @@ -197,14 +222,12 @@ public abstract class JGitTestUtil { * @throws IOException * the file could not be written. */ - public static void write(final File f, final String body) + public static void write(File f, String body) throws IOException { FileUtils.mkdirs(f.getParentFile(), true); - Writer w = new OutputStreamWriter(new FileOutputStream(f), "UTF-8"); - try { + try (Writer w = new OutputStreamWriter(new FileOutputStream(f), + UTF_8)) { w.write(body); - } finally { - w.close(); } } @@ -218,26 +241,99 @@ public abstract class JGitTestUtil { * @throws IOException * the file does not exist, or could not be read. */ - public static String read(final File file) throws IOException { + public static String read(File file) throws IOException { final byte[] body = IO.readFully(file); - return new String(body, 0, body.length, "UTF-8"); + return new String(body, 0, body.length, UTF_8); } - public static String read(final Repository db, final String name) + /** + * Read a file's content + * + * @param db + * the repository + * @param name + * file name + * @return the content of the file + * @throws IOException + * if an IO error occurred + */ + public static String read(Repository db, String name) throws IOException { File file = new File(db.getWorkTree(), name); return read(file); } - public static boolean check(final Repository db, final String name) { + /** + * Check if file exists + * + * @param db + * the repository + * @param name + * name of the file + * @return {@code true} if the file exists + */ + public static boolean check(Repository db, String name) { File file = new File(db.getWorkTree(), name); return file.exists(); } + /** + * Delete a trash file. + * + * @param db + * the repository + * @param name + * file name + * @throws IOException + * if an IO error occurred + */ public static void deleteTrashFile(final Repository db, final String name) throws IOException { File path = new File(db.getWorkTree(), name); FileUtils.delete(path); } + /** + * Write a symbolic link + * + * @param db + * the repository + * @param link + * the path of the symbolic link to create + * @param target + * the target of the symbolic link + * @return the path to the symbolic link + * @throws Exception + * if an error occurred + * @since 4.2 + */ + public static Path writeLink(Repository db, String link, + String target) throws Exception { + return FileUtils.createSymLink(new File(db.getWorkTree(), link), + target); + } + + /** + * Concatenate byte arrays. + * + * @param b + * byte arrays to combine together. + * @return a single byte array that contains all bytes copied from input + * byte arrays. + * @since 4.9 + */ + public static byte[] concat(byte[]... b) { + int n = 0; + for (byte[] a : b) { + n += a.length; + } + + byte[] data = new byte[n]; + n = 0; + for (byte[] a : b) { + System.arraycopy(a, 0, data, n, a.length); + n += a.length; + } + return data; + } } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java index b98db7d187..0d20f6488a 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java @@ -1,64 +1,41 @@ /* * Copyright (C) 2009-2010, Google Inc. * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; -import java.util.ArrayList; +import java.io.PrintStream; +import java.time.Instant; +import java.time.ZoneId; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; @@ -69,6 +46,9 @@ import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; /** * JUnit TestCase with specialized support for temporary local repository. @@ -76,8 +56,9 @@ import org.junit.Before; * A temporary directory is created for each test, allowing each test to use a * fresh environment. The temporary directory is cleaned up after the test ends. * <p> - * Callers should not use {@link RepositoryCache} from within these tests as it - * may wedge file descriptors open past the end of the test. + * Callers should not use {@link org.eclipse.jgit.lib.RepositoryCache} from + * within these tests as it may wedge file descriptors open past the end of the + * test. * <p> * A system property {@code jgit.junit.usemmap} defines whether memory mapping * is used. Memory mapping has an effect on the file system, in that memory @@ -97,31 +78,92 @@ public abstract class LocalDiskRepositoryTestCase { /** A fake (but stable) identity for committer fields in the test. */ protected PersonIdent committer; - private final List<Repository> toClose = new ArrayList<Repository>(); + /** + * A {@link SystemReader} used to coordinate time, envars, etc. + * @since 4.2 + */ + protected MockSystemReader mockSystemReader; + + private final Set<Repository> toClose = new HashSet<>(); + + /** + * Temporary test root directory for files created by tests. + * @since 7.2 + */ + @Rule + public TemporaryFolder testRoot = new TemporaryFolder(); + + Random rand = new Random(); + private File tmp; - private MockSystemReader mockSystemReader; + private File homeDir; + /** + * The current test name. + * + * @since 6.0.1 + */ + @Rule + public TestName currentTest = new TestName(); + + private String getTestName() { + String name = currentTest.getMethodName(); + name = name.replaceAll("[^a-zA-Z0-9]", "_"); + name = name.replaceAll("__+", "_"); + if (name.startsWith("_")) { + name = name.substring(1); + } + return name; + } + + /** + * Setup test + * + * @throws Exception + * if an error occurred + */ @Before public void setUp() throws Exception { - tmp = File.createTempFile("jgit_test_", "_tmp"); - CleanupThread.deleteOnShutdown(tmp); - if (!tmp.delete() || !tmp.mkdir()) - throw new IOException("Cannot create " + tmp); + tmp = testRoot.newFolder(getTestName() + rand.nextInt()); mockSystemReader = new MockSystemReader(); - mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp, - "usergitconfig"), FS.DETECTED); - ceilTestDirectories(getCeilings()); SystemReader.setInstance(mockSystemReader); - final long now = mockSystemReader.getCurrentTime(); - final int tz = mockSystemReader.getTimezone(now); - author = new PersonIdent("J. Author", "jauthor@example.com"); - author = new PersonIdent(author, now, tz); + // Mock the home directory. We don't want to pick up the real user's git + // config, or global git ignore. + // XDG_CONFIG_HOME isn't set in the MockSystemReader. + mockSystemReader.setProperty("user.home", tmp.getAbsolutePath()); + mockSystemReader.setProperty("HOME", tmp.getAbsolutePath()); + homeDir = FS.DETECTED.userHome(); + FS.DETECTED.setUserHome(tmp.getAbsoluteFile()); + + // Measure timer resolution before the test to avoid time critical tests + // are affected by time needed for measurement. + // The MockSystemReader must be configured first since we need to use + // the same one here + FS.getFileStoreAttributes(tmp.toPath().getParent()); + + FileBasedConfig jgitConfig = new FileBasedConfig( + new File(tmp, "jgitconfig"), FS.DETECTED); + FileBasedConfig systemConfig = new FileBasedConfig(jgitConfig, + new File(tmp, "systemgitconfig"), FS.DETECTED); + FileBasedConfig userConfig = new FileBasedConfig(systemConfig, + new File(tmp, "usergitconfig"), FS.DETECTED); + // We have to set autoDetach to false for tests, because tests expect to be able + // to clean up by recursively removing the repository, and background GC might be + // in the middle of writing or deleting files, which would disrupt this. + userConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION, + null, ConfigConstants.CONFIG_KEY_AUTODETACH, false); + userConfig.save(); + mockSystemReader.setJGitConfig(jgitConfig); + mockSystemReader.setSystemGitConfig(systemConfig); + mockSystemReader.setUserGitConfig(userConfig); + + ceilTestDirectories(getCeilings()); + author = new PersonIdent("J. Author", "jauthor@example.com"); committer = new PersonIdent("J. Committer", "jcommitter@example.com"); - committer = new PersonIdent(committer, now, tz); final WindowCacheConfig c = new WindowCacheConfig(); c.setPackedGitLimit(128 * WindowCacheConfig.KB); @@ -131,10 +173,20 @@ public abstract class LocalDiskRepositoryTestCase { c.install(); } + /** + * Get temporary directory. + * + * @return the temporary directory + */ protected File getTemporaryDirectory() { return tmp.getAbsoluteFile(); } + /** + * Get list of ceiling directories + * + * @return list of ceiling directories + */ protected List<File> getCeilings() { return Collections.singletonList(getTemporaryDirectory()); } @@ -153,33 +205,38 @@ public abstract class LocalDiskRepositoryTestCase { return stringBuilder.toString(); } + /** + * Tear down the test + * + * @throws Exception + * if an error occurred + */ @After public void tearDown() throws Exception { RepositoryCache.clear(); - for (Repository r : toClose) + for (Repository r : toClose) { r.close(); + } toClose.clear(); // Since memory mapping is controlled by the GC we need to // tell it this is a good time to clean up and unlock // memory mapped files. // - if (useMMAP) + if (useMMAP) { System.gc(); - if (tmp != null) - recursiveDelete(tmp, false, true); - if (tmp != null && !tmp.exists()) - CleanupThread.removed(tmp); - + } + FS.DETECTED.setUserHome(homeDir); SystemReader.setInstance(null); } - /** Increment the {@link #author} and {@link #committer} times. */ + /** + * Increment the {@link #author} and {@link #committer} times. + */ protected void tick() { - final long delta = TimeUnit.MILLISECONDS.convert(5 * 60, - TimeUnit.SECONDS); - final long now = author.getWhen().getTime() + delta; - final int tz = mockSystemReader.getTimezone(now); + mockSystemReader.tick(5 * 60); + Instant now = mockSystemReader.now(); + ZoneId tz = mockSystemReader.getTimeZoneId(); author = new PersonIdent(author, now, tz); committer = new PersonIdent(committer, now, tz); @@ -191,48 +248,147 @@ public abstract class LocalDiskRepositoryTestCase { * @param dir * the recursively directory to delete, if present. */ - protected void recursiveDelete(final File dir) { + protected void recursiveDelete(File dir) { recursiveDelete(dir, false, true); } private static boolean recursiveDelete(final File dir, boolean silent, boolean failOnError) { assert !(silent && failOnError); - if (!dir.exists()) - return silent; - final File[] ls = dir.listFiles(); - if (ls != null) - for (int k = 0; k < ls.length; k++) { - final File e = ls[k]; - if (e.isDirectory()) - silent = recursiveDelete(e, silent, failOnError); - else if (!e.delete()) { - if (!silent) - reportDeleteFailure(failOnError, e); - silent = !failOnError; - } - } - if (!dir.delete()) { - if (!silent) - reportDeleteFailure(failOnError, dir); - silent = !failOnError; + int options = FileUtils.RECURSIVE | FileUtils.RETRY + | FileUtils.SKIP_MISSING; + if (silent) { + options |= FileUtils.IGNORE_ERRORS; } - return silent; + try { + FileUtils.delete(dir, options); + } catch (IOException e) { + reportDeleteFailure(failOnError, dir, e); + return !failOnError; + } + return true; } - private static void reportDeleteFailure(boolean failOnError, File e) { + private static void reportDeleteFailure(boolean failOnError, File f, + Exception cause) { String severity = failOnError ? "ERROR" : "WARNING"; - String msg = severity + ": Failed to delete " + e; - if (failOnError) + String msg = severity + ": Failed to delete " + f; + if (failOnError) { fail(msg); - else + } else { System.err.println(msg); + } + cause.printStackTrace(new PrintStream(System.err)); + } + + /** Constant <code>MOD_TIME=1</code> */ + public static final int MOD_TIME = 1; + + /** Constant <code>SMUDGE=2</code> */ + public static final int SMUDGE = 2; + + /** Constant <code>LENGTH=4</code> */ + public static final int LENGTH = 4; + + /** Constant <code>CONTENT_ID=8</code> */ + public static final int CONTENT_ID = 8; + + /** Constant <code>CONTENT=16</code> */ + public static final int CONTENT = 16; + + /** Constant <code>ASSUME_UNCHANGED=32</code> */ + public static final int ASSUME_UNCHANGED = 32; + + /** + * Represent the state of the index in one String. This representation is + * useful when writing tests which do assertions on the state of the index. + * By default information about path, mode, stage (if different from 0) is + * included. A bitmask controls which additional info about + * modificationTimes, smudge state and length is included. + * <p> + * The format of the returned string is described with this BNF: + * + * <pre> + * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* . + * mode = ", mode:" number . + * stage = ", stage:" number . + * time = ", time:t" timestamp-index . + * smudge = "" | ", smudged" . + * length = ", length:" number . + * sha1 = ", sha1:" hex-sha1 . + * content = ", content:" blob-data . + * </pre> + * + * 'stage' is only presented when the stage is different from 0. All + * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The + * smallest reported time-stamp will be called "t0". This allows to write + * assertions against the string although the concrete value of the time + * stamps is unknown. + * + * @param repo + * the repository the index state should be determined for + * @param includedOptions + * a bitmask constructed out of the constants {@link #MOD_TIME}, + * {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and + * {@link #CONTENT} controlling which info is present in the + * resulting string. + * @return a string encoding the index state + * @throws IOException + * if an IO error occurred + */ + public static String indexState(Repository repo, int includedOptions) + throws IOException { + DirCache dc = repo.readDirCache(); + StringBuilder sb = new StringBuilder(); + TreeSet<Instant> timeStamps = new TreeSet<>(); + + // iterate once over the dircache just to collect all time stamps + if (0 != (includedOptions & MOD_TIME)) { + for (int i = 0; i < dc.getEntryCount(); ++i) { + timeStamps.add(dc.getEntry(i).getLastModifiedInstant()); + } + } + + // iterate again, now produce the result string + for (int i=0; i<dc.getEntryCount(); ++i) { + DirCacheEntry entry = dc.getEntry(i); + sb.append("["+entry.getPathString()+", mode:" + entry.getFileMode()); + int stage = entry.getStage(); + if (stage != 0) + sb.append(", stage:" + stage); + if (0 != (includedOptions & MOD_TIME)) { + sb.append(", time:t"+ + timeStamps.headSet(entry.getLastModifiedInstant()) + .size()); + } + if (0 != (includedOptions & SMUDGE)) + if (entry.isSmudged()) + sb.append(", smudged"); + if (0 != (includedOptions & LENGTH)) + sb.append(", length:" + + Integer.toString(entry.getLength())); + if (0 != (includedOptions & CONTENT_ID)) + sb.append(", sha1:" + ObjectId.toString(entry.getObjectId())); + if (0 != (includedOptions & CONTENT)) { + sb.append(", content:" + + new String(repo.open(entry.getObjectId(), + Constants.OBJ_BLOB).getCachedBytes(), UTF_8)); + } + if (0 != (includedOptions & ASSUME_UNCHANGED)) + sb.append(", assume-unchanged:" + + Boolean.toString(entry.isAssumeValid())); + sb.append("]"); + } + return sb.toString(); } + /** * Creates a new empty bare repository. * - * @return the newly created repository, opened for access + * @return the newly created bare repository, opened for access. The + * repository will not be closed in {@link #tearDown()}; the caller + * is responsible for closing it. * @throws IOException * the repository could not be created in the temporary area */ @@ -243,7 +399,9 @@ public abstract class LocalDiskRepositoryTestCase { /** * Creates a new empty repository within a new empty working directory. * - * @return the newly created repository, opened for access + * @return the newly created repository, opened for access. The repository + * will not be closed in {@link #tearDown()}; the caller is + * responsible for closing it. * @throws IOException * the repository could not be created in the temporary area */ @@ -257,16 +415,41 @@ public abstract class LocalDiskRepositoryTestCase { * @param bare * true to create a bare repository; false to make a repository * within its working directory + * @return the newly created repository, opened for access. The repository + * will not be closed in {@link #tearDown()}; the caller is + * responsible for closing it. + * @throws IOException + * the repository could not be created in the temporary area + * @since 5.3 + */ + protected FileRepository createRepository(boolean bare) + throws IOException { + return createRepository(bare, false /* auto close */); + } + + /** + * Creates a new empty repository. + * + * @param bare + * true to create a bare repository; false to make a repository + * within its working directory + * @param autoClose + * auto close the repository in {@link #tearDown()} * @return the newly created repository, opened for access * @throws IOException * the repository could not be created in the temporary area + * @deprecated use {@link #createRepository(boolean)} instead */ - private FileRepository createRepository(boolean bare) throws IOException { + @Deprecated + public FileRepository createRepository(boolean bare, boolean autoClose) + throws IOException { File gitdir = createUniqueTestGitDir(bare); FileRepository db = new FileRepository(gitdir); assertFalse(gitdir.exists()); db.create(bare); - toClose.add(db); + if (autoClose) { + addRepoToClose(db); + } return db; } @@ -288,6 +471,7 @@ public abstract class LocalDiskRepositoryTestCase { * a subdirectory * @return a unique directory for a test * @throws IOException + * if an IO error occurred */ protected File createTempDirectory(String name) throws IOException { File directory = new File(createTempFile(), name); @@ -303,6 +487,7 @@ public abstract class LocalDiskRepositoryTestCase { * working directory * @return a unique directory for a test repository * @throws IOException + * if an IO error occurred */ protected File createUniqueTestGitDir(boolean bare) throws IOException { String gitdirName = createTempFile().getPath(); @@ -323,6 +508,7 @@ public abstract class LocalDiskRepositoryTestCase { * * @return a unique path that does not exist. * @throws IOException + * if an IO error occurred */ protected File createTempFile() throws IOException { File p = File.createTempFile("tmp_", "", tmp); @@ -386,18 +572,12 @@ public abstract class LocalDiskRepositoryTestCase { * @throws IOException * the file could not be written. */ - protected File write(final String body) throws IOException { + protected File write(String body) throws IOException { final File f = File.createTempFile("temp", "txt", tmp); try { write(f, body); return f; - } catch (Error e) { - f.delete(); - throw e; - } catch (RuntimeException e) { - f.delete(); - throw e; - } catch (IOException e) { + } catch (Error | RuntimeException | IOException e) { f.delete(); throw e; } @@ -417,15 +597,24 @@ public abstract class LocalDiskRepositoryTestCase { * @throws IOException * the file could not be written. */ - protected void write(final File f, final String body) throws IOException { + protected void write(File f, String body) throws IOException { JGitTestUtil.write(f, body); } - protected String read(final File f) throws IOException { + /** + * Read a file's content + * + * @param f + * the file + * @return the content of the file + * @throws IOException + * if an IO error occurred + */ + protected String read(File f) throws IOException { return JGitTestUtil.read(f); } - private static String[] toEnvArray(final Map<String, String> env) { + private static String[] toEnvArray(Map<String, String> env) { final String[] envp = new String[env.size()]; int i = 0; for (Map.Entry<String, String> e : env.entrySet()) @@ -434,44 +623,6 @@ public abstract class LocalDiskRepositoryTestCase { } private static HashMap<String, String> cloneEnv() { - return new HashMap<String, String>(System.getenv()); - } - - private static final class CleanupThread extends Thread { - private static final CleanupThread me; - static { - me = new CleanupThread(); - Runtime.getRuntime().addShutdownHook(me); - } - - static void deleteOnShutdown(File tmp) { - synchronized (me) { - me.toDelete.add(tmp); - } - } - - static void removed(File tmp) { - synchronized (me) { - me.toDelete.remove(tmp); - } - } - - private final List<File> toDelete = new ArrayList<File>(); - - @Override - public void run() { - // On windows accidentally open files or memory - // mapped regions may prevent files from being deleted. - // Suggesting a GC increases the likelihood that our - // test repositories actually get removed after the - // tests, even in the case of failure. - System.gc(); - synchronized (this) { - boolean silent = false; - boolean failOnError = false; - for (File tmp : toDelete) - recursiveDelete(tmp, silent, failOnError); - } - } + return new HashMap<>(System.getenv()); } } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java index 65551d6579..38f0d0b2cb 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java @@ -1,68 +1,46 @@ /* * Copyright (C) 2009, Google Inc. * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.TimeZone; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.SystemReader; +import org.eclipse.jgit.util.time.MonotonicClock; +import org.eclipse.jgit.util.time.ProposedTimestamp; +/** + * Mock {@link org.eclipse.jgit.util.SystemReader} for tests. + */ public class MockSystemReader extends SystemReader { - private final class MockConfig extends FileBasedConfig { + private static final class MockConfig extends FileBasedConfig { private MockConfig(File cfgLocation, FS fs) { super(cfgLocation, fs); } @@ -73,17 +51,73 @@ public class MockSystemReader extends SystemReader { } @Override + public void save() throws IOException { + // Do nothing + } + + @Override public boolean isOutdated() { return false; } + + @Override + public String toString() { + return "MockConfig"; + } } - final Map<String, String> values = new HashMap<String, String>(); + long now = 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009 - FileBasedConfig userGitConfig; + final Map<String, String> values = new HashMap<>(); + + private FileBasedConfig userGitConfig; + + private FileBasedConfig jgitConfig; FileBasedConfig systemGitConfig; + /** + * Set the user-level git config + * + * @param userGitConfig + * set another user-level git config + * @return the old user-level git config + * @since 5.1.9 + */ + public FileBasedConfig setUserGitConfig(FileBasedConfig userGitConfig) { + FileBasedConfig old = this.userGitConfig; + this.userGitConfig = userGitConfig; + return old; + } + + /** + * Set the jgit config stored at $XDG_CONFIG_HOME/jgit/config + * + * @param jgitConfig + * set the jgit configuration + * @since 5.5 + */ + public void setJGitConfig(FileBasedConfig jgitConfig) { + this.jgitConfig = jgitConfig; + } + + /** + * Set the system-level git config + * + * @param systemGitConfig + * the new system-level git config + * @return the old system-level config + * @since 5.1.9 + */ + public FileBasedConfig setSystemGitConfig(FileBasedConfig systemGitConfig) { + FileBasedConfig old = this.systemGitConfig; + this.systemGitConfig = systemGitConfig; + return old; + } + + /** + * Constructor for <code>MockSystemReader</code> + */ public MockSystemReader() { init(Constants.OS_USER_NAME_KEY); init(Constants.GIT_AUTHOR_NAME_KEY); @@ -92,18 +126,30 @@ public class MockSystemReader extends SystemReader { init(Constants.GIT_COMMITTER_EMAIL_KEY); setProperty(Constants.OS_USER_DIR, "."); userGitConfig = new MockConfig(null, null); + jgitConfig = new MockConfig(null, null); systemGitConfig = new MockConfig(null, null); setCurrentPlatform(); } - private void init(final String n) { + private void init(String n) { setProperty(n, n); } + /** + * Clear properties + */ public void clearProperties() { values.clear(); } + /** + * Set a property + * + * @param key + * the key + * @param value + * the value + */ public void setProperty(String key, String value) { values.put(key, value); } @@ -131,13 +177,60 @@ public class MockSystemReader extends SystemReader { } @Override + public StoredConfig getUserConfig() + throws IOException, ConfigInvalidException { + return userGitConfig; + } + + @Override + public FileBasedConfig getJGitConfig() { + return jgitConfig; + } + + @Override + public StoredConfig getSystemConfig() + throws IOException, ConfigInvalidException { + return systemGitConfig; + } + + @Override public String getHostname() { return "fake.host.example.com"; } @Override public long getCurrentTime() { - return 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009 + return now; + } + + @Override + public MonotonicClock getClock() { + return () -> { + long t = getCurrentTime(); + return new ProposedTimestamp() { + + @Override + public long read(TimeUnit unit) { + return unit.convert(t, TimeUnit.MILLISECONDS); + } + + @Override + public void blockUntil(Duration maxWait) { + // Do not wait. + } + }; + }; + } + + /** + * Adjusts the current time in seconds. + * + * @param secDelta + * number of seconds to add to the current time. + * @since 4.2 + */ + public void tick(int secDelta) { + now += secDelta * 1000L; } @Override @@ -151,6 +244,11 @@ public class MockSystemReader extends SystemReader { } @Override + public ZoneId getTimeZoneId() { + return ZoneOffset.ofHoursMinutes(-3, -30); + } + + @Override public Locale getLocale() { return Locale.US; } @@ -170,6 +268,7 @@ public class MockSystemReader extends SystemReader { * Assign some properties for the currently executing platform */ public void setCurrentPlatform() { + resetOsNames(); setProperty("os.name", System.getProperty("os.name")); setProperty("file.separator", System.getProperty("file.separator")); setProperty("path.separator", System.getProperty("path.separator")); @@ -180,6 +279,7 @@ public class MockSystemReader extends SystemReader { * Emulate Windows */ public void setWindows() { + resetOsNames(); setProperty("os.name", "Windows"); setProperty("file.separator", "\\"); setProperty("path.separator", ";"); @@ -191,10 +291,36 @@ public class MockSystemReader extends SystemReader { * Emulate Unix */ public void setUnix() { + resetOsNames(); setProperty("os.name", "*nix"); // Essentially anything but Windows setProperty("file.separator", "/"); setProperty("path.separator", ":"); setProperty("line.separator", "\n"); setPlatformChecker(); } + + private void resetOsNames() { + Field field; + try { + field = SystemReader.class.getDeclaredField("isWindows"); + field.setAccessible(true); + field.set(null, null); + field = SystemReader.class.getDeclaredField("isMacOS"); + field.setAccessible(true); + field.set(null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return "MockSystemReader"; + } + + @Override + public FileBasedConfig openJGitConfig(Config parent, FS fs) { + return jgitConfig; + } + } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java new file mode 100644 index 0000000000..4bf2eb59da --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016, Matthias Sohn <matthias.sohn@sap.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation enabling to run tests repeatedly + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ java.lang.annotation.ElementType.METHOD }) +public @interface Repeat { + /** + * Number of repetitions + * + * @return number of repetitions + */ + public abstract int n(); + + /** + * Whether to abort execution on first test failure + * + * @return {@code true} if execution should be aborted on the first failure, + * otherwise count failures and continue execution + * @since 5.1.9 + */ + public boolean abortOnFailure() default true; +} diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java new file mode 100644 index 0000000000..30fffe9d94 --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2016, Matthias Sohn <matthias.sohn@sap.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.junit; + +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * {@link org.junit.rules.TestRule} which enables to run the same JUnit test + * repeatedly. Add this rule to the test class + * + * <pre> + * public class MyTest { + * @Rule + * public RepeatRule repeatRule = new RepeatRule(); + * ... + * } + * </pre> + * + * and annotate the test to be repeated with the + * {@code @Repeat(n=<repetitions>)} annotation + * + * <pre> + * @Test + * @Repeat(n = 100) + * public void test() { + * ... + * } + * </pre> + * + * then this test will be repeated 100 times. If any test execution fails test + * repetition will be stopped. + */ +public class RepeatRule implements TestRule { + + private static final Logger LOG = Logger + .getLogger(RepeatRule.class.getName()); + + /** + * Exception thrown if repeated execution of a test annotated with + * {@code @Repeat} failed. + */ + public static class RepeatedTestException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Constructor + * + * @param message + * the error message + * @since 5.1.9 + */ + public RepeatedTestException(String message) { + super(message); + } + + /** + * Constructor + * + * @param message + * the error message + * @param cause + * exception causing this exception + */ + public RepeatedTestException(String message, Throwable cause) { + super(message, cause); + } + } + + private static class RepeatStatement extends Statement { + + private final int repetitions; + + private boolean abortOnFailure; + + private final Statement statement; + + private RepeatStatement(int repetitions, boolean abortOnFailure, + Statement statement) { + this.repetitions = repetitions; + this.abortOnFailure = abortOnFailure; + this.statement = statement; + } + + @Override + public void evaluate() throws Throwable { + int failures = 0; + for (int i = 0; i < repetitions; i++) { + try { + statement.evaluate(); + } catch (Throwable e) { + failures += 1; + RepeatedTestException ex = new RepeatedTestException( + MessageFormat.format( + "Repeated test failed when run for the {0}. time", + Integer.valueOf(i + 1)), + e); + LOG.log(Level.SEVERE, ex.getMessage(), ex); + if (abortOnFailure) { + throw ex; + } + } + } + if (failures > 0) { + RepeatedTestException e = new RepeatedTestException( + MessageFormat.format( + "Test failed {0} times out of {1} repeated executions", + Integer.valueOf(failures), + Integer.valueOf(repetitions))); + LOG.log(Level.SEVERE, e.getMessage(), e); + throw e; + } + } + } + + @Override + public Statement apply(Statement statement, Description description) { + Statement result = statement; + Repeat repeat = description.getAnnotation(Repeat.class); + if (repeat != null) { + int n = repeat.n(); + boolean abortOnFailure = repeat.abortOnFailure(); + result = new RepeatStatement(n, abortOnFailure, statement); + } + return result; + } +} diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java index 83148d0009..3a283ce10d 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java @@ -2,50 +2,18 @@ * Copyright (C) 2009, Google Inc. * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> - * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import java.io.File; @@ -55,20 +23,24 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; import java.util.Map; -import java.util.TreeSet; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; @@ -76,6 +48,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; +import org.junit.After; import org.junit.Before; /** @@ -85,58 +58,136 @@ import org.junit.Before; * repositories and destroying them when the tests are finished. */ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { - protected static void copyFile(final File src, final File dst) + /** + * Copy a file + * + * @param src + * file to copy + * @param dst + * destination of the copy + * @throws IOException + * if an IO error occurred + */ + protected static void copyFile(File src, File dst) throws IOException { - final FileInputStream fis = new FileInputStream(src); - try { - final FileOutputStream fos = new FileOutputStream(dst); - try { - final byte[] buf = new byte[4096]; - int r; - while ((r = fis.read(buf)) > 0) { - fos.write(buf, 0, r); - } - } finally { - fos.close(); + try (FileInputStream fis = new FileInputStream(src); + FileOutputStream fos = new FileOutputStream(dst)) { + final byte[] buf = new byte[4096]; + int r; + while ((r = fis.read(buf)) > 0) { + fos.write(buf, 0, r); } - } finally { - fis.close(); } } - protected File writeTrashFile(final String name, final String data) + /** + * Write a trash file + * + * @param name + * file name + * @param data + * file content + * @return the trash file + * @throws IOException + * if an IO error occurred + */ + protected File writeTrashFile(String name, String data) throws IOException { return JGitTestUtil.writeTrashFile(db, name, data); } + /** + * Create a symbolic link + * + * @param link + * the path of the symbolic link to create + * @param target + * the target of the symbolic link + * @return the path to the symbolic link + * @throws Exception + * if an error occurred + * @since 4.2 + */ + protected Path writeLink(String link, String target) + throws Exception { + return JGitTestUtil.writeLink(db, link, target); + } + + /** + * Write a trash file + * + * @param subdir + * in working tree + * @param name + * file name + * @param data + * file content + * @return the trash file + * @throws IOException + * if an IO error occurred + */ protected File writeTrashFile(final String subdir, final String name, final String data) throws IOException { return JGitTestUtil.writeTrashFile(db, subdir, name, data); } - protected String read(final String name) throws IOException { + /** + * Read content of a file + * + * @param name + * file name + * @return the file's content + * @throws IOException + * if an IO error occurred + */ + protected String read(String name) throws IOException { return JGitTestUtil.read(db, name); } - protected boolean check(final String name) { + /** + * Check if file exists + * + * @param name + * file name + * @return if the file exists + */ + protected boolean check(String name) { return JGitTestUtil.check(db, name); } - protected void deleteTrashFile(final String name) throws IOException { + /** + * Delete a trash file + * + * @param name + * file name + * @throws IOException + * if an IO error occurred + */ + protected void deleteTrashFile(String name) throws IOException { JGitTestUtil.deleteTrashFile(db, name); } - protected static void checkFile(File f, final String checkData) + /** + * Check content of a file. + * + * @param f + * file + * @param checkData + * expected content + * @throws IOException + * if an IO error occurred + */ + protected static void checkFile(File f, String checkData) throws IOException { - Reader r = new InputStreamReader(new FileInputStream(f), "ISO-8859-1"); - try { - char[] data = new char[(int) f.length()]; - if (f.length() != r.read(data)) - throw new IOException("Internal error reading file data from "+f); - assertEquals(checkData, new String(data)); - } finally { - r.close(); + try (Reader r = new InputStreamReader(new FileInputStream(f), + UTF_8)) { + if (checkData.length() > 0) { + char[] data = new char[checkData.length()]; + assertEquals(data.length, r.read(data)); + assertEquals(checkData, new String(data)); + } + assertEquals(-1, r.read()); } } @@ -154,99 +205,11 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { trash = db.getWorkTree(); } - public static final int MOD_TIME = 1; - - public static final int SMUDGE = 2; - - public static final int LENGTH = 4; - - public static final int CONTENT_ID = 8; - - public static final int CONTENT = 16; - - public static final int ASSUME_UNCHANGED = 32; - - /** - * Represent the state of the index in one String. This representation is - * useful when writing tests which do assertions on the state of the index. - * By default information about path, mode, stage (if different from 0) is - * included. A bitmask controls which additional info about - * modificationTimes, smudge state and length is included. - * <p> - * The format of the returned string is described with this BNF: - * - * <pre> - * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* . - * mode = ", mode:" number . - * stage = ", stage:" number . - * time = ", time:t" timestamp-index . - * smudge = "" | ", smudged" . - * length = ", length:" number . - * sha1 = ", sha1:" hex-sha1 . - * content = ", content:" blob-data . - * </pre> - * - * 'stage' is only presented when the stage is different from 0. All - * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The - * smallest reported time-stamp will be called "t0". This allows to write - * assertions against the string although the concrete value of the time - * stamps is unknown. - * - * @param repo - * the repository the index state should be determined for - * - * @param includedOptions - * a bitmask constructed out of the constants {@link #MOD_TIME}, - * {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and - * {@link #CONTENT} controlling which info is present in the - * resulting string. - * @return a string encoding the index state - * @throws IllegalStateException - * @throws IOException - */ - public String indexState(Repository repo, int includedOptions) - throws IllegalStateException, IOException { - DirCache dc = repo.readDirCache(); - StringBuilder sb = new StringBuilder(); - TreeSet<Long> timeStamps = null; - - // iterate once over the dircache just to collect all time stamps - if (0 != (includedOptions & MOD_TIME)) { - timeStamps = new TreeSet<Long>(); - for (int i=0; i<dc.getEntryCount(); ++i) - timeStamps.add(Long.valueOf(dc.getEntry(i).getLastModified())); - } - - // iterate again, now produce the result string - for (int i=0; i<dc.getEntryCount(); ++i) { - DirCacheEntry entry = dc.getEntry(i); - sb.append("["+entry.getPathString()+", mode:" + entry.getFileMode()); - int stage = entry.getStage(); - if (stage != 0) - sb.append(", stage:" + stage); - if (0 != (includedOptions & MOD_TIME)) { - sb.append(", time:t"+ - timeStamps.headSet(Long.valueOf(entry.getLastModified())).size()); - } - if (0 != (includedOptions & SMUDGE)) - if (entry.isSmudged()) - sb.append(", smudged"); - if (0 != (includedOptions & LENGTH)) - sb.append(", length:" - + Integer.toString(entry.getLength())); - if (0 != (includedOptions & CONTENT_ID)) - sb.append(", sha1:" + ObjectId.toString(entry.getObjectId())); - if (0 != (includedOptions & CONTENT)) { - sb.append(", content:" - + new String(db.open(entry.getObjectId(), - Constants.OBJ_BLOB).getCachedBytes(), "UTF-8")); - } - if (0 != (includedOptions & ASSUME_UNCHANGED)) - sb.append(", assume-unchanged:" - + Boolean.toString(entry.isAssumeValid())); - sb.append("]"); - } - return sb.toString(); + @Override + @After + public void tearDown() throws Exception { + db.close(); + super.tearDown(); } /** @@ -281,11 +244,11 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * {@link #CONTENT} controlling which info is present in the * resulting string. * @return a string encoding the index state - * @throws IllegalStateException * @throws IOException + * if an IO error occurred */ public String indexState(int includedOptions) - throws IllegalStateException, IOException { + throws IOException { return indexState(db, includedOptions); } @@ -300,10 +263,12 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * have an index which matches their prepared content. * * @param treeItr - * a {@link FileTreeIterator} which determines which files should - * go into the new index + * a {@link org.eclipse.jgit.treewalk.FileTreeIterator} which + * determines which files should go into the new index * @throws FileNotFoundException + * file was not found * @throws IOException + * if an IO error occurred */ protected void resetIndex(FileTreeIterator treeItr) throws FileNotFoundException, IOException { @@ -316,12 +281,13 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { dce = new DirCacheEntry(treeItr.getEntryPathString()); dce.setFileMode(treeItr.getEntryFileMode()); - dce.setLastModified(treeItr.getEntryLastModified()); + dce.setLastModified(treeItr.getEntryLastModifiedInstant()); dce.setLength((int) len); - FileInputStream in = new FileInputStream( - treeItr.getEntryFile()); - dce.setObjectId(inserter.insert(Constants.OBJ_BLOB, len, in)); - in.close(); + try (FileInputStream in = new FileInputStream( + treeItr.getEntryFile())) { + dce.setObjectId( + inserter.insert(Constants.OBJ_BLOB, len, in)); + } builder.add(dce); treeItr.next(1); } @@ -341,13 +307,13 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * * @param l * the object to lookup + * @param lookupTable + * a table storing object-name mappings. * @param nameTemplate * the name for that object. Can contain "%n" which will be * replaced by a running number before used as a name. If the * lookup table already contains the object this parameter will * be ignored - * @param lookupTable - * a table storing object-name mappings. * @return a name of that object. Is not guaranteed to be unique. Use * nameTemplates containing "%n" to always have unique names */ @@ -363,11 +329,25 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { } /** + * Replaces '\' by '/' + * + * @param str + * the string in which backslashes should be replaced + * @return the resulting string with slashes + * @since 4.2 + */ + public static String slashify(String str) { + str = str.replace('\\', '/'); + return str; + } + + /** * Waits until it is guaranteed that a subsequent file modification has a * younger modification timestamp than the modification timestamp of the * given file. This is done by touching a temporary file, reading the * lastmodified attribute and, if needed, sleeping. After sleeping this loop - * starts again until the filesystem timer has advanced enough. + * starts again until the filesystem timer has advanced enough. The + * temporary file will be created as a sibling of lastFile. * * @param lastFile * the file on which we want to wait until the filesystem timer @@ -376,25 +356,36 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * @return return the last measured value of the filesystem timer which is * greater than then the lastmodification time of lastfile. * @throws InterruptedException + * if thread was interrupted * @throws IOException + * if an IO error occurred + * @since 5.1.9 */ - public static long fsTick(File lastFile) throws InterruptedException, + public static Instant fsTick(File lastFile) + throws InterruptedException, IOException { - long sleepTime = 64; + File tmp; FS fs = FS.DETECTED; - if (lastFile != null && !fs.exists(lastFile)) - throw new FileNotFoundException(lastFile.getPath()); - File tmp = File.createTempFile("FileTreeIteratorWithTimeControl", null); + if (lastFile == null) { + lastFile = tmp = File + .createTempFile("fsTickTmpFile", null); + } else { + if (!fs.exists(lastFile)) { + throw new FileNotFoundException(lastFile.getPath()); + } + tmp = File.createTempFile("fsTickTmpFile", null, + lastFile.getParentFile()); + } + long res = FS.getFileStoreAttributes(tmp.toPath()) + .getFsTimestampResolution().toNanos(); + long sleepTime = res / 10; try { - long startTime = (lastFile == null) ? fs.lastModified(tmp) : fs - .lastModified(lastFile); - long actTime = fs.lastModified(tmp); - while (actTime <= startTime) { - Thread.sleep(sleepTime); - sleepTime *= 2; - FileOutputStream fos = new FileOutputStream(tmp); - fos.close(); - actTime = fs.lastModified(tmp); + Instant startTime = fs.lastModifiedInstant(lastFile); + Instant actTime = fs.lastModifiedInstant(tmp); + while (actTime.compareTo(startTime) <= 0) { + TimeUnit.NANOSECONDS.sleep(sleepTime); + FileUtils.touch(tmp.toPath()); + actTime = fs.lastModifiedInstant(tmp); } return actTime; } finally { @@ -402,6 +393,16 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { } } + /** + * Create a branch + * + * @param objectId + * new value to create the branch on + * @param branchName + * branch name + * @throws IOException + * if an IO error occurred + */ protected void createBranch(ObjectId objectId, String branchName) throws IOException { RefUpdate updateRef = db.updateRef(branchName); @@ -409,8 +410,27 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { updateRef.update(); } + /** + * Get all Refs + * + * @return list of refs + * @throws IOException + * if an IO error occurred + */ + public List<Ref> getRefs() throws IOException { + return db.getRefDatabase().getRefs(); + } + + /** + * Checkout a branch + * + * @param branchName + * branch name + * @throws IOException + * if an IO error occurred + */ protected void checkoutBranch(String branchName) - throws IllegalStateException, IOException { + throws IOException { try (RevWalk walk = new RevWalk(db)) { RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD)); RevCommit branch = walk.parseCommit(db.resolve(branchName)); @@ -440,7 +460,9 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * the contents which should be written into the files * @return the File object associated to the last written file. * @throws IOException + * if an IO error occurred * @throws InterruptedException + * if thread was interrupted */ protected File writeTrashFiles(boolean ensureDistinctTimestamps, String... contents) @@ -463,18 +485,20 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { * one. * * @param filename + * file name * @param contents + * file content * @param branch + * branch name * @return the created commit */ protected RevCommit commitFile(String filename, String contents, String branch) { - try { - Git git = new Git(db); + try (Git git = new Git(db)) { Repository repo = git.getRepository(); String originalBranch = repo.getFullBranch(); boolean empty = repo.resolve(Constants.HEAD) == null; if (!empty) { - if (repo.getRef(branch) == null) + if (repo.findRef(branch) == null) git.branchCreate().setName(branch).call(); git.checkout().setName(branch).call(); } @@ -490,31 +514,91 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { git.branchCreate().setName(branch).setStartPoint(commit).call(); return commit; - } catch (IOException e) { - throw new RuntimeException(e); - } catch (GitAPIException e) { + } catch (IOException | GitAPIException e) { throw new RuntimeException(e); } } - protected DirCacheEntry createEntry(final String path, final FileMode mode) { + /** + * Create <code>DirCacheEntry</code> + * + * @param path + * file path + * @param mode + * file mode + * @return the DirCacheEntry + */ + protected DirCacheEntry createEntry(String path, FileMode mode) { return createEntry(path, mode, DirCacheEntry.STAGE_0, path); } + /** + * Create <code>DirCacheEntry</code> + * + * @param path + * file path + * @param mode + * file mode + * @param content + * file content + * @return the DirCacheEntry + */ protected DirCacheEntry createEntry(final String path, final FileMode mode, final String content) { return createEntry(path, mode, DirCacheEntry.STAGE_0, content); } + /** + * Create <code>DirCacheEntry</code> + * + * @param path + * file path + * @param mode + * file mode + * @param stage + * stage index of the new entry + * @param content + * file content + * @return the DirCacheEntry + */ protected DirCacheEntry createEntry(final String path, final FileMode mode, final int stage, final String content) { final DirCacheEntry entry = new DirCacheEntry(path, stage); entry.setFileMode(mode); - entry.setObjectId(new ObjectInserter.Formatter().idFor( - Constants.OBJ_BLOB, Constants.encode(content))); + try (ObjectInserter.Formatter formatter = new ObjectInserter.Formatter()) { + entry.setObjectId(formatter.idFor( + Constants.OBJ_BLOB, Constants.encode(content))); + } return entry; } + /** + * Create <code>DirCacheEntry</code> + * + * @param path + * file path + * @param objectId + * of the entry + * @return the DirCacheEntry + */ + protected DirCacheEntry createGitLink(String path, AnyObjectId objectId) { + final DirCacheEntry entry = new DirCacheEntry(path, + DirCacheEntry.STAGE_0); + entry.setFileMode(FileMode.GITLINK); + entry.setObjectId(objectId); + return entry; + } + + /** + * Assert files are equal + * + * @param expected + * expected file + * @param actual + * actual file + * @throws IOException + * if an IO error occurred + */ public static void assertEqualsFile(File expected, File actual) throws IOException { assertEquals(expected.getCanonicalFile(), actual.getCanonicalFile()); diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java new file mode 100644 index 0000000000..2a482df04a --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 Nail Samatov <sanail@yandex.ru> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.junit; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; + +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.InitializationError; + +/** + * This class is used when it's required to load jgit classes in separate + * classloader for each test class. It can be needed to isolate static field + * initialization between separate tests. + * + * @since 5.5 + */ +public class SeparateClassloaderTestRunner extends BlockJUnit4ClassRunner { + + /** + * Creates a SeparateClassloaderTestRunner to run {@code klass}. + * + * @param klass + * test class to run. + * @throws InitializationError + * if the test class is malformed or can't be found. + */ + public SeparateClassloaderTestRunner(Class<?> klass) + throws InitializationError { + super(loadNewClass(klass)); + } + + private static Class<?> loadNewClass(Class<?> klass) + throws InitializationError { + try { + String pathSeparator = System.getProperty("path.separator"); + String[] classPathEntries = System.getProperty("java.class.path") + .split(pathSeparator, -1); + URL[] urls = new URL[classPathEntries.length]; + for (int i = 0; i < classPathEntries.length; i++) { + urls[i] = Paths.get(classPathEntries[i]).toUri().toURL(); + } + ClassLoader testClassLoader = new URLClassLoader(urls) { + + @Override + public Class<?> loadClass(String name) + throws ClassNotFoundException { + if (name.startsWith("org.eclipse.jgit.")) { + return super.findClass(name); + } + + return super.loadClass(name); + } + }; + return Class.forName(klass.getName(), true, testClassLoader); + } catch (ClassNotFoundException | MalformedURLException e) { + throw new InitializationError(e); + } + } +} diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/StrictWorkMonitor.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/StrictWorkMonitor.java new file mode 100644 index 0000000000..0168ecea30 --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/StrictWorkMonitor.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017 Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.junit; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.jgit.lib.ProgressMonitor; + +/** + * Strict work monitor + */ +public final class StrictWorkMonitor implements ProgressMonitor { + private int lastWork, totalWork; + + @Override + public void start(int totalTasks) { + // empty + } + + @Override + public void beginTask(String title, int total) { + this.totalWork = total; + lastWork = 0; + } + + @Override + public void update(int completed) { + lastWork += completed; + } + + @Override + public void endTask() { + assertEquals("Units of work recorded", totalWork, lastWork); + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void showDuration(boolean enabled) { + // not implemented + } +} diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java index 925a6b0216..c546ae9082 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java @@ -1,56 +1,27 @@ /* - * Copyright (C) 2009-2010, Google Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2009-2010, Google Inc. and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.security.MessageDigest; +import java.time.Instant; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -60,6 +31,7 @@ import java.util.List; import java.util.Set; import java.util.TimeZone; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -74,8 +46,10 @@ import org.eclipse.jgit.errors.ObjectWritingException; import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.internal.storage.file.ObjectDirectory; +import org.eclipse.jgit.internal.storage.file.Pack; import org.eclipse.jgit.internal.storage.file.PackFile; import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; +import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; @@ -99,11 +73,11 @@ import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.util.ChangeIdUtil; import org.eclipse.jgit.util.FileUtils; -import org.eclipse.jgit.util.io.SafeBufferedOutputStream; /** * Wrapper to make creating test data easier. @@ -111,24 +85,23 @@ import org.eclipse.jgit.util.io.SafeBufferedOutputStream; * @param <R> * type of Repository the test data is stored on. */ -public class TestRepository<R extends Repository> { - private static final PersonIdent defaultAuthor; +public class TestRepository<R extends Repository> implements AutoCloseable { - private static final PersonIdent defaultCommitter; + /** Constant <code>AUTHOR="J. Author"</code> */ + public static final String AUTHOR = "J. Author"; - static { - final MockSystemReader m = new MockSystemReader(); - final long now = m.getCurrentTime(); - final int tz = m.getTimezone(now); + /** Constant <code>AUTHOR_EMAIL="jauthor@example.com"</code> */ + public static final String AUTHOR_EMAIL = "jauthor@example.com"; - final String an = "J. Author"; - final String ae = "jauthor@example.com"; - defaultAuthor = new PersonIdent(an, ae, now, tz); + /** Constant <code>COMMITTER="J. Committer"</code> */ + public static final String COMMITTER = "J. Committer"; - final String cn = "J. Committer"; - final String ce = "jcommitter@example.com"; - defaultCommitter = new PersonIdent(cn, ce, now, tz); - } + /** Constant <code>COMMITTER_EMAIL="jcommitter@example.com"</code> */ + public static final String COMMITTER_EMAIL = "jcommitter@example.com"; + + private final PersonIdent defaultAuthor; + + private final PersonIdent defaultCommitter; private final R db; @@ -138,7 +111,7 @@ public class TestRepository<R extends Repository> { private final ObjectInserter inserter; - private long now; + private final MockSystemReader mockSystemReader; /** * Wrap a repository with test building tools. @@ -146,9 +119,10 @@ public class TestRepository<R extends Repository> { * @param db * the test repository to write into. * @throws IOException + * if an IO error occurred */ public TestRepository(R db) throws IOException { - this(db, new RevWalk(db)); + this(db, new RevWalk(db), new MockSystemReader()); } /** @@ -159,41 +133,110 @@ public class TestRepository<R extends Repository> { * @param rw * the RevObject pool to use for object lookup. * @throws IOException + * if an IO error occurred */ public TestRepository(R db, RevWalk rw) throws IOException { + this(db, rw, new MockSystemReader()); + } + + /** + * Wrap a repository with test building tools. + * + * @param db + * the test repository to write into. + * @param rw + * the RevObject pool to use for object lookup. + * @param reader + * the MockSystemReader to use for clock and other system + * operations. + * @throws IOException + * if an IO error occurred + * @since 4.2 + */ + public TestRepository(R db, RevWalk rw, MockSystemReader reader) + throws IOException { this.db = db; this.git = Git.wrap(db); this.pool = rw; this.inserter = db.newObjectInserter(); - this.now = 1236977987000L; + this.mockSystemReader = reader; + Instant now = mockSystemReader.now(); + ZoneId tz = mockSystemReader.getTimeZoneAt(now); + defaultAuthor = new PersonIdent(AUTHOR, AUTHOR_EMAIL, now, tz); + defaultCommitter = new PersonIdent(COMMITTER, COMMITTER_EMAIL, now, tz); } - /** @return the repository this helper class operates against. */ + /** + * Get repository + * + * @return the repository this helper class operates against. + */ public R getRepository() { return db; } - /** @return get the RevWalk pool all objects are allocated through. */ + /** + * Get RevWalk + * + * @return get the RevWalk pool all objects are allocated through. + */ public RevWalk getRevWalk() { return pool; } /** + * Return Git API wrapper + * * @return an API wrapper for the underlying repository. This wrapper does - * not allocate any new resources and need not be closed (but closing - * it is harmless). */ + * not allocate any new resources and need not be closed (but + * closing it is harmless). + */ public Git git() { return git; } - /** @return current time adjusted by {@link #tick(int)}. */ - public Date getClock() { - return new Date(now); + /** + * Get date + * + * @return current date. + * @since 4.2 + * @deprecated Use {@link #getInstant()} instead. + */ + @Deprecated(since = "7.2") + public Date getDate() { + return new Date(mockSystemReader.getCurrentTime()); + } + + /** + * Get instant + * + * @return current instant. + * @since 6.8 + */ + public Instant getInstant() { + return mockSystemReader.now(); } - /** @return timezone used for default identities. */ + /** + * Get timezone + * + * @return timezone used for default identities. + * @deprecated Use {@link #getTimeZoneId()} instead. + */ + @Deprecated(since = "7.2") public TimeZone getTimeZone() { - return defaultCommitter.getTimeZone(); + return mockSystemReader.getTimeZone(); + } + + + /** + * Get timezone + * + * @return timezone used for default identities. + * @since 7.2 + */ + public ZoneId getTimeZoneId() { + return mockSystemReader.getTimeZoneId(); } /** @@ -202,19 +245,19 @@ public class TestRepository<R extends Repository> { * @param secDelta * number of seconds to add to the current time. */ - public void tick(final int secDelta) { - now += secDelta * 1000L; + public void tick(int secDelta) { + mockSystemReader.tick(secDelta); } /** - * Set the author and committer using {@link #getClock()}. + * Set the author and committer using {@link #getInstant()}. * * @param c * the commit builder to store. */ public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) { - c.setAuthor(new PersonIdent(defaultAuthor, new Date(now))); - c.setCommitter(new PersonIdent(defaultCommitter, new Date(now))); + c.setAuthor(new PersonIdent(defaultAuthor, getInstant())); + c.setCommitter(new PersonIdent(defaultCommitter, getInstant())); } /** @@ -224,9 +267,10 @@ public class TestRepository<R extends Repository> { * file content, will be UTF-8 encoded. * @return reference to the blob. * @throws Exception + * if an error occurred */ - public RevBlob blob(final String content) throws Exception { - return blob(content.getBytes("UTF-8")); + public RevBlob blob(String content) throws Exception { + return blob(content.getBytes(UTF_8)); } /** @@ -234,16 +278,17 @@ public class TestRepository<R extends Repository> { * * @param content * binary file content. - * @return reference to the blob. + * @return the new, fully parsed blob. * @throws Exception + * if an error occurred */ - public RevBlob blob(final byte[] content) throws Exception { + public RevBlob blob(byte[] content) throws Exception { ObjectId id; try (ObjectInserter ins = inserter) { id = ins.insert(Constants.OBJ_BLOB, content); ins.flush(); } - return pool.lookupBlob(id); + return (RevBlob) pool.parseAny(id); } /** @@ -255,8 +300,9 @@ public class TestRepository<R extends Repository> { * a blob, previously constructed in the repository. * @return the entry. * @throws Exception + * if an error occurred */ - public DirCacheEntry file(final String path, final RevBlob blob) + public DirCacheEntry file(String path, RevBlob blob) throws Exception { final DirCacheEntry e = new DirCacheEntry(path); e.setFileMode(FileMode.REGULAR_FILE); @@ -265,26 +311,47 @@ public class TestRepository<R extends Repository> { } /** + * Construct a symlink mode tree entry. + * + * @param path + * path of the symlink. + * @param blob + * a blob, previously constructed in the repository. + * @return the entry. + * @throws Exception + * if an error occurred + * @since 6.3 + */ + public DirCacheEntry link(String path, RevBlob blob) throws Exception { + DirCacheEntry e = new DirCacheEntry(path); + e.setFileMode(FileMode.SYMLINK); + e.setObjectId(blob); + return e; + } + + /** * Construct a tree from a specific listing of file entries. * * @param entries * the files to include in the tree. The collection does not need * to be sorted properly and may be empty. - * @return reference to the tree specified by the entry list. + * @return the new, fully parsed tree specified by the entry list. * @throws Exception + * if an error occurred */ - public RevTree tree(final DirCacheEntry... entries) throws Exception { + public RevTree tree(DirCacheEntry... entries) throws Exception { final DirCache dc = DirCache.newInCore(); final DirCacheBuilder b = dc.builder(); - for (final DirCacheEntry e : entries) + for (DirCacheEntry e : entries) { b.add(e); + } b.finish(); ObjectId root; try (ObjectInserter ins = inserter) { root = dc.writeTree(ins); ins.flush(); } - return pool.lookupTree(root); + return pool.parseTree(root); } /** @@ -296,8 +363,9 @@ public class TestRepository<R extends Repository> { * the path to find the entry of. * @return the parsed object entry at this path, never null. * @throws Exception + * if an error occurred */ - public RevObject get(final RevTree tree, final String path) + public RevObject get(RevTree tree, String path) throws Exception { try (TreeWalk tw = new TreeWalk(pool.getObjectReader())) { tw.setFilter(PathFilterGroup.createFromStrings(Collections @@ -318,6 +386,23 @@ public class TestRepository<R extends Repository> { } /** + * Create a new, unparsed commit. + * <p> + * See {@link #unparsedCommit(int, RevTree, ObjectId...)}. The tree is the + * empty tree (no files or subdirectories). + * + * @param parents + * zero or more IDs of the commit's parents. + * @return the ID of the new commit. + * @throws Exception + * if an error occurred + * @since 5.5 + */ + public ObjectId unparsedCommit(ObjectId... parents) throws Exception { + return unparsedCommit(1, tree(), parents); + } + + /** * Create a new commit. * <p> * See {@link #commit(int, RevTree, RevCommit...)}. The tree is the empty @@ -327,8 +412,9 @@ public class TestRepository<R extends Repository> { * zero or more parents of the commit. * @return the new commit. * @throws Exception + * if an error occurred */ - public RevCommit commit(final RevCommit... parents) throws Exception { + public RevCommit commit(RevCommit... parents) throws Exception { return commit(1, tree(), parents); } @@ -343,8 +429,9 @@ public class TestRepository<R extends Repository> { * zero or more parents of the commit. * @return the new commit. * @throws Exception + * if an error occurred */ - public RevCommit commit(final RevTree tree, final RevCommit... parents) + public RevCommit commit(RevTree tree, RevCommit... parents) throws Exception { return commit(1, tree, parents); } @@ -361,8 +448,9 @@ public class TestRepository<R extends Repository> { * zero or more parents of the commit. * @return the new commit. * @throws Exception + * if an error occurred */ - public RevCommit commit(final int secDelta, final RevCommit... parents) + public RevCommit commit(int secDelta, RevCommit... parents) throws Exception { return commit(secDelta, tree(), parents); } @@ -380,11 +468,36 @@ public class TestRepository<R extends Repository> { * the root tree for the commit. * @param parents * zero or more parents of the commit. - * @return the new commit. + * @return the new, fully parsed commit. * @throws Exception + * if an error occurred */ public RevCommit commit(final int secDelta, final RevTree tree, final RevCommit... parents) throws Exception { + ObjectId id = unparsedCommit(secDelta, tree, parents); + return pool.parseCommit(id); + } + + /** + * Create a new, unparsed commit. + * <p> + * The author and committer identities are stored using the current + * timestamp, after being incremented by {@code secDelta}. The message body + * is empty. + * + * @param secDelta + * number of seconds to advance {@link #tick(int)} by. + * @param tree + * the root tree for the commit. + * @param parents + * zero or more IDs of the commit's parents. + * @return the ID of the new commit. + * @throws Exception + * if an error occurred + * @since 5.5 + */ + public ObjectId unparsedCommit(final int secDelta, final RevTree tree, + final ObjectId... parents) throws Exception { tick(secDelta); final org.eclipse.jgit.lib.CommitBuilder c; @@ -392,18 +505,22 @@ public class TestRepository<R extends Repository> { c = new org.eclipse.jgit.lib.CommitBuilder(); c.setTreeId(tree); c.setParentIds(parents); - c.setAuthor(new PersonIdent(defaultAuthor, new Date(now))); - c.setCommitter(new PersonIdent(defaultCommitter, new Date(now))); + c.setAuthor(new PersonIdent(defaultAuthor, getInstant())); + c.setCommitter(new PersonIdent(defaultCommitter, getInstant())); c.setMessage(""); ObjectId id; try (ObjectInserter ins = inserter) { id = ins.insert(c); ins.flush(); } - return pool.lookupCommit(id); + return id; } - /** @return a new commit builder. */ + /** + * Create commit builder + * + * @return a new commit builder. + */ public CommitBuilder commit() { return new CommitBuilder(); } @@ -421,21 +538,22 @@ public class TestRepository<R extends Repository> { * with {@code refs/tags/}. * @param dst * object the tag should be pointed at. - * @return the annotated tag object. + * @return the new, fully parsed annotated tag object. * @throws Exception + * if an error occurred */ - public RevTag tag(final String name, final RevObject dst) throws Exception { + public RevTag tag(String name, RevObject dst) throws Exception { final TagBuilder t = new TagBuilder(); t.setObjectId(dst); t.setTag(name); - t.setTagger(new PersonIdent(defaultCommitter, new Date(now))); + t.setTagger(new PersonIdent(defaultCommitter, getInstant())); t.setMessage(""); ObjectId id; try (ObjectInserter ins = inserter) { id = ins.insert(t); ins.flush(); } - return (RevTag) pool.lookupAny(id, Constants.OBJ_TAG); + return pool.parseTag(id); } /** @@ -451,6 +569,7 @@ public class TestRepository<R extends Repository> { * the target object. * @return the target object. * @throws Exception + * if an error occurred */ public RevCommit update(String ref, CommitBuilder to) throws Exception { return update(ref, to.create()); @@ -461,16 +580,17 @@ public class TestRepository<R extends Repository> { * * @param ref * the name of the reference to amend, which must already exist. - * If {@code ref} does not start with {@code refs/} and is not the - * magic names {@code HEAD} {@code FETCH_HEAD} or {@code - * MERGE_HEAD}, then {@code refs/heads/} will be prefixed in front - * of the given name, thereby assuming it is a branch. + * If {@code ref} does not start with {@code refs/} and is not + * the magic names {@code HEAD} {@code FETCH_HEAD} or {@code + * MERGE_HEAD}, then {@code refs/heads/} will be prefixed in + * front of the given name, thereby assuming it is a branch. * @return commit builder that amends the branch on commit. * @throws Exception + * if an error occurred */ public CommitBuilder amendRef(String ref) throws Exception { String name = normalizeRef(ref); - Ref r = db.getRef(name); + Ref r = db.exactRef(name); if (r == null) throw new IOException("Not a ref: " + ref); return amend(pool.parseCommit(r.getObjectId()), branch(name).commit()); @@ -483,6 +603,7 @@ public class TestRepository<R extends Repository> { * the id of the commit to amend. * @return commit builder. * @throws Exception + * if an error occurred */ public CommitBuilder amend(AnyObjectId id) throws Exception { return amend(pool.parseCommit(id), commit()); @@ -537,6 +658,7 @@ public class TestRepository<R extends Repository> { * the target object. * @return the target object. * @throws Exception + * if an error occurred */ public <T extends AnyObjectId> T update(String ref, T obj) throws Exception { ref = normalizeRef(ref); @@ -555,6 +677,33 @@ public class TestRepository<R extends Repository> { } } + /** + * Delete a reference. + * + * @param ref + * the name of the reference to delete. This is normalized in the + * same way as {@link #update(String, AnyObjectId)}. + * @throws Exception + * if an error occurred + * @since 4.4 + */ + public void delete(String ref) throws Exception { + ref = normalizeRef(ref); + RefUpdate u = db.updateRef(ref); + u.setForceUpdate(true); + switch (u.delete()) { + case FAST_FORWARD: + case FORCED: + case NEW: + case NO_CHANGE: + updateServerInfo(); + return; + + default: + throw new IOException("Cannot delete " + ref + " " + u.getResult()); + } + } + private static String normalizeRef(String ref) { if (Constants.HEAD.equals(ref)) { // nothing @@ -571,10 +720,11 @@ public class TestRepository<R extends Repository> { /** * Soft-reset HEAD to a detached state. - * <p> + * * @param id * ID of detached head. * @throws Exception + * if an error occurred * @see #reset(String) */ public void reset(AnyObjectId id) throws Exception { @@ -596,13 +746,14 @@ public class TestRepository<R extends Repository> { /** * Soft-reset HEAD to a different commit. * <p> - * This is equivalent to {@code git reset --soft} in that it modifies HEAD but - * not the index or the working tree of a non-bare repository. + * This is equivalent to {@code git reset --soft} in that it modifies HEAD + * but not the index or the working tree of a non-bare repository. * * @param name - * revision string; either an existing ref name, or something that - * can be parsed to an object ID. + * revision string; either an existing ref name, or something + * that can be parsed to an object ID. * @throws Exception + * if an error occurred */ public void reset(String name) throws Exception { RefUpdate.Result result; @@ -633,9 +784,10 @@ public class TestRepository<R extends Repository> { * * @param id * commit-ish to cherry-pick. - * @return newly created commit, or null if no work was done due to the - * resulting tree being identical. + * @return the new, fully parsed commit, or null if no work was done due to + * the resulting tree being identical. * @throws Exception + * if an error occurred */ public RevCommit cherryPick(AnyObjectId id) throws Exception { RevCommit commit = pool.parseCommit(id); @@ -647,7 +799,7 @@ public class TestRepository<R extends Repository> { RevCommit parent = commit.getParent(0); pool.parseHeaders(parent); - Ref headRef = db.getRef(Constants.HEAD); + Ref headRef = db.exactRef(Constants.HEAD); if (headRef == null) throw new IOException("Missing HEAD"); RevCommit head = pool.parseCommit(headRef.getObjectId()); @@ -655,7 +807,7 @@ public class TestRepository<R extends Repository> { ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true); merger.setBase(parent.getTree()); if (merger.merge(head, commit)) { - if (AnyObjectId.equals(head.getTree(), merger.getResultTreeId())) + if (AnyObjectId.isEqual(head.getTree(), merger.getResultTreeId())) return null; tick(1); org.eclipse.jgit.lib.CommitBuilder b = @@ -663,7 +815,7 @@ public class TestRepository<R extends Repository> { b.setParentId(head); b.setTreeId(merger.getResultTreeId()); b.setAuthor(commit.getAuthorIdent()); - b.setCommitter(new PersonIdent(defaultCommitter, new Date(now))); + b.setCommitter(new PersonIdent(defaultCommitter, getInstant())); b.setMessage(commit.getFullMessage()); ObjectId result; try (ObjectInserter ins = inserter) { @@ -672,22 +824,22 @@ public class TestRepository<R extends Repository> { } update(Constants.HEAD, result); return pool.parseCommit(result); - } else { - throw new IOException("Merge conflict"); } + throw new IOException("Merge conflict"); } /** * Update the dumb client server info files. * * @throws Exception + * if an error occurred */ public void updateServerInfo() throws Exception { if (db instanceof FileRepository) { final FileRepository fr = (FileRepository) db; - RefWriter rw = new RefWriter(fr.getAllRefs().values()) { + RefWriter rw = new RefWriter(fr.getRefDatabase().getRefs()) { @Override - protected void writeFile(final String name, final byte[] bin) + protected void writeFile(String name, byte[] bin) throws IOException { File path = new File(fr.getDirectory(), name); TestRepository.this.writeFile(path, bin); @@ -697,7 +849,7 @@ public class TestRepository<R extends Repository> { rw.writeInfoRefs(); final StringBuilder w = new StringBuilder(); - for (PackFile p : fr.getObjectDatabase().getPacks()) { + for (Pack p : fr.getObjectDatabase().getPacks()) { w.append("P "); w.append(p.getPackFile().getName()); w.append('\n'); @@ -711,14 +863,16 @@ public class TestRepository<R extends Repository> { * Ensure the body of the given object has been parsed. * * @param <T> - * type of object, e.g. {@link RevTag} or {@link RevCommit}. + * type of object, e.g. {@link org.eclipse.jgit.revwalk.RevTag} + * or {@link org.eclipse.jgit.revwalk.RevCommit}. * @param object * reference to the (possibly unparsed) object to force body * parsing of. * @return {@code object} * @throws Exception + * if an error occurred */ - public <T extends RevObject> T parseBody(final T object) throws Exception { + public <T extends RevObject> T parseBody(T object) throws Exception { pool.parseBody(object); return object; } @@ -752,6 +906,7 @@ public class TestRepository<R extends Repository> { * the object to tag * @return the tagged object * @throws Exception + * if an error occurred */ public ObjectId lightweightTag(String name, ObjectId obj) throws Exception { if (!name.startsWith(Constants.R_TAGS)) @@ -769,8 +924,11 @@ public class TestRepository<R extends Repository> { * the tips to start checking from; if not supplied the refs of * the repository are used instead. * @throws MissingObjectException + * if object is missing * @throws IncorrectObjectTypeException + * if object has unexpected type * @throws IOException + * if an IO error occurred */ public void fsck(RevObject... tips) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -779,7 +937,7 @@ public class TestRepository<R extends Repository> { for (RevObject o : tips) ow.markStart(ow.parseAny(o)); } else { - for (Ref r : db.getAllRefs().values()) + for (Ref r : db.getRefDatabase().getRefs()) ow.markStart(ow.parseAny(r.getObjectId())); } @@ -790,7 +948,7 @@ public class TestRepository<R extends Repository> { break; final byte[] bin = db.open(o, o.getType()).getCachedBytes(); - oc.checkCommit(bin); + oc.checkCommit(o, bin); assertHash(o, bin); } @@ -800,7 +958,7 @@ public class TestRepository<R extends Repository> { break; final byte[] bin = db.open(o, o.getType()).getCachedBytes(); - oc.check(o.getType(), bin); + oc.check(o, o.getType(), bin); assertHash(o, bin); } } @@ -823,34 +981,44 @@ public class TestRepository<R extends Repository> { * not removed. * * @throws Exception + * if an error occurred */ public void packAndPrune() throws Exception { if (db.getObjectDatabase() instanceof ObjectDirectory) { ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase(); NullProgressMonitor m = NullProgressMonitor.INSTANCE; - final File pack, idx; + PackFile pack; try (PackWriter pw = new PackWriter(db)) { - Set<ObjectId> all = new HashSet<ObjectId>(); - for (Ref r : db.getAllRefs().values()) + Set<ObjectId> all = new HashSet<>(); + for (Ref r : db.getRefDatabase().getRefs()) all.add(r.getObjectId()); - pw.preparePack(m, all, Collections.<ObjectId> emptySet()); + pw.preparePack(m, all, PackWriter.NONE); - final ObjectId name = pw.computeName(); - - pack = nameFor(odb, name, ".pack"); + pack = new PackFile(odb.getPackDirectory(), pw.computeName(), + PackExt.PACK); try (OutputStream out = - new SafeBufferedOutputStream(new FileOutputStream(pack))) { + new BufferedOutputStream(new FileOutputStream(pack))) { pw.writePack(m, m, out); } pack.setReadOnly(); - idx = nameFor(odb, name, ".idx"); + PackFile idx = pack.create(PackExt.INDEX); try (OutputStream out = - new SafeBufferedOutputStream(new FileOutputStream(idx))) { + new BufferedOutputStream(new FileOutputStream(idx))) { pw.writeIndex(out); } idx.setReadOnly(); + + PackConfig pc = new PackConfig(db); + if (pc.getMinBytesForObjSizeIndex() >= 0) { + PackFile oidx = pack.create(PackExt.OBJECT_SIZE_INDEX); + try (OutputStream out = new BufferedOutputStream( + new FileOutputStream(oidx))) { + pw.writeObjectSizeIndex(out); + } + oidx.setReadOnly(); + } } odb.openPack(pack); @@ -859,27 +1027,40 @@ public class TestRepository<R extends Repository> { } } - private static void prunePacked(ObjectDirectory odb) throws IOException { - for (PackFile p : odb.getPacks()) { - for (MutableEntry e : p) - FileUtils.delete(odb.fileFor(e.toObjectId())); + /** + * Closes the underlying {@link Repository} object and any other internal + * resources. + * <p> + * {@link AutoCloseable} resources that may escape this object, such as + * those returned by the {@link #git} and {@link #getRevWalk()} methods are + * not closed. + */ + @Override + public void close() { + try { + inserter.close(); + } finally { + db.close(); } } - private static File nameFor(ObjectDirectory odb, ObjectId name, String t) { - File packdir = new File(odb.getDirectory(), "pack"); - return new File(packdir, "pack-" + name.name() + t); + private static void prunePacked(ObjectDirectory odb) throws IOException { + for (Pack p : odb.getPacks()) { + for (MutableEntry e : p) + FileUtils.delete(odb.fileFor(e.toObjectId()), + FileUtils.SKIP_MISSING); + } } - private void writeFile(final File p, final byte[] bin) throws IOException, + private void writeFile(File p, byte[] bin) throws IOException, ObjectWritingException { - final LockFile lck = new LockFile(p, db.getFS()); + final LockFile lck = new LockFile(p); if (!lck.lock()) throw new ObjectWritingException("Can't write " + p); try { lck.write(bin); } catch (IOException ioe) { - throw new ObjectWritingException("Can't write " + p); + throw new ObjectWritingException("Can't write " + p, ioe); } if (!lck.commit()) throw new ObjectWritingException("Can't write " + p); @@ -889,11 +1070,13 @@ public class TestRepository<R extends Repository> { public class BranchBuilder { private final String ref; - BranchBuilder(final String ref) { + BranchBuilder(String ref) { this.ref = ref; } /** + * Create commit builder + * * @return construct a new commit builder that updates this branch. If * the branch already exists, the commit builder will have its * first parent as the current commit and its tree will be @@ -912,6 +1095,7 @@ public class TestRepository<R extends Repository> { * the commit to update to. * @return {@code to}. * @throws Exception + * if an error occurred */ public RevCommit update(CommitBuilder to) throws Exception { return update(to.create()); @@ -924,10 +1108,22 @@ public class TestRepository<R extends Repository> { * the commit to update to. * @return {@code to}. * @throws Exception + * if an error occurred */ public RevCommit update(RevCommit to) throws Exception { return TestRepository.this.update(ref, to); } + + /** + * Delete this branch. + * + * @throws Exception + * if an error occurred + * @since 4.4 + */ + public void delete() throws Exception { + TestRepository.this.delete(ref); + } } /** Helper to generate a commit. */ @@ -938,7 +1134,7 @@ public class TestRepository<R extends Repository> { private ObjectId topLevelTree; - private final List<RevCommit> parents = new ArrayList<RevCommit>(2); + private final List<RevCommit> parents = new ArrayList<>(2); private int tick = 1; @@ -960,7 +1156,7 @@ public class TestRepository<R extends Repository> { CommitBuilder(BranchBuilder b) throws Exception { branch = b; - Ref ref = db.getRef(branch.ref); + Ref ref = db.exactRef(branch.ref); if (ref != null && ref.getObjectId() != null) parent(pool.parseCommit(ref.getObjectId())); } @@ -976,7 +1172,19 @@ public class TestRepository<R extends Repository> { parents.add(prior.create()); } - public CommitBuilder parent(RevCommit p) throws Exception { + /** + * Set parent commit + * + * @param p + * parent commit, can be {@code null} + * @return this commit builder + * @throws Exception + * if an error occurred + */ + public CommitBuilder parent(@Nullable RevCommit p) throws Exception { + if (p == null) { + return this; + } if (parents.isEmpty()) { DirCacheBuilder b = tree.builder(); parseBody(p); @@ -988,30 +1196,74 @@ public class TestRepository<R extends Repository> { return this; } + /** + * Get parent commits + * + * @return parent commits + */ public List<RevCommit> parents() { return Collections.unmodifiableList(parents); } + /** + * Remove parent commits + * + * @return this commit builder + */ public CommitBuilder noParents() { parents.clear(); return this; } + /** + * Remove files + * + * @return this commit builder + */ public CommitBuilder noFiles() { tree.clear(); return this; } + /** + * Set top level tree + * + * @param treeId + * the top level tree + * @return this commit builder + */ public CommitBuilder setTopLevelTree(ObjectId treeId) { topLevelTree = treeId; return this; } + /** + * Add file with given content + * + * @param path + * path of the file + * @param content + * the file content + * @return this commit builder + * @throws Exception + * if an error occurred + */ public CommitBuilder add(String path, String content) throws Exception { return add(path, blob(content)); } - public CommitBuilder add(String path, final RevBlob id) + /** + * Add file with given path and blob + * + * @param path + * path of the file + * @param id + * blob for this file + * @return this commit builder + * @throws Exception + * if an error occurred + */ + public CommitBuilder add(String path, RevBlob id) throws Exception { return edit(new PathEdit(path) { @Override @@ -1022,6 +1274,13 @@ public class TestRepository<R extends Repository> { }); } + /** + * Edit the index + * + * @param edit + * the index record update + * @return this commit builder + */ public CommitBuilder edit(PathEdit edit) { DirCacheEditor e = tree.editor(); e.add(edit); @@ -1029,6 +1288,13 @@ public class TestRepository<R extends Repository> { return this; } + /** + * Remove a file + * + * @param path + * path of the file + * @return this commit builder + */ public CommitBuilder rm(String path) { DirCacheEditor e = tree.editor(); e.add(new DeletePath(path)); @@ -1037,49 +1303,111 @@ public class TestRepository<R extends Repository> { return this; } + /** + * Set commit message + * + * @param m + * the message + * @return this commit builder + */ public CommitBuilder message(String m) { message = m; return this; } + /** + * Get the commit message + * + * @return the commit message + */ public String message() { return message; } + /** + * Tick the clock + * + * @param secs + * number of seconds + * @return this commit builder + */ public CommitBuilder tick(int secs) { tick = secs; return this; } + /** + * Set author and committer identity + * + * @param ident + * identity to set + * @return this commit builder + */ public CommitBuilder ident(PersonIdent ident) { author = ident; committer = ident; return this; } + /** + * Set the author identity + * + * @param a + * the author's identity + * @return this commit builder + */ public CommitBuilder author(PersonIdent a) { author = a; return this; } + /** + * Get the author identity + * + * @return the author identity + */ public PersonIdent author() { return author; } + /** + * Set the committer identity + * + * @param c + * the committer identity + * @return this commit builder + */ public CommitBuilder committer(PersonIdent c) { committer = c; return this; } + /** + * Get the committer identity + * + * @return the committer identity + */ public PersonIdent committer() { return committer; } + /** + * Insert changeId + * + * @return this commit builder + */ public CommitBuilder insertChangeId() { changeId = ""; return this; } + /** + * Insert given changeId + * + * @param c + * changeId + * @return this commit builder + */ public CommitBuilder insertChangeId(String c) { // Validate, but store as a string so we can use "" as a sentinel. ObjectId.fromString(c); @@ -1087,6 +1415,13 @@ public class TestRepository<R extends Repository> { return this; } + /** + * Create the commit + * + * @return the new commit + * @throws Exception + * if creation failed + */ public RevCommit create() throws Exception { if (self == null) { TestRepository.this.tick(tick); @@ -1100,7 +1435,7 @@ public class TestRepository<R extends Repository> { c.setAuthor(author); if (committer != null) { if (updateCommitterTime) - committer = new PersonIdent(committer, new Date(now)); + committer = new PersonIdent(committer, getInstant()); c.setCommitter(committer); } @@ -1115,7 +1450,7 @@ public class TestRepository<R extends Repository> { commitId = ins.insert(c); ins.flush(); } - self = pool.lookupCommit(commitId); + self = pool.parseCommit(commitId); if (branch != null) branch.update(self); @@ -1123,8 +1458,7 @@ public class TestRepository<R extends Repository> { return self; } - private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) - throws IOException { + private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) { if (changeId == null) return; int idx = ChangeIdUtil.indexOfChangeId(message, "\n"); @@ -1136,7 +1470,7 @@ public class TestRepository<R extends Repository> { firstParentId = parents.get(0); ObjectId cid; - if (changeId.equals("")) + if (changeId.isEmpty()) cid = ChangeIdUtil.computeChangeId(c.getTreeId(), firstParentId, c.getAuthor(), c.getCommitter(), message); else @@ -1148,6 +1482,13 @@ public class TestRepository<R extends Repository> { + cid.getName() + "\n"); //$NON-NLS-1$ } + /** + * Create child commit builder + * + * @return child commit builder + * @throws Exception + * if an error occurred + */ public CommitBuilder child() throws Exception { return new CommitBuilder(this); } diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRng.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRng.java index 93facc3777..e7fe48845b 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRng.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRng.java @@ -1,49 +1,18 @@ /* - * Copyright (C) 2008-2010, Google Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2008-2010, Google Inc. and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.junit; -/** Toy RNG to ensure we get predictable numbers during unit tests. */ +/** + * Toy RNG to ensure we get predictable numbers during unit tests. + */ public class TestRng { private int next; @@ -53,7 +22,7 @@ public class TestRng { * @param seed * seed to bootstrap, usually this is the test method name. */ - public TestRng(final String seed) { + public TestRng(String seed) { next = 0; for (int i = 0; i < seed.length(); i++) next = next * 11 + seed.charAt(i); @@ -66,7 +35,7 @@ public class TestRng { * number of random bytes to produce. * @return array of {@code cnt} randomly generated bytes. */ - public byte[] nextBytes(final int cnt) { + public byte[] nextBytes(int cnt) { final byte[] r = new byte[cnt]; for (int i = 0; i < cnt; i++) r[i] = (byte) nextInt(); @@ -74,6 +43,8 @@ public class TestRng { } /** + * Next int + * * @return the next random integer. */ public int nextInt() { diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/MonotonicFakeClock.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/MonotonicFakeClock.java new file mode 100644 index 0000000000..31db6a2c7b --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/MonotonicFakeClock.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.junit.time; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jgit.util.time.MonotonicClock; +import org.eclipse.jgit.util.time.ProposedTimestamp; + +/** + * Fake {@link org.eclipse.jgit.util.time.MonotonicClock} for testing code that + * uses Clock. + * + * @since 4.6 + */ +public class MonotonicFakeClock implements MonotonicClock { + private long now = TimeUnit.SECONDS.toMicros(42); + + /** + * Advance the time returned by future calls to {@link #propose()}. + * + * @param add + * amount of time to add; must be {@code > 0}. + * @param unit + * unit of {@code add}. + */ + public void tick(long add, TimeUnit unit) { + if (add <= 0) { + throw new IllegalArgumentException(); + } + now += unit.toMillis(add); + } + + @Override + public ProposedTimestamp propose() { + long t = now++; + return new ProposedTimestamp() { + @Override + public long read(TimeUnit unit) { + return unit.convert(t, TimeUnit.MILLISECONDS); + } + + @Override + public void blockUntil(Duration maxWait) { + // Nothing to do, since fake time does not go backwards. + } + }; + } +} diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java new file mode 100644 index 0000000000..f93ac57f13 --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/TimeUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.junit.time; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; + +import org.eclipse.jgit.util.FS; + +/** + * Utility methods for handling timestamps + * + * @since 5.1.9 + */ +public class TimeUtil { + /** + * Set the lastModified time of a given file by adding a given offset to the + * current lastModified time + * + * @param path + * path of a file to set last modified + * @param offsetMillis + * offset in milliseconds, if negative the new lastModified time + * is offset before the original lastModified time, otherwise + * after the original time + * @return the new lastModified time + */ + public static Instant setLastModifiedWithOffset(Path path, + long offsetMillis) { + Instant mTime = FS.DETECTED.lastModifiedInstant(path) + .plusMillis(offsetMillis); + try { + Files.setLastModifiedTime(path, FileTime.from(mTime)); + return mTime; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Set the lastModified time of file a to the one from file b + * + * @param a + * file to set lastModified time + * @param b + * file to read lastModified time from + */ + public static void setLastModifiedOf(Path a, Path b) { + Instant mTime = FS.DETECTED.lastModifiedInstant(b); + try { + Files.setLastModifiedTime(a, FileTime.from(mTime)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +} |