Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(135)

Side by Side Diff: chrome/browser/sync/internal_api/syncapi_unittest.cc

Issue 7926001: [Sync] Move change-related methods out of SyncManager::Observer (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address comments Created 9 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 // Unit tests for the SyncApi. Note that a lot of the underlying 5 // Unit tests for the SyncApi. Note that a lot of the underlying
6 // functionality is provided by the Syncable layer, which has its own 6 // functionality is provided by the Syncable layer, which has its own
7 // unit tests. We'll test SyncApi specific things in this harness. 7 // unit tests. We'll test SyncApi specific things in this harness.
8 8
9 #include <cstddef> 9 #include <cstddef>
10 #include <map> 10 #include <map>
(...skipping 22 matching lines...) Expand all
33 #include "chrome/browser/sync/internal_api/write_node.h" 33 #include "chrome/browser/sync/internal_api/write_node.h"
34 #include "chrome/browser/sync/internal_api/write_transaction.h" 34 #include "chrome/browser/sync/internal_api/write_transaction.h"
35 #include "chrome/browser/sync/js/js_arg_list.h" 35 #include "chrome/browser/sync/js/js_arg_list.h"
36 #include "chrome/browser/sync/js/js_backend.h" 36 #include "chrome/browser/sync/js/js_backend.h"
37 #include "chrome/browser/sync/js/js_event_handler.h" 37 #include "chrome/browser/sync/js/js_event_handler.h"
38 #include "chrome/browser/sync/js/js_reply_handler.h" 38 #include "chrome/browser/sync/js/js_reply_handler.h"
39 #include "chrome/browser/sync/js/js_test_util.h" 39 #include "chrome/browser/sync/js/js_test_util.h"
40 #include "chrome/browser/sync/notifier/sync_notifier.h" 40 #include "chrome/browser/sync/notifier/sync_notifier.h"
41 #include "chrome/browser/sync/notifier/sync_notifier_observer.h" 41 #include "chrome/browser/sync/notifier/sync_notifier_observer.h"
42 #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" 42 #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h"
43 #include "chrome/browser/sync/protocol/extension_specifics.pb.h"
43 #include "chrome/browser/sync/protocol/password_specifics.pb.h" 44 #include "chrome/browser/sync/protocol/password_specifics.pb.h"
44 #include "chrome/browser/sync/protocol/proto_value_conversions.h" 45 #include "chrome/browser/sync/protocol/proto_value_conversions.h"
45 #include "chrome/browser/sync/protocol/sync.pb.h" 46 #include "chrome/browser/sync/protocol/sync.pb.h"
46 #include "chrome/browser/sync/sessions/sync_session.h" 47 #include "chrome/browser/sync/sessions/sync_session.h"
47 #include "chrome/browser/sync/syncable/directory_manager.h" 48 #include "chrome/browser/sync/syncable/directory_manager.h"
48 #include "chrome/browser/sync/syncable/syncable.h" 49 #include "chrome/browser/sync/syncable/syncable.h"
49 #include "chrome/browser/sync/syncable/syncable_id.h" 50 #include "chrome/browser/sync/syncable/syncable_id.h"
50 #include "chrome/browser/sync/test/engine/test_user_share.h" 51 #include "chrome/browser/sync/test/engine/test_user_share.h"
51 #include "chrome/browser/sync/util/cryptographer.h" 52 #include "chrome/browser/sync/util/cryptographer.h"
52 #include "chrome/test/base/values_test_util.h" 53 #include "chrome/test/base/values_test_util.h"
(...skipping 13 matching lines...) Expand all
66 using browser_sync::MockJsReplyHandler; 67 using browser_sync::MockJsReplyHandler;
67 using browser_sync::ModelSafeRoutingInfo; 68 using browser_sync::ModelSafeRoutingInfo;
68 using browser_sync::ModelSafeWorker; 69 using browser_sync::ModelSafeWorker;
69 using browser_sync::ModelSafeWorkerRegistrar; 70 using browser_sync::ModelSafeWorkerRegistrar;
70 using browser_sync::sessions::SyncSessionSnapshot; 71 using browser_sync::sessions::SyncSessionSnapshot;
71 using browser_sync::WeakHandle; 72 using browser_sync::WeakHandle;
72 using syncable::GetAllRealModelTypes; 73 using syncable::GetAllRealModelTypes;
73 using syncable::kEncryptedString; 74 using syncable::kEncryptedString;
74 using syncable::ModelType; 75 using syncable::ModelType;
75 using syncable::ModelTypeSet; 76 using syncable::ModelTypeSet;
76 using test::ExpectDictDictionaryValue;
77 using test::ExpectDictStringValue; 77 using test::ExpectDictStringValue;
78 using testing::_; 78 using testing::_;
79 using testing::AnyNumber; 79 using testing::AnyNumber;
80 using testing::AtLeast; 80 using testing::AtLeast;
81 using testing::InSequence; 81 using testing::InSequence;
82 using testing::Invoke; 82 using testing::Invoke;
83 using testing::SaveArg; 83 using testing::SaveArg;
84 using testing::StrictMock; 84 using testing::StrictMock;
85 85
86 namespace sync_api { 86 namespace sync_api {
(...skipping 464 matching lines...) Expand 10 before | Expand all | Expand 10 after
551 scoped_ptr<DictionaryValue> details(node.GetDetailsAsValue()); 551 scoped_ptr<DictionaryValue> details(node.GetDetailsAsValue());
552 if (details.get()) { 552 if (details.get()) {
553 CheckNodeValue(node, *details, true); 553 CheckNodeValue(node, *details, true);
554 } else { 554 } else {
555 ADD_FAILURE(); 555 ADD_FAILURE();
556 } 556 }
557 } 557 }
558 558
559 namespace { 559 namespace {
560 560
561 void ExpectChangeRecordActionValue(ChangeRecord::Action expected_value,
562 const DictionaryValue& value,
563 const std::string& key) {
564 std::string str_value;
565 EXPECT_TRUE(value.GetString(key, &str_value));
566 switch (expected_value) {
567 case ChangeRecord::ACTION_ADD:
568 EXPECT_EQ("Add", str_value);
569 break;
570 case ChangeRecord::ACTION_UPDATE:
571 EXPECT_EQ("Update", str_value);
572 break;
573 case ChangeRecord::ACTION_DELETE:
574 EXPECT_EQ("Delete", str_value);
575 break;
576 default:
577 NOTREACHED();
578 break;
579 }
580 }
581
582 void CheckNonDeleteChangeRecordValue(const ChangeRecord& record,
583 const DictionaryValue& value,
584 BaseTransaction* trans) {
585 EXPECT_NE(ChangeRecord::ACTION_DELETE, record.action);
586 ExpectChangeRecordActionValue(record.action, value, "action");
587 {
588 ReadNode node(trans);
589 EXPECT_TRUE(node.InitByIdLookup(record.id));
590 scoped_ptr<DictionaryValue> expected_details(node.GetDetailsAsValue());
591 ExpectDictDictionaryValue(*expected_details, value, "node");
592 }
593 }
594
595 void CheckDeleteChangeRecordValue(const ChangeRecord& record,
596 const DictionaryValue& value) {
597 EXPECT_EQ(ChangeRecord::ACTION_DELETE, record.action);
598 ExpectChangeRecordActionValue(record.action, value, "action");
599 DictionaryValue* node_value = NULL;
600 EXPECT_TRUE(value.GetDictionary("node", &node_value));
601 if (node_value) {
602 ExpectInt64Value(record.id, *node_value, "id");
603 scoped_ptr<DictionaryValue> expected_specifics_value(
604 browser_sync::EntitySpecificsToValue(record.specifics));
605 ExpectDictDictionaryValue(*expected_specifics_value,
606 *node_value, "specifics");
607 scoped_ptr<DictionaryValue> expected_extra_value;
608 if (record.extra.get()) {
609 expected_extra_value.reset(record.extra->ToValue());
610 }
611 Value* extra_value = NULL;
612 EXPECT_EQ(record.extra.get() != NULL,
613 node_value->Get("extra", &extra_value));
614 EXPECT_TRUE(Value::Equals(extra_value, expected_extra_value.get()));
615 }
616 }
617
618 class MockExtraChangeRecordData
619 : public ExtraPasswordChangeRecordData {
620 public:
621 MOCK_CONST_METHOD0(ToValue, DictionaryValue*());
622 };
623
624 } // namespace
625
626 TEST_F(SyncApiTest, ChangeRecordToValue) {
627 int64 child_id = MakeNode(test_user_share_.user_share(),
628 syncable::BOOKMARKS, "testtag");
629 sync_pb::EntitySpecifics child_specifics;
630 {
631 ReadTransaction trans(FROM_HERE, test_user_share_.user_share());
632 ReadNode node(&trans);
633 EXPECT_TRUE(node.InitByIdLookup(child_id));
634 child_specifics = node.GetEntry()->Get(syncable::SPECIFICS);
635 }
636
637 // Add
638 {
639 ReadTransaction trans(FROM_HERE, test_user_share_.user_share());
640 ChangeRecord record;
641 record.action = ChangeRecord::ACTION_ADD;
642 record.id = 1;
643 record.specifics = child_specifics;
644 record.extra.reset(new StrictMock<MockExtraChangeRecordData>());
645 scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
646 CheckNonDeleteChangeRecordValue(record, *value, &trans);
647 }
648
649 // Update
650 {
651 ReadTransaction trans(FROM_HERE, test_user_share_.user_share());
652 ChangeRecord record;
653 record.action = ChangeRecord::ACTION_UPDATE;
654 record.id = child_id;
655 record.specifics = child_specifics;
656 record.extra.reset(new StrictMock<MockExtraChangeRecordData>());
657 scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
658 CheckNonDeleteChangeRecordValue(record, *value, &trans);
659 }
660
661 // Delete (no extra)
662 {
663 ReadTransaction trans(FROM_HERE, test_user_share_.user_share());
664 ChangeRecord record;
665 record.action = ChangeRecord::ACTION_DELETE;
666 record.id = child_id + 1;
667 record.specifics = child_specifics;
668 scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
669 CheckDeleteChangeRecordValue(record, *value);
670 }
671
672 // Delete (with extra)
673 {
674 ReadTransaction trans(FROM_HERE, test_user_share_.user_share());
675 ChangeRecord record;
676 record.action = ChangeRecord::ACTION_DELETE;
677 record.id = child_id + 1;
678 record.specifics = child_specifics;
679
680 DictionaryValue extra_value;
681 extra_value.SetString("foo", "bar");
682 scoped_ptr<StrictMock<MockExtraChangeRecordData> > extra(
683 new StrictMock<MockExtraChangeRecordData>());
684 EXPECT_CALL(*extra, ToValue()).Times(2).WillRepeatedly(
685 Invoke(&extra_value, &DictionaryValue::DeepCopy));
686
687 record.extra.reset(extra.release());
688 scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
689 CheckDeleteChangeRecordValue(record, *value);
690 }
691 }
692
693 namespace {
694
695 class TestHttpPostProviderInterface : public HttpPostProviderInterface { 561 class TestHttpPostProviderInterface : public HttpPostProviderInterface {
696 public: 562 public:
697 virtual ~TestHttpPostProviderInterface() {} 563 virtual ~TestHttpPostProviderInterface() {}
698 564
699 virtual void SetUserAgent(const char* user_agent) OVERRIDE {} 565 virtual void SetUserAgent(const char* user_agent) OVERRIDE {}
700 virtual void SetExtraRequestHeaders(const char* headers) OVERRIDE {} 566 virtual void SetExtraRequestHeaders(const char* headers) OVERRIDE {}
701 virtual void SetURL(const char* url, int port) OVERRIDE {} 567 virtual void SetURL(const char* url, int port) OVERRIDE {}
702 virtual void SetPostPayload(const char* content_type, 568 virtual void SetPostPayload(const char* content_type,
703 int content_length, 569 int content_length,
704 const char* content) OVERRIDE {} 570 const char* content) OVERRIDE {}
(...skipping 20 matching lines...) Expand all
725 virtual HttpPostProviderInterface* Create() OVERRIDE { 591 virtual HttpPostProviderInterface* Create() OVERRIDE {
726 return new TestHttpPostProviderInterface(); 592 return new TestHttpPostProviderInterface();
727 } 593 }
728 virtual void Destroy(HttpPostProviderInterface* http) OVERRIDE { 594 virtual void Destroy(HttpPostProviderInterface* http) OVERRIDE {
729 delete http; 595 delete http;
730 } 596 }
731 }; 597 };
732 598
733 class SyncManagerObserverMock : public SyncManager::Observer { 599 class SyncManagerObserverMock : public SyncManager::Observer {
734 public: 600 public:
735 MOCK_METHOD3(OnChangesApplied,
736 void(ModelType,
737 const BaseTransaction*,
738 const ImmutableChangeRecordList&)); // NOLINT
739 MOCK_METHOD1(OnChangesComplete, void(ModelType)); // NOLINT
740 MOCK_METHOD1(OnSyncCycleCompleted, 601 MOCK_METHOD1(OnSyncCycleCompleted,
741 void(const SyncSessionSnapshot*)); // NOLINT 602 void(const SyncSessionSnapshot*)); // NOLINT
742 MOCK_METHOD2(OnInitializationComplete, 603 MOCK_METHOD2(OnInitializationComplete,
743 void(const WeakHandle<JsBackend>&, bool)); // NOLINT 604 void(const WeakHandle<JsBackend>&, bool)); // NOLINT
744 MOCK_METHOD1(OnAuthError, void(const GoogleServiceAuthError&)); // NOLINT 605 MOCK_METHOD1(OnAuthError, void(const GoogleServiceAuthError&)); // NOLINT
745 MOCK_METHOD1(OnPassphraseRequired, 606 MOCK_METHOD1(OnPassphraseRequired,
746 void(sync_api::PassphraseRequiredReason)); // NOLINT 607 void(sync_api::PassphraseRequiredReason)); // NOLINT
747 MOCK_METHOD1(OnPassphraseAccepted, void(const std::string&)); // NOLINT 608 MOCK_METHOD1(OnPassphraseAccepted, void(const std::string&)); // NOLINT
748 MOCK_METHOD0(OnStopSyncingPermanently, void()); // NOLINT 609 MOCK_METHOD0(OnStopSyncingPermanently, void()); // NOLINT
749 MOCK_METHOD1(OnUpdatedToken, void(const std::string&)); // NOLINT 610 MOCK_METHOD1(OnUpdatedToken, void(const std::string&)); // NOLINT
(...skipping 11 matching lines...) Expand all
761 MOCK_METHOD1(SetUniqueId, void(const std::string&)); 622 MOCK_METHOD1(SetUniqueId, void(const std::string&));
762 MOCK_METHOD1(SetState, void(const std::string&)); 623 MOCK_METHOD1(SetState, void(const std::string&));
763 MOCK_METHOD2(UpdateCredentials, 624 MOCK_METHOD2(UpdateCredentials,
764 void(const std::string&, const std::string&)); 625 void(const std::string&, const std::string&));
765 MOCK_METHOD1(UpdateEnabledTypes, 626 MOCK_METHOD1(UpdateEnabledTypes,
766 void(const syncable::ModelTypeSet&)); 627 void(const syncable::ModelTypeSet&));
767 MOCK_METHOD1(SendNotification, void(const syncable::ModelTypeSet&)); 628 MOCK_METHOD1(SendNotification, void(const syncable::ModelTypeSet&));
768 }; 629 };
769 630
770 class SyncManagerTest : public testing::Test, 631 class SyncManagerTest : public testing::Test,
771 public ModelSafeWorkerRegistrar { 632 public ModelSafeWorkerRegistrar,
633 public SyncManager::ChangeDelegate {
772 protected: 634 protected:
773 SyncManagerTest() 635 SyncManagerTest()
774 : ui_thread_(BrowserThread::UI, &ui_loop_), 636 : ui_thread_(BrowserThread::UI, &ui_loop_),
775 sync_notifier_mock_(NULL), 637 sync_notifier_mock_(NULL),
776 sync_manager_("Test sync manager"), 638 sync_manager_("Test sync manager"),
777 sync_notifier_observer_(NULL), 639 sync_notifier_observer_(NULL),
778 update_enabled_types_call_count_(0) {} 640 update_enabled_types_call_count_(0) {}
779 641
780 virtual ~SyncManagerTest() { 642 virtual ~SyncManagerTest() {
781 EXPECT_FALSE(sync_notifier_mock_); 643 EXPECT_FALSE(sync_notifier_mock_);
(...skipping 25 matching lines...) Expand all
807 EXPECT_CALL(observer_, OnInitializationComplete(_, _)). 669 EXPECT_CALL(observer_, OnInitializationComplete(_, _)).
808 WillOnce(SaveArg<0>(&js_backend_)); 670 WillOnce(SaveArg<0>(&js_backend_));
809 671
810 EXPECT_FALSE(sync_notifier_observer_); 672 EXPECT_FALSE(sync_notifier_observer_);
811 EXPECT_FALSE(js_backend_.IsInitialized()); 673 EXPECT_FALSE(js_backend_.IsInitialized());
812 674
813 // Takes ownership of |sync_notifier_mock_|. 675 // Takes ownership of |sync_notifier_mock_|.
814 sync_manager_.Init(temp_dir_.path(), 676 sync_manager_.Init(temp_dir_.path(),
815 WeakHandle<JsEventHandler>(), 677 WeakHandle<JsEventHandler>(),
816 "bogus", 0, false, 678 "bogus", 0, false,
817 new TestHttpPostProviderFactory(), this, "bogus", 679 new TestHttpPostProviderFactory(), this, this, "bogus",
818 credentials, sync_notifier_mock_, "", 680 credentials, sync_notifier_mock_, "",
819 true /* setup_for_test_mode */); 681 true /* setup_for_test_mode */);
820 682
821 EXPECT_TRUE(sync_notifier_observer_); 683 EXPECT_TRUE(sync_notifier_observer_);
822 EXPECT_TRUE(js_backend_.IsInitialized()); 684 EXPECT_TRUE(js_backend_.IsInitialized());
823 685
824 EXPECT_EQ(1, update_enabled_types_call_count_); 686 EXPECT_EQ(1, update_enabled_types_call_count_);
825 687
826 ModelSafeRoutingInfo routes; 688 ModelSafeRoutingInfo routes;
827 GetModelSafeRoutingInfo(&routes); 689 GetModelSafeRoutingInfo(&routes);
828 for (ModelSafeRoutingInfo::iterator i = routes.begin(); i != routes.end(); 690 for (ModelSafeRoutingInfo::iterator i = routes.begin(); i != routes.end();
829 ++i) { 691 ++i) {
830 EXPECT_CALL(observer_, OnChangesApplied(i->first, _, _))
831 .RetiresOnSaturation();
832 EXPECT_CALL(observer_, OnChangesComplete(i->first))
833 .RetiresOnSaturation();
834 type_roots_[i->first] = MakeServerNodeForType( 692 type_roots_[i->first] = MakeServerNodeForType(
835 sync_manager_.GetUserShare(), i->first); 693 sync_manager_.GetUserShare(), i->first);
836 } 694 }
837 PumpLoop(); 695 PumpLoop();
838 } 696 }
839 697
840 void TearDown() { 698 void TearDown() {
841 sync_manager_.RemoveObserver(&observer_); 699 sync_manager_.RemoveObserver(&observer_);
842 sync_manager_.Shutdown(); 700 sync_manager_.Shutdown();
843 sync_notifier_mock_ = NULL; 701 sync_notifier_mock_ = NULL;
844 EXPECT_FALSE(sync_notifier_observer_); 702 EXPECT_FALSE(sync_notifier_observer_);
845 PumpLoop(); 703 PumpLoop();
846 } 704 }
847 705
848 // ModelSafeWorkerRegistrar implementation. 706 // ModelSafeWorkerRegistrar implementation.
849 virtual void GetWorkers(std::vector<ModelSafeWorker*>* out) { 707 virtual void GetWorkers(std::vector<ModelSafeWorker*>* out) OVERRIDE {
850 NOTIMPLEMENTED(); 708 NOTIMPLEMENTED();
851 out->clear(); 709 out->clear();
852 } 710 }
853 virtual void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) { 711 virtual void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) OVERRIDE {
854 (*out)[syncable::NIGORI] = browser_sync::GROUP_PASSIVE; 712 (*out)[syncable::NIGORI] = browser_sync::GROUP_PASSIVE;
855 (*out)[syncable::BOOKMARKS] = browser_sync::GROUP_PASSIVE; 713 (*out)[syncable::BOOKMARKS] = browser_sync::GROUP_PASSIVE;
856 (*out)[syncable::THEMES] = browser_sync::GROUP_PASSIVE; 714 (*out)[syncable::THEMES] = browser_sync::GROUP_PASSIVE;
857 (*out)[syncable::SESSIONS] = browser_sync::GROUP_PASSIVE; 715 (*out)[syncable::SESSIONS] = browser_sync::GROUP_PASSIVE;
858 (*out)[syncable::PASSWORDS] = browser_sync::GROUP_PASSIVE; 716 (*out)[syncable::PASSWORDS] = browser_sync::GROUP_PASSIVE;
859 } 717 }
860 718
719 virtual void OnChangesApplied(
720 syncable::ModelType model_type,
721 const BaseTransaction* trans,
722 const ImmutableChangeRecordList& changes) OVERRIDE {}
723
724 virtual void OnChangesComplete(syncable::ModelType model_type) OVERRIDE {}
725
861 // Helper methods. 726 // Helper methods.
862 bool SetUpEncryption() { 727 bool SetUpEncryption() {
863 // Mock the Mac Keychain service. The real Keychain can block on user input. 728 // Mock the Mac Keychain service. The real Keychain can block on user input.
864 #if defined(OS_MACOSX) 729 #if defined(OS_MACOSX)
865 Encryptor::UseMockKeychain(true); 730 Encryptor::UseMockKeychain(true);
866 #endif 731 #endif
867 732
868 // We need to create the nigori node as if it were an applied server update. 733 // We need to create the nigori node as if it were an applied server update.
869 UserShare* share = sync_manager_.GetUserShare(); 734 UserShare* share = sync_manager_.GetUserShare();
870 int64 nigori_id = GetIdForDataType(syncable::NIGORI); 735 int64 nigori_id = GetIdForDataType(syncable::NIGORI);
(...skipping 714 matching lines...) Expand 10 before | Expand all | Expand 10 after
1585 EXPECT_EQ(syncable::BOOKMARKS, node2.GetModelType()); 1450 EXPECT_EQ(syncable::BOOKMARKS, node2.GetModelType());
1586 // We should de-canonicalize the title in GetTitle(), but the title in the 1451 // We should de-canonicalize the title in GetTitle(), but the title in the
1587 // specifics should be stored in the server legal form. 1452 // specifics should be stored in the server legal form.
1588 EXPECT_EQ(raw_title2, node2.GetTitle()); 1453 EXPECT_EQ(raw_title2, node2.GetTitle());
1589 EXPECT_EQ(title2, node2.GetBookmarkSpecifics().title()); 1454 EXPECT_EQ(title2, node2.GetBookmarkSpecifics().title());
1590 EXPECT_EQ(url2, node2.GetBookmarkSpecifics().url()); 1455 EXPECT_EQ(url2, node2.GetBookmarkSpecifics().url());
1591 } 1456 }
1592 } 1457 }
1593 1458
1594 } // namespace browser_sync 1459 } // namespace browser_sync
OLDNEW
« no previous file with comments | « chrome/browser/sync/internal_api/sync_manager.cc ('k') | chrome/browser/sync/js/js_sync_manager_observer.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698