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

Side by Side Diff: Source/modules/fetch/CompositeDataConsumerHandleTest.cpp

Issue 1162043007: Introduce CompositeDataConsumerHandle. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Created 5 years, 6 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
« no previous file with comments | « Source/modules/fetch/CompositeDataConsumerHandle.cpp ('k') | Source/modules/modules.gypi » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 // found in the LICENSE file.
4
5 #include "config.h"
6 #include "modules/fetch/CompositeDataConsumerHandle.h"
7
8 #include "platform/Task.h"
9 #include "platform/ThreadSafeFunctional.h"
10 #include "platform/heap/Handle.h"
11 #include "public/platform/Platform.h"
12 #include "public/platform/WebThread.h"
13 #include "public/platform/WebTraceLocation.h"
14 #include "public/platform/WebWaitableEvent.h"
15 #include "wtf/Locker.h"
16
17 #include <gmock/gmock.h>
18 #include <gtest/gtest.h>
19
20 namespace blink {
21
22 namespace {
23
24 using Result = WebDataConsumerHandle::Result;
25 using Flags = WebDataConsumerHandle::Flags;
26 using ::testing::InSequence;
27 using ::testing::Return;
28 using ::testing::StrictMock;
29 using Checkpoint = StrictMock<::testing::MockFunction<void(int)>>;
30
31 const Result kShouldWait = WebDataConsumerHandle::ShouldWait;
32 const Result kDone = WebDataConsumerHandle::Done;
33 const Result kOk = WebDataConsumerHandle::Ok;
34 const Result kUnexpectedError = WebDataConsumerHandle::UnexpectedError;
35 const Flags kNone = WebDataConsumerHandle::FlagNone;
36
37 class NoopClient final : public WebDataConsumerHandle::Client {
38 public:
39 void didGetReadable() override { }
40 };
41
42 class MockReader : public WebDataConsumerHandle::Reader {
43 public:
44 static PassOwnPtr<StrictMock<MockReader>> create() { return adoptPtr(new Str ictMock<MockReader>); }
45
46 MOCK_METHOD4(read, Result(void*, size_t, Flags, size_t*));
47 MOCK_METHOD3(beginRead, Result(const void**, Flags, size_t*));
48 MOCK_METHOD1(endRead, Result(size_t));
49 };
50
51 class MockHandle : public WebDataConsumerHandle {
52 public:
53 static PassOwnPtr<StrictMock<MockHandle>> create() { return adoptPtr(new Str ictMock<MockHandle>); }
54
55 MOCK_METHOD1(obtainReaderInternal, Reader*(Client*));
56 };
57
58 class ThreadingTestBase : public ThreadSafeRefCounted<ThreadingTestBase> {
59 public:
60 class Context : public ThreadSafeRefCounted<Context> {
61 public:
62 static PassRefPtr<Context> create() { return adoptRef(new Context); }
63 void recordAttach(const String& handle)
64 {
65 MutexLocker locker(m_loggingMutex);
66 m_result.append("A reader is attached to " + handle.isolatedCopy() + " on " + threadName() + ".\n");
67 }
68 void recordDetach(const String& handle)
69 {
70 MutexLocker locker(m_loggingMutex);
71 m_result.append("A reader is detached from " + handle.isolatedCopy() + " on " + threadName() + ".\n");
72 }
73
74 const String& result()
75 {
76 MutexLocker locker(m_loggingMutex);
77 return m_result;
78 }
79 WebThread* readingThread() { return m_readingThread.get(); }
80 WebThread* updatingThread() { return m_updatingThread.get(); }
81
82 private:
83 Context()
84 {
85 m_readingThread = adoptPtr(Platform::current()->createThread("readin g thread"));
hiroshige 2015/06/15 07:29:37 nit optional: can this be in the initializer list?
yhirano 2015/06/16 09:03:53 Done.
86 m_updatingThread = adoptPtr(Platform::current()->createThread("updat ing thread"));
87 }
88 String threadName()
hiroshige 2015/06/15 07:29:37 nit optional: currentThreadName() might be a bette
yhirano 2015/06/16 09:03:53 Done.
89 {
90 if (m_readingThread->isCurrentThread())
91 return "the reading thread";
92 if (m_updatingThread->isCurrentThread())
93 return "the updating thread";
94 return "an unknown thread";
95 }
96
97 OwnPtr<WebThread> m_readingThread;
98 OwnPtr<WebThread> m_updatingThread;
99 Mutex m_loggingMutex;
100 String m_result;
101 };
102
103 class ReaderImpl final : public WebDataConsumerHandle::Reader {
104 public:
105 ReaderImpl(const String& name, PassRefPtr<Context> context) : m_name(nam e.isolatedCopy()), m_context(context)
106 {
107 m_context->recordAttach(m_name);
hiroshige 2015/06/15 07:29:37 Please take m_name.isolatedCopy() here (rather tha
yhirano 2015/06/16 09:03:53 Done.
108 }
109 ~ReaderImpl() override { m_context->recordDetach(m_name); }
hiroshige 2015/06/15 07:29:37 ditto.
yhirano 2015/06/16 09:03:53 Done.
110 Result read(void*, size_t, Flags, size_t*) override { return kShouldWait ; }
111 Result beginRead(const void**, Flags, size_t*) override { return kShould Wait; }
112 Result endRead(size_t) override { return kUnexpectedError; }
113
114 private:
115 const String m_name;
116 RefPtr<Context> m_context;
117 };
118 class DataConsumerHandle final : public WebDataConsumerHandle {
119 public:
120 DataConsumerHandle(const String& name, PassRefPtr<Context> context) : m_ name(name.isolatedCopy()), m_context(context) { }
121
122 private:
123 Reader* obtainReaderInternal(Client*) { return new ReaderImpl(m_name, m_ context); }
124 const String m_name;
125 RefPtr<Context> m_context;
126 };
127
128 void resetReader() { m_reader = nullptr; }
129 void signalDone() { m_waitableEvent->signal(); }
130 const String& result() { return m_context->result(); }
131 WebThread* readingThread() { return m_context->readingThread(); }
132 WebThread* updatingThread() { return m_context->updatingThread(); }
133
134 protected:
135 RefPtr<Context> m_context;
136 OwnPtr<CompositeDataConsumerHandle> m_handle;
137 OwnPtr<WebDataConsumerHandle::Reader> m_reader;
138 OwnPtr<WebWaitableEvent> m_waitableEvent;
139 NoopClient m_client;
140 };
141
142 class ThreadingRegistrationTest : public ThreadingTestBase {
143 public:
144 using Self = ThreadingRegistrationTest;
145 void run()
146 {
147 m_context = Context::create();
148 m_waitableEvent = adoptPtr(Platform::current()->createWaitableEvent());
149 m_handle = CompositeDataConsumerHandle::create(adoptPtr(new DataConsumer Handle("handle1", m_context)));
150
151 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::obta inReader, this)));
152
153 m_waitableEvent->wait();
154 }
155
156 private:
157 void obtainReader()
158 {
159 m_reader = m_handle->obtainReader(&m_client);
160 updatingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::upd ate, this)));
161 }
162 void update()
163 {
164 m_handle->update(adoptPtr(new DataConsumerHandle("handle2", m_context))) ;
165 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::rese tReader, this)));
166 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::sign alDone, this)));
167 }
168 };
169
170 class ThreadingRegistrationDeleteHandleTest : public ThreadingTestBase {
171 public:
172 using Self = ThreadingRegistrationDeleteHandleTest;
173 void run()
174 {
175 m_context = Context::create();
176 m_waitableEvent = adoptPtr(Platform::current()->createWaitableEvent());
177 m_handle = CompositeDataConsumerHandle::create(adoptPtr(new DataConsumer Handle("handle1", m_context)));
178
179 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::obta inReader, this)));
180
181 m_waitableEvent->wait();
182 }
183
184 private:
185 void obtainReader()
186 {
187 m_reader = m_handle->obtainReader(&m_client);
188 updatingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::upd ate, this)));
189 }
190 void update()
191 {
192 m_handle->update(adoptPtr(new DataConsumerHandle("handle2", m_context))) ;
193 m_handle = nullptr;
194 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::rese tReader, this)));
195 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::sign alDone, this)));
196 }
197 };
198
199 class ThreadingRegistrationDeleteReaderTest : public ThreadingTestBase {
hiroshige 2015/06/15 07:29:37 Could you add tests for the case where a reader is
yhirano 2015/06/16 09:03:53 Added: ThreadingUpdatingReaderWhileUpdatingTest
200 public:
201 using Self = ThreadingRegistrationDeleteReaderTest;
202 void run()
203 {
204 m_context = Context::create();
205 m_waitableEvent = adoptPtr(Platform::current()->createWaitableEvent());
206 m_handle = CompositeDataConsumerHandle::create(adoptPtr(new DataConsumer Handle("handle1", m_context)));
207
208 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::obta inReader, this)));
209
210 m_waitableEvent->wait();
211 }
212
213 private:
214 void obtainReader()
215 {
216 m_reader = m_handle->obtainReader(&m_client);
217 updatingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::upd ate, this)));
218 }
219 void update()
220 {
221 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::rese tReader, this)));
222 m_handle->update(adoptPtr(new DataConsumerHandle("handle2", m_context))) ;
223 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::rese tReader, this)));
224 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::sign alDone, this)));
225 }
226 };
227
228 class ThreadingRegistrationUpdateTwiceAtOneTimeTest : public ThreadingTestBase {
229 public:
230 using Self = ThreadingRegistrationUpdateTwiceAtOneTimeTest;
231 void run()
232 {
233 m_context = Context::create();
234 m_waitableEvent = adoptPtr(Platform::current()->createWaitableEvent());
235 m_handle = CompositeDataConsumerHandle::create(adoptPtr(new DataConsumer Handle("handle1", m_context)));
236
237 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::obta inReader, this)));
238
239 m_waitableEvent->wait();
240 }
241
242 private:
243 void obtainReader()
244 {
245 m_reader = m_handle->obtainReader(&m_client);
246 updatingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::upd ate, this)));
247 }
248 void update()
249 {
250 m_handle->update(adoptPtr(new DataConsumerHandle("handle2", m_context))) ;
251 m_handle->update(adoptPtr(new DataConsumerHandle("handle3", m_context))) ;
252 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::rese tReader, this)));
253 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::sign alDone, this)));
254 }
255 };
256
257 class ThreadingDoneHandleNotificationTest : public ThreadingTestBase, public Web DataConsumerHandle::Client {
hiroshige 2015/06/15 07:29:37 Could you add a test similar to ThreadingDoneHandl
yhirano 2015/06/16 09:03:52 Added: ThreadingDoneHandleNoNotificationTest
258 public:
259 using Self = ThreadingDoneHandleNotificationTest;
260 void run()
261 {
262 m_context = Context::create();
263 m_waitableEvent = adoptPtr(Platform::current()->createWaitableEvent());
264 m_handle = CompositeDataConsumerHandle::create(CompositeDataConsumerHand le::createDoneHandle());
265
266 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::obta inReader, this)));
267
268 m_waitableEvent->wait();
269 }
270
271 private:
272 void obtainReader()
273 {
274 m_reader = m_handle->obtainReader(this);
275 }
276 void didGetReadable() override
277 {
278 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::rese tReader, this)));
279 readingThread()->postTask(FROM_HERE, new Task(threadSafeBind(&Self::sign alDone, this)));
hiroshige 2015/06/15 07:29:37 These tasks are same-thread tasks so please use bi
yhirano 2015/06/16 09:03:53 Discussed offline. We use threadSafeBind for these
280 }
281 };
282
283 TEST(CompositeDataConsumerHandleTest, CreateWaitingHandle)
284 {
285 char buffer[20];
286 const void* p = nullptr;
287 size_t size = 0;
288 OwnPtr<WebDataConsumerHandle> handle = CompositeDataConsumerHandle::createWa itingHandle();
289 OwnPtr<WebDataConsumerHandle::Reader> reader = handle->obtainReader(nullptr) ;
290
291 EXPECT_EQ(kShouldWait, reader->read(buffer, sizeof(buffer), kNone, &size));
hiroshige 2015/06/15 09:28:57 Should we check |size| for read() and beginRead(),
yhirano 2015/06/16 09:03:52 The behavior is not specified, so we don't have ex
292 EXPECT_EQ(kShouldWait, reader->beginRead(&p, kNone, &size));
293 EXPECT_EQ(kUnexpectedError, reader->endRead(99));
294 }
295
296 TEST(CompositeDataConsumerHandleTest, CreateDoneHandle)
297 {
298 char buffer[20];
299 const void* p = nullptr;
300 size_t size = 0;
301 OwnPtr<WebDataConsumerHandle> handle = CompositeDataConsumerHandle::createDo neHandle();
302 OwnPtr<WebDataConsumerHandle::Reader> reader = handle->obtainReader(nullptr) ;
303
304 EXPECT_EQ(kDone, reader->read(buffer, sizeof(buffer), kNone, &size));
305 EXPECT_EQ(kDone, reader->beginRead(&p, kNone, &size));
306 EXPECT_EQ(kUnexpectedError, reader->endRead(99));
307 (void)kOk;
hiroshige 2015/06/15 07:29:37 What is this line?
yhirano 2015/06/16 09:03:52 Deleted.
308 }
309
310 TEST(CompositeDataConsumerHandleTest, Read)
311 {
312 char buffer[20];
313 size_t size = 0;
314 NoopClient client;
315 Checkpoint checkpoint;
316
317 OwnPtr<MockHandle> handle1 = MockHandle::create();
318 OwnPtr<MockHandle> handle2 = MockHandle::create();
319 OwnPtr<MockReader> reader1 = MockReader::create();
320 OwnPtr<MockReader> reader2 = MockReader::create();
321
322 InSequence s;
323 EXPECT_CALL(checkpoint, Call(0));
324 EXPECT_CALL(*handle1, obtainReaderInternal(&client)).WillOnce(Return(reader1 .get()));
325 EXPECT_CALL(checkpoint, Call(1));
326 EXPECT_CALL(*reader1, read(buffer, sizeof(buffer), kNone, &size)).WillOnce(R eturn(kOk));
327 EXPECT_CALL(checkpoint, Call(2));
328 EXPECT_CALL(*handle2, obtainReaderInternal(&client)).WillOnce(Return(reader2 .get()));
329 EXPECT_CALL(checkpoint, Call(3));
330 EXPECT_CALL(*reader2, read(buffer, sizeof(buffer), kNone, &size)).WillOnce(R eturn(kOk));
331 EXPECT_CALL(checkpoint, Call(4));
332
333 // They are adopted by |obtainReader|.
334 (void)reader1.leakPtr();
335 (void)reader2.leakPtr();
336
337 OwnPtr<CompositeDataConsumerHandle> handle = CompositeDataConsumerHandle::cr eate(handle1.release());
338 checkpoint.Call(0);
339 OwnPtr<CompositeDataConsumerHandle::Reader> reader = handle->obtainReader(&c lient);
340 checkpoint.Call(1);
341 EXPECT_EQ(kOk, reader->read(buffer, sizeof(buffer), kNone, &size));
342 checkpoint.Call(2);
343 handle->update(handle2.release());
344 checkpoint.Call(3);
345 EXPECT_EQ(kOk, reader->read(buffer, sizeof(buffer), kNone, &size));
346 checkpoint.Call(4);
347 }
348
349 TEST(CompositeDataConsumerHandleTest, TwoPhaseRead)
350 {
351 const void* p = nullptr;
352 size_t size = 0;
353 Checkpoint checkpoint;
354
355 OwnPtr<MockHandle> handle1 = MockHandle::create();
356 OwnPtr<MockHandle> handle2 = MockHandle::create();
357 OwnPtr<MockReader> reader1 = MockReader::create();
358 OwnPtr<MockReader> reader2 = MockReader::create();
359
360 InSequence s;
361 EXPECT_CALL(checkpoint, Call(0));
362 EXPECT_CALL(*handle1, obtainReaderInternal(nullptr)).WillOnce(Return(reader1 .get()));
363 EXPECT_CALL(checkpoint, Call(1));
364 EXPECT_CALL(*reader1, beginRead(&p, kNone, &size)).WillOnce(Return(kOk));
365 EXPECT_CALL(checkpoint, Call(2));
366 EXPECT_CALL(*reader1, endRead(0)).WillOnce(Return(kOk));
367 EXPECT_CALL(checkpoint, Call(3));
368 EXPECT_CALL(*handle2, obtainReaderInternal(nullptr)).WillOnce(Return(reader2 .get()));
369 EXPECT_CALL(checkpoint, Call(4));
370 EXPECT_CALL(*reader2, beginRead(&p, kNone, &size)).WillOnce(Return(kOk));
371 EXPECT_CALL(checkpoint, Call(5));
372 EXPECT_CALL(*reader2, endRead(0)).WillOnce(Return(kOk));
373 EXPECT_CALL(checkpoint, Call(6));
374
375 // They are adopted by |obtainReader|.
376 (void)reader1.leakPtr();
377 (void)reader2.leakPtr();
378
379 OwnPtr<CompositeDataConsumerHandle> handle = CompositeDataConsumerHandle::cr eate(handle1.release());
380 checkpoint.Call(0);
381 OwnPtr<CompositeDataConsumerHandle::Reader> reader = handle->obtainReader(nu llptr);
382 checkpoint.Call(1);
383 EXPECT_EQ(kOk, reader->beginRead(&p, kNone, &size));
384 checkpoint.Call(2);
385 EXPECT_EQ(kOk, reader->endRead(0));
386 checkpoint.Call(3);
387 handle->update(handle2.release());
388 checkpoint.Call(4);
389 EXPECT_EQ(kOk, reader->beginRead(&p, kNone, &size));
390 checkpoint.Call(5);
391 EXPECT_EQ(kOk, reader->endRead(0));
392 checkpoint.Call(6);
393 }
394
395 TEST(CompositeDataConsumerHandleTest, HangingTwoPhaseRead)
hiroshige 2015/06/15 07:29:37 Could you add a test similar to HangingTwoPhaseRea
yhirano 2015/06/16 09:03:53 Done.
396 {
397 const void* p = nullptr;
398 size_t size = 0;
399 Checkpoint checkpoint;
400
401 OwnPtr<MockHandle> handle1 = MockHandle::create();
402 OwnPtr<MockHandle> handle2 = MockHandle::create();
403 OwnPtr<MockReader> reader1 = MockReader::create();
404 OwnPtr<MockReader> reader2 = MockReader::create();
405
406 InSequence s;
407 EXPECT_CALL(checkpoint, Call(0));
408 EXPECT_CALL(*handle1, obtainReaderInternal(nullptr)).WillOnce(Return(reader1 .get()));
409 EXPECT_CALL(checkpoint, Call(1));
410 EXPECT_CALL(*reader1, beginRead(&p, kNone, &size)).WillOnce(Return(kOk));
411 EXPECT_CALL(checkpoint, Call(2));
412 EXPECT_CALL(checkpoint, Call(3));
413 EXPECT_CALL(*reader1, endRead(0)).WillOnce(Return(kOk));
414 EXPECT_CALL(*handle2, obtainReaderInternal(nullptr)).WillOnce(Return(reader2 .get()));
415 EXPECT_CALL(checkpoint, Call(4));
416 EXPECT_CALL(*reader2, beginRead(&p, kNone, &size)).WillOnce(Return(kOk));
417 EXPECT_CALL(checkpoint, Call(5));
418 EXPECT_CALL(*reader2, endRead(0)).WillOnce(Return(kOk));
419 EXPECT_CALL(checkpoint, Call(6));
420
421 // They are adopted by |obtainReader|.
422 (void)reader1.leakPtr();
423 (void)reader2.leakPtr();
424
425 OwnPtr<CompositeDataConsumerHandle> handle = CompositeDataConsumerHandle::cr eate(handle1.release());
426 checkpoint.Call(0);
427 OwnPtr<CompositeDataConsumerHandle::Reader> reader = handle->obtainReader(nu llptr);
428 checkpoint.Call(1);
429 EXPECT_EQ(kOk, reader->beginRead(&p, kNone, &size));
430 checkpoint.Call(2);
431 handle->update(handle2.release());
432 checkpoint.Call(3);
433 EXPECT_EQ(kOk, reader->endRead(0));
434 checkpoint.Call(4);
435 EXPECT_EQ(kOk, reader->beginRead(&p, kNone, &size));
436 checkpoint.Call(5);
437 EXPECT_EQ(kOk, reader->endRead(0));
438 checkpoint.Call(6);
439
440 }
441
442 TEST(CompositeDataConsumerHandleTest, RegisterClientOnDifferentThreads)
443 {
444 ThreadingRegistrationTest test;
445 test.run();
446
447 EXPECT_EQ(
448 "A reader is attached to handle1 on the reading thread.\n"
449 "A reader is detached from handle1 on the reading thread.\n"
450 "A reader is attached to handle2 on the reading thread.\n"
451 "A reader is detached from handle2 on the reading thread.\n",
452 test.result());
453 }
454
455 TEST(CompositeDataConsumerHandleTest, DeleteHandleWhileUpdating)
456 {
457 ThreadingRegistrationDeleteHandleTest test;
458 test.run();
459
460 EXPECT_EQ(
461 "A reader is attached to handle1 on the reading thread.\n"
462 "A reader is detached from handle1 on the reading thread.\n"
463 "A reader is attached to handle2 on the reading thread.\n"
464 "A reader is detached from handle2 on the reading thread.\n",
465 test.result());
466 }
467
468 TEST(CompositeDataConsumerHandleTest, DeleteReaderWhileUpdating)
469 {
470 ThreadingRegistrationDeleteReaderTest test;
471 test.run();
472
473 EXPECT_EQ(
474 "A reader is attached to handle1 on the reading thread.\n"
475 "A reader is detached from handle1 on the reading thread.\n",
476 test.result());
477 }
478
479 TEST(CompositeDataConsumerHandleTest, UpdateTwiceAtOnce)
480 {
481 ThreadingRegistrationUpdateTwiceAtOneTimeTest test;
482 test.run();
483
484 EXPECT_EQ(
485 "A reader is attached to handle1 on the reading thread.\n"
486 "A reader is detached from handle1 on the reading thread.\n"
487 "A reader is attached to handle3 on the reading thread.\n"
488 "A reader is detached from handle3 on the reading thread.\n",
489 test.result());
490 }
491
492 TEST(CompositeDataConsumerHandleTest, DoneHandleNotification)
493 {
494 ThreadingDoneHandleNotificationTest test;
495 // Test this function returns.
496 test.run();
497 }
498
499 } // namespace
500
501 } // namespace blink
OLDNEW
« no previous file with comments | « Source/modules/fetch/CompositeDataConsumerHandle.cpp ('k') | Source/modules/modules.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698