Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include <algorithm> | 5 #include <algorithm> |
| 6 #include <vector> | 6 #include <vector> |
| 7 | 7 |
| 8 #include "base/basictypes.h" | 8 #include "base/basictypes.h" |
| 9 #include "base/command_line.h" | 9 #include "base/command_line.h" |
| 10 #include "base/file_path.h" | 10 #include "base/file_path.h" |
| 11 #include "base/file_util.h" | 11 #include "base/file_util.h" |
| 12 #include "base/path_service.h" | 12 #include "base/path_service.h" |
| 13 #include "base/ref_counted_memory.h" | 13 #include "base/ref_counted_memory.h" |
| 14 #include "base/scoped_temp_dir.h" | 14 #include "base/scoped_temp_dir.h" |
| 15 #include "chrome/browser/history/thumbnail_database.h" | 15 #include "chrome/browser/history/thumbnail_database.h" |
| 16 #include "chrome/common/chrome_paths.h" | 16 #include "chrome/common/chrome_paths.h" |
| 17 #include "chrome/browser/history/top_sites.h" | 17 #include "chrome/browser/history/top_sites.h" |
|
sky
2011/01/13 16:55:52
Can this be removed?
satorux1
2011/01/14 07:36:46
Done.
| |
| 18 #include "chrome/common/thumbnail_score.h" | 18 #include "chrome/common/thumbnail_score.h" |
| 19 #include "chrome/tools/profiles/thumbnail-inl.h" | 19 #include "chrome/tools/profiles/thumbnail-inl.h" |
| 20 #include "gfx/codec/jpeg_codec.h" | 20 #include "gfx/codec/jpeg_codec.h" |
| 21 #include "googleurl/src/gurl.h" | 21 #include "googleurl/src/gurl.h" |
| 22 #include "testing/gtest/include/gtest/gtest.h" | 22 #include "testing/gtest/include/gtest/gtest.h" |
| 23 #include "third_party/skia/include/core/SkBitmap.h" | 23 #include "third_party/skia/include/core/SkBitmap.h" |
| 24 | 24 |
| 25 using base::Time; | 25 using base::Time; |
| 26 using base::TimeDelta; | 26 using base::TimeDelta; |
| 27 | 27 |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 64 gfx::JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail))); | 64 gfx::JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail))); |
| 65 } | 65 } |
| 66 | 66 |
| 67 scoped_ptr<SkBitmap> google_bitmap_; | 67 scoped_ptr<SkBitmap> google_bitmap_; |
| 68 | 68 |
| 69 ScopedTempDir temp_dir_; | 69 ScopedTempDir temp_dir_; |
| 70 FilePath file_name_; | 70 FilePath file_name_; |
| 71 FilePath new_file_name_; | 71 FilePath new_file_name_; |
| 72 }; | 72 }; |
| 73 | 73 |
| 74 TEST_F(ThumbnailDatabaseTest, AddDelete) { | |
| 75 if (history::TopSites::IsEnabled()) | |
| 76 return; // TopSitesTest replaces this. | |
| 77 | |
| 78 ThumbnailDatabase db; | |
| 79 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | |
| 80 | |
| 81 // Add one page & verify it got added. | |
| 82 ThumbnailScore boring(kBoringness, true, true); | |
| 83 Time time; | |
| 84 GURL gurl; | |
| 85 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, boring, time); | |
| 86 ThumbnailScore score_output; | |
| 87 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_output)); | |
| 88 ASSERT_TRUE(boring.Equals(score_output)); | |
| 89 | |
| 90 // Verify a random page is not found. | |
| 91 int64 page2 = 5678; | |
| 92 std::vector<unsigned char> jpeg_data; | |
| 93 EXPECT_FALSE(db.GetPageThumbnail(page2, &jpeg_data)); | |
| 94 EXPECT_FALSE(db.ThumbnailScoreForId(page2, &score_output)); | |
| 95 | |
| 96 // Add another page with a better boringness & verify it got added. | |
| 97 ThumbnailScore better_boringness(kBetterBoringness, true, true); | |
| 98 | |
| 99 db.SetPageThumbnail(gurl, page2, *google_bitmap_, better_boringness, time); | |
| 100 ASSERT_TRUE(db.ThumbnailScoreForId(page2, &score_output)); | |
| 101 ASSERT_TRUE(better_boringness.Equals(score_output)); | |
| 102 | |
| 103 // Delete the thumbnail for the second page. | |
| 104 ThumbnailScore worse_boringness(kWorseBoringness, true, true); | |
| 105 db.SetPageThumbnail(gurl, page2, SkBitmap(), worse_boringness, time); | |
| 106 ASSERT_FALSE(db.GetPageThumbnail(page2, &jpeg_data)); | |
| 107 ASSERT_FALSE(db.ThumbnailScoreForId(page2, &score_output)); | |
| 108 | |
| 109 // Delete the first thumbnail using the explicit delete API. | |
| 110 ASSERT_TRUE(db.DeleteThumbnail(kPage1)); | |
| 111 | |
| 112 // Make sure it is gone | |
| 113 ASSERT_FALSE(db.ThumbnailScoreForId(kPage1, &score_output)); | |
| 114 ASSERT_FALSE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 115 ASSERT_FALSE(db.ThumbnailScoreForId(page2, &score_output)); | |
| 116 ASSERT_FALSE(db.GetPageThumbnail(page2, &jpeg_data)); | |
| 117 } | |
| 118 | |
| 119 TEST_F(ThumbnailDatabaseTest, UseLessBoringThumbnails) { | |
| 120 if (history::TopSites::IsEnabled()) | |
| 121 return; // TopSitesTest replaces this. | |
| 122 | |
| 123 ThumbnailDatabase db; | |
| 124 Time now = Time::Now(); | |
| 125 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | |
| 126 | |
| 127 // Add one page & verify it got added. | |
| 128 ThumbnailScore boring(kBoringness, true, true); | |
| 129 | |
| 130 Time time; | |
| 131 GURL gurl; | |
| 132 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, boring, time); | |
| 133 std::vector<unsigned char> jpeg_data; | |
| 134 ThumbnailScore score_out; | |
| 135 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 136 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 137 ASSERT_TRUE(boring.Equals(score_out)); | |
| 138 | |
| 139 // Attempt to update the first page entry with a thumbnail that | |
| 140 // is more boring and verify that it doesn't change. | |
| 141 ThumbnailScore more_boring(kWorseBoringness, true, true); | |
| 142 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, more_boring, time); | |
| 143 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 144 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 145 ASSERT_TRUE(boring.Equals(score_out)); | |
| 146 | |
| 147 // Attempt to update the first page entry with a thumbnail that | |
| 148 // is less boring and verify that we update it. | |
| 149 ThumbnailScore less_boring(kBetterBoringness, true, true); | |
| 150 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, less_boring, time); | |
| 151 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 152 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 153 ASSERT_TRUE(less_boring.Equals(score_out)); | |
| 154 } | |
| 155 | |
| 156 TEST_F(ThumbnailDatabaseTest, UseAtTopThumbnails) { | |
| 157 if (history::TopSites::IsEnabled()) | |
| 158 return; // TopSitesTest replaces this. | |
| 159 | |
| 160 ThumbnailDatabase db; | |
| 161 Time now = Time::Now(); | |
| 162 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | |
| 163 | |
| 164 // Add one page & verify it got added. Note that it doesn't have | |
| 165 // |good_clipping| and isn't |at_top|. | |
| 166 ThumbnailScore boring_and_bad(kBoringness, false, false); | |
| 167 | |
| 168 Time time; | |
| 169 GURL gurl; | |
| 170 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, boring_and_bad, time); | |
| 171 std::vector<unsigned char> jpeg_data; | |
| 172 ThumbnailScore score_out; | |
| 173 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 174 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 175 ASSERT_TRUE(boring_and_bad.Equals(score_out)); | |
| 176 | |
| 177 // A thumbnail that's at the top of the page should replace | |
| 178 // thumbnails that are in the middle, for the same boringness. | |
| 179 ThumbnailScore boring_but_better(kBoringness, false, true); | |
| 180 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, boring_but_better, time); | |
| 181 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 182 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 183 ASSERT_TRUE(boring_but_better.Equals(score_out)); | |
| 184 | |
| 185 // The only case where we should replace a thumbnail at the top with | |
| 186 // a thumbnail in the middle/bottom is when the current thumbnail is | |
| 187 // weirdly stretched and the incoming thumbnail isn't. | |
| 188 ThumbnailScore better_boring_bad_framing(kBetterBoringness, false, false); | |
| 189 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, better_boring_bad_framing, | |
| 190 time); | |
| 191 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 192 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 193 ASSERT_TRUE(boring_but_better.Equals(score_out)); | |
| 194 | |
| 195 ThumbnailScore boring_good_clipping(kBoringness, true, false); | |
| 196 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, boring_good_clipping, | |
| 197 time); | |
| 198 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 199 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 200 ASSERT_TRUE(boring_good_clipping.Equals(score_out)); | |
| 201 | |
| 202 // Now that we have a non-stretched, middle of the page thumbnail, | |
| 203 // we shouldn't be able to replace it with: | |
| 204 | |
| 205 // 1) A stretched thumbnail in the middle of the page | |
| 206 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, | |
| 207 ThumbnailScore(kBetterBoringness, false, false, now), | |
| 208 time); | |
| 209 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 210 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 211 ASSERT_TRUE(boring_good_clipping.Equals(score_out)); | |
| 212 | |
| 213 // 2) A stretched thumbnail at the top of the page | |
| 214 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, | |
| 215 ThumbnailScore(kBetterBoringness, false, true, now), | |
| 216 time); | |
| 217 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 218 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 219 ASSERT_TRUE(boring_good_clipping.Equals(score_out)); | |
| 220 | |
| 221 // But it should be replaced by a thumbnail that's clipped properly | |
| 222 // and is at the top | |
| 223 ThumbnailScore best_score(kBetterBoringness, true, true); | |
| 224 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, best_score, time); | |
| 225 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 226 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 227 ASSERT_TRUE(best_score.Equals(score_out)); | |
| 228 } | |
| 229 | |
| 230 TEST_F(ThumbnailDatabaseTest, ThumbnailTimeDegradation) { | |
| 231 if (history::TopSites::IsEnabled()) | |
| 232 return; // TopSitesTest replaces this. | |
| 233 | |
| 234 ThumbnailDatabase db; | |
| 235 const Time kNow = Time::Now(); | |
| 236 const Time kThreeHoursAgo = kNow - TimeDelta::FromHours(4); | |
| 237 const Time kFiveHoursAgo = kNow - TimeDelta::FromHours(6); | |
| 238 const double kBaseBoringness = 0.305; | |
| 239 const double kWorseBoringness = 0.345; | |
| 240 | |
| 241 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | |
| 242 | |
| 243 // add one page & verify it got added. | |
| 244 ThumbnailScore base_boringness(kBaseBoringness, true, true, kFiveHoursAgo); | |
| 245 | |
| 246 Time time; | |
| 247 GURL gurl; | |
| 248 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, base_boringness, time); | |
| 249 std::vector<unsigned char> jpeg_data; | |
| 250 ThumbnailScore score_out; | |
| 251 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 252 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 253 ASSERT_TRUE(base_boringness.Equals(score_out)); | |
| 254 | |
| 255 // Try to add a different thumbnail with a worse score an hour later | |
| 256 // (but not enough to trip the boringness degradation threshold). | |
| 257 ThumbnailScore hour_later(kWorseBoringness, true, true, kThreeHoursAgo); | |
| 258 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, hour_later, time); | |
| 259 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 260 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 261 ASSERT_TRUE(base_boringness.Equals(score_out)); | |
| 262 | |
| 263 // After a full five hours, things should have degraded enough | |
| 264 // that we'll allow the same thumbnail with the same (worse) | |
| 265 // boringness that we previous rejected. | |
| 266 ThumbnailScore five_hours_later(kWorseBoringness, true, true, kNow); | |
| 267 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, five_hours_later, time); | |
| 268 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 269 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 270 ASSERT_TRUE(five_hours_later.Equals(score_out)); | |
| 271 } | |
| 272 | |
| 273 TEST_F(ThumbnailDatabaseTest, NeverAcceptTotallyBoringThumbnail) { | |
| 274 // We enforce a maximum boringness score: even in cases where we | |
| 275 // should replace a thumbnail with another because of reasons other | |
| 276 // than straight up boringness score, still reject because the | |
| 277 // thumbnail is totally boring. | |
| 278 if (history::TopSites::IsEnabled()) | |
| 279 return; // TopSitesTest replaces this. | |
| 280 | |
| 281 ThumbnailDatabase db; | |
| 282 Time now = Time::Now(); | |
| 283 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | |
| 284 | |
| 285 std::vector<unsigned char> jpeg_data; | |
| 286 ThumbnailScore score_out; | |
| 287 const double kBaseBoringness = 0.50; | |
| 288 const Time kNow = Time::Now(); | |
| 289 const int kSizeOfTable = 4; | |
| 290 struct { | |
| 291 bool good_scaling; | |
| 292 bool at_top; | |
| 293 } const heiarchy_table[] = { | |
| 294 {false, false}, | |
| 295 {false, true}, | |
| 296 {true, false}, | |
| 297 {true, true} | |
| 298 }; | |
| 299 | |
| 300 Time time; | |
| 301 GURL gurl; | |
| 302 | |
| 303 // Test that for each entry type, all entry types that are better | |
| 304 // than it still will reject thumbnails which are totally boring. | |
| 305 for (int i = 0; i < kSizeOfTable; ++i) { | |
| 306 ThumbnailScore base(kBaseBoringness, | |
| 307 heiarchy_table[i].good_scaling, | |
| 308 heiarchy_table[i].at_top, | |
| 309 kNow); | |
| 310 | |
| 311 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, base, time); | |
| 312 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 313 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 314 ASSERT_TRUE(base.Equals(score_out)); | |
| 315 | |
| 316 for (int j = i; j < kSizeOfTable; ++j) { | |
| 317 ThumbnailScore shouldnt_replace( | |
| 318 kTotallyBoring, heiarchy_table[j].good_scaling, | |
| 319 heiarchy_table[j].at_top, kNow); | |
| 320 | |
| 321 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, shouldnt_replace, | |
| 322 time); | |
| 323 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 324 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 325 ASSERT_TRUE(base.Equals(score_out)); | |
| 326 } | |
| 327 | |
| 328 // Clean up for the next iteration | |
| 329 ASSERT_TRUE(db.DeleteThumbnail(kPage1)); | |
| 330 ASSERT_FALSE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 331 ASSERT_FALSE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 332 } | |
| 333 | |
| 334 // We should never accept a totally boring thumbnail no matter how | |
| 335 // much old the current thumbnail is. | |
| 336 ThumbnailScore base_boring(kBaseBoringness, true, true, kNow); | |
| 337 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, base_boring, time); | |
| 338 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 339 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 340 ASSERT_TRUE(base_boring.Equals(score_out)); | |
| 341 | |
| 342 ThumbnailScore totally_boring_in_the_future( | |
| 343 kTotallyBoring, true, true, kNow + TimeDelta::FromDays(365)); | |
| 344 db.SetPageThumbnail(gurl, kPage1, *google_bitmap_, | |
| 345 totally_boring_in_the_future, time); | |
| 346 ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data)); | |
| 347 ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out)); | |
| 348 ASSERT_TRUE(base_boring.Equals(score_out)); | |
| 349 } | |
| 350 | |
| 351 TEST_F(ThumbnailDatabaseTest, NeedsMigrationToTopSites) { | |
| 352 if (history::TopSites::IsEnabled()) | |
| 353 return; // TopSitesTest replaces this. | |
| 354 | |
| 355 ThumbnailDatabase db; | |
| 356 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | |
| 357 db.BeginTransaction(); | |
| 358 EXPECT_TRUE(db.NeedsMigrationToTopSites()); | |
| 359 EXPECT_TRUE(db.RenameAndDropThumbnails(file_name_, new_file_name_)); | |
| 360 EXPECT_FALSE(db.NeedsMigrationToTopSites()); | |
| 361 EXPECT_FALSE(file_util::PathExists(file_name_)); | |
| 362 EXPECT_TRUE(file_util::PathExists(new_file_name_)); | |
| 363 } | |
| 364 | |
| 365 TEST_F(ThumbnailDatabaseTest, GetFaviconAfterMigrationToTopSites) { | 74 TEST_F(ThumbnailDatabaseTest, GetFaviconAfterMigrationToTopSites) { |
| 366 ThumbnailDatabase db; | 75 ThumbnailDatabase db; |
| 367 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); | 76 ASSERT_EQ(sql::INIT_OK, db.Init(file_name_, NULL)); |
| 368 db.BeginTransaction(); | 77 db.BeginTransaction(); |
| 369 | 78 |
| 370 std::vector<unsigned char> data(blob1, blob1 + sizeof(blob1)); | 79 std::vector<unsigned char> data(blob1, blob1 + sizeof(blob1)); |
| 371 scoped_refptr<RefCountedBytes> favicon(new RefCountedBytes(data)); | 80 scoped_refptr<RefCountedBytes> favicon(new RefCountedBytes(data)); |
| 372 | 81 |
| 373 GURL url("http://google.com"); | 82 GURL url("http://google.com"); |
| 374 FavIconID id = db.AddFavIcon(url); | 83 FavIconID id = db.AddFavIcon(url); |
| 375 base::Time time = base::Time::Now(); | 84 base::Time time = base::Time::Now(); |
| 376 db.SetFavIcon(id, favicon, time); | 85 db.SetFavIcon(id, favicon, time); |
| 377 EXPECT_TRUE(db.RenameAndDropThumbnails(file_name_, new_file_name_)); | 86 EXPECT_TRUE(db.RenameAndDropThumbnails(file_name_, new_file_name_)); |
| 378 | 87 |
| 379 base::Time time_out; | 88 base::Time time_out; |
| 380 std::vector<unsigned char> favicon_out; | 89 std::vector<unsigned char> favicon_out; |
| 381 GURL url_out; | 90 GURL url_out; |
| 382 EXPECT_TRUE(db.GetFavIcon(id, &time_out, &favicon_out, &url_out)); | 91 EXPECT_TRUE(db.GetFavIcon(id, &time_out, &favicon_out, &url_out)); |
| 383 EXPECT_EQ(url, url_out); | 92 EXPECT_EQ(url, url_out); |
| 384 EXPECT_EQ(time.ToTimeT(), time_out.ToTimeT()); | 93 EXPECT_EQ(time.ToTimeT(), time_out.ToTimeT()); |
| 385 ASSERT_EQ(data.size(), favicon_out.size()); | 94 ASSERT_EQ(data.size(), favicon_out.size()); |
| 386 EXPECT_TRUE(std::equal(data.begin(), | 95 EXPECT_TRUE(std::equal(data.begin(), |
| 387 data.end(), | 96 data.end(), |
| 388 favicon_out.begin())); | 97 favicon_out.begin())); |
| 389 } | 98 } |
| 390 | 99 |
| 391 } // namespace history | 100 } // namespace history |
| OLD | NEW |