// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/app/app_metrics_app_state_agent.h"
#import "base/test/task_environment.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/metrics_mediator.h"
#import "ios/chrome/browser/metrics/model/ios_profile_session_durations_service.h"
#import "ios/chrome/browser/metrics/model/ios_profile_session_durations_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
namespace {
class FakeProfileSessionDurationsService
: public IOSProfileSessionDurationsService {
public:
FakeProfileSessionDurationsService() = default;
FakeProfileSessionDurationsService(
const FakeProfileSessionDurationsService&) = delete;
FakeProfileSessionDurationsService& operator=(
const FakeProfileSessionDurationsService&) = delete;
~FakeProfileSessionDurationsService() override = default;
static std::unique_ptr<KeyedService> Create(
web::BrowserState* browser_state) {
return std::make_unique<FakeProfileSessionDurationsService>();
}
void OnSessionStarted(base::TimeTicks session_start) override {
++session_started_count_;
}
void OnSessionEnded(base::TimeDelta session_length) override {
++session_ended_count_;
}
bool IsSessionActive() override {
return session_started_count_ > session_ended_count_;
}
// IOSProfileSessionDurationsService:
int session_started_count() const { return session_started_count_; }
int session_ended_count() const { return session_ended_count_; }
private:
int session_started_count_ = 0;
int session_ended_count_ = 0;
};
} // namespace
// A fake that allows overriding connectedScenes.
@interface FakeAppState : AppState
@property(nonatomic, strong) NSArray<SceneState*>* connectedScenes;
// Init stage that will be returned by the initStage getter when testing.
@property(nonatomic, assign) InitStage initStageForTesting;
@end
@implementation FakeAppState
- (InitStage)initStage {
return self.initStageForTesting;
}
@end
InitStage GetMinimalInitStageThatAllowsLogging() {
return static_cast<InitStage>(InitStageBrowserObjectsForBackgroundHandlers);
}
InitStage GetMaximalInitStageThatDontAllowLogging() {
return static_cast<InitStage>(InitStageBrowserObjectsForBackgroundHandlers -
1);
}
class AppMetricsAppStateAgentTest : public PlatformTest {
protected:
AppMetricsAppStateAgentTest() {
agent_ = [[AppMetricsAppStateAgent alloc] init];
TestChromeBrowserState::Builder test_cbs_builder;
test_cbs_builder.AddTestingFactory(
IOSProfileSessionDurationsServiceFactory::GetInstance(),
base::BindRepeating(&FakeProfileSessionDurationsService::Create));
browser_state_ =
profile_manager_.AddProfileWithBuilder(std::move(test_cbs_builder));
app_state_ = [[FakeAppState alloc] initWithStartupInformation:nil];
}
void SetUp() override {
PlatformTest::SetUp();
app_state_.initStageForTesting = GetMinimalInitStageThatAllowsLogging();
[agent_ setAppState:app_state_];
}
FakeProfileSessionDurationsService* getProfileSessionDurationsService() {
return static_cast<FakeProfileSessionDurationsService*>(
IOSProfileSessionDurationsServiceFactory::GetForBrowserState(
browser_state_.get()));
}
void SimulateTransitionToCurrentStage() {
InitStage previousStage =
app_state_.initStage == InitStageStart
? InitStageStart
: static_cast<InitStage>(app_state_.initStage - 1);
[agent_ appState:app_state_ didTransitionFromInitStage:previousStage];
}
base::test::TaskEnvironment task_environment_;
IOSChromeScopedTestingLocalState scoped_testing_local_state_;
TestProfileManagerIOS profile_manager_;
AppMetricsAppStateAgent* agent_;
raw_ptr<ChromeBrowserState> browser_state_;
FakeAppState* app_state_;
};
TEST_F(AppMetricsAppStateAgentTest, CountSessionDuration) {
SceneState* scene = [[SceneState alloc] initWithAppState:app_state_];
app_state_.connectedScenes = @[ scene ];
[agent_ appState:app_state_ sceneConnected:scene];
EXPECT_EQ(0, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Going to background at app start doesn't log anything.
scene.activationLevel = SceneActivationLevelBackground;
EXPECT_EQ(0, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
SimulateTransitionToCurrentStage();
// Going foreground starts the session.
scene.activationLevel = SceneActivationLevelForegroundInactive;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Going to background stops the session.
scene.activationLevel = SceneActivationLevelBackground;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(1, getProfileSessionDurationsService()->session_ended_count());
}
TEST_F(AppMetricsAppStateAgentTest, CountSessionDurationMultiwindow) {
SceneState* sceneA = [[SceneState alloc] initWithAppState:app_state_];
SceneState* sceneB = [[SceneState alloc] initWithAppState:app_state_];
app_state_.connectedScenes = @[ sceneA, sceneB ];
[agent_ appState:app_state_ sceneConnected:sceneA];
[agent_ appState:app_state_ sceneConnected:sceneB];
EXPECT_EQ(0, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
SimulateTransitionToCurrentStage();
// One scene is enough to start a session.
sceneA.activationLevel = SceneActivationLevelForegroundInactive;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Two scenes at the same time, still the session goes on.
sceneB.activationLevel = SceneActivationLevelForegroundInactive;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Only scene B in foreground, session still going.
sceneA.activationLevel = SceneActivationLevelBackground;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// No sessions in foreground, session is over.
sceneB.activationLevel = SceneActivationLevelBackground;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(1, getProfileSessionDurationsService()->session_ended_count());
}
TEST_F(AppMetricsAppStateAgentTest, CountSessionDurationSafeMode) {
SceneState* scene = [[SceneState alloc] initWithAppState:app_state_];
app_state_.connectedScenes = @[ scene ];
app_state_.initStageForTesting = InitStageSafeMode;
[agent_ appState:app_state_ sceneConnected:scene];
EXPECT_EQ(0, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
SimulateTransitionToCurrentStage();
// Going to background at app start doesn't log anything.
scene.activationLevel = SceneActivationLevelBackground;
EXPECT_EQ(0, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Going foreground doesn't start the session while in safe mode.
scene.activationLevel = SceneActivationLevelForegroundInactive;
EXPECT_EQ(0, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Session starts when safe mode completes.
app_state_.initStageForTesting = GetMinimalInitStageThatAllowsLogging();
SimulateTransitionToCurrentStage();
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(0, getProfileSessionDurationsService()->session_ended_count());
// Going to background stops the session.
scene.activationLevel = SceneActivationLevelBackground;
EXPECT_EQ(1, getProfileSessionDurationsService()->session_started_count());
EXPECT_EQ(1, getProfileSessionDurationsService()->session_ended_count());
}
// Tests that -logStartupDuration: and -createStartupTrackingTask are called
// once and in the right order for a regular startup (no safe mode).
TEST_F(AppMetricsAppStateAgentTest, logStartupDuration) {
id metricsMediator = [OCMockObject mockForClass:[MetricsMediator class]];
[[metricsMediator expect] createStartupTrackingTask];
[[metricsMediator expect] logStartupDuration:nil];
[metricsMediator setExpectationOrderMatters:YES];
SceneState* sceneA = [[SceneState alloc] initWithAppState:app_state_];
SceneState* sceneB = [[SceneState alloc] initWithAppState:app_state_];
app_state_.connectedScenes = @[ sceneA, sceneB ];
[agent_ appState:app_state_ sceneConnected:sceneA];
[agent_ appState:app_state_ sceneConnected:sceneB];
// Simulate transitioning to the current app init stage before scenes going on
// the foreground.
[agent_ appState:app_state_
didTransitionFromInitStage:static_cast<InitStage>(app_state_.initStage -
1)];
// Should not log startup duration until the scene is active.
sceneA.activationLevel = SceneActivationLevelForegroundInactive;
sceneB.activationLevel = SceneActivationLevelForegroundInactive;
// Should only log startup once when the first scene becomes active.
sceneA.activationLevel = SceneActivationLevelForegroundActive;
sceneB.activationLevel = SceneActivationLevelForegroundActive;
// Should not log startup when scene becomes active again.
sceneA.activationLevel = SceneActivationLevelBackground;
sceneB.activationLevel = SceneActivationLevelBackground;
sceneA.activationLevel = SceneActivationLevelForegroundActive;
sceneB.activationLevel = SceneActivationLevelForegroundActive;
EXPECT_OCMOCK_VERIFY(metricsMediator);
}
// Tests that -logStartupDuration: and and -createStartupTrackingTask are not
// called when there is safe mode during startup.
TEST_F(AppMetricsAppStateAgentTest, logStartupDurationWhenSafeMode) {
id metricsMediator = [OCMockObject mockForClass:[MetricsMediator class]];
[[metricsMediator reject] createStartupTrackingTask];
[[metricsMediator reject] logStartupDuration:nil];
app_state_.initStageForTesting = GetMaximalInitStageThatDontAllowLogging();
SceneState* sceneA = [[SceneState alloc] initWithAppState:app_state_];
app_state_.connectedScenes = @[ sceneA ];
[agent_ appState:app_state_ sceneConnected:sceneA];
// Simulate transitioning to the current app init stage before scenes going on
// the foreground.
[agent_ appState:app_state_
didTransitionFromInitStage:static_cast<InitStage>(app_state_.initStage -
1)];
// This would normally log startup information, but not when in safe mode.
sceneA.activationLevel = SceneActivationLevelForegroundActive;
EXPECT_OCMOCK_VERIFY(metricsMediator);
}