blob: 83660dfa37492fe618c6ae0e6a1c93e1b521380e [file] [log] [blame]
// 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 CompletionDomainHandlerListTokenDetailsTest {
static const String CURSOR_MARKER = '!';
List get suggestedCompletions => suggestions
.map((CompletionSuggestion suggestion) => suggestion.completion)
.toList();
void assertHasCompletion(String completion) {
var expectedOffset = completion.indexOf(CURSOR_MARKER);
if (expectedOffset >= 0) {
if (completion.contains(CURSOR_MARKER, expectedOffset + 1)) {
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) {
var charsAlreadyTyped =
content.substring(replacementOffset, completionOffset).toLowerCase();
suggestions = suggestions
.where((CompletionSuggestion suggestion) =>
suggestion.completion.toLowerCase().startsWith(charsAlreadyTyped))
.toList();
}
Future runTest(LocationSpec spec, [Map<String, String> extraFiles]) {
super.setUp();
return Future(() {
var content = spec.source;
newFile(testFile, content: content);
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 (var result in spec.positiveResults) {
assertHasCompletion(result);
}
for (var 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 = HashMap<String, LocationSpec>();
var modifiedSource = originalSource;
var modifiedPosition = 0;
while (true) {
var index = modifiedSource.indexOf('!', modifiedPosition);
if (index < 0) {
break;
}
var n = 1; // only delete one char for double-bangs
var id = modifiedSource.substring(index + 1, index + 2);
if (id != '!') {
n = 2;
var test = 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 StateError('No tests in source: ' + originalSource);
}
for (var result in validationStrings) {
if (result.length < 3) {
throw StateError('Invalid location result: ' + result);
}
var id = result.substring(0, 1);
var sign = result.substring(1, 2);
var value = result.substring(2);
var test = tests[id];
if (test == null) {
throw 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 {
var err = 'Invalid location result sign: $sign for: $result';
throw StateError(err);
}
}
var badPoints = <String>[];
var badResults = <String>[];
for (var 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)) {
var err = StringBuffer();
if (badPoints.isNotEmpty) {
err.write('No test location for tests:');
for (var ch in badPoints) {
err..write(' ')..write(ch);
}
err.write(' ');
}
if (badResults.isNotEmpty) {
err.write('No results for tests:');
for (var ch in badResults) {
err..write(' ')..write(ch);
}
}
throw StateError(err.toString());
}
return tests.values.toList();
}
}