OLD | NEW |
| (Empty) |
1 // Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file | |
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.md file. | |
4 | |
5 /// Test suite for running tests in a shared Dart VM. Take a look at | |
6 /// ../../../tests/fletch_tests/all_tests.dart for more information. | |
7 library test.fletch_test_suite; | |
8 | |
9 import 'dart:io' as io; | |
10 | |
11 import 'dart:convert' show | |
12 JSON, | |
13 LineSplitter, | |
14 UTF8, | |
15 Utf8Decoder; | |
16 | |
17 import 'dart:async' show | |
18 Completer, | |
19 Future, | |
20 Stream, | |
21 StreamIterator, | |
22 Timer; | |
23 | |
24 import 'test_suite.dart' show | |
25 TestSuite, | |
26 TestUtils; | |
27 | |
28 import 'test_runner.dart' show | |
29 Command, | |
30 CommandBuilder, | |
31 CommandOutput, | |
32 TestCase; | |
33 | |
34 import 'runtime_configuration.dart' show | |
35 RuntimeConfiguration; | |
36 | |
37 import 'status_file_parser.dart' show | |
38 Expectation, | |
39 ReadTestExpectationsInto, | |
40 TestExpectations; | |
41 | |
42 import '../../../tests/fletch_tests/messages.dart' show | |
43 ErrorMessage, | |
44 Info, | |
45 ListTests, | |
46 ListTestsReply, | |
47 Message, | |
48 NamedMessage, | |
49 RunTest, | |
50 TestFailed, | |
51 TestStdoutLine, | |
52 TimedOut, | |
53 messageTransformer; | |
54 | |
55 class FletchTestRuntimeConfiguration extends RuntimeConfiguration { | |
56 final String system; | |
57 final String dartBinary; | |
58 | |
59 FletchTestRuntimeConfiguration(Map configuration) | |
60 : system = configuration['system'], | |
61 dartBinary = '${TestUtils.buildDir(configuration)}' | |
62 '${io.Platform.pathSeparator}dart', | |
63 super.subclass(); | |
64 } | |
65 | |
66 class FletchTestSuite extends TestSuite { | |
67 final String testSuiteDir; | |
68 | |
69 TestCompleter completer; | |
70 | |
71 FletchTestSuite(Map configuration, this.testSuiteDir) | |
72 : super(configuration, "fletch_tests"); | |
73 | |
74 void forEachTest( | |
75 void onTest(TestCase testCase), | |
76 Map testCache, | |
77 [void onDone()]) { | |
78 this.doTest = onTest; | |
79 if (configuration['runtime'] != 'fletch_tests') { | |
80 onDone(); | |
81 return; | |
82 } | |
83 | |
84 FletchTestRuntimeConfiguration runtimeConfiguration = | |
85 new RuntimeConfiguration(configuration); | |
86 | |
87 TestExpectations expectations = new TestExpectations(); | |
88 String buildDir = TestUtils.buildDir(configuration); | |
89 String version; | |
90 | |
91 // Define a path for temporary output generated during tests. | |
92 String tempDirPath = '$buildDir/temporary_test_output'; | |
93 io.Directory tempDir = new io.Directory(tempDirPath); | |
94 try { | |
95 tempDir.deleteSync(recursive: true); | |
96 } on io.FileSystemException catch (e) { | |
97 // Ignored, we assume the file did not exist. | |
98 } | |
99 | |
100 String javaHome = _guessJavaHome(configuration["arch"]); | |
101 if (javaHome == null) { | |
102 String arch = configuration["arch"]; | |
103 print("Notice: Java tests are disabled"); | |
104 print("Unable to find a JDK installation for architecture $arch"); | |
105 print("Install a JDK or set JAVA_PATH to an existing installation."); | |
106 // TODO(zerny): Throw an error if no-java is not supplied. | |
107 } else { | |
108 print("Notice: Enabled Java tests using JDK at $javaHome"); | |
109 } | |
110 | |
111 bool helperProgramExited = false; | |
112 io.Process vmProcess; | |
113 ReadTestExpectationsInto( | |
114 expectations, '$testSuiteDir/fletch_tests.status', | |
115 configuration).then((_) { | |
116 return new io.File('$buildDir/gen/version.cc').readAsLines(); | |
117 }).then((List<String> versionFileLines) { | |
118 // Search for the 'return "version_string";' line. | |
119 for (String line in versionFileLines) { | |
120 if (line.contains('return')) { | |
121 version = line.substring( | |
122 line.indexOf('"') + 1, line.lastIndexOf('"')); | |
123 } | |
124 } | |
125 assert(version != null); | |
126 }).then((_) { | |
127 return io.ServerSocket.bind(io.InternetAddress.LOOPBACK_IP_V4, 0); | |
128 }).then((io.ServerSocket server) { | |
129 return io.Process.start( | |
130 runtimeConfiguration.dartBinary, | |
131 ['-Dfletch-vm=$buildDir/fletch-vm', | |
132 '-Dfletch.version=$version', | |
133 '-Ddart-sdk=third_party/dart/sdk/', | |
134 '-Dtests-dir=tests/', | |
135 '-Djava-home=$javaHome', | |
136 '-Dtest.dart.build-dir=$buildDir', | |
137 '-Dtest.dart.build-arch=${configuration["arch"]}', | |
138 '-Dtest.dart.build-system=${configuration["system"]}', | |
139 '-Dtest.dart.build-clang=${configuration["clang"]}', | |
140 '-Dtest.dart.build-asan=${configuration["asan"]}', | |
141 '-Dtest.dart.temp-dir=$tempDirPath', | |
142 '-Dtest.dart.servicec-dir=tools/servicec/', | |
143 '-c', | |
144 '--packages=.packages', | |
145 '-Dtest.fletch_test_suite.port=${server.port}', | |
146 '$testSuiteDir/fletch_test_suite.dart']).then((io.Process process) { | |
147 process.exitCode.then((_) { | |
148 helperProgramExited = true; | |
149 server.close(); | |
150 }); | |
151 vmProcess = process; | |
152 return process.stdin.close(); | |
153 }).then((_) { | |
154 return server.first.catchError((error) { | |
155 // The VM died before we got a connection. | |
156 assert(helperProgramExited); | |
157 return null; | |
158 }); | |
159 }).then((io.Socket socket) { | |
160 server.close(); | |
161 return socket; | |
162 }); | |
163 }).then((io.Socket socket) { | |
164 assert(socket != null || helperProgramExited); | |
165 completer = new TestCompleter(vmProcess, socket); | |
166 completer.initialize(); | |
167 return completer.requestTestNames(); | |
168 }).then((List<String> testNames) { | |
169 for (String name in testNames) { | |
170 Set<Expectation> expectedOutcomes = expectations.expectations(name); | |
171 TestCase testCase = new TestCase( | |
172 'fletch_tests/$name', <Command>[], configuration, expectedOutcomes); | |
173 var command = new FletchTestCommand(name, completer); | |
174 testCase.commands.add(command); | |
175 if (!expectedOutcomes.contains(Expectation.SKIP)) { | |
176 completer.expect(command); | |
177 } | |
178 enqueueNewTestCase(testCase); | |
179 } | |
180 }).then((_) { | |
181 onDone(); | |
182 }); | |
183 } | |
184 | |
185 void cleanup() { | |
186 completer.allDone(); | |
187 } | |
188 | |
189 String _guessJavaHome(String buildArchitecture) { | |
190 String arch = buildArchitecture == 'ia32' ? '32' : '64'; | |
191 | |
192 // Try to locate a valid installation based on JAVA_HOME. | |
193 String javaHome = | |
194 _guessJavaHomeArch(io.Platform.environment['JAVA_HOME'], arch); | |
195 if (javaHome != null) return javaHome; | |
196 | |
197 // Try to locate a valid installation using the java_home utility. | |
198 String javaHomeUtil = '/usr/libexec/java_home'; | |
199 if (new io.File(javaHomeUtil).existsSync()) { | |
200 List<String> args = <String>['-v', '1.6+', '-d', arch]; | |
201 io.ProcessResult result = | |
202 io.Process.runSync(javaHomeUtil, args); | |
203 if (result.exitCode == 0) { | |
204 String javaHome = result.stdout.trim(); | |
205 if (_isValidJDK(javaHome)) return javaHome; | |
206 } | |
207 } | |
208 | |
209 // Try to locate a valid installation using the path to javac. | |
210 io.ProcessResult result = | |
211 io.Process.runSync('command', ['-v', 'javac'], runInShell: true); | |
212 if (result.exitCode == 0) { | |
213 String javac = result.stdout.trim(); | |
214 while (io.FileSystemEntity.isLinkSync(javac)) { | |
215 javac = new io.Link(javac).resolveSymbolicLinksSync(); | |
216 } | |
217 // TODO(zerny): Take into account Mac javac paths can be of the form: | |
218 // .../Versions/X/Commands/javac | |
219 String javaHome = | |
220 _guessJavaHomeArch(javac.replaceAll('/bin/javac', ''), arch); | |
221 if (javaHome != null) return javaHome; | |
222 } | |
223 | |
224 return null; | |
225 } | |
226 | |
227 String _guessJavaHomeArch(String javaHome, String arch) { | |
228 if (javaHome == null) return null; | |
229 | |
230 // Check if the java installation supports the requested architecture. | |
231 if (new io.File('$javaHome/bin/java').existsSync()) { | |
232 int supportsVersion = io.Process.runSync( | |
233 '$javaHome/bin/java', ['-d$arch', '-version']).exitCode; | |
234 if (supportsVersion == 0 && _isValidJDK(javaHome)) return javaHome; | |
235 } | |
236 | |
237 // Check for architecture specific installation by post-fixing arch. | |
238 String archPostfix = '${javaHome}-$arch'; | |
239 if (_isValidJDK(archPostfix)) return archPostfix; | |
240 | |
241 // Check for architecture specific installation by replacing amd64 and i386. | |
242 String archReplace; | |
243 if (arch == '32' && javaHome.contains('amd64')) { | |
244 archReplace = javaHome.replaceAll('amd64', 'i386'); | |
245 } else if (arch == '64' && javaHome.contains('i386')) { | |
246 archReplace = javaHome.replaceAll('i386', 'amd64'); | |
247 } | |
248 if (_isValidJDK(archReplace)) return archReplace; | |
249 | |
250 return null; | |
251 } | |
252 | |
253 bool _isValidJDK(String javaHome) { | |
254 if (javaHome == null) return false; | |
255 return new io.File('$javaHome/include/jni.h').existsSync(); | |
256 } | |
257 } | |
258 | |
259 /// Pattern that matches warnings (from dart2js) that contain a comment saying | |
260 /// "NO_LINT". | |
261 final RegExp noLintFilter = | |
262 new RegExp(r"[^\n]*\n[^\n]*\n[^\n]* // NO_LINT\n *\^+\n"); | |
263 | |
264 class FletchTestOutputCommand implements CommandOutput { | |
265 final Command command; | |
266 final Duration time; | |
267 final Message message; | |
268 final List<String> stdoutLines; | |
269 | |
270 FletchTestOutputCommand( | |
271 this.command, | |
272 this.message, | |
273 this.time, | |
274 this.stdoutLines); | |
275 | |
276 Expectation result(TestCase testCase) { | |
277 switch (message.type) { | |
278 case 'TestPassed': | |
279 return Expectation.PASS; | |
280 | |
281 case 'TestFailed': | |
282 return Expectation.FAIL; | |
283 | |
284 case 'TimedOut': | |
285 return Expectation.TIMEOUT; | |
286 | |
287 default: | |
288 return Expectation.CRASH; | |
289 } | |
290 } | |
291 | |
292 bool get hasCrashed => false; | |
293 | |
294 bool get hasTimedOut => false; | |
295 | |
296 bool didFail(testCase) => message.type != 'TestPassed'; | |
297 | |
298 bool hasFailed(TestCase testCase) { | |
299 return testCase.isNegative ? !didFail(testCase) : didFail(testCase); | |
300 } | |
301 | |
302 bool get canRunDependendCommands => false; | |
303 | |
304 bool get successful => true; | |
305 | |
306 int get exitCode => 0; | |
307 | |
308 int get pid => 0; | |
309 | |
310 List<int> get stdout { | |
311 if (stdoutLines != null) { | |
312 return UTF8.encode(stdoutLines.join("\n")); | |
313 } else { | |
314 return <int>[]; | |
315 } | |
316 } | |
317 | |
318 List<int> get stderr { | |
319 String result; | |
320 | |
321 switch (message.type) { | |
322 case 'TestPassed': | |
323 case 'TimedOut': | |
324 return <int>[]; | |
325 | |
326 case 'TestFailed': | |
327 TestFailed failed = message; | |
328 result = '${failed.error}\n${failed.stackTrace}'; | |
329 break; | |
330 | |
331 default: | |
332 result = '$message'; | |
333 break; | |
334 } | |
335 return UTF8.encode(result); | |
336 } | |
337 | |
338 List<String> get diagnostics => <String>[]; | |
339 | |
340 bool get compilationSkipped => false; | |
341 } | |
342 | |
343 class FletchTestCommand implements Command { | |
344 final String _name; | |
345 | |
346 final TestCompleter _completer; | |
347 | |
348 FletchTestCommand(this._name, this._completer); | |
349 | |
350 String get displayName => "fletch_test"; | |
351 | |
352 int get maxNumRetries => 0; | |
353 | |
354 Future<FletchTestOutputCommand> run(int timeout) { | |
355 Stopwatch sw = new Stopwatch()..start(); | |
356 return _completer.run(this, timeout).then((NamedMessage message) { | |
357 FletchTestOutputCommand output = | |
358 new FletchTestOutputCommand( | |
359 this, message, sw.elapsed, _completer.testOutput[message.name]); | |
360 _completer.done(this); | |
361 return output; | |
362 }); | |
363 } | |
364 | |
365 String toString() => 'FletchTestCommand($_name)'; | |
366 | |
367 set displayName(_) => throw "not supported"; | |
368 | |
369 get commandLine => throw "not supported"; | |
370 set commandLine(_) => throw "not supported"; | |
371 | |
372 String get reproductionCommand => throw "not supported"; | |
373 | |
374 get outputIsUpToDate => throw "not supported"; | |
375 } | |
376 | |
377 class TestCompleter { | |
378 final Map<String, FletchTestCommand> expected = | |
379 new Map<String, FletchTestCommand>(); | |
380 final Map<String, Completer> completers = new Map<String, Completer>(); | |
381 final Completer<List<String>> testNamesCompleter = | |
382 new Completer<List<String>>(); | |
383 final Map<String, List<String>> testOutput = new Map<String, List<String>>(); | |
384 final io.Process vmProcess; | |
385 final io.Socket socket; | |
386 | |
387 int exitCode; | |
388 String stderr = ""; | |
389 | |
390 TestCompleter(this.vmProcess, this.socket); | |
391 | |
392 void initialize() { | |
393 List<String> stderrLines = <String>[]; | |
394 Future stdoutFuture = | |
395 vmProcess.stdout.transform(UTF8.decoder).transform(new LineSplitter()) | |
396 .listen(io.stdout.writeln).asFuture(); | |
397 bool inDartVmUncaughtMessage = false; | |
398 Future stderrFuture = | |
399 vmProcess.stderr.transform(UTF8.decoder).transform(new LineSplitter()) | |
400 .listen((String line) { | |
401 io.stderr.writeln(line); | |
402 stderrLines.add(line); | |
403 }).asFuture(); | |
404 vmProcess.exitCode.then((value) { | |
405 exitCode = value; | |
406 stderr = stderrLines.join("\n"); | |
407 for (String name in completers.keys) { | |
408 Completer completer = completers[name]; | |
409 completer.complete( | |
410 new TestFailed( | |
411 name, | |
412 "Helper program exited prematurely with exit code $exitCode.", | |
413 stderr)); | |
414 } | |
415 if (exitCode != 0) { | |
416 stdoutFuture.then((_) => stderrFuture).then((_) { | |
417 throw "Helper program exited with exit code $exitCode.\n$stderr"; | |
418 }); | |
419 } | |
420 }); | |
421 // TODO(ahe): Don't use StreamIterator here, just use listen and | |
422 // processMessage. | |
423 StreamIterator<Message> messages; | |
424 if (socket == null) { | |
425 messages = new StreamIterator<Message>( | |
426 new Stream<Message>.fromIterable(<Message>[])); | |
427 } else { | |
428 messages = new StreamIterator<Message>( | |
429 socket | |
430 .transform(UTF8.decoder).transform(new LineSplitter()) | |
431 .transform(messageTransformer)); | |
432 } | |
433 process(messages); | |
434 } | |
435 | |
436 Future<List<String>> requestTestNames() { | |
437 if (socket == null) return new Future.value(<String>[]); | |
438 socket.writeln(JSON.encode(const ListTests().toJson())); | |
439 return testNamesCompleter.future; | |
440 } | |
441 | |
442 void expect(FletchTestCommand command) { | |
443 expected[command._name] = command; | |
444 } | |
445 | |
446 void done(FletchTestCommand command) { | |
447 expected.remove(command._name); | |
448 if (expected.isEmpty) { | |
449 allDone(); | |
450 } | |
451 } | |
452 | |
453 Future run(FletchTestCommand command, int timeout) { | |
454 if (command._name == "self/testNeverCompletes") { | |
455 // Ensure timeout test times out quickly. | |
456 timeout = 1; | |
457 } | |
458 socket.writeln( | |
459 JSON.encode(new RunTest(command._name).toJson())); | |
460 Timer timer = new Timer(new Duration(seconds: timeout), () { | |
461 socket.writeln( | |
462 JSON.encode(new TimedOut(command._name).toJson())); | |
463 }); | |
464 | |
465 Completer completer = new Completer(); | |
466 completers[command._name] = completer; | |
467 if (exitCode != null) { | |
468 completer.complete( | |
469 new TestFailed( | |
470 command._name, | |
471 "Helper program exited prematurely with exit code $exitCode.", | |
472 stderr)); | |
473 } | |
474 return completer.future.then((value) { | |
475 timer.cancel(); | |
476 return value; | |
477 }); | |
478 } | |
479 | |
480 void processMessage(Message message) { | |
481 switch (message.type) { | |
482 case 'Info': | |
483 Info info = message; | |
484 // For debugging, shouldn't normally be called. | |
485 print(info.data); | |
486 break; | |
487 case 'TestPassed': | |
488 case 'TestFailed': | |
489 case 'TimedOut': | |
490 NamedMessage namedMessage = message; | |
491 Completer completer = completers.remove(namedMessage.name); | |
492 completer.complete(message); | |
493 break; | |
494 case 'ListTestsReply': | |
495 ListTestsReply reply = message; | |
496 testNamesCompleter.complete(reply.tests); | |
497 break; | |
498 case 'InternalErrorMessage': | |
499 ErrorMessage error = message; | |
500 print(error.error); | |
501 print(error.stackTrace); | |
502 throw "Internal error in helper process: ${error.error}"; | |
503 case 'TestStdoutLine': | |
504 TestStdoutLine line = message; | |
505 testOutput.putIfAbsent(line.name, () => <String>[]).add(line.line); | |
506 break; | |
507 default: | |
508 throw "Unhandled message from helper process: $message"; | |
509 } | |
510 } | |
511 | |
512 void process(StreamIterator<Message> messages) { | |
513 messages.moveNext().then((bool hasNext) { | |
514 if (hasNext) { | |
515 processMessage(messages.current); | |
516 process(messages); | |
517 } | |
518 }); | |
519 } | |
520 | |
521 void allDone() { | |
522 // This should cause the vmProcess to exit. | |
523 socket.close(); | |
524 } | |
525 } | |
OLD | NEW |