// 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. #import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h" #import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.h" #import #import #import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h" #import #import "FirebasePerformance/Tests/Unit/FPRTestCase.h" /** Registers and returns an instance of a custom subclass of UIViewController. */ static UIViewController *FPRCustomViewController(NSString *className, BOOL isViewLoaded) { Class customClass = NSClassFromString(className); if (!customClass) { // Register the class if it does not already exist. customClass = objc_allocateClassPair([UIViewController class], className.UTF8String, 0); objc_registerClassPair(customClass); } UIViewController *customVC = [[customClass alloc] init]; if (isViewLoaded) { [customVC view]; } return customVC; } /** Test UINavigationController subclass. */ @interface FPRTestNavigationViewController : UINavigationController @end @implementation FPRTestNavigationViewController @end /** Test UITabBarController subclass. */ @interface FPRTestTabBarController : UITabBarController @end @implementation FPRTestTabBarController @end /** Test UISplitViewController subclass. */ @interface FPRTestSplitViewController : UISplitViewController @end @implementation FPRTestSplitViewController @end /** Test UIPageViewController. */ @interface FPRTestPageViewController : UIPageViewController @end @implementation FPRTestPageViewController @end @interface FPRScreenTraceTrackerTest : FPRTestCase /** The FPRScreenTraceTracker instance that's being used for a given test. */ @property(nonatomic, nullable) FPRScreenTraceTracker *tracker; /** The dispatch group a test should wait for completion on before asserting behavior under test. */ @property(nonatomic, nullable) dispatch_group_t dispatchGroupToWaitOn; @end @implementation FPRScreenTraceTrackerTest - (void)setUp { [super setUp]; FIRPerformance *performance = [FIRPerformance sharedInstance]; [performance setDataCollectionEnabled:YES]; self.tracker = [[FPRScreenTraceTracker alloc] init]; self.tracker.displayLink.paused = YES; self.dispatchGroupToWaitOn = self.tracker.screenTraceTrackerDispatchGroup; } - (void)tearDown { [super tearDown]; FIRPerformance *performance = [FIRPerformance sharedInstance]; [performance setDataCollectionEnabled:NO]; self.tracker = nil; self.dispatchGroupToWaitOn = nil; } /** Tests that shared instance returns the same instance. */ - (void)testSingleton { FPRScreenTraceTracker *trackerOne = [FPRScreenTraceTracker sharedInstance]; FPRScreenTraceTracker *trackerTwo = [FPRScreenTraceTracker sharedInstance]; XCTAssertEqual(trackerOne, trackerTwo); // Check that it's the same instance. } /** Tests that the atomic counters are initialized to zero during init. */ - (void)testCountersInitToZero { FPRScreenTraceTracker *tracker = [[FPRScreenTraceTracker alloc] init]; XCTAssertEqual(tracker.frozenFramesCount, 0); XCTAssertEqual(tracker.slowFramesCount, 0); XCTAssertEqual(tracker.totalFramesCount, 0); } /** Tests that viewControllerDidAppear starts a trace. */ - (void)testViewControllerDidAppearStartsATraceForVCWithLoadedView { UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 1); NSString *expectedTraceName = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController]; XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController]); FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertEqualObjects(createdTrace.name, expectedTraceName); XCTAssertFalse(createdTrace.isCompleteAndValid); } /** Tests that the trace is not created when data collection is disabled */ - (void)testTraceCreationDisabledWhenDataCollectionDisabled { @autoreleasepool { BOOL dataCollectionEnabled = [FIRPerformance sharedInstance].dataCollectionEnabled; [[FIRPerformance sharedInstance] setDataCollectionEnabled:NO]; UIViewController *newVCInstance = FPRCustomViewController(@"MyModule.UIFancyViewController", YES); [self.tracker viewControllerDidAppear:newVCInstance]; // objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own // autorelesepool. Without the autoreleasepool, the ViewController instance is not released // in a timely manner and this test becomes flaky. FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:newVCInstance]; XCTAssertNil(createdTrace); // Clean up. [self.tracker viewControllerDidDisappear:newVCInstance]; newVCInstance = nil; [[FIRPerformance sharedInstance] setDataCollectionEnabled:dataCollectionEnabled]; } } /** Tests that the trace is named correctly in case of Swift classes which are of the format * ModuleName.ClassName. */ - (void)testUnprefixedClassName { @autoreleasepool { UIViewController *newVCInstance = FPRCustomViewController(@"MyModule.UIFancyViewController", YES); [self.tracker viewControllerDidAppear:newVCInstance]; NSString *expectedTraceName = @"_st_UIFancyViewController"; // objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own // autorelesepool. Without the autoreleasepool, the ViewController instance is not released // in a timely manner and this test becomes flaky. FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:newVCInstance]; XCTAssertEqualObjects(createdTrace.name, expectedTraceName); createdTrace = nil; // Clean up. [self.tracker viewControllerDidDisappear:newVCInstance]; newVCInstance = nil; } } /** Tests that the module name length is not factored into truncating the screen trace name in case * of Swift classes. */ - (void)testDoesNotTruncateClassNameExtraLongSwiftModuleName { NSUInteger valueGreaterThanMaxTraceLength = kFPRMaxNameLength + 10; NSMutableString *extraLongModuleName = [[NSMutableString alloc] initWithCapacity:valueGreaterThanMaxTraceLength]; for (int i = 0; i < valueGreaterThanMaxTraceLength; ++i) { [extraLongModuleName appendString:@"a"]; } XCTAssertEqual(extraLongModuleName.length, valueGreaterThanMaxTraceLength); NSString *swiftClassName = [NSString stringWithFormat:@"%@.%@", extraLongModuleName, @"MyViewController"]; NSString *expectedTraceName = @"_st_MyViewController"; @autoreleasepool { UIViewController *newVCInstance = FPRCustomViewController(swiftClassName, YES); [self.tracker viewControllerDidAppear:newVCInstance]; // objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own // autorelesepool. Without the autoreleasepool, the ViewController instance is not released // in a timely manner and this test becomes flaky. FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:newVCInstance]; XCTAssertEqualObjects(createdTrace.name, expectedTraceName); createdTrace = nil; // Clean up. [self.tracker viewControllerDidDisappear:newVCInstance]; newVCInstance = nil; } } /** Tests that if a Swift class name pushes the screen trace name beyond the max trace name length, * the screen trace name is truncated. */ - (void)testTruncatesExtraLongSwiftClassName { NSUInteger valueGreaterThanMaxTraceLength = kFPRMaxNameLength + 10; NSMutableString *extraLongClassName = [[NSMutableString alloc] init]; for (int i = 0; i < valueGreaterThanMaxTraceLength; ++i) { [extraLongClassName appendString:@"a"]; } XCTAssertEqual(extraLongClassName.length, valueGreaterThanMaxTraceLength); NSString *swiftClassName = [NSString stringWithFormat:@"%@.%@", @"MyModule", extraLongClassName]; @autoreleasepool { UIViewController *newVCInstance = FPRCustomViewController(swiftClassName, YES); [self.tracker viewControllerDidAppear:newVCInstance]; // objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own // autorelesepool. Without the autoreleasepool, the ViewController instance is not released // in a timely manner and this test becomes flaky. FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:newVCInstance]; XCTAssertEqual(createdTrace.name.length, kFPRMaxNameLength); createdTrace = nil; // Clean up. [self.tracker viewControllerDidDisappear:newVCInstance]; newVCInstance = nil; } } /** Tests that if an ObjC class name pushes the screen trace name beyond the max trace name length, * the screen trace name is truncated. */ - (void)testTruncatesExtraLongObjCClassName { NSUInteger valueGreaterThanMaxTraceLength = kFPRMaxNameLength + 10; NSMutableString *extraLongClassName = [[NSMutableString alloc] init]; for (int i = 0; i < valueGreaterThanMaxTraceLength; ++i) { [extraLongClassName appendString:@"a"]; } XCTAssertEqual(extraLongClassName.length, valueGreaterThanMaxTraceLength); @autoreleasepool { UIViewController *newVCInstance = FPRCustomViewController(extraLongClassName, YES); [self.tracker viewControllerDidAppear:newVCInstance]; // objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own // autorelesepool. Without the autoreleasepool, the ViewController instance is not released // in a timely manner and this test becomes flaky. FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:newVCInstance]; XCTAssertEqual(createdTrace.name.length, kFPRMaxNameLength); createdTrace = nil; // Clean up. [self.tracker viewControllerDidDisappear:newVCInstance]; newVCInstance = nil; } } /** Tests that a viewController isn't retained by the ScreenTraceTracker. */ - (void)testViewControllerIsHeldWeaklyByTheScreenTraceTracker { __block UIViewController *newVCInstance = nil; __weak UIViewController *weakVCReference = nil; @autoreleasepool { newVCInstance = [[UIViewController alloc] init]; [newVCInstance view]; // Loads the view so that a screen trace is created for it. [self.tracker viewControllerDidAppear:newVCInstance]; [self.tracker viewControllerDidDisappear:newVCInstance]; weakVCReference = newVCInstance; newVCInstance = nil; } XCTAssertNil(weakVCReference); } /** Tests that viewControllerDidDisappear stops a trace. */ - (void)testViewControllerDidDisappearStopsATrace { // First screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); NSString *expectedTraceName = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController]; FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertNotNil(createdTrace); XCTAssertEqualObjects(expectedTraceName, createdTrace.name); // First screen disappears. [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertTrue(createdTrace.isCompleteAndValid); } /** Tests that viewControllerDidAppear starts multiple traces if multiple view controllers with the * same class appear one after the other. */ - (void)testViewControllerDidAppearStartsMultipleScreenTracesForSameClassIfNeeded { // First screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; // Second screen appears, first screen is still visible. UIViewController *testViewController2 = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController2]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); NSString *expectedTraceNameOne = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController]; XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController]); FIRTrace *traceForScreenOne = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertEqualObjects(traceForScreenOne.name, expectedTraceNameOne); NSString *expectedTraceNameTwo = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController2]; XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController2]); FIRTrace *traceForScreenTwo = [self.tracker.activeScreenTraces objectForKey:testViewController2]; XCTAssertEqualObjects(traceForScreenTwo.name, expectedTraceNameTwo); // Test that they're different instances. XCTAssertNotEqual(traceForScreenOne, traceForScreenTwo); XCTAssertEqualObjects(traceForScreenOne.name, traceForScreenTwo.name); } /** Tests that viewControllerDidAppear starts multiple traces if multiple view controllers with * different classes appear one after the other. */ - (void)testViewControllerDidAppearStartsMultipleScreenTracesForDifferentClassIfNeeded { // First screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; // Second screen appears, first screen is still visible. UIViewController *testViewController2 = FPRCustomViewController(@"FPRTestViewController", YES); [self.tracker viewControllerDidAppear:testViewController2]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); NSString *expectedTraceNameOne = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController]; XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController]); FIRTrace *traceForScreenOne = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertEqualObjects(traceForScreenOne.name, expectedTraceNameOne); NSString *expectedTraceNameTwo = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController2]; XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController2]); FIRTrace *traceForScreenTwo = [self.tracker.activeScreenTraces objectForKey:testViewController2]; XCTAssertEqualObjects(traceForScreenTwo.name, expectedTraceNameTwo); XCTAssertNotEqual(traceForScreenOne, traceForScreenTwo); // Test that they're different instances. XCTAssertNotEqualObjects(traceForScreenOne.name, traceForScreenTwo.name); } /** Tests that viewControllerDidDisappear stops the correct trace when multiple traces are present. */ - (void)testViewControllerDidDisappearStopsCorrectTraceWhenMultiplePresent { // First screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; // Second screen appears, first screen is still visible. UIViewController *testViewController2 = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController2]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 2); FIRTrace *traceForScreenOne = [self.tracker.activeScreenTraces objectForKey:testViewController]; FIRTrace *traceForScreenTwo = [self.tracker.activeScreenTraces objectForKey:testViewController2]; XCTAssertFalse(traceForScreenOne.isCompleteAndValid); XCTAssertFalse(traceForScreenTwo.isCompleteAndValid); [self.tracker viewControllerDidDisappear:testViewController2]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertFalse(traceForScreenOne.isCompleteAndValid); XCTAssertTrue(traceForScreenTwo.isCompleteAndValid); } /** Tests that viewControllerDidAppear doesn't start a duplicate trace. */ - (void)testViewControllerDidAppearIgnoresDuplicateEvent { // First screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); NSString *expectedTraceName = [FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController]; FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertNotNil(createdTrace); XCTAssertEqualObjects(createdTrace.name, expectedTraceName); // Send the same event again. [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 1); FIRTrace *activeTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertEqual(createdTrace, activeTrace); // Test that it is the same trace. } /** Tests that viewControllerDidAppear gracefully handles a nil viewController. */ - (void)testViewControllerDidAppearGracefullyHandlesNilViewController { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" [self.tracker viewControllerDidAppear:nil]; #pragma clang diagnostic pop dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 0); } /** Tests that viewControllerDidDisappear for a viewController that did not appear does nothing. */ - (void)testViewControllerDidDisappearIgnoresViewControllerThatWasntScreenTraced { UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); UIViewController *testViewController2 = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 1); [self.tracker viewControllerDidDisappear:testViewController2]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 1); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" [self.tracker viewControllerDidDisappear:nil]; #pragma clang diagnostic pop dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 1); [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 0); } /** Tests that UIViewControllers are weakly retained in the map table that holds the mapping between * them. */ - (void)testViewControllerIsWeaklyRetained { @autoreleasepool { UIViewController *testViewController = [[UIViewController alloc] init]; id mockTrace = OCMClassMock([FIRTrace class]); [self.tracker.activeScreenTraces setObject:mockTrace forKey:testViewController]; testViewController = nil; } XCTAssertEqual([self.tracker.activeScreenTraces dictionaryRepresentation].count, 0); } /** Tests that a FIRTrace is strongly retained in the map table that holds the mapping between a * view controller and its screen trace. */ - (void)testFIRTraceIsStronglyRetained { UIViewController *testViewController = [[UIViewController alloc] init]; NSString *traceName = @"screenTrace"; FIRTrace *trace = [[FIRTrace alloc] initInternalTraceWithName:traceName]; [self.tracker.activeScreenTraces setObject:trace forKey:testViewController]; trace = nil; XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController]); FIRTrace *returnedTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; XCTAssertEqualObjects(returnedTrace.name, traceName); } /** Tests that a screen trace that doesn't collect any data isn't sent. */ - (void)testTraceWithNoCountersIsNotSent { id mockTrace = OCMClassMock([FIRTrace class]); UIViewController *testViewController = [[UIViewController alloc] init]; [self.tracker.activeScreenTraces setObject:mockTrace forKey:testViewController]; OCMExpect([mockTrace cancel]); [[mockTrace reject] stop]; [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); OCMVerifyAll(mockTrace); } /** Test that all active traces are stopped when the app resigns active status. */ - (void)testWillAppResignActiveStopsAllActiveTraces { // First screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; // Second screen appears. UIViewController *testViewController2 = FPRCustomViewController(@"FPRTestViewController", YES); [self.tracker viewControllerDidAppear:testViewController2]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); FIRTrace *traceScreenOne = [self.tracker.activeScreenTraces objectForKey:testViewController]; FIRTrace *traceScreenTwo = [self.tracker.activeScreenTraces objectForKey:testViewController2]; XCTAssertNotNil(traceScreenOne); XCTAssertNotNil(traceScreenTwo); // App is backgrounded. NSNotification *appWillResignActiveNSNotification = [NSNotification notificationWithName:UIApplicationWillResignActiveNotification object:nil]; [self.tracker appWillResignActiveNotification:appWillResignActiveNSNotification]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertTrue(traceScreenOne.isCompleteAndValid); XCTAssertTrue(traceScreenTwo.isCompleteAndValid); } /** Test that viewController refs are weakly saved for future use when the app resigns active * status. */ - (void)disabled_testWillAppResignActiveWeaklySavesAllVisibleViewControllers { // Screen appears. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); // App is backgrounded. NSNotification *appWillResignActiveNSNotification = [NSNotification notificationWithName:UIApplicationWillResignActiveNotification object:nil]; [self.tracker appWillResignActiveNotification:appWillResignActiveNSNotification]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.previouslyVisibleViewControllers.count, 1); XCTAssertEqual(self.tracker.activeScreenTraces.count, 0); __weak id weakTestViewController = testViewController; testViewController = nil; // The blocks retain the view controllers and it sometimes takes some time to release them. // This is in place to prevent test flakiness. Autoreleasepools do not work in this case. while (weakTestViewController) { continue; } XCTAssertNil([self.tracker.previouslyVisibleViewControllers pointerAtIndex:0]); } /** Tests that new traces are started with the screens that are currently visible after the app * regains active status. */ - (void)testAppDidBecomeActiveWillRestoreTracesOfVisibleScreens { // Simulate state where two screen traces were previously active. UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); UIViewController *testViewController2 = FPRCustomViewController(@"FPRTestViewController", YES); self.tracker.previouslyVisibleViewControllers = [NSPointerArray weakObjectsPointerArray]; [self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController]; [self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController2]; // App becomes active. NSNotification *appDidBecomeActiveNSNotification = [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification object:nil]; [self.tracker appDidBecomeActiveNotification:appDidBecomeActiveNSNotification]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 2); XCTAssertNil(self.tracker.previouslyVisibleViewControllers); XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController]); XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController2]); } /** Tests that if one of the previously visible ViewControllers is deallocated, a new trace isn't * started for it, and the app doesn't crash. */ - (void)testAppDidBecomeActiveWillNotRestoreTracesOfNilledViewControllers { // Simulate state where two screen traces were previously active. UIViewController *testViewController = [[UIViewController alloc] init]; [testViewController view]; // Loads the view so that a screen trace is created for it. UIViewController *testViewController2 = [[UIViewController alloc] init]; [testViewController view]; // Loads the view so that a screen trace is created for it. self.tracker.previouslyVisibleViewControllers = [NSPointerArray weakObjectsPointerArray]; [self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController]; [self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController2]; // UIKit deallocates one of the ViewControllers that was previously visible. testViewController2 = nil; // App becomes active. NSNotification *appDidBecomeActiveNSNotification = [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification object:nil]; [self.tracker appDidBecomeActiveNotification:appDidBecomeActiveNSNotification]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 1); XCTAssertNil(self.tracker.previouslyVisibleViewControllers); XCTAssertNotNil([self.tracker.activeScreenTraces objectForKey:testViewController]); XCTAssertNil([self.tracker.activeScreenTraces objectForKey:testViewController2]); } /** Tests that if consecutive frames take more time to render than the slow frames threshold, the * slow frame counter of the screen trace tracker is incremented. */ - (void)testSlowFrameIsRecorded { CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Buffer for float comparison. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t newSlowFramesCount = self.tracker.slowFramesCount; XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1); } /** Tests that the slow and frozen frame counter is not incremented in the case of a good frame. */ - (void)testSlowAndFrozenFrameIsNotRecordedInCaseOfGoodFrame { CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + kFPRSlowFrameThreshold - 0.005; // Good frame. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t newSlowFramesCount = self.tracker.slowFramesCount; int64_t newFrozenFramesCount = self.tracker.frozenFramesCount; XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount); XCTAssertEqual(newFrozenFramesCount, initialFrozenFramesCount); } /* Tests that the frozen frame counter is not incremented in case of a slow frame. */ - (void)testFrozenFrameIsNotRecordedInCaseOfSlowFrame { CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Slow frame. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t newFrozenFramesCount = self.tracker.frozenFramesCount; XCTAssertEqual(newFrozenFramesCount, initialFrozenFramesCount); } /** Tests that the total frames counter is incremented in the case of good, slow and frozen * frames. */ - (void)testTotalFramesAreAlwaysRecorded { CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + kFPRSlowFrameThreshold - 0.005; // Good frame. CFAbsoluteTime thirdFrameRenderTimestamp = secondFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Slow frame. CFAbsoluteTime fourthFrameRenderTimestamp = thirdFrameRenderTimestamp + kFPRFrozenFrameThreshold + 0.005; // Frozen frame. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t initialTotalFramesCount = self.tracker.totalFramesCount; OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t newTotalFramesCount = self.tracker.totalFramesCount; XCTAssertEqual(newTotalFramesCount, initialTotalFramesCount + 1); OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp); [self.tracker displayLinkStep]; newTotalFramesCount = self.tracker.totalFramesCount; XCTAssertEqual(newTotalFramesCount, initialTotalFramesCount + 2); OCMExpect([displayLinkMock timestamp]).andReturn(fourthFrameRenderTimestamp); [self.tracker displayLinkStep]; newTotalFramesCount = self.tracker.totalFramesCount; XCTAssertEqual(newTotalFramesCount, initialTotalFramesCount + 3); } /** Tests that if consecutive frames take more time to render than the frozen frames threshold, the * frozen frame counter and slow frame counter of the screen trace tracker is incremented. */ - (void)testFrozenFrameAndSlowFrameIsRecorded { CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + kFPRFrozenFrameThreshold + 0.005; // Buffer for float comparison. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); [self.tracker displayLinkStep]; int64_t newSlowFramesCount = self.tracker.slowFramesCount; int64_t newFrozenFramesCount = self.tracker.frozenFramesCount; XCTAssertEqual(newFrozenFramesCount, initialFrozenFramesCount + 1); XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1); } /** Tests that the correct number of slow, frozen and total frames are recorded when all 3 are * present. */ - (void)testTraceHasCorrectFrozenSlowAndTotalFrameMetricsWhenThoseFramesAreRecorded { int64_t initialTotalFramesCount = self.tracker.totalFramesCount; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; int64_t expectedTotalFramesOnTrace = 5; int64_t expectedSlowFramesOnTrace = 3; int64_t expectedFrozenFramesOnTrace = 1; UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; self.tracker.totalFramesCount = initialTotalFramesCount + expectedTotalFramesOnTrace; self.tracker.slowFramesCount = initialSlowFramesCount + expectedSlowFramesOnTrace; self.tracker.frozenFramesCount = initialFrozenFramesCount + expectedFrozenFramesOnTrace; [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual([createdTrace valueForIntMetric:kFPRSlowFrameCounterName], expectedSlowFramesOnTrace); XCTAssertEqual([createdTrace valueForIntMetric:kFPRFrozenFrameCounterName], expectedFrozenFramesOnTrace); XCTAssertEqual([createdTrace valueForIntMetric:kFPRTotalFramesCounterName], expectedTotalFramesOnTrace); } /** Tests that if just total and slow frame and no frozen frames are recorded, then the frozen * frames metric is not present on the trace. */ - (void)testTraceHasJustSlowAndTotalFrameMetricsWhenNoFrozenFramesAreRecorded { int64_t initialTotalFramesCount = self.tracker.totalFramesCount; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; int64_t expectedTotalFramesOnTrace = 5; int64_t expectedSlowFramesOnTrace = 3; int64_t expectedFrozenFramesOnTrace = 0; UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; self.tracker.totalFramesCount = initialTotalFramesCount + expectedTotalFramesOnTrace; self.tracker.slowFramesCount = initialSlowFramesCount + expectedSlowFramesOnTrace; self.tracker.frozenFramesCount = initialFrozenFramesCount + expectedFrozenFramesOnTrace; [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual([createdTrace valueForIntMetric:kFPRSlowFrameCounterName], expectedSlowFramesOnTrace); XCTAssertEqual([createdTrace valueForIntMetric:kFPRTotalFramesCounterName], expectedTotalFramesOnTrace); XCTAssertNil(createdTrace.counters[kFPRFrozenFrameCounterName]); XCTAssertEqual(createdTrace.counters.count, 2); } /** Tests that when no frozen or slow frames are recorded, the trace only has the total frames * counter. */ - (void)testTraceHasJustTotalFrameMetricsWhenNoFrozenOrSlowFramesAreRecorded { int64_t initialTotalFramesCount = self.tracker.totalFramesCount; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; int64_t expectedTotalFramesOnTrace = 5; int64_t expectedSlowFramesOnTrace = 0; int64_t expectedFrozenFramesOnTrace = 0; UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; self.tracker.totalFramesCount = initialTotalFramesCount + expectedTotalFramesOnTrace; self.tracker.slowFramesCount = initialSlowFramesCount + expectedSlowFramesOnTrace; self.tracker.frozenFramesCount = initialFrozenFramesCount + expectedFrozenFramesOnTrace; [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual([createdTrace valueForIntMetric:kFPRTotalFramesCounterName], expectedTotalFramesOnTrace); XCTAssertNil(createdTrace.counters[kFPRSlowFrameCounterName]); XCTAssertNil(createdTrace.counters[kFPRFrozenFrameCounterName]); XCTAssertEqual(createdTrace.counters.count, 1); } /** Tests that if no frames are recorded between a trace being started and stopped, it doesn't have * any metrics associated with it. */ - (void)testTraceHasNoMetricsWhenNoFramesAreRecorded { UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES); [self.tracker viewControllerDidAppear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); FIRTrace *createdTrace = [self.tracker.activeScreenTraces objectForKey:testViewController]; [self.tracker viewControllerDidDisappear:testViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(createdTrace.counters.count, 0); } /** Tests that screen traces are NOT created for container view controllers. */ - (void)testScreenTracesAreNotCreatedForContainerViewControllers { UINavigationController *testNavigationController = (UINavigationController *)FPRCustomViewController(@"UINavigationController", YES); UITabBarController *testTabBarController = (UITabBarController *)FPRCustomViewController(@"UITabBarController", YES); UISplitViewController *testSplitViewController = (UISplitViewController *)FPRCustomViewController(@"UISplitViewController", YES); UIPageViewController *testPageViewController = (UIPageViewController *)FPRCustomViewController(@"UIPageViewController", YES); [self.tracker viewControllerDidAppear:testNavigationController]; [self.tracker viewControllerDidAppear:testTabBarController]; [self.tracker viewControllerDidAppear:testSplitViewController]; [self.tracker viewControllerDidAppear:testPageViewController]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 0); } /** Tests that screen traces are created for canonical container view controller subclasses. */ - (void)testScreenTracesAreCreatedForContainerViewControllerSubclasses { FPRTestNavigationViewController *testNavigationControllerSubclass = (FPRTestNavigationViewController *)FPRCustomViewController(@"FPRTestNavigationViewController", YES); FPRTestTabBarController *testTabBarControllerSubclass = (FPRTestTabBarController *)FPRCustomViewController(@"FPRTestTabBarController", YES); FPRTestSplitViewController *testSplitViewControllerSubclass = (FPRTestSplitViewController *)FPRCustomViewController(@"FPRTestSplitViewController", YES); FPRTestPageViewController *testPageViewControllerSubclass = (FPRTestPageViewController *)FPRCustomViewController(@"FPRTestPageViewController", YES); [self.tracker viewControllerDidAppear:testNavigationControllerSubclass]; [self.tracker viewControllerDidAppear:testTabBarControllerSubclass]; [self.tracker viewControllerDidAppear:testSplitViewControllerSubclass]; [self.tracker viewControllerDidAppear:testPageViewControllerSubclass]; dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER); XCTAssertEqual(self.tracker.activeScreenTraces.count, 4); } #pragma mark - Helper methods + (NSString *)expectedTraceNameForViewController:(UIViewController *)viewController { return [@"_st_" stringByAppendingString:NSStringFromClass([viewController class])]; } @end