| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/mac/exception_processor.h" |
| |
| #import <Cocoa/Cocoa.h> |
| #include <stddef.h> |
| #include <sys/wait.h> |
| |
| #include "base/mac/os_crash_dumps.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| class ExceptionProcessorTest : public testing::Test { |
| public: |
| ExceptionProcessorTest() { |
| features_.InitWithFeatures({kForceCrashOnExceptions}, {}); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList features_; |
| }; |
| |
| void RaiseExceptionInRunLoop() { |
| CFRunLoopRef run_loop = CFRunLoopGetCurrent(); |
| |
| CFRunLoopPerformBlock(run_loop, kCFRunLoopCommonModes, ^{ |
| [NSException raise:@"ThrowExceptionInRunLoop" format:@""]; |
| }); |
| CFRunLoopPerformBlock(run_loop, kCFRunLoopCommonModes, ^{ |
| CFRunLoopStop(run_loop); |
| }); |
| CFRunLoopRun(); |
| } |
| |
| void ThrowExceptionInRunLoop() { |
| base::mac::DisableOSCrashDumps(); |
| InstallObjcExceptionPreprocessor(); |
| |
| RaiseExceptionInRunLoop(); |
| |
| fprintf(stderr, "TEST FAILED\n"); |
| exit(1); |
| } |
| |
| // Tests that when the preprocessor is installed, exceptions thrown from |
| // a runloop callout are made fatal, so that the stack trace is useful. |
| TEST_F(ExceptionProcessorTest, ThrowExceptionInRunLoop) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| EXPECT_DEATH(ThrowExceptionInRunLoop(), |
| ".*FATAL:.*exception_processor\\.mm.*" |
| "Terminating from Objective-C exception:.*"); |
| } |
| |
| void ThrowAndCatchExceptionInRunLoop() { |
| base::mac::DisableOSCrashDumps(); |
| InstallObjcExceptionPreprocessor(); |
| |
| CFRunLoopRef run_loop = CFRunLoopGetCurrent(); |
| CFRunLoopPerformBlock(run_loop, kCFRunLoopCommonModes, ^{ |
| @try { |
| [NSException raise:@"ObjcExceptionPreprocessCaught" format:@""]; |
| } @catch (id exception) { |
| } |
| }); |
| |
| CFRunLoopPerformBlock(run_loop, kCFRunLoopCommonModes, ^{ |
| CFRunLoopStop(run_loop); |
| }); |
| |
| CFRunLoopRun(); |
| |
| fprintf(stderr, "TEST PASS\n"); |
| exit(0); |
| } |
| |
| // Tests that exceptions can still be caught when the preprocessor is enabled. |
| TEST_F(ExceptionProcessorTest, ThrowAndCatchExceptionInRunLoop) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| EXPECT_EXIT(ThrowAndCatchExceptionInRunLoop(), |
| [](int exit_code) -> bool { |
| return WEXITSTATUS(exit_code) == 0; |
| }, |
| ".*TEST PASS.*"); |
| } |
| |
| void ThrowExceptionFromSelector() { |
| base::mac::DisableOSCrashDumps(); |
| InstallObjcExceptionPreprocessor(); |
| |
| NSException* exception = [NSException exceptionWithName:@"ThrowFromSelector" |
| reason:@"" |
| userInfo:nil]; |
| |
| [exception performSelector:@selector(raise) withObject:nil afterDelay:0.1]; |
| |
| [[NSRunLoop currentRunLoop] runUntilDate: |
| [NSDate dateWithTimeIntervalSinceNow:10]]; |
| |
| fprintf(stderr, "TEST FAILED\n"); |
| exit(1); |
| } |
| |
| TEST_F(ExceptionProcessorTest, ThrowExceptionFromSelector) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| EXPECT_DEATH(ThrowExceptionFromSelector(), |
| ".*FATAL:.*exception_processor\\.mm.*" |
| "Terminating from Objective-C exception:.*"); |
| } |
| |
| void ThrowInNotificationObserver() { |
| base::mac::DisableOSCrashDumps(); |
| InstallObjcExceptionPreprocessor(); |
| |
| NSNotification* notification = |
| [NSNotification notificationWithName:@"TestExceptionInObserver" |
| object:nil]; |
| |
| NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| [center addObserverForName:[notification name] |
| object:nil |
| queue:nil |
| usingBlock:^(NSNotification*) { |
| [NSException raise:@"ThrowInNotificationObserver" |
| format:@""]; |
| }]; |
| |
| [center performSelector:@selector(postNotification:) |
| withObject:notification |
| afterDelay:0]; |
| |
| [[NSRunLoop currentRunLoop] runUntilDate: |
| [NSDate dateWithTimeIntervalSinceNow:10]]; |
| |
| fprintf(stderr, "TEST FAILED\n"); |
| exit(1); |
| } |
| |
| TEST_F(ExceptionProcessorTest, ThrowInNotificationObserver) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| EXPECT_DEATH(ThrowInNotificationObserver(), |
| ".*FATAL:.*exception_processor\\.mm.*" |
| "Terminating from Objective-C exception:.*"); |
| } |
| |
| void ThrowExceptionInRunLoopWithoutProcessor() { |
| base::mac::DisableOSCrashDumps(); |
| UninstallObjcExceptionPreprocessor(); |
| |
| @try { |
| RaiseExceptionInRunLoop(); |
| } @catch (id exception) { |
| fprintf(stderr, "TEST PASS\n"); |
| exit(0); |
| } |
| |
| fprintf(stderr, "TEST FAILED\n"); |
| exit(1); |
| } |
| |
| // Under LSAN this dies from leaking the run loop instead of how we expect it to |
| // die, so the exit code is wrong. |
| #if defined(LEAK_SANITIZER) |
| #define MAYBE_ThrowExceptionInRunLoopWithoutProcessor \ |
| DISABLED_ThrowExceptionInRunLoopWithoutProcessor |
| #else |
| #define MAYBE_ThrowExceptionInRunLoopWithoutProcessor \ |
| ThrowExceptionInRunLoopWithoutProcessor |
| #endif |
| // Tests basic exception handling when the preprocessor is disabled. |
| TEST_F(ExceptionProcessorTest, MAYBE_ThrowExceptionInRunLoopWithoutProcessor) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| EXPECT_EXIT(ThrowExceptionInRunLoopWithoutProcessor(), |
| [](int exit_code) -> bool { |
| return WEXITSTATUS(exit_code) == 0; |
| }, |
| ".*TEST PASS.*"); |
| } |