diff --git a/app/src/test/java/eu/darken/capod/common/compression/ZipperTest.kt b/app/src/test/java/eu/darken/capod/common/compression/ZipperTest.kt new file mode 100644 index 00000000..08b66e70 --- /dev/null +++ b/app/src/test/java/eu/darken/capod/common/compression/ZipperTest.kt @@ -0,0 +1,103 @@ +package eu.darken.capod.common.compression + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.longs.shouldBeGreaterThan +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import testhelpers.BaseTest +import java.io.File +import java.io.FileNotFoundException +import java.util.zip.ZipFile + +class ZipperTest : BaseTest() { + + @TempDir + lateinit var tempDir: File + + private fun createFile(name: String, content: String): File { + return File(tempDir, name).also { it.writeText(content) } + } + + @Nested + inner class ZipCreation { + @Test + fun `zip creates valid zip file`() { + val file1 = createFile("a.txt", "hello") + val file2 = createFile("b.txt", "world") + val zipPath = File(tempDir, "out.zip").path + + Zipper().zip(arrayOf(file1.path, file2.path), zipPath) + + val zipFile = File(zipPath) + zipFile.exists() shouldBe true + zipFile.length() shouldBeGreaterThan 0L + } + + @Test + fun `zip contains all input files`() { + val file1 = createFile("core.log", "log content") + val file2 = createFile("extra.log", "extra content") + val zipPath = File(tempDir, "out.zip").path + + Zipper().zip(arrayOf(file1.path, file2.path), zipPath) + + ZipFile(zipPath).use { zf -> + zf.entries().toList().map { it.name } shouldContainExactlyInAnyOrder listOf("core.log", "extra.log") + } + } + + @Test + fun `zip file contents match originals`() { + val content1 = "first file content" + val content2 = "second file content" + val file1 = createFile("a.txt", content1) + val file2 = createFile("b.txt", content2) + val zipPath = File(tempDir, "out.zip").path + + Zipper().zip(arrayOf(file1.path, file2.path), zipPath) + + ZipFile(zipPath).use { zf -> + zf.getInputStream(zf.getEntry("a.txt")).bufferedReader().readText() shouldBe content1 + zf.getInputStream(zf.getEntry("b.txt")).bufferedReader().readText() shouldBe content2 + } + } + + @Test + fun `zip with single file works`() { + val file = createFile("only.txt", "solo") + val zipPath = File(tempDir, "out.zip").path + + Zipper().zip(arrayOf(file.path), zipPath) + + ZipFile(zipPath).use { zf -> + zf.entries().toList().map { it.name } shouldBe listOf("only.txt") + } + } + + @Test + fun `zip with empty file includes entry`() { + val file = createFile("empty.txt", "") + val zipPath = File(tempDir, "out.zip").path + + Zipper().zip(arrayOf(file.path), zipPath) + + ZipFile(zipPath).use { zf -> + val entry = zf.getEntry("empty.txt") + entry.size shouldBe 0L + } + } + + @Test + fun `zip throws on nonexistent input file`() { + val missing = File(tempDir, "missing.txt").path + val zipPath = File(tempDir, "out.zip").path + + shouldThrow { + Zipper().zip(arrayOf(missing), zipPath) + } + } + } +} diff --git a/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugLogZipperTest.kt b/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugLogZipperTest.kt new file mode 100644 index 00000000..11cd0b0d --- /dev/null +++ b/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugLogZipperTest.kt @@ -0,0 +1,126 @@ +package eu.darken.capod.common.debug.recording.core + +import android.content.Context +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import testhelpers.BaseTest +import java.io.File +import java.util.zip.ZipFile + +class DebugLogZipperTest : BaseTest() { + + @TempDir + lateinit var tempDir: File + + private lateinit var zipper: DebugLogZipper + + @BeforeEach + fun setup() { + zipper = DebugLogZipper(mockk(relaxed = true)) + } + + private fun createLogDir(name: String = "session1", vararg files: Pair): File { + val logDir = File(tempDir, name).also { it.mkdirs() } + for ((fileName, content) in files) { + File(logDir, fileName).writeText(content) + } + return logDir + } + + @Nested + inner class ZipCreation { + @Test + fun `zip creates sibling zip file`() { + val logDir = createLogDir(files = arrayOf("core.log" to "log content")) + + val result = zipper.zip(logDir) + + result.exists() shouldBe true + result.name shouldBe "session1.zip" + result.parentFile shouldBe tempDir + } + + @Test + fun `zip removes temp file on success`() { + val logDir = createLogDir(files = arrayOf("core.log" to "log content")) + + zipper.zip(logDir) + + File(tempDir, "session1.zip.tmp").exists() shouldBe false + } + + @Test + fun `zip result contains correct entries`() { + val logDir = createLogDir( + files = arrayOf( + "core.log" to "core log content", + "extra.log" to "extra content", + ) + ) + + val result = zipper.zip(logDir) + + ZipFile(result).use { zf -> + zf.entries().toList().map { it.name } shouldContainExactlyInAnyOrder listOf("core.log", "extra.log") + } + } + + @Test + fun `zip overwrites existing zip`() { + val logDir = createLogDir(files = arrayOf("core.log" to "new content")) + val existingZip = File(tempDir, "session1.zip") + existingZip.writeText("old zip data") + val oldLength = existingZip.length() + + val result = zipper.zip(logDir) + + result.exists() shouldBe true + // New zip should be different from the old placeholder text + result.length() shouldBe result.length() // exists + ZipFile(result).use { zf -> + zf.entries().toList().map { it.name } shouldBe listOf("core.log") + } + } + } + + @Nested + inner class ErrorCases { + @Test + fun `zip throws on empty logDir`() { + val logDir = File(tempDir, "empty_session").also { it.mkdirs() } + + shouldThrow { + zipper.zip(logDir) + } + } + + @Test + fun `zip throws when logDir cannot list files`() { + val logDir = File(tempDir, "nonexistent") + + shouldThrow { + zipper.zip(logDir) + } + } + + @Test + fun `zip cleans temp file on failure`() { + // logDir with no files triggers require() failure after Zipper.zip() runs + val logDir = File(tempDir, "empty_session").also { it.mkdirs() } + + try { + zipper.zip(logDir) + } catch (_: IllegalArgumentException) { + // expected + } + + File(tempDir, "empty_session.zip.tmp").exists() shouldBe false + } + } +} diff --git a/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugSessionManagerSessionLogicTest.kt b/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugSessionManagerSessionLogicTest.kt index 8514335c..a4c3250a 100644 --- a/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugSessionManagerSessionLogicTest.kt +++ b/app/src/test/java/eu/darken/capod/common/debug/recording/core/DebugSessionManagerSessionLogicTest.kt @@ -205,6 +205,82 @@ class DebugSessionManagerSessionLogicTest : BaseTest() { (session as DebugSession.Recording).startedAt shouldBe 1700000000000L } + @Test + fun `ignores non-directory non-zip files`() { + File(externalLogsDir, "capod_1.0_1700000000000_abcd1234").also { it.mkdirs() } + File(externalLogsDir, "capod_1.0_1700000000000_abcd1234/core.log").writeText("log") + File(externalLogsDir, "random_notes.txt").writeText("not a session") + + val result = DebugSessionManager.scanSessions(logDirectories = logDirs()) + + result shouldHaveSize 1 + result.first().id shouldBe "ext:capod_1.0_1700000000000_abcd1234" + } + + @Test + fun `handles missing log directory gracefully`() { + val missing = File(tempDir, "nonexistent/path") + val result = DebugSessionManager.scanSessions(logDirectories = listOf(missing)) + + result.shouldBeEmpty() + } + + @Test + fun `cache directory gets cache prefix`() { + val sessionDir = File(cacheLogsDir, "capod_1.0_1700000000000_abcd1234").also { it.mkdirs() } + File(sessionDir, "core.log").writeText("cached log") + + val result = DebugSessionManager.scanSessions(logDirectories = logDirs()) + + result shouldHaveSize 1 + result.first().id shouldBe "cache:capod_1.0_1700000000000_abcd1234" + } + + @Test + fun `empty core log with valid sibling zip returns Ready`() { + val sessionDir = File(externalLogsDir, "capod_1.0_1700000000000_abcd1234").also { it.mkdirs() } + File(sessionDir, "core.log").createNewFile() + File(externalLogsDir, "capod_1.0_1700000000000_abcd1234.zip").writeText("valid zip data") + + val result = DebugSessionManager.scanSessions(logDirectories = logDirs()) + + result shouldHaveSize 1 + val session = result.first() + session.shouldBeInstanceOf() + (session as DebugSession.Ready).logDir shouldBe null + session.zipFile shouldBe File(externalLogsDir, "capod_1.0_1700000000000_abcd1234.zip") + } + + @Test + fun `multiple log directories are combined`() { + val extDir = File(externalLogsDir, "capod_1.0_1700000000000_aaaa1111").also { it.mkdirs() } + File(extDir, "core.log").writeText("ext log") + + val cacheDir = File(cacheLogsDir, "capod_1.0_1600000000000_bbbb2222").also { it.mkdirs() } + File(cacheDir, "core.log").writeText("cache log") + + val result = DebugSessionManager.scanSessions(logDirectories = logDirs()) + + result shouldHaveSize 2 + result.any { it.id == "ext:capod_1.0_1700000000000_aaaa1111" } shouldBe true + result.any { it.id == "cache:capod_1.0_1600000000000_bbbb2222" } shouldBe true + } + + @Test + fun `sessions with same createdAt sorted by id ascending`() { + val dirA = File(externalLogsDir, "capod_1.0_1700000000000_aaaa").also { it.mkdirs() } + File(dirA, "core.log").writeText("log a") + + val dirB = File(externalLogsDir, "capod_1.0_1700000000000_zzzz").also { it.mkdirs() } + File(dirB, "core.log").writeText("log b") + + val result = DebugSessionManager.scanSessions(logDirectories = logDirs()) + + result shouldHaveSize 2 + result[0].id shouldBe "ext:capod_1.0_1700000000000_aaaa" + result[1].id shouldBe "ext:capod_1.0_1700000000000_zzzz" + } + @Test fun `multiple sessions sorted by createdAt descending then id ascending`() { val oldDir = File(externalLogsDir, "capod_1.0_1600000000000_abcd1234").also { it.mkdirs() } diff --git a/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleStateTest.kt b/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleStateTest.kt new file mode 100644 index 00000000..8270e9b0 --- /dev/null +++ b/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleStateTest.kt @@ -0,0 +1,37 @@ +package eu.darken.capod.common.debug.recording.core + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RecorderModuleStateTest : BaseTest() { + + @Nested + inner class DefaultState { + @Test + fun `shouldRecord is false`() { + RecorderModule.State().shouldRecord shouldBe false + } + + @Test + fun `isRecording is false`() { + RecorderModule.State().isRecording shouldBe false + } + + @Test + fun `currentLogDir is null`() { + RecorderModule.State().currentLogDir shouldBe null + } + + @Test + fun `recordingStartedAt is zero`() { + RecorderModule.State().recordingStartedAt shouldBe 0L + } + + @Test + fun `currentLogPath is null`() { + RecorderModule.State().currentLogPath shouldBe null + } + } +}