chromium/ios/testing/earl_grey/app_launch_argument_generator.mm

// Copyright 2024 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/testing/earl_grey/app_launch_argument_generator.h"

#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"

namespace {

// Escapes separators used by enable-features command line.
// E.g. Feature '<' Study '.' Group ':' param1 '/' value1 ','
// ('*' is not a separator. No need to escape it.)
std::string EscapeValue(const std::string& value) {
  std::string escaped_str;
  for (const auto ch : value) {
    if (ch == ',' || ch == '/' || ch == ':' || ch == '<' || ch == '.') {
      escaped_str.append(base::StringPrintf("%%%02X", ch));
    } else {
      escaped_str.append(1, ch);
    }
  }
  return escaped_str;
}
}  // namespace

NSArray<NSString*>* ArgumentsFromConfiguration(
    AppLaunchConfiguration configuration) {
  CHECK(configuration.features_enabled.empty() ||
        configuration.features_enabled_and_params.empty());

  NSMutableArray<NSString*>* namesToEnable = [NSMutableArray array];
  NSMutableArray<NSString*>* namesToDisable = [NSMutableArray array];
  NSMutableArray<NSString*>* variations = [NSMutableArray array];

  for (const auto& feature : configuration.features_enabled) {
    [namesToEnable addObject:base::SysUTF8ToNSString(feature->name)];
  }

  for (const auto& featureWithParam :
       configuration.features_enabled_and_params) {
    // If features.params has 2 params whose values are value1 and value2,
    // `params` will be "param1/value1/param2/value2/".
    std::string params;
    for (const auto& param : featureWithParam.params) {
      // Add separator from previous param information if it exists.
      if (!params.empty()) {
        params.append(1, '/');
      }
      params.append(EscapeValue(param.first));
      params.append(1, '/');
      params.append(EscapeValue(param.second));
    }

    std::string featureWithParamString =
        std::string(featureWithParam.feature->name) + ":" + params;

    [namesToEnable addObject:base::SysUTF8ToNSString(featureWithParamString)];
  }

  for (const auto& feature : configuration.features_disabled) {
    [namesToDisable addObject:base::SysUTF8ToNSString(feature->name)];
  }

  for (const variations::VariationID& variation :
       configuration.variations_enabled) {
    [variations addObject:[NSString stringWithFormat:@"%d", variation]];
  }

  for (const variations::VariationID& variation :
       configuration.trigger_variations_enabled) {
    [variations addObject:[NSString stringWithFormat:@"t%d", variation]];
  }

  NSMutableArray<NSString*>* arguments = [[NSMutableArray alloc] init];

  if (configuration.iph_feature_enabled.has_value()) {
    std::string iph_enable_argument = base::StringPrintf(
        "--enable-iph=%s", configuration.iph_feature_enabled.value().c_str());
    [arguments addObject:base::SysUTF8ToNSString(iph_enable_argument)];
  }

  std::string enableKey = "--enable-features=";
  std::string disableKey = "--disable-features=";
  for (const std::string& arg : configuration.additional_args) {
    // Extract any args enabling or disabling features and combine all, plus
    // anything already provided directly in the configuration.
    if (arg.starts_with(enableKey)) {
      [namesToEnable
          addObject:base::SysUTF8ToNSString(arg.substr(enableKey.length()))];
      continue;
    }
    if (arg.starts_with(disableKey)) {
      [namesToDisable
          addObject:base::SysUTF8ToNSString(arg.substr(disableKey.length()))];
      continue;
    }
    [arguments addObject:base::SysUTF8ToNSString(arg)];
  }

  NSString* enabledString = @"";
  NSString* disabledString = @"";
  NSString* variationString = @"";
  if ([namesToEnable count] > 0) {
    enabledString = [NSString
        stringWithFormat:@"--enable-features=%@",
                         [namesToEnable componentsJoinedByString:@","]];
  }
  if ([namesToDisable count] > 0) {
    disabledString = [NSString
        stringWithFormat:@"--disable-features=%@",
                         [namesToDisable componentsJoinedByString:@","]];
  }
  if (variations.count > 0) {
    variationString =
        [NSString stringWithFormat:@"--force-variation-ids=%@",
                                   [variations componentsJoinedByString:@","]];
  }

  [arguments insertObject:enabledString atIndex:0];
  [arguments insertObject:disabledString atIndex:1];
  [arguments insertObject:variationString atIndex:2];

  // For the app to use the same language as the test module. This workaround
  // the fact that EarlGrey tests depends on localizable strings (unfortunate)
  // even though the test module does not have access to the system locale. To
  // make this work, we force the app to use the same locale as used by the
  // test module (which is the first item in -preferredLocalizations).
  NSArray<NSString*>* languages = [NSBundle mainBundle].preferredLocalizations;
  if (languages.count != 0) {
    NSString* language = [languages firstObject];
    [arguments addObject:@"-AppleLanguages"];
    [arguments addObject:[NSString stringWithFormat:@"(%@)", language]];
  }
  return arguments;
}