Index: pkg/analysis_server/test/integration/integration_tests.dart |
diff --git a/pkg/analysis_server/test/integration/integration_tests.dart b/pkg/analysis_server/test/integration/integration_tests.dart |
index 65ca8bab1826edab35e5cd84e0c95446f4c043a0..a2bd8cc33fe8528196a26ff372b4306dbc29735a 100644 |
--- a/pkg/analysis_server/test/integration/integration_tests.dart |
+++ b/pkg/analysis_server/test/integration/integration_tests.dart |
@@ -17,6 +17,49 @@ import 'package:unittest/unittest.dart'; |
import 'integration_test_methods.dart'; |
import 'protocol_matchers.dart'; |
+const Matcher isBool = const isInstanceOf<bool>('bool'); |
+ |
+const Matcher isInt = const isInstanceOf<int>('int'); |
+ |
+const Matcher isNotification = const MatchesJsonObject('notification', const { |
+ 'event': isString |
+}, optionalFields: const { |
+ 'params': isMap |
+}); |
+ |
+const Matcher isObject = isMap; |
+ |
+final Matcher isResponse = new MatchesJsonObject('response', { |
+ 'id': isString |
+}, optionalFields: { |
+ 'result': anything, |
+ 'error': isRequestError |
+}); |
+ |
+const Matcher isString = const isInstanceOf<String>('String'); |
+ |
+Matcher isListOf(Matcher elementMatcher) => new _ListOf(elementMatcher); |
+ |
+Matcher isMapOf(Matcher keyMatcher, Matcher valueMatcher) => |
+ new _MapOf(keyMatcher, valueMatcher); |
+ |
+Matcher isOneOf(List<Matcher> choiceMatchers) => new _OneOf(choiceMatchers); |
+ |
+/** |
+ * Type of closures used by LazyMatcher. |
+ */ |
+typedef Matcher MatcherCreator(); |
+ |
+/** |
+ * Type of closures used by MatchesJsonObject to record field mismatches. |
+ */ |
+typedef Description MismatchDescriber(Description mismatchDescription); |
+ |
+/** |
+ * Type of callbacks used to process notifications. |
+ */ |
+typedef void NotificationProcessor(String event, params); |
+ |
/** |
* Base class for analysis server integration tests. |
*/ |
@@ -61,47 +104,6 @@ abstract class AbstractAnalysisServerIntegrationTest extends |
} |
/** |
- * Write a source file with the given absolute [pathname] and [contents]. |
- * |
- * If the file didn't previously exist, it is created. If it did, it is |
- * overwritten. |
- * |
- * Parent directories are created as necessary. |
- * |
- * Return a normalized path to the file (with symbolic links resolved). |
- */ |
- String writeFile(String pathname, String contents) { |
- new Directory(dirname(pathname)).createSync(recursive: true); |
- File file = new File(pathname); |
- file.writeAsStringSync(contents); |
- return file.resolveSymbolicLinksSync(); |
- } |
- |
- /** |
- * Convert the given [relativePath] to an absolute path, by interpreting it |
- * relative to [sourceDirectory]. On Windows any forward slashes in |
- * [relativePath] are converted to backslashes. |
- */ |
- String sourcePath(String relativePath) { |
- return join(sourceDirectory.path, relativePath.replaceAll('/', separator)); |
- } |
- |
- /** |
- * Send the server an 'analysis.setAnalysisRoots' command directing it to |
- * analyze [sourceDirectory]. If [subscribeStatus] is true (the default), |
- * then also enable [SERVER_STATUS] notifications so that [analysisFinished] |
- * can be used. |
- */ |
- Future standardAnalysisSetup({bool subscribeStatus: true}) { |
- List<Future> futures = <Future>[]; |
- if (subscribeStatus) { |
- futures.add(sendServerSetSubscriptions([ServerService.STATUS])); |
- } |
- futures.add(sendAnalysisSetAnalysisRoots([sourceDirectory.path], [])); |
- return Future.wait(futures); |
- } |
- |
- /** |
* Return a future which will complete when a 'server.status' notification is |
* received from the server with 'analyzing' set to false. |
* |
@@ -168,6 +170,30 @@ abstract class AbstractAnalysisServerIntegrationTest extends |
} |
/** |
+ * Convert the given [relativePath] to an absolute path, by interpreting it |
+ * relative to [sourceDirectory]. On Windows any forward slashes in |
+ * [relativePath] are converted to backslashes. |
+ */ |
+ String sourcePath(String relativePath) { |
+ return join(sourceDirectory.path, relativePath.replaceAll('/', separator)); |
+ } |
+ |
+ /** |
+ * Send the server an 'analysis.setAnalysisRoots' command directing it to |
+ * analyze [sourceDirectory]. If [subscribeStatus] is true (the default), |
+ * then also enable [SERVER_STATUS] notifications so that [analysisFinished] |
+ * can be used. |
+ */ |
+ Future standardAnalysisSetup({bool subscribeStatus: true}) { |
+ List<Future> futures = <Future>[]; |
+ if (subscribeStatus) { |
+ futures.add(sendServerSetSubscriptions([ServerService.STATUS])); |
+ } |
+ futures.add(sendAnalysisSetAnalysisRoots([sourceDirectory.path], [])); |
+ return Future.wait(futures); |
+ } |
+ |
+ /** |
* After every test, the server is stopped and [sourceDirectory] is deleted. |
*/ |
Future tearDown() { |
@@ -177,6 +203,23 @@ abstract class AbstractAnalysisServerIntegrationTest extends |
} |
/** |
+ * Write a source file with the given absolute [pathname] and [contents]. |
+ * |
+ * If the file didn't previously exist, it is created. If it did, it is |
+ * overwritten. |
+ * |
+ * Parent directories are created as necessary. |
+ * |
+ * Return a normalized path to the file (with symbolic links resolved). |
+ */ |
+ String writeFile(String pathname, String contents) { |
+ new Directory(dirname(pathname)).createSync(recursive: true); |
+ File file = new File(pathname); |
+ file.writeAsStringSync(contents); |
+ return file.resolveSymbolicLinksSync(); |
+ } |
+ |
+ /** |
* If [skipShutdown] is not set, shut down the server. |
*/ |
Future _shutdownIfNeeded() { |
@@ -192,149 +235,83 @@ abstract class AbstractAnalysisServerIntegrationTest extends |
} |
} |
-final Matcher isResponse = new MatchesJsonObject('response', { |
- 'id': isString |
-}, optionalFields: { |
- 'result': anything, |
- 'error': isRequestError |
-}); |
- |
-const Matcher isNotification = const MatchesJsonObject('notification', const { |
- 'event': isString |
-}, optionalFields: const { |
- 'params': isMap |
-}); |
- |
-const Matcher isString = const isInstanceOf<String>('String'); |
- |
-const Matcher isInt = const isInstanceOf<int>('int'); |
- |
-const Matcher isBool = const isInstanceOf<bool>('bool'); |
- |
-const Matcher isObject = isMap; |
- |
/** |
- * Type of closures used by MatchesJsonObject to record field mismatches. |
+ * Wrapper class for Matcher which doesn't create the underlying Matcher object |
+ * until it is needed. This is necessary in order to create matchers that can |
+ * refer to themselves (so that recursive data structures can be represented). |
*/ |
-typedef Description MismatchDescriber(Description mismatchDescription); |
+class LazyMatcher implements Matcher { |
+ /** |
+ * Callback that will be used to create the matcher the first time it is |
+ * needed. |
+ */ |
+ final MatcherCreator _creator; |
-/** |
- * Base class for matchers that operate by recursing through the contents of |
- * an object. |
- */ |
-abstract class _RecursiveMatcher extends Matcher { |
- const _RecursiveMatcher(); |
+ /** |
+ * The matcher returned by [_creator], if it has already been called. |
+ * Otherwise null. |
+ */ |
+ Matcher _wrappedMatcher; |
+ |
+ LazyMatcher(this._creator); |
@override |
- bool matches(item, Map matchState) { |
- List<MismatchDescriber> mismatches = <MismatchDescriber>[]; |
- populateMismatches(item, mismatches); |
- if (mismatches.isEmpty) { |
- return true; |
- } else { |
- addStateInfo(matchState, { |
- 'mismatches': mismatches |
- }); |
- return false; |
- } |
+ Description describe(Description description) { |
+ _createMatcher(); |
+ return _wrappedMatcher.describe(description); |
} |
@override |
Description describeMismatch(item, Description mismatchDescription, |
Map matchState, bool verbose) { |
- List<MismatchDescriber> mismatches = matchState['mismatches']; |
- if (mismatches != null) { |
- for (int i = 0; i < mismatches.length; i++) { |
- MismatchDescriber mismatch = mismatches[i]; |
- if (i > 0) { |
- if (mismatches.length == 2) { |
- mismatchDescription = mismatchDescription.add(' and '); |
- } else if (i == mismatches.length - 1) { |
- mismatchDescription = mismatchDescription.add(', and '); |
- } else { |
- mismatchDescription = mismatchDescription.add(', '); |
- } |
- } |
- mismatchDescription = mismatch(mismatchDescription); |
- } |
- return mismatchDescription; |
- } else { |
- return super.describeMismatch( |
- item, |
- mismatchDescription, |
- matchState, |
- verbose); |
- } |
+ _createMatcher(); |
+ return _wrappedMatcher.describeMismatch( |
+ item, |
+ mismatchDescription, |
+ matchState, |
+ verbose); |
+ } |
+ |
+ @override |
+ bool matches(item, Map matchState) { |
+ _createMatcher(); |
+ return _wrappedMatcher.matches(item, matchState); |
} |
/** |
- * Populate [mismatches] with descriptions of all the ways in which [item] |
- * does not match. |
+ * Create the wrapped matcher object, if it hasn't been created already. |
*/ |
- void populateMismatches(item, List<MismatchDescriber> mismatches); |
+ void _createMatcher() { |
+ if (_wrappedMatcher == null) { |
+ _wrappedMatcher = _creator(); |
+ } |
+ } |
+} |
+/** |
+ * Matcher that matches a String drawn from a limited set. |
+ */ |
+class MatchesEnum extends Matcher { |
/** |
- * Create a [MismatchDescriber] describing a mismatch with a simple string. |
+ * Short description of the expected type. |
*/ |
- MismatchDescriber simpleDescription(String description) => |
- (Description mismatchDescription) { |
- mismatchDescription.add(description); |
- }; |
+ final String description; |
/** |
- * Check the type of a substructure whose value is [item], using [matcher]. |
- * If it doesn't match, record a closure in [mismatches] which can describe |
- * the mismatch. [describeSubstructure] is used to describe which |
- * substructure did not match. |
- */ |
- checkSubstructure(item, Matcher matcher, List<MismatchDescriber> mismatches, |
- Description describeSubstructure(Description)) { |
- Map subState = {}; |
- if (!matcher.matches(item, subState)) { |
- mismatches.add((Description mismatchDescription) { |
- mismatchDescription = mismatchDescription.add('contains malformed '); |
- mismatchDescription = describeSubstructure(mismatchDescription); |
- mismatchDescription = |
- mismatchDescription.add(' (should be ').addDescriptionOf(matcher); |
- String subDescription = matcher.describeMismatch( |
- item, |
- new StringDescription(), |
- subState, |
- false).toString(); |
- if (subDescription.isNotEmpty) { |
- mismatchDescription = |
- mismatchDescription.add('; ').add(subDescription); |
- } |
- return mismatchDescription.add(')'); |
- }); |
- } |
- } |
-} |
- |
-/** |
- * Matcher that matches a String drawn from a limited set. |
- */ |
-class MatchesEnum extends Matcher { |
- /** |
- * Short description of the expected type. |
- */ |
- final String description; |
- |
- /** |
- * The set of enum values that are allowed. |
+ * The set of enum values that are allowed. |
*/ |
final List<String> allowedValues; |
const MatchesEnum(this.description, this.allowedValues); |
@override |
+ Description describe(Description description) => |
+ description.add(this.description); |
+ |
+ @override |
bool matches(item, Map matchState) { |
return allowedValues.contains(item); |
} |
- |
- @override |
- Description describe(Description description) => |
- description.add(this.description); |
} |
/** |
@@ -363,6 +340,10 @@ class MatchesJsonObject extends _RecursiveMatcher { |
{this.optionalFields}); |
@override |
+ Description describe(Description description) => |
+ description.add(this.description); |
+ |
+ @override |
void populateMismatches(item, List<MismatchDescriber> mismatches) { |
if (item is! Map) { |
mismatches.add(simpleDescription('is not a map')); |
@@ -394,10 +375,6 @@ class MatchesJsonObject extends _RecursiveMatcher { |
}); |
} |
- @override |
- Description describe(Description description) => |
- description.add(this.description); |
- |
/** |
* Check the type of a field called [key], having value [value], using |
* [valueMatcher]. If it doesn't match, record a closure in [mismatches] |
@@ -414,212 +391,6 @@ class MatchesJsonObject extends _RecursiveMatcher { |
} |
/** |
- * Matcher that matches a list of objects, each of which satisfies the given |
- * matcher. |
- */ |
-class _ListOf extends Matcher { |
- /** |
- * Matcher which every element of the list must satisfy. |
- */ |
- final Matcher elementMatcher; |
- |
- /** |
- * Iterable matcher which we use to test the contents of the list. |
- */ |
- final Matcher iterableMatcher; |
- |
- _ListOf(elementMatcher) |
- : elementMatcher = elementMatcher, |
- iterableMatcher = everyElement(elementMatcher); |
- |
- @override |
- bool matches(item, Map matchState) { |
- if (item is! List) { |
- return false; |
- } |
- return iterableMatcher.matches(item, matchState); |
- } |
- |
- @override |
- Description describe(Description description) => |
- description.add('List of ').addDescriptionOf(elementMatcher); |
- |
- @override |
- Description describeMismatch(item, Description mismatchDescription, |
- Map matchState, bool verbose) { |
- if (item is! List) { |
- return super.describeMismatch( |
- item, |
- mismatchDescription, |
- matchState, |
- verbose); |
- } else { |
- return iterableMatcher.describeMismatch( |
- item, |
- mismatchDescription, |
- matchState, |
- verbose); |
- } |
- } |
-} |
- |
-Matcher isListOf(Matcher elementMatcher) => new _ListOf(elementMatcher); |
- |
-/** |
- * Type of closures used by LazyMatcher. |
- */ |
-typedef Matcher MatcherCreator(); |
- |
-/** |
- * Wrapper class for Matcher which doesn't create the underlying Matcher object |
- * until it is needed. This is necessary in order to create matchers that can |
- * refer to themselves (so that recursive data structures can be represented). |
- */ |
-class LazyMatcher implements Matcher { |
- /** |
- * Callback that will be used to create the matcher the first time it is |
- * needed. |
- */ |
- final MatcherCreator _creator; |
- |
- /** |
- * The matcher returned by [_creator], if it has already been called. |
- * Otherwise null. |
- */ |
- Matcher _wrappedMatcher; |
- |
- LazyMatcher(this._creator); |
- |
- @override |
- Description describe(Description description) { |
- _createMatcher(); |
- return _wrappedMatcher.describe(description); |
- } |
- |
- @override |
- Description describeMismatch(item, Description mismatchDescription, |
- Map matchState, bool verbose) { |
- _createMatcher(); |
- return _wrappedMatcher.describeMismatch( |
- item, |
- mismatchDescription, |
- matchState, |
- verbose); |
- } |
- |
- @override |
- bool matches(item, Map matchState) { |
- _createMatcher(); |
- return _wrappedMatcher.matches(item, matchState); |
- } |
- |
- /** |
- * Create the wrapped matcher object, if it hasn't been created already. |
- */ |
- void _createMatcher() { |
- if (_wrappedMatcher == null) { |
- _wrappedMatcher = _creator(); |
- } |
- } |
-} |
- |
-/** |
- * Matcher that matches a union of different types, each of which is described |
- * by a matcher. |
- */ |
-class _OneOf extends Matcher { |
- /** |
- * Matchers for the individual choices. |
- */ |
- final List<Matcher> choiceMatchers; |
- |
- _OneOf(this.choiceMatchers); |
- |
- @override |
- bool matches(item, Map matchState) { |
- for (Matcher choiceMatcher in choiceMatchers) { |
- Map subState = {}; |
- if (choiceMatcher.matches(item, subState)) { |
- return true; |
- } |
- } |
- return false; |
- } |
- |
- @override |
- Description describe(Description description) { |
- for (int i = 0; i < choiceMatchers.length; i++) { |
- if (i != 0) { |
- if (choiceMatchers.length == 2) { |
- description = description.add(' or '); |
- } else { |
- description = description.add(', '); |
- if (i == choiceMatchers.length - 1) { |
- description = description.add('or '); |
- } |
- } |
- } |
- description = description.addDescriptionOf(choiceMatchers[i]); |
- } |
- return description; |
- } |
-} |
- |
-Matcher isOneOf(List<Matcher> choiceMatchers) => new _OneOf(choiceMatchers); |
- |
-/** |
- * Matcher that matches a map of objects, where each key/value pair in the |
- * map satisies the given key and value matchers. |
- */ |
-class _MapOf extends _RecursiveMatcher { |
- /** |
- * Matcher which every key in the map must satisfy. |
- */ |
- final Matcher keyMatcher; |
- |
- /** |
- * Matcher which every value in the map must satisfy. |
- */ |
- final Matcher valueMatcher; |
- |
- _MapOf(this.keyMatcher, this.valueMatcher); |
- |
- @override |
- void populateMismatches(item, List<MismatchDescriber> mismatches) { |
- if (item is! Map) { |
- mismatches.add(simpleDescription('is not a map')); |
- return; |
- } |
- item.forEach((key, value) { |
- checkSubstructure( |
- key, |
- keyMatcher, |
- mismatches, |
- (Description description) => description.add('key ').addDescriptionOf(key)); |
- checkSubstructure( |
- value, |
- valueMatcher, |
- mismatches, |
- (Description description) => description.add('field ').addDescriptionOf(key)); |
- }); |
- } |
- |
- @override |
- Description describe(Description description) => |
- description.add( |
- 'Map from ').addDescriptionOf( |
- keyMatcher).add(' to ').addDescriptionOf(valueMatcher); |
-} |
- |
-Matcher isMapOf(Matcher keyMatcher, Matcher valueMatcher) => |
- new _MapOf(keyMatcher, valueMatcher); |
- |
-/** |
- * Type of callbacks used to process notifications. |
- */ |
-typedef void NotificationProcessor(String event, params); |
- |
-/** |
* Instances of the class [Server] manage a connection to a server process, and |
* facilitate communication to and from the server. |
*/ |
@@ -666,57 +437,55 @@ class Server { |
Stopwatch _time = new Stopwatch(); |
/** |
- * Find the root directory of the analysis_server package by proceeding |
- * upward to the 'test' dir, and then going up one more directory. |
+ * Future that completes when the server process exits. |
*/ |
- String findRoot(String pathname) { |
- while (basename(pathname) != 'test') { |
- String parent = dirname(pathname); |
- if (parent.length >= pathname.length) { |
- throw new Exception("Can't find root directory"); |
- } |
+ Future<int> get exitCode => _process.exitCode; |
+ |
+ /** |
+ * Print out any messages exchanged with the server. If some messages have |
+ * already been exchanged with the server, they are printed out immediately. |
+ */ |
+ void debugStdio() { |
+ if (_debuggingStdio) { |
+ return; |
+ } |
+ _debuggingStdio = true; |
+ for (String line in _recordedStdio) { |
+ print(line); |
+ } |
+ } |
+ |
+ /** |
+ * Find the root directory of the analysis_server package by proceeding |
+ * upward to the 'test' dir, and then going up one more directory. |
+ */ |
+ String findRoot(String pathname) { |
+ while (basename(pathname) != 'test') { |
+ String parent = dirname(pathname); |
+ if (parent.length >= pathname.length) { |
+ throw new Exception("Can't find root directory"); |
+ } |
pathname = parent; |
} |
return dirname(pathname); |
} |
/** |
- * Start the server. If [debugServer] is `true`, the server will be started |
- * with "--debug", allowing a debugger to be attached. If [profileServer] is |
- * `true`, the server will be started with "--observe" and |
- * "--pause-isolates-on-exit", allowing the observatory to be used. |
+ * Return a future that will complete when all commands that have been sent |
+ * to the server so far have been flushed to the OS buffer. |
*/ |
- Future start({bool debugServer: false, bool profileServer: false}) { |
- if (_process != null) { |
- throw new Exception('Process already started'); |
- } |
- _time.start(); |
- String dartBinary = Platform.executable; |
- String rootDir = |
- findRoot(Platform.script.toFilePath(windows: Platform.isWindows)); |
- String serverPath = normalize(join(rootDir, 'bin', 'server.dart')); |
- List<String> arguments = []; |
- if (debugServer) { |
- arguments.add('--debug'); |
- } |
- if (profileServer) { |
- arguments.add('--observe'); |
- arguments.add('--pause-isolates-on-exit'); |
- } |
- if (Platform.packageRoot.isNotEmpty) { |
- arguments.add('--package-root=${Platform.packageRoot}'); |
- } |
- arguments.add('--checked'); |
- arguments.add(serverPath); |
- return Process.start(dartBinary, arguments).then((Process process) { |
- _process = process; |
- process.exitCode.then((int code) { |
- _recordStdio('TERMINATED WITH EXIT CODE $code'); |
- if (code != 0) { |
- _badDataFromServer(); |
- } |
- }); |
- }); |
+ Future flushCommands() { |
+ return _process.stdin.flush(); |
+ } |
+ |
+ /** |
+ * Stop the server. |
+ */ |
+ Future kill() { |
+ debugStdio(); |
+ _recordStdio('PROCESS FORCIBLY TERMINATED'); |
+ _process.kill(); |
+ return _process.exitCode; |
} |
/** |
@@ -779,21 +548,6 @@ class Server { |
} |
/** |
- * Future that completes when the server process exits. |
- */ |
- Future<int> get exitCode => _process.exitCode; |
- |
- /** |
- * Stop the server. |
- */ |
- Future kill() { |
- debugStdio(); |
- _recordStdio('PROCESS FORCIBLY TERMINATED'); |
- _process.kill(); |
- return _process.exitCode; |
- } |
- |
- /** |
* Send a command to the server. An 'id' will be automatically assigned. |
* The returned [Future] will be completed when the server acknowledges the |
* command with a response. If the server acknowledges the command with a |
@@ -819,25 +573,42 @@ class Server { |
} |
/** |
- * Print out any messages exchanged with the server. If some messages have |
- * already been exchanged with the server, they are printed out immediately. |
+ * Start the server. If [debugServer] is `true`, the server will be started |
+ * with "--debug", allowing a debugger to be attached. If [profileServer] is |
+ * `true`, the server will be started with "--observe" and |
+ * "--pause-isolates-on-exit", allowing the observatory to be used. |
*/ |
- void debugStdio() { |
- if (_debuggingStdio) { |
- return; |
+ Future start({bool debugServer: false, bool profileServer: false}) { |
+ if (_process != null) { |
+ throw new Exception('Process already started'); |
} |
- _debuggingStdio = true; |
- for (String line in _recordedStdio) { |
- print(line); |
+ _time.start(); |
+ String dartBinary = Platform.executable; |
+ String rootDir = |
+ findRoot(Platform.script.toFilePath(windows: Platform.isWindows)); |
+ String serverPath = normalize(join(rootDir, 'bin', 'server.dart')); |
+ List<String> arguments = []; |
+ if (debugServer) { |
+ arguments.add('--debug'); |
} |
- } |
- |
- /** |
- * Return a future that will complete when all commands that have been sent |
- * to the server so far have been flushed to the OS buffer. |
- */ |
- Future flushCommands() { |
- return _process.stdin.flush(); |
+ if (profileServer) { |
+ arguments.add('--observe'); |
+ arguments.add('--pause-isolates-on-exit'); |
+ } |
+ if (Platform.packageRoot.isNotEmpty) { |
+ arguments.add('--package-root=${Platform.packageRoot}'); |
+ } |
+ arguments.add('--checked'); |
+ arguments.add(serverPath); |
+ return Process.start(dartBinary, arguments).then((Process process) { |
+ _process = process; |
+ process.exitCode.then((int code) { |
+ _recordStdio('TERMINATED WITH EXIT CODE $code'); |
+ if (code != 0) { |
+ _badDataFromServer(); |
+ } |
+ }); |
+ }); |
} |
/** |
@@ -873,3 +644,232 @@ class Server { |
_recordedStdio.add(line); |
} |
} |
+ |
+/** |
+ * Matcher that matches a list of objects, each of which satisfies the given |
+ * matcher. |
+ */ |
+class _ListOf extends Matcher { |
+ /** |
+ * Matcher which every element of the list must satisfy. |
+ */ |
+ final Matcher elementMatcher; |
+ |
+ /** |
+ * Iterable matcher which we use to test the contents of the list. |
+ */ |
+ final Matcher iterableMatcher; |
+ |
+ _ListOf(elementMatcher) |
+ : elementMatcher = elementMatcher, |
+ iterableMatcher = everyElement(elementMatcher); |
+ |
+ @override |
+ Description describe(Description description) => |
+ description.add('List of ').addDescriptionOf(elementMatcher); |
+ |
+ @override |
+ Description describeMismatch(item, Description mismatchDescription, |
+ Map matchState, bool verbose) { |
+ if (item is! List) { |
+ return super.describeMismatch( |
+ item, |
+ mismatchDescription, |
+ matchState, |
+ verbose); |
+ } else { |
+ return iterableMatcher.describeMismatch( |
+ item, |
+ mismatchDescription, |
+ matchState, |
+ verbose); |
+ } |
+ } |
+ |
+ @override |
+ bool matches(item, Map matchState) { |
+ if (item is! List) { |
+ return false; |
+ } |
+ return iterableMatcher.matches(item, matchState); |
+ } |
+} |
+ |
+/** |
+ * Matcher that matches a map of objects, where each key/value pair in the |
+ * map satisies the given key and value matchers. |
+ */ |
+class _MapOf extends _RecursiveMatcher { |
+ /** |
+ * Matcher which every key in the map must satisfy. |
+ */ |
+ final Matcher keyMatcher; |
+ |
+ /** |
+ * Matcher which every value in the map must satisfy. |
+ */ |
+ final Matcher valueMatcher; |
+ |
+ _MapOf(this.keyMatcher, this.valueMatcher); |
+ |
+ @override |
+ Description describe(Description description) => |
+ description.add( |
+ 'Map from ').addDescriptionOf( |
+ keyMatcher).add(' to ').addDescriptionOf(valueMatcher); |
+ |
+ @override |
+ void populateMismatches(item, List<MismatchDescriber> mismatches) { |
+ if (item is! Map) { |
+ mismatches.add(simpleDescription('is not a map')); |
+ return; |
+ } |
+ item.forEach((key, value) { |
+ checkSubstructure( |
+ key, |
+ keyMatcher, |
+ mismatches, |
+ (Description description) => description.add('key ').addDescriptionOf(key)); |
+ checkSubstructure( |
+ value, |
+ valueMatcher, |
+ mismatches, |
+ (Description description) => description.add('field ').addDescriptionOf(key)); |
+ }); |
+ } |
+} |
+ |
+/** |
+ * Matcher that matches a union of different types, each of which is described |
+ * by a matcher. |
+ */ |
+class _OneOf extends Matcher { |
+ /** |
+ * Matchers for the individual choices. |
+ */ |
+ final List<Matcher> choiceMatchers; |
+ |
+ _OneOf(this.choiceMatchers); |
+ |
+ @override |
+ Description describe(Description description) { |
+ for (int i = 0; i < choiceMatchers.length; i++) { |
+ if (i != 0) { |
+ if (choiceMatchers.length == 2) { |
+ description = description.add(' or '); |
+ } else { |
+ description = description.add(', '); |
+ if (i == choiceMatchers.length - 1) { |
+ description = description.add('or '); |
+ } |
+ } |
+ } |
+ description = description.addDescriptionOf(choiceMatchers[i]); |
+ } |
+ return description; |
+ } |
+ |
+ @override |
+ bool matches(item, Map matchState) { |
+ for (Matcher choiceMatcher in choiceMatchers) { |
+ Map subState = {}; |
+ if (choiceMatcher.matches(item, subState)) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+} |
+ |
+/** |
+ * Base class for matchers that operate by recursing through the contents of |
+ * an object. |
+ */ |
+abstract class _RecursiveMatcher extends Matcher { |
+ const _RecursiveMatcher(); |
+ |
+ /** |
+ * Check the type of a substructure whose value is [item], using [matcher]. |
+ * If it doesn't match, record a closure in [mismatches] which can describe |
+ * the mismatch. [describeSubstructure] is used to describe which |
+ * substructure did not match. |
+ */ |
+ checkSubstructure(item, Matcher matcher, List<MismatchDescriber> mismatches, |
+ Description describeSubstructure(Description)) { |
+ Map subState = {}; |
+ if (!matcher.matches(item, subState)) { |
+ mismatches.add((Description mismatchDescription) { |
+ mismatchDescription = mismatchDescription.add('contains malformed '); |
+ mismatchDescription = describeSubstructure(mismatchDescription); |
+ mismatchDescription = |
+ mismatchDescription.add(' (should be ').addDescriptionOf(matcher); |
+ String subDescription = matcher.describeMismatch( |
+ item, |
+ new StringDescription(), |
+ subState, |
+ false).toString(); |
+ if (subDescription.isNotEmpty) { |
+ mismatchDescription = |
+ mismatchDescription.add('; ').add(subDescription); |
+ } |
+ return mismatchDescription.add(')'); |
+ }); |
+ } |
+ } |
+ |
+ @override |
+ Description describeMismatch(item, Description mismatchDescription, |
+ Map matchState, bool verbose) { |
+ List<MismatchDescriber> mismatches = matchState['mismatches']; |
+ if (mismatches != null) { |
+ for (int i = 0; i < mismatches.length; i++) { |
+ MismatchDescriber mismatch = mismatches[i]; |
+ if (i > 0) { |
+ if (mismatches.length == 2) { |
+ mismatchDescription = mismatchDescription.add(' and '); |
+ } else if (i == mismatches.length - 1) { |
+ mismatchDescription = mismatchDescription.add(', and '); |
+ } else { |
+ mismatchDescription = mismatchDescription.add(', '); |
+ } |
+ } |
+ mismatchDescription = mismatch(mismatchDescription); |
+ } |
+ return mismatchDescription; |
+ } else { |
+ return super.describeMismatch( |
+ item, |
+ mismatchDescription, |
+ matchState, |
+ verbose); |
+ } |
+ } |
+ |
+ @override |
+ bool matches(item, Map matchState) { |
+ List<MismatchDescriber> mismatches = <MismatchDescriber>[]; |
+ populateMismatches(item, mismatches); |
+ if (mismatches.isEmpty) { |
+ return true; |
+ } else { |
+ addStateInfo(matchState, { |
+ 'mismatches': mismatches |
+ }); |
+ return false; |
+ } |
+ } |
+ |
+ /** |
+ * Populate [mismatches] with descriptions of all the ways in which [item] |
+ * does not match. |
+ */ |
+ void populateMismatches(item, List<MismatchDescriber> mismatches); |
+ |
+ /** |
+ * Create a [MismatchDescriber] describing a mismatch with a simple string. |
+ */ |
+ MismatchDescriber simpleDescription(String description) => |
+ (Description mismatchDescription) { |
+ mismatchDescription.add(description); |
+ }; |
+} |