Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 15 additions & 21 deletions Crashlytics/UnitTests/FIRCLSSettingsTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -127,27 +127,33 @@
- (void)cacheSettingsWithGoogleAppID:(NSString *)googleAppID
currentTimestamp:(NSTimeInterval)currentTimestamp
expectedRemoveCount:(NSInteger)expectedRemoveCount {
self.fileManager.removeExpectation = [[XCTestExpectation alloc]
initWithDescription:@"FIRCLSMockFileManager.removeExpectation.cache"];
self.fileManager.removeCount = 0;
self.fileManager.expectedRemoveCount = expectedRemoveCount;

XCTestExpectation *expectation =
[self expectationForNotification:FIRCLSMockFileManagerDidRemoveItemNotification
object:nil
handler:nil];
expectation.expectedFulfillmentCount = expectedRemoveCount;

[self.settings cacheSettingsWithGoogleAppID:googleAppID currentTimestamp:currentTimestamp];

[self waitForExpectations:@[ self.fileManager.removeExpectation ] timeout:1];
[self waitForExpectations:@[ expectation ] timeout:16.0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The timeout for waiting on this expectation has been increased significantly to 16.0 seconds (and similarly to 14.0 seconds in reloadFromCacheWithGoogleAppID). While this is effective at preventing timeouts on slower CI environments and fixing flakiness, it's worth noting that it can also increase the overall test suite execution time. This is a reasonable and pragmatic approach to improve test stability, but if these operations aren't expected to take this long, it might be worth investigating if there's an underlying performance issue that could be addressed in the future.

}

- (void)reloadFromCacheWithGoogleAppID:(NSString *)googleAppID
currentTimestamp:(NSTimeInterval)currentTimestamp
expectedRemoveCount:(NSInteger)expectedRemoveCount {
self.fileManager.removeExpectation = [[XCTestExpectation alloc]
initWithDescription:@"FIRCLSMockFileManager.removeExpectation.reload"];
self.fileManager.removeCount = 0;
self.fileManager.expectedRemoveCount = expectedRemoveCount;

XCTestExpectation *expectation =
[self expectationForNotification:FIRCLSMockFileManagerDidRemoveItemNotification
object:nil
handler:nil];
expectation.expectedFulfillmentCount = expectedRemoveCount;

[self.settings reloadFromCacheWithGoogleAppID:googleAppID currentTimestamp:currentTimestamp];

[self waitForExpectations:@[ self.fileManager.removeExpectation ] timeout:5.0];
[self waitForExpectations:@[ expectation ] timeout:14.0];

Check failure on line 156 in Crashlytics/UnitTests/FIRCLSSettingsTests.m

View workflow job for this annotation

GitHub Actions / spm / spm (macos-14, Xcode_16.2, iOS)

testActivatedSettingsMissingCacheKey, Asynchronous wait failed: Exceeded timeout of 14 seconds, with unfulfilled expectations: "Expect notification 'FIRCLSMockFileManagerDidRemoveItemNotification' from any object".
}

- (void)testActivatedSettingsCached {
Expand Down Expand Up @@ -205,10 +211,6 @@
[self writeSettings:FIRCLSTestSettingsActivated error:&error];
XCTAssertNil(error, "%@", error);

// 1 delete for clearing the cache key, plus 2 for the deletes from reloading and clearing the
// cache and cache key
self.fileManager.expectedRemoveCount = 3;

NSTimeInterval currentTimestamp = [NSDate timeIntervalSinceReferenceDate];
[self.settings cacheSettingsWithGoogleAppID:TestGoogleAppID currentTimestamp:currentTimestamp];

Expand Down Expand Up @@ -238,10 +240,6 @@
[self writeSettings:FIRCLSTestSettingsActivated error:&error];
XCTAssertNil(error, "%@", error);

// 1 delete for clearing the cache key, plus 2 for the deletes from reloading and clearing the
// cache and cache key
self.fileManager.expectedRemoveCount = 3;

NSTimeInterval currentTimestamp = [NSDate timeIntervalSinceReferenceDate];
[self.settings cacheSettingsWithGoogleAppID:TestGoogleAppID currentTimestamp:currentTimestamp];

Expand Down Expand Up @@ -272,10 +270,6 @@
[self writeSettings:FIRCLSTestSettingsActivated error:&error];
XCTAssertNil(error, "%@", error);

// 1 delete for clearing the cache key, plus 2 for the deletes from reloading and clearing the
// cache and cache key
self.fileManager.expectedRemoveCount = 3;

NSTimeInterval currentTimestamp = [NSDate timeIntervalSinceReferenceDate];
[self.settings cacheSettingsWithGoogleAppID:TestGoogleAppID currentTimestamp:currentTimestamp];

Expand Down Expand Up @@ -387,7 +381,7 @@
// Cache them, and reload. Since it's corrupted we should delete it all
[self cacheSettingsWithGoogleAppID:TestGoogleAppID
currentTimestamp:currentTimestamp
expectedRemoveCount:2];
expectedRemoveCount:3];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we changed the expected count here without change any actual code logic?


// Should have default values because we deleted the cache and settingsDictionary
XCTAssertEqual(self.settings.isCacheExpired, YES);
Expand Down
3 changes: 3 additions & 0 deletions Crashlytics/UnitTests/FIRCLSUserDefaultsTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ - (void)setUp {

- (void)tearDown {
[_userDefaults removeAllObjects];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:_testKey1];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:_testKey2];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:_testKey3];
[super tearDown];
}

Expand Down
12 changes: 3 additions & 9 deletions Crashlytics/UnitTests/Mocks/FIRCLSMockFileManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,12 @@

#import <XCTest/XCTest.h>

@interface FIRCLSMockFileManager : FIRCLSFileManager
// Notification posted when an item is removed via `removeItemAtPath`.
extern NSNotificationName const FIRCLSMockFileManagerDidRemoveItemNotification;

// Number of calls to removeItemAtPath are expected for the unit test
@property(nonatomic) NSInteger expectedRemoveCount;
@interface FIRCLSMockFileManager : FIRCLSFileManager

// Incremented when a remove happens with removeItemAtPath
@property(nonatomic) NSInteger removeCount;

// Will be fulfilled when the expected number of removes have happened
// using removeItemAtPath
//
// Users should initialize this in their test.
@property(nonatomic, strong) XCTestExpectation *removeExpectation;

@end
87 changes: 49 additions & 38 deletions Crashlytics/UnitTests/Mocks/FIRCLSMockFileManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

#import "Crashlytics/UnitTests/Mocks/FIRCLSMockFileManager.h"

NSNotificationName const FIRCLSMockFileManagerDidRemoveItemNotification =
@"FIRCLSMockFileManagerDidRemoveItemNotification";

@interface FIRCLSMockFileManager ()

@property(nonatomic) NSMutableDictionary<NSString *, NSData *> *fileSystemDict;
Expand All @@ -34,68 +37,76 @@ - (instancetype)init {
}

- (BOOL)removeItemAtPath:(NSString *)path {
[self.fileSystemDict removeObjectForKey:path];

self.removeCount += 1;

// If we set up the expectation, and we went over the expected count or removes, fulfill the
// expectation
if (self.removeExpectation && self.removeCount >= self.expectedRemoveCount) {
[self.removeExpectation fulfill];
@synchronized(self) {
[self.fileSystemDict removeObjectForKey:path];
self.removeCount += 1;
}

[[NSNotificationCenter defaultCenter]
postNotificationName:FIRCLSMockFileManagerDidRemoveItemNotification
object:self];

return YES;
}

- (BOOL)fileExistsAtPath:(NSString *)path {
return self.fileSystemDict[path] != nil;
@synchronized(self) {
return self.fileSystemDict[path] != nil;
}
}

- (BOOL)createFileAtPath:(NSString *)path
contents:(NSData *)data
attributes:(NSDictionary<NSFileAttributeKey, id> *)attr {
self.fileSystemDict[path] = data;
@synchronized(self) {
self.fileSystemDict[path] = data;
}
return YES;
}

- (NSArray *)activePathContents {
NSMutableArray *pathsWithActive = [[NSMutableArray alloc] init];
for (NSString *path in [_fileSystemDict allKeys]) {
if ([path containsString:@"v5/reports/active"]) {
[pathsWithActive addObject:path];
@synchronized(self) {
NSMutableArray *pathsWithActive = [[NSMutableArray alloc] init];
for (NSString *path in [_fileSystemDict allKeys]) {
if ([path containsString:@"v5/reports/active"]) {
[pathsWithActive addObject:path];
}
}
return pathsWithActive;
}

return pathsWithActive;
}

- (NSData *)dataWithContentsOfFile:(NSString *)path {
return self.fileSystemDict[path];
@synchronized(self) {
return self.fileSystemDict[path];
}
}

- (void)enumerateFilesInDirectory:(NSString *)directory
usingBlock:(void (^)(NSString *filePath, NSString *extension))block {
NSArray<NSString *> *filteredPaths = [self.fileSystemDict.allKeys
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *path,
NSDictionary *bindings) {
return [path hasPrefix:directory];
}]];

for (NSString *path in filteredPaths) {
NSString *extension;
NSString *fullPath;

// Skip files that start with a dot. This is important, because if you try to move a .DS_Store
// file, it will fail if the target directory also has a .DS_Store file in it. Plus, its
// wasteful, because we don't care about dot files.
if ([path hasPrefix:@"."]) {
continue;
}

extension = [path pathExtension];
fullPath = [directory stringByAppendingPathComponent:path];
if (block) {
block(fullPath, extension);
@synchronized(self) {
NSArray<NSString *> *filteredPaths = [self.fileSystemDict.allKeys
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *path,
NSDictionary *bindings) {
return [path hasPrefix:directory];
}]];

for (NSString *path in filteredPaths) {
NSString *extension;
NSString *fullPath;

// Skip files that start with a dot. This is important, because if you try to move a
// .DS_Store file, it will fail if the target directory also has a .DS_Store file in
// it. Plus, it's wasteful, because we don't care about dot files.
if ([path hasPrefix:@"."]) {
continue;
}

extension = [path pathExtension];
fullPath = [directory stringByAppendingPathComponent:path];
if (block) {
block(fullPath, extension);
}
}
}
}
Expand Down
Loading