| // Copyright (c) 2014, 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 'dart:collection'; |
| |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'domain_completion_test.dart'; |
| |
| /** |
| * A base class for classes containing completion tests. |
| */ |
| class CompletionTestCase extends CompletionDomainHandlerTest { |
| static const String CURSOR_MARKER = '!'; |
| |
| List get suggestedCompletions => suggestions |
| .map((CompletionSuggestion suggestion) => suggestion.completion) |
| .toList(); |
| |
| void assertHasCompletion(String completion) { |
| int expectedOffset = completion.indexOf(CURSOR_MARKER); |
| if (expectedOffset >= 0) { |
| if (completion.indexOf(CURSOR_MARKER, expectedOffset + 1) >= 0) { |
| fail( |
| "Invalid completion, contains multiple cursor positions: '$completion'"); |
| } |
| completion = completion.replaceFirst(CURSOR_MARKER, ''); |
| } else { |
| expectedOffset = completion.length; |
| } |
| CompletionSuggestion matchingSuggestion; |
| suggestions.forEach((CompletionSuggestion suggestion) { |
| if (suggestion.completion == completion) { |
| if (matchingSuggestion == null) { |
| matchingSuggestion = suggestion; |
| } else { |
| // It is OK to have a class and its default constructor suggestions. |
| if (matchingSuggestion.element?.kind == ElementKind.CLASS && |
| suggestion.element?.kind == ElementKind.CONSTRUCTOR || |
| matchingSuggestion.element?.kind == ElementKind.CONSTRUCTOR && |
| suggestion.element?.kind == ElementKind.CLASS) { |
| return; |
| } |
| fail( |
| "Expected exactly one '$completion' but found multiple:\n $suggestedCompletions"); |
| } |
| } |
| }); |
| if (matchingSuggestion == null) { |
| fail("Expected '$completion' but found none:\n $suggestedCompletions"); |
| } |
| expect(matchingSuggestion.selectionOffset, equals(expectedOffset)); |
| expect(matchingSuggestion.selectionLength, equals(0)); |
| } |
| |
| void assertHasNoCompletion(String completion) { |
| if (suggestions.any((CompletionSuggestion suggestion) => |
| suggestion.completion == completion)) { |
| fail( |
| "Did not expect completion '$completion' but found:\n $suggestedCompletions"); |
| } |
| } |
| |
| /** |
| * Discard any results that do not start with the characters the user has |
| * "already typed". |
| */ |
| void filterResults(String content) { |
| String charsAlreadyTyped = |
| content.substring(replacementOffset, completionOffset).toLowerCase(); |
| suggestions = suggestions |
| .where((CompletionSuggestion suggestion) => |
| suggestion.completion.toLowerCase().startsWith(charsAlreadyTyped)) |
| .toList(); |
| } |
| |
| runTest(LocationSpec spec, [Map<String, String> extraFiles]) { |
| super.setUp(); |
| return new Future(() { |
| String content = spec.source; |
| newFile(testFile, content: content); |
| this.testCode = content; |
| completionOffset = spec.testLocation; |
| if (extraFiles != null) { |
| extraFiles.forEach((String fileName, String content) { |
| newFile(fileName, content: content); |
| }); |
| } |
| }).then((_) => getSuggestions()).then((_) { |
| filterResults(spec.source); |
| for (String result in spec.positiveResults) { |
| assertHasCompletion(result); |
| } |
| for (String result in spec.negativeResults) { |
| assertHasNoCompletion(result); |
| } |
| }).whenComplete(() { |
| super.tearDown(); |
| }); |
| } |
| } |
| |
| /** |
| * A specification of the completion results expected at a given location. |
| */ |
| class LocationSpec { |
| String id; |
| int testLocation = -1; |
| List<String> positiveResults = <String>[]; |
| List<String> negativeResults = <String>[]; |
| String source; |
| |
| LocationSpec(this.id); |
| |
| /** |
| * Parse a set of tests from the given `originalSource`. Return a list of the |
| * specifications that were parsed. |
| * |
| * The source string has test locations embedded in it, which are identified |
| * by '!X' where X is a single character. Each X is matched to positive or |
| * negative results in the array of [validationStrings]. Validation strings |
| * contain the name of a prediction with a two character prefix. The first |
| * character of the prefix corresponds to an X in the [originalSource]. The |
| * second character is either a '+' or a '-' indicating whether the string is |
| * a positive or negative result. If logical not is needed in the source it |
| * can be represented by '!!'. |
| * |
| * The [originalSource] is the source for a test that contains test locations. |
| * The [validationStrings] are the positive and negative predictions. |
| */ |
| static List<LocationSpec> from( |
| String originalSource, List<String> validationStrings) { |
| Map<String, LocationSpec> tests = new HashMap<String, LocationSpec>(); |
| String modifiedSource = originalSource; |
| int modifiedPosition = 0; |
| while (true) { |
| int index = modifiedSource.indexOf('!', modifiedPosition); |
| if (index < 0) { |
| break; |
| } |
| int n = 1; // only delete one char for double-bangs |
| String id = modifiedSource.substring(index + 1, index + 2); |
| if (id != '!') { |
| n = 2; |
| LocationSpec test = new LocationSpec(id); |
| tests[id] = test; |
| test.testLocation = index; |
| } else { |
| modifiedPosition = index + 1; |
| } |
| modifiedSource = modifiedSource.substring(0, index) + |
| modifiedSource.substring(index + n); |
| } |
| if (modifiedSource == originalSource) { |
| throw new StateError("No tests in source: " + originalSource); |
| } |
| for (String result in validationStrings) { |
| if (result.length < 3) { |
| throw new StateError("Invalid location result: " + result); |
| } |
| String id = result.substring(0, 1); |
| String sign = result.substring(1, 2); |
| String value = result.substring(2); |
| LocationSpec test = tests[id]; |
| if (test == null) { |
| throw new StateError("Invalid location result id: $id for: $result"); |
| } |
| test.source = modifiedSource; |
| if (sign == '+') { |
| test.positiveResults.add(value); |
| } else if (sign == '-') { |
| test.negativeResults.add(value); |
| } else { |
| String err = "Invalid location result sign: $sign for: $result"; |
| throw new StateError(err); |
| } |
| } |
| List<String> badPoints = <String>[]; |
| List<String> badResults = <String>[]; |
| for (LocationSpec test in tests.values) { |
| if (test.testLocation == -1) { |
| badPoints.add(test.id); |
| } |
| if (test.positiveResults.isEmpty && test.negativeResults.isEmpty) { |
| badResults.add(test.id); |
| } |
| } |
| if (!(badPoints.isEmpty && badResults.isEmpty)) { |
| StringBuffer err = new StringBuffer(); |
| if (!badPoints.isEmpty) { |
| err.write("No test location for tests:"); |
| for (String ch in badPoints) { |
| err..write(' ')..write(ch); |
| } |
| err.write(' '); |
| } |
| if (!badResults.isEmpty) { |
| err.write("No results for tests:"); |
| for (String ch in badResults) { |
| err..write(' ')..write(ch); |
| } |
| } |
| throw new StateError(err.toString()); |
| } |
| return tests.values.toList(); |
| } |
| } |