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.Companion.seconds 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(MIN_MODIFICATION_TIME.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(MIN_MODIFICATION_TIME.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") 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 symlinkOutsideRoot() { Assume.assumeTrue(sourceSupportsSymlinks()) val link = src.createLink("bad_link", "../../escaped") // Some sources doesn't allow to create symlinks that point outside filesystem when (val target = link.resolve()) { is Link.NoTarget -> Assume.assumeTrue(target.path == "../../escaped") is Link.FileTarget, is Link.DirectoryTarget, -> Assert.fail("Should not target anything") } val actions = SingleMerge.calculate(tgt, src) assertThat(actions).isEmpty() } @Test fun symlinkOutsideDir() { Assume.assumeTrue(sourceSupportsSymlinks()) val dir = src.createDirectory("foo") val escaped = src.createDirectory("escaped") dir.createLink("bad_link", escaped) src.removeDirectory("escaped") val actions = SingleMerge.calculate(tgt, dir) assertThat(actions).isEmpty() } @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.readBytes() }).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.readBytes() }).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 private companion object { // See SingleMerge, anything less than a second is ignored // Not great, but for network file systems we really can't // be asking for better. val MIN_MODIFICATION_TIME = 1.seconds } }