OLD | NEW |
---|---|
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, 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 /** | 5 /** |
6 * A stress test for the analysis server. | 6 * A stress test for the analysis server. |
7 */ | 7 */ |
8 library analysis_server.test.stress.replay.replay; | 8 library analysis_server.test.stress.replay.replay; |
9 | 9 |
10 import 'dart:async'; | 10 import 'dart:async'; |
11 import 'dart:io'; | 11 import 'dart:io'; |
12 | 12 |
13 import 'package:analysis_server/plugin/protocol/protocol.dart'; | 13 import 'package:analysis_server/plugin/protocol/protocol.dart'; |
14 import 'package:analyzer/src/generated/error.dart' as error; | |
14 import 'package:analyzer/src/generated/java_engine.dart'; | 15 import 'package:analyzer/src/generated/java_engine.dart'; |
16 import 'package:analyzer/src/generated/scanner.dart'; | |
15 import 'package:analyzer/src/generated/source.dart'; | 17 import 'package:analyzer/src/generated/source.dart'; |
16 import 'package:analyzer/src/util/glob.dart'; | 18 import 'package:analyzer/src/util/glob.dart'; |
17 import 'package:args/args.dart'; | 19 import 'package:args/args.dart'; |
18 import 'package:path/path.dart' as path; | 20 import 'package:path/path.dart' as path; |
19 | 21 |
20 import '../utilities/git.dart'; | 22 import '../utilities/git.dart'; |
21 import '../utilities/server.dart'; | 23 import '../utilities/server.dart'; |
22 import 'operation.dart'; | 24 import 'operation.dart'; |
23 | 25 |
24 /** | 26 /** |
25 * Run the simulation based on the given command-line [arguments]. | 27 * Run the simulation based on the given command-line [arguments]. |
26 */ | 28 */ |
27 Future main(List<String> arguments) async { | 29 Future main(List<String> arguments) async { |
28 Driver driver = new Driver(); | 30 Driver driver = new Driver(); |
29 await driver.run(arguments); | 31 await driver.run(arguments); |
30 } | 32 } |
31 | 33 |
32 /** | 34 /** |
33 * The driver class that runs the simulation. | 35 * The driver class that runs the simulation. |
34 */ | 36 */ |
35 class Driver { | 37 class Driver { |
36 /** | 38 /** |
skybrian
2015/12/12 02:01:20
Any plans to switch to '///' in the analyzer?
Brian Wilkerson
2015/12/12 15:28:23
Nope. At least two of us really dislike that form
| |
39 * The value of the [OVERLAY_STYLE_OPTION_NAME] indicating that modifications | |
40 * to a file should be represented by an add overlay, followed by zero or more | |
41 * change overlays, followed by a remove overlay. | |
42 */ | |
43 static String CHANGE_OVERLAY_STYLE = 'change'; | |
44 | |
45 /** | |
37 * The name of the command-line flag that will print help text. | 46 * The name of the command-line flag that will print help text. |
38 */ | 47 */ |
39 static String HELP_FLAG_NAME = 'help'; | 48 static String HELP_FLAG_NAME = 'help'; |
40 | 49 |
41 /** | 50 /** |
51 * The value of the [OVERLAY_STYLE_OPTION_NAME] indicating that modifications | |
52 * to a file should be represented by an add overlay, followed by zero or more | |
53 * additional add overlays, followed by a remove overlay. | |
54 */ | |
55 static String MULTIPLE_ADD_OVERLAY_STYLE = 'multipleAdd'; | |
56 | |
57 /** | |
58 * The name of the command-line option used to specify the style of | |
59 * interaction to use when making `analysis.updateContent` requests. | |
60 */ | |
61 static String OVERLAY_STYLE_OPTION_NAME = 'overlay-style'; | |
62 | |
63 /** | |
42 * The name of the pubspec file. | 64 * The name of the pubspec file. |
43 */ | 65 */ |
44 static const String PUBSPEC_FILE_NAME = 'pubspec.yaml'; | 66 static const String PUBSPEC_FILE_NAME = 'pubspec.yaml'; |
45 | 67 |
46 /** | 68 /** |
47 * The name of the branch used to clean-up after making temporary changes. | 69 * The name of the branch used to clean-up after making temporary changes. |
48 */ | 70 */ |
49 static const String TEMP_BRANCH_NAME = 'temp'; | 71 static const String TEMP_BRANCH_NAME = 'temp'; |
50 | 72 |
51 /** | 73 /** |
74 * The style of interaction to use for analysis.updateContent requests. | |
75 */ | |
76 OverlayStyle overlayStyle; | |
77 | |
78 /** | |
52 * The absolute path of the repository. | 79 * The absolute path of the repository. |
53 */ | 80 */ |
54 String repositoryPath; | 81 String repositoryPath; |
55 | 82 |
56 /** | 83 /** |
57 * The absolute paths to the analysis roots. | 84 * The absolute paths to the analysis roots. |
58 */ | 85 */ |
59 List<String> analysisRoots; | 86 List<String> analysisRoots; |
60 | 87 |
61 /** | 88 /** |
(...skipping 18 matching lines...) Expand all Loading... | |
80 Statistics statistics; | 107 Statistics statistics; |
81 | 108 |
82 /** | 109 /** |
83 * Initialize a newly created driver. | 110 * Initialize a newly created driver. |
84 */ | 111 */ |
85 Driver() { | 112 Driver() { |
86 statistics = new Statistics(this); | 113 statistics = new Statistics(this); |
87 } | 114 } |
88 | 115 |
89 /** | 116 /** |
90 * Run the test based on the given command-line arguments ([args]). | 117 * Run the simulation based on the given command-line arguments ([args]). |
91 */ | 118 */ |
92 Future run(List<String> args) async { | 119 Future run(List<String> args) async { |
93 // | 120 // |
94 // Process the command-line arguments. | 121 // Process the command-line arguments. |
95 // | 122 // |
96 ArgParser parser = _createArgParser(); | 123 if (!_processCommandLine(args)) { |
97 ArgResults results; | |
98 try { | |
99 results = parser.parse(args); | |
100 } catch (exception) { | |
101 _showUsage(parser); | |
102 return null; | 124 return null; |
103 } | 125 } |
104 | |
105 if (results[HELP_FLAG_NAME]) { | |
106 _showUsage(parser); | |
107 return null; | |
108 } | |
109 | |
110 List<String> arguments = results.arguments; | |
111 if (arguments.length < 2) { | |
112 _showUsage(parser); | |
113 return null; | |
114 } | |
115 repositoryPath = path.normalize(arguments[0]); | |
116 repository = new GitRepository(repositoryPath); | |
117 | |
118 analysisRoots = arguments | |
119 .sublist(1) | |
120 .map((String analysisRoot) => path.normalize(analysisRoot)) | |
121 .toList(); | |
122 for (String analysisRoot in analysisRoots) { | |
123 if (repositoryPath != analysisRoot && | |
124 !path.isWithin(repositoryPath, analysisRoot)) { | |
125 _showUsage(parser, | |
126 'Analysis roots must be contained within the repository: $analysisRo ot'); | |
127 return null; | |
128 } | |
129 } | |
130 // | 126 // |
131 // Replay the commit history. | 127 // Simulate interactions with the server. |
132 // | 128 // |
133 Stopwatch stopwatch = new Stopwatch(); | 129 await _runSimulation(); |
134 statistics.stopwatch = stopwatch; | |
135 stopwatch.start(); | |
136 await server.start(); | |
137 server.sendServerSetSubscriptions([ServerService.STATUS]); | |
138 server.sendAnalysisSetGeneralSubscriptions( | |
139 [GeneralAnalysisService.ANALYZED_FILES]); | |
140 // TODO(brianwilkerson) Get the list of glob patterns from the server after | |
141 // an API for getting them has been implemented. | |
142 fileGlobs = <Glob>[ | |
143 new Glob(path.context.separator, '**.dart'), | |
144 new Glob(path.context.separator, '**.html'), | |
145 new Glob(path.context.separator, '**.htm'), | |
146 new Glob(path.context.separator, '**/.analysisOptions') | |
147 ]; | |
148 try { | |
149 _replayChanges(); | |
150 } finally { | |
151 server.sendServerShutdown(); | |
152 repository.checkout('master'); | |
153 } | |
154 stopwatch.stop(); | |
155 // | 130 // |
156 // Print out statistics gathered while performing the simulation. | 131 // Print out statistics gathered while performing the simulation. |
157 // | 132 // |
158 statistics.print(); | 133 statistics.print(); |
134 exit(0); | |
159 return null; | 135 return null; |
160 } | 136 } |
161 | 137 |
162 /** | 138 /** |
163 * Create and return a parser that can be used to parse the command-line | 139 * Create and return a parser that can be used to parse the command-line |
164 * arguments. | 140 * arguments. |
165 */ | 141 */ |
166 ArgParser _createArgParser() { | 142 ArgParser _createArgParser() { |
167 ArgParser parser = new ArgParser(); | 143 ArgParser parser = new ArgParser(); |
168 parser.addFlag(HELP_FLAG_NAME, | 144 parser.addFlag(HELP_FLAG_NAME, |
169 abbr: 'h', | 145 abbr: 'h', |
170 help: 'Print usage information', | 146 help: 'Print usage information', |
171 defaultsTo: false, | 147 defaultsTo: false, |
172 negatable: false); | 148 negatable: false); |
149 | |
150 parser.addOption(OVERLAY_STYLE_OPTION_NAME, | |
151 help: | |
152 'The style of interaction to use for analysis.updateContent requests ', | |
153 allowed: [CHANGE_OVERLAY_STYLE, MULTIPLE_ADD_OVERLAY_STYLE], | |
154 allowedHelp: { | |
155 CHANGE_OVERLAY_STYLE: '<add> <change>* <remove>', | |
156 MULTIPLE_ADD_OVERLAY_STYLE: '<add>+ <remove>' | |
157 }, | |
158 defaultsTo: 'change'); | |
173 return parser; | 159 return parser; |
174 } | 160 } |
175 | 161 |
162 /** | |
163 * Add source edits to the given [fileEdit] based on the given [blobDiff]. | |
164 */ | |
176 void _createSourceEdits(FileEdit fileEdit, BlobDiff blobDiff) { | 165 void _createSourceEdits(FileEdit fileEdit, BlobDiff blobDiff) { |
177 LineInfo info = fileEdit.lineInfo; | 166 LineInfo info = fileEdit.lineInfo; |
178 for (DiffHunk hunk in blobDiff.hunks) { | 167 for (DiffHunk hunk in blobDiff.hunks) { |
179 List<SourceEdit> sourceEdits = <SourceEdit>[]; | |
180 int srcStart = info.getOffsetOfLine(hunk.srcLine); | 168 int srcStart = info.getOffsetOfLine(hunk.srcLine); |
181 int srcEnd = info.getOffsetOfLine(hunk.srcLine + hunk.removeLines.length); | 169 int srcEnd = info.getOffsetOfLine(hunk.srcLine + hunk.removeLines.length); |
182 // TODO(brianwilkerson) Create multiple edits instead of a single edit. | 170 String addedText = _join(hunk.addLines); |
183 sourceEdits.add(new SourceEdit( | 171 // |
184 srcStart, srcEnd - srcStart + 1, _join(hunk.addLines))); | 172 // Create the source edits. |
173 // | |
174 List<int> breakOffsets = _getBreakOffsets(addedText); | |
175 int breakCount = breakOffsets.length; | |
176 List<SourceEdit> sourceEdits = <SourceEdit>[]; | |
177 if (breakCount == 0) { | |
178 sourceEdits | |
179 .add(new SourceEdit(srcStart, srcEnd - srcStart + 1, addedText)); | |
180 } else { | |
181 int previousOffset = breakOffsets[0]; | |
182 String string = addedText.substring(0, previousOffset); | |
183 sourceEdits | |
184 .add(new SourceEdit(srcStart, srcEnd - srcStart + 1, string)); | |
185 String reconstruction = string; | |
186 for (int i = 1; i < breakCount; i++) { | |
187 int offset = breakOffsets[i]; | |
188 string = addedText.substring(previousOffset, offset); | |
189 reconstruction += string; | |
190 sourceEdits.add(new SourceEdit(srcStart + previousOffset, 0, string)); | |
191 previousOffset = offset; | |
192 } | |
193 string = addedText.substring(previousOffset); | |
194 reconstruction += string; | |
195 sourceEdits.add(new SourceEdit(srcStart + previousOffset, 0, string)); | |
196 if (reconstruction != addedText) { | |
197 throw new AssertionError(); | |
198 } | |
199 } | |
185 fileEdit.addSourceEdits(sourceEdits); | 200 fileEdit.addSourceEdits(sourceEdits); |
186 } | 201 } |
187 } | 202 } |
188 | 203 |
189 /** | 204 /** |
190 * Return athe absolute paths of all of the pubspec files in all of the | 205 * Return the absolute paths of all of the pubspec files in all of the |
191 * analysis roots. | 206 * analysis roots. |
192 */ | 207 */ |
193 Iterable<String> _findPubspecsInAnalysisRoots() { | 208 Iterable<String> _findPubspecsInAnalysisRoots() { |
194 List<String> pubspecFiles = <String>[]; | 209 List<String> pubspecFiles = <String>[]; |
195 for (String directoryPath in analysisRoots) { | 210 for (String directoryPath in analysisRoots) { |
196 Directory directory = new Directory(directoryPath); | 211 Directory directory = new Directory(directoryPath); |
197 List<FileSystemEntity> children = | 212 List<FileSystemEntity> children = |
198 directory.listSync(recursive: true, followLinks: false); | 213 directory.listSync(recursive: true, followLinks: false); |
199 for (FileSystemEntity child in children) { | 214 for (FileSystemEntity child in children) { |
200 String filePath = child.path; | 215 String filePath = child.path; |
201 if (path.basename(filePath) == PUBSPEC_FILE_NAME) { | 216 if (path.basename(filePath) == PUBSPEC_FILE_NAME) { |
202 pubspecFiles.add(filePath); | 217 pubspecFiles.add(filePath); |
203 } | 218 } |
204 } | 219 } |
205 } | 220 } |
206 return pubspecFiles; | 221 return pubspecFiles; |
207 } | 222 } |
208 | 223 |
224 /** | |
225 * Return a list of offsets into the given [text] that represent good places | |
226 * to break the text when building edits. | |
227 */ | |
228 List<int> _getBreakOffsets(String text) { | |
skybrian
2015/12/12 02:01:20
It seems like we could simulate typing one charact
Brian Wilkerson
2015/12/12 15:28:23
We're trying to simulate how the client (editor) w
| |
229 List<int> breakOffsets = <int>[]; | |
230 Scanner scanner = new Scanner(null, new CharSequenceReader(text), | |
231 error.AnalysisErrorListener.NULL_LISTENER); | |
232 Token token = scanner.tokenize(); | |
233 // TODO(brianwilkerson) Randomize. Sometimes add zero (0) as a break point. | |
234 while (token.type != TokenType.EOF) { | |
235 // TODO(brianwilkerson) Break inside comments? | |
236 // Token comment = token.precedingComments; | |
237 int offset = token.offset; | |
238 int length = token.length; | |
239 breakOffsets.add(offset); | |
240 if (token.type == TokenType.IDENTIFIER && length > 3) { | |
241 breakOffsets.add(offset + (length ~/ 2)); | |
242 } | |
243 token = token.next; | |
244 } | |
245 return breakOffsets; | |
246 } | |
247 | |
248 /** | |
249 * Join the given [lines] into a single string. | |
250 */ | |
209 String _join(List<String> lines) { | 251 String _join(List<String> lines) { |
210 StringBuffer buffer = new StringBuffer(); | 252 StringBuffer buffer = new StringBuffer(); |
211 for (int i = 0; i < lines.length; i++) { | 253 for (int i = 0; i < lines.length; i++) { |
212 buffer.writeln(lines[i]); | 254 buffer.writeln(lines[i]); |
213 } | 255 } |
214 return buffer.toString(); | 256 return buffer.toString(); |
215 } | 257 } |
216 | 258 |
217 /** | 259 /** |
260 * Process the command-line [arguments]. Return `true` if the simulation | |
261 * should be run. | |
262 */ | |
263 bool _processCommandLine(List<String> args) { | |
264 ArgParser parser = _createArgParser(); | |
265 ArgResults results; | |
266 try { | |
267 results = parser.parse(args); | |
268 } catch (exception) { | |
269 _showUsage(parser); | |
270 return false; | |
271 } | |
272 | |
273 if (results[HELP_FLAG_NAME]) { | |
274 _showUsage(parser); | |
275 return false; | |
276 } | |
277 | |
278 String overlayStyleValue = results[OVERLAY_STYLE_OPTION_NAME]; | |
279 if (overlayStyleValue == CHANGE_OVERLAY_STYLE) { | |
280 overlayStyle = OverlayStyle.change; | |
281 } else if (overlayStyleValue == MULTIPLE_ADD_OVERLAY_STYLE) { | |
282 overlayStyle = OverlayStyle.multipleAdd; | |
283 } | |
284 | |
285 List<String> arguments = results.arguments; | |
286 if (arguments.length < 2) { | |
287 _showUsage(parser); | |
288 return false; | |
289 } | |
290 repositoryPath = path.normalize(arguments[0]); | |
291 repository = new GitRepository(repositoryPath); | |
292 | |
293 analysisRoots = arguments | |
294 .sublist(1) | |
295 .map((String analysisRoot) => path.normalize(analysisRoot)) | |
296 .toList(); | |
297 for (String analysisRoot in analysisRoots) { | |
298 if (repositoryPath != analysisRoot && | |
299 !path.isWithin(repositoryPath, analysisRoot)) { | |
300 _showUsage(parser, | |
301 'Analysis roots must be contained within the repository: $analysisRo ot'); | |
302 return false; | |
303 } | |
304 } | |
305 return true; | |
306 } | |
307 | |
308 /** | |
218 * Replay the changes in each commit. | 309 * Replay the changes in each commit. |
219 */ | 310 */ |
220 void _replayChanges() { | 311 Future _replayChanges() async { |
221 // | 312 // |
222 // Get the revision history of the repo. | 313 // Get the revision history of the repo. |
223 // | 314 // |
224 LinearCommitHistory history = repository.getCommitHistory(); | 315 LinearCommitHistory history = repository.getCommitHistory(); |
225 statistics.commitCount = history.commitIds.length; | 316 statistics.commitCount = history.commitIds.length; |
226 LinearCommitHistoryIterator iterator = history.iterator(); | 317 LinearCommitHistoryIterator iterator = history.iterator(); |
227 // | 318 // |
228 // Iterate over the history, applying changes. | 319 // Iterate over the history, applying changes. |
229 // | 320 // |
230 bool firstCheckout = true; | 321 bool firstCheckout = true; |
231 // Map<String, List<AnalysisError>> expectedErrors = null; | 322 ErrorMap expectedErrors = null; |
232 Iterable<String> changedPubspecs; | 323 Iterable<String> changedPubspecs; |
233 while (iterator.moveNext()) { | 324 while (iterator.moveNext()) { |
234 // | 325 // |
235 // Checkout the commit on which the changes are based. | 326 // Checkout the commit on which the changes are based. |
236 // | 327 // |
237 repository.checkout(iterator.srcCommit); | 328 String commit = iterator.srcCommit; |
238 // if (expectedErrors != null) { | 329 repository.checkout(commit); |
239 // await server.analysisFinished; | 330 if (expectedErrors != null) { |
240 // server.expectErrorState(expectedErrors); | 331 ErrorMap actualErrors = |
241 // } | 332 await server.computeErrorMap(server.analyzedDartFiles); |
333 String difference = expectedErrors.expectErrorMap(actualErrors); | |
334 if (difference != null) { | |
335 stdout.write('Mismatched errors after commit '); | |
336 stdout.writeln(commit); | |
337 stdout.writeln(); | |
338 stdout.writeln(difference); | |
339 return; | |
340 } | |
341 } | |
242 if (firstCheckout) { | 342 if (firstCheckout) { |
243 changedPubspecs = _findPubspecsInAnalysisRoots(); | 343 changedPubspecs = _findPubspecsInAnalysisRoots(); |
244 server.sendAnalysisSetAnalysisRoots(analysisRoots, []); | 344 server.sendAnalysisSetAnalysisRoots(analysisRoots, []); |
245 firstCheckout = false; | 345 firstCheckout = false; |
246 } else { | 346 } else { |
247 server.removeAllOverlays(); | 347 server.removeAllOverlays(); |
248 } | 348 } |
249 // await server.analysisFinished; | 349 expectedErrors = await server.computeErrorMap(server.analyzedDartFiles); |
250 // expectedErrors = server.errorMap; | |
251 for (String filePath in changedPubspecs) { | 350 for (String filePath in changedPubspecs) { |
252 _runPub(filePath); | 351 _runPub(filePath); |
253 } | 352 } |
254 // | 353 // |
255 // Apply the changes. | 354 // Apply the changes. |
256 // | 355 // |
257 CommitDelta commitDelta = iterator.next(); | 356 CommitDelta commitDelta = iterator.next(); |
258 commitDelta.filterDiffs(analysisRoots, fileGlobs); | 357 commitDelta.filterDiffs(analysisRoots, fileGlobs); |
259 if (commitDelta.hasDiffs) { | 358 if (commitDelta.hasDiffs) { |
260 statistics.commitsWithChangeInRootCount++; | 359 statistics.commitsWithChangeInRootCount++; |
261 _replayDiff(commitDelta); | 360 _replayDiff(commitDelta); |
262 } | 361 } |
263 changedPubspecs = commitDelta.filesMatching(PUBSPEC_FILE_NAME); | 362 changedPubspecs = commitDelta.filesMatching(PUBSPEC_FILE_NAME); |
363 stdout.write('.'); | |
264 } | 364 } |
265 server.removeAllOverlays(); | 365 server.removeAllOverlays(); |
366 stdout.writeln(); | |
266 } | 367 } |
267 | 368 |
369 /** | |
370 * Replay the changes between two commits, as represented by the given | |
371 * [commitDelta]. | |
372 */ | |
268 void _replayDiff(CommitDelta commitDelta) { | 373 void _replayDiff(CommitDelta commitDelta) { |
269 List<FileEdit> editList = <FileEdit>[]; | 374 List<FileEdit> editList = <FileEdit>[]; |
270 for (DiffRecord record in commitDelta.diffRecords) { | 375 for (DiffRecord record in commitDelta.diffRecords) { |
271 FileEdit edit = new FileEdit(record); | 376 FileEdit edit = new FileEdit(overlayStyle, record); |
272 _createSourceEdits(edit, record.getBlobDiff()); | 377 _createSourceEdits(edit, record.getBlobDiff()); |
273 editList.add(edit); | 378 editList.add(edit); |
274 } | 379 } |
380 // | |
275 // TODO(brianwilkerson) Randomize. | 381 // TODO(brianwilkerson) Randomize. |
276 // Randomly select operations from different files to simulate a user | 382 // Randomly select operations from different files to simulate a user |
277 // editing multiple files simultaneously. | 383 // editing multiple files simultaneously. |
384 // | |
278 for (FileEdit edit in editList) { | 385 for (FileEdit edit in editList) { |
279 List<String> currentFile = <String>[edit.filePath]; | 386 List<String> currentFile = <String>[edit.filePath]; |
280 server.sendAnalysisSetPriorityFiles(currentFile); | 387 server.sendAnalysisSetPriorityFiles(currentFile); |
281 server.sendAnalysisSetSubscriptions({ | 388 server.sendAnalysisSetSubscriptions({ |
282 AnalysisService.FOLDING: currentFile, | 389 AnalysisService.FOLDING: currentFile, |
283 AnalysisService.HIGHLIGHTS: currentFile, | 390 AnalysisService.HIGHLIGHTS: currentFile, |
284 AnalysisService.IMPLEMENTED: currentFile, | 391 AnalysisService.IMPLEMENTED: currentFile, |
285 AnalysisService.NAVIGATION: currentFile, | 392 AnalysisService.NAVIGATION: currentFile, |
286 AnalysisService.OCCURRENCES: currentFile, | 393 AnalysisService.OCCURRENCES: currentFile, |
287 AnalysisService.OUTLINE: currentFile, | 394 AnalysisService.OUTLINE: currentFile, |
(...skipping 11 matching lines...) Expand all Loading... | |
299 void _runPub(String filePath) { | 406 void _runPub(String filePath) { |
300 String directoryPath = path.dirname(filePath); | 407 String directoryPath = path.dirname(filePath); |
301 if (new Directory(directoryPath).existsSync()) { | 408 if (new Directory(directoryPath).existsSync()) { |
302 Process.runSync( | 409 Process.runSync( |
303 '/Users/brianwilkerson/Dev/dart/dart-sdk/bin/pub', ['get'], | 410 '/Users/brianwilkerson/Dev/dart/dart-sdk/bin/pub', ['get'], |
304 workingDirectory: directoryPath); | 411 workingDirectory: directoryPath); |
305 } | 412 } |
306 } | 413 } |
307 | 414 |
308 /** | 415 /** |
416 * Run the simulation by starting up a server and sending it requests. | |
417 */ | |
418 Future _runSimulation() async { | |
419 Stopwatch stopwatch = new Stopwatch(); | |
420 statistics.stopwatch = stopwatch; | |
421 stopwatch.start(); | |
422 await server.start(); | |
423 server.sendServerSetSubscriptions([ServerService.STATUS]); | |
424 server.sendAnalysisSetGeneralSubscriptions( | |
425 [GeneralAnalysisService.ANALYZED_FILES]); | |
426 // TODO(brianwilkerson) Get the list of glob patterns from the server after | |
427 // an API for getting them has been implemented. | |
428 fileGlobs = <Glob>[ | |
429 new Glob(path.context.separator, '**.dart'), | |
430 new Glob(path.context.separator, '**.html'), | |
431 new Glob(path.context.separator, '**.htm'), | |
432 new Glob(path.context.separator, '**/.analysisOptions') | |
433 ]; | |
434 try { | |
435 await _replayChanges(); | |
436 } finally { | |
437 server.sendServerShutdown(); | |
438 repository.checkout('master'); | |
439 } | |
440 stopwatch.stop(); | |
441 } | |
442 | |
443 /** | |
309 * Display usage information, preceeded by the [errorMessage] if one is given. | 444 * Display usage information, preceeded by the [errorMessage] if one is given. |
310 */ | 445 */ |
311 void _showUsage(ArgParser parser, [String errorMessage = null]) { | 446 void _showUsage(ArgParser parser, [String errorMessage = null]) { |
312 if (errorMessage != null) { | 447 if (errorMessage != null) { |
313 stderr.writeln(errorMessage); | 448 stderr.writeln(errorMessage); |
314 stderr.writeln(); | 449 stderr.writeln(); |
315 } | 450 } |
316 stderr.writeln(''' | 451 stderr.writeln(''' |
317 Usage: replay [options...] repositoryPath analysisRoot... | 452 Usage: replay [options...] repositoryPath analysisRoot... |
318 | 453 |
(...skipping 12 matching lines...) Expand all Loading... | |
331 OPTIONS:'''); | 466 OPTIONS:'''); |
332 stderr.writeln(parser.usage); | 467 stderr.writeln(parser.usage); |
333 } | 468 } |
334 } | 469 } |
335 | 470 |
336 /** | 471 /** |
337 * A representation of the edits to be applied to a single file. | 472 * A representation of the edits to be applied to a single file. |
338 */ | 473 */ |
339 class FileEdit { | 474 class FileEdit { |
340 /** | 475 /** |
476 * The style of interaction to use for analysis.updateContent requests. | |
477 */ | |
478 OverlayStyle overlayStyle; | |
479 | |
480 /** | |
341 * The absolute path of the file to be edited. | 481 * The absolute path of the file to be edited. |
342 */ | 482 */ |
343 String filePath; | 483 String filePath; |
344 | 484 |
345 /** | 485 /** |
346 * The content of the file before any edits have been applied. | 486 * The content of the file before any edits have been applied. |
347 */ | 487 */ |
348 String content; | 488 String content; |
349 | 489 |
350 /** | 490 /** |
351 * The line info for the file before any edits have been applied. | 491 * The line info for the file before any edits have been applied. |
352 */ | 492 */ |
353 LineInfo lineInfo; | 493 LineInfo lineInfo; |
354 | 494 |
355 /** | 495 /** |
356 * The lists of source edits, one list for each hunk being edited. | 496 * The lists of source edits, one list for each hunk being edited. |
357 */ | 497 */ |
358 List<List<SourceEdit>> editLists = <List<SourceEdit>>[]; | 498 List<List<SourceEdit>> editLists = <List<SourceEdit>>[]; |
359 | 499 |
360 /** | 500 /** |
501 * The current content of the file. This field is only used if the overlay | |
502 * style is [OverlayStyle.multipleAdd]. | |
503 */ | |
504 String currentContent; | |
505 | |
506 /** | |
361 * Initialize a collection of edits to be associated with the file at the | 507 * Initialize a collection of edits to be associated with the file at the |
362 * given [filePath]. | 508 * given [filePath]. |
363 */ | 509 */ |
364 FileEdit(DiffRecord record) { | 510 FileEdit(this.overlayStyle, DiffRecord record) { |
365 filePath = record.srcPath; | 511 filePath = record.srcPath; |
366 if (record.isAddition) { | 512 if (record.isAddition) { |
367 content = ''; | 513 content = ''; |
368 lineInfo = new LineInfo(<int>[0]); | 514 lineInfo = new LineInfo(<int>[0]); |
369 } else if (record.isCopy || record.isRename || record.isTypeChange) { | 515 } else if (record.isCopy || record.isRename || record.isTypeChange) { |
370 throw new ArgumentError('Unhandled change of type ${record.status}'); | 516 throw new ArgumentError('Unhandled change of type ${record.status}'); |
371 } else { | 517 } else { |
372 content = new File(filePath).readAsStringSync(); | 518 content = new File(filePath).readAsStringSync(); |
373 lineInfo = new LineInfo(StringUtilities.computeLineStarts(content)); | 519 lineInfo = new LineInfo(StringUtilities.computeLineStarts(content)); |
374 } | 520 } |
521 currentContent = content; | |
375 } | 522 } |
376 | 523 |
377 /** | 524 /** |
378 * Add a list of source edits that, taken together, transform a single hunk in | 525 * Add a list of source edits that, taken together, transform a single hunk in |
379 * the file. | 526 * the file. |
380 */ | 527 */ |
381 void addSourceEdits(List<SourceEdit> sourceEdits) { | 528 void addSourceEdits(List<SourceEdit> sourceEdits) { |
382 editLists.add(sourceEdits); | 529 editLists.add(sourceEdits); |
383 } | 530 } |
384 | 531 |
385 /** | 532 /** |
386 * Return a list of operations to be sent to the server. | 533 * Return a list of operations to be sent to the server. |
387 */ | 534 */ |
388 List<ServerOperation> getOperations() { | 535 List<ServerOperation> getOperations() { |
536 List<ServerOperation> operations = <ServerOperation>[]; | |
537 void addUpdateContent(var overlay) { | |
538 operations.add(new Analysis_UpdateContent(filePath, overlay)); | |
539 } | |
540 | |
389 // TODO(brianwilkerson) Randomize. | 541 // TODO(brianwilkerson) Randomize. |
390 // Make the order of edits random. Doing so will require updating the | 542 // Make the order of edits random. Doing so will require updating the |
391 // offsets of edits after the selected edit point. | 543 // offsets of edits after the selected edit point. |
392 List<ServerOperation> operations = <ServerOperation>[]; | 544 addUpdateContent(new AddContentOverlay(content)); |
393 operations.add( | |
394 new AnalysisUpdateContent(filePath, new AddContentOverlay(content))); | |
395 for (List<SourceEdit> editList in editLists.reversed) { | 545 for (List<SourceEdit> editList in editLists.reversed) { |
396 for (SourceEdit edit in editList.reversed) { | 546 for (SourceEdit edit in editList.reversed) { |
397 operations.add(new AnalysisUpdateContent( | 547 var overlay = null; |
398 filePath, new ChangeContentOverlay([edit]))); | 548 if (overlayStyle == OverlayStyle.change) { |
549 overlay = new ChangeContentOverlay([edit]); | |
550 } else if (overlayStyle == OverlayStyle.multipleAdd) { | |
551 currentContent = edit.apply(currentContent); | |
552 overlay = new AddContentOverlay(currentContent); | |
553 } else { | |
554 throw new StateError( | |
555 'Failed to handle overlay style = $overlayStyle'); | |
556 } | |
557 if (overlay != null) { | |
558 addUpdateContent(overlay); | |
559 } | |
399 } | 560 } |
400 } | 561 } |
401 operations | 562 addUpdateContent(new RemoveContentOverlay()); |
402 .add(new AnalysisUpdateContent(filePath, new RemoveContentOverlay())); | |
403 return operations; | 563 return operations; |
404 } | 564 } |
405 } | 565 } |
406 | 566 |
407 /** | 567 /** |
568 * The possible styles of interaction to use for analysis.updateContent requests . | |
569 */ | |
570 enum OverlayStyle { change, multipleAdd } | |
571 | |
572 /** | |
408 * A set of statistics related to the execution of the simulation. | 573 * A set of statistics related to the execution of the simulation. |
409 */ | 574 */ |
410 class Statistics { | 575 class Statistics { |
411 /** | 576 /** |
412 * The driver driving the simulation. | 577 * The driver driving the simulation. |
413 */ | 578 */ |
414 final Driver driver; | 579 final Driver driver; |
415 | 580 |
416 /** | 581 /** |
417 * The stopwatch being used to time the simulation. | 582 * The stopwatch being used to time the simulation. |
418 */ | 583 */ |
419 Stopwatch stopwatch; | 584 Stopwatch stopwatch; |
420 | 585 |
421 /** | 586 /** |
422 * The total number of commits in the repository. | 587 * The total number of commits in the repository. |
423 */ | 588 */ |
424 int commitCount; | 589 int commitCount; |
425 | 590 |
426 /** | 591 /** |
427 * The number of commits in the repository that touched one of the files in | 592 * The number of commits in the repository that touched one of the files in |
428 * one of the analysis roots. | 593 * one of the analysis roots. |
429 */ | 594 */ |
430 int commitsWithChangeInRootCount = 0; | 595 int commitsWithChangeInRootCount = 0; |
431 | 596 |
432 /** | 597 /** |
433 * Initialize a newly created set of statistics. | 598 * Initialize a newly created set of statistics. |
434 */ | 599 */ |
435 Statistics(this.driver); | 600 Statistics(this.driver); |
436 | 601 |
602 /** | |
603 * Print the statistics to [stdout]. | |
604 */ | |
437 void print() { | 605 void print() { |
438 stdout.write('Replay commits in '); | 606 stdout.write('Replay commits in '); |
439 stdout.writeln(driver.repositoryPath); | 607 stdout.writeln(driver.repositoryPath); |
440 stdout.write(' replay took '); | 608 stdout.write(' replay took '); |
441 stdout.writeln(_printTime(stopwatch.elapsedMilliseconds)); | 609 stdout.writeln(_printTime(stopwatch.elapsedMilliseconds)); |
442 stdout.write(' analysis roots = '); | 610 stdout.write(' analysis roots = '); |
443 stdout.writeln(driver.analysisRoots); | 611 stdout.writeln(driver.analysisRoots); |
444 stdout.write(' number of commits = '); | 612 stdout.write(' number of commits = '); |
445 stdout.writeln(commitCount); | 613 stdout.writeln(commitCount); |
446 stdout.write(' number of commits with a change in an analysis root = '); | 614 stdout.write(' number of commits with a change in an analysis root = '); |
447 stdout.writeln(commitsWithChangeInRootCount); | 615 stdout.writeln(commitsWithChangeInRootCount); |
448 } | 616 } |
449 | 617 |
618 /** | |
619 * Return a textual representation of the given duration, represented in | |
620 * [milliseconds]. | |
621 */ | |
450 String _printTime(int milliseconds) { | 622 String _printTime(int milliseconds) { |
451 int seconds = milliseconds ~/ 1000; | 623 int seconds = milliseconds ~/ 1000; |
452 milliseconds -= seconds * 1000; | 624 milliseconds -= seconds * 1000; |
453 int minutes = seconds ~/ 60; | 625 int minutes = seconds ~/ 60; |
454 seconds -= minutes * 60; | 626 seconds -= minutes * 60; |
455 int hours = minutes ~/ 60; | 627 int hours = minutes ~/ 60; |
456 minutes -= hours * 60; | 628 minutes -= hours * 60; |
457 | 629 |
458 if (hours > 0) { | 630 if (hours > 0) { |
459 return '$hours:$minutes:$seconds.$milliseconds'; | 631 return '$hours:$minutes:$seconds.$milliseconds'; |
460 } else if (minutes > 0) { | 632 } else if (minutes > 0) { |
461 return '$minutes:$seconds.$milliseconds m'; | 633 return '$minutes:$seconds.$milliseconds m'; |
462 } else if (seconds > 0) { | 634 } else if (seconds > 0) { |
463 return '$seconds.$milliseconds s'; | 635 return '$seconds.$milliseconds s'; |
464 } | 636 } |
465 return '$milliseconds ms'; | 637 return '$milliseconds ms'; |
466 } | 638 } |
467 } | 639 } |
OLD | NEW |