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

Side by Side Diff: Source/modules/serviceworkers/CacheTest.cpp

Issue 694083002: Service Worker Cache: Simplify exception/rejection processing (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Created 6 years, 1 month 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
« no previous file with comments | « Source/modules/serviceworkers/Cache.idl ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 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 "config.h" 5 #include "config.h"
6 #include "modules/serviceworkers/Cache.h" 6 #include "modules/serviceworkers/Cache.h"
7 7
8 #include "bindings/core/v8/ExceptionState.h" 8 #include "bindings/core/v8/ExceptionState.h"
9 #include "bindings/core/v8/ScriptFunction.h" 9 #include "bindings/core/v8/ScriptFunction.h"
10 #include "bindings/core/v8/ScriptPromise.h" 10 #include "bindings/core/v8/ScriptPromise.h"
(...skipping 244 matching lines...) Expand 10 before | Expand all | Expand 10 after
255 255
256 // Lifetime is that of the text fixture. 256 // Lifetime is that of the text fixture.
257 OwnPtr<DummyPageHolder> m_page; 257 OwnPtr<DummyPageHolder> m_page;
258 258
259 // Lifetime is per test instance. 259 // Lifetime is per test instance.
260 OwnPtr<ScriptState::Scope> m_scriptScope; 260 OwnPtr<ScriptState::Scope> m_scriptScope;
261 }; 261 };
262 262
263 TEST_F(ServiceWorkerCacheTest, Basics) 263 TEST_F(ServiceWorkerCacheTest, Basics)
264 { 264 {
265 NonThrowableExceptionState exceptionState;
jsbell 2014/10/31 23:06:19 Should I make this a member of the fixture instead
jkarlin 2014/11/03 18:51:04 I'm in favor of making it a private member as it's
266
265 ErrorWebCacheForTests* testCache; 267 ErrorWebCacheForTests* testCache;
266 Cache* cache = Cache::create(testCache = new NotImplementedErrorCache()); 268 Cache* cache = Cache::create(testCache = new NotImplementedErrorCache());
267 ASSERT(cache); 269 ASSERT(cache);
268 270
269 const String url = "http://www.cachetest.org/"; 271 const String url = "http://www.cachetest.org/";
270 272
271 CacheQueryOptions options; 273 CacheQueryOptions options;
272 ScriptPromise matchPromise = cache->match(scriptState(), url, options); 274 ScriptPromise matchPromise = cache->match(scriptState(), url, options, excep tionState);
273 EXPECT_EQ(kNotImplementedString, getRejectString(matchPromise)); 275 EXPECT_EQ(kNotImplementedString, getRejectString(matchPromise));
274 276
275 cache = Cache::create(testCache = new ErrorWebCacheForTests(WebServiceWorker CacheErrorNotFound)); 277 cache = Cache::create(testCache = new ErrorWebCacheForTests(WebServiceWorker CacheErrorNotFound));
276 matchPromise = cache->match(scriptState(), url, options); 278 matchPromise = cache->match(scriptState(), url, options, exceptionState);
277 ScriptValue scriptValue = getResolveValue(matchPromise); 279 ScriptValue scriptValue = getResolveValue(matchPromise);
278 EXPECT_TRUE(scriptValue.isUndefined()); 280 EXPECT_TRUE(scriptValue.isUndefined());
279 281
280 cache = Cache::create(testCache = new ErrorWebCacheForTests(WebServiceWorker CacheErrorExists)); 282 cache = Cache::create(testCache = new ErrorWebCacheForTests(WebServiceWorker CacheErrorExists));
281 matchPromise = cache->match(scriptState(), url, options); 283 matchPromise = cache->match(scriptState(), url, options, exceptionState);
282 EXPECT_EQ("InvalidAccessError: Entry already exists.", getRejectString(match Promise)); 284 EXPECT_EQ("InvalidAccessError: Entry already exists.", getRejectString(match Promise));
283 } 285 }
284 286
285 // Tests that arguments are faithfully passed on calls to Cache methods, except for methods which use batch operations, 287 // Tests that arguments are faithfully passed on calls to Cache methods, except for methods which use batch operations,
286 // which are tested later. 288 // which are tested later.
287 TEST_F(ServiceWorkerCacheTest, BasicArguments) 289 TEST_F(ServiceWorkerCacheTest, BasicArguments)
288 { 290 {
291 NonThrowableExceptionState exceptionState;
292
289 ErrorWebCacheForTests* testCache; 293 ErrorWebCacheForTests* testCache;
290 Cache* cache = Cache::create(testCache = new NotImplementedErrorCache()); 294 Cache* cache = Cache::create(testCache = new NotImplementedErrorCache());
291 ASSERT(cache); 295 ASSERT(cache);
292 296
293 const String url = "http://www.cache.arguments.test/"; 297 const String url = "http://www.cache.arguments.test/";
294 testCache->setExpectedUrl(&url); 298 testCache->setExpectedUrl(&url);
295 299
296 WebServiceWorkerCache::QueryParams expectedQueryParams; 300 WebServiceWorkerCache::QueryParams expectedQueryParams;
297 expectedQueryParams.ignoreVary = true; 301 expectedQueryParams.ignoreVary = true;
298 expectedQueryParams.cacheName = "this is a cache name"; 302 expectedQueryParams.cacheName = "this is a cache name";
299 testCache->setExpectedQueryParams(&expectedQueryParams); 303 testCache->setExpectedQueryParams(&expectedQueryParams);
300 304
301 CacheQueryOptions options; 305 CacheQueryOptions options;
302 options.setIgnoreVary(1); 306 options.setIgnoreVary(1);
303 options.setCacheName(expectedQueryParams.cacheName); 307 options.setCacheName(expectedQueryParams.cacheName);
304 308
305 Request* request = newRequestFromUrl(url); 309 Request* request = newRequestFromUrl(url);
306 ASSERT(request); 310 ASSERT(request);
307 ScriptPromise matchResult = cache->match(scriptState(), request, options); 311 ScriptPromise matchResult = cache->match(scriptState(), request, options);
308 EXPECT_EQ("dispatchMatch", testCache->getAndClearLastErrorWebCacheMethodCall ed()); 312 EXPECT_EQ("dispatchMatch", testCache->getAndClearLastErrorWebCacheMethodCall ed());
309 EXPECT_EQ(kNotImplementedString, getRejectString(matchResult)); 313 EXPECT_EQ(kNotImplementedString, getRejectString(matchResult));
310 314
311 ScriptPromise stringMatchResult = cache->match(scriptState(), url, options); 315 ScriptPromise stringMatchResult = cache->match(scriptState(), url, options, exceptionState);
312 EXPECT_EQ("dispatchMatch", testCache->getAndClearLastErrorWebCacheMethodCall ed()); 316 EXPECT_EQ("dispatchMatch", testCache->getAndClearLastErrorWebCacheMethodCall ed());
313 EXPECT_EQ(kNotImplementedString, getRejectString(stringMatchResult)); 317 EXPECT_EQ(kNotImplementedString, getRejectString(stringMatchResult));
314 318
315 request = newRequestFromUrl(url); 319 request = newRequestFromUrl(url);
316 ASSERT(request); 320 ASSERT(request);
317 ScriptPromise matchAllResult = cache->matchAll(scriptState(), request, optio ns); 321 ScriptPromise matchAllResult = cache->matchAll(scriptState(), request, optio ns);
318 EXPECT_EQ("dispatchMatchAll", testCache->getAndClearLastErrorWebCacheMethodC alled()); 322 EXPECT_EQ("dispatchMatchAll", testCache->getAndClearLastErrorWebCacheMethodC alled());
319 EXPECT_EQ(kNotImplementedString, getRejectString(matchAllResult)); 323 EXPECT_EQ(kNotImplementedString, getRejectString(matchAllResult));
320 324
321 ScriptPromise stringMatchAllResult = cache->matchAll(scriptState(), url, opt ions); 325 ScriptPromise stringMatchAllResult = cache->matchAll(scriptState(), url, opt ions, exceptionState);
322 EXPECT_EQ("dispatchMatchAll", testCache->getAndClearLastErrorWebCacheMethodC alled()); 326 EXPECT_EQ("dispatchMatchAll", testCache->getAndClearLastErrorWebCacheMethodC alled());
323 EXPECT_EQ(kNotImplementedString, getRejectString(stringMatchAllResult)); 327 EXPECT_EQ(kNotImplementedString, getRejectString(stringMatchAllResult));
324 328
325 ScriptPromise keysResult1 = cache->keys(scriptState()); 329 ScriptPromise keysResult1 = cache->keys(scriptState());
326 EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalle d()); 330 EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalle d());
327 EXPECT_EQ(kNotImplementedString, getRejectString(keysResult1)); 331 EXPECT_EQ(kNotImplementedString, getRejectString(keysResult1));
328 332
329 request = newRequestFromUrl(url); 333 request = newRequestFromUrl(url);
330 ASSERT(request); 334 ASSERT(request);
331 ScriptPromise keysResult2 = cache->keys(scriptState(), request, options); 335 ScriptPromise keysResult2 = cache->keys(scriptState(), request, options);
332 EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalle d()); 336 EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalle d());
333 EXPECT_EQ(kNotImplementedString, getRejectString(keysResult2)); 337 EXPECT_EQ(kNotImplementedString, getRejectString(keysResult2));
334 338
335 ScriptPromise stringKeysResult2 = cache->keys(scriptState(), url, options); 339 ScriptPromise stringKeysResult2 = cache->keys(scriptState(), url, options, e xceptionState);
336 EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalle d()); 340 EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalle d());
337 EXPECT_EQ(kNotImplementedString, getRejectString(stringKeysResult2)); 341 EXPECT_EQ(kNotImplementedString, getRejectString(stringKeysResult2));
338 } 342 }
339 343
340 // Tests that arguments are faithfully passed to API calls that degrade to batch operations. 344 // Tests that arguments are faithfully passed to API calls that degrade to batch operations.
341 TEST_F(ServiceWorkerCacheTest, BatchOperationArguments) 345 TEST_F(ServiceWorkerCacheTest, BatchOperationArguments)
342 { 346 {
347 NonThrowableExceptionState exceptionState;
348
343 ErrorWebCacheForTests* testCache; 349 ErrorWebCacheForTests* testCache;
344 Cache* cache = Cache::create(testCache = new NotImplementedErrorCache()); 350 Cache* cache = Cache::create(testCache = new NotImplementedErrorCache());
345 ASSERT(cache); 351 ASSERT(cache);
346 352
347 WebServiceWorkerCache::QueryParams expectedQueryParams; 353 WebServiceWorkerCache::QueryParams expectedQueryParams;
348 expectedQueryParams.prefixMatch = true; 354 expectedQueryParams.prefixMatch = true;
349 expectedQueryParams.cacheName = "this is another cache name"; 355 expectedQueryParams.cacheName = "this is another cache name";
350 testCache->setExpectedQueryParams(&expectedQueryParams); 356 testCache->setExpectedQueryParams(&expectedQueryParams);
351 357
352 CacheQueryOptions options; 358 CacheQueryOptions options;
(...skipping 15 matching lines...) Expand all
368 request->populateWebServiceWorkerRequest(deleteOperation.request); 374 request->populateWebServiceWorkerRequest(deleteOperation.request);
369 deleteOperation.matchParams = expectedQueryParams; 375 deleteOperation.matchParams = expectedQueryParams;
370 expectedDeleteOperations[0] = deleteOperation; 376 expectedDeleteOperations[0] = deleteOperation;
371 } 377 }
372 testCache->setExpectedBatchOperations(&expectedDeleteOperations); 378 testCache->setExpectedBatchOperations(&expectedDeleteOperations);
373 379
374 ScriptPromise deleteResult = cache->deleteFunction(scriptState(), request, o ptions); 380 ScriptPromise deleteResult = cache->deleteFunction(scriptState(), request, o ptions);
375 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed()); 381 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed());
376 EXPECT_EQ(kNotImplementedString, getRejectString(deleteResult)); 382 EXPECT_EQ(kNotImplementedString, getRejectString(deleteResult));
377 383
378 ScriptPromise stringDeleteResult = cache->deleteFunction(scriptState(), url, options); 384 ScriptPromise stringDeleteResult = cache->deleteFunction(scriptState(), url, options, exceptionState);
379 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed()); 385 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed());
380 EXPECT_EQ(kNotImplementedString, getRejectString(stringDeleteResult)); 386 EXPECT_EQ(kNotImplementedString, getRejectString(stringDeleteResult));
381 387
382 WebVector<WebServiceWorkerCache::BatchOperation> expectedPutOperations(size_ t(1)); 388 WebVector<WebServiceWorkerCache::BatchOperation> expectedPutOperations(size_ t(1));
383 { 389 {
384 WebServiceWorkerCache::BatchOperation putOperation; 390 WebServiceWorkerCache::BatchOperation putOperation;
385 putOperation.operationType = WebServiceWorkerCache::OperationTypePut; 391 putOperation.operationType = WebServiceWorkerCache::OperationTypePut;
386 request->populateWebServiceWorkerRequest(putOperation.request); 392 request->populateWebServiceWorkerRequest(putOperation.request);
387 response->populateWebServiceWorkerResponse(putOperation.response); 393 response->populateWebServiceWorkerResponse(putOperation.response);
388 expectedPutOperations[0] = putOperation; 394 expectedPutOperations[0] = putOperation;
389 } 395 }
390 testCache->setExpectedBatchOperations(&expectedPutOperations); 396 testCache->setExpectedBatchOperations(&expectedPutOperations);
391 397
392 request = newRequestFromUrl(url); 398 request = newRequestFromUrl(url);
393 ASSERT(request); 399 ASSERT(request);
394 ScriptPromise putResult = cache->put(scriptState(), request, response); 400 ScriptPromise putResult = cache->put(scriptState(), request, response);
395 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed()); 401 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed());
396 EXPECT_EQ(kNotImplementedString, getRejectString(putResult)); 402 EXPECT_EQ(kNotImplementedString, getRejectString(putResult));
397 403
398 ScriptPromise stringPutResult = cache->put(scriptState(), url, response); 404 ScriptPromise stringPutResult = cache->put(scriptState(), url, response, exc eptionState);
399 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed()); 405 EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCall ed());
400 EXPECT_EQ(kNotImplementedString, getRejectString(stringPutResult)); 406 EXPECT_EQ(kNotImplementedString, getRejectString(stringPutResult));
401 407
402 // FIXME: test add & addAll. 408 // FIXME: test add & addAll.
403 } 409 }
404 410
405 class MatchTestCache : public NotImplementedErrorCache { 411 class MatchTestCache : public NotImplementedErrorCache {
406 public: 412 public:
407 MatchTestCache(WebServiceWorkerResponse& response) 413 MatchTestCache(WebServiceWorkerResponse& response)
408 : m_response(response) { } 414 : m_response(response) { }
409 415
410 // From WebServiceWorkerCache: 416 // From WebServiceWorkerCache:
411 virtual void dispatchMatch(CacheMatchCallbacks* callbacks, const WebServiceW orkerRequest& webRequest, const QueryParams& queryParams) override 417 virtual void dispatchMatch(CacheMatchCallbacks* callbacks, const WebServiceW orkerRequest& webRequest, const QueryParams& queryParams) override
412 { 418 {
413 OwnPtr<CacheMatchCallbacks> ownedCallbacks(adoptPtr(callbacks)); 419 OwnPtr<CacheMatchCallbacks> ownedCallbacks(adoptPtr(callbacks));
414 return callbacks->onSuccess(&m_response); 420 return callbacks->onSuccess(&m_response);
415 } 421 }
416 422
417 private: 423 private:
418 WebServiceWorkerResponse& m_response; 424 WebServiceWorkerResponse& m_response;
419 }; 425 };
420 426
421 TEST_F(ServiceWorkerCacheTest, MatchResponseTest) 427 TEST_F(ServiceWorkerCacheTest, MatchResponseTest)
422 { 428 {
429 NonThrowableExceptionState exceptionState;
430
423 const String requestUrl = "http://request.url/"; 431 const String requestUrl = "http://request.url/";
424 const String responseUrl = "http://match.response.test/"; 432 const String responseUrl = "http://match.response.test/";
425 433
426 WebServiceWorkerResponse webResponse; 434 WebServiceWorkerResponse webResponse;
427 webResponse.setURL(KURL(ParsedURLString, responseUrl)); 435 webResponse.setURL(KURL(ParsedURLString, responseUrl));
428 webResponse.setResponseType(WebServiceWorkerResponseTypeDefault); 436 webResponse.setResponseType(WebServiceWorkerResponseTypeDefault);
429 437
430 Cache* cache = Cache::create(new MatchTestCache(webResponse)); 438 Cache* cache = Cache::create(new MatchTestCache(webResponse));
431 CacheQueryOptions options; 439 CacheQueryOptions options;
432 440
433 ScriptPromise result = cache->match(scriptState(), requestUrl, options); 441 ScriptPromise result = cache->match(scriptState(), requestUrl, options, exce ptionState);
434 ScriptValue scriptValue = getResolveValue(result); 442 ScriptValue scriptValue = getResolveValue(result);
435 Response* response = V8Response::toImplWithTypeCheck(isolate(), scriptValue. v8Value()); 443 Response* response = V8Response::toImplWithTypeCheck(isolate(), scriptValue. v8Value());
436 ASSERT_TRUE(response); 444 ASSERT_TRUE(response);
437 EXPECT_EQ(responseUrl, response->url()); 445 EXPECT_EQ(responseUrl, response->url());
438 } 446 }
439 447
440 class KeysTestCache : public NotImplementedErrorCache { 448 class KeysTestCache : public NotImplementedErrorCache {
441 public: 449 public:
442 KeysTestCache(WebVector<WebServiceWorkerRequest>& requests) 450 KeysTestCache(WebVector<WebServiceWorkerRequest>& requests)
443 : m_requests(requests) { } 451 : m_requests(requests) { }
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
497 OwnPtr<CacheWithResponsesCallbacks> ownedCallbacks(adoptPtr(callbacks)); 505 OwnPtr<CacheWithResponsesCallbacks> ownedCallbacks(adoptPtr(callbacks));
498 return callbacks->onSuccess(&m_responses); 506 return callbacks->onSuccess(&m_responses);
499 } 507 }
500 508
501 private: 509 private:
502 WebVector<WebServiceWorkerResponse>& m_responses; 510 WebVector<WebServiceWorkerResponse>& m_responses;
503 }; 511 };
504 512
505 TEST_F(ServiceWorkerCacheTest, MatchAllAndBatchResponseTest) 513 TEST_F(ServiceWorkerCacheTest, MatchAllAndBatchResponseTest)
506 { 514 {
515 NonThrowableExceptionState exceptionState;
516
507 const String url1 = "http://first.response/"; 517 const String url1 = "http://first.response/";
508 const String url2 = "http://second.response/"; 518 const String url2 = "http://second.response/";
509 519
510 Vector<String> expectedUrls(size_t(2)); 520 Vector<String> expectedUrls(size_t(2));
511 expectedUrls[0] = url1; 521 expectedUrls[0] = url1;
512 expectedUrls[1] = url2; 522 expectedUrls[1] = url2;
513 523
514 WebVector<WebServiceWorkerResponse> webResponses(size_t(2)); 524 WebVector<WebServiceWorkerResponse> webResponses(size_t(2));
515 webResponses[0].setURL(KURL(ParsedURLString, url1)); 525 webResponses[0].setURL(KURL(ParsedURLString, url1));
516 webResponses[0].setResponseType(WebServiceWorkerResponseTypeDefault); 526 webResponses[0].setResponseType(WebServiceWorkerResponseTypeDefault);
517 webResponses[1].setURL(KURL(ParsedURLString, url2)); 527 webResponses[1].setURL(KURL(ParsedURLString, url2));
518 webResponses[1].setResponseType(WebServiceWorkerResponseTypeDefault); 528 webResponses[1].setResponseType(WebServiceWorkerResponseTypeDefault);
519 529
520 Cache* cache = Cache::create(new MatchAllAndBatchTestCache(webResponses)); 530 Cache* cache = Cache::create(new MatchAllAndBatchTestCache(webResponses));
521 531
522 CacheQueryOptions options; 532 CacheQueryOptions options;
523 ScriptPromise result = cache->matchAll(scriptState(), "http://some.url/", op tions); 533 ScriptPromise result = cache->matchAll(scriptState(), "http://some.url/", op tions, exceptionState);
524 ScriptValue scriptValue = getResolveValue(result); 534 ScriptValue scriptValue = getResolveValue(result);
525 535
526 NonThrowableExceptionState exceptionState;
527 Vector<v8::Handle<v8::Value> > responses = toImplArray<v8::Handle<v8::Value> >(scriptValue.v8Value(), 0, isolate(), exceptionState); 536 Vector<v8::Handle<v8::Value> > responses = toImplArray<v8::Handle<v8::Value> >(scriptValue.v8Value(), 0, isolate(), exceptionState);
528 EXPECT_EQ(expectedUrls.size(), responses.size()); 537 EXPECT_EQ(expectedUrls.size(), responses.size());
529 for (int i = 0, minsize = std::min(expectedUrls.size(), responses.size()); i < minsize; ++i) { 538 for (int i = 0, minsize = std::min(expectedUrls.size(), responses.size()); i < minsize; ++i) {
530 Response* response = V8Response::toImplWithTypeCheck(isolate(), response s[i]); 539 Response* response = V8Response::toImplWithTypeCheck(isolate(), response s[i]);
531 EXPECT_TRUE(response); 540 EXPECT_TRUE(response);
532 if (response) 541 if (response)
533 EXPECT_EQ(expectedUrls[i], response->url()); 542 EXPECT_EQ(expectedUrls[i], response->url());
534 } 543 }
535 544
536 result = cache->deleteFunction(scriptState(), "http://some.url/", options); 545 result = cache->deleteFunction(scriptState(), "http://some.url/", options, e xceptionState);
537 scriptValue = getResolveValue(result); 546 scriptValue = getResolveValue(result);
538 EXPECT_TRUE(scriptValue.v8Value()->IsBoolean()); 547 EXPECT_TRUE(scriptValue.v8Value()->IsBoolean());
539 EXPECT_EQ(true, scriptValue.v8Value()->BooleanValue()); 548 EXPECT_EQ(true, scriptValue.v8Value()->BooleanValue());
540 } 549 }
541 550
542 } // namespace 551 } // namespace
543 } // namespace blink 552 } // namespace blink
OLDNEW
« no previous file with comments | « Source/modules/serviceworkers/Cache.idl ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698