Chromium Code Reviews| Index: pkg/analysis_server/test/stress/replay/replay.dart |
| diff --git a/pkg/analysis_server/test/stress/replay/replay.dart b/pkg/analysis_server/test/stress/replay/replay.dart |
| index 37de9114da57fad6d84c2a367d62ad81879a35b2..159944f92395a33cd5c3a3c6f318363c377bab71 100644 |
| --- a/pkg/analysis_server/test/stress/replay/replay.dart |
| +++ b/pkg/analysis_server/test/stress/replay/replay.dart |
| @@ -11,7 +11,9 @@ import 'dart:async'; |
| import 'dart:io'; |
| import 'package:analysis_server/plugin/protocol/protocol.dart'; |
| +import 'package:analyzer/src/generated/error.dart' as error; |
| import 'package:analyzer/src/generated/java_engine.dart'; |
| +import 'package:analyzer/src/generated/scanner.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/util/glob.dart'; |
| import 'package:args/args.dart'; |
| @@ -34,11 +36,31 @@ Future main(List<String> arguments) async { |
| */ |
| class Driver { |
| /** |
|
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
|
| + * The value of the [OVERLAY_STYLE_OPTION_NAME] indicating that modifications |
| + * to a file should be represented by an add overlay, followed by zero or more |
| + * change overlays, followed by a remove overlay. |
| + */ |
| + static String CHANGE_OVERLAY_STYLE = 'change'; |
| + |
| + /** |
| * The name of the command-line flag that will print help text. |
| */ |
| static String HELP_FLAG_NAME = 'help'; |
| /** |
| + * The value of the [OVERLAY_STYLE_OPTION_NAME] indicating that modifications |
| + * to a file should be represented by an add overlay, followed by zero or more |
| + * additional add overlays, followed by a remove overlay. |
| + */ |
| + static String MULTIPLE_ADD_OVERLAY_STYLE = 'multipleAdd'; |
| + |
| + /** |
| + * The name of the command-line option used to specify the style of |
| + * interaction to use when making `analysis.updateContent` requests. |
| + */ |
| + static String OVERLAY_STYLE_OPTION_NAME = 'overlay-style'; |
| + |
| + /** |
| * The name of the pubspec file. |
| */ |
| static const String PUBSPEC_FILE_NAME = 'pubspec.yaml'; |
| @@ -49,6 +71,11 @@ class Driver { |
| static const String TEMP_BRANCH_NAME = 'temp'; |
| /** |
| + * The style of interaction to use for analysis.updateContent requests. |
| + */ |
| + OverlayStyle overlayStyle; |
| + |
| + /** |
| * The absolute path of the repository. |
| */ |
| String repositoryPath; |
| @@ -87,75 +114,24 @@ class Driver { |
| } |
| /** |
| - * Run the test based on the given command-line arguments ([args]). |
| + * Run the simulation based on the given command-line arguments ([args]). |
| */ |
| Future run(List<String> args) async { |
| // |
| // Process the command-line arguments. |
| // |
| - ArgParser parser = _createArgParser(); |
| - ArgResults results; |
| - try { |
| - results = parser.parse(args); |
| - } catch (exception) { |
| - _showUsage(parser); |
| + if (!_processCommandLine(args)) { |
| return null; |
| } |
| - |
| - if (results[HELP_FLAG_NAME]) { |
| - _showUsage(parser); |
| - return null; |
| - } |
| - |
| - List<String> arguments = results.arguments; |
| - if (arguments.length < 2) { |
| - _showUsage(parser); |
| - return null; |
| - } |
| - repositoryPath = path.normalize(arguments[0]); |
| - repository = new GitRepository(repositoryPath); |
| - |
| - analysisRoots = arguments |
| - .sublist(1) |
| - .map((String analysisRoot) => path.normalize(analysisRoot)) |
| - .toList(); |
| - for (String analysisRoot in analysisRoots) { |
| - if (repositoryPath != analysisRoot && |
| - !path.isWithin(repositoryPath, analysisRoot)) { |
| - _showUsage(parser, |
| - 'Analysis roots must be contained within the repository: $analysisRoot'); |
| - return null; |
| - } |
| - } |
| // |
| - // Replay the commit history. |
| + // Simulate interactions with the server. |
| // |
| - Stopwatch stopwatch = new Stopwatch(); |
| - statistics.stopwatch = stopwatch; |
| - stopwatch.start(); |
| - await server.start(); |
| - server.sendServerSetSubscriptions([ServerService.STATUS]); |
| - server.sendAnalysisSetGeneralSubscriptions( |
| - [GeneralAnalysisService.ANALYZED_FILES]); |
| - // TODO(brianwilkerson) Get the list of glob patterns from the server after |
| - // an API for getting them has been implemented. |
| - fileGlobs = <Glob>[ |
| - new Glob(path.context.separator, '**.dart'), |
| - new Glob(path.context.separator, '**.html'), |
| - new Glob(path.context.separator, '**.htm'), |
| - new Glob(path.context.separator, '**/.analysisOptions') |
| - ]; |
| - try { |
| - _replayChanges(); |
| - } finally { |
| - server.sendServerShutdown(); |
| - repository.checkout('master'); |
| - } |
| - stopwatch.stop(); |
| + await _runSimulation(); |
| // |
| // Print out statistics gathered while performing the simulation. |
| // |
| statistics.print(); |
| + exit(0); |
| return null; |
| } |
| @@ -170,24 +146,63 @@ class Driver { |
| help: 'Print usage information', |
| defaultsTo: false, |
| negatable: false); |
| + |
| + parser.addOption(OVERLAY_STYLE_OPTION_NAME, |
| + help: |
| + 'The style of interaction to use for analysis.updateContent requests', |
| + allowed: [CHANGE_OVERLAY_STYLE, MULTIPLE_ADD_OVERLAY_STYLE], |
| + allowedHelp: { |
| + CHANGE_OVERLAY_STYLE: '<add> <change>* <remove>', |
| + MULTIPLE_ADD_OVERLAY_STYLE: '<add>+ <remove>' |
| + }, |
| + defaultsTo: 'change'); |
| return parser; |
| } |
| + /** |
| + * Add source edits to the given [fileEdit] based on the given [blobDiff]. |
| + */ |
| void _createSourceEdits(FileEdit fileEdit, BlobDiff blobDiff) { |
| LineInfo info = fileEdit.lineInfo; |
| for (DiffHunk hunk in blobDiff.hunks) { |
| - List<SourceEdit> sourceEdits = <SourceEdit>[]; |
| int srcStart = info.getOffsetOfLine(hunk.srcLine); |
| int srcEnd = info.getOffsetOfLine(hunk.srcLine + hunk.removeLines.length); |
| - // TODO(brianwilkerson) Create multiple edits instead of a single edit. |
| - sourceEdits.add(new SourceEdit( |
| - srcStart, srcEnd - srcStart + 1, _join(hunk.addLines))); |
| + String addedText = _join(hunk.addLines); |
| + // |
| + // Create the source edits. |
| + // |
| + List<int> breakOffsets = _getBreakOffsets(addedText); |
| + int breakCount = breakOffsets.length; |
| + List<SourceEdit> sourceEdits = <SourceEdit>[]; |
| + if (breakCount == 0) { |
| + sourceEdits |
| + .add(new SourceEdit(srcStart, srcEnd - srcStart + 1, addedText)); |
| + } else { |
| + int previousOffset = breakOffsets[0]; |
| + String string = addedText.substring(0, previousOffset); |
| + sourceEdits |
| + .add(new SourceEdit(srcStart, srcEnd - srcStart + 1, string)); |
| + String reconstruction = string; |
| + for (int i = 1; i < breakCount; i++) { |
| + int offset = breakOffsets[i]; |
| + string = addedText.substring(previousOffset, offset); |
| + reconstruction += string; |
| + sourceEdits.add(new SourceEdit(srcStart + previousOffset, 0, string)); |
| + previousOffset = offset; |
| + } |
| + string = addedText.substring(previousOffset); |
| + reconstruction += string; |
| + sourceEdits.add(new SourceEdit(srcStart + previousOffset, 0, string)); |
| + if (reconstruction != addedText) { |
| + throw new AssertionError(); |
| + } |
| + } |
| fileEdit.addSourceEdits(sourceEdits); |
| } |
| } |
| /** |
| - * Return athe absolute paths of all of the pubspec files in all of the |
| + * Return the absolute paths of all of the pubspec files in all of the |
| * analysis roots. |
| */ |
| Iterable<String> _findPubspecsInAnalysisRoots() { |
| @@ -206,6 +221,33 @@ class Driver { |
| return pubspecFiles; |
| } |
| + /** |
| + * Return a list of offsets into the given [text] that represent good places |
| + * to break the text when building edits. |
| + */ |
| + 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
|
| + List<int> breakOffsets = <int>[]; |
| + Scanner scanner = new Scanner(null, new CharSequenceReader(text), |
| + error.AnalysisErrorListener.NULL_LISTENER); |
| + Token token = scanner.tokenize(); |
| + // TODO(brianwilkerson) Randomize. Sometimes add zero (0) as a break point. |
| + while (token.type != TokenType.EOF) { |
| + // TODO(brianwilkerson) Break inside comments? |
| +// Token comment = token.precedingComments; |
| + int offset = token.offset; |
| + int length = token.length; |
| + breakOffsets.add(offset); |
| + if (token.type == TokenType.IDENTIFIER && length > 3) { |
| + breakOffsets.add(offset + (length ~/ 2)); |
| + } |
| + token = token.next; |
| + } |
| + return breakOffsets; |
| + } |
| + |
| + /** |
| + * Join the given [lines] into a single string. |
| + */ |
| String _join(List<String> lines) { |
| StringBuffer buffer = new StringBuffer(); |
| for (int i = 0; i < lines.length; i++) { |
| @@ -215,9 +257,58 @@ class Driver { |
| } |
| /** |
| + * Process the command-line [arguments]. Return `true` if the simulation |
| + * should be run. |
| + */ |
| + bool _processCommandLine(List<String> args) { |
| + ArgParser parser = _createArgParser(); |
| + ArgResults results; |
| + try { |
| + results = parser.parse(args); |
| + } catch (exception) { |
| + _showUsage(parser); |
| + return false; |
| + } |
| + |
| + if (results[HELP_FLAG_NAME]) { |
| + _showUsage(parser); |
| + return false; |
| + } |
| + |
| + String overlayStyleValue = results[OVERLAY_STYLE_OPTION_NAME]; |
| + if (overlayStyleValue == CHANGE_OVERLAY_STYLE) { |
| + overlayStyle = OverlayStyle.change; |
| + } else if (overlayStyleValue == MULTIPLE_ADD_OVERLAY_STYLE) { |
| + overlayStyle = OverlayStyle.multipleAdd; |
| + } |
| + |
| + List<String> arguments = results.arguments; |
| + if (arguments.length < 2) { |
| + _showUsage(parser); |
| + return false; |
| + } |
| + repositoryPath = path.normalize(arguments[0]); |
| + repository = new GitRepository(repositoryPath); |
| + |
| + analysisRoots = arguments |
| + .sublist(1) |
| + .map((String analysisRoot) => path.normalize(analysisRoot)) |
| + .toList(); |
| + for (String analysisRoot in analysisRoots) { |
| + if (repositoryPath != analysisRoot && |
| + !path.isWithin(repositoryPath, analysisRoot)) { |
| + _showUsage(parser, |
| + 'Analysis roots must be contained within the repository: $analysisRoot'); |
| + return false; |
| + } |
| + } |
| + return true; |
| + } |
| + |
| + /** |
| * Replay the changes in each commit. |
| */ |
| - void _replayChanges() { |
| + Future _replayChanges() async { |
| // |
| // Get the revision history of the repo. |
| // |
| @@ -228,17 +319,26 @@ class Driver { |
| // Iterate over the history, applying changes. |
| // |
| bool firstCheckout = true; |
| -// Map<String, List<AnalysisError>> expectedErrors = null; |
| + ErrorMap expectedErrors = null; |
| Iterable<String> changedPubspecs; |
| while (iterator.moveNext()) { |
| // |
| // Checkout the commit on which the changes are based. |
| // |
| - repository.checkout(iterator.srcCommit); |
| -// if (expectedErrors != null) { |
| -// await server.analysisFinished; |
| -// server.expectErrorState(expectedErrors); |
| -// } |
| + String commit = iterator.srcCommit; |
| + repository.checkout(commit); |
| + if (expectedErrors != null) { |
| + ErrorMap actualErrors = |
| + await server.computeErrorMap(server.analyzedDartFiles); |
| + String difference = expectedErrors.expectErrorMap(actualErrors); |
| + if (difference != null) { |
| + stdout.write('Mismatched errors after commit '); |
| + stdout.writeln(commit); |
| + stdout.writeln(); |
| + stdout.writeln(difference); |
| + return; |
| + } |
| + } |
| if (firstCheckout) { |
| changedPubspecs = _findPubspecsInAnalysisRoots(); |
| server.sendAnalysisSetAnalysisRoots(analysisRoots, []); |
| @@ -246,8 +346,7 @@ class Driver { |
| } else { |
| server.removeAllOverlays(); |
| } |
| -// await server.analysisFinished; |
| -// expectedErrors = server.errorMap; |
| + expectedErrors = await server.computeErrorMap(server.analyzedDartFiles); |
| for (String filePath in changedPubspecs) { |
| _runPub(filePath); |
| } |
| @@ -261,20 +360,28 @@ class Driver { |
| _replayDiff(commitDelta); |
| } |
| changedPubspecs = commitDelta.filesMatching(PUBSPEC_FILE_NAME); |
| + stdout.write('.'); |
| } |
| server.removeAllOverlays(); |
| + stdout.writeln(); |
| } |
| + /** |
| + * Replay the changes between two commits, as represented by the given |
| + * [commitDelta]. |
| + */ |
| void _replayDiff(CommitDelta commitDelta) { |
| List<FileEdit> editList = <FileEdit>[]; |
| for (DiffRecord record in commitDelta.diffRecords) { |
| - FileEdit edit = new FileEdit(record); |
| + FileEdit edit = new FileEdit(overlayStyle, record); |
| _createSourceEdits(edit, record.getBlobDiff()); |
| editList.add(edit); |
| } |
| + // |
| // TODO(brianwilkerson) Randomize. |
| // Randomly select operations from different files to simulate a user |
| // editing multiple files simultaneously. |
| + // |
| for (FileEdit edit in editList) { |
| List<String> currentFile = <String>[edit.filePath]; |
| server.sendAnalysisSetPriorityFiles(currentFile); |
| @@ -306,6 +413,34 @@ class Driver { |
| } |
| /** |
| + * Run the simulation by starting up a server and sending it requests. |
| + */ |
| + Future _runSimulation() async { |
| + Stopwatch stopwatch = new Stopwatch(); |
| + statistics.stopwatch = stopwatch; |
| + stopwatch.start(); |
| + await server.start(); |
| + server.sendServerSetSubscriptions([ServerService.STATUS]); |
| + server.sendAnalysisSetGeneralSubscriptions( |
| + [GeneralAnalysisService.ANALYZED_FILES]); |
| + // TODO(brianwilkerson) Get the list of glob patterns from the server after |
| + // an API for getting them has been implemented. |
| + fileGlobs = <Glob>[ |
| + new Glob(path.context.separator, '**.dart'), |
| + new Glob(path.context.separator, '**.html'), |
| + new Glob(path.context.separator, '**.htm'), |
| + new Glob(path.context.separator, '**/.analysisOptions') |
| + ]; |
| + try { |
| + await _replayChanges(); |
| + } finally { |
| + server.sendServerShutdown(); |
| + repository.checkout('master'); |
| + } |
| + stopwatch.stop(); |
| + } |
| + |
| + /** |
| * Display usage information, preceeded by the [errorMessage] if one is given. |
| */ |
| void _showUsage(ArgParser parser, [String errorMessage = null]) { |
| @@ -338,6 +473,11 @@ OPTIONS:'''); |
| */ |
| class FileEdit { |
| /** |
| + * The style of interaction to use for analysis.updateContent requests. |
| + */ |
| + OverlayStyle overlayStyle; |
| + |
| + /** |
| * The absolute path of the file to be edited. |
| */ |
| String filePath; |
| @@ -358,10 +498,16 @@ class FileEdit { |
| List<List<SourceEdit>> editLists = <List<SourceEdit>>[]; |
| /** |
| + * The current content of the file. This field is only used if the overlay |
| + * style is [OverlayStyle.multipleAdd]. |
| + */ |
| + String currentContent; |
| + |
| + /** |
| * Initialize a collection of edits to be associated with the file at the |
| * given [filePath]. |
| */ |
| - FileEdit(DiffRecord record) { |
| + FileEdit(this.overlayStyle, DiffRecord record) { |
| filePath = record.srcPath; |
| if (record.isAddition) { |
| content = ''; |
| @@ -372,6 +518,7 @@ class FileEdit { |
| content = new File(filePath).readAsStringSync(); |
| lineInfo = new LineInfo(StringUtilities.computeLineStarts(content)); |
| } |
| + currentContent = content; |
| } |
| /** |
| @@ -386,25 +533,43 @@ class FileEdit { |
| * Return a list of operations to be sent to the server. |
| */ |
| List<ServerOperation> getOperations() { |
| + List<ServerOperation> operations = <ServerOperation>[]; |
| + void addUpdateContent(var overlay) { |
| + operations.add(new Analysis_UpdateContent(filePath, overlay)); |
| + } |
| + |
| // TODO(brianwilkerson) Randomize. |
| // Make the order of edits random. Doing so will require updating the |
| // offsets of edits after the selected edit point. |
| - List<ServerOperation> operations = <ServerOperation>[]; |
| - operations.add( |
| - new AnalysisUpdateContent(filePath, new AddContentOverlay(content))); |
| + addUpdateContent(new AddContentOverlay(content)); |
| for (List<SourceEdit> editList in editLists.reversed) { |
| for (SourceEdit edit in editList.reversed) { |
| - operations.add(new AnalysisUpdateContent( |
| - filePath, new ChangeContentOverlay([edit]))); |
| + var overlay = null; |
| + if (overlayStyle == OverlayStyle.change) { |
| + overlay = new ChangeContentOverlay([edit]); |
| + } else if (overlayStyle == OverlayStyle.multipleAdd) { |
| + currentContent = edit.apply(currentContent); |
| + overlay = new AddContentOverlay(currentContent); |
| + } else { |
| + throw new StateError( |
| + 'Failed to handle overlay style = $overlayStyle'); |
| + } |
| + if (overlay != null) { |
| + addUpdateContent(overlay); |
| + } |
| } |
| } |
| - operations |
| - .add(new AnalysisUpdateContent(filePath, new RemoveContentOverlay())); |
| + addUpdateContent(new RemoveContentOverlay()); |
| return operations; |
| } |
| } |
| /** |
| + * The possible styles of interaction to use for analysis.updateContent requests. |
| + */ |
| +enum OverlayStyle { change, multipleAdd } |
| + |
| +/** |
| * A set of statistics related to the execution of the simulation. |
| */ |
| class Statistics { |
| @@ -434,6 +599,9 @@ class Statistics { |
| */ |
| Statistics(this.driver); |
| + /** |
| + * Print the statistics to [stdout]. |
| + */ |
| void print() { |
| stdout.write('Replay commits in '); |
| stdout.writeln(driver.repositoryPath); |
| @@ -447,6 +615,10 @@ class Statistics { |
| stdout.writeln(commitsWithChangeInRootCount); |
| } |
| + /** |
| + * Return a textual representation of the given duration, represented in |
| + * [milliseconds]. |
| + */ |
| String _printTime(int milliseconds) { |
| int seconds = milliseconds ~/ 1000; |
| milliseconds -= seconds * 1000; |