chromium/ios/chrome/browser/variations/model/variations_safe_mode_end_to_end_egtest.mm

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <XCTest/XCTest.h>
#import <objc/runtime.h>

#import <memory>

#import "base/base_switches.h"
#import "base/files/scoped_temp_dir.h"
#import "base/strings/strcat.h"
#import "components/metrics/metrics_service.h"
#import "components/prefs/json_pref_store.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/pref_service_factory.h"
#import "components/variations/pref_names.h"
#import "components/variations/service/safe_seed_manager.h"
#import "components/variations/variations_test_utils.h"

#import "ios/chrome/browser/variations/model/variations_app_interface.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/chrome/test/earl_grey/scoped_allow_crash_on_startup.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"

namespace {
std::unique_ptr<ScopedAllowCrashOnStartup> gAllowCrashOnStartup;
}  // namespace

@interface VariationsSafeModeEndToEndTestCase : ChromeTestCase
@end

@implementation VariationsSafeModeEndToEndTestCase

#pragma mark - Helpers

// Returns an AppLaunchConfiguration that shuts down Chrome cleanly (if it is
// already running) and relaunches it. Disabling the testing config means that
// the only field trials after the relaunch, if any, are client-side field
// trials.
//
// Change the `allow_crash_on_startup` field of the returned config to afford
// the app an opportunity to crash on restart.
- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.relaunch_policy = ForceRelaunchByCleanShutdown;
  // Assign the test environment to be on the Canary channel. This ensures
  // compatibility with the crashing study in the seed.
  config.additional_args = {"--disable-field-trial-config",
                            "--fake-variations-channel=canary"};
  return config;
}

// Returns an AppLaunchConfiguration that shuts down Chrome cleanly (if it is
// already running) and relaunches it with no additional flags or settings.
- (AppLaunchConfiguration)appConfigurationForCleanRestart {
  AppLaunchConfiguration config;
  config.relaunch_policy = ForceRelaunchByCleanShutdown;
  return config;
}

// Checks that the variations crash streak is `value`.
- (void)checkCrashStreakValue:(int)value {
  int actualStreak = [VariationsAppInterface crashStreak];
  GREYAssertEqual(actualStreak, value,
                  @"Expected a crash streak of %d, but got %d", value,
                  actualStreak);
}

// Restarts the app and ensures there's no variations/crash state active.
- (void)resetAppState:(AppLaunchConfiguration)config {
  // Clear local state variations prefs since local state is persisted between
  // EG tests and restart Chrome. This is to avoid flakiness caused by tests
  // that may have run previously and to avoid introducing flakiness in tests
  // that might run after.
  //
  // See crbug.com/1069086.
  [VariationsAppInterface clearVariationsPrefs];
  [[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config];

  // Validate app state:
  //   * App is running
  //   * No safe seed value in local state
  //   * No evidence of safe seed settings in local state.
  //   * No active crash streak
  XCTAssertTrue([[AppLaunchManager sharedManager] appIsLaunched],
                @"App should be launched.");
  GREYAssertFalse([VariationsAppInterface hasSafeSeed], @"No safe seed.");
  GREYAssertFalse([VariationsAppInterface fieldTrialExistsForTestSeed],
                  @"No field trial from test seed.");
  [self checkCrashStreakValue:0];
}

#pragma mark - Lifecycle

+ (void)setUpForTestCase {
  [super setUpForTestCase];
  gAllowCrashOnStartup = std::make_unique<ScopedAllowCrashOnStartup>();
}

+ (void)tearDown {
  gAllowCrashOnStartup.reset();
  [super tearDown];
}

- (void)setUp {
  // `ChromeTestCase:isStartupTest` must be true before calling [super setUp] in
  // order to avoid opening a new tab on startup. While not strictly necessary,
  // this let's the test run a little faster.
  [[self class] testForStartup];

  [super setUp];
  [self resetAppState:[self appConfigurationForTestCase]];
  self.continueAfterFailure = YES;
}

- (void)tearDown {
  [self resetAppState:[self appConfigurationForCleanRestart]];
  [super tearDown];
}

#pragma mark - Tests

// Tests that three seed-driven crashes trigger variations safe mode.
//
// Corresponds to VariationsSafeModeEndToEndBrowserTest.ExtendedSafeSeedEndToEnd
// in variations_safe_mode_browsertest.cc.
- (void)testVariationsSafeModeEndToEnd {
// TODO(crbug.com/40215027): Test fails on iOS 17.5+ iPad devices.
#if !TARGET_IPHONE_SIMULATOR
  if (@available(iOS 17.5, *)) {
    if ([ChromeEarlGrey isIPadIdiom]) {
      EARL_GREY_TEST_DISABLED(@"This test fails on iOS 17.5+ iPad device.");
    }
  }
#endif
  AppLaunchConfiguration config = [self appConfigurationForTestCase];

  // Set the safe seed value. Validate that the seed is set but not active.
  [VariationsAppInterface setTestSafeSeedAndSignature];
  GREYAssertTrue([VariationsAppInterface hasSafeSeed],
                 @"The variations safe seed pref should be set.");
  GREYAssertFalse([VariationsAppInterface fieldTrialExistsForTestSeed],
                  @"Safe seed field trials should not be active.");

  // Set a Finch seed value that enables a crashing feature.
  [VariationsAppInterface setCrashingRegularSeedAndSignature];

  // Pretend chrome has repeatedly crashed, just one away from safe mode.
  int penultimateCrash = variations::kCrashStreakSafeSeedThreshold - 1;
  [VariationsAppInterface setCrashValue:penultimateCrash];
  [self checkCrashStreakValue:penultimateCrash];

  // The next restart should crash, hitting the crash streak threshold.
  NSLog(@"Next start should crash on startup...");
  [[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config];
  XCTAssertFalse([[AppLaunchManager sharedManager] appIsLaunched],
                 @"App should have crashed on startup");

  NSLog(@"Next start should enter safe mode...");
  // Subsequent restarts should succeed. Verify that Chrome fell back to
  // variations safe mode by checking that there is a field trial for the test
  // safe seed's study.
  [[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config];
  XCTAssertTrue([[AppLaunchManager sharedManager] appIsLaunched],
                @"App should be launched.");
  GREYAssertTrue([VariationsAppInterface hasSafeSeed],
                 @"The variations safe seed pref should be set.");
  GREYAssertTrue([VariationsAppInterface fieldTrialExistsForTestSeed],
                 @"There should be field trials from kTestSeedData.");
  [self checkCrashStreakValue:variations::kCrashStreakSafeSeedThreshold];
}

@end