Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 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 "chrome/browser/sync/profile_sync_service_harness.h" | 5 #include "chrome/browser/sync/profile_sync_service_harness.h" |
| 6 | 6 |
| 7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 #include <algorithm> | 8 #include <algorithm> |
| 9 #include <iterator> | 9 #include <iterator> |
| 10 #include <ostream> | 10 #include <ostream> |
| 11 #include <set> | 11 #include <set> |
| 12 #include <sstream> | 12 #include <sstream> |
| 13 #include <vector> | 13 #include <vector> |
| 14 | 14 |
| 15 #include "base/base64.h" | |
| 15 #include "base/json/json_writer.h" | 16 #include "base/json/json_writer.h" |
| 16 #include "base/logging.h" | 17 #include "base/logging.h" |
| 17 #include "base/memory/ref_counted.h" | 18 #include "base/memory/ref_counted.h" |
| 18 #include "base/message_loop.h" | 19 #include "base/message_loop.h" |
| 19 #include "base/task.h" | 20 #include "base/task.h" |
| 20 #include "base/tracked.h" | 21 #include "base/tracked.h" |
| 21 #include "chrome/browser/profiles/profile.h" | 22 #include "chrome/browser/profiles/profile.h" |
| 22 #include "chrome/browser/sync/sessions/session_state.h" | 23 #include "chrome/browser/sync/sessions/session_state.h" |
| 23 #include "chrome/browser/sync/signin_manager.h" | 24 #include "chrome/browser/sync/signin_manager.h" |
| 24 #include "chrome/browser/sync/sync_ui_util.h" | 25 #include "chrome/browser/sync/sync_ui_util.h" |
| (...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 213 void ProfileSyncServiceHarness::SignalStateCompleteWithNextState( | 214 void ProfileSyncServiceHarness::SignalStateCompleteWithNextState( |
| 214 WaitState next_state) { | 215 WaitState next_state) { |
| 215 wait_state_ = next_state; | 216 wait_state_ = next_state; |
| 216 SignalStateComplete(); | 217 SignalStateComplete(); |
| 217 } | 218 } |
| 218 | 219 |
| 219 void ProfileSyncServiceHarness::SignalStateComplete() { | 220 void ProfileSyncServiceHarness::SignalStateComplete() { |
| 220 MessageLoop::current()->Quit(); | 221 MessageLoop::current()->Quit(); |
| 221 } | 222 } |
| 222 | 223 |
| 224 void ProfileSyncServiceHarness::UpdateMigrationState() { | |
| 225 if (service()->HasPendingBackendMigration()) { | |
| 226 // Merge current pending migration types into | |
| 227 // |pending_migration_types_|. | |
|
Raghu Simha
2011/09/01 03:53:06
Should we not simply replace pending_migration_typ
akalin
2011/09/01 04:03:24
The idea is to accumulate all migrated types inste
Raghu Simha
2011/09/01 04:23:58
SGTM.
| |
| 228 syncable::ModelTypeSet new_pending_migration_types = | |
| 229 service()->GetPendingMigrationTypesForTest(); | |
| 230 syncable::ModelTypeSet temp; | |
| 231 std::set_union(pending_migration_types_.begin(), | |
| 232 pending_migration_types_.end(), | |
| 233 new_pending_migration_types.begin(), | |
| 234 new_pending_migration_types.end(), | |
| 235 std::inserter(temp, temp.end())); | |
| 236 std::swap(pending_migration_types_, temp); | |
| 237 VLOG(1) << profile_debug_name_ << ": new pending migration types " | |
| 238 << syncable::ModelTypeSetToString(pending_migration_types_); | |
| 239 } else { | |
| 240 // Merge just-finished pending migration types into | |
| 241 // |migration_types_|. | |
| 242 syncable::ModelTypeSet temp; | |
| 243 std::set_union(pending_migration_types_.begin(), | |
| 244 pending_migration_types_.end(), | |
| 245 migrated_types_.begin(), | |
| 246 migrated_types_.end(), | |
| 247 std::inserter(temp, temp.end())); | |
| 248 std::swap(migrated_types_, temp); | |
| 249 pending_migration_types_.clear(); | |
| 250 VLOG(1) << profile_debug_name_ << ": new migrated types " | |
| 251 << syncable::ModelTypeSetToString(migrated_types_); | |
| 252 } | |
| 253 } | |
| 254 | |
| 223 bool ProfileSyncServiceHarness::RunStateChangeMachine() { | 255 bool ProfileSyncServiceHarness::RunStateChangeMachine() { |
| 224 WaitState original_wait_state = wait_state_; | 256 WaitState original_wait_state = wait_state_; |
| 225 switch (wait_state_) { | 257 switch (wait_state_) { |
| 226 case WAITING_FOR_ON_BACKEND_INITIALIZED: { | 258 case WAITING_FOR_ON_BACKEND_INITIALIZED: { |
| 227 VLOG(1) << GetClientInfoString("WAITING_FOR_ON_BACKEND_INITIALIZED"); | 259 VLOG(1) << GetClientInfoString("WAITING_FOR_ON_BACKEND_INITIALIZED"); |
| 228 if (service()->sync_initialized()) { | 260 if (service()->sync_initialized()) { |
| 229 // The sync backend is initialized. | 261 // The sync backend is initialized. |
| 230 SignalStateCompleteWithNextState(WAITING_FOR_INITIAL_SYNC); | 262 SignalStateCompleteWithNextState(WAITING_FOR_INITIAL_SYNC); |
| 231 } | 263 } |
| 232 break; | 264 break; |
| (...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 337 VLOG(1) << GetClientInfoString( | 369 VLOG(1) << GetClientInfoString( |
| 338 "WAITING_FOR_EXPONENTIAL_BACKOFF_VERIFICATION"); | 370 "WAITING_FOR_EXPONENTIAL_BACKOFF_VERIFICATION"); |
| 339 const browser_sync::sessions::SyncSessionSnapshot *snap = | 371 const browser_sync::sessions::SyncSessionSnapshot *snap = |
| 340 GetLastSessionSnapshot(); | 372 GetLastSessionSnapshot(); |
| 341 CHECK(snap); | 373 CHECK(snap); |
| 342 retry_verifier_.VerifyRetryInterval(*snap); | 374 retry_verifier_.VerifyRetryInterval(*snap); |
| 343 if (retry_verifier_.done()) | 375 if (retry_verifier_.done()) |
| 344 SignalStateCompleteWithNextState(WAITING_FOR_NOTHING); | 376 SignalStateCompleteWithNextState(WAITING_FOR_NOTHING); |
| 345 break; | 377 break; |
| 346 } | 378 } |
| 379 case WAITING_FOR_MIGRATION_TO_START: { | |
| 380 VLOG(1) << GetClientInfoString("WAITING_FOR_MIGRATION_TO_START"); | |
| 381 if (service()->HasPendingBackendMigration()) { | |
| 382 SignalStateCompleteWithNextState(WAITING_FOR_MIGRATION_TO_FINISH); | |
| 383 } | |
| 384 break; | |
| 385 } | |
| 386 case WAITING_FOR_MIGRATION_TO_FINISH: { | |
| 387 VLOG(1) << GetClientInfoString("WAITING_FOR_MIGRATION_TO_FINISH"); | |
| 388 if (!service()->HasPendingBackendMigration()) { | |
| 389 SignalStateCompleteWithNextState(WAITING_FOR_NOTHING); | |
| 390 } | |
| 391 break; | |
| 392 } | |
| 347 case SERVER_UNREACHABLE: { | 393 case SERVER_UNREACHABLE: { |
| 348 VLOG(1) << GetClientInfoString("SERVER_UNREACHABLE"); | 394 VLOG(1) << GetClientInfoString("SERVER_UNREACHABLE"); |
| 349 if (GetStatus().server_reachable) { | 395 if (GetStatus().server_reachable) { |
| 350 // The client was offline due to the network being disabled, but is now | 396 // The client was offline due to the network being disabled, but is now |
| 351 // back online. Wait for the pending sync cycle to complete. | 397 // back online. Wait for the pending sync cycle to complete. |
| 352 SignalStateCompleteWithNextState(WAITING_FOR_SYNC_TO_FINISH); | 398 SignalStateCompleteWithNextState(WAITING_FOR_SYNC_TO_FINISH); |
| 353 } | 399 } |
| 354 break; | 400 break; |
| 355 } | 401 } |
| 356 case SET_PASSPHRASE_FAILED: { | 402 case SET_PASSPHRASE_FAILED: { |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 377 } | 423 } |
| 378 default: | 424 default: |
| 379 // Invalid state during observer callback which may be triggered by other | 425 // Invalid state during observer callback which may be triggered by other |
| 380 // classes using the the UI message loop. Defer to their handling. | 426 // classes using the the UI message loop. Defer to their handling. |
| 381 break; | 427 break; |
| 382 } | 428 } |
| 383 return original_wait_state != wait_state_; | 429 return original_wait_state != wait_state_; |
| 384 } | 430 } |
| 385 | 431 |
| 386 void ProfileSyncServiceHarness::OnStateChanged() { | 432 void ProfileSyncServiceHarness::OnStateChanged() { |
| 433 UpdateMigrationState(); | |
|
Raghu Simha
2011/09/01 03:53:06
Any particular reason for calling UpdateMigrationS
akalin
2011/09/01 04:03:24
Addressed in latest patch.
| |
| 387 RunStateChangeMachine(); | 434 RunStateChangeMachine(); |
| 388 } | 435 } |
| 389 | 436 |
| 390 bool ProfileSyncServiceHarness::AwaitPassphraseRequired() { | 437 bool ProfileSyncServiceHarness::AwaitPassphraseRequired() { |
| 391 VLOG(1) << GetClientInfoString("AwaitPassphraseRequired"); | 438 VLOG(1) << GetClientInfoString("AwaitPassphraseRequired"); |
| 392 if (wait_state_ == SYNC_DISABLED) { | 439 if (wait_state_ == SYNC_DISABLED) { |
| 393 LOG(ERROR) << "Sync disabled for " << profile_debug_name_ << "."; | 440 LOG(ERROR) << "Sync disabled for " << profile_debug_name_ << "."; |
| 394 return false; | 441 return false; |
| 395 } | 442 } |
| 396 | 443 |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 462 if (wait_state_ == SYNC_DISABLED) { | 509 if (wait_state_ == SYNC_DISABLED) { |
| 463 LOG(ERROR) << "Sync disabled for " << profile_debug_name_ << "."; | 510 LOG(ERROR) << "Sync disabled for " << profile_debug_name_ << "."; |
| 464 return false; | 511 return false; |
| 465 } | 512 } |
| 466 | 513 |
| 467 if (IsSynced()) { | 514 if (IsSynced()) { |
| 468 // Client is already synced; don't wait. | 515 // Client is already synced; don't wait. |
| 469 return true; | 516 return true; |
| 470 } | 517 } |
| 471 | 518 |
| 472 return AwaitSyncCycleCompletionHelper(reason); | |
| 473 } | |
| 474 | |
| 475 bool ProfileSyncServiceHarness::AwaitNextSyncCycleCompletion( | |
| 476 const std::string& reason) { | |
| 477 VLOG(1) << GetClientInfoString("AwaitNextSyncCycleCompletion"); | |
| 478 return AwaitSyncCycleCompletionHelper(reason); | |
| 479 } | |
| 480 | |
| 481 bool ProfileSyncServiceHarness::AwaitSyncCycleCompletionHelper( | |
| 482 const std::string& reason) { | |
| 483 if (wait_state_ == SERVER_UNREACHABLE) { | 519 if (wait_state_ == SERVER_UNREACHABLE) { |
| 484 // Client was offline; wait for it to go online, and then wait for sync. | 520 // Client was offline; wait for it to go online, and then wait for sync. |
| 485 AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, reason); | 521 AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, reason); |
| 486 DCHECK_EQ(wait_state_, WAITING_FOR_SYNC_TO_FINISH); | 522 DCHECK_EQ(wait_state_, WAITING_FOR_SYNC_TO_FINISH); |
| 487 return AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, reason); | 523 return AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, reason); |
| 488 } | 524 } |
| 489 | 525 |
| 490 DCHECK(service()->sync_initialized()); | 526 DCHECK(service()->sync_initialized()); |
| 491 wait_state_ = WAITING_FOR_SYNC_TO_FINISH; | 527 wait_state_ = WAITING_FOR_SYNC_TO_FINISH; |
| 492 AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, reason); | 528 AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, reason); |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 515 const browser_sync::sessions::SyncSessionSnapshot *snap = | 551 const browser_sync::sessions::SyncSessionSnapshot *snap = |
| 516 GetLastSessionSnapshot(); | 552 GetLastSessionSnapshot(); |
| 517 CHECK(snap); | 553 CHECK(snap); |
| 518 retry_verifier_.Initialize(*snap); | 554 retry_verifier_.Initialize(*snap); |
| 519 wait_state_ = WAITING_FOR_EXPONENTIAL_BACKOFF_VERIFICATION; | 555 wait_state_ = WAITING_FOR_EXPONENTIAL_BACKOFF_VERIFICATION; |
| 520 AwaitStatusChangeWithTimeout(kExponentialBackoffVerificationTimeoutMs, | 556 AwaitStatusChangeWithTimeout(kExponentialBackoffVerificationTimeoutMs, |
| 521 "Verify Exponential backoff"); | 557 "Verify Exponential backoff"); |
| 522 return (retry_verifier_.Succeeded()); | 558 return (retry_verifier_.Succeeded()); |
| 523 } | 559 } |
| 524 | 560 |
| 561 bool ProfileSyncServiceHarness::AwaitMigration( | |
| 562 const syncable::ModelTypeSet& expected_migrated_types) { | |
| 563 VLOG(1) << GetClientInfoString("AwaitMigration"); | |
| 564 VLOG(1) << profile_debug_name_ << ": waiting until migration is done for " | |
| 565 << syncable::ModelTypeSetToString(expected_migrated_types); | |
| 566 while (true) { | |
| 567 bool migration_finished = | |
| 568 std::includes(migrated_types_.begin(), migrated_types_.end(), | |
| 569 expected_migrated_types.begin(), | |
| 570 expected_migrated_types.end()); | |
| 571 VLOG(1) << "Migrated types " | |
| 572 << syncable::ModelTypeSetToString(migrated_types_) | |
| 573 << (migration_finished ? " contains " : " does not contain ") | |
| 574 << syncable::ModelTypeSetToString(expected_migrated_types); | |
| 575 if (migration_finished) { | |
| 576 return true; | |
| 577 } | |
| 578 | |
| 579 if (service()->HasPendingBackendMigration()) { | |
| 580 wait_state_ = WAITING_FOR_MIGRATION_TO_FINISH; | |
| 581 } else { | |
| 582 wait_state_ = WAITING_FOR_MIGRATION_TO_START; | |
| 583 AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, | |
| 584 "Wait for migration to start"); | |
| 585 if (wait_state_ != WAITING_FOR_MIGRATION_TO_FINISH) { | |
| 586 VLOG(1) << profile_debug_name_ | |
| 587 << ": wait state = " << wait_state_ | |
| 588 << " after migration start is not " | |
| 589 << "WAITING_FOR_MIGRATION_TO_FINISH"; | |
| 590 return false; | |
| 591 } | |
| 592 } | |
| 593 AwaitStatusChangeWithTimeout(kLiveSyncOperationTimeoutMs, | |
| 594 "Wait for migration to finish"); | |
| 595 if (wait_state_ != WAITING_FOR_NOTHING) { | |
| 596 VLOG(1) << profile_debug_name_ | |
| 597 << ": wait state = " << wait_state_ | |
| 598 << " after migration finish is not WAITING_FOR_NOTHING"; | |
| 599 return false; | |
| 600 } | |
| 601 if (!AwaitSyncCycleCompletion( | |
| 602 "Config sync cycle after migration cycle")) { | |
| 603 return false; | |
| 604 } | |
| 605 } | |
| 606 } | |
| 607 | |
| 525 bool ProfileSyncServiceHarness::AwaitMutualSyncCycleCompletion( | 608 bool ProfileSyncServiceHarness::AwaitMutualSyncCycleCompletion( |
| 526 ProfileSyncServiceHarness* partner) { | 609 ProfileSyncServiceHarness* partner) { |
| 527 VLOG(1) << GetClientInfoString("AwaitMutualSyncCycleCompletion"); | 610 VLOG(1) << GetClientInfoString("AwaitMutualSyncCycleCompletion"); |
| 528 if (!AwaitSyncCycleCompletion("Sync cycle completion on active client.")) | 611 if (!AwaitSyncCycleCompletion("Sync cycle completion on active client.")) |
| 529 return false; | 612 return false; |
| 530 return partner->WaitUntilTimestampMatches(this, | 613 return partner->WaitUntilTimestampMatches(this, |
| 531 "Sync cycle completion on passive client."); | 614 "Sync cycle completion on passive client."); |
| 532 } | 615 } |
| 533 | 616 |
| 534 bool ProfileSyncServiceHarness::AwaitGroupSyncCycleCompletion( | 617 bool ProfileSyncServiceHarness::AwaitGroupSyncCycleCompletion( |
| (...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 634 !service()->HasPendingBackendMigration() && | 717 !service()->HasPendingBackendMigration() && |
| 635 service()->passphrase_required_reason() != | 718 service()->passphrase_required_reason() != |
| 636 sync_api::REASON_SET_PASSPHRASE_FAILED; | 719 sync_api::REASON_SET_PASSPHRASE_FAILED; |
| 637 VLOG(1) << GetClientInfoString( | 720 VLOG(1) << GetClientInfoString( |
| 638 is_synced ? "IsSynced: true" : "IsSynced: false"); | 721 is_synced ? "IsSynced: true" : "IsSynced: false"); |
| 639 return is_synced; | 722 return is_synced; |
| 640 } | 723 } |
| 641 | 724 |
| 642 bool ProfileSyncServiceHarness::MatchesOtherClient( | 725 bool ProfileSyncServiceHarness::MatchesOtherClient( |
| 643 ProfileSyncServiceHarness* partner) { | 726 ProfileSyncServiceHarness* partner) { |
| 644 if (!IsSynced()) | 727 // TODO(akalin): Shouldn't this with the intersection check? |
|
Raghu Simha
2011/09/01 03:53:06
Is this comment Yoda-speak? :)
akalin
2011/09/01 04:03:24
Fixed!
| |
| 728 // Otherwise, this function isn't symmetric. | |
| 729 if (!IsSynced()) { | |
| 730 VLOG(1) << profile_debug_name_ << ": not synced, assuming doesn't match"; | |
|
Raghu Simha
2011/09/01 03:53:06
This should probably be a VLOG(2), since it's a ge
akalin
2011/09/01 04:03:24
Done.
| |
| 645 return false; | 731 return false; |
| 732 } | |
| 646 | 733 |
| 647 // Only look for a match if we have at least one enabled datatype in | 734 // Only look for a match if we have at least one enabled datatype in |
| 648 // common with the partner client. | 735 // common with the partner client. |
| 649 syncable::ModelTypeSet types, other_types, intersection_types; | 736 syncable::ModelTypeSet types, other_types, intersection_types; |
| 650 service()->GetPreferredDataTypes(&types); | 737 service()->GetPreferredDataTypes(&types); |
| 651 partner->service()->GetPreferredDataTypes(&other_types); | 738 partner->service()->GetPreferredDataTypes(&other_types); |
| 652 std::set_intersection(types.begin(), types.end(), other_types.begin(), | 739 std::set_intersection(types.begin(), types.end(), other_types.begin(), |
| 653 other_types.end(), | 740 other_types.end(), |
| 654 inserter(intersection_types, | 741 inserter(intersection_types, |
| 655 intersection_types.begin())); | 742 intersection_types.begin())); |
| 743 | |
| 744 VLOG(1) << profile_debug_name_ << ", " << partner->profile_debug_name_ | |
| 745 << ": common types are " | |
| 746 << syncable::ModelTypeSetToString(intersection_types); | |
| 747 | |
| 748 if (!intersection_types.empty() && !partner->IsSynced()) { | |
| 749 VLOG(1) << "non-empty common types and " | |
|
Raghu Simha
2011/09/01 03:53:06
Same as above. This too should probably be a VLOG(
akalin
2011/09/01 04:03:24
Done.
| |
| 750 << partner->profile_debug_name_ << " isn't synced"; | |
| 751 return false; | |
| 752 } | |
| 753 | |
| 656 for (syncable::ModelTypeSet::iterator i = intersection_types.begin(); | 754 for (syncable::ModelTypeSet::iterator i = intersection_types.begin(); |
| 657 i != intersection_types.end(); | 755 i != intersection_types.end(); ++i) { |
| 658 ++i) { | 756 const std::string timestamp = GetUpdatedTimestamp(*i); |
| 659 if (!partner->IsSynced() || | 757 const std::string partner_timestamp = partner->GetUpdatedTimestamp(*i); |
| 660 partner->GetUpdatedTimestamp(*i) != GetUpdatedTimestamp(*i)) { | 758 if (timestamp != partner_timestamp) { |
| 759 if (VLOG_IS_ON(1)) { | |
| 760 std::string timestamp_base64, partner_timestamp_base64; | |
| 761 if (!base::Base64Encode(timestamp, ×tamp_base64)) { | |
| 762 NOTREACHED(); | |
| 763 } | |
| 764 if (!base::Base64Encode( | |
| 765 partner_timestamp, &partner_timestamp_base64)) { | |
| 766 NOTREACHED(); | |
| 767 } | |
| 768 VLOG(1) << syncable::ModelTypeToString(*i) << ": " | |
| 769 << profile_debug_name_ << " timestamp = " | |
| 770 << timestamp_base64 << ", " | |
| 771 << partner->profile_debug_name_ | |
| 772 << " partner timestamp = " | |
| 773 << partner_timestamp_base64; | |
| 774 } | |
| 661 return false; | 775 return false; |
| 662 } | 776 } |
| 663 } | 777 } |
| 664 return true; | 778 return true; |
| 665 } | 779 } |
| 666 | 780 |
| 667 const SyncSessionSnapshot* | 781 const SyncSessionSnapshot* |
| 668 ProfileSyncServiceHarness::GetLastSessionSnapshot() const { | 782 ProfileSyncServiceHarness::GetLastSessionSnapshot() const { |
| 669 DCHECK(service_ != NULL) << "Sync service has not yet been set up."; | 783 DCHECK(service_ != NULL) << "Sync service has not yet been set up."; |
| 670 if (service_->sync_initialized()) { | 784 if (service_->sync_initialized()) { |
| (...skipping 224 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 895 return (synced_types.count(type) != 0); | 1009 return (synced_types.count(type) != 0); |
| 896 } | 1010 } |
| 897 | 1011 |
| 898 std::string ProfileSyncServiceHarness::GetServiceStatus() { | 1012 std::string ProfileSyncServiceHarness::GetServiceStatus() { |
| 899 DictionaryValue value; | 1013 DictionaryValue value; |
| 900 sync_ui_util::ConstructAboutInformation(service_, &value); | 1014 sync_ui_util::ConstructAboutInformation(service_, &value); |
| 901 std::string service_status; | 1015 std::string service_status; |
| 902 base::JSONWriter::Write(&value, true, &service_status); | 1016 base::JSONWriter::Write(&value, true, &service_status); |
| 903 return service_status; | 1017 return service_status; |
| 904 } | 1018 } |
| OLD | NEW |