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

Side by Side Diff: chrome/browser/android/data_usage/data_use_ui_tab_model_unittest.cc

Issue 1443683002: Notify DataUseTabModel of navigations and tab closures (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Remove unnecessary thread checks in the factory class Created 5 years 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
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 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/android/data_usage/data_use_ui_tab_model.h" 5 #include "chrome/browser/android/data_usage/data_use_ui_tab_model.h"
6 6
7 #include <stdint.h> 7 #include <stdint.h>
8 8
9 #include "base/run_loop.h"
10 #include "base/single_thread_task_runner.h"
11 #include "chrome/browser/android/data_usage/data_use_tab_model.h"
9 #include "chrome/browser/android/data_usage/data_use_ui_tab_model_factory.h" 12 #include "chrome/browser/android/data_usage/data_use_ui_tab_model_factory.h"
13 #include "chrome/browser/android/data_usage/external_data_use_observer.h"
10 #include "chrome/browser/profiles/profile.h" 14 #include "chrome/browser/profiles/profile.h"
11 #include "chrome/browser/sessions/session_tab_helper.h" 15 #include "chrome/browser/sessions/session_tab_helper.h"
12 #include "chrome/test/base/chrome_render_view_host_test_harness.h" 16 #include "components/data_usage/core/data_use.h"
13 #include "content/public/browser/web_contents.h" 17 #include "components/data_usage/core/data_use_aggregator.h"
14 #include "content/public/test/test_renderer_host.h" 18 #include "components/data_usage/core/data_use_amortizer.h"
15 #include "content/public/test/web_contents_tester.h" 19 #include "components/data_usage/core/data_use_annotator.h"
20 #include "content/public/browser/browser_thread.h"
21 #include "content/public/test/test_browser_thread_bundle.h"
16 #include "testing/gtest/include/gtest/gtest.h" 22 #include "testing/gtest/include/gtest/gtest.h"
17 #include "ui/base/page_transition_types.h" 23 #include "ui/base/page_transition_types.h"
18 #include "url/gurl.h" 24 #include "url/gurl.h"
19 25
20 namespace chrome { 26 namespace chrome {
21 27
22 namespace android { 28 namespace android {
23 29
24 class DataUseUITabModelTest : public ChromeRenderViewHostTestHarness { 30 namespace {
31
32 class TestDataUseTabModel : public DataUseTabModel {
25 public: 33 public:
26 using ChromeRenderViewHostTestHarness::web_contents; 34 TestDataUseTabModel(
27 35 ExternalDataUseObserver* external_data_use_observer,
36 scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner)
37 : DataUseTabModel(external_data_use_observer, ui_task_runner) {}
38
39 ~TestDataUseTabModel() override {}
40
41 using DataUseTabModel::NotifyObserversOfTrackingStarting;
42 using DataUseTabModel::NotifyObserversOfTrackingEnding;
43 };
44
45 class DataUseUITabModelTest : public testing::Test {
46 public:
28 DataUseUITabModel* data_use_ui_tab_model() { 47 DataUseUITabModel* data_use_ui_tab_model() {
29 return chrome::android::DataUseUITabModelFactory::GetForBrowserContext( 48 return data_use_ui_tab_model_.get();
30 Profile::FromBrowserContext(web_contents()->GetBrowserContext())); 49 }
31 } 50
51 ExternalDataUseObserver* external_data_use_observer() const {
52 return external_data_use_observer_.get();
53 }
54
55 TestDataUseTabModel* data_use_tab_model() const {
56 return data_use_tab_model_.get();
57 }
58
59 protected:
60 void SetUp() override {
61 thread_bundle_.reset(new content::TestBrowserThreadBundle(
62 content::TestBrowserThreadBundle::IO_MAINLOOP));
63 io_task_runner_ = content::BrowserThread::GetMessageLoopProxyForThread(
64 content::BrowserThread::IO);
65 data_use_ui_tab_model_.reset(
66 new chrome::android::DataUseUITabModel(io_task_runner_));
67
68 ui_task_runner_ = content::BrowserThread::GetMessageLoopProxyForThread(
69 content::BrowserThread::UI);
70
71 data_use_aggregator_.reset(
72 new data_usage::DataUseAggregator(nullptr, nullptr));
73
74 external_data_use_observer_.reset(new ExternalDataUseObserver(
75 data_use_aggregator_.get(), io_task_runner_, ui_task_runner_));
76 // Wait for |external_data_use_observer_| to create the Java object.
77 base::RunLoop().RunUntilIdle();
78
79 data_use_tab_model_.reset(
80 new TestDataUseTabModel(external_data_use_observer(), ui_task_runner_));
81 data_use_tab_model_->AddObserver(data_use_ui_tab_model()->GetWeakPtr());
82 data_use_ui_tab_model_->SetDataUseTabModel(
83 data_use_tab_model_->GetWeakPtr());
84 }
85
86 private:
87 scoped_ptr<content::TestBrowserThreadBundle> thread_bundle_;
88 scoped_ptr<DataUseUITabModel> data_use_ui_tab_model_;
89 scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_;
90 scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner_;
91 scoped_ptr<data_usage::DataUseAggregator> data_use_aggregator_;
92 scoped_ptr<ExternalDataUseObserver> external_data_use_observer_;
93 scoped_ptr<TestDataUseTabModel> data_use_tab_model_;
32 }; 94 };
33 95
34 // Tests that DataUseTabModel is notified of tab closure and navigation events. 96 } // namespace
97
98 // Tests that DataUseTabModel is notified of tab closure and navigation events,
99 // and DataUseTabModel notifies DataUseUITabModel.
35 TEST_F(DataUseUITabModelTest, ReportTabEventsTest) { 100 TEST_F(DataUseUITabModelTest, ReportTabEventsTest) {
36 data_use_ui_tab_model()->ReportBrowserNavigation( 101 const char kFooLabel[] = "foo_label";
37 GURL("https://www.example.com"), 102 const char kFooPackage[] = "com.foo";
38 ui::PageTransition::PAGE_TRANSITION_TYPED, 103
39 SessionTabHelper::IdForTab(web_contents())); 104 std::vector<std::string> url_regexes;
40 data_use_ui_tab_model()->ReportTabClosure( 105 url_regexes.push_back(
41 SessionTabHelper::IdForTab(web_contents())); 106 "http://www[.]foo[.]com/#q=.*|https://www[.]foo[.]com/#q=.*");
42 // TODO(tbansal): Test that DataUseTabModel is notified. 107 external_data_use_observer()->RegisterURLRegexes(
108 std::vector<std::string>(url_regexes.size(), kFooPackage), url_regexes,
109 std::vector<std::string>(url_regexes.size(), kFooLabel));
110
111 const struct {
112 ui::PageTransition transition_type;
113 std::string expected_label;
114 } tests[] = {
115 {ui::PageTransitionFromInt(ui::PageTransition::PAGE_TRANSITION_LINK |
116 ui::PAGE_TRANSITION_FROM_API),
117 std::string()},
118 {ui::PageTransition::PAGE_TRANSITION_LINK, std::string()},
119 {ui::PageTransition::PAGE_TRANSITION_TYPED, kFooLabel},
120 {ui::PageTransition::PAGE_TRANSITION_AUTO_BOOKMARK, std::string()},
121 {ui::PageTransition::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string()},
122 {ui::PageTransition::PAGE_TRANSITION_GENERATED, kFooLabel},
123 {ui::PageTransition::PAGE_TRANSITION_RELOAD, std::string()},
124 };
125
126 SessionID::id_type foo_tab_id = 100;
127
128 for (size_t i = 0; i < arraysize(tests); ++i) {
129 // Start a new tab.
130 ++foo_tab_id;
131 data_use_ui_tab_model()->ReportBrowserNavigation(
132 GURL("https://www.foo.com/#q=abc"), tests[i].transition_type,
133 foo_tab_id);
134 // Wait for DataUseUITabModel to notify DataUseTabModel, which should notify
135 // DataUseUITabModel back.
136 base::RunLoop().RunUntilIdle();
137
138 // |data_use_ui_tab_model| should receive callback about starting of
139 // tracking of data usage for |foo_tab_id|.
140 EXPECT_EQ(!tests[i].expected_label.empty(),
141 data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id))
142 << i;
143 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id))
144 << i;
145 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(foo_tab_id))
146 << i;
147
148 // DataUse object should be labeled correctly.
149 data_usage::DataUse data_use(GURL("http://foo.com/#q=abc"),
150 base::TimeTicks::Now(),
151 GURL("http://foobar.com"), foo_tab_id,
152 net::NetworkChangeNotifier::CONNECTION_UNKNOWN,
153 std::string(), 1000, 1000);
154 std::string got_label;
155 data_use_tab_model()->GetLabelForDataUse(data_use, &got_label);
156 EXPECT_EQ(tests[i].expected_label, got_label) << i;
157
158 // Report closure of tab.
159 data_use_ui_tab_model()->ReportTabClosure(foo_tab_id);
160 base::RunLoop().RunUntilIdle();
161
162 // DataUse object should not be labeled.
163 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(foo_tab_id));
164 data_use.request_start =
165 base::TimeTicks::Now() + base::TimeDelta::FromMinutes(10);
166 data_use_tab_model()->GetLabelForDataUse(data_use, &got_label);
167 EXPECT_EQ(std::string(), got_label) << i;
168 }
169
170 const SessionID::id_type bar_tab_id = foo_tab_id + 1;
171 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(bar_tab_id));
172 data_use_ui_tab_model()->ReportCustomTabInitialNavigation(
173 bar_tab_id, std::string(), kFooPackage);
174 base::RunLoop().RunUntilIdle();
175
176 // |data_use_ui_tab_model| should receive callback about starting of
177 // tracking of data usage for |bar_tab_id|.
178 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(bar_tab_id));
179 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(bar_tab_id));
180 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(bar_tab_id));
181
182 data_use_ui_tab_model()->ReportTabClosure(bar_tab_id);
183 base::RunLoop().RunUntilIdle();
184 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(bar_tab_id));
43 } 185 }
44 186
45 // Tests if the Entrance/Exit UI state is tracked correctly. 187 // Tests if the Entrance/Exit UI state is tracked correctly.
46 TEST_F(DataUseUITabModelTest, EntranceExitState) { 188 TEST_F(DataUseUITabModelTest, EntranceExitState) {
47 int32_t foo_tab_id = 1; 189 const SessionID::id_type kFooTabId = 1;
48 int32_t bar_tab_id = 2; 190 const SessionID::id_type kBarTabId = 2;
49 int32_t baz_tab_id = 3; 191 const SessionID::id_type kBazTabId = 3;
50 192
51 // ShowEntrance should return true only once. 193 // HasDataUseTrackingStarted should return true only once.
52 data_use_ui_tab_model()->OnTrackingStarted(foo_tab_id); 194 data_use_tab_model()->NotifyObserversOfTrackingStarting(kFooTabId);
53 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id)); 195 base::RunLoop().RunUntilIdle();
54 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id)); 196 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
55 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(foo_tab_id)); 197 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
56 198 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kFooTabId));
57 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(bar_tab_id)); 199
58 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(bar_tab_id)); 200 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kBarTabId));
201 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kBarTabId));
202
203 // HasDataUseTrackingEnded should return true only once.
204 data_use_tab_model()->NotifyObserversOfTrackingEnding(kFooTabId);
205 base::RunLoop().RunUntilIdle();
206 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
207 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kFooTabId));
208 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kFooTabId));
209 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
210
211 // The tab enters the tracking state again.
212 data_use_tab_model()->NotifyObserversOfTrackingStarting(kFooTabId);
213 base::RunLoop().RunUntilIdle();
214 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
215 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
216
217 // The tab exits the tracking state.
218 data_use_tab_model()->NotifyObserversOfTrackingEnding(kFooTabId);
219 base::RunLoop().RunUntilIdle();
220 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
221
222 // The tab enters the tracking state again.
223 data_use_tab_model()->NotifyObserversOfTrackingStarting(kFooTabId);
224 base::RunLoop().RunUntilIdle();
225 data_use_tab_model()->NotifyObserversOfTrackingStarting(kFooTabId);
226 base::RunLoop().RunUntilIdle();
227 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
228 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kFooTabId));
59 229
60 // ShowExit should return true only once. 230 // ShowExit should return true only once.
61 data_use_ui_tab_model()->OnTrackingEnded(foo_tab_id); 231 data_use_tab_model()->NotifyObserversOfTrackingEnding(kBarTabId);
62 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id)); 232 base::RunLoop().RunUntilIdle();
63 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingEnded(foo_tab_id)); 233 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kBarTabId));
64 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(foo_tab_id)); 234 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kBarTabId));
65 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id)); 235 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kBarTabId));
66 236 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kBarTabId));
67 // The tab enters the tracking state again. 237
68 data_use_ui_tab_model()->OnTrackingStarted(foo_tab_id); 238 data_use_ui_tab_model()->ReportTabClosure(kFooTabId);
69 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id)); 239 data_use_ui_tab_model()->ReportTabClosure(kBarTabId);
70 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id));
71
72 // The tab exits the tracking state.
73 data_use_ui_tab_model()->OnTrackingEnded(foo_tab_id);
74 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id));
75
76 // The tab enters the tracking state again.
77 data_use_ui_tab_model()->OnTrackingStarted(foo_tab_id);
78 data_use_ui_tab_model()->OnTrackingStarted(foo_tab_id);
79 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id));
80 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(foo_tab_id));
81
82 // ShowExit should return true only once.
83 data_use_ui_tab_model()->OnTrackingEnded(bar_tab_id);
84 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(bar_tab_id));
85 EXPECT_TRUE(data_use_ui_tab_model()->HasDataUseTrackingEnded(bar_tab_id));
86 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(bar_tab_id));
87 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(bar_tab_id));
88
89 data_use_ui_tab_model()->ReportTabClosure(foo_tab_id);
90 data_use_ui_tab_model()->ReportTabClosure(bar_tab_id);
91 240
92 // HasDataUseTrackingStarted/Ended should return false for closed tabs. 241 // HasDataUseTrackingStarted/Ended should return false for closed tabs.
93 data_use_ui_tab_model()->OnTrackingStarted(baz_tab_id); 242 data_use_tab_model()->NotifyObserversOfTrackingStarting(kBazTabId);
94 data_use_ui_tab_model()->ReportTabClosure(baz_tab_id); 243 base::RunLoop().RunUntilIdle();
95 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(baz_tab_id)); 244 data_use_ui_tab_model()->ReportTabClosure(kBazTabId);
96 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(baz_tab_id)); 245 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingStarted(kBazTabId));
246 EXPECT_FALSE(data_use_ui_tab_model()->HasDataUseTrackingEnded(kBazTabId));
97 } 247 }
98 248
249 // Checks if page transition type is converted correctly.
250 TEST_F(DataUseUITabModelTest, ConvertTransitionType) {
251 DataUseTabModel::TransitionType transition_type;
252 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
253 ui::PageTransition(ui::PAGE_TRANSITION_TYPED), &transition_type));
254 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_NAVIGATION, transition_type);
255 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
256 ui::PageTransition(ui::PAGE_TRANSITION_TYPED | 0xFF00),
257 &transition_type));
258 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_NAVIGATION, transition_type);
259 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
260 ui::PageTransition(ui::PAGE_TRANSITION_TYPED | 0xFFFF00),
261 &transition_type));
262 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_NAVIGATION, transition_type);
263 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
264 ui::PageTransition(ui::PAGE_TRANSITION_TYPED | 0x12FFFF00),
265 &transition_type));
266 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_NAVIGATION, transition_type);
267
268 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
269 ui::PageTransition(ui::PAGE_TRANSITION_AUTO_BOOKMARK), &transition_type));
270 EXPECT_EQ(DataUseTabModel::TRANSITION_BOOKMARK, transition_type);
271 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
272 ui::PageTransition(ui::PAGE_TRANSITION_AUTO_BOOKMARK | 0xFF00),
273 &transition_type));
274 EXPECT_EQ(DataUseTabModel::TRANSITION_BOOKMARK, transition_type);
275 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
276 ui::PageTransition(ui::PAGE_TRANSITION_AUTO_BOOKMARK | 0xFFFF00),
277 &transition_type));
278 EXPECT_EQ(DataUseTabModel::TRANSITION_BOOKMARK, transition_type);
279 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
280 ui::PageTransition(ui::PAGE_TRANSITION_AUTO_BOOKMARK | 0x12FFFF00),
281 &transition_type));
282 EXPECT_EQ(DataUseTabModel::TRANSITION_BOOKMARK, transition_type);
283
284 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
285 ui::PageTransition(ui::PAGE_TRANSITION_GENERATED), &transition_type));
286 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_SEARCH, transition_type);
287 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
288 ui::PageTransition(ui::PAGE_TRANSITION_GENERATED | 0xFF00),
289 &transition_type));
290 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_SEARCH, transition_type);
291 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
292 ui::PageTransition(ui::PAGE_TRANSITION_GENERATED | 0xFFFF00),
293 &transition_type));
294 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_SEARCH, transition_type);
295 EXPECT_TRUE(data_use_ui_tab_model()->ConvertTransitionType(
296 ui::PageTransition(ui::PAGE_TRANSITION_GENERATED | 0x12FFFF00),
297 &transition_type));
298 EXPECT_EQ(DataUseTabModel::TRANSITION_OMNIBOX_SEARCH, transition_type);
299
300 EXPECT_FALSE(data_use_ui_tab_model()->ConvertTransitionType(
301 ui::PageTransition(ui::PAGE_TRANSITION_AUTO_SUBFRAME), &transition_type));
302 EXPECT_FALSE(data_use_ui_tab_model()->ConvertTransitionType(
303 ui::PageTransition(ui::PAGE_TRANSITION_MANUAL_SUBFRAME),
304 &transition_type));
305 EXPECT_FALSE(data_use_ui_tab_model()->ConvertTransitionType(
306 ui::PageTransition(ui::PAGE_TRANSITION_FORM_SUBMIT), &transition_type));
307 }
308
99 } // namespace android 309 } // namespace android
100 310
101 } // namespace chrome 311 } // namespace chrome
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698