diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2019-08-08 12:30:47 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2019-08-08 13:04:38 +0200 |
commit | 718555518ded4fbcafe0c1203f0035d3251884e6 (patch) | |
tree | 3e49529cd0ebc21fcea581efe0fa15fdcdc8b81c /org.eclipse.jgit.test | |
parent | fcfe1299c37ae8308e9d930201823b3ac340dd2b (diff) | |
parent | 0046b2a8fefcbfecc10a7b198a075eb2775d3e7a (diff) | |
download | jgit-718555518ded4fbcafe0c1203f0035d3251884e6.tar.gz jgit-718555518ded4fbcafe0c1203f0035d3251884e6.zip |
Merge branch 'stable-5.2' into stable-5.3
* stable-5.2:
Fix OpenSshConfigTest#config
FileSnapshot: fix bug with timestamp thresholding
In LockFile#waitForStatChange wait in units of file time resolution
Cache FileStoreAttributeCache per directory
Fix FileSnapshot#save(long) and FileSnapshot#save(Instant)
Persist minimal racy threshold and allow manual configuration
Measure minimum racy interval to auto-configure FileSnapshot
Reuse FileUtils to recursively delete files created by tests
Fix FileAttributeCache.toString()
Add test for racy git detection in FileSnapshot
Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times
Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit
Add missing javadoc in org.eclipse.jgit.junit
Enhance RepeatRule to report number of failures at the end
Fix FileSnapshotTests for filesystem with high timestamp resolution
Retry deleting test files in FileBasedConfigTest
Measure filesystem timestamp resolution already in test setup
Refactor FileSnapshotTest to use NIO APIs
Measure stored timestamp resolution instead of time to touch file
Handle CancellationException in FileStoreAttributeCache
Fix FileSnapshot#saveNoConfig
Use Instant for smudge time in DirCache and DirCacheEntry
Use Instant instead of milliseconds for filesystem timestamp handling
Workaround SecurityException in FS#getFsTimestampResolution
Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution
FS: ignore AccessDeniedException when measuring timestamp resolution
Add debug trace for FileSnapshot
Use FileChannel.open to touch file and set mtime to now
Persist filesystem timestamp resolution and allow manual configuration
Increase bazel timeout for long running tests
Bazel: Fix lint warning flagged by buildifier
Update bazlets to latest version
Bazel: Add missing dependencies for ArchiveCommandTest
Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file
Add support for nanoseconds and microseconds for Config#getTimeUnit
Optionally measure filesystem timestamp resolution asynchronously
Delete unused FileTreeIteratorWithTimeControl
FileSnapshot#equals: consider UNKNOWN_SIZE
Timeout measuring file timestamp resolution after 2 seconds
Fix RacyGitTests#testRacyGitDetection
Change RacyGitTests to create a racy git situation in a stable way
Deprecate Constants.CHARACTER_ENCODING in favor of
StandardCharsets.UTF_8
Fix non-deterministic hash of archives created by ArchiveCommand
Update Maven plugins ecj, plexus, error-prone
Update Maven plugins and cleanup Maven warnings
Make inner classes static where possible
Fix API problem filters
Change-Id: I238adfd3080a5fed9d64c3c757297da6ea893918
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Diffstat (limited to 'org.eclipse.jgit.test')
31 files changed, 954 insertions, 433 deletions
diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD index 95fb79b22e..2d2f760c26 100644 --- a/org.eclipse.jgit.test/BUILD +++ b/org.eclipse.jgit.test/BUILD @@ -23,7 +23,6 @@ HELPERS = glob( "revwalk/RevWalkTestCase.java", "transport/ObjectIdMatcher.java", "transport/SpiTransport.java", - "treewalk/FileTreeIteratorWithTimeControl.java", "treewalk/filter/AlwaysCloneTreeFilter.java", "test/resources/SampleDataRepositoryTestCase.java", "util/CPUTimeStopWatch.java", @@ -37,7 +36,7 @@ DATA = [ RESOURCES = glob(["resources/**"]) -tests(glob( +tests(tests = glob( ["tst/**/*.java"], exclude = HELPERS + DATA, )) diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 14eef1351f..5533304913 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -11,10 +11,17 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", com.jcraft.jsch;version="[0.1.54,0.2.0)", net.bytebuddy.dynamic.loading;version="[1.7.0,2.0.0)", + org.apache.commons.compress.archivers;version="[1.15.0,2.0)", + org.apache.commons.compress.archivers.tar;version="[1.15.0,2.0)", + org.apache.commons.compress.archivers.zip;version="[1.15.0,2.0)", + org.apache.commons.compress.compressors.bzip2;version="[1.15.0,2.0)", + org.apache.commons.compress.compressors.gzip;version="[1.15.0,2.0)", + org.apache.commons.compress.compressors.xz;version="[1.15.0,2.0)", org.bouncycastle.util.encoders;version="[1.60.0,2.0.0)", org.eclipse.jgit.annotations;version="[5.3.3,5.4.0)", org.eclipse.jgit.api;version="[5.3.3,5.4.0)", org.eclipse.jgit.api.errors;version="[5.3.3,5.4.0)", + org.eclipse.jgit.archive;version="[5.3.3,5.4.0)", org.eclipse.jgit.attributes;version="[5.3.3,5.4.0)", org.eclipse.jgit.awtui;version="[5.3.3,5.4.0)", org.eclipse.jgit.blame;version="[5.3.3,5.4.0)", @@ -38,6 +45,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.internal.transport.parser;version="[5.3.3,5.4.0)", org.eclipse.jgit.junit;version="[5.3.3,5.4.0)", org.eclipse.jgit.junit.ssh;version="[5.3.3,5.4.0)", + org.eclipse.jgit.junit.time;version="[5.3.3,5.4.0)", org.eclipse.jgit.lfs;version="[5.3.3,5.4.0)", org.eclipse.jgit.lib;version="[5.3.3,5.4.0)", org.eclipse.jgit.merge;version="[5.3.3,5.4.0)", @@ -70,7 +78,8 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.mockito.junit;version="[2.13.0,3.0.0)", org.mockito.stubbing;version="[2.13.0,3.0.0)", org.objenesis;version="[2.6.0,3.0.0)", - org.slf4j;version="[1.7.0,2.0.0)" + org.slf4j;version="[1.7.0,2.0.0)", + org.tukaani.xz;version="[1.6.0,2.0)" Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)", org.hamcrest.library;bundle-version="[1.1.0,2.0.0)" Export-Package: org.eclipse.jgit.transport.ssh;version="5.3.3";x-friends:="org.eclipse.jgit.ssh.apache.test" diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml index 21c06428f2..9f66d7ed73 100644 --- a/org.eclipse.jgit.test/pom.xml +++ b/org.eclipse.jgit.test/pom.xml @@ -133,6 +133,12 @@ <artifactId>org.eclipse.jgit.pgm</artifactId> <version>${project.version}</version> </dependency> + + <dependency> + <groupId>org.tukaani</groupId> + <artifactId>xz</artifactId> + <optional>true</optional> + </dependency> </dependencies> <profiles> @@ -144,6 +150,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> + <version>${maven-surefire-plugin-version}</version> <configuration> <argLine>@{argLine} -Djgit.test.long=true</argLine> </configuration> diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl index dc2102964f..d2f6d705b6 100644 --- a/org.eclipse.jgit.test/tests.bzl +++ b/org.eclipse.jgit.test/tests.bzl @@ -7,15 +7,16 @@ def tests(tests): for src in tests: name = src[len("tst/"):len(src) - len(".java")].replace("/", "_") labels = [] + timeout = "moderate" if name.startswith("org_eclipse_jgit_"): - l = name[len("org.eclipse.jgit_"):] - if l.startswith("internal_storage_"): - l = l[len("internal.storage_"):] - i = l.find("_") - if i > 0: - labels.append(l[:i]) + package = name[len("org.eclipse.jgit_"):] + if package.startswith("internal_storage_"): + package = package[len("internal.storage_"):] + index = package.find("_") + if index > 0: + labels.append(package[:index]) else: - labels.append(i) + labels.append(index) if "lib" not in labels: labels.append("lib") @@ -53,9 +54,17 @@ def tests(tests): additional_deps = [ "//lib:mockito", ] + if src.endswith("ArchiveCommandTest.java"): + additional_deps = [ + "//lib:commons-compress", + "//lib:xz", + "//org.eclipse.jgit.archive:jgit-archive", + ] heap_size = "-Xmx256m" if src.endswith("HugeCommitMessageTest.java"): heap_size = "-Xmx512m" + if src.endswith("EolRepositoryTest.java") or src.endswith("GcCommitSelectionTest.java"): + timeout = "long" junit_tests( name = name, @@ -73,4 +82,5 @@ def tests(tests): ], flaky = flaky, jvm_flags = [heap_size, "-Dfile.encoding=UTF-8"], + timeout = timeout, ) diff --git a/org.eclipse.jgit.test/tst-rsrc/log4j.properties b/org.eclipse.jgit.test/tst-rsrc/log4j.properties index 14620ffae4..ee1ac35158 100644 --- a/org.eclipse.jgit.test/tst-rsrc/log4j.properties +++ b/org.eclipse.jgit.test/tst-rsrc/log4j.properties @@ -7,3 +7,8 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n +#log4j.appender.fileLogger.bufferedIO = true +#log4j.appender.fileLogger.bufferSize = 4096 + +#log4j.logger.org.eclipse.jgit.util.FS = DEBUG +#log4j.logger.org.eclipse.jgit.internal.storage.file.FileSnapshot = DEBUG diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index 3fee51a885..b28e26a639 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -1267,7 +1267,7 @@ public class AddCommandTest extends RepositoryTestCase { DirCacheEntry entry = new DirCacheEntry(path, stage); entry.setObjectId(id); entry.setFileMode(FileMode.REGULAR_FILE); - entry.setLastModified(file.lastModified()); + entry.setLastModified(FS.DETECTED.lastModifiedInstant(file)); entry.setLength((int) file.length()); builder.add(entry); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java index 1c41018161..0f2e6b8ac6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java @@ -47,20 +47,44 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.beans.Statement; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.UnmergedPathsException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.archive.ArchiveFormats; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.StringUtils; import org.junit.After; import org.junit.Before; @@ -68,9 +92,14 @@ import org.junit.Test; public class ArchiveCommandTest extends RepositoryTestCase { + // archives store timestamp with 1 second resolution + private static final int WAIT = 2000; private static final String UNEXPECTED_ARCHIVE_SIZE = "Unexpected archive size"; private static final String UNEXPECTED_FILE_CONTENTS = "Unexpected file contents"; private static final String UNEXPECTED_TREE_CONTENTS = "Unexpected tree contents"; + private static final String UNEXPECTED_LAST_MODIFIED = + "Unexpected lastModified mocked by MockSystemReader, truncated to 1 second"; + private static final String UNEXPECTED_DIFFERENT_HASH = "Unexpected different hash"; private MockFormat format = null; @@ -78,25 +107,20 @@ public class ArchiveCommandTest extends RepositoryTestCase { public void setup() { format = new MockFormat(); ArchiveCommand.registerFormat(format.SUFFIXES.get(0), format); + ArchiveFormats.registerAll(); } @Override @After public void tearDown() { ArchiveCommand.unregisterFormat(format.SUFFIXES.get(0)); + ArchiveFormats.unregisterAll(); } @Test public void archiveHeadAllFiles() throws IOException, GitAPIException { try (Git git = new Git(db)) { - writeTrashFile("file_1.txt", "content_1_1"); - git.add().addFilepattern("file_1.txt").call(); - git.commit().setMessage("create file").call(); - - writeTrashFile("file_1.txt", "content_1_2"); - writeTrashFile("file_2.txt", "content_2_2"); - git.add().addFilepattern(".").call(); - git.commit().setMessage("updated file").call(); + createTestContent(git); git.archive().setOutputStream(new MockOutputStream()) .setFormat(format.SUFFIXES.get(0)) @@ -191,6 +215,157 @@ public class ArchiveCommandTest extends RepositoryTestCase { } } + @Test + public void archiveHeadAllFilesTarTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "tar"; + File archive = new File(getTemporaryDirectory(), + "archive." + format); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + ArchiveInputStream o = new TarArchiveInputStream(bi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesTgzTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "tgz"; + File archive = new File(getTemporaryDirectory(), + "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + InputStream gzi = new GzipCompressorInputStream(bi); + ArchiveInputStream o = new TarArchiveInputStream(gzi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesTbz2Timestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "tbz2"; + File archive = new File(getTemporaryDirectory(), + "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + InputStream gzi = new BZip2CompressorInputStream(bi); + ArchiveInputStream o = new TarArchiveInputStream(gzi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesTxzTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "txz"; + File archive = new File(getTemporaryDirectory(), "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + InputStream gzi = new XZCompressorInputStream(bi); + ArchiveInputStream o = new TarArchiveInputStream(gzi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + @Test + public void archiveHeadAllFilesZipTimestamps() throws Exception { + try (Git git = new Git(db)) { + createTestContent(git); + String fmt = "zip"; + File archive = new File(getTemporaryDirectory(), "archive." + fmt); + archive(git, archive, fmt); + ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive)); + + try (InputStream fi = Files.newInputStream(archive.toPath()); + InputStream bi = new BufferedInputStream(fi); + ArchiveInputStream o = new ZipArchiveInputStream(bi)) { + assertEntries(o); + } + + Thread.sleep(WAIT); + archive(git, archive, fmt); + assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1, + ObjectId.fromRaw(IO.readFully(archive))); + } + } + + private void createTestContent(Git git) throws IOException, GitAPIException, + NoFilepatternException, NoHeadException, NoMessageException, + UnmergedPathsException, ConcurrentRefUpdateException, + WrongRepositoryStateException, AbortedByHookException { + writeTrashFile("file_1.txt", "content_1_1"); + git.add().addFilepattern("file_1.txt").call(); + git.commit().setMessage("create file").call(); + + writeTrashFile("file_1.txt", "content_1_2"); + writeTrashFile("file_2.txt", "content_2_2"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("updated file").call(); + } + + private static void archive(Git git, File archive, String fmt) + throws GitAPIException, + FileNotFoundException, AmbiguousObjectException, + IncorrectObjectTypeException, IOException { + git.archive().setOutputStream(new FileOutputStream(archive)) + .setFormat(fmt) + .setTree(git.getRepository().resolve("HEAD")).call(); + } + + private static void assertEntries(ArchiveInputStream o) throws IOException { + ArchiveEntry e; + int n = 0; + while ((e = o.getNextEntry()) != null) { + n++; + assertEquals(UNEXPECTED_LAST_MODIFIED, + (1250379778668L / 1000L) * 1000L, + e.getLastModifiedDate().getTime()); + } + assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, n); + } + private static class MockFormat implements ArchiveCommand.Format<MockOutputStream> { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java index 98a8adcc58..1f3527f479 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java @@ -43,6 +43,7 @@ */ package org.eclipse.jgit.api; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.lib.Constants.MASTER; import static org.eclipse.jgit.lib.Constants.R_HEADS; import static org.hamcrest.CoreMatchers.is; @@ -60,6 +61,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import org.eclipse.jgit.api.CheckoutResult.Status; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; @@ -75,6 +79,7 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.lfs.BuiltinLFS; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -87,6 +92,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.Before; @@ -376,14 +382,14 @@ public class CheckoutCommandTest extends RepositoryTestCase { File file = new File(db.getWorkTree(), "Test.txt"); long size = file.length(); - long mTime = file.lastModified() - 5000L; - assertTrue(file.setLastModified(mTime)); + Instant mTime = TimeUtil.setLastModifiedWithOffset(file.toPath(), + -5000L); DirCache cache = DirCache.lock(db.getIndexFile(), db.getFS()); DirCacheEntry entry = cache.getEntry("Test.txt"); assertNotNull(entry); entry.setLength(0); - entry.setLastModified(0); + entry.setLastModified(EPOCH); cache.write(); assertTrue(cache.commit()); @@ -391,10 +397,12 @@ public class CheckoutCommandTest extends RepositoryTestCase { entry = cache.getEntry("Test.txt"); assertNotNull(entry); assertEquals(0, entry.getLength()); - assertEquals(0, entry.getLastModified()); + assertEquals(EPOCH, entry.getLastModifiedInstant()); - db.getIndexFile().setLastModified( - db.getIndexFile().lastModified() - 5000); + Files.setLastModifiedTime(db.getIndexFile().toPath(), + FileTime.from(FS.DETECTED + .lastModifiedInstant(db.getIndexFile()) + .minusMillis(5000L))); assertNotNull(git.checkout().setName("test").call()); @@ -402,7 +410,7 @@ public class CheckoutCommandTest extends RepositoryTestCase { entry = cache.getEntry("Test.txt"); assertNotNull(entry); assertEquals(size, entry.getLength()); - assertEquals(mTime, entry.getLastModified()); + assertEquals(mTime, entry.getLastModifiedInstant()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java index cd96f41a42..e4b7ed7ba7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java @@ -65,6 +65,7 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -319,11 +320,11 @@ public class CommitCommandTest extends RepositoryTestCase { public void commitUpdatesSmudgedEntries() throws Exception { try (Git git = new Git(db)) { File file1 = writeTrashFile("file1.txt", "content1"); - assertTrue(file1.setLastModified(file1.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L); File file2 = writeTrashFile("file2.txt", "content2"); - assertTrue(file2.setLastModified(file2.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L); File file3 = writeTrashFile("file3.txt", "content3"); - assertTrue(file3.setLastModified(file3.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file3.toPath(), -5000L); assertNotNull(git.add().addFilepattern("file1.txt") .addFilepattern("file2.txt").addFilepattern("file3.txt").call()); @@ -354,11 +355,12 @@ public class CommitCommandTest extends RepositoryTestCase { assertEquals(0, cache.getEntry("file2.txt").getLength()); assertEquals(0, cache.getEntry("file3.txt").getLength()); - long indexTime = db.getIndexFile().lastModified(); - db.getIndexFile().setLastModified(indexTime - 5000); + TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(), + -5000L); write(file1, "content4"); - assertTrue(file1.setLastModified(file1.lastModified() + 2500)); + + TimeUtil.setLastModifiedWithOffset(file1.toPath(), 2500L); assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt") .call()); @@ -376,9 +378,9 @@ public class CommitCommandTest extends RepositoryTestCase { public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception { try (Git git = new Git(db)) { File file1 = writeTrashFile("file1.txt", "content1"); - assertTrue(file1.setLastModified(file1.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L); File file2 = writeTrashFile("file2.txt", "content2"); - assertTrue(file2.setLastModified(file2.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L); assertNotNull(git.add().addFilepattern("file1.txt") .addFilepattern("file2.txt").call()); @@ -407,11 +409,11 @@ public class CommitCommandTest extends RepositoryTestCase { assertEquals(0, cache.getEntry("file1.txt").getLength()); assertEquals(0, cache.getEntry("file2.txt").getLength()); - long indexTime = db.getIndexFile().lastModified(); - db.getIndexFile().setLastModified(indexTime - 5000); + TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(), + -5000L); write(file1, "content5"); - assertTrue(file1.setLastModified(file1.lastModified() + 1000)); + TimeUtil.setLastModifiedWithOffset(file1.toPath(), 1000L); assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt") .call()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java index 43c3a8cf92..3a93839e8c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DiffCommandTest.java @@ -44,7 +44,6 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; @@ -55,6 +54,7 @@ import java.util.List; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.revwalk.RevWalk; @@ -230,7 +230,7 @@ public class DiffCommandTest extends RepositoryTestCase { @Test public void testNoOutputStreamSet() throws Exception { File file = writeTrashFile("test.txt", "a"); - assertTrue(file.setLastModified(file.lastModified() - 5000)); + TimeUtil.setLastModifiedWithOffset(file.toPath(), -5000L); try (Git git = new Git(db)) { git.add().addFilepattern(".").call(); write(file, "b"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java index 9b12011aab..dd7230bdbf 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java @@ -42,6 +42,7 @@ */ package org.eclipse.jgit.api; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.api.ResetCommand.ResetType.HARD; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -52,6 +53,9 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.GitAPIException; @@ -252,13 +256,13 @@ public class ResetCommandTest extends RepositoryTestCase { public void testMixedResetRetainsSizeAndModifiedTime() throws Exception { git = new Git(db); - writeTrashFile("a.txt", "a").setLastModified( - System.currentTimeMillis() - 60 * 1000); + Files.setLastModifiedTime(writeTrashFile("a.txt", "a").toPath(), + FileTime.from(Instant.now().minusSeconds(60))); assertNotNull(git.add().addFilepattern("a.txt").call()); assertNotNull(git.commit().setMessage("a commit").call()); - writeTrashFile("b.txt", "b").setLastModified( - System.currentTimeMillis() - 60 * 1000); + Files.setLastModifiedTime(writeTrashFile("b.txt", "b").toPath(), + FileTime.from(Instant.now().minusSeconds(60))); assertNotNull(git.add().addFilepattern("b.txt").call()); RevCommit commit2 = git.commit().setMessage("b commit").call(); assertNotNull(commit2); @@ -268,12 +272,12 @@ public class ResetCommandTest extends RepositoryTestCase { DirCacheEntry aEntry = cache.getEntry("a.txt"); assertNotNull(aEntry); assertTrue(aEntry.getLength() > 0); - assertTrue(aEntry.getLastModified() > 0); + assertTrue(aEntry.getLastModifiedInstant().compareTo(EPOCH) > 0); DirCacheEntry bEntry = cache.getEntry("b.txt"); assertNotNull(bEntry); assertTrue(bEntry.getLength() > 0); - assertTrue(bEntry.getLastModified() > 0); + assertTrue(bEntry.getLastModifiedInstant().compareTo(EPOCH) > 0); assertSameAsHead(git.reset().setMode(ResetType.MIXED) .setRef(commit2.getName()).call()); @@ -282,13 +286,17 @@ public class ResetCommandTest extends RepositoryTestCase { DirCacheEntry mixedAEntry = cache.getEntry("a.txt"); assertNotNull(mixedAEntry); - assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified()); - assertEquals(aEntry.getLastModified(), mixedAEntry.getLastModified()); + assertEquals(aEntry.getLastModifiedInstant(), + mixedAEntry.getLastModifiedInstant()); + assertEquals(aEntry.getLastModifiedInstant(), + mixedAEntry.getLastModifiedInstant()); DirCacheEntry mixedBEntry = cache.getEntry("b.txt"); assertNotNull(mixedBEntry); - assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified()); - assertEquals(bEntry.getLastModified(), mixedBEntry.getLastModified()); + assertEquals(bEntry.getLastModifiedInstant(), + mixedBEntry.getLastModifiedInstant()); + assertEquals(bEntry.getLastModifiedInstant(), + mixedBEntry.getLastModifiedInstant()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java index b6291bfca4..50753ae1bd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java @@ -53,6 +53,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; +import java.time.Instant; import org.eclipse.jgit.events.IndexChangedEvent; import org.eclipse.jgit.events.IndexChangedListener; @@ -99,7 +100,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { public void testBuildOneFile_FinishWriteCommit() throws Exception { final String path = "a-file-path"; final FileMode mode = FileMode.REGULAR_FILE; - final long lastModified = 1218123387057L; + final Instant lastModified = Instant.ofEpochMilli(1218123387057L); final int length = 1342; final DirCacheEntry entOrig; { @@ -117,7 +118,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); b.add(entOrig); @@ -139,7 +140,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); } @@ -149,7 +150,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { public void testBuildOneFile_Commit() throws Exception { final String path = "a-file-path"; final FileMode mode = FileMode.REGULAR_FILE; - final long lastModified = 1218123387057L; + final Instant lastModified = Instant.ofEpochMilli(1218123387057L); final int length = 1342; final DirCacheEntry entOrig; { @@ -167,7 +168,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); b.add(entOrig); @@ -187,7 +188,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { assertEquals(ObjectId.zeroId(), entOrig.getObjectId()); assertEquals(mode.getBits(), entOrig.getRawMode()); assertEquals(0, entOrig.getStage()); - assertEquals(lastModified, entOrig.getLastModified()); + assertEquals(lastModified, entOrig.getLastModifiedInstant()); assertEquals(length, entOrig.getLength()); assertFalse(entOrig.isAssumeValid()); } @@ -204,7 +205,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { final String path = "a-file-path"; final FileMode mode = FileMode.REGULAR_FILE; // "old" date in 2008 - final long lastModified = 1218123387057L; + final Instant lastModified = Instant.ofEpochMilli(1218123387057L); final int length = 1342; DirCacheEntry entOrig; boolean receivedEvent = false; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java index 86e2852872..475819dbb7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.dircache; +import static java.time.Instant.EPOCH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; @@ -188,7 +189,7 @@ public class DirCacheEntryTest { e.setAssumeValid(false); e.setCreationTime(2L); e.setFileMode(FileMode.EXECUTABLE_FILE); - e.setLastModified(3L); + e.setLastModified(EPOCH.plusMillis(3L)); e.setLength(100L); e.setObjectId(ObjectId .fromString("0123456789012345678901234567890123456789")); @@ -199,7 +200,7 @@ public class DirCacheEntryTest { f.setAssumeValid(true); f.setCreationTime(10L); f.setFileMode(FileMode.SYMLINK); - f.setLastModified(20L); + f.setLastModified(EPOCH.plusMillis(20L)); f.setLength(100000000L); f.setObjectId(ObjectId .fromString("1234567890123456789012345678901234567890")); @@ -212,7 +213,7 @@ public class DirCacheEntryTest { ObjectId.fromString("1234567890123456789012345678901234567890"), e.getObjectId()); assertEquals(FileMode.SYMLINK, e.getFileMode()); - assertEquals(20L, e.getLastModified()); + assertEquals(EPOCH.plusMillis(20L), e.getLastModifiedInstant()); assertEquals(100000000L, e.getLength()); if (keepStage) assertEquals(DirCacheEntry.STAGE_2, e.getStage()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java index 643daa5c95..6bec056737 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java @@ -56,6 +56,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.time.Instant; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -71,6 +72,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.WindowCacheConfig; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.junit.After; import org.junit.Before; @@ -235,7 +237,8 @@ public class ConcurrentRepackTest extends RepositoryTestCase { private static void write(File[] files, PackWriter pw) throws IOException { - final long begin = files[0].getParentFile().lastModified(); + final Instant begin = FS.DETECTED + .lastModifiedInstant(files[0].getParentFile()); NullProgressMonitor m = NullProgressMonitor.INSTANCE; try (OutputStream out = new BufferedOutputStream( @@ -252,7 +255,8 @@ public class ConcurrentRepackTest extends RepositoryTestCase { } private static void delete(File[] list) throws IOException { - final long begin = list[0].getParentFile().lastModified(); + final Instant begin = FS.DETECTED + .lastModifiedInstant(list[0].getParentFile()); for (File f : list) { FileUtils.delete(f); assertFalse(f + " was removed", f.exists()); @@ -260,14 +264,14 @@ public class ConcurrentRepackTest extends RepositoryTestCase { touch(begin, list[0].getParentFile()); } - private static void touch(long begin, File dir) { - while (begin >= dir.lastModified()) { + private static void touch(Instant begin, File dir) throws IOException { + while (begin.compareTo(FS.DETECTED.lastModifiedInstant(dir)) >= 0) { try { Thread.sleep(25); } catch (InterruptedException ie) { // } - dir.setLastModified(System.currentTimeMillis()); + FS.DETECTED.setLastModified(dir.toPath(), Instant.now()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java index 5ebdeb6e8f..6fa35d64b0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java @@ -42,49 +42,68 @@ */ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.junit.JGitTestUtil.read; +import static org.eclipse.jgit.junit.JGitTestUtil.write; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.FileStoreAttributes; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.Stats; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class FileSnapshotTest { + private static final Logger LOG = LoggerFactory + .getLogger(FileSnapshotTest.class); - private List<File> files = new ArrayList<>(); + private Path trash; - private File trash; + private FileStoreAttributes fsAttrCache; @Before public void setUp() throws Exception { - trash = File.createTempFile("tmp_", ""); - trash.delete(); - assertTrue("mkdir " + trash, trash.mkdir()); + trash = Files.createTempDirectory("tmp_"); + // measure timer resolution before the test to avoid time critical tests + // are affected by time needed for measurement + fsAttrCache = FS + .getFileStoreAttributes(trash.getParent()); } @Before @After public void tearDown() throws Exception { - FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); + FileUtils.delete(trash.toFile(), + FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } - private static void waitNextSec(File f) { - long initialLastModified = f.lastModified(); + private static void waitNextTick(Path f) throws IOException { + Instant initialLastModified = FS.DETECTED.lastModifiedInstant(f); do { - f.setLastModified(System.currentTimeMillis()); - } while (f.lastModified() == initialLastModified); + FS.DETECTED.setLastModified(f, Instant.now()); + } while (FS.DETECTED.lastModifiedInstant(f) + .equals(initialLastModified)); } /** @@ -94,12 +113,12 @@ public class FileSnapshotTest { */ @Test public void testActuallyIsModifiedTrivial() throws Exception { - File f1 = createFile("simple"); - waitNextSec(f1); - FileSnapshot save = FileSnapshot.save(f1); + Path f1 = createFile("simple"); + waitNextTick(f1); + FileSnapshot save = FileSnapshot.save(f1.toFile()); append(f1, (byte) 'x'); - waitNextSec(f1); - assertTrue(save.isModified(f1)); + waitNextTick(f1); + assertTrue(save.isModified(f1.toFile())); } /** @@ -112,11 +131,17 @@ public class FileSnapshotTest { */ @Test public void testNewFileWithWait() throws Exception { - File f1 = createFile("newfile"); - waitNextSec(f1); - FileSnapshot save = FileSnapshot.save(f1); - Thread.sleep(1500); - assertTrue(save.isModified(f1)); + // if filesystem timestamp resolution is high the snapshot won't be + // racily clean + Assume.assumeTrue( + fsAttrCache.getFsTimestampResolution() + .compareTo(Duration.ofMillis(10)) > 0); + Path f1 = createFile("newfile"); + waitNextTick(f1); + FileSnapshot save = FileSnapshot.save(f1.toFile()); + TimeUnit.NANOSECONDS.sleep( + fsAttrCache.getFsTimestampResolution().dividedBy(2).toNanos()); + assertTrue(save.isModified(f1.toFile())); } /** @@ -126,9 +151,33 @@ public class FileSnapshotTest { */ @Test public void testNewFileNoWait() throws Exception { - File f1 = createFile("newfile"); - FileSnapshot save = FileSnapshot.save(f1); - assertTrue(save.isModified(f1)); + // if filesystem timestamp resolution is smaller than time needed to + // create a file and FileSnapshot the snapshot won't be racily clean + Assume.assumeTrue(fsAttrCache.getFsTimestampResolution() + .compareTo(Duration.ofMillis(10)) > 0); + for (int i = 0; i < 50; i++) { + Instant start = Instant.now(); + Path f1 = createFile("newfile"); + FileSnapshot save = FileSnapshot.save(f1.toFile()); + Duration res = FS.getFileStoreAttributes(f1) + .getFsTimestampResolution(); + Instant end = Instant.now(); + if (Duration.between(start, end) + .compareTo(res.multipliedBy(2)) > 0) { + // This test is racy: under load, there may be a delay between createFile() and + // FileSnapshot.save(). This can stretch the time between the read TS and FS + // creation TS to the point that it exceeds the FS granularity, and we + // conclude it cannot be racily clean, and therefore must be really clean. + // + // This should be relatively uncommon. + continue; + } + // The file wasn't really modified, but it looks just like a "maybe racily clean" + // file. + assertTrue(save.isModified(f1.toFile())); + return; + } + fail("too much load for this test"); } /** @@ -142,19 +191,19 @@ public class FileSnapshotTest { @Test public void testSimulatePackfileReplacement() throws Exception { Assume.assumeFalse(SystemReader.getInstance().isWindows()); - File f1 = createFile("file"); // inode y - File f2 = createFile("fool"); // Guarantees new inode x + Path f1 = createFile("file"); // inode y + Path f2 = createFile("fool"); // Guarantees new inode x // wait on f2 since this method resets lastModified of the file // and leaves lastModified of f1 untouched - waitNextSec(f2); - waitNextSec(f2); - FileTime timestamp = Files.getLastModifiedTime(f1.toPath()); - FileSnapshot save = FileSnapshot.save(f1); - Files.move(f2.toPath(), f1.toPath(), // Now "file" is inode x + waitNextTick(f2); + waitNextTick(f2); + FileTime timestamp = Files.getLastModifiedTime(f1); + FileSnapshot save = FileSnapshot.save(f1.toFile()); + Files.move(f2, f1, // Now "file" is inode x StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - Files.setLastModifiedTime(f1.toPath(), timestamp); - assertTrue(save.isModified(f1)); + Files.setLastModifiedTime(f1, timestamp); + assertTrue(save.isModified(f1.toFile())); assertTrue("unexpected change of fileKey", save.wasFileKeyChanged()); assertFalse("unexpected size change", save.wasSizeChanged()); assertFalse("unexpected lastModified change", @@ -171,24 +220,83 @@ public class FileSnapshotTest { */ @Test public void testFileSizeChanged() throws Exception { - File f = createFile("file"); - FileTime timestamp = Files.getLastModifiedTime(f.toPath()); - FileSnapshot save = FileSnapshot.save(f); + Path f = createFile("file"); + FileTime timestamp = Files.getLastModifiedTime(f); + FileSnapshot save = FileSnapshot.save(f.toFile()); append(f, (byte) 'x'); - Files.setLastModifiedTime(f.toPath(), timestamp); - assertTrue(save.isModified(f)); + Files.setLastModifiedTime(f, timestamp); + assertTrue(save.isModified(f.toFile())); assertTrue(save.wasSizeChanged()); } - private File createFile(String string) throws IOException { - trash.mkdirs(); - File f = File.createTempFile(string, "tdat", trash); - files.add(f); - return f; + @Test + public void fileSnapshotEquals() throws Exception { + // 0 sized FileSnapshot. + FileSnapshot fs1 = FileSnapshot.MISSING_FILE; + // UNKNOWN_SIZE FileSnapshot. + FileSnapshot fs2 = FileSnapshot.save(fs1.lastModifiedInstant()); + + assertTrue(fs1.equals(fs2)); + assertTrue(fs2.equals(fs1)); + } + + @SuppressWarnings("boxing") + @Test + public void detectFileModified() throws IOException { + int failures = 0; + long racyNanos = 0; + final int COUNT = 10000; + ArrayList<Long> deltas = new ArrayList<>(); + File f = createFile("test").toFile(); + for (int i = 0; i < COUNT; i++) { + write(f, "a"); + FileSnapshot snapshot = FileSnapshot.save(f); + assertEquals("file should contain 'a'", "a", read(f)); + write(f, "b"); + if (!snapshot.isModified(f)) { + deltas.add(snapshot.lastDelta()); + racyNanos = snapshot.lastRacyThreshold(); + failures++; + } + assertEquals("file should contain 'b'", "b", read(f)); + } + if (failures > 0) { + Stats stats = new Stats(); + LOG.debug( + "delta [ns] since modification FileSnapshot failed to detect"); + for (Long d : deltas) { + stats.add(d); + LOG.debug(String.format("%,d", d)); + } + LOG.error( + "count, failures, eff. racy threshold [ns], delta min [ns]," + + " delta max [ns], delta avg [ns]," + + " delta stddev [ns]"); + LOG.error(String.format( + "%,d, %,d, %,d, %,.0f, %,.0f, %,.0f, %,.0f", COUNT, + failures, racyNanos, stats.min(), stats.max(), + stats.avg(), stats.stddev())); + } + assertTrue( + String.format( + "FileSnapshot: failures to detect file modifications" + + " %d out of %d\n" + + "timestamp resolution %d µs" + + " min racy threshold %d µs" + , failures, COUNT, + fsAttrCache.getFsTimestampResolution().toNanos() / 1000, + fsAttrCache.getMinimalRacyInterval().toNanos() / 1000), + failures == 0); + } + + private Path createFile(String string) throws IOException { + Files.createDirectories(trash); + return Files.createTempFile(trash, string, "tdat"); } - private static void append(File f, byte b) throws IOException { - try (FileOutputStream os = new FileOutputStream(f, true)) { + private static void append(Path f, byte b) throws IOException { + try (OutputStream os = Files.newOutputStream(f, + StandardOpenOption.APPEND)) { os.write(b); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java index d16998db55..eaa245b4eb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java @@ -147,9 +147,10 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase { return tip; } - protected long lastModified(AnyObjectId objectId) throws IOException { - return repo.getFS().lastModified( - repo.getObjectDatabase().fileFor(objectId)); + protected long lastModified(AnyObjectId objectId) { + return repo.getFS() + .lastModifiedInstant(repo.getObjectDatabase().fileFor(objectId)) + .toEpochMilli(); } protected static void fsTick() throws InterruptedException, IOException { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java index a3a302d81a..9d47c7e3bc 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java @@ -66,6 +66,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; import org.junit.Assume; import org.junit.Rule; import org.junit.Test; @@ -158,13 +159,14 @@ public class ObjectDirectoryTest extends RepositoryTestCase { // To deal with racy-git situations JGit's Filesnapshot class will // report a file/folder potentially dirty if - // cachedLastReadTime-cachedLastModificationTime < 2500ms. This - // causes JGit to always rescan a file after modification. But: - // this was true only if the difference between current system time - // and cachedLastModification time was less than 2500ms. If the - // modification is more than 2500ms ago we may have reported a - // file/folder to be clean although it has not been rescanned. A - // Bug. To show the bug we sleep for more than 2500ms + // cachedLastReadTime-cachedLastModificationTime < filesystem + // timestamp resolution. This causes JGit to always rescan a file + // after modification. But: this was true only if the difference + // between current system time and cachedLastModification time was + // less than 2500ms. If the modification is more than 2500ms ago we + // may have reported a file/folder to be clean although it has not + // been rescanned. A bug. To show the bug we sleep for more than + // 2500ms Thread.sleep(2600); File[] ret = packsFolder.listFiles(new FilenameFilter() { @@ -174,7 +176,9 @@ public class ObjectDirectoryTest extends RepositoryTestCase { } }); assertTrue(ret != null && ret.length == 1); - Assume.assumeTrue(tmpFile.lastModified() == ret[0].lastModified()); + FS fs = db.getFS(); + Assume.assumeTrue(fs.lastModifiedInstant(tmpFile) + .equals(fs.lastModifiedInstant(ret[0]))); // all objects are in a new packfile but we will not detect it assertFalse(receivingDB.getObjectDatabase().has(unknownID)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java index a1433e9fe5..d5bc61a692 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java @@ -59,6 +59,7 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; //import java.nio.file.attribute.BasicFileAttributes; import java.text.ParseException; +import java.time.Instant; import java.util.Collection; import java.util.Iterator; import java.util.Random; @@ -80,6 +81,7 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.util.FS; import org.junit.Test; public class PackFileSnapshotTest extends RepositoryTestCase { @@ -188,7 +190,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { AnyObjectId chk1 = pf.getPackChecksum(); String name = pf.getPackName(); Long length = Long.valueOf(pf.getPackFile().length()); - long m1 = packFilePath.toFile().lastModified(); + FS fs = db.getFS(); + Instant m1 = fs.lastModifiedInstant(packFilePath); // Wait for a filesystem timer tick to enhance probability the rest of // this test is done before the filesystem timer ticks again. @@ -198,15 +201,15 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // content and checksum are different since compression level differs AnyObjectId chk2 = repackAndCheck(6, name, length, chk1) .getPackChecksum(); - long m2 = packFilePath.toFile().lastModified(); - assumeFalse(m2 == m1); + Instant m2 = fs.lastModifiedInstant(packFilePath); + assumeFalse(m2.equals(m1)); // Repack to create packfile with same name, length. Lastmodified is // equal to the previous one because we are in the same filesystem timer // slot. Content and its checksum are different AnyObjectId chk3 = repackAndCheck(7, name, length, chk2) .getPackChecksum(); - long m3 = packFilePath.toFile().lastModified(); + Instant m3 = fs.lastModifiedInstant(packFilePath); // ask for an unknown git object to force jgit to rescan the list of // available packs. If we would ask for a known objectid then JGit would @@ -214,7 +217,7 @@ public class PackFileSnapshotTest extends RepositoryTestCase { db.getObjectDatabase().has(unknownID); assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks()) .getPackChecksum()); - assumeTrue(m3 == m2); + assumeTrue(m3.equals(m2)); } // Try repacking so fast that we get two new packs which differ only in @@ -253,7 +256,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // Repack to create third packfile AnyObjectId chk3 = repackAndCheck(7, name, length, chk2) .getPackChecksum(); - long m3 = packFilePath.toFile().lastModified(); + FS fs = db.getFS(); + Instant m3 = fs.lastModifiedInstant(packFilePath); db.getObjectDatabase().has(unknownID); assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks()) .getPackChecksum()); @@ -265,8 +269,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // Copy copy2 to packfile data to force modification of packfile without // changing the packfile's filekey. copyPack(packFileBasePath, ".copy2", ""); - long m2 = packFilePath.toFile().lastModified(); - assumeFalse(m3 == m2); + Instant m2 = fs.lastModifiedInstant(packFilePath); + assumeFalse(m3.equals(m2)); db.getObjectDatabase().has(unknownID); assertEquals(chk2, getSinglePack(db.getObjectDatabase().getPacks()) @@ -275,8 +279,8 @@ public class PackFileSnapshotTest extends RepositoryTestCase { // Copy copy2 to packfile data to force modification of packfile without // changing the packfile's filekey. copyPack(packFileBasePath, ".copy1", ""); - long m1 = packFilePath.toFile().lastModified(); - assumeTrue(m2 == m1); + Instant m1 = fs.lastModifiedInstant(packFilePath); + assumeTrue(m2.equals(m1)); db.getObjectDatabase().has(unknownID); assertEquals(chk1, getSinglePack(db.getObjectDatabase().getPacks()) .getPackChecksum()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java index 7b3684c865..2357edf6cb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java @@ -60,6 +60,7 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -72,6 +73,7 @@ import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.events.RefsChangedListener; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.Repeat; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Ref; @@ -80,6 +82,7 @@ import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.util.FS; import org.junit.Before; import org.junit.Test; @@ -671,6 +674,7 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { assertEquals(B, all.get(HEAD).getObjectId()); } + @Repeat(n = 100, abortOnFailure = false) @Test public void testFindRef_DiscoversModifiedLoose() throws IOException { Map<String, Ref> all; @@ -1370,10 +1374,8 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { private void writePackedRefs(String content) throws IOException { File pr = new File(diskRepo.getDirectory(), "packed-refs"); write(pr, content); - - final long now = System.currentTimeMillis(); - final int oneHourAgo = 3600 * 1000; - pr.setLastModified(now - oneHourAgo); + FS fs = diskRepo.getFS(); + fs.setLastModified(pr.toPath(), Instant.now().minusSeconds(3600)); } private void deleteLooseRef(String name) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java index c6138495fc..e2887d9053 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java @@ -59,6 +59,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.time.Instant; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -82,6 +83,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.junit.Rule; @@ -790,12 +792,14 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { * * @param name * the file in the repository to force a time change on. + * @throws IOException */ - private void BUG_WorkAroundRacyGitIssues(String name) { + private void BUG_WorkAroundRacyGitIssues(String name) throws IOException { File path = new File(db.getDirectory(), name); - long old = path.lastModified(); + FS fs = db.getFS(); + Instant old = fs.lastModifiedInstant(path); long set = 1250379778668L; // Sat Aug 15 20:12:58 GMT-03:30 2009 - path.setLastModified(set); - assertTrue("time changed", old != path.lastModified()); + fs.setLastModified(path.toPath(), Instant.ofEpochMilli(set)); + assertFalse("time changed", old.equals(fs.lastModifiedInstant(path))); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java index 21d8d66adf..22dc471552 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java @@ -51,8 +51,10 @@ package org.eclipse.jgit.lib; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MICROSECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.eclipse.jgit.util.FileUtils.pathToString; import static org.junit.Assert.assertArrayEquals; @@ -1224,8 +1226,18 @@ public class ConfigTest { @Test public void testTimeUnit() throws ConfigInvalidException { + assertEquals(0, parseTime("0", NANOSECONDS)); + assertEquals(2, parseTime("2ns", NANOSECONDS)); + assertEquals(200, parseTime("200 nanoseconds", NANOSECONDS)); + + assertEquals(0, parseTime("0", MICROSECONDS)); + assertEquals(2, parseTime("2us", MICROSECONDS)); + assertEquals(2, parseTime("2000 nanoseconds", MICROSECONDS)); + assertEquals(200, parseTime("200 microseconds", MICROSECONDS)); + assertEquals(0, parseTime("0", MILLISECONDS)); assertEquals(2, parseTime("2ms", MILLISECONDS)); + assertEquals(2, parseTime("2000microseconds", MILLISECONDS)); assertEquals(200, parseTime("200 milliseconds", MILLISECONDS)); assertEquals(0, parseTime("0s", SECONDS)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java index b9bbbeb9e5..a3634141c3 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexModificationTimesTest.java @@ -37,8 +37,12 @@ */ package org.eclipse.jgit.lib; +import static java.time.Instant.EPOCH; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.time.Instant; + import org.eclipse.jgit.api.Git; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -63,11 +67,11 @@ public class IndexModificationTimesTest extends RepositoryTestCase { DirCacheEntry entry = dc.getEntry(path); DirCacheEntry entry2 = dc.getEntry(path); - assertTrue("last modified shall not be zero!", - entry.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry.getLastModifiedInstant().equals(EPOCH)); - assertTrue("last modified shall not be zero!", - entry2.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry2.getLastModifiedInstant().equals(EPOCH)); writeTrashFile(path, "new content"); git.add().addFilepattern(path).call(); @@ -77,11 +81,11 @@ public class IndexModificationTimesTest extends RepositoryTestCase { entry = dc.getEntry(path); entry2 = dc.getEntry(path); - assertTrue("last modified shall not be zero!", - entry.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry.getLastModifiedInstant().equals(EPOCH)); - assertTrue("last modified shall not be zero!", - entry2.getLastModified() != 0); + assertFalse("last modified shall not be the epoch!", + entry2.getLastModifiedInstant().equals(EPOCH)); } } @@ -97,7 +101,7 @@ public class IndexModificationTimesTest extends RepositoryTestCase { DirCache dc = db.readDirCache(); DirCacheEntry entry = dc.getEntry(path); - long masterLastMod = entry.getLastModified(); + Instant masterLastMod = entry.getLastModifiedInstant(); git.checkout().setCreateBranch(true).setName("side").call(); @@ -110,7 +114,7 @@ public class IndexModificationTimesTest extends RepositoryTestCase { dc = db.readDirCache(); entry = dc.getEntry(path); - long sideLastMode = entry.getLastModified(); + Instant sideLastMod = entry.getLastModifiedInstant(); Thread.sleep(2000); @@ -120,9 +124,10 @@ public class IndexModificationTimesTest extends RepositoryTestCase { dc = db.readDirCache(); entry = dc.getEntry(path); - assertTrue("shall have equal mod time!", masterLastMod == sideLastMode); - assertTrue("shall not equal master timestamp!", - entry.getLastModified() == masterLastMod); + assertTrue("shall have equal mod time!", + masterLastMod.equals(sideLastMod)); + assertTrue("shall have equal master timestamp!", + entry.getLastModifiedInstant().equals(masterLastMod)); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java index 11100b63c0..d3e4efef53 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java @@ -42,88 +42,26 @@ */ package org.eclipse.jgit.lib; -import static java.lang.Long.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.util.TreeSet; +import java.time.Instant; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.time.TimeUtil; import org.eclipse.jgit.treewalk.FileTreeIterator; -import org.eclipse.jgit.treewalk.FileTreeIteratorWithTimeControl; -import org.eclipse.jgit.treewalk.NameConflictTreeWalk; -import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.FS; import org.junit.Test; public class RacyGitTests extends RepositoryTestCase { - @Test - public void testIterator() - throws IllegalStateException, IOException, InterruptedException { - TreeSet<Long> modTimes = new TreeSet<>(); - File lastFile = null; - for (int i = 0; i < 10; i++) { - lastFile = new File(db.getWorkTree(), "0." + i); - FileUtils.createNewFile(lastFile); - if (i == 5) - fsTick(lastFile); - } - modTimes.add(valueOf(fsTick(lastFile))); - for (int i = 0; i < 10; i++) { - lastFile = new File(db.getWorkTree(), "1." + i); - FileUtils.createNewFile(lastFile); - } - modTimes.add(valueOf(fsTick(lastFile))); - for (int i = 0; i < 10; i++) { - lastFile = new File(db.getWorkTree(), "2." + i); - FileUtils.createNewFile(lastFile); - if (i % 4 == 0) - fsTick(lastFile); - } - FileTreeIteratorWithTimeControl fileIt = new FileTreeIteratorWithTimeControl( - db, modTimes); - try (NameConflictTreeWalk tw = new NameConflictTreeWalk(db)) { - tw.addTree(fileIt); - tw.setRecursive(true); - FileTreeIterator t; - long t0 = 0; - for (int i = 0; i < 10; i++) { - assertTrue(tw.next()); - t = tw.getTree(0, FileTreeIterator.class); - if (i == 0) { - t0 = t.getEntryLastModified(); - } else { - assertEquals(t0, t.getEntryLastModified()); - } - } - long t1 = 0; - for (int i = 0; i < 10; i++) { - assertTrue(tw.next()); - t = tw.getTree(0, FileTreeIterator.class); - if (i == 0) { - t1 = t.getEntryLastModified(); - assertTrue(t1 > t0); - } else { - assertEquals(t1, t.getEntryLastModified()); - } - } - long t2 = 0; - for (int i = 0; i < 10; i++) { - assertTrue(tw.next()); - t = tw.getTree(0, FileTreeIterator.class); - if (i == 0) { - t2 = t.getEntryLastModified(); - assertTrue(t2 > t1); - } else { - assertEquals(t2, t.getEntryLastModified()); - } - } - } - } @Test public void testRacyGitDetection() throws Exception { @@ -137,10 +75,10 @@ public class RacyGitTests extends RepositoryTestCase { fsTick(db.getIndexFile()); // create two files - File a = addToWorkDir("a", "a"); - File b = addToWorkDir("b", "b"); - assertTrue(a.setLastModified(b.lastModified())); - assertTrue(b.setLastModified(b.lastModified())); + File a = writeToWorkDir("a", "a"); + File b = writeToWorkDir("b", "b"); + TimeUtil.setLastModifiedOf(a.toPath(), b.toPath()); + TimeUtil.setLastModifiedOf(b.toPath(), b.toPath()); // wait to ensure that file-modTimes and therefore index entry modTime // doesn't match the modtime of index-file after next persistance @@ -158,23 +96,36 @@ public class RacyGitTests extends RepositoryTestCase { fsTick(db.getIndexFile()); // Create a racy git situation. This is a situation that the index is - // updated and then a file is modified within a second. By changing the - // index file artificially, we create a fake racy situation. - File updatedA = addToWorkDir("a", "a2"); - assertTrue(updatedA.setLastModified(updatedA.lastModified() + 100)); + // updated and then a file is modified within the same tick of the + // filesystem timestamp resolution. By changing the index file + // artificially, we create a fake racy situation. + File updatedA = writeToWorkDir("a", "a2"); + Instant newLastModified = TimeUtil + .setLastModifiedWithOffset(updatedA.toPath(), 100L); resetIndex(new FileTreeIterator(db)); - assertTrue(db.getIndexFile() - .setLastModified(updatedA.lastModified() + 90)); + FS.DETECTED.setLastModified(db.getIndexFile().toPath(), + newLastModified); - db.readDirCache(); - // although racily clean a should not be reported as being dirty + DirCache dc = db.readDirCache(); + // check index state: although racily clean a should not be reported as + // being dirty since we forcefully reset the index to match the working + // tree assertEquals( "[a, mode:100644, time:t1, smudged, length:0, content:a2]" + "[b, mode:100644, time:t0, length:1, content:b]", indexState(SMUDGE | MOD_TIME | LENGTH | CONTENT)); + + // compare state of files in working tree with index to check that + // FileTreeIterator.isModified() works as expected + FileTreeIterator f = new FileTreeIterator(db.getWorkTree(), db.getFS(), + db.getConfig().get(WorkingTreeOptions.KEY)); + assertTrue(f.findFile("a")); + try (ObjectReader reader = db.newObjectReader()) { + assertFalse(f.isModified(dc.getEntry("a"), false, reader)); + } } - private File addToWorkDir(String path, String content) throws IOException { + private File writeToWorkDir(String path, String content) throws IOException { File f = new File(db.getWorkTree(), path); try (FileOutputStream fos = new FileOutputStream(f)) { fos.write(content.getBytes(UTF_8)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java index f6fc00ce43..2ae9c11a7d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.merge; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.EPOCH; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -54,6 +55,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.time.Instant; import java.util.Arrays; import java.util.Map; @@ -1090,13 +1092,13 @@ public class MergerTest extends RepositoryTestCase { @Theory public void checkForCorrectIndex(MergeStrategy strategy) throws Exception { File f; - long lastTs4, lastTsIndex; + Instant lastTs4, lastTsIndex; Git git = Git.wrap(db); File indexFile = db.getIndexFile(); // Create initial content and remember when the last file was written. f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); - lastTs4 = FS.DETECTED.lastModified(f); + lastTs4 = FS.DETECTED.lastModifiedInstant(f); // add all files, commit and check this doesn't update any working tree // files and that the index is in a new file system timer tick. Make @@ -1109,8 +1111,9 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index"); assertEquals("Commit should not touch working tree file 4", lastTs4, - FS.DETECTED.lastModified(new File(db.getWorkTree(), "4"))); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + FS.DETECTED + .lastModifiedInstant(new File(db.getWorkTree(), "4"))); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // Do modifications on the master branch. Then add and commit. This // should touch only "0", "2 and "3" @@ -1124,7 +1127,7 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" + lastTsIndex, "<0", "2", "3", "<.git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // Checkout a side branch. This should touch only "0", "2 and "3" fsTick(indexFile); @@ -1133,7 +1136,7 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" + lastTsIndex, "<0", "2", "3", ".git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // This checkout may have populated worktree and index so fast that we // may have smudged entries now. Check that we have the right content @@ -1146,13 +1149,13 @@ public class MergerTest extends RepositoryTestCase { indexState(CONTENT)); fsTick(indexFile); f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); - lastTs4 = FS.DETECTED.lastModified(f); + lastTs4 = FS.DETECTED.lastModifiedInstant(f); fsTick(f); git.add().addFilepattern(".").call(); checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3", "4", "<.git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // Do modifications on the side branch. Touch only "1", "2 and "3" fsTick(indexFile); @@ -1163,7 +1166,7 @@ public class MergerTest extends RepositoryTestCase { checkConsistentLastModified("0", "1", "2", "3", "4"); checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*" + lastTsIndex, "<1", "2", "3", "<.git/index"); - lastTsIndex = FS.DETECTED.lastModified(indexFile); + lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); // merge master and side. Should only touch "0," "2" and "3" fsTick(indexFile); @@ -1330,9 +1333,10 @@ public class MergerTest extends RepositoryTestCase { assertEquals( "IndexEntry with path " + path - + " has lastmodified with is different from the worktree file", - FS.DETECTED.lastModified(new File(workTree, path)), dc.getEntry(path) - .getLastModified()); + + " has lastmodified which is different from the worktree file", + FS.DETECTED.lastModifiedInstant(new File(workTree, path)), + dc.getEntry(path) + .getLastModifiedInstant()); } // Assert that modification timestamps of working tree files are as @@ -1341,21 +1345,22 @@ public class MergerTest extends RepositoryTestCase { // then this file must be younger then file i. A path "*<modtime>" // represents a file with a modification time of <modtime> // E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt - private void checkModificationTimeStampOrder(String... pathes) - throws IOException { - long lastMod = Long.MIN_VALUE; + private void checkModificationTimeStampOrder(String... pathes) { + Instant lastMod = EPOCH; for (String p : pathes) { boolean strong = p.startsWith("<"); boolean fixed = p.charAt(strong ? 1 : 0) == '*'; p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0)); - long curMod = fixed ? Long.valueOf(p).longValue() - : FS.DETECTED.lastModified(new File(db.getWorkTree(), p)); - if (strong) + Instant curMod = fixed ? Instant.parse(p) + : FS.DETECTED + .lastModifiedInstant(new File(db.getWorkTree(), p)); + if (strong) { assertTrue("path " + p + " is not younger than predecesssor", - curMod > lastMod); - else + curMod.compareTo(lastMod) > 0); + } else { assertTrue("path " + p + " is older than predecesssor", - curMod >= lastMod); + curMod.compareTo(lastMod) >= 0); + } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java index b401d2bea2..43648206f0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java @@ -46,12 +46,13 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.util.FileUtils.pathToString; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.StringTokenizer; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -85,42 +86,44 @@ public class FileBasedConfigTest { private static final String CONTENT3 = "[" + USER + "]\n\t" + NAME + " = " + ALICE + "\n" + "[" + USER + "]\n\t" + EMAIL + " = " + ALICE_EMAIL; - private File trash; + private Path trash; @Before public void setUp() throws Exception { - trash = File.createTempFile("tmp_", ""); - trash.delete(); - assertTrue("mkdir " + trash, trash.mkdir()); + trash = Files.createTempDirectory("tmp_"); + FS.getFileStoreAttributes(trash.getParent()); } @After public void tearDown() throws Exception { - FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); + FileUtils.delete(trash.toFile(), + FileUtils.RECURSIVE | FileUtils.SKIP_MISSING | FileUtils.RETRY); } @Test public void testSystemEncoding() throws IOException, ConfigInvalidException { - final File file = createFile(CONTENT1.getBytes(UTF_8)); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(CONTENT1.getBytes(UTF_8)); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); config.setString(USER, null, NAME, BOB); config.save(); - assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file)); + assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file.toFile())); } @Test public void testUTF8withoutBOM() throws IOException, ConfigInvalidException { - final File file = createFile(CONTENT1.getBytes(UTF_8)); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(CONTENT1.getBytes(UTF_8)); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); config.setString(USER, null, NAME, BOB); config.save(); - assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file)); + assertArrayEquals(CONTENT2.getBytes(UTF_8), IO.readFully(file.toFile())); } @Test @@ -131,8 +134,9 @@ public class FileBasedConfigTest { bos1.write(0xBF); bos1.write(CONTENT1.getBytes(UTF_8)); - final File file = createFile(bos1.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos1.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); @@ -144,7 +148,7 @@ public class FileBasedConfigTest { bos2.write(0xBB); bos2.write(0xBF); bos2.write(CONTENT2.getBytes(UTF_8)); - assertArrayEquals(bos2.toByteArray(), IO.readFully(file)); + assertArrayEquals(bos2.toByteArray(), IO.readFully(file.toFile())); } @Test @@ -153,8 +157,9 @@ public class FileBasedConfigTest { bos1.write(" \n\t".getBytes(UTF_8)); bos1.write(CONTENT1.getBytes(UTF_8)); - final File file = createFile(bos1.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos1.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); @@ -164,19 +169,20 @@ public class FileBasedConfigTest { final ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); bos2.write(" \n\t".getBytes(UTF_8)); bos2.write(CONTENT2.getBytes(UTF_8)); - assertArrayEquals(bos2.toByteArray(), IO.readFully(file)); + assertArrayEquals(bos2.toByteArray(), IO.readFully(file.toFile())); } @Test public void testIncludeAbsolute() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8)); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8)); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(pathToString(includedFile).getBytes(UTF_8)); + bos.write(pathToString(includedFile.toFile()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -184,13 +190,14 @@ public class FileBasedConfigTest { @Test public void testIncludeRelativeDot() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("./" + includedFile.getName()).getBytes(UTF_8)); + bos.write(("./" + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray(), "dir1"); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray(), "dir1"); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -198,14 +205,15 @@ public class FileBasedConfigTest { @Test public void testIncludeRelativeDotDot() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("../" + includedFile.getParentFile().getName() + "/" - + includedFile.getName()).getBytes(UTF_8)); + bos.write(("../" + includedFile.getParent().getFileName() + "/" + + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray(), "dir2"); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray(), "dir2"); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -213,13 +221,14 @@ public class FileBasedConfigTest { @Test public void testIncludeRelativeDotDotNotFound() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8)); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8)); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("../" + includedFile.getName()).getBytes(UTF_8)); + bos.write(("../" + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray()); - final FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(bos.toByteArray()); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.load(); assertEquals(null, config.getString(USER, null, NAME)); } @@ -227,16 +236,16 @@ public class FileBasedConfigTest { @Test public void testIncludeWithTilde() throws IOException, ConfigInvalidException { - final File includedFile = createFile(CONTENT1.getBytes(UTF_8), "home"); + final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "home"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write("[include]\npath=".getBytes(UTF_8)); - bos.write(("~/" + includedFile.getName()).getBytes(UTF_8)); + bos.write(("~/" + includedFile.getFileName()).getBytes(UTF_8)); - final File file = createFile(bos.toByteArray(), "repo"); + final Path file = createFile(bos.toByteArray(), "repo"); final FS fs = FS.DETECTED.newInstance(); - fs.setUserHome(includedFile.getParentFile()); + fs.setUserHome(includedFile.getParent().toFile()); - final FileBasedConfig config = new FileBasedConfig(file, fs); + final FileBasedConfig config = new FileBasedConfig(file.toFile(), fs); config.load(); assertEquals(ALICE, config.getString(USER, null, NAME)); } @@ -246,13 +255,14 @@ public class FileBasedConfigTest { throws IOException, ConfigInvalidException { // use a content with multiple sections and multiple key/value pairs // because code for first line works different than for subsequent lines - final File includedFile = createFile(CONTENT3.getBytes(UTF_8), "dir1"); + final Path includedFile = createFile(CONTENT3.getBytes(UTF_8), "dir1"); - final File file = createFile(new byte[0], "dir2"); - FileBasedConfig config = new FileBasedConfig(file, FS.DETECTED); + final Path file = createFile(new byte[0], "dir2"); + FileBasedConfig config = new FileBasedConfig(file.toFile(), + FS.DETECTED); config.setString("include", null, "path", - ("../" + includedFile.getParentFile().getName() + "/" - + includedFile.getName())); + ("../" + includedFile.getParent().getFileName() + "/" + + includedFile.getFileName())); // just by setting the include.path, it won't be included assertEquals(null, config.getString(USER, null, NAME)); @@ -267,7 +277,7 @@ public class FileBasedConfigTest { assertEquals(2, new StringTokenizer(expectedText, "\n", false).countTokens()); - config = new FileBasedConfig(file, FS.DETECTED); + config = new FileBasedConfig(file.toFile(), FS.DETECTED); config.load(); String actualText = config.toText(); @@ -285,16 +295,17 @@ public class FileBasedConfigTest { assertEquals(ALICE_EMAIL, config.getString(USER, null, EMAIL)); } - private File createFile(byte[] content) throws IOException { + private Path createFile(byte[] content) throws IOException { return createFile(content, null); } - private File createFile(byte[] content, String subdir) throws IOException { - File dir = subdir != null ? new File(trash, subdir) : trash; - dir.mkdirs(); + private Path createFile(byte[] content, String subdir) throws IOException { + Path dir = subdir != null ? trash.resolve(subdir) : trash; + Files.createDirectories(dir); - File f = File.createTempFile(getClass().getName(), null, dir); - try (FileOutputStream os = new FileOutputStream(f, true)) { + Path f = Files.createTempFile(dir, getClass().getName(), null); + try (OutputStream os = Files.newOutputStream(f, + StandardOpenOption.APPEND)) { os.write(content); } return f; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index 1a22e10f4c..2e5027f7ec 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java @@ -56,10 +56,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.Before; @@ -91,13 +94,19 @@ public class OpenSshConfigTest extends RepositoryTestCase { } private void config(String data) throws IOException { - long lastMtime = configFile.lastModified(); + FS fs = FS.DETECTED; + long resolution = FS.getFileStoreAttributes(configFile.toPath()) + .getFsTimestampResolution().toNanos(); + Instant lastMtime = fs.lastModifiedInstant(configFile); do { try (final OutputStreamWriter fw = new OutputStreamWriter( new FileOutputStream(configFile), UTF_8)) { fw.write(data); + TimeUnit.NANOSECONDS.sleep(resolution); + } catch (InterruptedException e) { + Thread.interrupted(); } - } while (lastMtime == configFile.lastModified()); + } while (lastMtime.equals(fs.lastModifiedInstant(configFile))); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java index 6f61912f2b..754341dfea 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java @@ -54,6 +54,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.InvalidPathException; import java.security.MessageDigest; +import java.time.Instant; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand.ResetType; @@ -88,7 +89,7 @@ import org.junit.Test; public class FileTreeIteratorTest extends RepositoryTestCase { private final String[] paths = { "a,", "a,b", "a/b", "a0b" }; - private long[] mtime; + private Instant[] mtime; @Override @Before @@ -101,11 +102,11 @@ public class FileTreeIteratorTest extends RepositoryTestCase { // This should stress the sorting code better than doing it in // the correct order. // - mtime = new long[paths.length]; + mtime = new Instant[paths.length]; for (int i = paths.length - 1; i >= 0; i--) { final String s = paths[i]; writeTrashFile(s, s); - mtime[i] = FS.DETECTED.lastModified(new File(trash, s)); + mtime[i] = db.getFS().lastModifiedInstant(new File(trash, s)); } } @@ -201,7 +202,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); assertEquals(paths[0], nameOf(top)); assertEquals(paths[0].length(), top.getEntryLength()); - assertEquals(mtime[0], top.getEntryLastModified()); + assertEquals(mtime[0], top.getEntryLastModifiedInstant()); top.next(1); assertFalse(top.first()); @@ -209,7 +210,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); assertEquals(paths[1], nameOf(top)); assertEquals(paths[1].length(), top.getEntryLength()); - assertEquals(mtime[1], top.getEntryLastModified()); + assertEquals(mtime[1], top.getEntryLastModifiedInstant()); top.next(1); assertFalse(top.first()); @@ -224,7 +225,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertFalse(sub.eof()); assertEquals(paths[2], nameOf(sub)); assertEquals(paths[2].length(), subfti.getEntryLength()); - assertEquals(mtime[2], subfti.getEntryLastModified()); + assertEquals(mtime[2], subfti.getEntryLastModifiedInstant()); sub.next(1); assertTrue(sub.eof()); @@ -235,7 +236,7 @@ public class FileTreeIteratorTest extends RepositoryTestCase { assertEquals(FileMode.REGULAR_FILE.getBits(), top.mode); assertEquals(paths[3], nameOf(top)); assertEquals(paths[3].length(), top.getEntryLength()); - assertEquals(mtime[3], top.getEntryLastModified()); + assertEquals(mtime[3], top.getEntryLastModifiedInstant()); top.next(1); assertTrue(top.eof()); @@ -347,20 +348,21 @@ public class FileTreeIteratorTest extends RepositoryTestCase { @Test public void testIsModifiedFileSmudged() throws Exception { File f = writeTrashFile("file", "content"); + FS fs = db.getFS(); try (Git git = new Git(db)) { // The idea of this test is to check the smudged handling // Hopefully fsTick will make sure our entry gets smudged fsTick(f); writeTrashFile("file", "content"); - long lastModified = f.lastModified(); + Instant lastModified = fs.lastModifiedInstant(f); git.add().addFilepattern("file").call(); writeTrashFile("file", "conten2"); - f.setLastModified(lastModified); + fs.setLastModified(f.toPath(), lastModified); // We cannot trust this to go fast enough on // a system with less than one-second lastModified // resolution, so we force the index to have the // same timestamp as the file we look at. - db.getIndexFile().setLastModified(lastModified); + fs.setLastModified(db.getIndexFile().toPath(), lastModified); } DirCacheEntry dce = db.readDirCache().getEntry("file"); FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), db diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java deleted file mode 100644 index fc79d4586d..0000000000 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorWithTimeControl.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> - * and other copyright owners as documented in the project's IP log. - * - * 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 - * - * 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. - */ -package org.eclipse.jgit.treewalk; - -import java.io.File; -import java.util.SortedSet; -import java.util.TreeSet; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.util.FS; - -/** - * A {@link FileTreeIterator} used in tests which allows to specify explicitly - * what will be returned by {@link #getEntryLastModified()}. This allows to - * write tests where certain files have to have the same modification time. - * <p> - * This iterator is configured by a list of strictly increasing long values - * t(0), t(1), ..., t(n). For each file with a modification between t(x) and - * t(x+1) [ t(x) <= time < t(x+1) ] this iterator will report t(x). For - * files with a modification time smaller t(0) a modification time of 0 is - * returned. For files with a modification time greater or equal t(n) t(n) will - * be returned. - * <p> - * This class was written especially to test racy-git problems - */ -public class FileTreeIteratorWithTimeControl extends FileTreeIterator { - private TreeSet<Long> modTimes; - - public FileTreeIteratorWithTimeControl(FileTreeIterator p, Repository repo, - TreeSet<Long> modTimes) { - super(p, repo.getWorkTree(), repo.getFS()); - this.modTimes = modTimes; - } - - public FileTreeIteratorWithTimeControl(FileTreeIterator p, File f, FS fs, - TreeSet<Long> modTimes) { - super(p, f, fs); - this.modTimes = modTimes; - } - - public FileTreeIteratorWithTimeControl(Repository repo, - TreeSet<Long> modTimes) { - super(repo); - this.modTimes = modTimes; - } - - public FileTreeIteratorWithTimeControl(File f, FS fs, - TreeSet<Long> modTimes) { - super(f, fs, new Config().get(WorkingTreeOptions.KEY)); - this.modTimes = modTimes; - } - - @Override - public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) { - return new FileTreeIteratorWithTimeControl(this, - ((FileEntry) current()).getFile(), fs, modTimes); - } - - @Override - public long getEntryLastModified() { - if (modTimes == null) - return 0; - Long cutOff = Long.valueOf(super.getEntryLastModified() + 1); - SortedSet<Long> head = modTimes.headSet(cutOff); - return head.isEmpty() ? 0 : head.last().longValue(); - } -} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java index 6dfa6ef5aa..99d435100b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.util; +import static java.time.Instant.EPOCH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -67,6 +68,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.CommandFailedException; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.RepositoryCache; import org.junit.After; import org.junit.Assume; import org.junit.Before; @@ -107,7 +109,7 @@ public class FSTest { assertTrue(fs.exists(link)); String targetName = fs.readSymLink(link); assertEquals("b", targetName); - assertTrue(fs.lastModified(link) > 0); + assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0); assertTrue(fs.exists(link)); assertFalse(fs.canExecute(link)); // The length of a symbolic link is a length of the target file path. @@ -121,8 +123,9 @@ public class FSTest { // Now create the link target FileUtils.createNewFile(target); assertTrue(fs.exists(link)); - assertTrue(fs.lastModified(link) > 0); - assertTrue(fs.lastModified(target) > fs.lastModified(link)); + assertTrue(fs.lastModifiedInstant(link).compareTo(EPOCH) > 0); + assertTrue(fs.lastModifiedInstant(target) + .compareTo(fs.lastModifiedInstant(link)) > 0); assertFalse(fs.canExecute(link)); fs.setExecute(target, true); assertFalse(fs.canExecute(link)); @@ -229,7 +232,8 @@ public class FSTest { .ofPattern("uuuu-MMM-dd HH:mm:ss.nnnnnnnnn", Locale.ENGLISH) .withZone(ZoneId.systemDefault()); Path dir = Files.createTempDirectory("probe-filesystem"); - Duration resolution = FS.getFsTimerResolution(dir); + Duration resolution = FS.getFileStoreAttributes(dir) + .getFsTimestampResolution(); long resolutionNs = resolution.toNanos(); assertTrue(resolutionNs > 0); for (int i = 0; i < 10; i++) { @@ -252,4 +256,11 @@ public class FSTest { } } } + + // bug 548682 + @Test + public void testRepoCacheRelativePathUnbornRepo() { + assertFalse(RepositoryCache.FileKey + .isGitRepository(new File("repo.git"), FS.DETECTED)); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java new file mode 100644 index 0000000000..5894f7dc5c --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/SimpleLruCacheTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> + * and other copyright owners as documented in the project's IP log. + * + * 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 + * + * 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. + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class SimpleLruCacheTest { + + private Path trash; + + private SimpleLruCache<String, String> cache; + + + @Before + public void setup() throws IOException { + trash = Files.createTempDirectory("tmp_"); + cache = new SimpleLruCache<>(100, 0.2f); + } + + @Before + @After + public void tearDown() throws Exception { + FileUtils.delete(trash.toFile(), + FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); + } + + @Test + public void testPutGet() { + cache.put("a", "A"); + cache.put("z", "Z"); + assertEquals("A", cache.get("a")); + assertEquals("Z", cache.get("z")); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooLarge() { + cache.configure(5, 1.01f); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooLarge2() { + cache.configure(5, 100); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooSmall() { + cache.configure(5, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testPurgeFactorTooSmall2() { + cache.configure(5, -100); + } + + @Test + public void testGetMissing() { + assertEquals(null, cache.get("a")); + } + + @Test + public void testPurge() { + for (int i = 0; i < 101; i++) { + cache.put("a" + i, "a" + i); + } + assertEquals(80, cache.size()); + assertNull(cache.get("a0")); + assertNull(cache.get("a20")); + assertNotNull(cache.get("a21")); + assertNotNull(cache.get("a99")); + } + + @Test + public void testConfigure() { + for (int i = 0; i < 100; i++) { + cache.put("a" + i, "a" + i); + } + assertEquals(100, cache.size()); + cache.configure(10, 0.3f); + assertEquals(7, cache.size()); + assertNull(cache.get("a0")); + assertNull(cache.get("a92")); + assertNotNull(cache.get("a93")); + assertNotNull(cache.get("a99")); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java new file mode 100644 index 0000000000..8b253828c4 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StatsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> + * and other copyright owners as documented in the project's IP log. + * + * 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 + * + * 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. + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.util.Stats; +import org.junit.Test; + +public class StatsTest { + @Test + public void testStatsTrivial() { + Stats s = new Stats(); + s.add(1); + s.add(1); + s.add(1); + assertEquals(3, s.count()); + assertEquals(1.0, s.min(), 1E-6); + assertEquals(1.0, s.max(), 1E-6); + assertEquals(1.0, s.avg(), 1E-6); + assertEquals(0.0, s.var(), 1E-6); + assertEquals(0.0, s.stddev(), 1E-6); + } + + @Test + public void testStats() { + Stats s = new Stats(); + s.add(1); + s.add(2); + s.add(3); + s.add(4); + assertEquals(4, s.count()); + assertEquals(1.0, s.min(), 1E-6); + assertEquals(4.0, s.max(), 1E-6); + assertEquals(2.5, s.avg(), 1E-6); + assertEquals(1.666667, s.var(), 1E-6); + assertEquals(1.290994, s.stddev(), 1E-6); + } + + @Test + /** + * see + * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Example + */ + public void testStatsCancellationExample1() { + Stats s = new Stats(); + s.add(1E8 + 4); + s.add(1E8 + 7); + s.add(1E8 + 13); + s.add(1E8 + 16); + assertEquals(4, s.count()); + assertEquals(1E8 + 4, s.min(), 1E-6); + assertEquals(1E8 + 16, s.max(), 1E-6); + assertEquals(1E8 + 10, s.avg(), 1E-6); + assertEquals(30, s.var(), 1E-6); + assertEquals(5.477226, s.stddev(), 1E-6); + } + + @Test + /** + * see + * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Example + */ + public void testStatsCancellationExample2() { + Stats s = new Stats(); + s.add(1E9 + 4); + s.add(1E9 + 7); + s.add(1E9 + 13); + s.add(1E9 + 16); + assertEquals(4, s.count()); + assertEquals(1E9 + 4, s.min(), 1E-6); + assertEquals(1E9 + 16, s.max(), 1E-6); + assertEquals(1E9 + 10, s.avg(), 1E-6); + assertEquals(30, s.var(), 1E-6); + assertEquals(5.477226, s.stddev(), 1E-6); + } + + @Test + public void testNoValues() { + Stats s = new Stats(); + assertTrue(Double.isNaN(s.var())); + assertTrue(Double.isNaN(s.stddev())); + assertTrue(Double.isNaN(s.avg())); + assertTrue(Double.isNaN(s.min())); + assertTrue(Double.isNaN(s.max())); + s.add(42.3); + assertTrue(Double.isNaN(s.var())); + assertTrue(Double.isNaN(s.stddev())); + assertEquals(42.3, s.avg(), 1E-6); + assertEquals(42.3, s.max(), 1E-6); + assertEquals(42.3, s.min(), 1E-6); + s.add(42.3); + assertEquals(0, s.var(), 1E-6); + assertEquals(0, s.stddev(), 1E-6); + } +} |