Extend FieldTrials api

Intent of new api is to replace
field_trial::FieldTrialsStringIsValid with FieldTrials::Create
field_trial::MergeFieldTrialsStrings with FieldTrials::Merge and FieldTrials::Set

Thus allow to assemble field trials without relying on functions designed for creating global field trials string.

Bug: webrtc:42220378
Change-Id: Icc10b202f4d9c8d540fe579bd213d3c83c415263
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/392461
Reviewed-by: Tomas Gunnarsson <tommi@webrtc.org>
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#44700}
diff --git a/api/BUILD.gn b/api/BUILD.gn
index d0e6394..d7d103d 100644
--- a/api/BUILD.gn
+++ b/api/BUILD.gn
@@ -1712,6 +1712,8 @@
     ":field_trials_registry",
     "../rtc_base:checks",
     "../rtc_base/containers:flat_map",
+    "//third_party/abseil-cpp/absl/base:nullability",
+    "//third_party/abseil-cpp/absl/memory",
     "//third_party/abseil-cpp/absl/strings:string_view",
   ]
 }
diff --git a/api/field_trials.cc b/api/field_trials.cc
index c39d04c..fc896e1 100644
--- a/api/field_trials.cc
+++ b/api/field_trials.cc
@@ -10,36 +10,78 @@
 
 #include "api/field_trials.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
+#include "absl/base/nullability.h"
+#include "absl/memory/memory.h"
 #include "absl/strings/string_view.h"
 #include "rtc_base/checks.h"
 #include "rtc_base/containers/flat_map.h"
 
 namespace webrtc {
+namespace {
+
+absl::string_view NextKeyOrValue(absl::string_view& s) {
+  absl::string_view::size_type separator_pos = s.find('/');
+  if (separator_pos == absl::string_view::npos) {
+    // Missing separator '/' after field trial key or value.
+    return "";
+  }
+  absl::string_view result = s.substr(0, separator_pos);
+  s.remove_prefix(separator_pos + 1);
+  return result;
+}
+
+bool Parse(absl::string_view s,
+           flat_map<std::string, std::string>& key_value_map) {
+  while (!s.empty()) {
+    absl::string_view key = NextKeyOrValue(s);
+    absl::string_view value = NextKeyOrValue(s);
+    if (key.empty() || value.empty()) {
+      return false;
+    }
+
+    auto it = key_value_map.emplace(key, value).first;
+    if (it->second != value) {
+      // Duplicate trials with different values is not fine.
+      return false;
+    }
+  }
+  return true;
+}
+
+}  // namespace
+
+absl_nullable std::unique_ptr<FieldTrials> FieldTrials::Create(
+    absl::string_view s) {
+  flat_map<std::string, std::string> key_value_map;
+  if (!Parse(s, key_value_map)) {
+    return nullptr;
+  }
+  // Using `new` to access a private constructor.
+  return absl::WrapUnique(new FieldTrials(std::move(key_value_map)));
+}
 
 FieldTrials::FieldTrials(absl::string_view s) {
-  while (!s.empty()) {
-    absl::string_view::size_type separator_pos = s.find('/');
-    RTC_CHECK_NE(separator_pos, absl::string_view::npos)
-        << "Missing separator '/' after field trial key.";
-    RTC_CHECK_GT(separator_pos, 0) << "Field trial key cannot be empty.";
-    absl::string_view key = s.substr(0, separator_pos);
-    s.remove_prefix(separator_pos + 1);
+  RTC_CHECK(Parse(s, key_value_map_));
+}
 
-    RTC_CHECK(!s.empty())
-        << "Missing value after field trial key. String ended.";
-    separator_pos = s.find('/');
-    RTC_CHECK_NE(separator_pos, absl::string_view::npos)
-        << "Missing terminating '/' in field trial string.";
-    RTC_CHECK_GT(separator_pos, 0) << "Field trial value cannot be empty.";
-    absl::string_view value = s.substr(0, separator_pos);
-    s.remove_prefix(separator_pos + 1);
+void FieldTrials::Merge(const FieldTrials& other) {
+  for (const auto& [trial, group] : other.key_value_map_) {
+    key_value_map_.insert_or_assign(trial, group);
+  }
+}
 
-    // If a key is specified multiple times, only the value linked to the first
-    // key is stored.
-    key_value_map_.emplace(key, value);
+void FieldTrials::Set(absl::string_view trial, absl::string_view group) {
+  RTC_CHECK(!trial.empty());
+  RTC_CHECK_EQ(trial.find('/'), absl::string_view::npos);
+  RTC_CHECK_EQ(group.find('/'), absl::string_view::npos);
+  if (group.empty()) {
+    key_value_map_.erase(trial);
+  } else {
+    key_value_map_.insert_or_assign(trial, group);
   }
 }
 
diff --git a/api/field_trials.h b/api/field_trials.h
index ff42c32..4937b50 100644
--- a/api/field_trials.h
+++ b/api/field_trials.h
@@ -13,7 +13,9 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 
+#include "absl/base/nullability.h"
 #include "absl/strings/string_view.h"
 #include "api/field_trials_registry.h"
 #include "rtc_base/containers/flat_map.h"
@@ -32,10 +34,41 @@
 // The field trials are injected into objects that use them at creation time.
 class FieldTrials : public FieldTrialsRegistry {
  public:
+  // Creates field trials from a valid field trial string.
+  // Returns nullptr if the string is invalid.
+  // E.g., valid string:
+  //   "WebRTC-ExperimentFoo/Enabled/WebRTC-ExperimentBar/Enabled100kbps/"
+  //   Assigns to group "Enabled" on WebRTC-ExperimentFoo trial
+  //   and to group "Enabled100kbps" on WebRTC-ExperimentBar.
+  //
+  // E.g., invalid string:
+  //   "WebRTC-experiment1/Enabled"  (note missing / separator at the end).
+  static absl_nullable std::unique_ptr<FieldTrials> Create(absl::string_view s);
+
+  // Creates field trials from a string.
+  // It is an error to call the constructor with an invalid field trial string.
   explicit FieldTrials(absl::string_view s);
+
+  FieldTrials(const FieldTrials&) = default;
   FieldTrials(FieldTrials&&) = default;
+  FieldTrials& operator=(const FieldTrials&) = default;
   FieldTrials& operator=(FieldTrials&&) = default;
-  ~FieldTrials() = default;
+
+  ~FieldTrials() override = default;
+
+  template <typename Sink>
+  friend void AbslStringify(Sink& sink, const FieldTrials& self);
+
+  // Merges field trials from the `other` into this.
+  //
+  // If a key (trial) exists twice with conflicting values (groups), the value
+  // in `other` takes precedence.
+  void Merge(const FieldTrials& other);
+
+  // Sets value (`group`) for an indvidual `trial`.
+  // It is an error to call this function with an invalid `trial` or `group`.
+  // Setting empty `group` is valid and removes the `trial`.
+  void Set(absl::string_view trial, absl::string_view group);
 
   // TODO: bugs.webrtc.org/42220378 - Deprecate and inline once no longer used
   // within webrtc.
@@ -44,11 +77,27 @@
   }
 
  private:
+  explicit FieldTrials(flat_map<std::string, std::string> key_value_map)
+      : key_value_map_(std::move(key_value_map)) {}
+
   std::string GetValue(absl::string_view key) const override;
 
   flat_map<std::string, std::string> key_value_map_;
 };
 
+template <typename Sink>
+void AbslStringify(Sink& sink, const FieldTrials& self) {
+  for (const auto& [trial, group] : self.key_value_map_) {
+    sink.Append(trial);
+    sink.Append("/");
+    sink.Append(group);
+    // Intentionally output a string that is not a valid field trial string.
+    // Stringification is intended only for human readable logs, and is not
+    // intended for reusing as `FieldTrials` construction parameter.
+    sink.Append("//");
+  }
+}
+
 }  // namespace webrtc
 
 #endif  // API_FIELD_TRIALS_H_
diff --git a/api/field_trials_unittest.cc b/api/field_trials_unittest.cc
index 9d49252..d3521b4 100644
--- a/api/field_trials_unittest.cc
+++ b/api/field_trials_unittest.cc
@@ -10,10 +10,11 @@
 
 #include "api/field_trials.h"
 
-
+#include "absl/strings/str_cat.h"
 #include "rtc_base/containers/flat_set.h"
 #include "system_wrappers/include/field_trial.h"
 #include "test/field_trial.h"
+#include "test/gmock.h"
 #include "test/gtest.h"
 
 namespace webrtc {
@@ -21,6 +22,11 @@
 
 using field_trial::FieldTrialsAllowedInScopeForTesting;
 using test::ScopedFieldTrials;
+using ::testing::AllOf;
+using ::testing::HasSubstr;
+using ::testing::IsNull;
+using ::testing::Not;
+using ::testing::NotNull;
 
 TEST(FieldTrialsTest, EmptyStringHasNoEffect) {
   FieldTrialsAllowedInScopeForTesting k({"MyCoolTrial"});
@@ -89,5 +95,103 @@
   EXPECT_TRUE(f.IsEnabled("SomeOtherString"));
 }
 
+TEST(FieldTrialsTest, CreateAcceptsValidInputs) {
+  EXPECT_THAT(FieldTrials::Create(""), NotNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/"), NotNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/Video/Disabled/"), NotNull());
+
+  // Duplicate trials with the same value is fine
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/Audio/Enabled/"), NotNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/B/C/Audio/Enabled/"),
+              NotNull());
+}
+
+TEST(FieldTrialsTest, CreateRejectsBadInputs) {
+  // Bad delimiters
+  EXPECT_THAT(FieldTrials::Create("Audio/EnabledVideo/Disabled/"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled//Video/Disabled/"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("/Audio/Enabled/Video/Disabled/"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/Video/Disabled"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/Video/Disabled/garbage"),
+              IsNull());
+
+  // Empty trial or group
+  EXPECT_THAT(FieldTrials::Create("Audio//"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("/Enabled/"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("//"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("//Enabled"), IsNull());
+
+  // Duplicate trials with different values is not fine
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/Audio/Disabled/"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/B/C/Audio/Disabled/"),
+              IsNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/Audio/Disabled/"), IsNull());
+  EXPECT_THAT(FieldTrials::Create("Audio/Enabled/B/C/Audio/Disabled/"),
+              IsNull());
+}
+
+TEST(FieldTrialsTest, StringfiyMentionsKeysAndValues) {
+  // Exact format of the stringification is undefined.
+  EXPECT_THAT(absl::StrCat(FieldTrials("Audio/Enabled/Video/Value/")),
+              AllOf(HasSubstr("Audio"), HasSubstr("Enabled"),
+                    HasSubstr("Video"), HasSubstr("Value")));
+}
+
+TEST(FieldTrialsTest, MergeCombinesFieldTrials) {
+  FieldTrials f("Video/Value1/");
+  FieldTrials other("Audio/Value2/");
+
+  f.Merge(other);
+
+  f.RegisterKeysForTesting({"Audio", "Video"});
+  EXPECT_EQ(f.Lookup("Video"), "Value1");
+  EXPECT_EQ(f.Lookup("Audio"), "Value2");
+}
+
+TEST(FieldTrialsTest, MergeGivesPrecedenceToOther) {
+  FieldTrials f("Audio/Disabled/Video/Enabled/");
+  FieldTrials other("Audio/Enabled/");
+
+  f.Merge(other);
+
+  f.RegisterKeysForTesting({"Audio"});
+  EXPECT_EQ(f.Lookup("Audio"), "Enabled");
+}
+
+TEST(FieldTrialsTest, MergeDoesntChangeTrialAbsentInOther) {
+  FieldTrials f("Audio/Enabled/Video/Enabled/");
+  FieldTrials other("Audio/Enabled/");
+
+  f.Merge(other);
+
+  f.RegisterKeysForTesting({"Video"});
+  EXPECT_EQ(f.Lookup("Video"), "Enabled");
+}
+
+TEST(FieldTrialsTest, SetUpdatesTrial) {
+  FieldTrials f("Audio/Enabled/Video/Enabled/");
+
+  f.Set("Audio", "Disabled");
+
+  f.RegisterKeysForTesting({"Audio"});
+  EXPECT_EQ(f.Lookup("Audio"), "Disabled");
+}
+
+TEST(FieldTrialsTest, SettingEmptyValueRemovesFieldTrial) {
+  FieldTrials f("Audio/Enabled/Video/Enabled/");
+
+  f.Set("Audio", "");
+
+  f.RegisterKeysForTesting({"Audio"});
+  EXPECT_EQ(f.Lookup("Audio"), "");
+  EXPECT_THAT(absl::StrCat(f), Not(HasSubstr("Audio")));
+
+  // Absent field trials shouldn't override previous value during merge.
+  FieldTrials f2("Audio/Disabled/");
+  f2.Merge(f);
+  f2.RegisterKeysForTesting({"Audio"});
+  EXPECT_EQ(f2.Lookup("Audio"), "Disabled");
+}
+
 }  // namespace
 }  // namespace webrtc