Initial stress test for code completion

Change-Id: I5ec7ff846da2924ef26a7e738a5e30191a3df9c5
Reviewed-on: https://dart-review.googlesource.com/68440
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/test/stress/completion/completion.dart b/pkg/analysis_server/test/stress/completion/completion.dart
new file mode 100644
index 0000000..9581943
--- /dev/null
+++ b/pkg/analysis_server/test/stress/completion/completion.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:args/args.dart';
+
+import 'completion_runner.dart';
+
+/**
+ * The main entry point for the code completion stress test.
+ */
+void main(List<String> args) async {
+  ArgParser parser = createArgParser();
+  ArgResults result = parser.parse(args);
+
+  if (validArguments(parser, result)) {
+    String analysisRoot = result.rest[0];
+
+    CompletionRunner runner = new CompletionRunner(
+        output: stdout,
+        printMissing: result['missing'],
+        timing: result['timing'],
+        useCFE: result['use-cfe'],
+        verbose: result['verbose']);
+    await runner.runAll(analysisRoot);
+    stdout.flush();
+  }
+}
+
+/**
+ * Create a parser that can be used to parse the command-line arguments.
+ */
+ArgParser createArgParser() {
+  ArgParser parser = new ArgParser();
+  parser.addFlag(
+    'help',
+    abbr: 'h',
+    help: 'Print this help message',
+    negatable: false,
+  );
+  parser.addFlag(
+    'missing',
+    help: 'Report locations where the current identifier was not suggested',
+    negatable: false,
+  );
+  parser.addFlag(
+    'timing',
+    help: 'Report timing information',
+    negatable: false,
+  );
+  parser.addFlag(
+    'use-cfe',
+    help: 'Use the CFE to perform the analysis',
+    negatable: false,
+  );
+  parser.addFlag(
+    'verbose',
+    abbr: 'v',
+    help: 'Produce verbose output',
+    negatable: false,
+  );
+  return parser;
+}
+
+/**
+ * Print usage information for this tool.
+ */
+void printUsage(ArgParser parser, {String error}) {
+  if (error != null) {
+    print(error);
+    print('');
+  }
+  print('usage: dart completion path');
+  print('');
+  print('Test the completion engine by requesting completion at the offset of');
+  print('each identifier in the files contained in the given path. The path');
+  print('can be either a single Dart file or a directory.');
+  print('');
+  print(parser.usage);
+}
+
+/**
+ * Return `true` if the command-line arguments (represented by the [result] and
+ * parsed by the [parser]) are valid.
+ */
+bool validArguments(ArgParser parser, ArgResults result) {
+  if (result.wasParsed('help')) {
+    printUsage(parser);
+    return false;
+  } else if (result.rest.length < 1) {
+    printUsage(parser, error: 'Missing path to files');
+    return false;
+  } else if (result.rest.length > 1) {
+    printUsage(parser, error: 'Only one file can be analyzed');
+    return false;
+  }
+  return true;
+}
diff --git a/pkg/analysis_server/test/stress/completion/completion_runner.dart b/pkg/analysis_server/test/stress/completion/completion_runner.dart
new file mode 100644
index 0000000..2f49a24
--- /dev/null
+++ b/pkg/analysis_server/test/stress/completion/completion_runner.dart
@@ -0,0 +1,224 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:analysis_server/src/services/completion/completion_core.dart';
+import 'package:analysis_server/src/services/completion/completion_performance.dart';
+import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
+import 'package:analysis_server/src/utilities/null_string_sink.dart';
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/file_system/overlay_file_system.dart';
+import 'package:analyzer/file_system/physical_file_system.dart';
+import 'package:analyzer/source/line_info.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart';
+
+/**
+ * A runner that can request code completion at the location of each identifier
+ * in a Dart file.
+ */
+class CompletionRunner {
+  /**
+   * The sink to which output is to be written.
+   */
+  final StringSink output;
+
+  /**
+   * A flag indicating whether to produce output about missing suggestions.
+   */
+  final bool printMissing;
+
+  /**
+   * A flag indicating whether to produce timing information.
+   */
+  final bool timing;
+
+  /**
+   * A flag indicating whether to use the CFE when running the tests.
+   */
+  final bool useCFE;
+
+  /**
+   * A flag indicating whether to produce verbose output.
+   */
+  final bool verbose;
+
+  /**
+   * A flag indicating whether we should delete each identifier before
+   * attempting to complete at that offset.
+   */
+  bool deleteBeforeCompletion = false;
+
+  /**
+   * Initialize a newly created completion runner.
+   */
+  CompletionRunner(
+      {StringSink output,
+      bool printMissing,
+      bool timing,
+      bool useCFE,
+      bool verbose})
+      : this.output = output ?? new NullStringSink(),
+        this.printMissing = printMissing ?? false,
+        this.timing = timing ?? false,
+        this.useCFE = useCFE ?? false,
+        this.verbose = verbose ?? false;
+
+  /**
+   * Test the completion engine at the locations of each of the identifiers in
+   * each of the files in the given [analysisRoot].
+   */
+  Future<void> runAll(String analysisRoot) async {
+    OverlayResourceProvider resourceProvider =
+        new OverlayResourceProvider(PhysicalResourceProvider.INSTANCE);
+    AnalysisContextCollection collection = new AnalysisContextCollection(
+        includedPaths: <String>[analysisRoot],
+        resourceProvider: resourceProvider,
+        useCFE: useCFE); // ignore: deprecated_member_use
+    DartCompletionManager contributor = new DartCompletionManager();
+    CompletionPerformance performance = new CompletionPerformance();
+    int stamp = 1;
+
+    int fileCount = 0;
+    int identifierCount = 0;
+    int missingCount = 0;
+
+    // Consider getting individual timings so that we can also report the
+    // longest and shortest times, or even a distribution.
+    Stopwatch timer = new Stopwatch();
+
+    for (AnalysisContext context in collection.contexts) {
+      for (String path in context.contextRoot.analyzedFiles()) {
+        fileCount++;
+        output.write('.');
+        ResolveResult result =
+            await context.currentSession.getResolvedAst(path);
+        String content = result.content;
+        LineInfo lineInfo = result.lineInfo;
+        List<SimpleIdentifier> identifiers = _identifiersIn(result.unit);
+
+        for (SimpleIdentifier identifier in identifiers) {
+          identifierCount++;
+          int offset = identifier.offset;
+          if (deleteBeforeCompletion) {
+            String modifiedContent = content.substring(0, offset) +
+                content.substring(identifier.end);
+            resourceProvider.setOverlay(path,
+                content: modifiedContent, modificationStamp: stamp++);
+            result = await context.currentSession.getResolvedAst(path);
+          }
+
+          timer.start();
+          CompletionRequestImpl request =
+              new CompletionRequestImpl(result, offset, performance);
+          List<CompletionSuggestion> suggestions =
+              await contributor.computeSuggestions(request);
+          timer.stop();
+
+          if (!identifier.inDeclarationContext() &&
+              !_isNamedExpressionName(identifier)) {
+            if (!_hasSuggestion(suggestions, identifier.name)) {
+              missingCount++;
+              if (printMissing) {
+                CharacterLocation location = lineInfo.getLocation(offset);
+                output.writeln('Missing suggestion of "${identifier.name}" at '
+                    '$path:${location.lineNumber}:${location.columnNumber}');
+                if (verbose) {
+                  _printSuggestions(suggestions);
+                }
+              }
+            }
+          }
+        }
+        if (deleteBeforeCompletion) {
+          resourceProvider.removeOverlay(path);
+        }
+      }
+    }
+    output.writeln();
+    if (printMissing) {
+      output.writeln();
+    }
+    if (identifierCount == 0) {
+      output.writeln('No identifiers found in $fileCount files');
+    } else {
+      int percent = (missingCount * 100 / identifierCount).round();
+      output.writeln('$percent% missing suggestions '
+          '($missingCount of $identifierCount in $fileCount files)');
+    }
+    if (timing && identifierCount > 0) {
+      int time = timer.elapsedMilliseconds;
+      int averageTime = (time / identifierCount).round();
+      output.writeln('completion took $time ms, '
+          'which is an average of $averageTime ms per completion');
+    }
+  }
+
+  /**
+   * Return `true` if the given list of [suggestions] includes a suggestion for
+   * the given [identifier].
+   */
+  bool _hasSuggestion(
+      List<CompletionSuggestion> suggestions, String identifier) {
+    for (CompletionSuggestion suggestion in suggestions) {
+      if (suggestion.completion == identifier) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Return a list containing information about the identifiers in the given
+   * compilation [unit].
+   */
+  List<SimpleIdentifier> _identifiersIn(CompilationUnit unit) {
+    IdentifierCollector visitor = new IdentifierCollector();
+    unit.accept(visitor);
+    return visitor.identifiers;
+  }
+
+  /**
+   * Return `true` if the given [identifier] is being used as the name of a
+   * named expression.
+   */
+  bool _isNamedExpressionName(SimpleIdentifier identifier) {
+    AstNode parent = identifier.parent;
+    return parent is NamedExpression && parent.name == identifier;
+  }
+
+  /**
+   * Print information about the given [suggestions].
+   */
+  void _printSuggestions(List<CompletionSuggestion> suggestions) {
+    if (suggestions.length == 0) {
+      output.writeln('  No suggestions');
+      return;
+    }
+    output.writeln('  Suggestions:');
+    for (CompletionSuggestion suggestion in suggestions) {
+      output.writeln('    ${suggestion.completion}');
+    }
+  }
+}
+
+/**
+ * A visitor that will collect simple identifiers in the AST being visited.
+ */
+class IdentifierCollector extends RecursiveAstVisitor<void> {
+  /**
+   * The simple identifiers that were collected.
+   */
+  final List<SimpleIdentifier> identifiers = <SimpleIdentifier>[];
+
+  @override
+  visitSimpleIdentifier(SimpleIdentifier node) {
+    identifiers.add(node);
+  }
+}