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

Side by Side Diff: pkg/analysis_server/test/integration/integration_tests.dart

Issue 398693002: Start writing integration tests for the analysis server. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 5 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
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698