1 // Copyright 2011 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/about_flags.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/test/metrics/histogram_enum_reader.h"
18 #include "build/build_config.h"
19 #include "chrome/common/chrome_version.h"
20 #include "components/flags_ui/feature_entry.h"
21 #include "components/flags_ui/feature_entry_macros.h"
22 #include "components/flags_ui/flags_test_helpers.h"
23 #include "components/flags_ui/flags_ui_metrics.h"
24 #include "testing/gtest/include/gtest/gtest.h"
25 #include "third_party/abseil-cpp/absl/types/optional.h"
27 namespace about_flags {
31 using Sample = base::HistogramBase::Sample;
32 using SwitchToIdMap = std::map<std::string, Sample>;
34 // Get all associated switches corresponding to defined about_flags.cc entries.
35 std::set<std::string> GetAllPublicSwitchesAndFeaturesForTesting() {
36 std::set<std::string> result;
38 for (const auto& entry : testing::GetFeatureEntries()) {
39 // Skip over flags that are part of the flags system itself - they don't
40 // have any of the usual metadata or histogram entries for flags, since they
41 // are synthesized during the build process.
42 // TODO(https://crbug.com/1068258): Remove the need for this by generating
43 // histogram entries automatically.
44 if (entry.supported_platforms & flags_ui::kFlagInfrastructure)
48 case flags_ui::FeatureEntry::SINGLE_VALUE:
49 case flags_ui::FeatureEntry::SINGLE_DISABLE_VALUE:
50 result.insert(entry.switches.command_line_switch);
52 case flags_ui::FeatureEntry::ORIGIN_LIST_VALUE:
53 case flags_ui::FeatureEntry::STRING_VALUE:
54 // Do nothing, origin list values and string values are not added as
57 case flags_ui::FeatureEntry::MULTI_VALUE:
58 for (int j = 0; j < entry.NumOptions(); ++j) {
59 result.insert(entry.ChoiceForOption(j).command_line_switch);
62 case flags_ui::FeatureEntry::ENABLE_DISABLE_VALUE:
63 result.insert(entry.switches.command_line_switch);
64 result.insert(entry.switches.disable_command_line_switch);
66 case flags_ui::FeatureEntry::FEATURE_VALUE:
67 case flags_ui::FeatureEntry::FEATURE_WITH_PARAMS_VALUE:
68 result.insert(std::string(entry.feature.feature->name) + ":enabled");
69 result.insert(std::string(entry.feature.feature->name) + ":disabled");
71 #if BUILDFLAG(IS_CHROMEOS_ASH)
72 case flags_ui::FeatureEntry::PLATFORM_FEATURE_NAME_VALUE:
73 case flags_ui::FeatureEntry::PLATFORM_FEATURE_NAME_WITH_PARAMS_VALUE:
74 std::string name(entry.platform_feature_name.name);
75 result.insert(name + ":enabled");
76 result.insert(name + ":disabled");
78 #endif // BUILDFLAG(IS_CHROMEOS_ASH)
84 // Returns all variation ids defined in flags entries.
85 std::vector<std::string> GetAllVariationIds() {
86 std::vector<std::string> variation_ids;
87 for (const auto& entry : testing::GetFeatureEntries()) {
88 // Only FEATURE_WITH_PARAMS_VALUE or PLATFORM_FEATURE_NAME_WITH_PARAMS_VALUE
89 // entries can have a variation id.
90 if (entry.type != flags_ui::FeatureEntry::FEATURE_WITH_PARAMS_VALUE
91 #if BUILDFLAG(IS_CHROMEOS_ASH)
93 flags_ui::FeatureEntry::PLATFORM_FEATURE_NAME_WITH_PARAMS_VALUE
94 #endif // BUILDFLAG(IS_CHROMEOS_ASH)
99 for (const auto& variation : entry.GetVariations()) {
100 if (variation.variation_id)
101 variation_ids.push_back(variation.variation_id);
104 return variation_ids;
107 // Returns the parsed pair: <variation_id, is_triggering>.
108 std::pair<int, bool> ParseVariationId(const std::string& variation_str) {
109 // Fail if an empty string has been supplied as variation_id.
110 EXPECT_FALSE(variation_str.empty())
111 << "Empty string used to denote variation ID. Use `nullptr` instead.";
114 bool is_triggering = variation_str[0] == 't';
116 // Fail if we could not process the integer value.
118 base::StringToInt(&variation_str[is_triggering ? 1 : 0], &variation_id))
119 << "Invalid variation string: \"" << variation_str
120 << "\": must be either `#######` or `t#######`";
122 return {variation_id, is_triggering};
127 // Makes sure there are no separators in any of the entry names.
128 TEST(AboutFlagsTest, NoSeparators) {
129 for (const auto& entry : testing::GetFeatureEntries()) {
130 const std::string name(entry.internal_name);
131 EXPECT_EQ(std::string::npos, name.find(flags_ui::testing::kMultiSeparator))
136 // Makes sure that every flag has an owner and an expiry entry in
137 // flag-metadata.json.
138 TEST(AboutFlagsTest, EveryFlagHasMetadata) {
139 flags_ui::testing::EnsureEveryFlagHasMetadata(testing::GetFeatureEntries());
142 // Ensures that all flags marked as never expiring in flag-metadata.json is
143 // listed in flag-never-expire-list.json.
144 TEST(AboutFlagsTest, OnlyPermittedFlagsNeverExpire) {
145 flags_ui::testing::EnsureOnlyPermittedFlagsNeverExpire();
148 // Ensures that every flag has an owner.
149 TEST(AboutFlagsTest, EveryFlagHasNonEmptyOwners) {
150 flags_ui::testing::EnsureEveryFlagHasNonEmptyOwners();
153 // Ensures that owners conform to rules in flag-metadata.json.
154 TEST(AboutFlagsTest, OwnersLookValid) {
155 flags_ui::testing::EnsureOwnersLookValid();
158 // For some bizarre reason, far too many people see a file filled with
159 // alphabetically-ordered items and think "hey, let me drop this new item into a
160 // random location!" Prohibit such behavior in the flags files.
161 TEST(AboutFlagsTest, FlagsListedInAlphabeticalOrder) {
162 flags_ui::testing::EnsureFlagsAreListedInAlphabeticalOrder();
165 TEST(AboutFlagsTest, EveryFlagIsValid) {
166 for (const auto& entry : testing::GetFeatureEntries()) {
167 EXPECT_TRUE(entry.IsValid()) << entry.internal_name;
171 TEST(AboutFlagsTest, RecentUnexpireFlagsArePresent) {
172 flags_ui::testing::EnsureRecentUnexpireFlagsArePresent(
173 testing::GetFeatureEntries(), CHROME_VERSION_MAJOR);
176 // Ensures that all variation IDs specified are well-formed.
177 // - Variation IDs may be re-used, when multiple variants change client-side
179 // - Variation IDs must be associated with the appropriate pool of valid numbers
180 TEST(AboutFlagsTest, VariationIdsAreValid) {
181 std::set<int> nontriggering_variation_ids;
182 std::set<int> triggering_variation_ids;
184 // See: go/finch-allocating-gws-ids.
185 int LOWER_VALID_VARIATION_ID = 3340000;
186 int UPPER_VALID_VARIATION_ID = 3399999;
188 for (const std::string& variation_str : GetAllVariationIds()) {
189 auto [variation_id, is_triggering] = ParseVariationId(variation_str);
190 // Reject variation IDs used both as triggering and non-triggering.
191 // This is generally considered invalid.
193 // Triggering, but already recorded as visible.
194 (is_triggering && nontriggering_variation_ids.contains(variation_id)) ||
195 // Visible, but already recorded as triggering.
196 (!is_triggering && triggering_variation_ids.contains(variation_id)))
197 << "Variation ID \"" << variation_id
198 << "\" used both as triggering and "
199 << "non-triggering.";
201 EXPECT_TRUE(variation_id >= LOWER_VALID_VARIATION_ID &&
202 variation_id <= UPPER_VALID_VARIATION_ID)
203 << "Variation ID \"" << variation_id << "\" falls outside of range of "
204 << "valid variation IDs: [" << LOWER_VALID_VARIATION_ID << ", "
205 << UPPER_VALID_VARIATION_ID << "].";
208 triggering_variation_ids.insert(variation_id);
210 nontriggering_variation_ids.insert(variation_id);
215 // Test that ScopedFeatureEntries restores existing feature entries on
217 TEST(AboutFlagsTest, ScopedFeatureEntriesRestoresFeatureEntries) {
218 const base::span<const flags_ui::FeatureEntry> old_entries =
219 testing::GetFeatureEntries();
220 EXPECT_GT(old_entries.size(), 0U);
221 const char* first_feature_name = old_entries[0].internal_name;
223 static BASE_FEATURE(kTestFeature1, "FeatureName1",
224 base::FEATURE_ENABLED_BY_DEFAULT);
225 testing::ScopedFeatureEntries feature_entries(
226 {{"feature-1", "", "", flags_ui::FlagsState::GetCurrentPlatform(),
227 FEATURE_VALUE_TYPE(kTestFeature1)}});
228 EXPECT_EQ(testing::GetFeatureEntries().size(), 1U);
231 const base::span<const flags_ui::FeatureEntry> new_entries =
232 testing::GetFeatureEntries();
233 EXPECT_EQ(old_entries.size(), new_entries.size());
234 EXPECT_TRUE(about_flags::GetCurrentFlagsState()->FindFeatureEntryByName(
235 first_feature_name));
238 class AboutFlagsHistogramTest : public ::testing::Test {
240 // This is a helper function to check that all IDs in enum LoginCustomFlags in
241 // histograms.xml are unique.
242 void SetSwitchToHistogramIdMapping(const std::string& switch_name,
243 const Sample switch_histogram_id,
244 std::map<std::string, Sample>* out_map) {
245 const std::pair<std::map<std::string, Sample>::iterator, bool> status =
246 out_map->insert(std::make_pair(switch_name, switch_histogram_id));
247 if (!status.second) {
248 EXPECT_TRUE(status.first->second == switch_histogram_id)
249 << "Duplicate switch '" << switch_name
250 << "' found in enum 'LoginCustomFlags' in "
251 "tools/metrics/histograms/enums.xml.";
255 // This method generates a hint for the user for what string should be added
256 // to the enum LoginCustomFlags to make in consistent.
257 std::string GetHistogramEnumEntryText(const std::string& switch_name,
259 return base::StringPrintf(
260 "<int value=\"%d\" label=\"%s\"/>", value, switch_name.c_str());
264 TEST_F(AboutFlagsHistogramTest, CheckHistograms) {
265 absl::optional<base::HistogramEnumEntryMap> login_custom_flags =
266 base::ReadEnumFromEnumsXml("LoginCustomFlags");
267 ASSERT_TRUE(login_custom_flags)
268 << "Error reading enum 'LoginCustomFlags' from "
269 "tools/metrics/histograms/enums.xml.";
271 // Build reverse map {switch_name => id} from login_custom_flags.
272 SwitchToIdMap metadata_switches_ids;
275 login_custom_flags->count(flags_ui::testing::kBadSwitchFormatHistogramId))
276 << "Entry for UMA ID of incorrect command-line flag is not found in "
277 "tools/metrics/histograms/enums.xml enum LoginCustomFlags. "
278 "Consider adding entry:\n"
279 << " " << GetHistogramEnumEntryText("BAD_FLAG_FORMAT", 0);
280 // Check that all LoginCustomFlags entries have correct values.
281 for (const auto& entry : *login_custom_flags) {
282 if (entry.first == flags_ui::testing::kBadSwitchFormatHistogramId) {
283 // Add error value with empty name.
284 SetSwitchToHistogramIdMapping(std::string(), entry.first,
285 &metadata_switches_ids);
288 const Sample uma_id = flags_ui::GetSwitchUMAId(entry.second);
289 EXPECT_EQ(uma_id, entry.first)
290 << "tools/metrics/histograms/enums.xml enum LoginCustomFlags "
292 << entry.second << "' has incorrect value=" << entry.first << ", but "
293 << uma_id << " is expected. Consider changing entry to:\n"
294 << " " << GetHistogramEnumEntryText(entry.second, uma_id);
295 SetSwitchToHistogramIdMapping(entry.second, entry.first,
296 &metadata_switches_ids);
299 // Check that all flags in about_flags.cc have entries in login_custom_flags.
300 std::set<std::string> all_flags = GetAllPublicSwitchesAndFeaturesForTesting();
301 for (const std::string& flag : all_flags) {
302 // Skip empty placeholders.
305 const Sample uma_id = flags_ui::GetSwitchUMAId(flag);
306 EXPECT_NE(flags_ui::testing::kBadSwitchFormatHistogramId, uma_id)
307 << "Command-line switch '" << flag
308 << "' from about_flags.cc has UMA ID equal to reserved value "
309 "kBadSwitchFormatHistogramId="
310 << flags_ui::testing::kBadSwitchFormatHistogramId
311 << ". Please modify switch name.";
312 auto enum_entry = metadata_switches_ids.lower_bound(flag);
314 // Ignore case here when switch ID is incorrect - it has already been
315 // reported in the previous loop.
316 EXPECT_TRUE(enum_entry != metadata_switches_ids.end() &&
317 enum_entry->first == flag)
318 << "tools/metrics/histograms/enums.xml enum LoginCustomFlags doesn't "
320 << flag << "' (value=" << uma_id << " expected). Consider running:\n"
321 << " tools/metrics/histograms/generate_flag_enums.py --feature "
322 << flag.substr(0, flag.find(":")) << "\nOr manually adding the entry:\n"
323 << " " << GetHistogramEnumEntryText(flag, uma_id);
327 } // namespace about_flags