| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2014, the Dart 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 file. |
| 4 |
| 5 library test.integration.analysis; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:collection'; |
| 9 import 'dart:convert'; |
| 10 import 'dart:io'; |
| 11 |
| 12 import 'package:path/path.dart'; |
| 13 import 'package:unittest/unittest.dart'; |
| 14 |
| 15 /** |
| 16 * Base class for analysis server integration tests. |
| 17 */ |
| 18 abstract class AbstractAnalysisServerIntegrationTest { |
| 19 /** |
| 20 * Connection to the analysis server. |
| 21 */ |
| 22 Server server; |
| 23 |
| 24 /** |
| 25 * Temporary directory in which source files can be stored. |
| 26 */ |
| 27 Directory sourceDirectory; |
| 28 |
| 29 /** |
| 30 * Map from file path to the list of analysis errors which have most recently |
| 31 * been received for the file. |
| 32 */ |
| 33 HashMap<String, dynamic> currentAnalysisErrors = new HashMap<String, dynamic>( |
| 34 ); |
| 35 |
| 36 /** |
| 37 * Create a source file with the given contents. [relativePath] |
| 38 * is relative to [sourceDirectory]; on Windows any forward slashes it |
| 39 * contains are converted to backslashes. |
| 40 */ |
| 41 void createFile(String relativePath, String contents) { |
| 42 String absolutePath = normalizePath(relativePath); |
| 43 new Directory(dirname(absolutePath)).createSync(recursive: true); |
| 44 new File(absolutePath).writeAsStringSync(contents); |
| 45 } |
| 46 |
| 47 /** |
| 48 * Convert the given [relativePath] to an absolute path, by interpreting it |
| 49 * relative to [sourceDirectory]. On Windows any forward slashes in |
| 50 * [relativePath] are converted to backslashes. |
| 51 */ |
| 52 String normalizePath(String relativePath) { |
| 53 return join(sourceDirectory.path, relativePath.replaceAll('/', separator)); |
| 54 } |
| 55 |
| 56 /** |
| 57 * Send the server an 'analysis.setAnalysisRoots' command. |
| 58 */ |
| 59 Future setAnalysisRoots(List<String> relativeRoots) { |
| 60 return server.send('analysis.setAnalysisRoots', { |
| 61 'included': relativeRoots.map(normalizePath).toList(), |
| 62 'excluded': [] |
| 63 }); |
| 64 } |
| 65 |
| 66 /** |
| 67 * Return a future which will complete when a 'server.status' notification is |
| 68 * received from the server with 'analyzing' set to false. |
| 69 * |
| 70 * The future will only be completed by 'server.status' notifications that are |
| 71 * received after this function call. So it is safe to use this getter |
| 72 * multiple times in one test; each time it is used it will wait afresh for |
| 73 * analysis to finish. |
| 74 */ |
| 75 Future get analysisFinished { |
| 76 Completer completer = new Completer(); |
| 77 StreamSubscription subscription; |
| 78 subscription = server.onNotification('server.status').listen((params) { |
| 79 if (!params['analysis']['analyzing']) { |
| 80 completer.complete(params); |
| 81 subscription.cancel(); |
| 82 } |
| 83 }); |
| 84 return completer.future; |
| 85 } |
| 86 |
| 87 /** |
| 88 * Print out any messages exchanged with the server. If some messages have |
| 89 * already been exchanged with the server, they are printed out immediately. |
| 90 */ |
| 91 void debugStdio() { |
| 92 server.debugStdio(); |
| 93 } |
| 94 |
| 95 /** |
| 96 * The server is automatically started before every test, and a temporary |
| 97 * [sourceDirectory] is created. |
| 98 */ |
| 99 Future setUp() { |
| 100 sourceDirectory = Directory.systemTemp.createTempSync('analysisServer'); |
| 101 return Server.start().then((Server server) { |
| 102 this.server = server; |
| 103 server.onNotification('analysis.errors').listen((params) { |
| 104 expect(params, isMap); |
| 105 expect(params['file'], isString); |
| 106 currentAnalysisErrors[params['file']] = params['errors']; |
| 107 }); |
| 108 }); |
| 109 } |
| 110 |
| 111 /** |
| 112 * After every test, the server stopped and [sourceDirectory] is deleted. |
| 113 */ |
| 114 Future tearDown() { |
| 115 return server.kill().then((_) { |
| 116 sourceDirectory.deleteSync(recursive: true); |
| 117 }); |
| 118 } |
| 119 } |
| 120 |
| 121 // Matchers for data types defined in the analysis server API |
| 122 // ========================================================== |
| 123 // TODO(paulberry): add more matchers. |
| 124 |
| 125 const Matcher isString = const isInstanceOf<String>('String'); |
| 126 |
| 127 const Matcher isInt = const isInstanceOf<int>('Int'); |
| 128 |
| 129 const Matcher isResultResponse = const MatchesJsonObject('result response', cons
t { |
| 130 'id': isString |
| 131 }, optionalFields: const { |
| 132 'result': anything |
| 133 }); |
| 134 |
| 135 const Matcher isError = const MatchesJsonObject('Error', const { |
| 136 // TODO(paulberry): once we decide what the set of permitted error codes are, |
| 137 // add validation for 'code'. |
| 138 'code': anything, |
| 139 'message': isString, |
| 140 'data': anything |
| 141 }); |
| 142 |
| 143 const Matcher isErrorResponse = const MatchesJsonObject('error response', const
{ |
| 144 'id': isString, |
| 145 'error': isError |
| 146 }); |
| 147 |
| 148 const Matcher isNotification = const MatchesJsonObject('notification', const { |
| 149 'event': isString |
| 150 }, optionalFields: const { |
| 151 'params': isMap |
| 152 }); |
| 153 |
| 154 final Matcher isErrorSeverity = isIn(['INFO', 'WARNING', 'ERROR']); |
| 155 |
| 156 final Matcher isErrorType = isIn(['COMPILE_TIME_ERROR', 'HINT', |
| 157 'STATIC_TYPE_WARNING', 'STATIC_WARNING', 'SYNTACTIC_ERROR', 'TODO']); |
| 158 |
| 159 const Matcher isLocation = const MatchesJsonObject('Location', const { |
| 160 'file': isString, |
| 161 'offset': isInt, |
| 162 'length': isInt, |
| 163 'startLine': isInt, |
| 164 'startColumn': isInt |
| 165 }); |
| 166 |
| 167 final Matcher isAnalysisError = new MatchesJsonObject('AnalysisError', { |
| 168 'severity': isErrorSeverity, |
| 169 'type': isErrorType, |
| 170 'location': isLocation, |
| 171 'message': isString, |
| 172 }, optionalFields: { |
| 173 'correction': isString, |
| 174 // TODO(paulberry): remove 'errorCode' once server stops sending it |
| 175 'errorCode': anything |
| 176 }); |
| 177 |
| 178 |
| 179 /** |
| 180 * Type of closures used by MatchesJsonObject to record field mismatches. |
| 181 */ |
| 182 typedef Description MismatchDescriber(Description mismatchDescription, bool |
| 183 verbose); |
| 184 |
| 185 /** |
| 186 * Matcher that matches a JSON object, with a given set of required and |
| 187 * optional fields, and their associated types (expressed as [Matcher]s). |
| 188 */ |
| 189 class MatchesJsonObject extends Matcher { |
| 190 /** |
| 191 * Short description of the expected type. |
| 192 */ |
| 193 final String description; |
| 194 |
| 195 /** |
| 196 * Fields that are required to be in the JSON object, and [Matcher]s describin
g |
| 197 * their expected types. |
| 198 */ |
| 199 final Map<String, Matcher> requiredFields; |
| 200 |
| 201 /** |
| 202 * Fields that are optional in the JSON object, and [Matcher]s describing |
| 203 * their expected types. |
| 204 */ |
| 205 final Map<String, Matcher> optionalFields; |
| 206 |
| 207 const |
| 208 MatchesJsonObject(this.description, this.requiredFields, {this.optionalFie
lds}); |
| 209 |
| 210 @override |
| 211 bool matches(item, Map matchState) { |
| 212 if (item is! Map) { |
| 213 return false; |
| 214 } |
| 215 List<MismatchDescriber> mismatches = <MismatchDescriber>[]; |
| 216 if (requiredFields != null) { |
| 217 requiredFields.forEach((String key, Matcher valueMatcher) { |
| 218 if (!item.containsKey(key)) { |
| 219 mismatches.add((Description mismatchDescription, bool verbose) => |
| 220 mismatchDescription.add('is missing field ').addDescriptionOf(key)
.add(' (' |
| 221 ).addDescriptionOf(valueMatcher).add(')')); |
| 222 } else { |
| 223 _checkField(key, item[key], valueMatcher, mismatches); |
| 224 } |
| 225 }); |
| 226 } |
| 227 item.forEach((key, value) { |
| 228 if (requiredFields != null && requiredFields.containsKey(key)) { |
| 229 // Already checked this field |
| 230 } else if (optionalFields != null && optionalFields.containsKey(key)) { |
| 231 _checkField(key, value, optionalFields[key], mismatches); |
| 232 } else { |
| 233 mismatches.add((Description mismatchDescription, bool verbose) => |
| 234 mismatchDescription.add('has unexpected field ').addDescriptionOf(ke
y)); |
| 235 } |
| 236 }); |
| 237 if (mismatches.isEmpty) { |
| 238 return true; |
| 239 } else { |
| 240 addStateInfo(matchState, { |
| 241 'mismatches': mismatches |
| 242 }); |
| 243 return false; |
| 244 } |
| 245 } |
| 246 |
| 247 @override |
| 248 Description describe(Description description) => description.add( |
| 249 this.description); |
| 250 |
| 251 @override |
| 252 Description describeMismatch(item, Description mismatchDescription, Map |
| 253 matchState, bool verbose) { |
| 254 List<MismatchDescriber> mismatches = matchState['mismatches']; |
| 255 if (mismatches != null) { |
| 256 for (int i = 0; i < mismatches.length; i++) { |
| 257 MismatchDescriber mismatch = mismatches[i]; |
| 258 if (i > 0) { |
| 259 if (mismatches.length == 2) { |
| 260 mismatchDescription = mismatchDescription.add(' and '); |
| 261 } else if (i == mismatches.length - 1) { |
| 262 mismatchDescription = mismatchDescription.add(', and '); |
| 263 } else { |
| 264 mismatchDescription = mismatchDescription.add(', '); |
| 265 } |
| 266 } |
| 267 mismatchDescription = mismatch(mismatchDescription, verbose); |
| 268 } |
| 269 return mismatchDescription; |
| 270 } else { |
| 271 return super.describeMismatch(item, mismatchDescription, matchState, |
| 272 verbose); |
| 273 } |
| 274 } |
| 275 |
| 276 /** |
| 277 * Check the type of a field called [key], having value [value], using |
| 278 * [valueMatcher]. If it doesn't match, record a closure in [mismatches] |
| 279 * which can describe the mismatch. |
| 280 */ |
| 281 void _checkField(String key, value, Matcher |
| 282 valueMatcher, List<MismatchDescriber> mismatches) { |
| 283 Map subState = {}; |
| 284 if (!valueMatcher.matches(value, subState)) { |
| 285 mismatches.add((Description mismatchDescription, bool verbose) { |
| 286 mismatchDescription = mismatchDescription.add( |
| 287 'contains malformed field ').addDescriptionOf(key).add(' (should be
' |
| 288 ).addDescriptionOf(valueMatcher).add('; '); |
| 289 mismatchDescription = valueMatcher.describeMismatch(value, |
| 290 mismatchDescription, subState, verbose); |
| 291 return mismatchDescription.add(')'); |
| 292 }); |
| 293 } |
| 294 } |
| 295 } |
| 296 |
| 297 /** |
| 298 * Instances of the class [Server] manage a connection to a server process, and |
| 299 * facilitate communication to and from the server. |
| 300 */ |
| 301 class Server { |
| 302 /** |
| 303 * Server process object. |
| 304 */ |
| 305 Process _process; |
| 306 |
| 307 /** |
| 308 * Commands that have been sent to the server but not yet acknowledged, and |
| 309 * the [Completer] objects which should be completed when acknowledgement is |
| 310 * received. |
| 311 */ |
| 312 final HashMap<String, Completer> _pendingCommands = <String, Completer> {}; |
| 313 |
| 314 /** |
| 315 * Number which should be used to compute the 'id' to send in the next command |
| 316 * sent to the server. |
| 317 */ |
| 318 int _nextId = 0; |
| 319 |
| 320 /** |
| 321 * [StreamController]s to which notifications should be sent, organized by |
| 322 * event type. |
| 323 */ |
| 324 final HashMap<String, StreamController> _notificationControllers = |
| 325 new HashMap<String, StreamController>(); |
| 326 |
| 327 /** |
| 328 * [Stream]s associated with the controllers in [_notificationControllers], |
| 329 * but converted to broadcast streams. |
| 330 */ |
| 331 final HashMap<String, Stream> _notificationStreams = new HashMap<String, |
| 332 Stream>(); |
| 333 |
| 334 /** |
| 335 * Messages which have been exchanged with the server; we buffer these |
| 336 * up until the test finishes, so that they can be examined in the debugger |
| 337 * or printed out in response to a call to [debugStdio]. |
| 338 */ |
| 339 final List<String> _recordedStdio = <String>[]; |
| 340 |
| 341 /** |
| 342 * True if we are currently printing out messages exchanged with the server. |
| 343 */ |
| 344 bool _debuggingStdio = false; |
| 345 |
| 346 Server._(this._process); |
| 347 |
| 348 /** |
| 349 * Get a stream which will receive notifications of the given event type. |
| 350 * The values delivered to the stream will be the contents of the 'params' |
| 351 * field of the notification message. |
| 352 */ |
| 353 Stream onNotification(String event) { |
| 354 Stream notificationStream = _notificationStreams[event]; |
| 355 if (notificationStream == null) { |
| 356 StreamController notificationController = new StreamController(); |
| 357 _notificationControllers[event] = notificationController; |
| 358 notificationStream = notificationController.stream.asBroadcastStream(); |
| 359 _notificationStreams[event] = notificationStream; |
| 360 } |
| 361 return notificationStream; |
| 362 } |
| 363 |
| 364 /** |
| 365 * Start the server. If [debugServer] is true, the server will be started |
| 366 * with "--debug", allowing a debugger to be attached. |
| 367 */ |
| 368 static Future<Server> start({bool debugServer: false}) { |
| 369 String dartBinary = Platform.executable; |
| 370 String serverPath = normalize(join(dirname(Platform.script.path), '..', |
| 371 '..', 'bin', 'server.dart')); |
| 372 List<String> arguments = []; |
| 373 if (debugServer) { |
| 374 arguments.add('--debug'); |
| 375 } |
| 376 arguments.add(serverPath); |
| 377 return Process.start(dartBinary, arguments).then((Process process) { |
| 378 Server server = new Server._(process); |
| 379 process.stdout.transform((new Utf8Codec()).decoder).transform( |
| 380 new LineSplitter()).listen((String line) { |
| 381 String trimmedLine = line.trim(); |
| 382 server._recordStdio('RECV: $trimmedLine'); |
| 383 var message = JSON.decoder.convert(trimmedLine); |
| 384 expect(message, isMap); |
| 385 Map messageAsMap = message; |
| 386 if (messageAsMap.containsKey('id')) { |
| 387 expect(messageAsMap['id'], isString); |
| 388 String id = message['id']; |
| 389 Completer completer = server._pendingCommands[id]; |
| 390 if (completer == null) { |
| 391 fail('Unexpected response from server: id=$id'); |
| 392 } else { |
| 393 server._pendingCommands.remove(id); |
| 394 } |
| 395 if (messageAsMap.containsKey('error')) { |
| 396 // TODO(paulberry): propagate the error info to the completer. |
| 397 completer.completeError(null); |
| 398 // Check that the message is well-formed. We do this after calling |
| 399 // completer.completeError() so that we don't stall the test in the |
| 400 // event of an error. |
| 401 expect(message, isErrorResponse); |
| 402 } else { |
| 403 completer.complete(messageAsMap['result']); |
| 404 // Check that the message is well-formed. We do this after calling |
| 405 // completer.complete() so that we don't stall the test in the |
| 406 // event of an error. |
| 407 expect(message, isResultResponse); |
| 408 } |
| 409 } else { |
| 410 // Message is a notification. It should have an event and possibly |
| 411 // params. |
| 412 expect(messageAsMap, contains('event')); |
| 413 expect(messageAsMap['event'], isString); |
| 414 String event = messageAsMap['event']; |
| 415 StreamController notificationController = |
| 416 server._notificationControllers[event]; |
| 417 if (notificationController != null) { |
| 418 notificationController.add(messageAsMap['params']); |
| 419 } |
| 420 // Check that the message is well-formed. We do this after calling |
| 421 // notificationController.add() so that we don't stall the test in the |
| 422 // event of an error. |
| 423 expect(message, isNotification); |
| 424 } |
| 425 }); |
| 426 process.stderr.listen((List<int> data) { |
| 427 fail('Unexpected output from stderr'); |
| 428 }); |
| 429 return server; |
| 430 }); |
| 431 } |
| 432 |
| 433 /** |
| 434 * Stop the server. |
| 435 */ |
| 436 Future kill() { |
| 437 _process.kill(); |
| 438 return _process.exitCode; |
| 439 } |
| 440 |
| 441 /** |
| 442 * Send a command to the server. An 'id' will be automatically assigned. |
| 443 * The returned [Future] will be completed when the server acknowledges the |
| 444 * command with a response. If the server acknowledges the command with a |
| 445 * normal (non-error) response, the future will be completed with the 'result' |
| 446 * field from the response. If the server acknowledges the command with an |
| 447 * error response, the future will be completed with an error. |
| 448 */ |
| 449 Future send(String method, Map<String, dynamic> params) { |
| 450 String id = '${_nextId++}'; |
| 451 Map<String, dynamic> command = <String, dynamic> { |
| 452 'id': id, |
| 453 'method': method |
| 454 }; |
| 455 if (params != null) { |
| 456 command['params'] = params; |
| 457 } |
| 458 Completer completer = new Completer(); |
| 459 _pendingCommands[id] = completer; |
| 460 String line = JSON.encode(command); |
| 461 _recordStdio('SEND: $line'); |
| 462 _process.stdin.add(UTF8.encoder.convert("${line}\n")); |
| 463 return completer.future; |
| 464 } |
| 465 |
| 466 /** |
| 467 * Print out any messages exchanged with the server. If some messages have |
| 468 * already been exchanged with the server, they are printed out immediately. |
| 469 */ |
| 470 void debugStdio() { |
| 471 if (_debuggingStdio) { |
| 472 return; |
| 473 } |
| 474 _debuggingStdio = true; |
| 475 for (String line in _recordedStdio) { |
| 476 print(line); |
| 477 } |
| 478 } |
| 479 |
| 480 /** |
| 481 * Record a message that was exchanged with the server, and print it out if |
| 482 * [debugStdio] has been called. |
| 483 */ |
| 484 void _recordStdio(String line) { |
| 485 if (_debuggingStdio) { |
| 486 print(line); |
| 487 } |
| 488 _recordedStdio.add(line); |
| 489 } |
| 490 } |
| OLD | NEW |