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

Commit 3b87a685 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Simplify LogWtfHandlerRule and add tests" into main

parents 7a3746ea 5c06874d
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
    }
}