Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ plugins {
`maven-publish`
alias(libs.plugins.kmmbridgePlugin)
alias(libs.plugins.privacyPlugin)
alias(libs.plugins.koverPlugin)
}

dependencies {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ detekt = "1.23.8"
outdated = "0.54.0"
kmmbridge = "1.2.1"
privacy = "1.0.0"
kover = "0.9.8"

[libraries]
stately = { module = "co.touchlab:stately-concurrency", version.ref = "stately" }
Expand All @@ -29,3 +30,4 @@ versionsPlugin = { id = "com.github.ben-manes.versions", version.ref = "outdated
androidLibrary = { id = "com.android.library", version.ref = "androidGradle" }
kmmbridgePlugin = { id = "co.touchlab.kmmbridge.github", version.ref = "kmmbridge" }
privacyPlugin = { id = "org.jetbrains.kotlin.apple-privacy-manifests", version.ref = "privacy" }
koverPlugin = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
45 changes: 45 additions & 0 deletions src/commonTest/kotlin/com/airthings/lib/logging/LogArgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.airthings.lib.logging

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class LogArgTest {

@Test
fun `label is lowercased and trimmed`() {
val arg = LogArg(label = " STATUS ", value = 200)
assertEquals("status", arg.label)
}

@Test
fun `dashes in the label are replaced with underscores`() {
val arg = LogArg(label = "device-type", value = "wave")
assertEquals("device_type", arg.label)
}

@Test
fun `toString delegates to LogArgument format`() {
val arg = LogArg("status", 200)
assertEquals("[status=200]", arg.toString())
}

@Test
fun `equality is based on label and value`() {
assertEquals(LogArg("a", 1), LogArg("a", 1))
assertEquals(LogArg("A", 1), LogArg("a", 1)) // labels normalised equally.
assertNotEquals(LogArg("a", 1), LogArg("a", 2))
assertNotEquals(LogArg("a", 1), LogArg("b", 1))
}

@Test
fun `hashCode is stable across equal instances`() {
assertEquals(LogArg("a", 1).hashCode(), LogArg("a", 1).hashCode())
}

@Test
fun `equals rejects non-LogArg values`() {
assertNotEquals<Any?>(LogArg("a", 1), "a=1")
assertNotEquals<Any?>(LogArg("a", 1), null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.airthings.lib.logging

import kotlin.test.Test
import kotlin.test.assertEquals

class LogArgumentTest {

@Test
fun `format wraps label and value in brackets`() {
assertEquals("[status=200]", LogArgument.format("status", 200))
}

@Test
fun `formatValue renders null as explicit marker`() {
assertEquals("(null)", LogArgument.formatValue(null))
}

@Test
fun `formatValue quotes char sequences`() {
assertEquals("\"hello\"", LogArgument.formatValue("hello"))
assertEquals("\"\"", LogArgument.formatValue(""))
}

@Test
fun `formatValue renders scalars via toString`() {
assertEquals("42", LogArgument.formatValue(42))
assertEquals("3.14", LogArgument.formatValue(3.14))
assertEquals("true", LogArgument.formatValue(true))
}

@Test
fun `formatMap renders entries separated by comma`() {
val result = LogArgument.formatValue(mapOf("a" to 1, "b" to "two"))
assertEquals("{a: 1, b: \"two\"}", result)
}

@Test
fun `formatMap renders empty map as empty braces`() {
assertEquals("{}", LogArgument.formatValue(emptyMap<String, Any?>()))
}

@Test
fun `formatArray labels as Array with values`() {
val arr: Array<Any?> = arrayOf(1, "two", null)
assertEquals("Array(1, \"two\", (null))", LogArgument.formatValue(arr))
}

@Test
fun `formatList labels as List with values`() {
assertEquals("List(1, 2, 3)", LogArgument.formatValue(listOf(1, 2, 3)))
}

@Test
fun `formatValue for Set labels it as Collection`() {
val formatted = LogArgument.formatValue(setOf("x"))
assertEquals("Collection(\"x\")", formatted)
}

@Test
fun `formatValue for a pure Iterable labels it as Iterable`() {
val iterable: Iterable<Int> = Iterable { listOf(1, 2).iterator() }
assertEquals("Iterable(1, 2)", LogArgument.formatValue(iterable))
}

@Test
fun `formatValue recurses through nested containers`() {
val value = mapOf("list" to listOf("a", mapOf("k" to "v")))
assertEquals(
"{list: List(\"a\", {k: \"v\"})}",
LogArgument.formatValue(value),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.airthings.lib.logging

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

/**
* Basic hashCode coverage for [LogDate]. Full class coverage lives in `LogDateTest` on the
* `fix/logdate-after-precedence` branch / PR #40 — these tests pin the hashCode-specific
* invariants that hold on the current (pre-fix) code.
*/
class LogDateHashCodeTest {

@Test
fun `hashCode is equal for equal instances`() {
assertEquals(LogDate(2024, 3, 5).hashCode(), LogDate(2024, 3, 5).hashCode())
}

@Test
fun `hashCode ignores separator for realistic years`() {
assertEquals(
LogDate(2024, 3, 5, separator = '-').hashCode(),
LogDate(2024, 3, 5, separator = ':').hashCode(),
)
}

@Test
fun `hashCode differs for different days`() {
assertNotEquals(LogDate(2024, 3, 5).hashCode(), LogDate(2024, 3, 6).hashCode())
}

@Test
fun `hashCode differs for different months`() {
assertNotEquals(LogDate(2024, 3, 5).hashCode(), LogDate(2024, 4, 5).hashCode())
}

@Test
fun `hashCode differs for different years`() {
assertNotEquals(LogDate(2024, 3, 5).hashCode(), LogDate(2025, 3, 5).hashCode())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.airthings.lib.logging

import kotlin.test.Test
import kotlin.test.assertEquals

class LogDecorationTest {

@Test
fun `default construction uses the documented defaults`() {
val decoration = LogDecoration()
assertEquals("-- Lifecycle changed to: ", decoration.prefix)
assertEquals(null, decoration.suffix)
assertEquals(true, decoration.uppercase)
}

@Test
fun `custom values are preserved`() {
val decoration = LogDecoration(prefix = "<<", suffix = ">>", uppercase = false)
assertEquals("<<", decoration.prefix)
assertEquals(">>", decoration.suffix)
assertEquals(false, decoration.uppercase)
}

@Test
fun `null prefix and suffix are allowed`() {
val decoration = LogDecoration(prefix = null, suffix = null)
assertEquals(null, decoration.prefix)
assertEquals(null, decoration.suffix)
}

@Test
fun `data class equality is structural`() {
assertEquals(LogDecoration(), LogDecoration())
assertEquals(
LogDecoration(prefix = "X", suffix = "Y", uppercase = false),
LogDecoration(prefix = "X", suffix = "Y", uppercase = false),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.airthings.lib.logging

import kotlin.test.Test
import kotlin.test.assertEquals

class LogLifecycleTest {

@Test
fun `all five lifecycle events exist`() {
assertEquals(
setOf(
LogLifecycle.CREATED,
LogLifecycle.PAUSED,
LogLifecycle.RESUMED,
LogLifecycle.FINISHED,
LogLifecycle.DESTROYED,
),
LogLifecycle.entries.toSet(),
)
}

@Test
fun `toString uses the lowercase event label`() {
assertEquals("created", LogLifecycle.CREATED.toString())
assertEquals("paused", LogLifecycle.PAUSED.toString())
assertEquals("resumed", LogLifecycle.RESUMED.toString())
assertEquals("finished", LogLifecycle.FINISHED.toString())
assertEquals("destroyed", LogLifecycle.DESTROYED.toString())
}

@Test
fun `format without decoration uppercases the event`() {
assertEquals("RESUMED", LogLifecycle.format(LogLifecycle.RESUMED))
}

@Test
fun `format with default decoration applies prefix and uppercases`() {
val decoration = LogDecoration()

val result = LogLifecycle.format(LogLifecycle.CREATED, decoration)

assertEquals("-- LIFECYCLE CHANGED TO: CREATED", result)
}

@Test
fun `format respects custom prefix suffix and uppercase flag`() {
val decoration = LogDecoration(prefix = "<<", suffix = ">>", uppercase = false)

val result = LogLifecycle.format(LogLifecycle.PAUSED, decoration)

assertEquals("<<paused>>", result)
}

@Test
fun `format with decoration having null prefix and suffix just renders the event`() {
val decoration = LogDecoration(prefix = null, suffix = null, uppercase = true)

val result = LogLifecycle.format(LogLifecycle.DESTROYED, decoration)

assertEquals("DESTROYED", result)
}

@Test
fun `format with decoration having empty prefix and suffix just renders the event`() {
val decoration = LogDecoration(prefix = "", suffix = "", uppercase = false)

val result = LogLifecycle.format(LogLifecycle.FINISHED, decoration)

assertEquals("finished", result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.airthings.lib.logging

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class LogMessageTest {

@Test
fun `single-arg constructor yields empty args`() {
val message = LogMessage("hello")
assertEquals("hello", message.message)
assertTrue(message.args.isEmpty())
}

@Test
fun `map constructor converts into a list of LogArgs preserving order`() {
val message = LogMessage("hello", mapOf("a" to 1, "b" to 2))

assertEquals("hello", message.message)
assertEquals(listOf("a" to 1, "b" to 2), message.args.map { it.label to it.value })
}

@Test
fun `vararg constructor converts pairs into LogArgs`() {
val message = LogMessage("hello", "a" to 1, "b" to "two")

assertEquals("hello", message.message)
assertEquals(listOf("a" to 1, "b" to "two"), message.args.map { it.label to it.value })
}

@Test
fun `primary constructor accepts a raw LogArgument list`() {
val args = listOf<LogArgument>(LogArg("x", 42))
val message = LogMessage("hello", args)

assertEquals("hello", message.message)
assertEquals(args, message.args)
}

@Test
fun `toString formats the message followed by each argument`() {
val message = LogMessage("Sending request.", mapOf("status" to 200, "path" to "/v1/x"))

// Each LogArg renders as [label=value]; value rendering is covered in LogArgumentTest.
assertEquals(
"Sending request. [status=200] [path=\"/v1/x\"]",
message.toString(),
)
}

@Test
fun `format trims the leading and trailing whitespace off the message`() {
val formatted = LogMessage.format(message = " hello ", args = emptyList())
assertEquals("hello", formatted)
}

@Test
fun `format handles empty args`() {
assertEquals("hello", LogMessage.format("hello", emptyList()))
}
}
Loading