| // 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(); | |
| } | |
| } |