Commit 85560e5c authored by edchin's avatar edchin Committed by Commit Bot

Create Chrome Flag Ownership unit tests in iOS

We are adopting Chrome Flag Ownership in iOS.
go/chrome-flags-ios

More about flag ownership here:
https://chromium.googlesource.com/chromium/src/+/master/docs/flag_ownership.md

This CL creates a util file of data structures and functions that
are shared by:
//chrome/browser/about_flags_unittest.cc and
//ios/chrome/browser/flags/about_flags_unittest.mm

This CL migrates 5 tests to iOS. However, the main test that ensures
that flags in iOS have a corresponding metadata entry is disabled. This
will be enabled when the metadata file is filled out with iOS flags.

Additional followup CLs will add entries to the metadata files,
enable tests, and modify documentation.

bug: 1058614
Change-Id: I8c94183a47d2065c2fc4441e4fc7d9682d17da55
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2088034
Commit-Queue: edchin <edchin@chromium.org>
Reviewed-by: default avatarElly Fong-Jones <ellyjones@chromium.org>
Reviewed-by: default avatarRohit Rao <rohitrao@chromium.org>
Reviewed-by: default avataredchin <edchin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747921}
parent 1c33ce78
......@@ -6,25 +6,17 @@
#include <stddef.h>
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <utility>
#include "base/base_paths.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/format_macros.h"
#include "base/json/json_file_value_serializer.h"
#include "base/path_service.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_enum_reader.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/flags_ui/feature_entry.h"
#include "components/flags_ui/flags_test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace about_flags {
......@@ -71,113 +63,6 @@ std::set<std::string> GetAllSwitchesAndFeaturesForTesting() {
return result;
}
enum class FlagFile { kFlagMetadata, kFlagNeverExpire };
std::string FlagFileName(FlagFile file) {
switch (file) {
case FlagFile::kFlagMetadata:
return "flag-metadata.json";
case FlagFile::kFlagNeverExpire:
return "flag-never-expire-list.json";
}
}
base::Value FileContents(FlagFile file) {
std::string filename = FlagFileName(file);
base::FilePath metadata_path;
base::PathService::Get(base::DIR_SOURCE_ROOT, &metadata_path);
JSONFileValueDeserializer deserializer(
metadata_path.AppendASCII("chrome").AppendASCII("browser").AppendASCII(
filename));
int error_code;
std::string error_message;
std::unique_ptr<base::Value> json =
deserializer.Deserialize(&error_code, &error_message);
DCHECK(json) << "Failed to load " << filename << ": " << error_code << " "
<< error_message;
return std::move(*json);
}
struct FlagMetadataEntry {
std::vector<std::string> owners;
int expiry_milestone;
};
using FlagMetadataMap = std::map<std::string, FlagMetadataEntry>;
FlagMetadataMap LoadFlagMetadata() {
base::Value metadata_json = FileContents(FlagFile::kFlagMetadata);
FlagMetadataMap metadata;
for (const auto& entry : metadata_json.GetList()) {
std::string name = entry.FindKey("name")->GetString();
std::vector<std::string> owners;
if (const base::Value* e = entry.FindKey("owners")) {
for (const auto& owner : e->GetList())
owners.push_back(owner.GetString());
}
int expiry_milestone = entry.FindKey("expiry_milestone")->GetInt();
metadata[name] = FlagMetadataEntry{owners, expiry_milestone};
}
return metadata;
}
std::vector<std::string> LoadFlagNeverExpireList() {
base::Value list_json = FileContents(FlagFile::kFlagNeverExpire);
std::vector<std::string> result;
for (const auto& entry : list_json.GetList()) {
result.push_back(entry.GetString());
}
return result;
}
bool IsValidLookingOwner(base::StringPiece owner) {
// Per the specification at the top of flag-metadata.json, an owner is one of:
// 1) A string containing '@', which is treated as a full email address
// 2) A string beginning with '//', which is a path to an OWNERS file
// 3) Any other string, which is the username part of an @chromium.org email
const size_t at_pos = owner.find("@");
if (at_pos != std::string::npos) {
// If there's an @, check for a . after it. This catches one common error:
// writing "foo@" in the owners list rather than bare "foo" or full
// "foo@domain.com".
return owner.find(".", at_pos) != std::string::npos;
}
if (owner.starts_with("//")) {
// Looks like a path to a file. It would be nice to check that the file
// actually exists here, but that's not possible because when this test
// runs it runs in an isolated environment. To check for the presence of the
// file the test would need a build-time declaration that it depends on that
// file. Instead, just assume any file path ending in 'OWNERS' is valid.
// This doesn't check that the entire filename part of the path is 'OWNERS'
// because sometimes it is instead 'IPC_OWNERS' or similar.
return owner.ends_with("OWNERS");
}
// Otherwise, look for something that seems like the username part of an
// @chromium.org email. The criteria here is that it must look like an RFC5322
// "atom", which is neatly defined as any printable character *outside* a
// specific set:
// https://tools.ietf.org/html/rfc5322#section-3.2.3
//
// Note two extra wrinkles here:
// 1) while '.' IS NOT allowed in atoms by RFC5322 gmail and other mail
// handlers do allow it, so this does not reject '.'.
// 2) while '/' IS allowed in atoms by RFC5322, this is not commonly done, and
// checking for it here detects another common syntax error - namely
// writing:
// "owners": [ "foo/bar/OWNERS" ]
// where
// "owners": [ "//foo/bar/OWNERS" ]
// is meant.
return owner.find_first_of(R"(()<>[]:;@\,/)") == std::string::npos;
}
} // anonymous namespace
// Makes sure there are no separators in any of the entry names.
......@@ -196,132 +81,30 @@ TEST(AboutFlagsTest, NoSeparators) {
TEST(AboutFlagsTest, EveryFlagHasMetadata) {
size_t count;
const flags_ui::FeatureEntry* entries = testing::GetFeatureEntries(&count);
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> missing_flags;
for (size_t i = 0; i < count; ++i) {
if (metadata.count(entries[i].internal_name) == 0)
missing_flags.push_back(entries[i].internal_name);
}
std::sort(missing_flags.begin(), missing_flags.end());
EXPECT_EQ(0u, missing_flags.size())
<< "Missing flags: " << base::JoinString(missing_flags, "\n ");
flags_ui::testing::EnsureEveryFlagHasMetadata(entries, count);
}
// Ensures that all flags marked as never expiring in flag-metadata.json is
// listed in flag-never-expire-list.json.
TEST(AboutFlagsTest, OnlyPermittedFlagsNeverExpire) {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> listed_flags = LoadFlagNeverExpireList();
std::vector<std::string> missing_flags;
for (const auto& entry : metadata) {
if (entry.second.expiry_milestone == -1 &&
std::find(listed_flags.begin(), listed_flags.end(), entry.first) ==
listed_flags.end()) {
missing_flags.push_back(entry.first);
}
}
std::sort(missing_flags.begin(), missing_flags.end());
EXPECT_EQ(0u, missing_flags.size())
<< "Flags not listed for no-expire: "
<< base::JoinString(missing_flags, "\n ");
flags_ui::testing::EnsureOnlyPermittedFlagsNeverExpire();
}
// Ensures that every flag has an owner.
TEST(AboutFlagsTest, EveryFlagHasNonEmptyOwners) {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> sad_flags;
for (const auto& it : metadata) {
if (it.second.owners.empty())
sad_flags.push_back(it.first);
}
std::sort(sad_flags.begin(), sad_flags.end());
EXPECT_EQ(0u, sad_flags.size())
<< "Flags missing owners: " << base::JoinString(sad_flags, "\n ");
flags_ui::testing::EnsureEveryFlagHasNonEmptyOwners();
}
// Ensures that owners conform to rules in flag-metadata.json.
TEST(AboutFlagsTest, OwnersLookValid) {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> sad_flags;
for (const auto& flag : metadata) {
for (const auto& owner : flag.second.owners) {
if (!IsValidLookingOwner(owner))
sad_flags.push_back(flag.first);
}
}
EXPECT_EQ(0u, sad_flags.size()) << "Flags with invalid-looking owners: "
<< base::JoinString(sad_flags, "\n");
}
namespace {
void EnsureNamesAreAlphabetical(
const std::vector<std::string>& normalized_names,
const std::vector<std::string>& names,
FlagFile file) {
if (normalized_names.size() < 2)
return;
for (size_t i = 1; i < normalized_names.size(); ++i) {
if (i == normalized_names.size() - 1) {
// The last item on the list has less context.
EXPECT_TRUE(normalized_names[i - 1] < normalized_names[i])
<< "Correct alphabetical order does not place '" << names[i]
<< "' after '" << names[i - 1] << "' in " << FlagFileName(file);
} else {
EXPECT_TRUE(normalized_names[i - 1] < normalized_names[i] &&
normalized_names[i] < normalized_names[i + 1])
<< "Correct alphabetical order does not place '" << names[i]
<< "' between '" << names[i - 1] << "' and '" << names[i + 1]
<< "' in " << FlagFileName(file);
}
}
}
std::string NormalizeName(const std::string& name) {
std::string normalized_name = base::ToLowerASCII(name);
std::replace(normalized_name.begin(), normalized_name.end(), '_', '-');
return normalized_name;
flags_ui::testing::EnsureOwnersLookValid();
}
} // namespace
// For some bizarre reason, far too many people see a file filled with
// alphabetically-ordered items and think "hey, let me drop this new item into a
// random location!" Prohibit such behavior in the flags files.
TEST(AboutFlagsTest, FlagsListedInAlphabeticalOrder) {
base::Value metadata_json = FileContents(FlagFile::kFlagMetadata);
std::vector<std::string> normalized_names;
std::vector<std::string> names;
for (const auto& entry : metadata_json.GetList()) {
normalized_names.push_back(
NormalizeName(entry.FindKey("name")->GetString()));
names.push_back(entry.FindKey("name")->GetString());
}
EnsureNamesAreAlphabetical(normalized_names, names, FlagFile::kFlagMetadata);
base::Value expiration_json = FileContents(FlagFile::kFlagNeverExpire);
normalized_names.clear();
names.clear();
for (const auto& entry : expiration_json.GetList()) {
normalized_names.push_back(NormalizeName(entry.GetString()));
names.push_back(entry.GetString());
}
EnsureNamesAreAlphabetical(normalized_names, names,
FlagFile::kFlagNeverExpire);
flags_ui::testing::EnsureFlagsAreListedInAlphabeticalOrder();
}
class AboutFlagsHistogramTest : public ::testing::Test {
......
......@@ -3713,6 +3713,7 @@ test("unit_tests") {
"//components/data_reduction_proxy/core/browser:test_support",
"//components/data_use_measurement/core",
"//components/favicon/core/test:test_support",
"//components/flags_ui:test_support",
"//components/mirroring:mirroring_tests",
"//components/nacl/common:buildflags",
"//components/ntp_snippets:test_support",
......
......@@ -39,6 +39,20 @@ static_library("switches") {
]
}
static_library("test_support") {
testonly = true
sources = [
"flags_test_helpers.cc",
"flags_test_helpers.h",
]
deps = [
":flags_ui",
"//base",
"//testing/gtest",
]
}
source_set("unit_tests") {
testonly = true
sources = [ "flags_state_unittest.cc" ]
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/flags_ui/flags_test_helpers.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <map>
#include <string>
#include <vector>
#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/json/json_file_value_serializer.h"
#include "base/path_service.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "components/flags_ui/feature_entry.h"
namespace {
// Type of flag ownership file.
enum class FlagFile { kFlagMetadata, kFlagNeverExpire };
// Returns the filename based on the file enum.
std::string FlagFileName(FlagFile file) {
switch (file) {
case FlagFile::kFlagMetadata:
return "flag-metadata.json";
case FlagFile::kFlagNeverExpire:
return "flag-never-expire-list.json";
}
}
// Returns the JSON file contents.
base::Value FileContents(FlagFile file) {
std::string filename = FlagFileName(file);
base::FilePath metadata_path;
base::PathService::Get(base::DIR_SOURCE_ROOT, &metadata_path);
JSONFileValueDeserializer deserializer(
metadata_path.AppendASCII("chrome").AppendASCII("browser").AppendASCII(
filename));
int error_code;
std::string error_message;
std::unique_ptr<base::Value> json =
deserializer.Deserialize(&error_code, &error_message);
DCHECK(json) << "Failed to load " << filename << ": " << error_code << " "
<< error_message;
return std::move(*json);
}
// Data structure capturing the metadata of the flag.
struct FlagMetadataEntry {
std::vector<std::string> owners;
int expiry_milestone;
};
// Lookup of metadata by flag name.
using FlagMetadataMap = std::map<std::string, FlagMetadataEntry>;
// Reads the flag metadata file.
FlagMetadataMap LoadFlagMetadata() {
base::Value metadata_json = FileContents(FlagFile::kFlagMetadata);
FlagMetadataMap metadata;
for (const auto& entry : metadata_json.GetList()) {
std::string name = entry.FindKey("name")->GetString();
std::vector<std::string> owners;
if (const base::Value* e = entry.FindKey("owners")) {
for (const auto& owner : e->GetList())
owners.push_back(owner.GetString());
}
int expiry_milestone = entry.FindKey("expiry_milestone")->GetInt();
metadata[name] = FlagMetadataEntry{owners, expiry_milestone};
}
return metadata;
}
std::vector<std::string> LoadFlagNeverExpireList() {
base::Value list_json = FileContents(FlagFile::kFlagNeverExpire);
std::vector<std::string> result;
for (const auto& entry : list_json.GetList()) {
result.push_back(entry.GetString());
}
return result;
}
bool IsValidLookingOwner(base::StringPiece owner) {
// Per the specification at the top of flag-metadata.json, an owner is one of:
// 1) A string containing '@', which is treated as a full email address
// 2) A string beginning with '//', which is a path to an OWNERS file
// 3) Any other string, which is the username part of an @chromium.org email
const size_t at_pos = owner.find("@");
if (at_pos != std::string::npos) {
// If there's an @, check for a . after it. This catches one common error:
// writing "foo@" in the owners list rather than bare "foo" or full
// "foo@domain.com".
return owner.find(".", at_pos) != std::string::npos;
}
if (owner.starts_with("//")) {
// Looks like a path to a file. It would be nice to check that the file
// actually exists here, but that's not possible because when this test
// runs it runs in an isolated environment. To check for the presence of the
// file the test would need a build-time declaration that it depends on that
// file. Instead, just assume any file path ending in 'OWNERS' is valid.
// This doesn't check that the entire filename part of the path is 'OWNERS'
// because sometimes it is instead 'IPC_OWNERS' or similar.
return owner.ends_with("OWNERS");
}
// Otherwise, look for something that seems like the username part of an
// @chromium.org email. The criteria here is that it must look like an RFC5322
// "atom", which is neatly defined as any printable character *outside* a
// specific set:
// https://tools.ietf.org/html/rfc5322#section-3.2.3
//
// Note two extra wrinkles here:
// 1) while '.' IS NOT allowed in atoms by RFC5322 gmail and other mail
// handlers do allow it, so this does not reject '.'.
// 2) while '/' IS allowed in atoms by RFC5322, this is not commonly done, and
// checking for it here detects another common syntax error - namely
// writing:
// "owners": [ "foo/bar/OWNERS" ]
// where
// "owners": [ "//foo/bar/OWNERS" ]
// is meant.
return owner.find_first_of(R"(()<>[]:;@\,/)") == std::string::npos;
}
void EnsureNamesAreAlphabetical(
const std::vector<std::string>& normalized_names,
const std::vector<std::string>& names,
FlagFile file) {
if (normalized_names.size() < 2)
return;
for (size_t i = 1; i < normalized_names.size(); ++i) {
if (i == normalized_names.size() - 1) {
// The last item on the list has less context.
EXPECT_TRUE(normalized_names[i - 1] < normalized_names[i])
<< "Correct alphabetical order does not place '" << names[i]
<< "' after '" << names[i - 1] << "' in " << FlagFileName(file);
} else {
EXPECT_TRUE(normalized_names[i - 1] < normalized_names[i] &&
normalized_names[i] < normalized_names[i + 1])
<< "Correct alphabetical order does not place '" << names[i]
<< "' between '" << names[i - 1] << "' and '" << names[i + 1]
<< "' in " << FlagFileName(file);
}
}
}
std::string NormalizeName(const std::string& name) {
std::string normalized_name = base::ToLowerASCII(name);
std::replace(normalized_name.begin(), normalized_name.end(), '_', '-');
return normalized_name;
}
} // namespace
namespace flags_ui {
namespace testing {
void EnsureEveryFlagHasMetadata(const flags_ui::FeatureEntry* entries,
size_t count) {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> missing_flags;
for (size_t i = 0; i < count; ++i) {
if (metadata.count(entries[i].internal_name) == 0)
missing_flags.push_back(entries[i].internal_name);
}
std::sort(missing_flags.begin(), missing_flags.end());
EXPECT_EQ(0u, missing_flags.size())
<< "Missing flags: " << base::JoinString(missing_flags, "\n ");
}
void EnsureOnlyPermittedFlagsNeverExpire() {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> listed_flags = LoadFlagNeverExpireList();
std::vector<std::string> missing_flags;
for (const auto& entry : metadata) {
if (entry.second.expiry_milestone == -1 &&
std::find(listed_flags.begin(), listed_flags.end(), entry.first) ==
listed_flags.end()) {
missing_flags.push_back(entry.first);
}
}
std::sort(missing_flags.begin(), missing_flags.end());
EXPECT_EQ(0u, missing_flags.size())
<< "Flags not listed for no-expire: "
<< base::JoinString(missing_flags, "\n ");
}
void EnsureEveryFlagHasNonEmptyOwners() {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> sad_flags;
for (const auto& it : metadata) {
if (it.second.owners.empty())
sad_flags.push_back(it.first);
}
std::sort(sad_flags.begin(), sad_flags.end());
EXPECT_EQ(0u, sad_flags.size())
<< "Flags missing owners: " << base::JoinString(sad_flags, "\n ");
}
void EnsureOwnersLookValid() {
FlagMetadataMap metadata = LoadFlagMetadata();
std::vector<std::string> sad_flags;
for (const auto& flag : metadata) {
for (const auto& owner : flag.second.owners) {
if (!IsValidLookingOwner(owner))
sad_flags.push_back(flag.first);
}
}
EXPECT_EQ(0u, sad_flags.size()) << "Flags with invalid-looking owners: "
<< base::JoinString(sad_flags, "\n");
}
void EnsureFlagsAreListedInAlphabeticalOrder() {
base::Value metadata_json = FileContents(FlagFile::kFlagMetadata);
std::vector<std::string> normalized_names;
std::vector<std::string> names;
for (const auto& entry : metadata_json.GetList()) {
normalized_names.push_back(
NormalizeName(entry.FindKey("name")->GetString()));
names.push_back(entry.FindKey("name")->GetString());
}
EnsureNamesAreAlphabetical(normalized_names, names, FlagFile::kFlagMetadata);
base::Value expiration_json = FileContents(FlagFile::kFlagNeverExpire);
normalized_names.clear();
names.clear();
for (const auto& entry : expiration_json.GetList()) {
normalized_names.push_back(NormalizeName(entry.GetString()));
names.push_back(entry.GetString());
}
EnsureNamesAreAlphabetical(normalized_names, names,
FlagFile::kFlagNeverExpire);
}
} // namespace testing
} // namespace flags_ui
\ No newline at end of file
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_FLAGS_UI_FLAGS_TEST_HELPERS_H_
#define COMPONENTS_FLAGS_UI_FLAGS_TEST_HELPERS_H_
#include "components/flags_ui/feature_entry.h"
namespace flags_ui {
namespace testing {
// Ensures that all flags in |entries| has associated metadata. |count| is the
// number of flags in |entries|.
void EnsureEveryFlagHasMetadata(const flags_ui::FeatureEntry* entries,
size_t count);
// Ensures that all flags marked as never expiring in flag-metadata.json is
// listed in flag-never-expire-list.json.
void EnsureOnlyPermittedFlagsNeverExpire();
// Ensures that every flag has an owner.
void EnsureEveryFlagHasNonEmptyOwners();
// Ensures that owners conform to rules in flag-metadata.json.
void EnsureOwnersLookValid();
// Ensures that flags are listed in alphabetical order in flag-metadata.json and
// flag-never-expire-list.json.
void EnsureFlagsAreListedInAlphabeticalOrder();
} // namespace testing
} // namespace flags_ui
#endif // COMPONENTS_FLAGS_UI_FLAGS_TEST_HELPERS_H_
......@@ -63,3 +63,25 @@ source_set("flags") {
"//ios/web/public",
]
}
source_set("unit_tests") {
testonly = true
configs += [ "//build/config/compiler:enable_arc" ]
sources = [ "about_flags_unittest.mm" ]
deps = [
":flags",
":test_data",
"//base",
"//components/flags_ui",
"//components/flags_ui:test_support",
"//testing/gtest",
]
}
bundle_data("test_data") {
sources = [
"//chrome/browser/flag-metadata.json",
"//chrome/browser/flag-never-expire-list.json",
]
outputs = [ "{{bundle_resources_dir}}/{{source_root_relative_dir}}/{{source_file_part}}" ]
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/flags/about_flags.h"
#include "components/flags_ui/feature_entry.h"
#include "components/flags_ui/flags_test_helpers.h"
#include "testing/platform_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using AboutFlagsTest = PlatformTest;
// Makes sure that every flag has an owner and an expiry entry in
// flag-metadata.json.
// TODO(crbug.com/1058614): Enable once iOS flags are added to
// flag-metadata.json.
TEST_F(AboutFlagsTest, DISABLED_EveryFlagHasMetadata) {
size_t count;
const flags_ui::FeatureEntry* entries = testing::GetFeatureEntries(&count);
flags_ui::testing::EnsureEveryFlagHasMetadata(entries, count);
}
// Ensures that all flags marked as never expiring in flag-metadata.json is
// listed in flag-never-expire-list.json.
TEST_F(AboutFlagsTest, OnlyPermittedFlagsNeverExpire) {
flags_ui::testing::EnsureOnlyPermittedFlagsNeverExpire();
}
// Ensures that every flag has an owner.
TEST_F(AboutFlagsTest, EveryFlagHasNonEmptyOwners) {
flags_ui::testing::EnsureEveryFlagHasNonEmptyOwners();
}
// Ensures that owners conform to rules in flag-metadata.json.
TEST_F(AboutFlagsTest, OwnersLookValid) {
flags_ui::testing::EnsureOwnersLookValid();
}
// Ensures that flags are listed in alphabetical order in flag-metadata.json and
// flag-never-expire-list.json.
TEST_F(AboutFlagsTest, FlagsListedInAlphabeticalOrder) {
flags_ui::testing::EnsureFlagsAreListedInAlphabeticalOrder();
}
......@@ -182,6 +182,7 @@ test("ios_chrome_unittests") {
"//ios/chrome/browser/drag_and_drop:unit_tests",
"//ios/chrome/browser/favicon:unit_tests",
"//ios/chrome/browser/find_in_page:unit_tests",
"//ios/chrome/browser/flags:unit_tests",
"//ios/chrome/browser/geolocation:unit_tests",
"//ios/chrome/browser/history:unit_tests",
"//ios/chrome/browser/infobars:unit_tests",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment