// Copyright 2021 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. #import #include "Crashlytics/Crashlytics/Components/FIRCLSContext.h" #import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h" #import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h" #import "Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h" #import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h" #import "Crashlytics/UnitTests/Mocks/FIRCLSMockReportUploader.h" #import "Crashlytics/UnitTests/Mocks/FIRCLSTempMockFileManager.h" #define METADATA_FORMAT \ (@"{\"identity\":{\"generator\":\"Crashlytics iOS " \ @"SDK/" \ @"7.7.0\",\"display_version\":\"7.7.0\",\"build_version\":\"7.7.0\",\"started_at\":%ld," \ @"\"session_id\":\"%@\",\"install_id\":\"CA3EE845-594A-4AAD-8343-B0379559E5C5\",\"beta_" \ @"token\":\"\",\"absolute_log_timestamps\":true}}\n{\"host\":{\"model\":\"iOS Simulator " \ @"(iPhone)\",\"machine\":\"x86_64\",\"cpu\":\"Intel(R) Core(TM) i9-9880H CPU @ " \ @"2.30GHz\",\"os_build_version\":\"20D74\",\"os_display_version\":\"14.3.0\",\"platform\":" \ @"\"ios\",\"locale\":\"en_US\"}}\n{\"application\":{\"bundle_id\":\"com.google.firebase." \ @"quickstart.TestingNoAWS\",\"custom_bundle_id\":null,\"build_version\":\"131\",\"display_" \ @"version\":\"10.10.33\",\"extension_id\":null}}\n{\"executable\":{\"architecture\":\"x86_" \ @"64\",\"uuid\":\"6a082b52b92a36bdb766fda9049deb21\",\"base\":4471803904,\"size\":49152," \ @"\"encrypted\":false,\"minimum_sdk_version\":\"8.0.0\",\"built_sdk_version\":\"14.3.0\"}}") @interface FIRCLSExistingReportManagerTests : XCTestCase @property(nonatomic, strong) FIRCLSMockReportUploader *mockReportUploader; @property(nonatomic, strong) FIRCLSTempMockFileManager *fileManager; @property(nonatomic, strong) FIRCLSExistingReportManager *existingReportManager; @property(nonatomic, strong) FIRCLSManagerData *managerData; @end @implementation FIRCLSExistingReportManagerTests - (void)setUp { [super setUp]; FIRCLSContextBaseInit(); self.fileManager = [[FIRCLSTempMockFileManager alloc] init]; // Allow nil values only in tests #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" self.managerData = [[FIRCLSManagerData alloc] initWithGoogleAppID:@"TEST_GOOGLE_APP_ID" googleTransport:nil installations:nil analytics:nil fileManager:self.fileManager dataArbiter:nil settings:nil onDemandModel:nil]; #pragma clang diagnostic pop self.mockReportUploader = [[FIRCLSMockReportUploader alloc] initWithManagerData:self.managerData]; self.existingReportManager = [[FIRCLSExistingReportManager alloc] initWithManagerData:self.managerData reportUploader:self.mockReportUploader]; } - (void)tearDown { if ([[NSFileManager defaultManager] fileExistsAtPath:[self.fileManager rootPath]]) { assert([self.fileManager removeItemAtPath:[self.fileManager rootPath]]); } FIRCLSContextBaseDeinit(); [super tearDown]; } - (NSArray *)contentsOfActivePath { return [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.fileManager.activePath error:nil]; } #pragma mark - Helpers - (FIRCLSInternalReport *)createActiveReportWithID:(NSString *)reportID time:(time_t)time withEvents:(BOOL)withEvents { NSString *reportPath = [self.fileManager.activePath stringByAppendingPathComponent:reportID]; FIRCLSInternalReport *report = [[FIRCLSInternalReport alloc] initWithPath:reportPath executionIdentifier:reportID]; if (![self.fileManager createDirectoryAtPath:report.path]) { return nil; } NSString *metadata = [NSString stringWithFormat:METADATA_FORMAT, time, reportID]; if (![self createMetadata:metadata forReport:report]) { return nil; } if (withEvents) { [self createCrashExceptionFile:@"Content doesn't matter" forReport:report]; } return report; } - (BOOL)createFileWithContents:(NSString *)contents atPath:(NSString *)path { return [self.fileManager.underlyingFileManager createFileAtPath:path contents:[contents dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; } - (BOOL)createMetadata:(NSString *)value forReport:(FIRCLSInternalReport *)report { return [self createFileWithContents:value atPath:[report metadataPath]]; } - (BOOL)createCrashExceptionFile:(NSString *)value forReport:(FIRCLSInternalReport *)report { return [self createFileWithContents:value atPath:[report pathForContentFile:FIRCLSReportExceptionFile]]; } - (BOOL)reportPathAtIndex:(NSUInteger)index isReportID:(NSString *)reportID { return [[self.existingReportManager.existingUnemptyActiveReportPaths objectAtIndex:index] containsString:reportID]; } #pragma mark - Tests - (void)testNoReports { [self.existingReportManager collectExistingReports]; [self.existingReportManager.operationQueue waitUntilAllOperationsAreFinished]; // Reports without events should be deleted XCTAssertEqual([[self contentsOfActivePath] count], 0); XCTAssertEqual(self.existingReportManager.unsentReportsCount, 0); XCTAssertEqual(self.existingReportManager.newestUnsentReport, nil); XCTAssertEqual(self.existingReportManager.existingUnemptyActiveReportPaths.count, 0); } - (void)testReportNoEvents { [self createActiveReportWithID:@"report_A" time:12312 withEvents:NO]; [self createActiveReportWithID:@"report_B" time:12315 withEvents:NO]; [self.existingReportManager collectExistingReports]; [self.existingReportManager.operationQueue waitUntilAllOperationsAreFinished]; // Reports without events should be deleted XCTAssertEqual([[self contentsOfActivePath] count], 0); XCTAssertEqual(self.existingReportManager.unsentReportsCount, 0); XCTAssertEqual(self.existingReportManager.newestUnsentReport, nil); XCTAssertEqual(self.existingReportManager.existingUnemptyActiveReportPaths.count, 0); } - (void)testUnsentReportsUnderLimit { [self createActiveReportWithID:@"report_A" time:12312 withEvents:YES]; [self createActiveReportWithID:@"report_B" time:12315 withEvents:YES]; [self createActiveReportWithID:@"report_C" time:31533 withEvents:YES]; [self createActiveReportWithID:@"report_D" time:63263 withEvents:YES]; [self.existingReportManager collectExistingReports]; [self.existingReportManager.operationQueue waitUntilAllOperationsAreFinished]; // Reports with events should be kept if there's less than MAX_UNSENT_REPORTS reports XCTAssertEqual([[self contentsOfActivePath] count], FIRCLSMaxUnsentReports); XCTAssertEqual(self.existingReportManager.unsentReportsCount, FIRCLSMaxUnsentReports); XCTAssertEqual(self.existingReportManager.existingUnemptyActiveReportPaths.count, FIRCLSMaxUnsentReports); // Newest report based on started_at timestamp XCTAssertEqualObjects(self.existingReportManager.newestUnsentReport.reportID, @"report_D"); } /** * When we go over the limit of FIRCLSMaxUnsentReports, we delete any reports over the limit to * ensure performant startup and prevent disk space from filling up. Delete starting with the oldest * first. */ - (void)testUnsentReportsOverLimit { // Create a bunch of reports starting at different times [self createActiveReportWithID:@"report_A" time:12312 withEvents:YES]; [self createActiveReportWithID:@"report_B" time:12315 withEvents:YES]; [self createActiveReportWithID:@"report_C" time:31533 withEvents:YES]; [self createActiveReportWithID:@"report_D" time:63263 withEvents:YES]; [self createActiveReportWithID:@"report_E" time:33263 withEvents:YES]; [self createActiveReportWithID:@"report_F" time:43263 withEvents:YES]; [self createActiveReportWithID:@"report_G" time:77777 withEvents:YES]; [self createActiveReportWithID:@"report_H" time:13263 withEvents:YES]; [self createActiveReportWithID:@"report_I" time:61263 withEvents:YES]; [self.existingReportManager collectExistingReports]; [self.existingReportManager.operationQueue waitUntilAllOperationsAreFinished]; // Remove any reports over the limit, starting with the oldest XCTAssertEqual([[self contentsOfActivePath] count], FIRCLSMaxUnsentReports); XCTAssertEqual(self.existingReportManager.unsentReportsCount, FIRCLSMaxUnsentReports); XCTAssertEqual(self.existingReportManager.existingUnemptyActiveReportPaths.count, FIRCLSMaxUnsentReports); // Newest report based on started_at timestamp XCTAssertEqualObjects(self.existingReportManager.newestUnsentReport.reportID, @"report_G"); // Make sure we're sorting correctly and keeping the newest reports. XCTAssertEqual([self reportPathAtIndex:0 isReportID:@"report_G"], true); XCTAssertEqual([self reportPathAtIndex:1 isReportID:@"report_D"], true); XCTAssertEqual([self reportPathAtIndex:2 isReportID:@"report_I"], true); XCTAssertEqual([self reportPathAtIndex:3 isReportID:@"report_F"], true); } @end