1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// TODO(beaudoin): What is really needed here?
6
7#include <deque>
8#include <string>
9
10#include "base/memory/scoped_ptr.h"
11#include "base/stl_util.h"
12#include "base/strings/string_util.h"
13#include "base/values.h"
14#include "chrome/browser/ui/webui/ntp/suggestions_combiner.h"
15#include "chrome/browser/ui/webui/ntp/suggestions_page_handler.h"
16#include "chrome/browser/ui/webui/ntp/suggestions_source.h"
17#include "chrome/test/base/testing_profile.h"
18#include "testing/gtest/include/gtest/gtest.h"
19
20namespace {
21
22struct SourceInfo {
23  int weight;
24  const char* source_name;
25  int number_of_suggestions;
26};
27
28struct TestDescription {
29  SourceInfo sources[3];
30  const char* results[8];
31} test_suite[] = {
32  // One source, more than 8 items.
33  {
34    {{1, "A", 10}},
35    {"A 0", "A 1", "A 2", "A 3", "A 4", "A 5", "A 6", "A 7"}
36  },
37  // One source, exactly 8 items.
38  {
39    {{1, "A", 8}},
40    {"A 0", "A 1", "A 2", "A 3", "A 4", "A 5", "A 6", "A 7"}
41  },
42  // One source, not enough items.
43  {
44    {{1, "A", 3}},
45    {"A 0", "A 1", "A 2"}
46  },
47  // One source, no items.
48  {
49    {{1, "A", 0}},
50    {}
51  },
52  // Two sources, equal weight, more than 8 items.
53  {
54    {{1, "A", 10}, {1, "B", 10}},
55    {"A 0", "A 1", "A 2", "A 3", "B 0", "B 1", "B 2", "B 3"}
56  },
57  // Two sources, equal weight, exactly 8 items.
58  {
59    {{1, "A", 4}, {1, "B", 4}},
60    {"A 0", "A 1", "A 2", "A 3", "B 0", "B 1", "B 2", "B 3"}
61  },
62  // Two sources, equal weight, exactly 8 items but source A has more.
63  {
64    {{1, "A", 5}, {1, "B", 3}},
65    {"A 0", "A 1", "A 2", "A 3", "A 4", "B 0", "B 1", "B 2"}
66  },
67  // Two sources, equal weight, exactly 8 items but source B has more.
68  {
69    {{1, "A", 2}, {1, "B", 6}},
70    {"A 0", "A 1", "B 0", "B 1", "B 2", "B 3", "B 4", "B 5"}
71  },
72  // Two sources, equal weight, exactly 8 items but source A has none.
73  {
74    {{1, "A", 0}, {1, "B", 8}},
75    {"B 0", "B 1", "B 2", "B 3", "B 4", "B 5", "B 6", "B 7"}
76  },
77  // Two sources, equal weight, exactly 8 items but source B has none.
78  {
79    {{1, "A", 8}, {1, "B", 0}},
80    {"A 0", "A 1", "A 2", "A 3", "A 4", "A 5", "A 6", "A 7"}
81  },
82  // Two sources, equal weight, less than 8 items.
83  {
84    {{1, "A", 3}, {1, "B", 3}},
85    {"A 0", "A 1", "A 2", "B 0", "B 1", "B 2"}
86  },
87  // Two sources, equal weight, less than 8 items but source A has more.
88  {
89    {{1, "A", 4}, {1, "B", 3}},
90    {"A 0", "A 1", "A 2", "A 3", "B 0", "B 1", "B 2"}
91  },
92  // Two sources, equal weight, less than 8 items but source B has more.
93  {
94    {{1, "A", 1}, {1, "B", 3}},
95    {"A 0", "B 0", "B 1", "B 2"}
96  },
97  // Two sources, weights 3/4 A  1/4 B, more than 8 items.
98  {
99    {{3, "A", 10}, {1, "B", 10}},
100    {"A 0", "A 1", "A 2", "A 3", "A 4", "A 5", "B 0", "B 1"}
101  },
102  // Two sources, weights 1/8 A  7/8 B, more than 8 items.
103  {
104    {{1, "A", 10}, {7, "B", 10}},
105    {"A 0", "B 0", "B 1", "B 2", "B 3", "B 4", "B 5", "B 6"}
106  },
107  // Two sources, weights 1/3 A  2/3 B, more than 8 items.
108  {
109    {{1, "A", 10}, {2, "B", 10}},
110    {"A 0", "A 1", "B 0", "B 1", "B 2", "B 3", "B 4", "B 5"}
111  },
112  // Three sources, weights 1/2 A  1/4 B  1/4 C, more than 8 items.
113  {
114    {{2, "A", 10}, {1, "B", 10}, {1, "C", 10}},
115    {"A 0", "A 1", "A 2", "A 3", "B 0", "B 1", "C 0", "C 1"}
116  },
117  // Three sources, weights 1/3 A  1/3 B  1/3 C, more than 8 items.
118  {
119    {{1, "A", 10}, {1, "B", 10}, {1, "C", 10}},
120    {"A 0", "A 1", "B 0", "B 1", "B 2", "C 0", "C 1", "C 2"}
121  },
122  // Extra items should be grouped together.
123  {
124    {{1, "A", 3}, {1, "B", 4}, {10, "C", 1}},
125    {"A 0", "A 1", "A 2", "B 0", "B 1", "B 2", "B 3", "C 0"}
126  }
127};
128
129}  // namespace
130
131// Stub for a SuggestionsSource that can provide a number of fake suggestions.
132// Fake suggestions are DictionaryValue with a single "title" string field
133// containing the |source_name| followed by the index of the suggestion.
134// Not in the empty namespace since it's a friend of SuggestionsCombiner.
135class SuggestionsSourceStub : public SuggestionsSource {
136 public:
137  explicit SuggestionsSourceStub(int weight,
138      const std::string& source_name, int number_of_suggestions)
139      : combiner_(NULL),
140        weight_(weight),
141        source_name_(source_name),
142        number_of_suggestions_(number_of_suggestions),
143        debug_(false) {
144  }
145  virtual ~SuggestionsSourceStub() {
146    STLDeleteElements(&items_);
147  }
148
149  // Call this method to simulate that the SuggestionsSource has received all
150  // its suggestions.
151  void Done() {
152    combiner_->OnItemsReady();
153  }
154
155 private:
156  // SuggestionsSource Override and implementation.
157  virtual void SetDebug(bool enable) OVERRIDE {
158    debug_ = enable;
159  }
160  virtual int GetWeight() OVERRIDE {
161    return weight_;
162  }
163  virtual int GetItemCount() OVERRIDE {
164    return items_.size();
165  }
166  virtual base::DictionaryValue* PopItem() OVERRIDE {
167    if (items_.empty())
168      return NULL;
169    base::DictionaryValue* item = items_.front();
170    items_.pop_front();
171    return item;
172  }
173
174  virtual void FetchItems(Profile* profile) OVERRIDE {
175    char num_str[21];  // Enough to hold all numbers up to 64-bits.
176    for (int i = 0; i < number_of_suggestions_; ++i) {
177      base::snprintf(num_str, sizeof(num_str), "%d", i);
178      AddSuggestion(source_name_ + ' ' + num_str);
179    }
180  }
181
182  // Adds a fake suggestion. This suggestion is a DictionaryValue with a single
183  // "title" field containing |title|.
184  void AddSuggestion(const std::string& title) {
185    base::DictionaryValue* item = new base::DictionaryValue();
186    item->SetString("title", title);
187    items_.push_back(item);
188  }
189
190  virtual void SetCombiner(SuggestionsCombiner* combiner) OVERRIDE {
191    DCHECK(!combiner_);
192    combiner_ = combiner;
193  }
194
195  // Our combiner.
196  SuggestionsCombiner* combiner_;
197
198  int weight_;
199  std::string source_name_;
200  int number_of_suggestions_;
201  bool debug_;
202
203  // Keep the results of the db query here.
204  std::deque<base::DictionaryValue*> items_;
205
206  DISALLOW_COPY_AND_ASSIGN(SuggestionsSourceStub);
207};
208
209class SuggestionsCombinerTest : public testing::Test {
210 public:
211  SuggestionsCombinerTest() {
212  }
213
214 protected:
215  Profile* profile_;
216  SuggestionsHandler* suggestions_handler_;
217  SuggestionsCombiner* combiner_;
218
219  void Reset() {
220    delete combiner_;
221    combiner_ = new SuggestionsCombiner(suggestions_handler_, profile_);
222  }
223
224 private:
225  virtual void SetUp() {
226    profile_ = new TestingProfile();
227    suggestions_handler_ = new SuggestionsHandler();
228    combiner_ = new SuggestionsCombiner(suggestions_handler_, profile_);
229  }
230
231  virtual void TearDown() {
232    delete combiner_;
233    delete suggestions_handler_;
234    delete profile_;
235  }
236
237  DISALLOW_COPY_AND_ASSIGN(SuggestionsCombinerTest);
238};
239
240TEST_F(SuggestionsCombinerTest, NoSource) {
241  combiner_->FetchItems(NULL);
242  EXPECT_EQ(0UL, combiner_->GetPageValues()->GetSize());
243}
244
245TEST_F(SuggestionsCombinerTest, SourcesAreNotDoneFetching) {
246  combiner_->AddSource(new SuggestionsSourceStub(1, "sourceA", 10));
247  combiner_->AddSource(new SuggestionsSourceStub(1, "sourceB", 10));
248  combiner_->FetchItems(NULL);
249  EXPECT_EQ(0UL, combiner_->GetPageValues()->GetSize());
250}
251
252TEST_F(SuggestionsCombinerTest, TestSuite) {
253  size_t test_count = arraysize(test_suite);
254  for (size_t i = 0; i < test_count; ++i) {
255    const TestDescription& description = test_suite[i];
256    size_t source_count = arraysize(description.sources);
257
258    scoped_ptr<SuggestionsSourceStub*[]> sources(
259        new SuggestionsSourceStub*[source_count]);
260
261    // Setup sources.
262    for (size_t j = 0; j < source_count; ++j) {
263      const SourceInfo& source_info = description.sources[j];
264      // A NULL |source_name| means we shouldn't add this source.
265      if (source_info.source_name) {
266        sources[j] = new SuggestionsSourceStub(source_info.weight,
267            source_info.source_name, source_info.number_of_suggestions);
268        combiner_->AddSource(sources[j]);
269      } else {
270        sources[j] = NULL;
271      }
272    }
273
274    // Start fetching.
275    combiner_->FetchItems(NULL);
276
277    // Sources complete.
278    for (size_t j = 0; j < source_count; ++j) {
279      if (sources[j])
280        sources[j]->Done();
281    }
282
283    // Verify expectations.
284    base::ListValue* results = combiner_->GetPageValues();
285    size_t result_count = results->GetSize();
286    EXPECT_LE(result_count, 8UL);
287    for (size_t j = 0; j < 8; ++j) {
288      if (j < result_count) {
289        std::string value;
290        base::DictionaryValue* dictionary;
291        results->GetDictionary(j, &dictionary);
292        dictionary->GetString("title", &value);
293        EXPECT_STREQ(description.results[j], value.c_str()) <<
294            " test index:" << i;
295      } else {
296        EXPECT_EQ(description.results[j], static_cast<const char*>(NULL)) <<
297            " test index:" << i;
298      }
299    }
300
301    Reset();
302  }
303}
304
305