| OLD | NEW |
| (Empty) | |
| 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 |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * A stress test for the analysis server. |
| 7 */ |
| 8 library analysis_server.test.stress.replay.replay; |
| 9 |
| 10 import 'dart:async'; |
| 11 import 'dart:io'; |
| 12 |
| 13 import 'package:analysis_server/plugin/protocol/protocol.dart'; |
| 14 import 'package:analyzer/src/generated/java_engine.dart'; |
| 15 import 'package:analyzer/src/generated/source.dart'; |
| 16 import 'package:analyzer/src/util/glob.dart'; |
| 17 import 'package:args/args.dart'; |
| 18 import 'package:path/path.dart' as path; |
| 19 |
| 20 import '../utilities/git.dart'; |
| 21 import '../utilities/server.dart'; |
| 22 import 'operation.dart'; |
| 23 |
| 24 /** |
| 25 * Run the simulation based on the given command-line [arguments]. |
| 26 */ |
| 27 Future main(List<String> arguments) async { |
| 28 Driver driver = new Driver(); |
| 29 await driver.run(arguments); |
| 30 } |
| 31 |
| 32 /** |
| 33 * The driver class that runs the simulation. |
| 34 */ |
| 35 class Driver { |
| 36 /** |
| 37 * The name of the command-line flag that will print help text. |
| 38 */ |
| 39 static String HELP_FLAG_NAME = 'help'; |
| 40 |
| 41 /** |
| 42 * The name of the pubspec file. |
| 43 */ |
| 44 static const String PUBSPEC_FILE_NAME = 'pubspec.yaml'; |
| 45 |
| 46 /** |
| 47 * The name of the branch used to clean-up after making temporary changes. |
| 48 */ |
| 49 static const String TEMP_BRANCH_NAME = 'temp'; |
| 50 |
| 51 /** |
| 52 * The absolute path of the repository. |
| 53 */ |
| 54 String repositoryPath; |
| 55 |
| 56 /** |
| 57 * The absolute paths to the analysis roots. |
| 58 */ |
| 59 List<String> analysisRoots; |
| 60 |
| 61 /** |
| 62 * The git repository. |
| 63 */ |
| 64 GitRepository repository; |
| 65 |
| 66 /** |
| 67 * The connection to the analysis server. |
| 68 */ |
| 69 Server server = new Server(); |
| 70 |
| 71 /** |
| 72 * A list of the glob patterns used to identify the files being analyzed by |
| 73 * the server. |
| 74 */ |
| 75 List<Glob> fileGlobs; |
| 76 |
| 77 /** |
| 78 * An object gathering statistics about the simulation. |
| 79 */ |
| 80 Statistics statistics; |
| 81 |
| 82 /** |
| 83 * Initialize a newly created driver. |
| 84 */ |
| 85 Driver() { |
| 86 statistics = new Statistics(this); |
| 87 } |
| 88 |
| 89 /** |
| 90 * Run the test based on the given command-line arguments ([args]). |
| 91 */ |
| 92 Future run(List<String> args) async { |
| 93 // |
| 94 // Process the command-line arguments. |
| 95 // |
| 96 ArgParser parser = _createArgParser(); |
| 97 ArgResults results; |
| 98 try { |
| 99 results = parser.parse(args); |
| 100 } catch (exception) { |
| 101 _showUsage(parser); |
| 102 return null; |
| 103 } |
| 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 // |
| 131 // Replay the commit history. |
| 132 // |
| 133 Stopwatch stopwatch = new Stopwatch(); |
| 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 // |
| 156 // Print out statistics gathered while performing the simulation. |
| 157 // |
| 158 statistics.print(); |
| 159 return null; |
| 160 } |
| 161 |
| 162 /** |
| 163 * Create and return a parser that can be used to parse the command-line |
| 164 * arguments. |
| 165 */ |
| 166 ArgParser _createArgParser() { |
| 167 ArgParser parser = new ArgParser(); |
| 168 parser.addFlag(HELP_FLAG_NAME, |
| 169 abbr: 'h', |
| 170 help: 'Print usage information', |
| 171 defaultsTo: false, |
| 172 negatable: false); |
| 173 return parser; |
| 174 } |
| 175 |
| 176 void _createSourceEdits(FileEdit fileEdit, BlobDiff blobDiff) { |
| 177 LineInfo info = fileEdit.lineInfo; |
| 178 for (DiffHunk hunk in blobDiff.hunks) { |
| 179 List<SourceEdit> sourceEdits = <SourceEdit>[]; |
| 180 int srcStart = info.getOffsetOfLine(hunk.srcLine); |
| 181 int srcEnd = info.getOffsetOfLine(hunk.srcLine + hunk.removeLines.length); |
| 182 // TODO(brianwilkerson) Create multiple edits instead of a single edit. |
| 183 sourceEdits.add(new SourceEdit( |
| 184 srcStart, srcEnd - srcStart + 1, _join(hunk.addLines))); |
| 185 fileEdit.addSourceEdits(sourceEdits); |
| 186 } |
| 187 } |
| 188 |
| 189 /** |
| 190 * Return athe absolute paths of all of the pubspec files in all of the |
| 191 * analysis roots. |
| 192 */ |
| 193 Iterable<String> _findPubspecsInAnalysisRoots() { |
| 194 List<String> pubspecFiles = <String>[]; |
| 195 for (String directoryPath in analysisRoots) { |
| 196 Directory directory = new Directory(directoryPath); |
| 197 List<FileSystemEntity> children = |
| 198 directory.listSync(recursive: true, followLinks: false); |
| 199 for (FileSystemEntity child in children) { |
| 200 String filePath = child.path; |
| 201 if (path.basename(filePath) == PUBSPEC_FILE_NAME) { |
| 202 pubspecFiles.add(filePath); |
| 203 } |
| 204 } |
| 205 } |
| 206 return pubspecFiles; |
| 207 } |
| 208 |
| 209 String _join(List<String> lines) { |
| 210 StringBuffer buffer = new StringBuffer(); |
| 211 for (int i = 0; i < lines.length; i++) { |
| 212 buffer.writeln(lines[i]); |
| 213 } |
| 214 return buffer.toString(); |
| 215 } |
| 216 |
| 217 /** |
| 218 * Replay the changes in each commit. |
| 219 */ |
| 220 void _replayChanges() { |
| 221 // |
| 222 // Get the revision history of the repo. |
| 223 // |
| 224 LinearCommitHistory history = repository.getCommitHistory(); |
| 225 statistics.commitCount = history.commitIds.length; |
| 226 LinearCommitHistoryIterator iterator = history.iterator(); |
| 227 // |
| 228 // Iterate over the history, applying changes. |
| 229 // |
| 230 bool firstCheckout = true; |
| 231 // Map<String, List<AnalysisError>> expectedErrors = null; |
| 232 Iterable<String> changedPubspecs; |
| 233 while (iterator.moveNext()) { |
| 234 // |
| 235 // Checkout the commit on which the changes are based. |
| 236 // |
| 237 repository.checkout(iterator.srcCommit); |
| 238 // if (expectedErrors != null) { |
| 239 // await server.analysisFinished; |
| 240 // server.expectErrorState(expectedErrors); |
| 241 // } |
| 242 if (firstCheckout) { |
| 243 changedPubspecs = _findPubspecsInAnalysisRoots(); |
| 244 server.sendAnalysisSetAnalysisRoots(analysisRoots, []); |
| 245 firstCheckout = false; |
| 246 } else { |
| 247 server.removeAllOverlays(); |
| 248 } |
| 249 // await server.analysisFinished; |
| 250 // expectedErrors = server.errorMap; |
| 251 for (String filePath in changedPubspecs) { |
| 252 _runPub(filePath); |
| 253 } |
| 254 // |
| 255 // Apply the changes. |
| 256 // |
| 257 CommitDelta commitDelta = iterator.next(); |
| 258 commitDelta.filterDiffs(analysisRoots, fileGlobs); |
| 259 if (commitDelta.hasDiffs) { |
| 260 statistics.commitsWithChangeInRootCount++; |
| 261 _replayDiff(commitDelta); |
| 262 } |
| 263 changedPubspecs = commitDelta.filesMatching(PUBSPEC_FILE_NAME); |
| 264 } |
| 265 server.removeAllOverlays(); |
| 266 } |
| 267 |
| 268 void _replayDiff(CommitDelta commitDelta) { |
| 269 List<FileEdit> editList = <FileEdit>[]; |
| 270 for (DiffRecord record in commitDelta.diffRecords) { |
| 271 FileEdit edit = new FileEdit(record); |
| 272 _createSourceEdits(edit, record.getBlobDiff()); |
| 273 editList.add(edit); |
| 274 } |
| 275 // TODO(brianwilkerson) Randomize. |
| 276 // Randomly select operations from different files to simulate a user |
| 277 // editing multiple files simultaneously. |
| 278 for (FileEdit edit in editList) { |
| 279 List<String> currentFile = <String>[edit.filePath]; |
| 280 server.sendAnalysisSetPriorityFiles(currentFile); |
| 281 server.sendAnalysisSetSubscriptions({ |
| 282 AnalysisService.FOLDING: currentFile, |
| 283 AnalysisService.HIGHLIGHTS: currentFile, |
| 284 AnalysisService.IMPLEMENTED: currentFile, |
| 285 AnalysisService.NAVIGATION: currentFile, |
| 286 AnalysisService.OCCURRENCES: currentFile, |
| 287 AnalysisService.OUTLINE: currentFile, |
| 288 AnalysisService.OVERRIDES: currentFile |
| 289 }); |
| 290 for (ServerOperation operation in edit.getOperations()) { |
| 291 operation.perform(server); |
| 292 } |
| 293 } |
| 294 } |
| 295 |
| 296 /** |
| 297 * Run `pub` on the pubspec with the given [filePath]. |
| 298 */ |
| 299 void _runPub(String filePath) { |
| 300 String directoryPath = path.dirname(filePath); |
| 301 if (new Directory(directoryPath).existsSync()) { |
| 302 Process.runSync( |
| 303 '/Users/brianwilkerson/Dev/dart/dart-sdk/bin/pub', ['get'], |
| 304 workingDirectory: directoryPath); |
| 305 } |
| 306 } |
| 307 |
| 308 /** |
| 309 * Display usage information, preceeded by the [errorMessage] if one is given. |
| 310 */ |
| 311 void _showUsage(ArgParser parser, [String errorMessage = null]) { |
| 312 if (errorMessage != null) { |
| 313 stderr.writeln(errorMessage); |
| 314 stderr.writeln(); |
| 315 } |
| 316 stderr.writeln(''' |
| 317 Usage: replay [options...] repositoryPath analysisRoot... |
| 318 |
| 319 Uses the commit history of the git repository at the given repository path to |
| 320 simulate the development of a code base while using the analysis server to |
| 321 analyze the code base. |
| 322 |
| 323 The repository path must be the absolute path of a directory containing a git |
| 324 repository. |
| 325 |
| 326 There must be at least one analysis root, and all of the analysis roots must be |
| 327 the absolute path of a directory contained within the repository directory. The |
| 328 analysis roots represent the portion of the repository that will be analyzed by |
| 329 the analysis server. |
| 330 |
| 331 OPTIONS:'''); |
| 332 stderr.writeln(parser.usage); |
| 333 } |
| 334 } |
| 335 |
| 336 /** |
| 337 * A representation of the edits to be applied to a single file. |
| 338 */ |
| 339 class FileEdit { |
| 340 /** |
| 341 * The absolute path of the file to be edited. |
| 342 */ |
| 343 String filePath; |
| 344 |
| 345 /** |
| 346 * The content of the file before any edits have been applied. |
| 347 */ |
| 348 String content; |
| 349 |
| 350 /** |
| 351 * The line info for the file before any edits have been applied. |
| 352 */ |
| 353 LineInfo lineInfo; |
| 354 |
| 355 /** |
| 356 * The lists of source edits, one list for each hunk being edited. |
| 357 */ |
| 358 List<List<SourceEdit>> editLists = <List<SourceEdit>>[]; |
| 359 |
| 360 /** |
| 361 * Initialize a collection of edits to be associated with the file at the |
| 362 * given [filePath]. |
| 363 */ |
| 364 FileEdit(DiffRecord record) { |
| 365 filePath = record.srcPath; |
| 366 if (record.isAddition) { |
| 367 content = ''; |
| 368 lineInfo = new LineInfo(<int>[0]); |
| 369 } else if (record.isCopy || record.isRename || record.isTypeChange) { |
| 370 throw new ArgumentError('Unhandled change of type ${record.status}'); |
| 371 } else { |
| 372 content = new File(filePath).readAsStringSync(); |
| 373 lineInfo = new LineInfo(StringUtilities.computeLineStarts(content)); |
| 374 } |
| 375 } |
| 376 |
| 377 /** |
| 378 * Add a list of source edits that, taken together, transform a single hunk in |
| 379 * the file. |
| 380 */ |
| 381 void addSourceEdits(List<SourceEdit> sourceEdits) { |
| 382 editLists.add(sourceEdits); |
| 383 } |
| 384 |
| 385 /** |
| 386 * Return a list of operations to be sent to the server. |
| 387 */ |
| 388 List<ServerOperation> getOperations() { |
| 389 // TODO(brianwilkerson) Randomize. |
| 390 // Make the order of edits random. Doing so will require updating the |
| 391 // offsets of edits after the selected edit point. |
| 392 List<ServerOperation> operations = <ServerOperation>[]; |
| 393 operations.add( |
| 394 new AnalysisUpdateContent(filePath, new AddContentOverlay(content))); |
| 395 for (List<SourceEdit> editList in editLists.reversed) { |
| 396 for (SourceEdit edit in editList.reversed) { |
| 397 operations.add(new AnalysisUpdateContent( |
| 398 filePath, new ChangeContentOverlay([edit]))); |
| 399 } |
| 400 } |
| 401 operations |
| 402 .add(new AnalysisUpdateContent(filePath, new RemoveContentOverlay())); |
| 403 return operations; |
| 404 } |
| 405 } |
| 406 |
| 407 /** |
| 408 * A set of statistics related to the execution of the simulation. |
| 409 */ |
| 410 class Statistics { |
| 411 /** |
| 412 * The driver driving the simulation. |
| 413 */ |
| 414 final Driver driver; |
| 415 |
| 416 /** |
| 417 * The stopwatch being used to time the simulation. |
| 418 */ |
| 419 Stopwatch stopwatch; |
| 420 |
| 421 /** |
| 422 * The total number of commits in the repository. |
| 423 */ |
| 424 int commitCount; |
| 425 |
| 426 /** |
| 427 * The number of commits in the repository that touched one of the files in |
| 428 * one of the analysis roots. |
| 429 */ |
| 430 int commitsWithChangeInRootCount = 0; |
| 431 |
| 432 /** |
| 433 * Initialize a newly created set of statistics. |
| 434 */ |
| 435 Statistics(this.driver); |
| 436 |
| 437 void print() { |
| 438 stdout.write('Replay commits in '); |
| 439 stdout.writeln(driver.repositoryPath); |
| 440 stdout.write(' replay took '); |
| 441 stdout.writeln(_printTime(stopwatch.elapsedMilliseconds)); |
| 442 stdout.write(' analysis roots = '); |
| 443 stdout.writeln(driver.analysisRoots); |
| 444 stdout.write(' number of commits = '); |
| 445 stdout.writeln(commitCount); |
| 446 stdout.write(' number of commits with a change in an analysis root = '); |
| 447 stdout.writeln(commitsWithChangeInRootCount); |
| 448 } |
| 449 |
| 450 String _printTime(int milliseconds) { |
| 451 int seconds = milliseconds ~/ 1000; |
| 452 milliseconds -= seconds * 1000; |
| 453 int minutes = seconds ~/ 60; |
| 454 seconds -= minutes * 60; |
| 455 int hours = minutes ~/ 60; |
| 456 minutes -= hours * 60; |
| 457 |
| 458 if (hours > 0) { |
| 459 return '$hours:$minutes:$seconds.$milliseconds'; |
| 460 } else if (minutes > 0) { |
| 461 return '$minutes:$seconds.$milliseconds m'; |
| 462 } else if (seconds > 0) { |
| 463 return '$seconds.$milliseconds s'; |
| 464 } |
| 465 return '$milliseconds ms'; |
| 466 } |
| 467 } |
| OLD | NEW |