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

Commit 5c06874d authored by Jeff DeCew's avatar Jeff DeCew
Browse files

Simplify LogWtfHandlerRule and add tests

Bug: 381916615
Flag: TEST_ONLY
Test: atest LogWtfHandlerRule
Change-Id: I8433e8b8ef5662d011bb2b12426fe45a16d8270e
parent 18cdf526
Loading
Loading
Loading
Loading
+155 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.log

import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.model.Statement
import org.mockito.kotlin.mock

@RunWith(AndroidJUnit4::class)
@SmallTest
class LogWtfHandlerRuleTest : SysuiTestCase() {

    val underTest = LogWtfHandlerRule()

    @Test
    fun passingTestWithoutWtf_shouldPass() {
        val result = runTestCodeWithRule {
            Log.e(TAG, "just an error", IndexOutOfBoundsException())
        }
        assertThat(result.isSuccess).isTrue()
    }

    @Test
    fun passingTestWithWtf_shouldFail() {
        val result = runTestCodeWithRule {
            Log.wtf(TAG, "some terrible failure", IllegalStateException())
        }
        assertThat(result.isFailure).isTrue()
        val exception = result.exceptionOrNull()
        assertThat(exception).isInstanceOf(AssertionError::class.java)
        assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java)
        assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java)
    }

    @Test
    fun failingTestWithoutWtf_shouldFail() {
        val result = runTestCodeWithRule {
            Log.e(TAG, "just an error", IndexOutOfBoundsException())
            throw NullPointerException("some npe")
        }
        assertThat(result.isFailure).isTrue()
        assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
    }

    @Test
    fun failingTestWithWtf_shouldFail() {
        val result = runTestCodeWithRule {
            Log.wtf(TAG, "some terrible failure", IllegalStateException())
            throw NullPointerException("some npe")
        }
        assertThat(result.isFailure).isTrue()
        assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
        val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions
        assertThat(suppressedExceptions).hasSize(1)
        val suppressed = suppressedExceptions.first()
        assertThat(suppressed).isInstanceOf(AssertionError::class.java)
        assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java)
        assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java)
    }

    @Test
    fun passingTestWithExemptWtf_shouldPass() {
        underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
        val result = runTestCodeWithRule {
            Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
        }
        assertThat(result.isSuccess).isTrue()
    }

    @Test
    fun failingTestWithExemptWtf_shouldFail() {
        underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
        val result = runTestCodeWithRule {
            Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
            throw NullPointerException("some npe")
        }
        assertThat(result.isFailure).isTrue()
        assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
        val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions
        assertThat(suppressedExceptions).isEmpty()
    }

    @Test
    fun passingTestWithOneExemptWtfOfTwo_shouldFail() {
        underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
        val result = runTestCodeWithRule {
            Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
            Log.wtf(TAG, "some terrible failure", IllegalStateException())
        }
        assertThat(result.isFailure).isTrue()
        val exception = result.exceptionOrNull()
        assertThat(exception).isInstanceOf(AssertionError::class.java)
        assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java)
        assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java)
    }

    @Test
    fun failingTestWithOneExemptWtfOfTwo_shouldFail() {
        underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
        val result = runTestCodeWithRule {
            Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
            Log.wtf(TAG, "some terrible failure", IllegalStateException())
            throw NullPointerException("some npe")
        }
        assertThat(result.isFailure).isTrue()
        assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
        val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions
        assertThat(suppressedExceptions).hasSize(1)
        val suppressed = suppressedExceptions.first()
        assertThat(suppressed).isInstanceOf(AssertionError::class.java)
        assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java)
        assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java)
    }

    private fun runTestCodeWithRule(testCode: () -> Unit): Result<Unit> {
        val testCodeStatement =
            object : Statement() {
                override fun evaluate() {
                    testCode()
                }
            }
        val wrappedTest = underTest.apply(testCodeStatement, mock())
        return try {
            wrappedTest.evaluate()
            Result.success(Unit)
        } catch (e: Throwable) {
            Result.failure(e)
        }
    }

    companion object {
        const val TAG = "LogWtfHandlerRuleTest"
        const val TAG_EXPECTED = "EXPECTED"
    }
}
+41 −61
Original line number Diff line number Diff line
@@ -24,21 +24,23 @@ import org.junit.runners.model.Statement

class LogWtfHandlerRule : TestRule {

    private var started = false
    private var handler = ThrowAndFailAtEnd
    private var failureLogExemptions = mutableListOf<FailureLogExemption>()

    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            override fun evaluate() {
                started = true
                val handler = TerribleFailureTestHandler()
                val originalWtfHandler = Log.setWtfHandler(handler)
                var failure: Throwable? = null
                try {
                    base.evaluate()
                } catch (ex: Throwable) {
                    failure = ex.runAndAddSuppressed { handler.onTestFailure(ex) }
                    failure = ex
                } finally {
                    failure = failure.runAndAddSuppressed { handler.onTestFinished() }
                    failure =
                        runAndAddSuppressed(failure) {
                            handler.onTestFinished(failureLogExemptions)
                        }
                    Log.setWtfHandler(originalWtfHandler)
                }
                if (failure != null) {
@@ -48,74 +50,52 @@ class LogWtfHandlerRule : TestRule {
        }
    }

    fun Throwable?.runAndAddSuppressed(block: () -> Unit): Throwable? {
    /** Adds a log failure exemption. Exemptions are evaluated at the end of the test. */
    fun addFailureLogExemption(exemption: FailureLogExemption) {
        failureLogExemptions.add(exemption)
    }

    /** Clears and sets exemptions. Exemptions are evaluated at the end of the test. */
    fun resetFailureLogExemptions(vararg exemptions: FailureLogExemption) {
        failureLogExemptions = exemptions.toMutableList()
    }

    private fun runAndAddSuppressed(currentError: Throwable?, block: () -> Unit): Throwable? {
        try {
            block()
        } catch (t: Throwable) {
            if (this == null) {
            if (currentError == null) {
                return t
            }
            addSuppressed(t)
            currentError.addSuppressed(t)
        }
        return this
    }

    fun setWtfHandler(handler: TerribleFailureTestHandler) {
        check(!started) { "Should only be called before the test starts" }
        this.handler = handler
    }

    fun interface TerribleFailureTestHandler : TerribleFailureHandler {
        fun onTestFailure(failure: Throwable) {}
        fun onTestFinished() {}
        return currentError
    }

    companion object Handlers {
        val ThrowAndFailAtEnd
            get() =
                object : TerribleFailureTestHandler {
                    val failures = mutableListOf<Log.TerribleFailure>()
    private class TerribleFailureTestHandler : TerribleFailureHandler {
        private val failureLogs = mutableListOf<FailureLog>()

                    override fun onTerribleFailure(
                        tag: String,
                        what: Log.TerribleFailure,
                        system: Boolean
                    ) {
                        failures.add(what)
                        throw what
        override fun onTerribleFailure(tag: String, what: Log.TerribleFailure, system: Boolean) {
            failureLogs.add(FailureLog(tag = tag, failure = what, system = system))
        }

                    override fun onTestFailure(failure: Throwable) {
                        super.onTestFailure(failure)
        fun onTestFinished(exemptions: List<FailureLogExemption>) {
            val failures =
                failureLogs.filter { failureLog ->
                    !exemptions.any { it.isFailureLogExempt(failureLog) }
                }

                    override fun onTestFinished() {
            if (failures.isNotEmpty()) {
                            throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
                throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0].failure)
            }
        }
    }

        val JustThrow = TerribleFailureTestHandler { _, what, _ -> throw what }

        val JustFailAtEnd
            get() =
                object : TerribleFailureTestHandler {
                    val failures = mutableListOf<Log.TerribleFailure>()

                    override fun onTerribleFailure(
                        tag: String,
                        what: Log.TerribleFailure,
                        system: Boolean
                    ) {
                        failures.add(what)
                    }
    /** All the information from a call to [Log.wtf] that was handed to [TerribleFailureHandler] */
    data class FailureLog(val tag: String, val failure: Log.TerribleFailure, val system: Boolean)

                    override fun onTestFinished() {
                        if (failures.isNotEmpty()) {
                            throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
                        }
                    }
                }
    /** An interface for exempting a [FailureLog] from causing a test failure. */
    fun interface FailureLogExemption {
        /** Determines whether a log should be except from failing the test. */
        fun isFailureLogExempt(log: FailureLog): Boolean
    }
}