mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
test(debug): Add unit tests for Zipper, DebugLogZipper, RecorderModule.State, and scanSessions edge cases
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -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() }
|
||||
|
||||
+37
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user