Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit a7302fda authored by Wolf-Martell Montwé's avatar Wolf-Martell Montwé
Browse files

refactor(core-logging): remove tag handling from ConsoleLogSink

parent 6c2f9ba3
Loading
Loading
Loading
Loading
+0 −4
Original line number Diff line number Diff line
@@ -15,9 +15,5 @@ kotlin {
        androidMain.dependencies {
            implementation(libs.timber)
        }

        androidUnitTest.dependencies {
            implementation(libs.robolectric)
        }
    }
}
+5 −38
Original line number Diff line number Diff line
package net.thunderbird.core.logging.console

import android.os.Build
import java.util.regex.Pattern
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogSink
import timber.log.Timber

internal class AndroidConsoleLogSink(
    level: LogLevel,
) : BaseConsoleLogSink(level) {
    override val level: LogLevel,
) : LogSink {

    override fun logWithTag(event: LogEvent, tag: String?) {
        val timber = tag?.let { Timber.tag(it) } ?: Timber
    override fun log(event: LogEvent) {
        val timber = event.tag?.let { Timber.tag(it) } ?: Timber

        when (event.level) {
            LogLevel.VERBOSE -> timber.v(event.throwable, event.message)
@@ -21,36 +20,4 @@ internal class AndroidConsoleLogSink(
            LogLevel.ERROR -> timber.e(event.throwable, event.message)
        }
    }

    override fun processTag(tag: String): String {
        // Truncate tags to MAX_TAG_LENGTH when API level is lower than 26
        return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            tag
        } else {
            tag.substring(0, MAX_TAG_LENGTH)
        }
    }

    override fun getAnonymousClassPattern(): Pattern {
        return ANONYMOUS_CLASS
    }

    override fun getIgnoreClasses(): Set<String> {
        return IGNORE_CLASSES
    }

    companion object {
        private const val MAX_TAG_LENGTH = 23
        private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")

        private val IGNORE_CLASSES = setOf(
            Timber::class.java.name,
            Timber.Forest::class.java.name,
            Timber.Tree::class.java.name,
            Timber.DebugTree::class.java.name,
            AndroidConsoleLogSink::class.java.name,
            BaseConsoleLogSink::class.java.name,
            // Add other classes to ignore if needed
        )
    }
}
+0 −87
Original line number Diff line number Diff line
@@ -4,11 +4,9 @@ import android.util.Log
import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import kotlin.test.Test
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import org.robolectric.annotation.Config
import timber.log.Timber

class AndroidConsoleLoggerTest {
@@ -81,91 +79,6 @@ class AndroidConsoleLoggerTest {
        assertThat(testTree.events[4]).isEqualTo(eventError)
    }

    @Test
    fun shouldExtractTagFromStackTraceWhenNoTagProvided() {
        // Arrange
        val testTree = TestTree()
        Timber.plant(testTree)
        val eventWithoutTag = LogEvent(
            level = LogLevel.INFO,
            tag = null, // No tag provided
            message = "This is a message without a tag",
            throwable = null,
            timestamp = 0L,
        )

        val testSubject = AndroidConsoleLogSink(LogLevel.VERBOSE)

        // Act
        testSubject.log(eventWithoutTag)

        // Assert
        assertThat(testTree.events).hasSize(1)
        // The tag should have been extracted from the stack trace
        assertThat(testTree.events[0].tag).isNotNull()
        // The tag should be the class name of the caller (AndroidConsoleLoggerTest)
        // Note: The tag is truncated to 23 characters on older Android versions
        assertThat(testTree.events[0].tag).isEqualTo("AndroidConsoleLoggerTes")
    }

    @Config(sdk = [25])
    @Test
    fun shouldTruncateLongTagsToMaxLength() {
        // Arrange
        val testTree = TestTree()
        Timber.plant(testTree)
        val longTag = "ThisIsAVeryLongTagThatExceedsTheMaximumLength"
        val expectedTruncatedTag = "ThisIsAVeryLongTagThatE" // 23 characters
        val eventWithLongTag = LogEvent(
            level = LogLevel.INFO,
            tag = longTag,
            message = "This is a message with a long tag",
            throwable = null,
            timestamp = 0L,
        )

        val testSubject = AndroidConsoleLogSink(LogLevel.VERBOSE)

        // Act
        testSubject.log(eventWithLongTag)

        // Assert
        assertThat(testTree.events).hasSize(1)

        // Debug: Print the actual tag and its length
        val actualTag = testTree.events[0].tag
        println("[DEBUG_LOG] Actual tag: '$actualTag', length: ${actualTag?.length}")
        println("[DEBUG_LOG] Expected tag: '$expectedTruncatedTag', length: ${expectedTruncatedTag.length}")

        // The tag should always be truncated to 23 characters for consistency
        assertThat(actualTag).isEqualTo(expectedTruncatedTag)
    }

    @Config(sdk = [26])
    fun shouldNotTruncateTagsOnNewAndroidVersions() {
        // Arrange
        val testTree = TestTree()
        Timber.plant(testTree)
        val longTag = "ThisIsAVeryLongTagThatExceedsTheMaximumLength"
        val expectedTag = longTag // No truncation on API 26+
        val eventWithLongTag = LogEvent(
            level = LogLevel.INFO,
            tag = longTag,
            message = "This is a message with a long tag",
            throwable = null,
            timestamp = 0L,
        )

        val testSubject = AndroidConsoleLogSink(LogLevel.VERBOSE)

        // Act
        testSubject.log(eventWithLongTag)

        // Assert
        assertThat(testTree.events).hasSize(1)
        assertThat(testTree.events[0].tag).isEqualTo(expectedTag)
    }

    class TestTree : Timber.DebugTree() {

        val events = mutableListOf<LogEvent>()
+0 −103
Original line number Diff line number Diff line
package net.thunderbird.core.logging.console

import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogSink

/**
 * An abstract base class for console log sinks that provides common functionality.
 *
 * This class handles tag extraction from stack traces and other common operations.
 *
 * @param level The minimum [LogLevel] for messages to be logged.
 */
abstract class BaseConsoleLogSink(
    override val level: LogLevel,
) : LogSink {

    /**
     * Logs a [LogEvent].
     *
     * @param event The [LogEvent] to log.
     */
    override fun log(event: LogEvent) {
        val tag = composeTag(event)
        logWithTag(event, tag)
    }

    /**
     * Logs a [LogEvent] with the given tag.
     *
     * @param event The [LogEvent] to log.
     * @param tag The tag to use for logging.
     */
    protected abstract fun logWithTag(event: LogEvent, tag: String?)

    /**
     * Composes a tag for the given [LogEvent].
     *
     * If the event has a tag, it is used; otherwise, a tag is extracted from the stack trace.
     * The tag is processed using the [processTag] method before being returned.
     *
     * @param event The [LogEvent] to compose a tag for.
     * @return The composed tag, or null if no tag could be determined.
     */
    protected fun composeTag(event: LogEvent): String? {
        // If a tag is provided, use it; otherwise, extract it from the stack trace
        val rawTag = event.tag ?: extractTagFromStackTrace()
        // Process the tag before returning it
        return rawTag?.let { processTag(it) }
    }

    /**
     * Extracts a tag from the stack trace.
     *
     * @return The extracted tag, or null if no suitable tag could be found.
     */
    protected fun extractTagFromStackTrace(): String? {
        return Throwable().stackTrace
            .firstOrNull { it.className !in getIgnoreClasses() }
            ?.let(::createStackElementTag)
    }

    /**
     * Creates a tag from a stack trace element.
     *
     * @param element The stack trace element to create a tag from.
     * @return The created tag.
     */
    protected fun createStackElementTag(element: StackTraceElement): String {
        var tag = element.className.substringAfterLast('.')
        val matcher = getAnonymousClassPattern().matcher(tag)
        if (matcher.find()) {
            tag = matcher.replaceAll("")
        }
        return processTag(tag)
    }

    /**
     * Processes a tag before it is used for logging.
     *
     * This method can be overridden by subclasses to perform platform-specific tag processing.
     *
     * @param tag The tag to process.
     * @return The processed tag.
     */
    protected open fun processTag(tag: String): String {
        return tag
    }

    /**
     * Gets the pattern used to identify anonymous classes in class names.
     *
     * @return The pattern for anonymous classes.
     */
    protected abstract fun getAnonymousClassPattern(): java.util.regex.Pattern

    /**
     * Gets the set of class names to ignore when extracting tags from stack traces.
     *
     * @return The set of class names to ignore.
     */
    protected abstract fun getIgnoreClasses(): Set<String>
}
+8 −26
Original line number Diff line number Diff line
package net.thunderbird.core.logging.console

import java.util.regex.Pattern
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogSink

internal class JvmConsoleLogSink(
    level: LogLevel,
) : BaseConsoleLogSink(level) {
    override val level: LogLevel,
) : LogSink {

    override fun logWithTag(event: LogEvent, tag: String?) {
        println("[$level] ${composeMessage(event, tag)}")
    override fun log(event: LogEvent) {
        println("[$level] ${composeMessage(event)}")
        event.throwable?.printStackTrace()
    }

    private fun composeMessage(event: LogEvent, tag: String?): String {
        return if (tag != null) {
            "[$tag] ${event.message}"
    private fun composeMessage(event: LogEvent): String {
        return if (event.tag != null) {
            "[${event.tag}] ${event.message}"
        } else {
            event.message
        }
    }

    override fun getAnonymousClassPattern(): Pattern {
        return ANONYMOUS_CLASS
    }

    override fun getIgnoreClasses(): Set<String> {
        return IGNORE_CLASSES
    }

    companion object {
        private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")

        private val IGNORE_CLASSES = setOf(
            JvmConsoleLogSink::class.java.name,
            BaseConsoleLogSink::class.java.name,
            // Add other classes to ignore if needed
        )
    }
}
Loading