test(debug): Add unit tests for Zipper, DebugLogZipper, RecorderModule.State, and scanSessions edge cases

This commit is contained in:
darken
2026-03-09 04:53:01 +00:00
committed by Matthias Urhahn
parent 62e39ce3d2
commit c614e7bd4f
4 changed files with 342 additions and 0 deletions
@@ -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<FileNotFoundException> {
Zipper().zip(arrayOf(missing), zipPath)
}
}
}
}
@@ -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<String, String>): 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<IllegalArgumentException> {
zipper.zip(logDir)
}
}
@Test
fun `zip throws when logDir cannot list files`() {
val logDir = File(tempDir, "nonexistent")
shouldThrow<IllegalStateException> {
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
}
}
}
@@ -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<DebugSession.Ready>()
(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() }
@@ -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
}
}
}