summaryrefslogtreecommitdiff
path: root/libs/io/src/test/java/org/the_jk/cleversync
diff options
context:
space:
mode:
Diffstat (limited to 'libs/io/src/test/java/org/the_jk/cleversync')
-rw-r--r--libs/io/src/test/java/org/the_jk/cleversync/io/BaseSingleMergeTest.kt709
-rw-r--r--libs/io/src/test/java/org/the_jk/cleversync/io/SingleMergeLocalTest.kt26
2 files changed, 735 insertions, 0 deletions
diff --git a/libs/io/src/test/java/org/the_jk/cleversync/io/BaseSingleMergeTest.kt b/libs/io/src/test/java/org/the_jk/cleversync/io/BaseSingleMergeTest.kt
new file mode 100644
index 0000000..65d870b
--- /dev/null
+++ b/libs/io/src/test/java/org/the_jk/cleversync/io/BaseSingleMergeTest.kt
@@ -0,0 +1,709 @@
+package org.the_jk.cleversync.io
+
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import kotlin.time.Duration
+
+abstract class BaseSingleMergeTest {
+ private lateinit var src: ModifiableTree
+ private lateinit var tgt: ModifiableTree
+
+ @Before
+ fun setUp() {
+ src = source()
+ tgt = target()
+ }
+
+ @Test
+ fun empty() {
+ assertThat(SingleMerge.calculate(tgt, src)).isEmpty()
+ }
+
+ @Test
+ fun oneFile() {
+ val srcFile = src.createFile("foo")
+ srcFile.write().writer().use { it.write("Hello World") }
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(Action.Copy("foo"))
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFile = tgt.openFile("foo")
+ assertThat(tgtFile?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ }
+
+ @Test
+ fun oneFileAlreadyExistsSameContent() {
+ val srcFile = src.createFile("foo")
+ srcFile.write().writer().use { it.write("Hello World") }
+ val tgtFile = tgt.createFile("foo")
+ tgtFile.write().writer().use { it.write("Hello World") }
+
+ assertThat(SingleMerge.calculate(tgt, src)).isEmpty()
+ }
+
+ @Test
+ fun oneFileAlreadyExistsDifferentContent() {
+ val srcFile = src.createFile("foo")
+ srcFile.write().writer().use { it.write("Hello World") }
+ val tgtFile = tgt.createFile("foo")
+ tgtFile.write().writer().use { it.write("Bye bye!") }
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(Action.Copy("foo", overwrite = true))
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val newTgtFile = tgt.openFile("foo")
+ assertThat(newTgtFile?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ }
+
+ @Test
+ fun oneFileAlreadyExistsSameSizeContentOlder() {
+ val tgtFile = tgt.createFile("foo")
+ tgtFile.write().writer().use { it.write("Cruel World") }
+ Thread.sleep(minModificationTime().inWholeMilliseconds)
+ val srcFile = src.createFile("foo")
+ srcFile.write().writer().use { it.write("Hello World") }
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(Action.Copy("foo", overwrite = true))
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val newTgtFile = tgt.openFile("foo")
+ assertThat(newTgtFile?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ }
+
+ @Test
+ fun oneFileAlreadyExistsSameSizeContentNewer() {
+ val srcFile = src.createFile("foo")
+ srcFile.write().writer().use { it.write("Hello World") }
+ Thread.sleep(minModificationTime().inWholeMilliseconds)
+ val tgtFile = tgt.createFile("foo")
+ tgtFile.write().writer().use { it.write("hello world") }
+
+ assertThat(SingleMerge.calculate(tgt, src)).isEmpty()
+ }
+
+ @Test
+ fun filesInDirectories() {
+ val srcFoo = src.createFile("foo")
+ srcFoo.write().writer().use { it.write("Hello World") }
+ val srcDir = src.createDirectory("bar")
+ val srcBarFoo = srcDir.createFile("foo")
+ srcBarFoo.write().writer().use { it.write("World Hello") }
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(
+ Action.Copy("foo"),
+ Action.ChangeDir("bar", listOf(Action.Copy("foo")), create = true),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFoo = tgt.openFile("foo")
+ assertThat(tgtFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ val tgtBarFoo = tgt.openFile("bar/foo")
+ assertThat(tgtBarFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("World Hello")
+ }
+
+ @Test
+ fun dirInDirInDir() {
+ val srcFoo = src.createDirectory("foo")
+ val srcFooBar = srcFoo.createDirectory("bar")
+ srcFooBar.createDirectory("fum")
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(
+ Action.ChangeDir(
+ "foo",
+ listOf(
+ Action.ChangeDir(
+ "bar",
+ listOf(
+ Action.ChangeDir(
+ "fum",
+ emptyList(),
+ create = true,
+ ),
+ ),
+ create = true,
+ ),
+ ),
+ create = true,
+ ),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFooBarFum = tgt.openDir("foo/bar/fum")
+ assertThat(tgtFooBarFum).isNotNull()
+ assertThat(tgtFooBarFum?.list()?.directories).isEmpty()
+ }
+
+ @Test
+ fun symlink() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ val srcFoo = src.createFile("foo")
+ srcFoo.write().writer().use { it.write("Hello World") }
+
+ src.createLink("link1", srcFoo)
+ src.createLink("link2", "does-not-exist")
+ src.createLink("link3", "../../escaped")
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(
+ Action.Copy("foo"),
+ Action.Link("link1", "foo"),
+ Action.Link("link2", "does-not-exist"),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFoo = tgt.openFile("foo")
+ assertThat(tgtFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ val tgtLink1 = tgt.openLink("link1")
+ when (val linkTarget = tgtLink1?.resolve()) {
+ is Link.FileTarget -> assertThat(linkTarget.file).isEqualTo(tgtFoo)
+ else -> Assert.fail()
+ }
+ val tgtLink2 = tgt.openLink("link2")
+ when (val linkTarget = tgtLink2?.resolve()) {
+ is Link.NoTarget -> assertThat(linkTarget.path).isEqualTo("does-not-exist")
+ else -> Assert.fail()
+ }
+ assertThat(tgt.openLink("link3")).isNull()
+ }
+
+ @Test
+ fun resolveSymlink() {
+ Assume.assumeTrue(sourceSupportsSymlinks())
+
+ val srcFoo = src.createFile("foo")
+ srcFoo.write().writer().use { it.write("Hello World") }
+ val srcBar = src.createDirectory("bar")
+
+ src.createLink("link1", srcFoo)
+ src.createLink("link2", srcBar)
+ src.createLink("link3", "does-not-exist")
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(
+ resolveLinks = true,
+ ))
+ assertThat(actions).containsExactly(
+ Action.Copy("foo"),
+ Action.Copy("link1"),
+ Action.ChangeDir("bar", emptyList(), create = true),
+ Action.ChangeDir("link2", emptyList(), create = true),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFoo = tgt.openFile("foo")
+ assertThat(tgtFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ val tgtLink1 = tgt.openFile("link1")
+ assertThat(tgtLink1?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ assertThat(tgt.openDir("bar")).isNotNull()
+ assertThat(tgt.openDir("link2")).isNotNull()
+ }
+
+ @Test
+ fun symlinkInDir() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ val srcFoo = src.createDirectory("foo")
+ srcFoo.createLink("link", srcFoo)
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(
+ Action.ChangeDir(
+ "foo",
+ listOf(Action.Link("link", "../foo")),
+ create = true,
+ ),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFoo = tgt.openDir("foo")
+ val tgtLink = tgtFoo?.openLink("link")
+ when (val linkTarget = tgtLink?.resolve()) {
+ is Link.DirectoryTarget -> assertThat(linkTarget.directory).isEqualTo(tgtFoo)
+ else -> Assert.fail()
+ }
+ }
+
+ @Test
+ fun symlinkUpdate() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ src.createLink("link", "does-not-exist")
+ tgt.createLink("link", "also-does-not-exist")
+
+ val actions = SingleMerge.calculate(tgt, src)
+ assertThat(actions).containsExactly(
+ Action.Link(
+ "link",
+ target = "does-not-exist",
+ overwrite = true,
+ ),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtLink = tgt.openLink("link")
+ when (val linkTarget = tgtLink?.resolve()) {
+ is Link.NoTarget -> assertThat(linkTarget.path).isEqualTo("does-not-exist")
+ else -> Assert.fail()
+ }
+ }
+
+ @Test
+ fun symlinkNoUpdate() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ src.createLink("link", "does-not-exist")
+ tgt.createLink("link", "does-not-exist")
+
+ assertThat(SingleMerge.calculate(tgt, src)).isEmpty()
+ }
+
+ @Test
+ fun deleteInTarget() {
+ val tgtFoo = tgt.createFile("foo")
+ tgtFoo.write().writer().use { it.write("aaa") }
+ val tgtBar = tgt.createDirectory("bar")
+ val tgtBarFoo = tgtBar.createFile("foo")
+ tgtBarFoo.write().writer().use { it.write("aaa") }
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(
+ deleteFilesOnlyInTarget = true,
+ ))
+ assertThat(actions).containsExactly(
+ Action.RemoveFile("foo"),
+ Action.RemoveDir("bar"),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val content = tgt.list()
+ assertThat(content.directories).isEmpty()
+ assertThat(content.files).isEmpty()
+ assertThat(content.links).isEmpty()
+ }
+
+ @Test
+ fun deleteInTargetMixed() {
+ val srcFoo = src.createFile("foo")
+ srcFoo.write().writer().use { it.write("Hello World") }
+ val srcDir = src.createDirectory("bar")
+ val srcBarFoo = srcDir.createFile("foo")
+ srcBarFoo.write().writer().use { it.write("World Hello") }
+
+ val tgtFile = tgt.createFile("file")
+ tgtFile.write().writer().use { it.write("aaa") }
+ val tgtBar = tgt.createDirectory("bar")
+ val tgtBarFile = tgtBar.createFile("file")
+ tgtBarFile.write().writer().use { it.write("aaa") }
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(
+ deleteFilesOnlyInTarget = true,
+ ))
+ assertThat(actions).containsExactly(
+ Action.Copy("foo"),
+ Action.RemoveFile("file"),
+ Action.ChangeDir(
+ "bar",
+ listOf(
+ Action.Copy("foo"),
+ Action.RemoveFile("file"),
+ ),
+ ),
+ )
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFoo = tgt.openFile("foo")
+ assertThat(tgtFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ assertThat(tgt.openFile("file")).isNull()
+ val tgtBarFoo = tgt.openFile("bar/foo")
+ assertThat(tgtBarFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("World Hello")
+ assertThat(tgt.openFile("bar/file")).isNull()
+ }
+
+ @Test
+ fun deleteInTargetLink() {
+ Assume.assumeTrue(targetSupportsSymlinks())
+
+ tgt.createLink("foo", "does-not-exist")
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(
+ deleteFilesOnlyInTarget = true,
+ ))
+ assertThat(actions).containsExactly(Action.RemoveLink("foo"))
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun mixedTypes() {
+ Assume.assumeTrue(targetSupportsSymlinks())
+
+ val srcFoo = src.createFile("foo")
+ srcFoo.write().writer().use { it.write("Hello World") }
+ src.createDirectory("bar")
+
+ val tgtBar = tgt.createFile("bar")
+ tgtBar.write().writer().use { it.write("Remove me") }
+ tgt.createLink("foo", tgtBar)
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtBar2 = tgt.openDir("bar")
+ assertThat(tgtBar2).isNotNull()
+ val tgtFoo = tgt.openFile("foo")
+ assertThat(tgtFoo?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ }
+
+ @Test
+ fun mixedTypes2() {
+ Assume.assumeTrue(targetSupportsSymlinks())
+
+ src.createDirectory("foo")
+
+ tgt.createLink("foo", "does-not-exist")
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ assertThat(tgt.openDir("foo")).isNotNull()
+ }
+
+ @Test
+ fun mixedTypes3() {
+ val srcFoo = src.createFile("foo")
+ srcFoo.write().writer().use { it.write("Hello World") }
+ src.createDirectory("bar")
+
+ val tgtBar = tgt.createFile("bar")
+ tgtBar.write().writer().use { it.write("Remove me") }
+ tgt.createDirectory("foo")
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ assertThat(tgt.openFile("foo")?.read()?.reader()?.use { it.readText() }).isEqualTo("Hello World")
+ assertThat(tgt.openDir("bar")).isNotNull()
+ }
+
+ @Test
+ fun mixedTypes4() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ src.createLink("foo", "does-not-exist")
+ src.createLink("bar", "also-does-not-exist")
+
+ tgt.createDirectory("foo")
+ tgt.createFile("bar").write().writer().use { it.write("Foo") }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+
+ val tgtFoo = tgt.openLink("foo")
+ when (val linkTarget = tgtFoo?.resolve()) {
+ is Link.NoTarget -> assertThat(linkTarget.path).isEqualTo("does-not-exist")
+ else -> Assert.fail()
+ }
+ val tgtBar = tgt.openLink("bar")
+ when (val linkTarget = tgtBar?.resolve()) {
+ is Link.NoTarget -> assertThat(linkTarget.path).isEqualTo("also-does-not-exist")
+ else -> Assert.fail()
+ }
+ }
+
+ @Test
+ fun errorSourceDirGone() {
+ val foo = src.createDirectory("foo")
+ foo.createFile("bar").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(
+ Action.ChangeDir("foo", actions = listOf(Action.Copy("bar", overwrite = false)), create = true),
+ )
+
+ assertThat(src.removeDirectory("foo")).isTrue()
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("/foo: Source directory does not exist")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorTargetDirGone() {
+ val foo = src.createDirectory("foo")
+ foo.createFile("bar").write().use { it.write(1) }
+ tgt.createDirectory("foo")
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(
+ Action.ChangeDir("foo", actions = listOf(Action.Copy("bar", overwrite = false)), create = false),
+ )
+
+ assertThat(tgt.removeDirectory("foo")).isTrue()
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("foo: Target directory does not exist")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorUnableToCreateTargetDir() {
+ val foo = src.createDirectory("foo")
+ foo.createFile("bar").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(
+ Action.ChangeDir("foo", actions = listOf(Action.Copy("bar", overwrite = false)), create = true),
+ )
+
+ val tgtFoo = tgt.createFile("foo")
+ tgtFoo.write().use { it.write(2) }
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("foo: Unable to create directory")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).containsExactly(tgtFoo)
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorSourceFileGone() {
+ src.createFile("foo").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(Action.Copy("foo", overwrite = false))
+
+ assertThat(src.removeFile("foo")).isTrue()
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("/foo: Unable to open file")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorSourceFileIsNowADirectory() {
+ src.createFile("foo").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(Action.Copy("foo", overwrite = false))
+
+ assertThat(src.removeFile("foo")).isTrue()
+ src.createDirectory("foo")
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("/foo: Unable to open file")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorTargetFileExists() {
+ src.createFile("foo").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(Action.Copy("foo", overwrite = false))
+
+ val tgtFoo = tgt.createFile("foo")
+ tgtFoo.write().use { it.write(2) }
+
+ assertThat(Modifier.apply(tgt, src, actions)).comparingElementsUsing(
+ Correspondence.from({ a: String, b: String -> a.startsWith(b) }, "startsWith"),
+ ).containsExactly("/foo: Unable to create file: ")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).containsExactly(tgtFoo)
+ assertThat(tgtFoo.read().use { it.readAllBytes() }).asList().containsExactly(2.toByte())
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorTargetFileExists2() {
+ src.createFile("foo").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(Action.Copy("foo", overwrite = false))
+
+ val tgtFoo = tgt.createDirectory("foo")
+
+ assertThat(Modifier.apply(tgt, src, actions)).comparingElementsUsing(
+ Correspondence.from({ a: String, b: String -> a.startsWith(b) }, "startsWith"),
+ ).containsExactly("/foo: Unable to create file: ")
+ assertThat(tgt.list().directories).containsExactly(tgtFoo)
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorTargetLinkExists() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ src.createLink("foo", "bar")
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(Action.Link("foo", "bar", overwrite = false))
+
+ val tgtFoo = tgt.createFile("foo")
+ tgtFoo.write().use { it.write(2) }
+
+ assertThat(Modifier.apply(tgt, src, actions)).comparingElementsUsing(
+ Correspondence.from({ a: String, b: String -> a.startsWith(b) }, "startsWith"),
+ ).containsExactly("/foo: Unable to create link: ")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).containsExactly(tgtFoo)
+ assertThat(tgtFoo.read().use { it.readAllBytes() }).asList().containsExactly(2.toByte())
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun errorTargetLinkExists2() {
+ Assume.assumeTrue(sourceSupportsSymlinks() && targetSupportsSymlinks())
+
+ src.createLink("foo", "bar")
+
+ val actions = SingleMerge.calculate(tgt, src)
+
+ assertThat(actions).containsExactly(Action.Link("foo", "bar", overwrite = false))
+
+ val tgtFoo = tgt.createDirectory("foo")
+
+ assertThat(Modifier.apply(tgt, src, actions)).comparingElementsUsing(
+ Correspondence.from({ a: String, b: String -> a.startsWith(b) }, "startsWith"),
+ ).containsExactly("/foo: Unable to create link: ")
+ assertThat(tgt.list().directories).containsExactly(tgtFoo)
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun targetDirAlreadyGone() {
+ tgt.createDirectory("foo")
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(deleteFilesOnlyInTarget = true))
+
+ assertThat(actions).containsExactly(Action.RemoveDir("foo"))
+
+ assertThat(tgt.removeDirectory("foo")).isTrue()
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+ }
+
+ @Test
+ fun errorTargetDirIsNowAFile() {
+ tgt.createDirectory("foo")
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(deleteFilesOnlyInTarget = true))
+
+ assertThat(actions).containsExactly(Action.RemoveDir("foo"))
+
+ assertThat(tgt.removeDirectory("foo")).isTrue()
+ val foo = tgt.createFile("foo")
+ foo.write().use { it.write(1) }
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("/foo: Unable to remove dir")
+ assertThat(tgt.list().directories).isEmpty()
+ assertThat(tgt.list().files).containsExactly(foo)
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun targetFileAlreadyGone() {
+ tgt.createFile("foo").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(deleteFilesOnlyInTarget = true))
+
+ assertThat(actions).containsExactly(Action.RemoveFile("foo"))
+
+ assertThat(tgt.removeFile("foo")).isTrue()
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+ }
+
+ @Test
+ fun errorTargetFileIsNowADir() {
+ tgt.createFile("foo").write().use { it.write(1) }
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(deleteFilesOnlyInTarget = true))
+
+ assertThat(actions).containsExactly(Action.RemoveFile("foo"))
+
+ assertThat(tgt.removeFile("foo")).isTrue()
+ val foo = tgt.createDirectory("foo")
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("/foo: Unable to remove file")
+ assertThat(tgt.list().directories).containsExactly(foo)
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ @Test
+ fun targetLinkAlreadyGone() {
+ Assume.assumeTrue(targetSupportsSymlinks())
+ tgt.createLink("foo", "bar")
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(deleteFilesOnlyInTarget = true))
+
+ assertThat(actions).containsExactly(Action.RemoveLink("foo"))
+
+ assertThat(tgt.removeLink("foo")).isTrue()
+
+ assertThat(Modifier.apply(tgt, src, actions)).isEmpty()
+ }
+
+ @Test
+ fun errorTargetLinkIsNowADir() {
+ Assume.assumeTrue(targetSupportsSymlinks())
+ tgt.createLink("foo", "bar")
+
+ val actions = SingleMerge.calculate(tgt, src, options = SingleMerge.Options(deleteFilesOnlyInTarget = true))
+
+ assertThat(actions).containsExactly(Action.RemoveLink("foo"))
+
+ assertThat(tgt.removeLink("foo")).isTrue()
+ val foo = tgt.createDirectory("foo")
+
+ assertThat(Modifier.apply(tgt, src, actions)).containsExactly("/foo: Unable to remove link")
+ assertThat(tgt.list().directories).containsExactly(foo)
+ assertThat(tgt.list().files).isEmpty()
+ assertThat(tgt.list().links).isEmpty()
+ }
+
+ abstract fun source(): ModifiableTree
+ abstract fun sourceSupportsSymlinks(): Boolean
+ abstract fun target(): ModifiableTree
+ abstract fun targetSupportsSymlinks(): Boolean
+
+ abstract fun minModificationTime(): Duration
+}
diff --git a/libs/io/src/test/java/org/the_jk/cleversync/io/SingleMergeLocalTest.kt b/libs/io/src/test/java/org/the_jk/cleversync/io/SingleMergeLocalTest.kt
new file mode 100644
index 0000000..07cc848
--- /dev/null
+++ b/libs/io/src/test/java/org/the_jk/cleversync/io/SingleMergeLocalTest.kt
@@ -0,0 +1,26 @@
+package org.the_jk.cleversync.io
+
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import org.the_jk.cleversync.local.LocalTreeFactory
+import kotlin.time.Duration.Companion.milliseconds
+
+class SingleMergeLocalTest : BaseSingleMergeTest() {
+ @Rule
+ @JvmField
+ val temp = TemporaryFolder()
+
+ override fun source(): ModifiableTree {
+ return LocalTreeFactory.modifiableTree(temp.newFolder("source").toPath())
+ }
+
+ override fun sourceSupportsSymlinks() = true
+
+ override fun target(): ModifiableTree {
+ return LocalTreeFactory.modifiableTree(temp.newFolder("target").toPath())
+ }
+
+ override fun targetSupportsSymlinks() = true
+
+ override fun minModificationTime() = 10.milliseconds
+}