From 71e9a88cca01050200489ca716928f3b9c3177b7 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Sun, 10 Nov 2024 15:47:01 +0100 Subject: Add verifier Used to check if target files have the expected hash. Using a memory cache to not have to read source each time but falls back to reading source if needed. --- .../org/the_jk/cleversync/io/BaseVerifierTest.kt | 230 +++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 libs/test-utils/src/main/java/org/the_jk/cleversync/io/BaseVerifierTest.kt (limited to 'libs/test-utils/src/main/java/org/the_jk') diff --git a/libs/test-utils/src/main/java/org/the_jk/cleversync/io/BaseVerifierTest.kt b/libs/test-utils/src/main/java/org/the_jk/cleversync/io/BaseVerifierTest.kt new file mode 100644 index 0000000..f72788b --- /dev/null +++ b/libs/test-utils/src/main/java/org/the_jk/cleversync/io/BaseVerifierTest.kt @@ -0,0 +1,230 @@ +package org.the_jk.cleversync.io + +import com.google.common.truth.Truth.assertThat +import org.junit.Assume +import org.junit.Before +import org.junit.Test + +abstract class BaseVerifierTest { + private lateinit var src: ModifiableTree + private lateinit var tgt: ModifiableTree + private val memory = FakeMemory() + + @Before + fun setUp() { + src = source() + tgt = target() + } + + @Test + fun empty() { + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + assertThat(memory.isEmpty()).isTrue() + } + + @Test + fun contentMatchNoMemory() { + val sourceFoo = src.createFile("foo") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT1) } + + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + assertThat(memory.get("/foo")).isEqualTo(HASH1) + } + + @Test + fun contentMatchMemory() { + memory.put("/foo", HASH1) + val sourceFoo = src.createFile("foo") + // Intentionally wrong, to show that source is not read when memory hash matches + sourceFoo.write().use { it.write(0) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT1) } + + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + } + + @Test + fun contentMatchMemoryOldFormat() { + Assume.assumeTrue(Hasher.DEFAULT != Hasher.Format.SHA160) + + memory.put("/foo", HASH1_OLD) + val sourceFoo = src.createFile("foo") + // Intentionally wrong, to show that source is not read when memory hash matches + sourceFoo.write().use { it.write(0) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT1) } + + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + } + + @Test + fun contentMismatchNoMemory() { + val sourceFoo = src.createFile("foo") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + val actions = Verifier.calculate(tgt, src, memory) + assertThat(actions).containsExactly(Action.Copy(name = "foo", overwrite = true)) + assertThat(Modifier.apply(tgt, src, actions, memory = memory)).isEmpty() + assertThat(memory.get("/foo")).isEqualTo(HASH1) + assertThat(targetFoo.read().use { it.readBytes() }) + .isEqualTo(CONTENT1) + } + + @Test + fun contentMismatchMemory() { + memory.put("/foo", HASH1) + val sourceFoo = src.createFile("foo") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + val actions = Verifier.calculate(tgt, src, memory) + assertThat(actions).containsExactly(Action.Copy(name = "foo", overwrite = true)) + assertThat(Modifier.apply(tgt, src, actions, memory = memory)).isEmpty() + assertThat(memory.get("/foo")).isEqualTo(HASH1) + assertThat(targetFoo.read().use { it.readBytes() }) + .isEqualTo(CONTENT1) + } + + @Test + fun contentMismatchMemoryOldFormat() { + memory.put("/foo", HASH1_OLD) + val sourceFoo = src.createFile("foo") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + val actions = Verifier.calculate(tgt, src, memory) + assertThat(actions).containsExactly(Action.Copy(name = "foo", overwrite = true)) + assertThat(Modifier.apply(tgt, src, actions, memory = memory)).isEmpty() + assertThat(memory.get("/foo")).isEqualTo(HASH1) + assertThat(targetFoo.read().use { it.readBytes() }) + .isEqualTo(CONTENT1) + } + + @Test + fun contentMismatchBadMemory() { + memory.put("/foo", "".toByteArray().inputStream().use { Hasher.hash(it)!! }) + val sourceFoo = src.createFile("foo") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + val actions = Verifier.calculate(tgt, src, memory) + assertThat(actions).containsExactly(Action.Copy(name = "foo", overwrite = true)) + assertThat(Modifier.apply(tgt, src, actions, memory = memory)).isEmpty() + assertThat(memory.get("/foo")).isEqualTo(HASH1) + assertThat(targetFoo.read().use { it.readBytes() }) + .isEqualTo(CONTENT1) + } + + @Test + fun contentMismatchBadMemory2() { + memory.put("/foo", HASH2) + val sourceFoo = src.createFile("foo") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + // We trust memory, so source will not be read and the difference + // will not be noticed. Remember that this is used together with merge. + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + } + + @Test + fun contentMismatchNoSource() { + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + } + + @Test + fun contentMismatchMemoryNoSource() { + memory.put("/foo", HASH1) + val targetFoo = tgt.createFile("foo") + targetFoo.write().use { it.write(CONTENT2) } + + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + } + + @Test + fun dirContentMatchMemory() { + memory.put("/foo/bar", HASH1) + val sourceFoo = src.createDirectory("foo").createFile("bar") + // Intentionally wrong, to show that source is not read when memory hash matches + sourceFoo.write().use { it.write(0) } + val targetFoo = tgt.createDirectory("foo").createFile("bar") + targetFoo.write().use { it.write(CONTENT1) } + + assertThat(Verifier.calculate(tgt, src, memory)).isEmpty() + } + + @Test + fun dirContentMismatchNoMemory() { + val sourceFoo = src.createDirectory("foo").createFile("bar") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createDirectory("foo").createFile("bar") + targetFoo.write().use { it.write(CONTENT2) } + + val actions = Verifier.calculate(tgt, src, memory) + assertThat(actions).containsExactly( + Action.ChangeDir( + name = "foo", + actions = listOf(Action.Copy(name = "bar", overwrite = true)), + ), + ) + assertThat(Modifier.apply(tgt, src, actions, memory = memory)).isEmpty() + assertThat(memory.get("/foo/bar")).isEqualTo(HASH1) + assertThat(targetFoo.read().use { it.readBytes() }) + .isEqualTo(CONTENT1) + } + @Test + fun dirContentMismatchMemory() { + memory.put("/foo/bar", HASH1) + val sourceFoo = src.createDirectory("foo").createFile("bar") + sourceFoo.write().use { it.write(CONTENT1) } + val targetFoo = tgt.createDirectory("foo").createFile("bar") + targetFoo.write().use { it.write(CONTENT2) } + + val actions = Verifier.calculate(tgt, src, memory) + assertThat(actions).containsExactly( + Action.ChangeDir( + name = "foo", + actions = listOf(Action.Copy(name = "bar", overwrite = true)), + ), + ) + assertThat(Modifier.apply(tgt, src, actions, memory = memory)).isEmpty() + assertThat(memory.get("/foo/bar")).isEqualTo(HASH1) + assertThat(targetFoo.read().use { it.readBytes() }) + .isEqualTo(CONTENT1) + } + + abstract fun source(): ModifiableTree + abstract fun target(): ModifiableTree + + private class FakeMemory : Verifier.Memory { + private val data = mutableMapOf() + + override fun get(path: String) = data[path] + + override fun put(path: String, hash: String) { + data[path] = hash + } + + fun isEmpty() = data.isEmpty() + } + + private companion object { + val CONTENT1 = "Hello world".toByteArray() + val HASH1 = CONTENT1.inputStream().use { Hasher.hash(it)!! } + val HASH1_OLD = CONTENT1.inputStream().use { Hasher.hash(it, Hasher.Format.SHA160)!! } + + val CONTENT2 = "Goodbye world".toByteArray() + val HASH2 = CONTENT2.inputStream().use { Hasher.hash(it)!! } + } +} -- cgit v1.2.3-70-g09d2