1 // Copyright (c) 2013 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.
5 #include "chrome/browser/history/typed_url_syncable_service.h"
7 #include "base/logging.h"
8 #include "base/memory/ref_counted.h"
9 #include "base/memory/scoped_ptr.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "chrome/browser/history/history_backend.h"
12 #include "components/history/core/browser/history_types.h"
13 #include "content/public/browser/notification_types.h"
14 #include "sync/api/fake_sync_change_processor.h"
15 #include "sync/api/sync_change_processor_wrapper_for_test.h"
16 #include "sync/api/sync_error.h"
17 #include "sync/api/sync_error_factory_mock.h"
18 #include "sync/protocol/sync.pb.h"
19 #include "sync/protocol/typed_url_specifics.pb.h"
20 #include "testing/gtest/include/gtest/gtest.h"
22 using history::HistoryBackend;
24 using history::URLRow;
25 using history::URLRows;
26 using history::VisitRow;
27 using history::VisitVector;
31 // Constants used to limit size of visits processed.
32 const int kMaxTypedUrlVisits = 100;
34 // Visits with this timestamp are treated as expired.
35 const int EXPIRED_VISIT = -1;
37 // TestHistoryBackend ----------------------------------------------------------
39 class TestHistoryBackend : public HistoryBackend {
41 TestHistoryBackend() : HistoryBackend(base::FilePath(), NULL, NULL) {}
43 // HistoryBackend test implementation.
44 bool IsExpiredVisitTime(const base::Time& time) override {
45 return time.ToInternalValue() == EXPIRED_VISIT;
48 bool GetMostRecentVisitsForURL(URLID id,
50 VisitVector* visits) override {
51 if (local_db_visits_[id].empty())
54 visits->insert(visits->end(),
55 local_db_visits_[id].begin(),
56 local_db_visits_[id].end());
61 void SetVisitsForUrl(URLID id, VisitVector* visits) {
62 if (!local_db_visits_[id].empty()) {
63 local_db_visits_[id].clear();
66 local_db_visits_[id].insert(local_db_visits_[id].end(),
71 void DeleteVisitsForUrl(const URLID& id) {
72 local_db_visits_.erase(id);
76 ~TestHistoryBackend() override {}
78 // Mock of visit table in local db.
79 std::map<URLID, VisitVector> local_db_visits_;
86 // TypedUrlSyncableServiceTest -------------------------------------------------
88 class TypedUrlSyncableServiceTest : public testing::Test {
90 // Create a new row object and add a typed visit to the |visits| vector.
91 // Note that the real history db returns visits in reverse chronological
92 // order, so |visits| is treated this way. If the newest (first) visit
93 // in visits does not match |last_visit|, then a typed visit for this
94 // time is prepended to the front (or if |last_visit| is too old, it is
95 // set equal to the time of the newest visit).
96 static URLRow MakeTypedUrlRow(const char* url,
101 VisitVector* visits);
103 static void AddNewestVisit(URLRow* url,
105 ui::PageTransition transition,
108 static void AddOldestVisit(URLRow* url,
110 ui::PageTransition transition,
113 static bool URLsEqual(URLRow& row,
114 sync_pb::TypedUrlSpecifics& specifics) {
115 return ((row.url().spec().compare(specifics.url()) == 0) &&
116 (base::UTF16ToUTF8(row.title()).compare(specifics.title()) == 0) &&
117 (row.hidden() == specifics.hidden()));
120 bool InitiateServerState(unsigned int num_typed_urls,
121 unsigned int num_reload_urls,
123 std::vector<VisitVector>* visit_vectors,
124 const std::vector<const char*>& urls);
127 TypedUrlSyncableServiceTest() {}
129 ~TypedUrlSyncableServiceTest() override {}
131 void SetUp() override {
132 fake_history_backend_ = new TestHistoryBackend();
133 typed_url_sync_service_.reset(
134 new TypedUrlSyncableService(fake_history_backend_.get()));
135 fake_change_processor_.reset(new syncer::FakeSyncChangeProcessor);
138 scoped_refptr<HistoryBackend> fake_history_backend_;
139 scoped_ptr<TypedUrlSyncableService> typed_url_sync_service_;
140 scoped_ptr<syncer::FakeSyncChangeProcessor> fake_change_processor_;
143 URLRow TypedUrlSyncableServiceTest::MakeTypedUrlRow(
149 VisitVector* visits) {
150 DCHECK(visits->empty());
152 // Give each URL a unique ID, to mimic the behavior of the real database.
153 static int unique_url_id = 0;
155 URLRow history_url(gurl, ++unique_url_id);
156 history_url.set_title(base::UTF8ToUTF16(title));
157 history_url.set_typed_count(typed_count);
158 history_url.set_hidden(hidden);
160 base::Time last_visit_time = base::Time::FromInternalValue(last_visit);
161 history_url.set_last_visit(last_visit_time);
163 VisitVector::iterator first = visits->begin();
164 if (typed_count > 0) {
165 // Add a typed visit for time |last_visit|.
166 visits->insert(first,
167 VisitRow(history_url.id(), last_visit_time, 0,
168 ui::PAGE_TRANSITION_TYPED, 0));
170 // Add a non-typed visit for time |last_visit|.
171 visits->insert(first,
172 VisitRow(history_url.id(), last_visit_time, 0,
173 ui::PAGE_TRANSITION_RELOAD, 0));
176 history_url.set_visit_count(visits->size());
180 void TypedUrlSyncableServiceTest::AddNewestVisit(
183 ui::PageTransition transition,
185 base::Time time = base::Time::FromInternalValue(visit_time);
186 visits->insert(visits->begin(),
187 VisitRow(url->id(), time, 0, transition, 0));
189 if (transition == ui::PAGE_TRANSITION_TYPED) {
190 url->set_typed_count(url->typed_count() + 1);
193 url->set_last_visit(time);
194 url->set_visit_count(visits->size());
197 void TypedUrlSyncableServiceTest::AddOldestVisit(
200 ui::PageTransition transition,
202 base::Time time = base::Time::FromInternalValue(visit_time);
203 visits->push_back(VisitRow(url->id(), time, 0, transition, 0));
205 if (transition == ui::PAGE_TRANSITION_TYPED) {
206 url->set_typed_count(url->typed_count() + 1);
209 url->set_visit_count(visits->size());
212 bool TypedUrlSyncableServiceTest::InitiateServerState(
213 unsigned int num_typed_urls,
214 unsigned int num_reload_urls,
216 std::vector<VisitVector>* visit_vectors,
217 const std::vector<const char*>& urls) {
218 unsigned int total_urls = num_typed_urls + num_reload_urls;
219 DCHECK(urls.size() >= total_urls);
220 if (!typed_url_sync_service_.get())
223 // Set change processor.
224 syncer::SyncMergeResult result =
225 typed_url_sync_service_->MergeDataAndStartSyncing(
227 syncer::SyncDataList(),
228 scoped_ptr<syncer::SyncChangeProcessor>(
229 new syncer::SyncChangeProcessorWrapperForTest(
230 fake_change_processor_.get())),
231 scoped_ptr<syncer::SyncErrorFactory>(
232 new syncer::SyncErrorFactoryMock()));
233 EXPECT_FALSE(result.error().IsSet()) << result.error().message();
236 // Create new URL rows, populate the mock backend with its visits, and
237 // send to the sync service.
238 URLRows changed_urls;
240 for (unsigned int i = 0; i < total_urls; ++i) {
241 int typed = i < num_typed_urls ? 1 : 0;
243 visit_vectors->push_back(visits);
244 rows->push_back(MakeTypedUrlRow(
245 urls[i], "pie", typed, i + 3, false, &visit_vectors->back()));
246 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
247 SetVisitsForUrl(rows->back().id(), &visit_vectors->back());
248 changed_urls.push_back(rows->back());
251 typed_url_sync_service_->OnUrlsModified(&changed_urls);
253 // Check that communication with sync was successful.
254 if (num_typed_urls != fake_change_processor_->changes().size())
259 TEST_F(TypedUrlSyncableServiceTest, AddLocalTypedUrlAndSync) {
260 // Create a local typed URL (simulate a typed visit) that is not already
261 // in sync. Check that sync is sent an ADD change for the existing URL.
263 std::vector<VisitVector> visit_vectors;
264 std::vector<const char*> urls;
265 urls.push_back("http://pie.com/");
267 ASSERT_TRUE(InitiateServerState(1, 0, &url_rows, &visit_vectors, urls));
269 URLRow url_row = url_rows.front();
270 VisitVector visits = visit_vectors.front();
272 // Check change processor.
273 const syncer::SyncChangeList& changes = fake_change_processor_->changes();
274 ASSERT_EQ(1u, changes.size());
275 ASSERT_TRUE(changes[0].IsValid());
276 EXPECT_EQ(syncer::TYPED_URLS, changes[0].sync_data().GetDataType());
277 EXPECT_EQ(syncer::SyncChange::ACTION_ADD, changes[0].change_type());
279 // Get typed url specifics.
280 sync_pb::TypedUrlSpecifics url_specifics =
281 changes[0].sync_data().GetSpecifics().typed_url();
283 EXPECT_TRUE(URLsEqual(url_row, url_specifics));
284 ASSERT_EQ(1, url_specifics.visits_size());
285 ASSERT_EQ(static_cast<const int>(visits.size()), url_specifics.visits_size());
286 EXPECT_EQ(visits[0].visit_time.ToInternalValue(), url_specifics.visits(0));
287 EXPECT_EQ(static_cast<const int>(visits[0].transition),
288 url_specifics.visit_transitions(0));
290 // Check that in-memory representation of sync state is accurate.
291 std::set<GURL> sync_state;
292 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state);
293 EXPECT_FALSE(sync_state.empty());
294 EXPECT_EQ(1u, sync_state.size());
295 EXPECT_TRUE(sync_state.end() != sync_state.find(url_row.url()));
298 TEST_F(TypedUrlSyncableServiceTest, UpdateLocalTypedUrlAndSync) {
300 std::vector<VisitVector> visit_vectors;
301 std::vector<const char*> urls;
302 urls.push_back("http://pie.com/");
304 ASSERT_TRUE(InitiateServerState(1, 0, &url_rows, &visit_vectors, urls));
305 syncer::SyncChangeList& changes = fake_change_processor_->changes();
308 // Update the URL row, adding another typed visit to the visit vector.
309 URLRow url_row = url_rows.front();
310 VisitVector visits = visit_vectors.front();
312 URLRows changed_urls;
313 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_TYPED, 7);
314 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
315 SetVisitsForUrl(url_row.id(), &visits);
316 changed_urls.push_back(url_row);
318 // Notify typed url sync service of the update.
319 typed_url_sync_service_->OnUrlsModified(&changed_urls);
321 ASSERT_EQ(1u, changes.size());
322 ASSERT_TRUE(changes[0].IsValid());
323 EXPECT_EQ(syncer::TYPED_URLS, changes[0].sync_data().GetDataType());
324 EXPECT_EQ(syncer::SyncChange::ACTION_UPDATE, changes[0].change_type());
326 sync_pb::TypedUrlSpecifics url_specifics =
327 changes[0].sync_data().GetSpecifics().typed_url();
329 EXPECT_TRUE(URLsEqual(url_row, url_specifics));
330 ASSERT_EQ(2, url_specifics.visits_size());
331 ASSERT_EQ(static_cast<const int>(visits.size()), url_specifics.visits_size());
333 // Check that each visit has been translated/communicated correctly.
334 // Note that the specifics record visits in chronological order, and the
335 // visits from the db are in reverse chronological order.
336 EXPECT_EQ(visits[0].visit_time.ToInternalValue(), url_specifics.visits(1));
337 EXPECT_EQ(static_cast<const int>(visits[0].transition),
338 url_specifics.visit_transitions(1));
339 EXPECT_EQ(visits[1].visit_time.ToInternalValue(), url_specifics.visits(0));
340 EXPECT_EQ(static_cast<const int>(visits[1].transition),
341 url_specifics.visit_transitions(0));
343 // Check that in-memory representation of sync state is accurate.
344 std::set<GURL> sync_state;
345 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state);
346 EXPECT_FALSE(sync_state.empty());
347 EXPECT_EQ(1u, sync_state.size());
348 EXPECT_TRUE(sync_state.end() != sync_state.find(url_row.url()));
351 TEST_F(TypedUrlSyncableServiceTest, LinkVisitLocalTypedUrlAndSync) {
353 std::vector<VisitVector> visit_vectors;
354 std::vector<const char*> urls;
355 urls.push_back("http://pie.com/");
357 ASSERT_TRUE(InitiateServerState(1, 0, &url_rows, &visit_vectors, urls));
358 syncer::SyncChangeList& changes = fake_change_processor_->changes();
361 URLRow url_row = url_rows.front();
362 VisitVector visits = visit_vectors.front();
364 // Update the URL row, adding a non-typed visit to the visit vector.
365 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_LINK, 6);
366 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
367 SetVisitsForUrl(url_row.id(), &visits);
369 ui::PageTransition transition = ui::PAGE_TRANSITION_LINK;
370 // Notify typed url sync service of non-typed visit, expect no change.
371 typed_url_sync_service_->OnUrlVisited(transition, &url_row);
372 ASSERT_EQ(0u, changes.size());
375 TEST_F(TypedUrlSyncableServiceTest, TypedVisitLocalTypedUrlAndSync) {
377 std::vector<VisitVector> visit_vectors;
378 std::vector<const char*> urls;
379 urls.push_back("http://pie.com/");
381 ASSERT_TRUE(InitiateServerState(1, 0, &url_rows, &visit_vectors, urls));
382 syncer::SyncChangeList& changes = fake_change_processor_->changes();
385 URLRow url_row = url_rows.front();
386 VisitVector visits = visit_vectors.front();
388 // Update the URL row, adding another typed visit to the visit vector.
389 AddOldestVisit(&url_row, &visits, ui::PAGE_TRANSITION_LINK, 1);
390 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_LINK, 6);
391 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_TYPED, 7);
392 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
393 SetVisitsForUrl(url_row.id(), &visits);
395 // Notify typed url sync service of typed visit.
396 ui::PageTransition transition = ui::PAGE_TRANSITION_TYPED;
397 typed_url_sync_service_->OnUrlVisited(transition, &url_row);
399 ASSERT_EQ(1u, changes.size());
400 ASSERT_TRUE(changes[0].IsValid());
401 EXPECT_EQ(syncer::TYPED_URLS, changes[0].sync_data().GetDataType());
402 EXPECT_EQ(syncer::SyncChange::ACTION_UPDATE, changes[0].change_type());
404 sync_pb::TypedUrlSpecifics url_specifics =
405 changes[0].sync_data().GetSpecifics().typed_url();
407 EXPECT_TRUE(URLsEqual(url_row, url_specifics));
408 ASSERT_EQ(4u, visits.size());
409 EXPECT_EQ(static_cast<const int>(visits.size()), url_specifics.visits_size());
411 // Check that each visit has been translated/communicated correctly.
412 // Note that the specifics record visits in chronological order, and the
413 // visits from the db are in reverse chronological order.
414 int r = url_specifics.visits_size() - 1;
415 for (int i = 0; i < url_specifics.visits_size(); ++i, --r) {
416 EXPECT_EQ(visits[i].visit_time.ToInternalValue(), url_specifics.visits(r));
417 EXPECT_EQ(static_cast<const int>(visits[i].transition),
418 url_specifics.visit_transitions(r));
421 // Check that in-memory representation of sync state is accurate.
422 std::set<GURL> sync_state;
423 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state);
424 EXPECT_FALSE(sync_state.empty());
425 EXPECT_EQ(1u, sync_state.size());
426 EXPECT_TRUE(sync_state.end() != sync_state.find(url_row.url()));
429 TEST_F(TypedUrlSyncableServiceTest, DeleteLocalTypedUrlAndSync) {
431 std::vector<VisitVector> visit_vectors;
432 std::vector<const char*> urls;
433 urls.push_back("http://pie.com/");
434 urls.push_back("http://cake.com/");
435 urls.push_back("http://google.com/");
436 urls.push_back("http://foo.com/");
437 urls.push_back("http://bar.com/");
439 ASSERT_TRUE(InitiateServerState(4, 1, &url_rows, &visit_vectors, urls));
440 syncer::SyncChangeList& changes = fake_change_processor_->changes();
443 // Check that in-memory representation of sync state is accurate.
444 std::set<GURL> sync_state;
445 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state);
446 EXPECT_FALSE(sync_state.empty());
447 EXPECT_EQ(4u, sync_state.size());
449 // Simulate visit expiry of typed visit, no syncing is done
450 // This is to test that sync relies on the in-memory cache to know
451 // which urls were typed and synced, and should be deleted.
452 url_rows[0].set_typed_count(0);
454 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
455 SetVisitsForUrl(url_rows[0].id(), &visits);
457 // Delete some urls from backend and create deleted row vector.
459 for (size_t i = 0; i < 3u; ++i) {
460 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
461 DeleteVisitsForUrl(url_rows[i].id());
462 rows.push_back(url_rows[i]);
465 // Notify typed url sync service.
466 typed_url_sync_service_->OnUrlsDeleted(false, false, &rows);
468 ASSERT_EQ(3u, changes.size());
469 for (size_t i = 0; i < changes.size(); ++i) {
470 ASSERT_TRUE(changes[i].IsValid());
471 ASSERT_EQ(syncer::TYPED_URLS, changes[i].sync_data().GetDataType());
472 EXPECT_EQ(syncer::SyncChange::ACTION_DELETE, changes[i].change_type());
473 sync_pb::TypedUrlSpecifics url_specifics =
474 changes[i].sync_data().GetSpecifics().typed_url();
475 EXPECT_EQ(url_rows[i].url().spec(), url_specifics.url());
478 // Check that in-memory representation of sync state is accurate.
479 std::set<GURL> sync_state_deleted;
480 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state_deleted);
481 ASSERT_EQ(1u, sync_state_deleted.size());
482 EXPECT_TRUE(sync_state_deleted.end() !=
483 sync_state_deleted.find(url_rows[3].url()));
486 TEST_F(TypedUrlSyncableServiceTest, DeleteAllLocalTypedUrlAndSync) {
488 std::vector<VisitVector> visit_vectors;
489 std::vector<const char*> urls;
490 urls.push_back("http://pie.com/");
491 urls.push_back("http://cake.com/");
492 urls.push_back("http://google.com/");
493 urls.push_back("http://foo.com/");
494 urls.push_back("http://bar.com/");
496 ASSERT_TRUE(InitiateServerState(4, 1, &url_rows, &visit_vectors, urls));
497 syncer::SyncChangeList& changes = fake_change_processor_->changes();
500 // Check that in-memory representation of sync state is accurate.
501 std::set<GURL> sync_state;
502 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state);
503 EXPECT_EQ(4u, sync_state.size());
505 // Delete urls from backend.
506 for (size_t i = 0; i < 4u; ++ i) {
507 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
508 DeleteVisitsForUrl(url_rows[i].id());
510 // Delete urls with |all_history| flag set.
511 bool all_history = true;
513 // Notify typed url sync service.
514 typed_url_sync_service_->OnUrlsDeleted(all_history, false, NULL);
516 ASSERT_EQ(4u, changes.size());
517 for (size_t i = 0; i < changes.size(); ++i) {
518 ASSERT_TRUE(changes[i].IsValid());
519 ASSERT_EQ(syncer::TYPED_URLS, changes[i].sync_data().GetDataType());
520 EXPECT_EQ(syncer::SyncChange::ACTION_DELETE, changes[i].change_type());
522 // Check that in-memory representation of sync state is accurate.
523 std::set<GURL> sync_state_deleted;
524 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state_deleted);
525 EXPECT_TRUE(sync_state_deleted.empty());
528 TEST_F(TypedUrlSyncableServiceTest, MaxVisitLocalTypedUrlAndSync) {
530 std::vector<VisitVector> visit_vectors;
531 std::vector<const char*> urls;
532 urls.push_back("http://pie.com/");
534 ASSERT_TRUE(InitiateServerState(0, 1, &url_rows, &visit_vectors, urls));
536 URLRow url_row = url_rows.front();
539 // Add |kMaxTypedUrlVisits| + 10 visits to the url. The 10 oldest
540 // non-typed visits are expected to be skipped.
542 for (; i <= kMaxTypedUrlVisits - 20; ++i)
543 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_TYPED, i);
544 for (; i <= kMaxTypedUrlVisits; ++i)
545 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_LINK, i);
546 for (; i <= kMaxTypedUrlVisits + 10; ++i)
547 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_TYPED, i);
549 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
550 SetVisitsForUrl(url_row.id(), &visits);
552 // Notify typed url sync service of typed visit.
553 ui::PageTransition transition = ui::PAGE_TRANSITION_TYPED;
554 typed_url_sync_service_->OnUrlVisited(transition, &url_row);
556 const syncer::SyncChangeList& changes = fake_change_processor_->changes();
557 ASSERT_EQ(1u, changes.size());
558 ASSERT_TRUE(changes[0].IsValid());
559 sync_pb::TypedUrlSpecifics url_specifics =
560 changes[0].sync_data().GetSpecifics().typed_url();
561 ASSERT_EQ(kMaxTypedUrlVisits, url_specifics.visits_size());
563 // Check that each visit has been translated/communicated correctly.
564 // Note that the specifics record visits in chronological order, and the
565 // visits from the db are in reverse chronological order.
566 int num_typed_visits_synced = 0;
567 int num_other_visits_synced = 0;
568 int r = url_specifics.visits_size() - 1;
569 for (int i = 0; i < url_specifics.visits_size(); ++i, --r) {
570 if (url_specifics.visit_transitions(i) == ui::PAGE_TRANSITION_TYPED) {
571 ++num_typed_visits_synced;
573 ++num_other_visits_synced;
576 EXPECT_EQ(kMaxTypedUrlVisits - 10, num_typed_visits_synced);
577 EXPECT_EQ(10, num_other_visits_synced);
580 TEST_F(TypedUrlSyncableServiceTest, ThrottleVisitLocalTypedUrlSync) {
582 std::vector<VisitVector> visit_vectors;
583 std::vector<const char*> urls;
584 urls.push_back("http://pie.com/");
586 ASSERT_TRUE(InitiateServerState(0, 1, &url_rows, &visit_vectors, urls));
588 URLRow url_row = url_rows.front();
591 // Add enough visits to the url so that typed count is above the throttle
592 // limit, and not right on the interval that gets synced.
593 for (int i = 1; i < 42; ++i)
594 AddNewestVisit(&url_row, &visits, ui::PAGE_TRANSITION_TYPED, i);
596 static_cast<TestHistoryBackend*>(fake_history_backend_.get())->
597 SetVisitsForUrl(url_row.id(), &visits);
599 // Notify typed url sync service of typed visit.
600 ui::PageTransition transition = ui::PAGE_TRANSITION_TYPED;
601 typed_url_sync_service_->OnUrlVisited(transition, &url_row);
603 // Should throttle, so sync and local cache should not update.
604 const syncer::SyncChangeList& changes = fake_change_processor_->changes();
605 ASSERT_EQ(0u, changes.size());
606 std::set<GURL> sync_state;
607 typed_url_sync_service_.get()->GetSyncedUrls(&sync_state);
608 EXPECT_TRUE(sync_state.empty());
611 } // namespace history