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 20 namespace { 21 22 struct SourceInfo { 23 int weight; 24 const char* source_name; 25 int number_of_suggestions; 26 }; 27 28 struct 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. 135 class 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 209 class 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 240 TEST_F(SuggestionsCombinerTest, NoSource) { 241 combiner_->FetchItems(NULL); 242 EXPECT_EQ(0UL, combiner_->GetPageValues()->GetSize()); 243 } 244 245 TEST_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 252 TEST_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