// Copyright 2020 Google LLC // // 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.google.firebase.crashlytics.internal.common; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.LARGEST_FILE_NAME_FIRST; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_APP_TAG; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_BEGIN_TAG; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_DEVICE_TAG; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_FATAL_TAG; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_NON_FATAL_TAG; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_OS_TAG; import static com.google.firebase.crashlytics.internal.common.CrashlyticsController.SESSION_USER_TAG; import static com.google.firebase.crashlytics.internal.proto.ClsFileOutputStream.IN_PROGRESS_SESSION_FILE_EXTENSION; import static com.google.firebase.crashlytics.internal.proto.ClsFileOutputStream.SESSION_FILE_EXTENSION; import static org.mockito.Mockito.*; import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.BatteryManager; import android.os.Bundle; import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.crashlytics.device.session.Crashlytics; import com.google.firebase.crashlytics.device.session.Crashlytics.Session; import com.google.firebase.crashlytics.internal.CrashlyticsNativeComponent; import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; import com.google.firebase.crashlytics.internal.MissingNativeComponent; import com.google.firebase.crashlytics.internal.NativeSessionFileProvider; import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger; import com.google.firebase.crashlytics.internal.network.HttpRequestFactory; import com.google.firebase.crashlytics.internal.persistence.FileStore; import com.google.firebase.crashlytics.internal.report.ReportManager; import com.google.firebase.crashlytics.internal.report.ReportUploader; import com.google.firebase.crashlytics.internal.report.model.Report; import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider; import com.google.firebase.crashlytics.internal.settings.TestSettingsData; import com.google.firebase.crashlytics.internal.settings.model.AppSettingsData; import com.google.firebase.crashlytics.internal.settings.model.FeaturesSettingsData; import com.google.firebase.crashlytics.internal.settings.model.SessionSettingsData; import com.google.firebase.crashlytics.internal.settings.model.SettingsData; import com.google.firebase.crashlytics.internal.unity.UnityVersionProvider; import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FilenameFilter; import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; public class CrashlyticsControllerTest extends CrashlyticsTestCase { private static final String GOOGLE_APP_ID = "google:app:id"; // Finds all directories other than the log file directory. private static final FileFilter NON_LOG_DIRECTORY_FILTER = new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory() && !pathname.getName().equals("log-files") && !pathname.getName().equals("report-persistence"); } }; private Context testContext; private IdManager idManager; private SettingsDataProvider testSettingsDataProvider; private FileStore mockFileStore; private File testFilesDirectory; private AppSettingsData appSettingsData; private SessionSettingsData sessionSettingsData; @Override protected void setUp() throws Exception { super.setUp(); testContext = getContext(); FirebaseInstanceIdInternal instanceIdMock = mock(FirebaseInstanceIdInternal.class); idManager = new IdManager(testContext, testContext.getPackageName(), instanceIdMock); BatteryIntentProvider.returnNull = false; // For each test case, create a new, random subdirectory to guarantee a clean slate for file // manipulation. testFilesDirectory = new File(testContext.getFilesDir(), UUID.randomUUID().toString()); testFilesDirectory.mkdirs(); mockFileStore = mock(FileStore.class); when(mockFileStore.getFilesDir()).thenReturn(testFilesDirectory); when(mockFileStore.getFilesDirPath()).thenReturn(testFilesDirectory.getPath()); final SettingsData testSettingsData = new TestSettingsData( 3, DataTransportState.REPORT_UPLOAD_VARIANT_LEGACY, DataTransportState.REPORT_UPLOAD_VARIANT_LEGACY); appSettingsData = testSettingsData.appData; sessionSettingsData = testSettingsData.sessionData; testSettingsDataProvider = mock(SettingsDataProvider.class); when(testSettingsDataProvider.getSettings()).thenReturn(testSettingsData); when(testSettingsDataProvider.getAppSettings()) .thenReturn(Tasks.forResult(testSettingsData.appData)); } @Override protected void tearDown() throws Exception { recursiveDelete(testFilesDirectory); super.tearDown(); } private static void recursiveDelete(File file) { if (file.isDirectory()) { for (File f : file.listFiles()) { recursiveDelete(f); } } file.delete(); } /** A convenience class for building CrashlyticsController instances for testing. */ private class ControllerBuilder { private DataCollectionArbiter dataCollectionArbiter; private ReportManager reportManager; private ReportUploader.Provider reportUploaderProvider; private CrashlyticsNativeComponent nativeComponent; private UnityVersionProvider unityVersionProvider; private AnalyticsEventLogger analyticsEventLogger; ControllerBuilder() { dataCollectionArbiter = mock(DataCollectionArbiter.class); when(dataCollectionArbiter.isAutomaticDataCollectionEnabled()).thenReturn(true); nativeComponent = new MissingNativeComponent(); unityVersionProvider = mock(UnityVersionProvider.class); when(unityVersionProvider.getUnityVersion()).thenReturn(null); analyticsEventLogger = mock(AnalyticsEventLogger.class); reportManager = null; } ControllerBuilder setDataCollectionArbiter(DataCollectionArbiter arbiter) { dataCollectionArbiter = arbiter; return this; } ControllerBuilder setReportUploaderProvider(ReportUploader.Provider provider) { reportUploaderProvider = provider; return this; } public ControllerBuilder setReportManager(ReportManager reportManager) { this.reportManager = reportManager; return this; } ControllerBuilder setReportUploader(ReportUploader uploader) { return setReportUploaderProvider( new ReportUploader.Provider() { @Override public ReportUploader createReportUploader(@NonNull AppSettingsData settingsData) { return uploader; } }); } public ControllerBuilder setNativeComponent(CrashlyticsNativeComponent nativeComponent) { this.nativeComponent = nativeComponent; return this; } public ControllerBuilder setUnityVersionProvider(UnityVersionProvider provider) { unityVersionProvider = provider; return this; } public ControllerBuilder setAnalyticsEventLogger(AnalyticsEventLogger logger) { analyticsEventLogger = logger; return this; } public CrashlyticsController build() { HttpRequestFactory mockRequestFactory = mock(HttpRequestFactory.class); CrashlyticsFileMarker crashMarker = new CrashlyticsFileMarker(CrashlyticsCore.CRASH_MARKER_FILE_NAME, mockFileStore); AppData appData = new AppData( GOOGLE_APP_ID, "buildId", "installerPackageName", "packageName", "versionCode", "versionName"); final CrashlyticsController controller = new CrashlyticsController( testContext.getApplicationContext(), new CrashlyticsBackgroundWorker(new SameThreadExecutorService()), mockRequestFactory, idManager, dataCollectionArbiter, mockFileStore, crashMarker, appData, reportManager, reportUploaderProvider, nativeComponent, unityVersionProvider, analyticsEventLogger, testSettingsDataProvider); return controller; } } private ControllerBuilder builder() { return new ControllerBuilder(); } /** Creates a new CrashlyticsController with default options and opens a session. */ private CrashlyticsController createController() { final CrashlyticsController controller = builder().build(); controller.openSession(); controller.cleanInvalidTempFiles(); return controller; } public void testLoggedExceptionsOnlyCaptureMainThread() throws Exception { final CrashlyticsController controller = createController(); controller.writeNonFatalException(Thread.currentThread(), new RuntimeException("Logged")); controller.doCloseSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertEquals(1, session.getEventsCount()); assertEquals(1, session.getEvents(0).getApp().getExecution().getThreadsCount()); } finally { if (fis != null) { fis.close(); } } } public void testFatalExceptionsCaptureAllThreads() throws Exception { final CrashlyticsController controller = createController(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertEquals(1, session.getEventsCount()); assertTrue(session.getEvents(0).getApp().getExecution().getThreadsCount() > 1); } finally { if (fis != null) { fis.close(); } } } public void testSessionTrimmingResultsInAppropriateCompletedSessionsCount() throws Exception { final CrashlyticsController controller = createController(); final int maxSessions = 2; final int totalSessions = maxSessions + 2; for (int i = 0; i < totalSessions; i++) { controller.writeNonFatalException(Thread.currentThread(), new RuntimeException("Logged")); controller.doCloseSessions(sessionSettingsData.maxCustomExceptionEvents); controller.openSession(); } assertEquals(totalSessions, controller.listCompleteSessionFiles().length); controller.trimSessionFiles(maxSessions); assertEquals(maxSessions, controller.listCompleteSessionFiles().length); } public void testSessionTrimmingFavorsCrashSessions() throws Exception { final CrashlyticsController controller = createController(); final int numNonFatalSessions = 2; final int numFatalSessions = 2; // Create some fatal sessions. for (int i = 0; i < numFatalSessions; i++) { controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException()); } // Create some more recent nonfatal sessions. for (int i = 0; i < numNonFatalSessions; i++) { controller.writeNonFatalException(Thread.currentThread(), new RuntimeException("Logged")); controller.doCloseSessions(sessionSettingsData.maxCustomExceptionEvents); controller.openSession(); } final int totalSessions = numNonFatalSessions + numFatalSessions; assertEquals(totalSessions, controller.listCompleteSessionFiles().length); assertTrue(totalSessions > numFatalSessions); controller.trimSessionFiles(numFatalSessions); final File[] trimmedSessions = controller.listCompleteSessionFiles(); assertEquals(numFatalSessions, trimmedSessions.length); // Make sure all remaining sessions are fatal. for (File f : trimmedSessions) { FileInputStream fis = null; try { fis = new FileInputStream(f); final Session session = Session.parseFrom(fis); assertEquals(1, session.getEventsCount()); assertEquals("crash", session.getEvents(0).getType()); } finally { if (fis != null) { fis.close(); } } } } public void testNativeCrashDataCausesNativeReport() throws Exception { final File testDir = new File(testContext.getFilesDir(), "testNative"); testDir.mkdir(); final File minidump = new File(testDir, "crash.dmp"); final File binaryImages = new File(testDir, "crash.maps"); final File metadata = new File(testDir, "crash.device_info"); final File session = new File(testDir, "session.json"); final File app = new File(testDir, "app.json"); final File device = new File(testDir, "device.json"); final File os = new File(testDir, "os.json"); TestUtils.writeStringToFile("minidump", minidump); TestUtils.writeStringToFile("binaryLibs", binaryImages); TestUtils.writeStringToFile("metadata", metadata); TestUtils.writeStringToFile("session", session); TestUtils.writeStringToFile("app", app); TestUtils.writeStringToFile("device", device); TestUtils.writeStringToFile("os", os); final CrashlyticsNativeComponent mockNativeComponent = mock(CrashlyticsNativeComponent.class); when(mockNativeComponent.hasCrashDataForSession(anyString())).thenReturn(true); when(mockNativeComponent.getSessionFileProvider(anyString())) .thenReturn( new NativeSessionFileProvider() { @Override public File getMinidumpFile() { return minidump; } @Override public File getBinaryImagesFile() { return binaryImages; } @Override public File getMetadataFile() { return metadata; } @Override public File getSessionFile() { return session; } @Override public File getAppFile() { return app; } @Override public File getDeviceFile() { return device; } @Override public File getOsFile() { return os; } }); final CrashlyticsController controller = builder().setNativeComponent(mockNativeComponent).build(); controller.openSession(); // Create a new session, leaving the previous session to be finalized. controller.openSession(); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] nativeDirectories = controller.listNativeSessionFileDirectories(); assertEquals(1, nativeDirectories.length); final File[] processedFiles = nativeDirectories[0].listFiles(); assertEquals( "Unexpected number of files found: " + Arrays.toString(processedFiles), 7, processedFiles.length); } public void testMissingNativeComponentCausesNoReports() { final CrashlyticsController controller = createController(); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listNativeSessionFileDirectories(); assertEquals(0, sessionFiles.length); } public void testCleanupSessionsWithInvalidParts() throws Exception { final String invalidSessionId = new CLSUUID(idManager).toString(); final FilenameFilter invalidSessionPartFilter = new FilenameFilter() { @Override public boolean accept(File f, String name) { return name.startsWith(invalidSessionId) && name.endsWith(SESSION_FILE_EXTENSION); } }; // These are the files we expect to get quarantined final File sdkDir = mockFileStore.getFilesDir(); new File(sdkDir, invalidSessionId + SESSION_BEGIN_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, invalidSessionId + SESSION_APP_TAG + IN_PROGRESS_SESSION_FILE_EXTENSION) .createNewFile(); new File(sdkDir, invalidSessionId + SESSION_DEVICE_TAG + SESSION_FILE_EXTENSION) .createNewFile(); new File(sdkDir, invalidSessionId + SESSION_FATAL_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, invalidSessionId + SESSION_NON_FATAL_TAG + SESSION_FILE_EXTENSION) .createNewFile(); new File(sdkDir, invalidSessionId + SESSION_OS_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, invalidSessionId + SESSION_USER_TAG + SESSION_FILE_EXTENSION).createNewFile(); final String validSessionId = new CLSUUID(idManager).toString(); final FilenameFilter validSessionPartFilter = new FilenameFilter() { @Override public boolean accept(File f, String name) { return name.startsWith(validSessionId) && name.endsWith(SESSION_FILE_EXTENSION); } }; // Finds all directories other than the log file directory. // These are the files we expect to get left alone new File(sdkDir, validSessionId + SESSION_BEGIN_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, validSessionId + SESSION_APP_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, validSessionId + SESSION_DEVICE_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, validSessionId + SESSION_OS_TAG + SESSION_FILE_EXTENSION).createNewFile(); final CrashlyticsController controller = createController(); controller.cleanInvalidTempFiles(); // Invalid files were moved, valid files were left alone assertEquals(0, sdkDir.listFiles(invalidSessionPartFilter).length); assertEquals(4, sdkDir.listFiles(validSessionPartFilter).length); assertEquals(0, sdkDir.listFiles(NON_LOG_DIRECTORY_FILTER).length); } public void testCleanupSessions_deletesSessionWithNoBinaryImages() throws Exception { final String invalidSessionId = new CLSUUID(idManager).toString(); final FilenameFilter invalidSessionPartFilter = new FilenameFilter() { @Override public boolean accept(File f, String name) { return name.startsWith(invalidSessionId) && name.endsWith(SESSION_FILE_EXTENSION); } }; // These are the files we expect to get deleted final File sdkDir = mockFileStore.getFilesDir(); new File(sdkDir, invalidSessionId + SESSION_BEGIN_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, invalidSessionId + SESSION_APP_TAG + SESSION_FILE_EXTENSION).createNewFile(); final File eventFileMissingBinaryImages = new File( sdkDir, invalidSessionId + CrashlyticsController.SESSION_EVENT_MISSING_BINARY_IMGS_TAG + SESSION_FILE_EXTENSION); eventFileMissingBinaryImages.createNewFile(); final String validSessionId = new CLSUUID(idManager).toString(); final FilenameFilter validSessionPartFilter = new FilenameFilter() { @Override public boolean accept(File f, String name) { return name.startsWith(validSessionId) && name.endsWith(SESSION_FILE_EXTENSION); } }; // These are the files we expect to get left alone new File(sdkDir, validSessionId + SESSION_BEGIN_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, validSessionId + SESSION_APP_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, validSessionId + SESSION_DEVICE_TAG + SESSION_FILE_EXTENSION).createNewFile(); new File(sdkDir, validSessionId + SESSION_OS_TAG + SESSION_FILE_EXTENSION).createNewFile(); assertEquals(3, sdkDir.listFiles(invalidSessionPartFilter).length); final CrashlyticsController controller = createController(); controller.cleanInvalidTempFiles(); // Invalid files were deleted, valid files were left alone assertEquals(0, sdkDir.listFiles(invalidSessionPartFilter).length); assertEquals(4, sdkDir.listFiles(validSessionPartFilter).length); assertEquals(0, sdkDir.listFiles(NON_LOG_DIRECTORY_FILTER).length); } public void testLargestFileNameFirst() { final File smaller = new File(new CLSUUID(idManager).toString()); final File larger = new File(new CLSUUID(idManager).toString()); final File[] expectedOrder = new File[] {larger, smaller}; final File[] testOrder = new File[] {smaller, larger}; Arrays.sort(testOrder, LARGEST_FILE_NAME_FIRST); assertTrue(Arrays.equals(expectedOrder, testOrder)); } // TODO: There's only ever one open session now that we can close sessions while offline. // Is that the behavior we want? /* public void testMaxOpenSessions() throws Exception { // This test relies on not having any settings because of no internet connection final SettingsDataProvider nullSettingsProvider = mock(SettingsDataProvider.class); Mockito.when(nullSettingsProvider.getSettingsData()).thenReturn(null); final CrashlyticsController controller = builder().build(); final int CRASH_COUNT = 12; final int expectedCount = MAX_OPEN_SESSIONS + 1; for (int i = 0; i < CRASH_COUNT; i++) { controller.handleUncaughtException( nullSettingsProvider, Thread.currentThread(), new RuntimeException()); } final int openSessions = controller.listSessionBeginFiles().length; assertEquals(expectedCount, openSessions); } */ // TODO: MW 2016-10-04 Restore this test once the exception handler is separate. /* * Not my favorite kind of test b/c it involves timing... but timing/responsiveness is what we're trying * to test, so thems the breaks. * public void testBigBacklogOfLoggedExceptionsIgnored() throws InterruptedException { // Used to block the test until the exception handling is complete. final CountDownLatch latch = new CountDownLatch(1); final UncaughtExceptionHandler origHandler = new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable ex) { // When the exception gets passed to the original handler, the app should quit. // Release the latch so that the test can continue latch.countDown(); } }; final DefaultCrashlyticsController controller = new DefaultCrashlyticsController(crashlyticsCore, new CrashlyticsExecutorServiceWrapper(Executors.newSingleThreadExecutor()), idManager, mockFileStore, mockUnityVersionProvider); // Queue up a large number of non-fatal exceptions to be processed, such that they would // take the handler a while to get through. final int NON_FATAL_COUNT = 1000; for (int i = 0; i < NON_FATAL_COUNT; i++) { controller.writeNonFatalException(Thread.currentThread(), new RuntimeException()); } // We expect the fatal exception to halt the processing of the backlog of non-fatal // exceptions, and bring things to a halt immediately. If not, we'd expect processing them // to take longer than the timeout we're providing. If we throw an InterruptedException here // and fail the test, it's because we wouldn't have allowed the Android app to crash quickly // enough to prevent an ANR. controller.handleUncaughtException(Thread.currentThread(), new RuntimeException()); // An ANR happens after 5 seconds, so we need to be done faster than that. latch.await(3, TimeUnit.SECONDS); }*/ /** * Crashing should shut down the executor service, but we don't want further calls that would use * it to throw exceptions! */ public void testLoggedExceptionsAfterCrashOk() { final CrashlyticsController controller = builder().build(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException()); // This should not throw. controller.writeNonFatalException(Thread.currentThread(), new RuntimeException()); } /** * Crashing should shut down the executor service, but we don't want further calls that would use * it to throw exceptions! */ public void testLogStringAfterCrashOk() { final CrashlyticsController controller = builder().build(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException()); // This should not throw. controller.writeToLog(System.currentTimeMillis(), "Hi"); } /** * Crashing should shut down the executor service, but we don't want further calls that would use * it to throw exceptions! */ public void testFinalizeSessionAfterCrashOk() throws Exception { final CrashlyticsController controller = builder().build(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException()); // This should not throw. controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); } private static <T> T await(Task<T> task) throws Exception { return Tasks.await(task, 5, TimeUnit.SECONDS); } public void testUploadWithNoReports() throws Exception { ReportManager mockReportManager = mock(ReportManager.class); when(mockReportManager.areReportsAvailable()).thenReturn(false); ReportUploader uploader = mock(ReportUploader.class); final ControllerBuilder builder = builder(); builder.setReportManager(mockReportManager); builder.setReportUploader(uploader); final CrashlyticsController controller = builder.build(); Task<Void> task = controller.submitAllReports(1.0f, testSettingsDataProvider.getAppSettings()); await(task); verify(mockReportManager).areReportsAvailable(); verifyNoMoreInteractions(mockReportManager); verifyZeroInteractions(uploader); } public void testUploadWithDataCollectionAlwaysEnabled() throws Exception { final File reportFile = new File(testFilesDirectory, "reportFile.cls"); Report mockReport = mock(Report.class); List<Report> mockReportList = Arrays.asList(mockReport); ReportManager mockReportManager = mock(ReportManager.class); when(mockReport.getType()).thenReturn(Report.Type.JAVA); when(mockReport.getFile()).thenReturn(reportFile); when(mockReportManager.areReportsAvailable()).thenReturn(true); when(mockReportManager.findReports()).thenReturn(mockReportList); ReportUploader mockUploader = mock(ReportUploader.class); final ControllerBuilder builder = builder(); builder.setReportManager(mockReportManager); builder.setReportUploader(mockUploader); final CrashlyticsController controller = builder.build(); Task<Void> task = controller.submitAllReports(1.0f, testSettingsDataProvider.getAppSettings()); await(task); verify(mockReportManager).areReportsAvailable(); verify(mockReportManager).findReports(); verifyNoMoreInteractions(mockReportManager); verify(mockUploader).uploadReportsAsync(mockReportList, true, 1.0f); verifyNoMoreInteractions(mockUploader); verify(mockReport).getType(); verify(mockReport).getFile(); verifyNoMoreInteractions(mockReport); } public void testUploadDisabledThenOptIn() throws Exception { final File reportFile = new File(testFilesDirectory, "reportFile.cls"); Report mockReport = mock(Report.class); List<Report> mockReportList = Arrays.asList(mockReport); ReportManager mockReportManager = mock(ReportManager.class); when(mockReport.getType()).thenReturn(Report.Type.JAVA); when(mockReport.getFile()).thenReturn(reportFile); when(mockReportManager.areReportsAvailable()).thenReturn(true); when(mockReportManager.findReports()).thenReturn(mockReportList); DataCollectionArbiter arbiter = mock(DataCollectionArbiter.class); when(arbiter.isAutomaticDataCollectionEnabled()).thenReturn(false); when(arbiter.waitForDataCollectionPermission()) .thenReturn(new TaskCompletionSource<Void>().getTask()); when(arbiter.waitForAutomaticDataCollectionEnabled()) .thenReturn(new TaskCompletionSource<Void>().getTask()); ReportUploader mockUploader = mock(ReportUploader.class); final ControllerBuilder builder = builder(); builder.setDataCollectionArbiter(arbiter); builder.setReportManager(mockReportManager); builder.setReportUploader(mockUploader); final CrashlyticsController controller = builder.build(); Task<Void> task = controller.submitAllReports(1.0f, testSettingsDataProvider.getAppSettings()); await(controller.sendUnsentReports()); await(task); verify(mockReportManager).areReportsAvailable(); verify(mockReportManager).findReports(); verifyNoMoreInteractions(mockReportManager); verify(mockUploader).uploadReportsAsync(mockReportList, true, 1.0f); verifyNoMoreInteractions(mockUploader); verify(mockReport).getType(); verify(mockReport).getFile(); verifyNoMoreInteractions(mockReport); } public void testUploadDisabledThenOptOut() throws Exception { final File reportFile = new File(testFilesDirectory, "reportFile.cls"); Report mockReport = mock(Report.class); List<Report> mockReportList = Arrays.asList(mockReport); ReportManager mockReportManager = mock(ReportManager.class); when(mockReport.getType()).thenReturn(Report.Type.JAVA); when(mockReport.getFile()).thenReturn(reportFile); when(mockReportManager.areReportsAvailable()).thenReturn(true); when(mockReportManager.findReports()).thenReturn(mockReportList); ReportUploader mockUploader = mock(ReportUploader.class); DataCollectionArbiter arbiter = mock(DataCollectionArbiter.class); when(arbiter.isAutomaticDataCollectionEnabled()).thenReturn(false); when(arbiter.waitForAutomaticDataCollectionEnabled()) .thenReturn(new TaskCompletionSource<Void>().getTask()); final ControllerBuilder builder = builder(); builder.setDataCollectionArbiter(arbiter); builder.setReportManager(mockReportManager); builder.setReportUploader(mockUploader); final CrashlyticsController controller = builder.build(); Task<Void> task = controller.submitAllReports(1.0f, testSettingsDataProvider.getAppSettings()); await(controller.deleteUnsentReports()); await(task); verify(mockReportManager).areReportsAvailable(); verify(mockReportManager).findReports(); verify(mockReportManager).deleteReports(mockReportList); verifyNoMoreInteractions(mockReportManager); verifyZeroInteractions(mockUploader); verifyZeroInteractions(mockReport); } public void testUploadDisabledThenEnabled() throws Exception { final File reportFile = new File(testFilesDirectory, "reportFile.cls"); reportFile.createNewFile(); Report mockReport = mock(Report.class); List<Report> mockReportList = Arrays.asList(mockReport); ReportManager mockReportManager = mock(ReportManager.class); when(mockReport.getType()).thenReturn(Report.Type.JAVA); when(mockReport.getFile()).thenReturn(reportFile); when(mockReportManager.areReportsAvailable()).thenReturn(true); when(mockReportManager.findReports()).thenReturn(mockReportList); ReportUploader mockUploader = mock(ReportUploader.class); // Mock the DataCollectionArbiter dependencies. final String PREFS_NAME = CommonUtils.SHARED_PREFS_NAME; final String PREFS_KEY = "firebase_crashlytics_collection_enabled"; SharedPreferences.Editor mockEditor = mock(SharedPreferences.Editor.class); when(mockEditor.putBoolean(PREFS_KEY, true)).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); SharedPreferences mockPrefs = mock(SharedPreferences.class); when(mockPrefs.contains(PREFS_KEY)).thenReturn(true); when(mockPrefs.getBoolean(PREFS_KEY, true)).thenReturn(false); when(mockPrefs.edit()).thenReturn(mockEditor); Context mockContext = mock(Context.class); when(mockContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)).thenReturn(mockPrefs); FirebaseApp app = mock(FirebaseApp.class); when(app.getApplicationContext()).thenReturn(mockContext); // Use a real DataCollectionArbiter to test its switching behavior. DataCollectionArbiter arbiter = new DataCollectionArbiter(app); final ControllerBuilder builder = builder(); builder.setDataCollectionArbiter(arbiter); builder.setReportManager(mockReportManager); builder.setReportUploader(mockUploader); final CrashlyticsController controller = builder.build(); Task<Void> task = controller.submitAllReports(1.0f, testSettingsDataProvider.getAppSettings()); arbiter.setCrashlyticsDataCollectionEnabled(true); await(task); verify(mockReportManager).areReportsAvailable(); verify(mockReportManager).findReports(); verifyNoMoreInteractions(mockReportManager); verify(mockUploader).uploadReportsAsync(mockReportList, true, 1.0f); verifyNoMoreInteractions(mockUploader); verify(mockReport).getType(); verify(mockReport).getFile(); verifyNoMoreInteractions(mockReport); } public void testCrashTimeUploadAddsOrganizationId() throws Exception { final ControllerBuilder builder = builder(); final CrashlyticsController controller = builder.build(); controller.openSession(); controller.cleanInvalidTempFiles(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertEquals(appSettingsData.organizationId, session.getApp().getOrganization().getClsId()); } finally { if (fis != null) { fis.close(); } } } public void testStartupUploadAddsOrganizationId() throws Exception { Report mockReport = mock(Report.class); ReportManager mockReportManager = mock(ReportManager.class); when(mockReportManager.areReportsAvailable()).thenReturn(true); ReportUploader mockUploader = mock(ReportUploader.class); final ControllerBuilder builder = builder(); builder.setReportManager(mockReportManager); builder.setReportUploader(mockUploader); final CrashlyticsController controller = builder.build(); controller.openSession(); controller.cleanInvalidTempFiles(); controller.writeNonFatalException(Thread.currentThread(), new RuntimeException("Logged")); controller.doCloseSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); final File reportFile = sessionFiles[0]; List<Report> mockReportList = Arrays.asList(mockReport); when(mockReport.getType()).thenReturn(Report.Type.JAVA); when(mockReport.getFile()).thenReturn(reportFile); when(mockReportManager.findReports()).thenReturn(mockReportList); Task<Void> task = controller.submitAllReports(1.0f, testSettingsDataProvider.getAppSettings()); await(task); FileInputStream fis = null; try { fis = new FileInputStream(reportFile); final Session session = Session.parseFrom(fis); assertEquals(appSettingsData.organizationId, session.getApp().getOrganization().getClsId()); } finally { if (fis != null) { fis.close(); } } } public void testUnityVersionAppearsInDeveloperPlatformFields() throws Exception { final String expectedUnityVersion = "1.0.0"; final UnityVersionProvider unityVersionProvider = new UnityVersionProvider() { @Override public String getUnityVersion() { return expectedUnityVersion; } }; final CrashlyticsController controller = builder().setUnityVersionProvider(unityVersionProvider).build(); controller.openSession(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertEquals(1, session.getEventsCount()); assertEquals("Unity", session.getApp().getDevelopmentPlatform()); assertEquals(expectedUnityVersion, session.getApp().getDevelopmentPlatformVersion()); } finally { if (fis != null) { fis.close(); } } } public void testNoDeveloperPlatformFields_whenUnityIsMissing() throws Exception { final CrashlyticsController controller = createController(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertEquals(1, session.getEventsCount()); assertEquals("", session.getApp().getDevelopmentPlatform()); assertEquals("", session.getApp().getDevelopmentPlatformVersion()); } finally { if (fis != null) { fis.close(); } } } public void testFirebaseAnalyticsEventIsSent_whenSettingFalseClientFlagTrue() throws Exception { final AnalyticsEventLogger mockFirebaseAnalyticsLogger = mock(AnalyticsEventLogger.class); final CrashlyticsController controller = builder().setAnalyticsEventLogger(mockFirebaseAnalyticsLogger).build(); controller.openSession(); controller.handleUncaughtException( firebaseCrashlyticsSettingsProvider(), Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); assertFirebaseAnalyticsCrashEvent(mockFirebaseAnalyticsLogger); } public void testFirebaseAnalyticsEventIsSent_whenSettingTrueClientFlagFalse() throws Exception { final AnalyticsEventLogger mockFirebaseAnalyticsLogger = mock(AnalyticsEventLogger.class); final CrashlyticsController controller = builder().setAnalyticsEventLogger(mockFirebaseAnalyticsLogger).build(); controller.openSession(); controller.handleUncaughtException( firebaseCrashlyticsSettingsProvider(), Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); assertFirebaseAnalyticsCrashEvent(mockFirebaseAnalyticsLogger); } public void testFirebaseAnalyticsEventIsSent_whenSettingTrueClientFlagTrue() throws Exception { final AnalyticsEventLogger mockFirebaseAnalyticsLogger = mock(AnalyticsEventLogger.class); final CrashlyticsController controller = builder().setAnalyticsEventLogger(mockFirebaseAnalyticsLogger).build(); controller.openSession(); controller.handleUncaughtException( firebaseCrashlyticsSettingsProvider(), Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); assertFirebaseAnalyticsCrashEvent(mockFirebaseAnalyticsLogger); } public void testGeneratorAndAnalyzerVersion() throws Exception { final CrashlyticsController controller = createController(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertEquals(1, session.getAnalyzer()); assertEquals(Crashlytics.GeneratorType.ANDROID_SDK, session.getGeneratorType()); } finally { if (fis != null) { fis.close(); } } } public void testBatteryLevel() throws Exception { final CrashlyticsController controller = createController(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertTrue(session.getEvents(0).getDevice().hasBatteryLevel()); assertTrue(session.getEvents(0).getDevice().getBatteryLevel() > 0.0); assertEquals(2, session.getEvents(0).getDevice().getBatteryVelocity()); } finally { if (fis != null) { fis.close(); } } } public void testBatteryLevelNotWritten_whenBatteryIntentIsNull() throws Exception { BatteryIntentProvider.returnNull = true; final CrashlyticsController controller = createController(); controller.handleUncaughtException( testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal")); controller.finalizeSessions(sessionSettingsData.maxCustomExceptionEvents); final File[] sessionFiles = controller.listCompleteSessionFiles(); assertEquals(1, sessionFiles.length); FileInputStream fis = null; try { fis = new FileInputStream(sessionFiles[0]); final Session session = Session.parseFrom(fis); assertFalse(session.getEvents(0).getDevice().hasBatteryLevel()); assertEquals(1, session.getEvents(0).getDevice().getBatteryVelocity()); } finally { if (fis != null) { fis.close(); } } } private SettingsDataProvider firebaseCrashlyticsSettingsProvider() { final FeaturesSettingsData featureData = new FeaturesSettingsData(false); final SettingsData testSettingsData = new TestSettingsData(); final SettingsData settingsData = new SettingsData( 1, testSettingsData.appData, testSettingsData.sessionData, featureData, 2, 1000); final SettingsDataProvider settingsProvider = mock(SettingsDataProvider.class); Mockito.when(settingsProvider.getSettings()).thenReturn(settingsData); Mockito.when(settingsProvider.getAppSettings()) .thenReturn(Tasks.forResult(settingsData.appData)); return settingsProvider; } private void assertFirebaseAnalyticsCrashEvent(AnalyticsEventLogger mockFirebaseAnalyticsLogger) { final ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class); Mockito.verify(mockFirebaseAnalyticsLogger, Mockito.times(1)) .logEvent( Mockito.eq(CrashlyticsController.FIREBASE_APPLICATION_EXCEPTION), captor.capture()); assertEquals( CrashlyticsController.FIREBASE_CRASH_TYPE_FATAL, captor.getValue().getInt(CrashlyticsController.FIREBASE_CRASH_TYPE)); assertTrue(captor.getValue().getLong(CrashlyticsController.FIREBASE_TIMESTAMP) > 0); } @Override public Context getContext() { // Return a context wrapper that will allow us to override the behavior of registering // the receiver for battery changed events. return new ContextWrapper(super.getContext()) { @Override public Context getApplicationContext() { return this; } @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { // For the BatteryIntent, use test values to avoid breaking from emulator changes. if (filter.hasAction(Intent.ACTION_BATTERY_CHANGED)) { // If we ever call this with a receiver, it will be broken. assertNull(receiver); return BatteryIntentProvider.getBatteryIntent(); } return getBaseContext().registerReceiver(receiver, filter); } }; } private static class BatteryIntentProvider { public static boolean returnNull; public static Intent getBatteryIntent() { if (returnNull) { return null; } final Intent intent = new Intent(); // Set the battery level to 25% and charging. intent.putExtra(BatteryManager.EXTRA_LEVEL, 50); intent.putExtra(BatteryManager.EXTRA_SCALE, 200); intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING); return intent; } } }