Merge "Ensure log buffers are dumped to file on uncaught exceptions" into tm-dev

This commit is contained in:
Justin Weir
2022-05-03 11:38:59 +00:00
committed by Android (Google) Code Review
9 changed files with 212 additions and 77 deletions

View File

@@ -31,13 +31,13 @@ import android.widget.Toast;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler; import java.lang.Thread.UncaughtExceptionHandler;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
/** /**
* @see Plugin * @see Plugin
@@ -61,7 +61,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
public PluginManagerImpl(Context context, public PluginManagerImpl(Context context,
PluginActionManager.Factory actionManagerFactory, PluginActionManager.Factory actionManagerFactory,
boolean debuggable, boolean debuggable,
Optional<UncaughtExceptionHandler> defaultHandlerOptional, UncaughtExceptionPreHandlerManager preHandlerManager,
PluginEnabler pluginEnabler, PluginEnabler pluginEnabler,
PluginPrefs pluginPrefs, PluginPrefs pluginPrefs,
List<String> privilegedPlugins) { List<String> privilegedPlugins) {
@@ -72,9 +72,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
mPluginPrefs = pluginPrefs; mPluginPrefs = pluginPrefs;
mPluginEnabler = pluginEnabler; mPluginEnabler = pluginEnabler;
PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler( preHandlerManager.registerHandler(new PluginExceptionHandler());
defaultHandlerOptional);
Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
} }
public boolean isDebuggable() { public boolean isDebuggable() {
@@ -266,20 +264,12 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
} }
private class PluginExceptionHandler implements UncaughtExceptionHandler { private class PluginExceptionHandler implements UncaughtExceptionHandler {
private final Optional<UncaughtExceptionHandler> mExceptionHandlerOptional;
private PluginExceptionHandler( private PluginExceptionHandler() {}
Optional<UncaughtExceptionHandler> exceptionHandlerOptional) {
mExceptionHandlerOptional = exceptionHandlerOptional;
}
@Override @Override
public void uncaughtException(Thread thread, Throwable throwable) { public void uncaughtException(Thread thread, Throwable throwable) {
if (SystemProperties.getBoolean("plugin.debugging", false)) { if (SystemProperties.getBoolean("plugin.debugging", false)) {
Throwable finalThrowable = throwable;
mExceptionHandlerOptional.ifPresent(
handler -> handler.uncaughtException(thread, finalThrowable));
return; return;
} }
// Search for and disable plugins that may have been involved in this crash. // Search for and disable plugins that may have been involved in this crash.
@@ -297,11 +287,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
if (disabledAny) { if (disabledAny) {
throwable = new CrashWhilePluginActiveException(throwable); throwable = new CrashWhilePluginActiveException(throwable);
} }
// Run the normal exception handler so we can crash and cleanup our state.
Throwable finalThrowable = throwable;
mExceptionHandlerOptional.ifPresent(
handler -> handler.uncaughtException(thread, finalThrowable));
} }
private boolean checkStack(Throwable throwable) { private boolean checkStack(Throwable throwable) {

View File

@@ -0,0 +1,71 @@
package com.android.systemui.shared.system
import android.util.Log
import java.lang.Thread.UncaughtExceptionHandler
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
import javax.inject.Singleton
/**
* Sets the global (static var in Thread) uncaught exception pre-handler to an implementation that
* delegates to each item in a list of registered UncaughtExceptionHandlers.
*/
@Singleton
class UncaughtExceptionPreHandlerManager @Inject constructor() {
private val handlers: MutableList<UncaughtExceptionHandler> = CopyOnWriteArrayList()
private val globalUncaughtExceptionPreHandler = GlobalUncaughtExceptionHandler()
/**
* Adds an exception pre-handler to the list of handlers. If this has not yet set the global
* (static var in Thread) uncaught exception pre-handler yet, it will do so.
*/
fun registerHandler(handler: UncaughtExceptionHandler) {
checkGlobalHandlerSetup()
addHandler(handler)
}
/**
* Verifies that the global handler is set in Thread. If not, sets is up.
*/
private fun checkGlobalHandlerSetup() {
val currentHandler = Thread.getUncaughtExceptionPreHandler()
if (currentHandler != globalUncaughtExceptionPreHandler) {
if (currentHandler is GlobalUncaughtExceptionHandler) {
throw IllegalStateException("Two UncaughtExceptionPreHandlerManagers created")
}
currentHandler?.let { addHandler(it) }
Thread.setUncaughtExceptionPreHandler(globalUncaughtExceptionPreHandler)
}
}
/**
* Adds a handler if it has not already been added, preserving order.
*/
private fun addHandler(it: UncaughtExceptionHandler) {
if (it !in handlers) {
handlers.add(it)
}
}
/**
* Calls uncaughtException on all registered handlers, catching and logging any new exceptions.
*/
fun handleUncaughtException(thread: Thread?, throwable: Throwable?) {
for (handler in handlers) {
try {
handler.uncaughtException(thread, throwable)
} catch (e: Exception) {
Log.wtf("Uncaught exception pre-handler error", e)
}
}
}
/**
* UncaughtExceptionHandler impl that will be set as Thread's pre-handler static variable.
*/
inner class GlobalUncaughtExceptionHandler : UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread?, throwable: Throwable?) {
handleUncaughtException(thread, throwable)
}
}
}

View File

@@ -71,6 +71,7 @@ public class SystemUIService extends Service {
// Finish initializing dump logic // Finish initializing dump logic
mLogBufferFreezer.attach(mBroadcastDispatcher); mLogBufferFreezer.attach(mBroadcastDispatcher);
mDumpHandler.init();
// If configured, set up a battery notification // If configured, set up a battery notification
if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) { if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) {

View File

@@ -25,6 +25,7 @@ import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
import java.io.PrintWriter import java.io.PrintWriter
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -82,8 +83,20 @@ class DumpHandler @Inject constructor(
private val context: Context, private val context: Context,
private val dumpManager: DumpManager, private val dumpManager: DumpManager,
private val logBufferEulogizer: LogBufferEulogizer, private val logBufferEulogizer: LogBufferEulogizer,
private val startables: MutableMap<Class<*>, Provider<CoreStartable>> private val startables: MutableMap<Class<*>, Provider<CoreStartable>>,
private val uncaughtExceptionPreHandlerManager: UncaughtExceptionPreHandlerManager
) { ) {
/**
* Registers an uncaught exception handler
*/
fun init() {
uncaughtExceptionPreHandlerManager.registerHandler { _, e ->
if (e is Exception) {
logBufferEulogizer.record(e)
}
}
}
/** /**
* Dump the diagnostics! Behavior can be controlled via [args]. * Dump the diagnostics! Behavior can be controlled via [args].
*/ */

View File

@@ -16,8 +16,6 @@
package com.android.systemui.plugins; package com.android.systemui.plugins;
import static com.android.systemui.util.concurrency.GlobalConcurrencyModule.PRE_HANDLER;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@@ -32,12 +30,12 @@ import com.android.systemui.shared.plugins.PluginInstance;
import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.shared.plugins.PluginManagerImpl; import com.android.systemui.shared.plugins.PluginManagerImpl;
import com.android.systemui.shared.plugins.PluginPrefs; import com.android.systemui.shared.plugins.PluginPrefs;
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
import com.android.systemui.util.concurrency.GlobalConcurrencyModule; import com.android.systemui.util.concurrency.GlobalConcurrencyModule;
import com.android.systemui.util.concurrency.ThreadFactory; import com.android.systemui.util.concurrency.ThreadFactory;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.inject.Named; import javax.inject.Named;
@@ -107,13 +105,12 @@ public abstract class PluginsModule {
Context context, Context context,
PluginActionManager.Factory instanceManagerFactory, PluginActionManager.Factory instanceManagerFactory,
@Named(PLUGIN_DEBUG) boolean debug, @Named(PLUGIN_DEBUG) boolean debug,
@Named(PRE_HANDLER) UncaughtExceptionPreHandlerManager preHandlerManager,
Optional<Thread.UncaughtExceptionHandler> uncaughtExceptionHandlerOptional,
PluginEnabler pluginEnabler, PluginEnabler pluginEnabler,
PluginPrefs pluginPrefs, PluginPrefs pluginPrefs,
@Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins) { @Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins) {
return new PluginManagerImpl(context, instanceManagerFactory, debug, return new PluginManagerImpl(context, instanceManagerFactory, debug,
uncaughtExceptionHandlerOptional, pluginEnabler, pluginPrefs, preHandlerManager, pluginEnabler, pluginPrefs,
privilegedPlugins); privilegedPlugins);
} }

View File

@@ -23,11 +23,9 @@ import android.os.Looper;
import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.dagger.qualifiers.UiBackground;
import java.util.Optional;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import dagger.Binds; import dagger.Binds;
@@ -110,11 +108,4 @@ public abstract class GlobalConcurrencyModule {
@Binds @Binds
@Singleton @Singleton
public abstract Execution provideExecution(ExecutionImpl execution); public abstract Execution provideExecution(ExecutionImpl execution);
/** */
@Provides
@Named(PRE_HANDLER)
public static Optional<Thread.UncaughtExceptionHandler> providesUncaughtExceptionHandler() {
return Optional.ofNullable(Thread.getUncaughtExceptionPreHandler());
}
} }

View File

@@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -37,6 +38,8 @@ class DumpHandlerTest : SysuiTestCase() {
@Mock @Mock
private lateinit var logBufferEulogizer: LogBufferEulogizer private lateinit var logBufferEulogizer: LogBufferEulogizer
@Mock
private lateinit var exceptionHandlerManager: UncaughtExceptionPreHandlerManager
@Mock @Mock
private lateinit var pw: PrintWriter private lateinit var pw: PrintWriter
@@ -59,7 +62,13 @@ class DumpHandlerTest : SysuiTestCase() {
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
dumpHandler = DumpHandler(mContext, dumpManager, logBufferEulogizer, mutableMapOf()) dumpHandler = DumpHandler(
mContext,
dumpManager,
logBufferEulogizer,
mutableMapOf(),
exceptionHandlerManager
)
} }
@Test @Test

View File

@@ -15,6 +15,7 @@ package com.android.systemui.shared.plugins;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -27,65 +28,62 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableLooper.RunWithLooper;
import androidx.test.filters.SmallTest;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.annotations.ProvidesInterface; import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.lang.Thread.UncaughtExceptionHandler; import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Optional; import java.util.List;
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner.class) @RunWith(AndroidTestingRunner.class)
@RunWithLooper @RunWithLooper
public class PluginManagerTest extends SysuiTestCase { public class PluginManagerTest extends SysuiTestCase {
private static final String PRIVILEGED_PACKAGE = "com.android.systemui"; private static final String PRIVILEGED_PACKAGE = "com.android.systemui";
private PluginActionManager.Factory mMockFactory; @Mock PluginActionManager.Factory mMockFactory;
private PluginActionManager<TestPlugin> mMockPluginInstance; @Mock PluginActionManager<TestPlugin> mMockPluginInstance;
private PluginManagerImpl mPluginManager; @Mock PluginListener<TestPlugin> mMockListener;
private PluginListener<TestPlugin> mMockListener; @Mock PackageManager mMockPackageManager;
private PackageManager mMockPackageManager; @Mock PluginEnabler mMockPluginEnabler;
private PluginEnabler mPluginEnabler; @Mock PluginPrefs mMockPluginPrefs;
private PluginPrefs mPluginPrefs; @Mock UncaughtExceptionPreHandlerManager mMockExPreHandlerManager;
private UncaughtExceptionHandler mRealExceptionHandler; @Captor ArgumentCaptor<UncaughtExceptionHandler> mExPreHandlerCaptor;
private UncaughtExceptionHandler mMockExceptionHandler;
private PluginManagerImpl mPluginManager;
private UncaughtExceptionHandler mPluginExceptionHandler; private UncaughtExceptionHandler mPluginExceptionHandler;
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
mRealExceptionHandler = Thread.getUncaughtExceptionPreHandler(); MockitoAnnotations.initMocks(this);
mMockExceptionHandler = mock(UncaughtExceptionHandler.class);
mMockFactory = mock(PluginActionManager.Factory.class);
mMockPluginInstance = mock(PluginActionManager.class);
mPluginEnabler = mock(PluginEnabler.class);
mPluginPrefs = mock(PluginPrefs.class);
when(mMockFactory.create(any(), any(), eq(TestPlugin.class), anyBoolean(), anyBoolean())) when(mMockFactory.create(any(), any(), eq(TestPlugin.class), anyBoolean(), anyBoolean()))
.thenReturn(mMockPluginInstance); .thenReturn(mMockPluginInstance);
mMockPackageManager = mock(PackageManager.class);
mPluginManager = new PluginManagerImpl( mPluginManager = new PluginManagerImpl(
getContext(), mMockFactory, true, getContext(), mMockFactory, true,
Optional.of(mMockExceptionHandler), mPluginEnabler, mMockExPreHandlerManager, mMockPluginEnabler,
mPluginPrefs, new ArrayList<>()); mMockPluginPrefs, new ArrayList<>());
captureExceptionHandler();
resetExceptionHandler();
mMockListener = mock(PluginListener.class);
} }
@Test @Test
@@ -108,9 +106,9 @@ public class PluginManagerTest extends SysuiTestCase {
public void testNonDebuggable_nonPrivileged() { public void testNonDebuggable_nonPrivileged() {
mPluginManager = new PluginManagerImpl( mPluginManager = new PluginManagerImpl(
getContext(), mMockFactory, false, getContext(), mMockFactory, false,
Optional.of(mMockExceptionHandler), mPluginEnabler, mMockExPreHandlerManager, mMockPluginEnabler,
mPluginPrefs, new ArrayList<>()); mMockPluginPrefs, new ArrayList<>());
resetExceptionHandler(); captureExceptionHandler();
String sourceDir = "myPlugin"; String sourceDir = "myPlugin";
ApplicationInfo applicationInfo = new ApplicationInfo(); ApplicationInfo applicationInfo = new ApplicationInfo();
@@ -127,9 +125,9 @@ public class PluginManagerTest extends SysuiTestCase {
public void testNonDebuggable_privilegedPackage() { public void testNonDebuggable_privilegedPackage() {
mPluginManager = new PluginManagerImpl( mPluginManager = new PluginManagerImpl(
getContext(), mMockFactory, false, getContext(), mMockFactory, false,
Optional.of(mMockExceptionHandler), mPluginEnabler, mMockExPreHandlerManager, mMockPluginEnabler,
mPluginPrefs, Collections.singletonList(PRIVILEGED_PACKAGE)); mMockPluginPrefs, Collections.singletonList(PRIVILEGED_PACKAGE));
resetExceptionHandler(); captureExceptionHandler();
String sourceDir = "myPlugin"; String sourceDir = "myPlugin";
ApplicationInfo privilegedApplicationInfo = new ApplicationInfo(); ApplicationInfo privilegedApplicationInfo = new ApplicationInfo();
@@ -154,9 +152,6 @@ public class PluginManagerTest extends SysuiTestCase {
verify(mMockPluginInstance, Mockito.atLeastOnce()).checkAndDisable( verify(mMockPluginInstance, Mockito.atLeastOnce()).checkAndDisable(
ArgumentCaptor.forClass(String.class).capture()); ArgumentCaptor.forClass(String.class).capture());
verify(mMockPluginInstance, Mockito.never()).disableAll(); verify(mMockPluginInstance, Mockito.never()).disableAll();
verify(mMockExceptionHandler).uncaughtException(
ArgumentCaptor.forClass(Thread.class).capture(),
ArgumentCaptor.forClass(Throwable.class).capture());
} }
@Test @Test
@@ -169,9 +164,6 @@ public class PluginManagerTest extends SysuiTestCase {
verify(mMockPluginInstance, Mockito.atLeastOnce()).checkAndDisable( verify(mMockPluginInstance, Mockito.atLeastOnce()).checkAndDisable(
ArgumentCaptor.forClass(String.class).capture()); ArgumentCaptor.forClass(String.class).capture());
verify(mMockPluginInstance).disableAll(); verify(mMockPluginInstance).disableAll();
verify(mMockExceptionHandler).uncaughtException(
ArgumentCaptor.forClass(Thread.class).capture(),
ArgumentCaptor.forClass(Throwable.class).capture());
} }
@Test @Test
@@ -186,13 +178,15 @@ public class PluginManagerTest extends SysuiTestCase {
intent.setData(Uri.parse("package://" + testComponent.flattenToString())); intent.setData(Uri.parse("package://" + testComponent.flattenToString()));
mPluginManager.onReceive(mContext, intent); mPluginManager.onReceive(mContext, intent);
verify(nm).cancel(eq(testComponent.getClassName()), eq(SystemMessage.NOTE_PLUGIN)); verify(nm).cancel(eq(testComponent.getClassName()), eq(SystemMessage.NOTE_PLUGIN));
verify(mPluginEnabler).setDisabled(testComponent, PluginEnabler.DISABLED_INVALID_VERSION); verify(mMockPluginEnabler).setDisabled(testComponent,
PluginEnabler.DISABLED_INVALID_VERSION);
} }
private void resetExceptionHandler() { private void captureExceptionHandler() {
mPluginExceptionHandler = Thread.getUncaughtExceptionPreHandler(); verify(mMockExPreHandlerManager, atLeastOnce()).registerHandler(
// Set back the real exception handler so the test can crash if it wants to. mExPreHandlerCaptor.capture());
Thread.setUncaughtExceptionPreHandler(mRealExceptionHandler); List<UncaughtExceptionHandler> allValues = mExPreHandlerCaptor.getAllValues();
mPluginExceptionHandler = allValues.get(allValues.size() - 1);
} }
@ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION) @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)

View File

@@ -0,0 +1,74 @@
package com.android.systemui.shared.system
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.only
import org.mockito.Mockito.any
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.lang.Thread.UncaughtExceptionHandler
@SmallTest
class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
@Mock
private lateinit var mockHandler: UncaughtExceptionHandler
@Mock
private lateinit var mockHandler2: UncaughtExceptionHandler
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
Thread.setUncaughtExceptionPreHandler(null)
preHandlerManager = UncaughtExceptionPreHandlerManager()
}
@Test
fun registerHandler_registersOnceOnly() {
preHandlerManager.registerHandler(mockHandler)
preHandlerManager.registerHandler(mockHandler)
preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
verify(mockHandler, only()).uncaughtException(any(), any())
}
@Test
fun registerHandler_setsUncaughtExceptionPreHandler() {
Thread.setUncaughtExceptionPreHandler(null)
preHandlerManager.registerHandler(mockHandler)
assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
}
@Test
fun registerHandler_preservesOriginalHandler() {
Thread.setUncaughtExceptionPreHandler(mockHandler)
preHandlerManager.registerHandler(mockHandler2)
preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
verify(mockHandler, only()).uncaughtException(any(), any())
}
@Test
fun registerHandler_toleratesHandlersThatThrow() {
`when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
preHandlerManager.registerHandler(mockHandler2)
preHandlerManager.registerHandler(mockHandler)
preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
verify(mockHandler2, only()).uncaughtException(any(), any())
verify(mockHandler, only()).uncaughtException(any(), any())
}
@Test
fun registerHandler_doesNotSetUpTwice() {
UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
assertThrows(IllegalStateException::class.java) {
preHandlerManager.registerHandler(mockHandler)
}
}
}