Add support for lists to the FieldTrialParser.

List elements are separated by a |. If the key is given without a : we
treat that as a empty list.

We also support parsing multiple lists as a list-of-structs, see the
unit test for usage examples.

Bug: webrtc:9346
Change-Id: I32d3ce612fef476b1c481c00a893d7fa2f339e92
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/130464
Reviewed-by: Sebastian Jansson <srte@webrtc.org>
Commit-Queue: Jonas Olsson <jonasolsson@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#27560}
diff --git a/rtc_base/experiments/BUILD.gn b/rtc_base/experiments/BUILD.gn
index b8709dc..70dcb5b 100644
--- a/rtc_base/experiments/BUILD.gn
+++ b/rtc_base/experiments/BUILD.gn
@@ -40,6 +40,8 @@
 
 rtc_static_library("field_trial_parser") {
   sources = [
+    "field_trial_list.cc",
+    "field_trial_list.h",
     "field_trial_parser.cc",
     "field_trial_parser.h",
     "field_trial_units.cc",
@@ -51,6 +53,7 @@
     "../../api/units:time_delta",
     "../../rtc_base:checks",
     "../../rtc_base:logging",
+    "../../rtc_base:stringutils",
     "//third_party/abseil-cpp/absl/types:optional",
   ]
 }
@@ -151,6 +154,7 @@
 
     sources = [
       "cpu_speed_experiment_unittest.cc",
+      "field_trial_list_unittest.cc",
       "field_trial_parser_unittest.cc",
       "field_trial_units_unittest.cc",
       "keyframe_interval_settings_unittest.cc",
diff --git a/rtc_base/experiments/field_trial_list.cc b/rtc_base/experiments/field_trial_list.cc
new file mode 100644
index 0000000..de897bf
--- /dev/null
+++ b/rtc_base/experiments/field_trial_list.cc
@@ -0,0 +1,57 @@
+/*
+ *  Copyright 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "rtc_base/experiments/field_trial_list.h"
+
+namespace webrtc {
+
+FieldTrialListBase::FieldTrialListBase(std::string key)
+    : FieldTrialParameterInterface(key),
+      failed_(false),
+      parse_got_called_(false) {}
+
+bool FieldTrialListBase::Failed() const {
+  return failed_;
+}
+bool FieldTrialListBase::Used() const {
+  return parse_got_called_;
+}
+
+int FieldTrialListWrapper::Length() {
+  return GetList()->Size();
+}
+bool FieldTrialListWrapper::Failed() {
+  return GetList()->Failed();
+}
+bool FieldTrialListWrapper::Used() {
+  return GetList()->Used();
+}
+
+bool FieldTrialStructListBase::Parse(absl::optional<std::string> str_value) {
+  RTC_NOTREACHED();
+  return true;
+}
+
+int FieldTrialStructListBase::ValidateAndGetLength() {
+  int length = -1;
+  for (std::unique_ptr<FieldTrialListWrapper>& list : sub_lists_) {
+    if (list->Failed())
+      return -1;
+    else if (!list->Used())
+      continue;
+    else if (length == -1)
+      length = list->Length();
+    else if (length != list->Length())
+      return -1;
+  }
+
+  return length;
+}
+
+}  // namespace webrtc
diff --git a/rtc_base/experiments/field_trial_list.h b/rtc_base/experiments/field_trial_list.h
new file mode 100644
index 0000000..cfbcce9
--- /dev/null
+++ b/rtc_base/experiments/field_trial_list.h
@@ -0,0 +1,224 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef RTC_BASE_EXPERIMENTS_FIELD_TRIAL_LIST_H_
+#define RTC_BASE_EXPERIMENTS_FIELD_TRIAL_LIST_H_
+
+#include <initializer_list>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "rtc_base/experiments/field_trial_parser.h"
+#include "rtc_base/string_encode.h"
+
+// List support for field trial strings. FieldTrialList and FieldTrialStructList
+// are used similarly to the other FieldTrialParameters, but take a variable
+// number of parameters. A FieldTrialList<T> parses a |-delimeted string into a
+// list of T, using ParseTypedParameter to parse the individual tokens.
+// Example string: "my_list:1|2|3,empty_list,other_list:aardvark".
+
+// A FieldTrialStructList combines multiple lists into a list-of-structs. It
+// ensures that all its sublists parse correctly and have the same length, then
+// uses user-supplied accessor functions to write those elements into structs of
+// a user-supplied type.
+
+// See the unit test for usage and behavior.
+
+namespace webrtc {
+
+class FieldTrialListBase : public FieldTrialParameterInterface {
+ protected:
+  friend class FieldTrialListWrapper;
+  explicit FieldTrialListBase(std::string key);
+
+  bool Failed() const;
+  bool Used() const;
+
+  virtual int Size() = 0;
+
+  bool failed_;
+  bool parse_got_called_;
+};
+
+// This class represents a vector of type T. The elements are separated by a |
+// and parsed using ParseTypedParameter.
+template <typename T>
+class FieldTrialList : public FieldTrialListBase {
+ public:
+  explicit FieldTrialList(std::string key) : FieldTrialList(key, {}) {}
+  FieldTrialList(std::string key, std::initializer_list<T> default_values)
+      : FieldTrialListBase(key), values_(default_values) {}
+
+  std::vector<T> Get() const { return values_; }
+  operator std::vector<T>() const { return Get(); }
+  const T& operator[](size_t index) const { return values_[index]; }
+  const std::vector<T>* operator->() const { return &values_; }
+
+ protected:
+  bool Parse(absl::optional<std::string> str_value) override {
+    parse_got_called_ = true;
+
+    if (!str_value) {
+      values_.clear();
+      return true;
+    }
+
+    std::vector<std::string> tokens;
+    std::vector<T> new_values_;
+    rtc::split(str_value.value(), '|', &tokens);
+
+    for (std::string token : tokens) {
+      absl::optional<T> value = ParseTypedParameter<T>(token);
+      if (value) {
+        new_values_.push_back(*value);
+      } else {
+        failed_ = true;
+        return false;
+      }
+    }
+
+    values_.swap(new_values_);
+    return true;
+  }
+
+  int Size() override { return values_.size(); }
+
+ private:
+  std::vector<T> values_;
+};
+
+class FieldTrialListWrapper {
+ public:
+  virtual ~FieldTrialListWrapper() = default;
+
+  // Takes the element at the given index in the wrapped list and writes it to
+  // the given struct.
+  virtual void WriteElement(void* struct_to_write, int index) = 0;
+
+  virtual FieldTrialListBase* GetList() = 0;
+
+  int Length();
+
+  // Returns true iff the wrapped list has failed to parse at least one token.
+  bool Failed();
+
+  bool Used();
+
+ protected:
+  FieldTrialListWrapper() = default;
+};
+
+namespace field_trial_list_impl {
+// The LambdaTypeTraits struct provides type information about lambdas in the
+// template expressions below.
+template <typename T>
+struct LambdaTypeTraits : public LambdaTypeTraits<decltype(&T::operator())> {};
+
+template <typename ClassType, typename RetType, typename SourceType>
+struct LambdaTypeTraits<RetType* (ClassType::*)(SourceType*)const> {
+  using ret = RetType;
+  using src = SourceType;
+};
+
+template <typename T>
+struct TypedFieldTrialListWrapper : FieldTrialListWrapper {
+ public:
+  TypedFieldTrialListWrapper(std::string key,
+                             std::function<void(void*, T)> sink)
+      : list_(key), sink_(sink) {}
+
+  void WriteElement(void* struct_to_write, int index) override {
+    sink_(struct_to_write, list_[index]);
+  }
+
+  FieldTrialListBase* GetList() { return &list_; }
+
+ private:
+  FieldTrialList<T> list_;
+  std::function<void(void*, T)> sink_;
+};
+
+}  // namespace field_trial_list_impl
+
+template <typename F,
+          typename Traits = typename field_trial_list_impl::LambdaTypeTraits<F>>
+FieldTrialListWrapper* FieldTrialStructMember(std::string key, F accessor) {
+  return new field_trial_list_impl::TypedFieldTrialListWrapper<
+      typename Traits::ret>(key, [accessor](void* s, typename Traits::ret t) {
+    *accessor(static_cast<typename Traits::src*>(s)) = t;
+  });
+}
+
+// This base class is here to reduce the amount of code we have to generate for
+// each type of FieldTrialStructList.
+class FieldTrialStructListBase : public FieldTrialParameterInterface {
+ protected:
+  FieldTrialStructListBase(
+      std::initializer_list<FieldTrialListWrapper*> sub_lists)
+      : FieldTrialParameterInterface(""), sub_lists_() {
+    // Take ownership of the list wrappers generated by FieldTrialStructMember
+    // on the call site.
+    for (FieldTrialListWrapper* const* it = sub_lists.begin();
+         it != sub_lists.end(); it++) {
+      sub_parameters_.push_back((*it)->GetList());
+      sub_lists_.push_back(std::unique_ptr<FieldTrialListWrapper>(*it));
+    }
+  }
+
+  // Check that all of our sublists that were in the field trial string had the
+  // same number of elements. If they do, we return that length. If they had
+  // different lengths, any sublist had parse failures or no sublists had
+  // user-supplied values, we return -1.
+  int ValidateAndGetLength();
+
+  bool Parse(absl::optional<std::string> str_value) override;
+
+  std::vector<std::unique_ptr<FieldTrialListWrapper>> sub_lists_;
+};
+
+template <typename S>
+class FieldTrialStructList : public FieldTrialStructListBase {
+ public:
+  FieldTrialStructList(std::initializer_list<FieldTrialListWrapper*> l,
+                       std::initializer_list<S> default_list)
+      : FieldTrialStructListBase(l), values_(default_list) {}
+
+  std::vector<S> Get() const { return values_; }
+  operator std::vector<S>() const { return Get(); }
+  const S& operator[](size_t index) const { return values_[index]; }
+  const std::vector<S>* operator->() const { return &values_; }
+
+ protected:
+  void ParseDone() override {
+    int length = ValidateAndGetLength();
+
+    if (length == -1)
+      return;
+
+    std::vector<S> new_values(length, S());
+
+    for (std::unique_ptr<FieldTrialListWrapper>& li : sub_lists_) {
+      if (li->Used()) {
+        for (int i = 0; i < length; i++) {
+          li->WriteElement(&new_values[i], i);
+        }
+      }
+    }
+
+    values_.swap(new_values);
+  }
+
+ private:
+  std::vector<S> values_;
+};
+
+}  // namespace webrtc
+
+#endif  // RTC_BASE_EXPERIMENTS_FIELD_TRIAL_LIST_H_
diff --git a/rtc_base/experiments/field_trial_list_unittest.cc b/rtc_base/experiments/field_trial_list_unittest.cc
new file mode 100644
index 0000000..a1abfe4
--- /dev/null
+++ b/rtc_base/experiments/field_trial_list_unittest.cc
@@ -0,0 +1,133 @@
+/*
+ *  Copyright 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "rtc_base/experiments/field_trial_list.h"
+
+#include "rtc_base/gunit.h"
+#include "test/gmock.h"
+
+using testing::ElementsAre;
+using testing::IsEmpty;
+
+namespace webrtc {
+
+struct Garment {
+  int price = 0;
+  std::string color = "";
+
+  // Only needed for testing.
+  Garment() = default;
+  Garment(int p, std::string c) : price(p), color(c) {}
+
+  bool operator==(const Garment& other) const {
+    return price == other.price && color == other.color;
+  }
+};
+
+TEST(FieldTrialListTest, ParsesListParameter) {
+  FieldTrialList<int> my_list("l", {5});
+  EXPECT_THAT(my_list.Get(), ElementsAre(5));
+  // If one element is invalid the list is unchanged.
+  ParseFieldTrial({&my_list}, "l:1|2|hat");
+  EXPECT_THAT(my_list.Get(), ElementsAre(5));
+  ParseFieldTrial({&my_list}, "l");
+  EXPECT_THAT(my_list.Get(), IsEmpty());
+  ParseFieldTrial({&my_list}, "l:1|2|3");
+  EXPECT_THAT(my_list.Get(), ElementsAre(1, 2, 3));
+  ParseFieldTrial({&my_list}, "l:-1");
+  EXPECT_THAT(my_list.Get(), ElementsAre(-1));
+
+  FieldTrialList<std::string> another_list("l", {"hat"});
+  EXPECT_THAT(another_list.Get(), ElementsAre("hat"));
+  ParseFieldTrial({&another_list}, "l");
+  EXPECT_THAT(another_list.Get(), IsEmpty());
+  ParseFieldTrial({&another_list}, "l:");
+  EXPECT_THAT(another_list.Get(), ElementsAre(""));
+  ParseFieldTrial({&another_list}, "l:scarf|hat|mittens");
+  EXPECT_THAT(another_list.Get(), ElementsAre("scarf", "hat", "mittens"));
+  ParseFieldTrial({&another_list}, "l:scarf");
+  EXPECT_THAT(another_list.Get(), ElementsAre("scarf"));
+}
+
+// Normal usage.
+TEST(FieldTrialListTest, ParsesStructList) {
+  FieldTrialStructList<Garment> my_list(
+      {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }),
+       FieldTrialStructMember("price", [](Garment* g) { return &g->price; })},
+      {{1, "blue"}, {2, "red"}});
+
+  ParseFieldTrial({&my_list},
+                  "color:mauve|red|gold,"
+                  "price:10|20|30,"
+                  "other_param:asdf");
+
+  ASSERT_THAT(my_list.Get(),
+              ElementsAre(Garment{10, "mauve"}, Garment{20, "red"},
+                          Garment{30, "gold"}));
+}
+
+// One FieldTrialList has the wrong length, so we use the user-provided default
+// list.
+TEST(FieldTrialListTest, StructListKeepsDefaultWithMismatchingLength) {
+  FieldTrialStructList<Garment> my_list(
+      {FieldTrialStructMember("wrong_length",
+                              [](Garment* g) { return &g->color; }),
+       FieldTrialStructMember("price", [](Garment* g) { return &g->price; })},
+      {{1, "blue"}, {2, "red"}});
+
+  ParseFieldTrial({&my_list},
+                  "wrong_length:mauve|magenta|chartreuse|indigo,"
+                  "garment:hat|hat|crown,"
+                  "price:10|20|30");
+
+  ASSERT_THAT(my_list.Get(),
+              ElementsAre(Garment{1, "blue"}, Garment{2, "red"}));
+}
+
+// One list is missing. We set the values we're given, and the others remain
+// as whatever the Garment default constructor set them to.
+TEST(FieldTrialListTest, StructListUsesDefaultForMissingList) {
+  FieldTrialStructList<Garment> my_list(
+      {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }),
+       FieldTrialStructMember("price", [](Garment* g) { return &g->price; })},
+      {{1, "blue"}, {2, "red"}});
+
+  ParseFieldTrial({&my_list}, "price:10|20|30");
+
+  ASSERT_THAT(my_list.Get(),
+              ElementsAre(Garment{10, ""}, Garment{20, ""}, Garment{30, ""}));
+}
+
+// The user haven't provided values for any lists, so we use the default list.
+TEST(FieldTrialListTest, StructListUsesDefaultListWithoutValues) {
+  FieldTrialStructList<Garment> my_list(
+      {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }),
+       FieldTrialStructMember("price", [](Garment* g) { return &g->price; })},
+      {{1, "blue"}, {2, "red"}});
+
+  ParseFieldTrial({&my_list}, "");
+
+  ASSERT_THAT(my_list.Get(),
+              ElementsAre(Garment{1, "blue"}, Garment{2, "red"}));
+}
+
+// Some lists are provided and all are empty, so we return a empty list.
+TEST(FieldTrialListTest, StructListHandlesEmptyLists) {
+  FieldTrialStructList<Garment> my_list(
+      {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }),
+       FieldTrialStructMember("price", [](Garment* g) { return &g->price; })},
+      {{1, "blue"}, {2, "red"}});
+
+  ParseFieldTrial({&my_list}, "color,price");
+
+  ASSERT_EQ(my_list.Get().size(), 0u);
+}
+
+}  // namespace webrtc
diff --git a/rtc_base/experiments/field_trial_parser.cc b/rtc_base/experiments/field_trial_parser.cc
index f823b48..b2c48c9 100644
--- a/rtc_base/experiments/field_trial_parser.cc
+++ b/rtc_base/experiments/field_trial_parser.cc
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *  Copyright 2019 The WebRTC project authors. All Rights Reserved.
  *
  *  Use of this source code is governed by a BSD-style license
  *  that can be found in the LICENSE file in the root of the source
@@ -33,9 +33,6 @@
   RTC_DCHECK(used_) << "Field trial parameter with key: '" << key_
                     << "' never used.";
 }
-std::string FieldTrialParameterInterface::Key() const {
-  return key_;
-}
 
 void ParseFieldTrial(
     std::initializer_list<FieldTrialParameterInterface*> fields,
@@ -44,13 +41,23 @@
   FieldTrialParameterInterface* keyless_field = nullptr;
   for (FieldTrialParameterInterface* field : fields) {
     field->MarkAsUsed();
-    if (field->Key().empty()) {
+    if (!field->sub_parameters_.empty()) {
+      for (FieldTrialParameterInterface* sub_field : field->sub_parameters_) {
+        RTC_DCHECK(!sub_field->key_.empty());
+        sub_field->MarkAsUsed();
+        field_map[sub_field->key_] = sub_field;
+      }
+      continue;
+    }
+
+    if (field->key_.empty()) {
       RTC_DCHECK(!keyless_field);
       keyless_field = field;
     } else {
-      field_map[field->Key()] = field;
+      field_map[field->key_] = field;
     }
   }
+
   size_t i = 0;
   while (i < trial_string.length()) {
     int val_end = FindOrEnd(trial_string, i, ',');
@@ -78,6 +85,10 @@
                        << "' (found in trial: \"" << trial_string << "\")";
     }
   }
+
+  for (FieldTrialParameterInterface* field : fields) {
+    field->ParseDone();
+  }
 }
 
 template <>
diff --git a/rtc_base/experiments/field_trial_parser.h b/rtc_base/experiments/field_trial_parser.h
index e6ae4c5..a63cb20 100644
--- a/rtc_base/experiments/field_trial_parser.h
+++ b/rtc_base/experiments/field_trial_parser.h
@@ -11,10 +11,13 @@
 #define RTC_BASE_EXPERIMENTS_FIELD_TRIAL_PARSER_H_
 
 #include <stdint.h>
+
 #include <initializer_list>
 #include <map>
 #include <set>
 #include <string>
+#include <vector>
+
 #include "absl/types/optional.h"
 
 // Field trial parser functionality. Provides funcitonality to parse field trial
@@ -24,7 +27,7 @@
 // ignored. Parameters are declared with a given type for which an
 // implementation of ParseTypedParameter should be provided. The
 // ParseTypedParameter implementation is given whatever is between the : and the
-// ,. FieldTrialOptional will use nullopt if the key is provided without :.
+// ,. If the key is provided without : a FieldTrialOptional will use nullopt.
 
 // Example string: "my_optional,my_int:3,my_string:hello"
 
@@ -47,10 +50,14 @@
       std::string raw_string);
   void MarkAsUsed() { used_ = true; }
   virtual bool Parse(absl::optional<std::string> str_value) = 0;
-  std::string Key() const;
+
+  virtual void ParseDone() {}
+
+  std::vector<FieldTrialParameterInterface*> sub_parameters_;
+
+  std::string key_;
 
  private:
-  std::string key_;
   bool used_ = false;
 };
 
diff --git a/rtc_base/experiments/field_trial_parser_unittest.cc b/rtc_base/experiments/field_trial_parser_unittest.cc
index 806b895..cf483d7 100644
--- a/rtc_base/experiments/field_trial_parser_unittest.cc
+++ b/rtc_base/experiments/field_trial_parser_unittest.cc
@@ -8,9 +8,12 @@
  *  be found in the AUTHORS file in the root of the source tree.
  */
 #include "rtc_base/experiments/field_trial_parser.h"
+
+#include "rtc_base/experiments/field_trial_list.h"
 #include "rtc_base/gunit.h"
 #include "system_wrappers/include/field_trial.h"
 #include "test/field_trial.h"
+#include "test/gmock.h"
 
 namespace webrtc {
 namespace {
@@ -38,6 +41,7 @@
   kRed = 1,
   kBlue = 2,
 };
+
 }  // namespace
 
 TEST(FieldTrialParserTest, ParsesValidParameters) {
@@ -152,4 +156,5 @@
   ParseFieldTrial({&my_enum}, "e:5");
   EXPECT_EQ(my_enum.Get(), CustomEnum::kBlue);
 }
+
 }  // namespace webrtc