// 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 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 positiveResults = []; List negativeResults = []; 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 from( String originalSource, List validationStrings) { Map tests = new HashMap(); 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 badPoints = []; List badResults = []; 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(); } }