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

Side by Side Diff: utils/testrunner/run_pipeline.dart

Issue 14247033: Updated testrunner: (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 8 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
« no previous file with comments | « utils/testrunner/pubspec.yaml ('k') | utils/testrunner/standard_test_runner.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 /** The default pipeline code for running a test file. */ 5 /** The default pipeline code for running a test file. */
6 library pipeline; 6 library pipeline;
7 import 'dart:async';
8 import 'dart:io';
7 import 'dart:isolate'; 9 import 'dart:isolate';
8 import 'dart:io'; 10 import 'dart:math';
9 part 'pipeline_utils.dart'; 11 part 'pipeline_utils.dart';
10 12
11 /** 13 /**
12 * The configuration passed in to the pipeline runner; this essentially 14 * The configuration passed in to the pipeline runner; this essentially
13 * contains all the command line arguments passded to testrunner plus some 15 * contains all the command line arguments passded to testrunner plus some
14 * synthesized ones. 16 * synthesized ones.
15 */ 17 */
16 Map config; 18 Map config;
17 19
18 /** Paths to the various generated temporary files. */ 20 /** Paths to the various generated temporary files. */
(...skipping 23 matching lines...) Expand all
42 44
43 /** Root directory for static files used by HTTP server. */ 45 /** Root directory for static files used by HTTP server. */
44 String serverRoot; 46 String serverRoot;
45 47
46 /** Path of the HTTP server script. */ 48 /** Path of the HTTP server script. */
47 String serverPath; 49 String serverPath;
48 50
49 /** Number of attempts we will make to start the HTTP server. */ 51 /** Number of attempts we will make to start the HTTP server. */
50 const int MAX_SERVER_TRIES = 10; 52 const int MAX_SERVER_TRIES = 10;
51 53
54 /** Pipeline output. */
55 List stdout;
56
57 /** Pipeline errors. */
58 List stderr;
59
60 /** Directory where test wrappers are created. */
61 String tmpDir;
62
52 void main() { 63 void main() {
53 port.receive((cfg, replyPort) { 64 port.receive((cfg, replyPort) {
54 config = cfg; 65 config = cfg;
66 stdout = new List();
67 stderr = new List();
55 initPipeline(replyPort); 68 initPipeline(replyPort);
56 startHTTPServerStage(); 69 startHTTPServerStage();
57 }); 70 });
58 } 71 }
59 72
60 /** Initial pipeline stage - starts the HTTP server, if appropriate. */ 73 /** Initial pipeline stage - starts the HTTP server, if appropriate. */
61
62 startHTTPServerStage() { 74 startHTTPServerStage() {
63 if (config["server"]) { 75 if (config["server"]) {
64 serverPath = config["testfile"]; 76 serverPath = config["testfile"];
65 // Replace .dart with _server.dart to get test's server file, if any. 77 // Replace .dart with _server.dart to get test's server file, if any.
66 var truncLen = serverPath.length - '.dart'.length; 78 var truncLen = serverPath.length - '.dart'.length;
67 serverPath = '${serverPath.substring(0, truncLen)}_server.dart'; 79 serverPath = '${serverPath.substring(0, truncLen)}_server.dart';
68 var serverFile = new File(serverPath); 80 var serverFile = new File(serverPath);
69 if (!serverFile.existsSync()) { 81 if (!serverFile.existsSync()) {
70 // No custom server; run the default server. 82 // No custom server; run the default server.
71 serverPath = '${config["runnerDir"]}/http_server_runner.dart'; 83 serverPath = '${config["runnerDir"]}/http_server_runner.dart';
72 } 84 }
73 if (serverPath != null) { 85 if (serverPath != null) {
74 serverRoot = config["root"]; 86 serverRoot = config["root"];
75 if (serverRoot == null) { 87 if (serverRoot == null) {
76 // Set the root to be the directory containing the test file. 88 // Set the root to be the directory containing the test file.
77 serverRoot = getDirectory(config["testfile"]); 89 serverRoot = getDirectory(config["testfile"]);
78 } 90 }
79 91
80 if (config["port"] == null) { 92 if (config["port"] == null) {
81 // In this case we have to choose a random port and we need 93 // In this case we have to choose a random port and we need
82 // to see if the server starts successfully on that port. 94 // to see if the server starts successfully on that port.
83 var r = new Random(); 95 var r = new Random();
84 tryStartHTTPServer(r, MAX_SERVER_TRIES); 96 tryStartHTTPServer(r, MAX_SERVER_TRIES);
85 } else { 97 } else {
86 serverPort = int.parse(config["port"]); 98 serverPort = int.parse(config["port"]);
87 // Start the HTTP server. 99 // Start the HTTP server.
88 serverId = startProcess(config["dart"], 100 serverId = startProcess(config["dart"],
89 [ serverPath, '--port=$serverPort', '--root=$serverRoot']); 101 [ serverPath, '--port=$serverPort', '--root=$serverRoot'],
102 stdout, stderr);
90 } 103 }
91 } 104 }
92 } 105 }
93 wrapStage(); 106 wrapStage();
94 } 107 }
95 108
96 void tryStartHTTPServer(Random r, int remainingAttempts) { 109 void tryStartHTTPServer(Random r, int remainingAttempts) {
97 // Pick a port from 1024 to 32767. 110 // Pick a port from 1024 to 32767.
98 serverPort = 1024 + r.nextInt(32768 - 1024); 111 serverPort = 1024 + r.nextInt(32768 - 1024);
99 logMessage('Trying ${config["dart"]} $serverPath --port=$serverPort ' 112 logMessage('Trying ${config["dart"]} $serverPath --port=$serverPort '
100 '--root=$serverRoot'); 113 '--root=$serverRoot');
101 serverId = startProcess(config["dart"], 114 serverId = startProcess(config["dart"],
102 [ serverPath, '--port=$serverPort', '--root=$serverRoot'], 115 [ serverPath, '--port=$serverPort', '--root=$serverRoot'],
116 stdout, stderr,
103 (line) { 117 (line) {
104 if (line.startsWith('Server listening')) { 118 if (line.startsWith('Server listening')) {
105 wrapStage(); 119 wrapStage();
106 } else if (remainingAttempts == 0) { 120 } else if (remainingAttempts == 0) {
107 print('Failed to start HTTP server after $MAX_SERVER_TRIES' 121 print('Failed to start HTTP server after $MAX_SERVER_TRIES'
108 ' attempts; aborting.'); 122 ' attempts; aborting.');
109 exit(1); 123 exit(1);
110 } else { 124 } else {
111 tryStartHTTPServer(r, remainingAttempts - 1); 125 tryStartHTTPServer(r, remainingAttempts - 1);
112 } 126 }
113 }); 127 });
114 } 128 }
115 129
116 /** Initial pipeline stage - generates Dart and HTML wrapper files. */ 130 /** Initial pipeline stage - generates Dart and HTML wrapper files. */
117 wrapStage() { 131 wrapStage() {
118 var tmpDir = config["tempdir"]; 132 tmpDir = config["targetDir"];
119 var testFile = config["testfile"]; 133 var testFile = config["testfile"];
120 134
121 // Make sure the temp dir exists.
122 var d = new Directory(tmpDir);
123 if (!d.existsSync()) {
124 d.createSync();
125 }
126
127 // Generate names for the generated wrapper files. 135 // Generate names for the generated wrapper files.
128 tempDartFile = createTempName(tmpDir, testFile, '.dart'); 136 tempDartFile = createTempName(tmpDir, testFile, '.dart');
129 if (config["runtime"] != 'vm') { 137 if (config["runtime"] != 'vm') {
130 tempHtmlFile = createTempName(tmpDir, testFile, '.html'); 138 tempHtmlFile = createTempName(tmpDir, testFile, '.html');
131 if (config["layout"]) { 139 if (config["layout"]) {
132 tempChildDartFile = 140 tempChildDartFile =
133 createTempName(tmpDir, testFile, '-child.dart'); 141 createTempName(tmpDir, testFile, '-child.dart');
134 } 142 }
135 if (config["runtime"] == 'drt-js') { 143 if (config["runtime"] == 'drt-js') {
136 tempJsFile = createTempName(tmpDir, testFile, '.js'); 144 tempJsFile = createTempName(tmpDir, testFile, '.js');
137 if (config["layout"]) { 145 if (config["layout"]) {
138 tempChildJsFile = 146 tempChildJsFile =
139 createTempName(tmpDir, testFile, '-child.js'); 147 createTempName(tmpDir, testFile, '-child.js');
140 } 148 }
141 } 149 }
142 } 150 }
143 151
144 // Create the test controller Dart wrapper. 152 // Create the test controller Dart wrapper.
145 var directives, extras; 153 var directives, extras;
146 154
147 if (config["layout"]) { 155 if (config["layout"]) {
148 directives = ''' 156 directives = '''
149 import 'dart:uri'; 157 import 'dart:async';
150 import 'dart:io'; 158 import 'dart:io';
151 import 'dart:math'; 159 import 'dart:math';
152 part '${config["runnerDir"]}/layout_test_controller.dart'; 160 import 'dart:uri';
161 part '${normalizePath('${config["runnerDir"]}/layout_test_controller.dart')}';
153 '''; 162 ''';
154 extras = ''' 163 extras = '''
155 sourceDir = '${config["expectedDirectory"]}'; 164 baseUrl = 'file://${normalizePath('$tempHtmlFile')}';
156 baseUrl = 'file://$tempHtmlFile'; 165 tprint = (msg) => print('###\$msg');
157 tprint = (msg) => print('###\$msg'); 166 notifyDone = (e) => exit(e);
158 notifyDone = (e) => exit(e);
159 '''; 167 ''';
160 } else if (config["runtime"] == "vm") { 168 } else if (config["runtime"] == "vm") {
161 directives = ''' 169 directives = '''
162 import 'dart:io'; 170 import 'dart:async';
163 import 'dart:isolate'; 171 import 'dart:io';
164 import '${config["unittest"]}' as unittest; 172 import 'dart:isolate';
165 import '${config["testfile"]}' as test; 173 import 'package:unittest/unittest.dart';
166 part '${config["runnerDir"]}/standard_test_runner.dart'; 174 import '${normalizePath('${config["testfile"]}')}' as test;
175 part '${normalizePath('${config["runnerDir"]}/standard_test_runner.dart')}';
167 '''; 176 ''';
168 extras = ''' 177 extras = '''
169 includeFilters = ${config["include"]}; 178 includeFilters = ${config["include"]};
170 excludeFilters = ${config["exclude"]}; 179 excludeFilters = ${config["exclude"]};
171 tprint = (msg) => print('###\$msg'); 180 tprint = (msg) => print('###\$msg');
172 notifyDone = (e) {}; 181 notifyDone = (e) { exit(e); };
173 unittest.testState["port"] = $serverPort; 182 testState["port"] = $serverPort;
174 '''; 183 ''';
175 } else { 184 } else {
176 directives = ''' 185 directives = '''
177 import 'dart:html'; 186 import 'dart:async';
178 import 'dart:isolate'; 187 import 'dart:html';
179 import '${config["unittest"]}' as unittest; 188 import 'dart:isolate';
180 import '${config["testfile"]}' as test; 189 import 'package:unittest/unittest.dart';
181 part '${config["runnerDir"]}/standard_test_runner.dart'; 190 import '${normalizePath('${config["testfile"]}')}' as test;
191 part '${normalizePath('${config["runnerDir"]}/standard_test_runner.dart')}';
182 '''; 192 ''';
183 extras = ''' 193 extras = '''
184 includeFilters = ${config["include"]}; 194 includeFilters = ${config["include"]};
185 excludeFilters = ${config["exclude"]}; 195 excludeFilters = ${config["exclude"]};
186 tprint = (msg) => query('#console').addText('###\$msg\\n'); 196 tprint = (msg) => query('#console').appendText('###\$msg\\n');
187 notifyDone = (e) => window.postMessage('done', '*'); 197 notifyDone = (e) => window.postMessage('done', '*');
188 unittest.testState["port"] = $serverPort; 198 testState["port"] = $serverPort;
189 '''; 199 ''';
190 } 200 }
191 201
192 var action = 'process(test.main, unittest.runTests)'; 202 var action = 'process(test.main, runTests)';
193 if (config["layout-text"]) { 203 if (config["layout-text"]) {
194 action = 'runTextLayoutTests()'; 204 action = 'runTextLayoutTests()';
195 } else if (config["layout-pixel"]) { 205 } else if (config["layout-pixel"]) {
196 action = 'runPixelLayoutTests()'; 206 action = 'runPixelLayoutTests()';
197 } else if (config["list-tests"]) { 207 } else if (config["list-tests"]) {
198 action = 'process(test.main, listTests)'; 208 action = 'process(test.main, listTests)';
199 } else if (config["list-groups"]) { 209 } else if (config["list-groups"]) {
200 action = 'process(test.main, listGroups)'; 210 action = 'process(test.main, listGroups)';
201 } else if (config["isolate"]) { 211 } else if (config["isolate"]) {
202 action = 'process(test.main, runIsolateTests)'; 212 action = 'process(test.main, runIsolateTests)';
203 } 213 }
204 214
205 logMessage('Creating $tempDartFile'); 215 logMessage('Creating $tempDartFile');
206 writeFile(tempDartFile, ''' 216 writeFile(tempDartFile, '''
207 library test_controller; 217 library test_controller;
208 $directives 218 $directives
209 219
210 main() { 220 main() {
211 immediate = ${config["immediate"]}; 221 immediate = ${config["immediate"]};
212 includeTime = ${config["time"]}; 222 includeTime = ${config["time"]};
213 passFormat = '${config["pass-format"]}'; 223 passFormat = '${config["pass-format"]}';
214 failFormat = '${config["fail-format"]}'; 224 failFormat = '${config["fail-format"]}';
215 errorFormat = '${config["error-format"]}'; 225 errorFormat = '${config["error-format"]}';
216 listFormat = '${config["list-format"]}'; 226 listFormat = '${config["list-format"]}';
217 regenerate = ${config["regenerate"]}; 227 regenerate = ${config["regenerate"]};
218 summarize = ${config["summary"]}; 228 summarize = ${config["summary"]};
219 testfile = '$testFile'; 229 testfile = '${testFile.replaceAll("\\","\\\\")}';
220 drt = '${config["drt"]}'; 230 drt = '${config["drt"].replaceAll("\\","\\\\")}';
221 $extras 231 $extras
222 $action; 232 $action;
223 } 233 }
224 '''); 234 ''');
225 235
226 // Create the child wrapper for layout tests. 236 // Create the child wrapper for layout tests.
227 if (config["layout"]) { 237 if (config["layout"]) {
228 logMessage('Creating $tempChildDartFile'); 238 logMessage('Creating $tempChildDartFile');
229 writeFile(tempChildDartFile, ''' 239 writeFile(tempChildDartFile, '''
230 library layout_test; 240 library layout_test;
231 import 'dart:math'; 241 import 'dart:math';
232 import 'dart:isolate'; 242 import 'dart:isolate';
233 import 'dart:html'; 243 import 'dart:html';
234 import 'dart:uri'; 244 import 'dart:uri';
235 import '${config["unittest"]}' as 'unittest' ; 245 import 'package:unittest/unittest.dart' as unittest;
236 import '$testFile', prefix: 'test' ; 246 import '${normalizePath('$testFile')}' as test;
237 part '${config["runnerDir"]}/layout_test_runner.dart'; 247 part '${normalizePath('${config["runnerDir"]}/layout_test_runner.dart')}';
238 248
239 main() { 249 main() {
240 includeFilters = ${config["include"]}; 250 includeFilters = ${config["include"]};
241 excludeFilters = ${config["exclude"]}; 251 excludeFilters = ${config["exclude"]};
242 unittest.testState["port"] = $serverPort; 252 unittest.testState["port"] = $serverPort;
243 runTests(test.main); 253 runTests(test.main);
244 } 254 }
245 '''); 255 ''');
246 } 256 }
247 257
248 // Create the HTML wrapper and compile to Javascript if necessary. 258 // Create the HTML wrapper and compile to Javascript if necessary.
249 var isJavascript = config["runtime"] == 'drt-js'; 259 var isJavascript = config["runtime"] == 'drt-js';
250 if (config["runtime"] == 'drt-dart' || isJavascript) { 260 if (config["runtime"] == 'drt-dart' || isJavascript) {
251 var bodyElements, runAsText; 261 var bodyElements, runAsText;
252 262
253 if (config["layout"]) { 263 if (config["layout"]) {
254 sourceFile = tempChildDartFile; 264 sourceFile = tempChildDartFile;
255 scriptFile = isJavascript ? tempChildJsFile : tempChildDartFile; 265 scriptFile = isJavascript ? tempChildJsFile : tempChildDartFile;
256 bodyElements = ''; 266 bodyElements = '';
257 } else { 267 } else {
258 sourceFile = tempDartFile; 268 sourceFile = tempDartFile;
259 scriptFile = isJavascript ? tempJsFile : tempDartFile; 269 scriptFile = isJavascript ? tempJsFile : tempDartFile;
260 bodyElements = '<div id="container"></div><pre id="console"></pre>'; 270 bodyElements = '<div id="container"></div><pre id="console"></pre>';
261 runAsText = "window.testRunner.dumpAsText();"; 271 runAsText = "testRunner.dumpAsText();";
262 } 272 }
263 scriptType = isJavascript ? 'text/javascript' : 'application/dart'; 273 scriptType = isJavascript ? 'text/javascript' : 'application/dart';
264 274
265 if (config["runtime"] == 'drt-dart' || isJavascript) { 275 if (config["runtime"] == 'drt-dart' || isJavascript) {
266 logMessage('Creating $tempHtmlFile'); 276 logMessage('Creating $tempHtmlFile');
267 writeFile(tempHtmlFile, ''' 277 writeFile(tempHtmlFile, '''
268 <!DOCTYPE html> 278 <!DOCTYPE html>
269 <html> 279 <html>
270 <head> 280 <head>
271 <meta charset="utf-8"> 281 <meta charset="utf-8">
272 <title>$testFile</title> 282 <title>$testFile</title>
273 <link rel="stylesheet" href="${config["runnerDir"]}/testrunner.css"> 283 <link rel="stylesheet" href="${config["runnerDir"]}/testrunner.css">
274 <script type='text/javascript'> 284 <script type='text/javascript'>
275 if (window.testRunner) { 285 var testRunner = window.testRunner || window.layoutTestController;
286 if (testRunner) {
276 function handleMessage(m) { 287 function handleMessage(m) {
277 if (m.data == 'done') { 288 if (m.data == 'done') {
278 window.testRunner.notifyDone(); 289 testRunner.notifyDone();
279 } 290 }
280 } 291 }
281 window.testRunner.waitUntilDone(); 292 testRunner.waitUntilDone();
282 $runAsText 293 $runAsText
283 window.addEventListener("message", handleMessage, false); 294 window.addEventListener("message", handleMessage, false);
284 } 295 }
285 if (!$isJavascript && navigator.webkitStartDart) { 296 if (!$isJavascript && navigator.webkitStartDart) {
286 navigator.webkitStartDart(); 297 navigator.webkitStartDart();
287 } 298 }
288 </script> 299 </script>
289 </head> 300 </head>
290 <body> 301 <body>
291 $bodyElements 302 $bodyElements
292 <script type='$scriptType' src='$scriptFile'></script> 303 <script type='$scriptType' src='$scriptFile'></script>
293 </script> 304 </script>
294 <script
295 src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js">
296 </script>
297 </body> 305 </body>
298 </html> 306 </html>
299 '''); 307 ''');
300 } 308 }
301 } 309 }
302 compileStage(isJavascript); 310 compileStage(isJavascript);
303 } 311 }
304 312
305 /** Second stage of pipeline - compiles Dart to Javascript if needed. */ 313 /** Second stage of pipeline - compiles Dart to Javascript if needed. */
306 compileStage(isJavascript) { 314 compileStage(isJavascript) {
307 if (isJavascript) { // Compile the Dart file. 315 if (isJavascript) { // Compile the Dart file.
316 var cmd = config["dart2js"];
317 var input = sourceFile.replaceAll('/', Platform.pathSeparator);
318 var output = scriptFile.replaceAll('/', Platform.pathSeparator);
308 if (config["checked"]) { 319 if (config["checked"]) {
309 runCommand(config["dart2js"], 320 runCommand(cmd, [ '-c', '-o$output', '$input' ], stdout, stderr)
310 [ '--enable_checked_mode', '--out=$scriptFile', '$sourceFile' ]). 321 .then(runTestStage);
311 then(runTestStage);
312 } else { 322 } else {
313 runCommand(config["dart2js"], 323 runCommand(cmd, [ '-o$output', '$input' ], stdout, stderr)
314 [ '--out=$scriptFile', '$sourceFile' ]). 324 .then(runTestStage);
315 then(runTestStage);
316 } 325 }
317 } else { 326 } else {
318 runTestStage(0); 327 runTestStage(0);
319 } 328 }
320 } 329 }
321 330
322 /** Third stage of pipeline - runs the tests. */ 331 /** Third stage of pipeline - runs the tests. */
323 runTestStage(_) { 332 runTestStage(_) {
324 var cmd, args; 333 var cmd, args;
325 if (config["runtime"] == 'vm' || config["layout"]) { // Run the tests. 334 if (config["runtime"] == 'vm' || config["layout"]) { // Run the tests.
326 if (config["checked"]) { 335 if (config["checked"]) {
327 cmd = config["dart"]; 336 cmd = config["dart"];
328 args = [ '--enable_asserts', '--enable_type_checks', tempDartFile ]; 337 args = [ '--enable_asserts', '--enable_type_checks', tempDartFile ];
329 } else { 338 } else {
330 cmd = config["dart"]; 339 cmd = config["dart"];
331 args = [ tempDartFile ]; 340 args = [ tempDartFile ];
332 } 341 }
333 } else { 342 } else {
334 cmd = config["drt"]; 343 cmd = config["drt"];
335 args = [ '--no-timeout', tempHtmlFile ]; 344 args = [ '--no-timeout', tempHtmlFile ];
336 } 345 }
337 runCommand(cmd, args, config["timeout"]).then(cleanupStage); 346 runCommand(cmd, args, stdout, stderr, config["timeout"]).then(cleanupStage);
338 } 347 }
339 348
340 /** 349 /**
341 * Final stage of the pipeline - clean up generated files and notify 350 * Final stage of the pipeline - clean up generated files and notify
342 * the originator that started the isolate. 351 * the originator that started the isolate.
343 */ 352 */
344 cleanupStage(exitcode) { 353 cleanupStage(exitcode) {
345 if (config["server"]) { // Stop the HTTP server. 354 if (config["server"]) { // Stop the HTTP server.
346 stopProcess(serverId); 355 stopProcess(serverId);
347 } 356 }
348 357
349 if (!config["keep-files"]) { // Remove the temporary files. 358 if (config["clean-files"]) { // Remove the temporary files.
350 cleanup(tempDartFile); 359 cleanup(tempDartFile);
351 cleanup(tempHtmlFile); 360 cleanup(tempHtmlFile);
352 cleanup(tempJsFile); 361 cleanup(tempJsFile);
353 cleanup(tempChildDartFile); 362 cleanup(tempChildDartFile);
354 cleanup(tempChildJsFile); 363 cleanup(tempChildJsFile);
364 cleanup(createTempName(tmpDir, "pubspec", "yaml"));
365 cleanup(createTempName(tmpDir, "pubspec", "lock"));
366 cleanupDir(createTempName(tmpDir, "packages"));
355 } 367 }
356 completePipeline(exitcode); 368 completePipeline(stdout, stderr, exitcode);
357 } 369 }
OLDNEW
« no previous file with comments | « utils/testrunner/pubspec.yaml ('k') | utils/testrunner/standard_test_runner.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698