diff --git a/pkgs/matcher/.github/dependabot.yml b/pkgs/matcher/.github/dependabot.yml new file mode 100644 index 000000000..a19a66adf --- /dev/null +++ b/pkgs/matcher/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Set update schedule for GitHub Actions +# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot + +version: 2 +updates: + +- package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - autosubmit + groups: + github-actions: + patterns: + - "*" diff --git a/pkgs/matcher/.github/workflows/ci.yml b/pkgs/matcher/.github/workflows/ci.yml new file mode 100644 index 000000000..3f7ea7629 --- /dev/null +++ b/pkgs/matcher/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: ci + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev and stable. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev, stable + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: dart test --platform chrome + if: always() && steps.install.outcome == 'success' diff --git a/pkgs/matcher/.github/workflows/no-response.yml b/pkgs/matcher/.github/workflows/no-response.yml new file mode 100644 index 000000000..ab1ac4984 --- /dev/null +++ b/pkgs/matcher/.github/workflows/no-response.yml @@ -0,0 +1,37 @@ +# A workflow to close issues where the author hasn't responded to a request for +# more information; see https://github.com/actions/stale. + +name: No Response + +# Run as a daily cron. +on: + schedule: + # Every day at 8am + - cron: '0 8 * * *' + +# All permissions not specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + no-response: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dart-lang' }} + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + with: + # Don't automatically mark inactive issues+PRs as stale. + days-before-stale: -1 + # Close needs-info issues and PRs after 14 days of inactivity. + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution! diff --git a/pkgs/matcher/.github/workflows/publish.yaml b/pkgs/matcher/.github/workflows/publish.yaml new file mode 100644 index 000000000..27157a046 --- /dev/null +++ b/pkgs/matcher/.github/workflows/publish.yaml @@ -0,0 +1,17 @@ +# A CI configuration to auto-publish pub packages. + +name: Publish + +on: + pull_request: + branches: [ master ] + push: + tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] + +jobs: + publish: + if: ${{ github.repository_owner == 'dart-lang' }} + uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main + permissions: + id-token: write # Required for authentication using OIDC + pull-requests: write # Required for writing the pull request note diff --git a/pkgs/matcher/.gitignore b/pkgs/matcher/.gitignore new file mode 100644 index 000000000..ab3cb76e6 --- /dev/null +++ b/pkgs/matcher/.gitignore @@ -0,0 +1,16 @@ +# Don’t commit the following directories created by pub. +.buildlog +.dart_tool/ +.pub/ +build/ +packages +.packages + +# Or the files created by dart2js. +*.dart.js +*.js_ +*.js.deps +*.js.map + +# Include when developing application packages. +pubspec.lock diff --git a/pkgs/matcher/CHANGELOG.md b/pkgs/matcher/CHANGELOG.md new file mode 100644 index 000000000..afe767de5 --- /dev/null +++ b/pkgs/matcher/CHANGELOG.md @@ -0,0 +1,284 @@ +## 0.12.17-wip + +* Require Dart 3.4 + +## 0.12.16+1 + +* Require Dart 3.0 +* Support latest version of `package:test_api`. + +## 0.12.16 + +* Expand bounds on `test_api` dependency to allow the next breaking release + which will remove the cyclic dependency on this package. + +## 0.12.15 + +* Add `package:matcher/expect.dart` library. Copies the implementation of + `expect` and the asynchronous matchers from `package:test`. + +## 0.12.14 + +* Add `containsOnce` matcher. +* Deprecate `isCyclicInitializationError` and `NullThrownError`. These errors + will be removed from the SDK. Update them to catch more general errors. + +## 0.12.13 + +* Require Dart 2.17 or greater. +* Make `isCastError` no longer depend on the deprecated `CastError` type. +* Annotate `TypeMatcher.having` with `useResult`. + +## 0.12.12 + +* Add a best practices section to readme. +* Populate the pubspec `repository` field. + +## 0.12.11 + +* Change many argument types from `dynamic` to `Object?`. +* Fix `stringContainsInOrder` to account for repetitions and empty strings. + * **Note**: This may break some existing tests, as the behavior does change. + +## 0.12.10 + +* Stable release for null safety. + +## 0.12.10-nullsafety.3 + +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 0.12.10-nullsafety.2 + +- Allow prerelease versions of the 2.12 sdk. + +## 0.12.10-nullsafety.1 + +- Allow 2.10 stable and 2.11.0 dev SDK versions. + +## 0.12.10-nullsafety + +- Migrate to NNBD. + - Apis have been updated to express intent of the existing code and how it + handled nulls. + +## 0.12.9 + +- Improve mismatch descriptions for deep matches. Previously, if the user tried + to do a deep match where the expectation included a complex matcher (such as a + "having" matcher), the failure message would just say "failed to match ..."; + it wouldn't call on the expectation's matcher to explain why the match failed. + +## 0.12.8 + +- Add a mismatch description to `TypeMatcher`. + +## 0.12.7 + +- Deprecate the `mirror_matchers.dart` library. + +## 0.12.6 + +- Update minimum Dart SDK to `2.2.0`. +- Consistently point to `isA` as a replacement for `instanceOf`. +- Pretty print with private type names. + +## 0.12.5 + +- Add `isA()` to create `TypeMatcher` instances in a more fluent way. +- **Potentially breaking bug fix**. Ordering matchers no longer treat objects + with a partial ordering (such as NaN for double values) as if they had a + complete ordering. For instance `greaterThan` now compares with the `>` + operator rather not `<` and not `=`. This could cause tests which relied on + this bug to start failing. + +## 0.12.4 + +- Add isCastError. + +## 0.12.3+1 + +- Set max SDK version to <3.0.0, and adjusted other dependencies. + +## 0.12.3 + +- Many improvements to `TypeMatcher` + - Can now be used directly as `const TypeMatcher()`. + - Added a type parameter to specify the target `Type`. + - Made the `name` constructor parameter optional and marked it deprecated. + It's redundant to the type parameter. + - Migrated all `isType` matchers to `TypeMatcher`. + - Added a `having` function that allows chained validations of specific + features of the target type. + + ```dart + /// Validates that the object is a [RangeError] with a message containing + /// the string 'details' and `start` and `end` properties that are `null`. + final _rangeMatcher = isRangeError + .having((e) => e.message, 'message', contains('details')) + .having((e) => e.start, 'start', isNull) + .having((e) => e.end, 'end', isNull); + ``` + +- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead. + +- Improved the output of `Matcher` instances that fail due to type errors. + +## 0.12.2+1 + +- Updated SDK version to 2.0.0-dev.17.0 + +## 0.12.2 + +* Fixed `unorderedMatches` in cases where the matchers may match more than one + element and order of the elements doesn't line up with the order of the + matchers. + +* Add containsAll matcher for Iterables. This Matcher checks that all + values/matchers in an expected iterable are satisfied by an element in the + value without allowing the same value to satisfy multiple matchers. + +## 0.12.1+4 + +* Fixed SDK constraint to allow edge builds. + +## 0.12.1+3 + +* Make `predicate` and `pairwiseCompare` generic methods to allow typed + functions to be passed to them as arguments. + +* Make internal implementations take better advantage of type promotion to avoid + dynamic call overhead. + +## 0.12.1+2 + +* Fixed small documentation issues. + +* Fixed small issue in `StringEqualsMatcher`. + +* Update to support future Dart language changes. + +## 0.12.1+1 + +* Produce a better error message when a `CustomMatcher`'s feature throws. + +## 0.12.1 + +* Add containsAllInOrder matcher for Iterables + +## 0.12.0+2 + +* Fix all strong-mode warnings. + +## 0.12.0+1 + +* Fix test files to use `test` instead of `unittest` pkg. + +## 0.12.0 + +* Moved a number of members to the + [`unittest`](https://pub.dev/packages/unittest) package. + * `TestFailure`, `ErrorFormatter`, `expect`, `fail`, and 'wrapAsync'. + * `completes`, `completion`, `throws`, and `throwsA` Matchers. + * The `Throws` class. + * All of the `throws...Error` Matchers. + +* Removed `FailureHandler`, `DefaultFailureHandler`, + `configureExpectFailureHandler`, and `getOrCreateExpectFailureHandler`. + Now that `expect` is in the `unittest` package, these are no longer needed. + +* Removed the `name` parameter for `isInstanceOf`. This was previously + deprecated, and is no longer necessary since all language implementations now + support converting the type parameter to a string directly. + +## 0.11.4+6 + +* Fix a bug introduced in 0.11.4+5 in which operator matchers broke when taking + lists of matchers. + +## 0.11.4+5 + +* Fix all strong-mode warnings. + +## 0.11.4+4 + +* Deprecate the name parameter to `isInstanceOf`. All language implementations + now support converting the type parameter to a string directly. + +## 0.11.4+3 + +* Fix the examples for `equalsIgnoringWhitespace`. + +## 0.11.4+2 + +* Improve the formatting of strings that contain unprintable ASCII characters. + +## 0.11.4+1 + +* Correctly match and print `String`s containing characters that must be + represented as escape sequences. + +## 0.11.4 + +* Remove the type checks in the `isEmpty` and `isNotEmpty` matchers and simply + access the `isEmpty` respectively `isNotEmpty` fields. This allows them to + work with custom collections. See [Issue + 21792](https://code.google.com/p/dart/issues/detail?id=21792) and [Issue + 21562](https://code.google.com/p/dart/issues/detail?id=21562) + +## 0.11.3+1 + +* Fix the `prints` matcher test on dart2js. + +## 0.11.3 + +* Add a `prints` matcher that matches output a callback emits via `print`. + +## 0.11.2 + +* Add an `isNotEmpty` matcher. + +## 0.11.1+1 + +* Refactored libraries and tests. + +* Fixed spelling mistake. + +## 0.11.1 + +* Added `isNaN` and `isNotNaN` matchers. + +## 0.11.0 + +* Removed deprecated matchers. + +## 0.10.1+1 + +* Get the tests passing when run on dart2js in minified mode. + +## 0.10.1 + +* Compare sets order-independently when using `equals()`. + +## 0.10.0+3 + +* Removed `@deprecated` annotation on matchers due to +[Issue 19173](https://code.google.com/p/dart/issues/detail?id=19173) + +## 0.10.0+2 + +* Added types to a number of constants. + +## 0.10.0+1 + +* Matchers related to bad language use have been removed. These represent code +structure that should rarely or never be validated in tests. + * `isAbstractClassInstantiationError` + * `throwsAbstractClassInstantiationError` + * `isFallThroughError` + * `throwsFallThroughError` + +* Added types to a number of method arguments. + +* The structure of the library and test code has been updated. diff --git a/pkgs/matcher/LICENSE b/pkgs/matcher/LICENSE new file mode 100644 index 000000000..000cd7bec --- /dev/null +++ b/pkgs/matcher/LICENSE @@ -0,0 +1,27 @@ +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/matcher/README.md b/pkgs/matcher/README.md new file mode 100644 index 000000000..30442389a --- /dev/null +++ b/pkgs/matcher/README.md @@ -0,0 +1,266 @@ +[![Dart CI](https://github.com/dart-lang/matcher/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/matcher/actions/workflows/ci.yml) +[![pub package](https://img.shields.io/pub/v/matcher.svg)](https://pub.dev/packages/matcher) +[![package publisher](https://img.shields.io/pub/publisher/matcher.svg)](https://pub.dev/packages/matcher/publisher) + +Support for specifying test expectations, such as for unit tests. + +The matcher library provides a third-generation assertion mechanism, drawing +inspiration from [Hamcrest](https://code.google.com/p/hamcrest/). + +For more information on testing, see +[Unit Testing with Dart](https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#writing-tests). + +## Using matcher + +Expectations start with a call to [`expect()`] or [`expectAsync()`]. + +[`expect()`]: https://pub.dev/documentation/matcher/latest/expect/expect.html +[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html + +Any matchers package can be used with `expect()` to do +complex validations: + +[`matcher`]: https://pub.dev/documentation/matcher/latest/matcher/matcher-library.html + +```dart +import 'package:test/test.dart'; + +void main() { + test('.split() splits the string on the delimiter', () { + expect('foo,bar,baz', allOf([ + contains('foo'), + isNot(startsWith('bar')), + endsWith('baz') + ])); + }); +} +``` + +If a non-matcher value is passed, it will be wrapped with [`equals()`]. + +[`equals()`]: https://pub.dev/documentation/matcher/latest/expect/equals.html + +## Exception matchers + +You can also test exceptions with the [`throwsA()`] function or a matcher such +as [`throwsFormatException`]: + +[`throwsA()`]: https://pub.dev/documentation/matcher/latest/expect/throwsA.html +[`throwsFormatException`]: https://pub.dev/documentation/matcher/latest/expect/throwsFormatException-constant.html + +```dart +import 'package:test/test.dart'; + +void main() { + test('.parse() fails on invalid input', () { + expect(() => int.parse('X'), throwsFormatException); + }); +} +``` + +### Future Matchers + +There are a number of useful functions and matchers for more advanced +asynchrony. The [`completion()`] matcher can be used to test `Futures`; it +ensures that the test doesn't finish until the `Future` completes, and runs a +matcher against that `Future`'s value. + +[`completion()`]: https://pub.dev/documentation/matcher/latest/expect/completion.html + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Future.value() returns the value', () { + expect(Future.value(10), completion(equals(10))); + }); +} +``` + +The [`throwsA()`] matcher and the various [`throwsExceptionType`] matchers work +with both synchronous callbacks and asynchronous `Future`s. They ensure that a +particular type of exception is thrown: + +[`throwsExceptionType`]: https://pub.dev/documentation/matcher/latest/expect/throwsException-constant.html + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Future.error() throws the error', () { + expect(Future.error('oh no'), throwsA(equals('oh no'))); + expect(Future.error(StateError('bad state')), throwsStateError); + }); +} +``` + +The [`expectAsync()`] function wraps another function and has two jobs. First, +it asserts that the wrapped function is called a certain number of times, and +will cause the test to fail if it's called too often; second, it keeps the test +from finishing until the function is called the requisite number of times. + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Stream.fromIterable() emits the values in the iterable', () { + var stream = Stream.fromIterable([1, 2, 3]); + + stream.listen(expectAsync1((number) { + expect(number, inInclusiveRange(1, 3)); + }, count: 3)); + }); +} +``` + +[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html + +### Stream Matchers + +The `test` package provides a suite of powerful matchers for dealing with +[asynchronous streams][Stream]. They're expressive and composable, and make it +easy to write complex expectations about the values emitted by a stream. For +example: + +[Stream]: https://api.dart.dev/stable/dart-async/Stream-class.html + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('process emits status messages', () { + // Dummy data to mimic something that might be emitted by a process. + var stdoutLines = Stream.fromIterable([ + 'Ready.', + 'Loading took 150ms.', + 'Succeeded!' + ]); + + expect(stdoutLines, emitsInOrder([ + // Values match individual events. + 'Ready.', + + // Matchers also run against individual events. + startsWith('Loading took'), + + // Stream matchers can be nested. This asserts that one of two events are + // emitted after the "Loading took" line. + emitsAnyOf(['Succeeded!', 'Failed!']), + + // By default, more events are allowed after the matcher finishes + // matching. This asserts instead that the stream emits a done event and + // nothing else. + emitsDone + ])); + }); +} +``` + +A stream matcher can also match the [`async`] package's [`StreamQueue`] class, +which allows events to be requested from a stream rather than pushed to the +consumer. The matcher will consume the matched events, but leave the rest of the +queue alone so that it can still be used by the test, unlike a normal `Stream` +which can only have one subscriber. For example: + +[`async`]: https://pub.dev/packages/async +[`StreamQueue`]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html + +```dart +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + test('process emits a WebSocket URL', () async { + // Wrap the Stream in a StreamQueue so that we can request events. + var stdout = StreamQueue(Stream.fromIterable([ + 'WebSocket URL:', + 'ws://localhost:1234/', + 'Waiting for connection...' + ])); + + // Ignore lines from the process until it's about to emit the URL. + await expectLater(stdout, emitsThrough('WebSocket URL:')); + + // Parse the next line as a URL. + var url = Uri.parse(await stdout.next); + expect(url.host, equals('localhost')); + + // You can match against the same StreamQueue multiple times. + await expectLater(stdout, emits('Waiting for connection...')); + }); +} +``` + +The following built-in stream matchers are available: + +* [`emits()`] matches a single data event. +* [`emitsError()`] matches a single error event. +* [`emitsDone`] matches a single done event. +* [`mayEmit()`] consumes events if they match an inner matcher, without + requiring them to match. +* [`mayEmitMultiple()`] works like `mayEmit()`, but it matches events against + the matcher as many times as possible. +* [`emitsAnyOf()`] consumes events matching one (or more) of several possible + matchers. +* [`emitsInOrder()`] consumes events matching multiple matchers in a row. +* [`emitsInAnyOrder()`] works like `emitsInOrder()`, but it allows the + matchers to match in any order. +* [`neverEmits()`] matches a stream that finishes *without* matching an inner + matcher. + +You can also define your own custom stream matchers with [`StreamMatcher()`]. + +[`emits()`]: https://pub.dev/documentation/matcher/latest/expect/emits.html +[`emitsError()`]: https://pub.dev/documentation/matcher/latest/expect/emitsError.html +[`emitsDone`]: https://pub.dev/documentation/matcher/latest/expect/emitsDone.html +[`mayEmit()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmit.html +[`mayEmitMultiple()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmitMultiple.html +[`emitsAnyOf()`]: https://pub.dev/documentation/matcher/latest/expect/emitsAnyOf.html +[`emitsInOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInOrder.html +[`emitsInAnyOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInAnyOrder.html +[`neverEmits()`]: https://pub.dev/documentation/matcher/latest/expect/neverEmits.html +[`StreamMatcher()`]: https://pub.dev/documentation/matcher/latest/expect/StreamMatcher-class.html + +## Best Practices + +### Prefer semantically meaningful matchers to comparing derived values + +Matchers which have knowledge of the semantics that are tested are able to emit +more meaningful messages which don't require reading test source to understand +why the test failed. For instance compare the failures between +`expect(someList.length, 1)`, and `expect(someList, hasLength(1))`: + +``` +// expect(someList.length, 1); + Expected: <1> + Actual: <2> +``` + +``` +// expect(someList, hasLength(1)); + Expected: an object with length of <1> + Actual: ['expected value', 'unexpected value'] + Which: has length of <2> + +``` + +### Prefer TypeMatcher to predicate if the match can fail in multiple ways + +The `predicate` utility is a convenient shortcut for testing an arbitrary +(synchronous) property of a value, but it discards context and failures are +opaque. Different failure modes cannot be distinguished in the output which is +determined by a single "description" argument. Using `isA()` and the +`TypeMatcher.having` API to extract and test derived properties in a structured +way brings the context of that structure through to failure messages, so +failures for different reasons will have distinguishable and actionable failure +messages. diff --git a/pkgs/matcher/analysis_options.yaml b/pkgs/matcher/analysis_options.yaml new file mode 100644 index 000000000..f0f137ff3 --- /dev/null +++ b/pkgs/matcher/analysis_options.yaml @@ -0,0 +1,30 @@ +include: package:lints/recommended.yaml + +linter: + rules: + - always_declare_return_types + - avoid_private_typedef_functions + - avoid_unused_constructor_parameters + - cancel_subscriptions + - comment_references + - directives_ordering + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - omit_local_variable_types + - only_throw_errors + - package_api_docs + - prefer_const_constructors + - prefer_relative_imports + - prefer_single_quotes + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_lambdas + - unnecessary_parenthesis + - unnecessary_statements + - use_super_parameters diff --git a/pkgs/matcher/lib/expect.dart b/pkgs/matcher/lib/expect.dart new file mode 100644 index 000000000..c842d302f --- /dev/null +++ b/pkgs/matcher/lib/expect.dart @@ -0,0 +1,64 @@ +// Copyright (c) 2021, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +export 'matcher.dart'; + +export 'src/expect/expect.dart' show ErrorFormatter, expect, expectLater, fail; +export 'src/expect/expect_async.dart' + show + Func0, + Func1, + Func2, + Func3, + Func4, + Func5, + Func6, + expectAsync, + expectAsync0, + expectAsync1, + expectAsync2, + expectAsync3, + expectAsync4, + expectAsync5, + expectAsync6, + expectAsyncUntil0, + expectAsyncUntil1, + expectAsyncUntil2, + expectAsyncUntil3, + expectAsyncUntil4, + expectAsyncUntil5, + expectAsyncUntil6; +export 'src/expect/future_matchers.dart' + show completes, completion, doesNotComplete; +export 'src/expect/never_called.dart' show neverCalled; +export 'src/expect/prints_matcher.dart' show prints; +export 'src/expect/stream_matcher.dart' show StreamMatcher; +export 'src/expect/stream_matchers.dart' + show + emitsDone, + emits, + emitsError, + mayEmit, + emitsAnyOf, + emitsInOrder, + emitsInAnyOrder, + emitsThrough, + mayEmitMultiple, + neverEmits; +export 'src/expect/throws_matcher.dart' show Throws, throws, throwsA; +export 'src/expect/throws_matchers.dart' + show + throwsArgumentError, + throwsConcurrentModificationError, + throwsCyclicInitializationError, + throwsException, + throwsFormatException, + throwsNoSuchMethodError, + throwsNullThrownError, + throwsRangeError, + throwsStateError, + throwsUnimplementedError, + throwsUnsupportedError; diff --git a/pkgs/matcher/lib/matcher.dart b/pkgs/matcher/lib/matcher.dart new file mode 100644 index 000000000..236d6f44a --- /dev/null +++ b/pkgs/matcher/lib/matcher.dart @@ -0,0 +1,21 @@ +// 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. + +/// Support for specifying test expectations, such as for unit tests. +library; + +export 'src/core_matchers.dart'; +export 'src/custom_matcher.dart'; +export 'src/description.dart'; +export 'src/equals_matcher.dart'; +export 'src/error_matchers.dart'; +export 'src/interfaces.dart'; +export 'src/iterable_matchers.dart'; +export 'src/map_matchers.dart'; +export 'src/numeric_matchers.dart'; +export 'src/operator_matchers.dart'; +export 'src/order_matchers.dart'; +export 'src/string_matchers.dart'; +export 'src/type_matcher.dart'; +export 'src/util.dart'; diff --git a/pkgs/matcher/lib/mirror_matchers.dart b/pkgs/matcher/lib/mirror_matchers.dart new file mode 100644 index 000000000..ff001f812 --- /dev/null +++ b/pkgs/matcher/lib/mirror_matchers.dart @@ -0,0 +1,85 @@ +// 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. + +@Deprecated('Check properties on known types') +library mirror_matchers; + +/// The mirror matchers library provides some additional matchers that +/// make use of `dart:mirrors`. +import 'dart:mirrors'; + +import 'matcher.dart'; + +/// Returns a matcher that checks if a class instance has a property +/// with name [name], and optionally, if that property in turn satisfies +/// a [matcher]. +Matcher hasProperty(String name, [Object? matcher]) => + _HasProperty(name, matcher == null ? null : wrapMatcher(matcher)); + +class _HasProperty extends Matcher { + final String _name; + final Matcher? _matcher; + + const _HasProperty(this._name, [this._matcher]); + + @override + bool matches(Object? item, Map matchState) { + var mirror = reflect(item); + var classMirror = mirror.type; + var symbol = Symbol(_name); + var candidate = classMirror.declarations[symbol]; + if (candidate == null) { + addStateInfo(matchState, {'reason': 'has no property named "$_name"'}); + return false; + } + var isInstanceField = candidate is VariableMirror && !candidate.isStatic; + var isInstanceGetter = + candidate is MethodMirror && candidate.isGetter && !candidate.isStatic; + if (!(isInstanceField || isInstanceGetter)) { + addStateInfo(matchState, { + 'reason': + 'has a member named "$_name", but it is not an instance property' + }); + return false; + } + var matcher = _matcher; + if (matcher == null) return true; + var result = mirror.getField(symbol); + var resultMatches = matcher.matches(result.reflectee, matchState); + if (!resultMatches) { + addStateInfo(matchState, {'value': result.reflectee}); + } + return resultMatches; + } + + @override + Description describe(Description description) { + description.add('has property "$_name"'); + if (_matcher != null) { + description.add(' which matches ').addDescriptionOf(_matcher); + } + return description; + } + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + var reason = matchState['reason']; + if (reason != null) { + mismatchDescription.add(reason as String); + } else { + mismatchDescription + .add('has property "$_name" with value ') + .addDescriptionOf(matchState['value']); + var innerDescription = StringDescription(); + matchState['state'] ??= {}; + _matcher?.describeMismatch(matchState['value'], innerDescription, + matchState['state'] as Map, verbose); + if (innerDescription.length > 0) { + mismatchDescription.add(' which ').add(innerDescription.toString()); + } + } + return mismatchDescription; + } +} diff --git a/pkgs/matcher/lib/src/core_matchers.dart b/pkgs/matcher/lib/src/core_matchers.dart new file mode 100644 index 000000000..70e34491d --- /dev/null +++ b/pkgs/matcher/lib/src/core_matchers.dart @@ -0,0 +1,326 @@ +// Copyright (c) 2012, 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 'feature_matcher.dart'; +import 'interfaces.dart'; +import 'type_matcher.dart'; +import 'util.dart'; + +/// Returns a matcher that matches the isEmpty property. +const Matcher isEmpty = _Empty(); + +class _Empty extends Matcher { + const _Empty(); + + @override + bool matches(Object? item, Map matchState) => (item as dynamic).isEmpty; + + @override + Description describe(Description description) => description.add('empty'); +} + +/// Returns a matcher that matches the isNotEmpty property. +const Matcher isNotEmpty = _NotEmpty(); + +class _NotEmpty extends Matcher { + const _NotEmpty(); + + @override + bool matches(Object? item, Map matchState) => (item as dynamic).isNotEmpty; + + @override + Description describe(Description description) => description.add('non-empty'); +} + +/// A matcher that matches any null value. +const Matcher isNull = _IsNull(); + +/// A matcher that matches any non-null value. +const Matcher isNotNull = _IsNotNull(); + +class _IsNull extends Matcher { + const _IsNull(); + @override + bool matches(Object? item, Map matchState) => item == null; + @override + Description describe(Description description) => description.add('null'); +} + +class _IsNotNull extends Matcher { + const _IsNotNull(); + @override + bool matches(Object? item, Map matchState) => item != null; + @override + Description describe(Description description) => description.add('not null'); +} + +/// A matcher that matches the Boolean value true. +const Matcher isTrue = _IsTrue(); + +/// A matcher that matches anything except the Boolean value true. +const Matcher isFalse = _IsFalse(); + +class _IsTrue extends Matcher { + const _IsTrue(); + @override + bool matches(Object? item, Map matchState) => item == true; + @override + Description describe(Description description) => description.add('true'); +} + +class _IsFalse extends Matcher { + const _IsFalse(); + @override + bool matches(Object? item, Map matchState) => item == false; + @override + Description describe(Description description) => description.add('false'); +} + +/// A matcher that matches the numeric value NaN. +const Matcher isNaN = _IsNaN(); + +/// A matcher that matches any non-NaN value. +const Matcher isNotNaN = _IsNotNaN(); + +class _IsNaN extends FeatureMatcher { + const _IsNaN(); + @override + bool typedMatches(num item, Map matchState) => + double.nan.compareTo(item) == 0; + @override + Description describe(Description description) => description.add('NaN'); +} + +class _IsNotNaN extends FeatureMatcher { + const _IsNotNaN(); + @override + bool typedMatches(num item, Map matchState) => + double.nan.compareTo(item) != 0; + @override + Description describe(Description description) => description.add('not NaN'); +} + +/// Returns a matches that matches if the value is the same instance +/// as [expected], using [identical]. +Matcher same(Object? expected) => _IsSameAs(expected); + +class _IsSameAs extends Matcher { + final Object? _expected; + const _IsSameAs(this._expected); + @override + bool matches(Object? item, Map matchState) => identical(item, _expected); + // If all types were hashable we could show a hash here. + @override + Description describe(Description description) => + description.add('same instance as ').addDescriptionOf(_expected); +} + +/// A matcher that matches any value. +const Matcher anything = _IsAnything(); + +class _IsAnything extends Matcher { + const _IsAnything(); + @override + bool matches(Object? item, Map matchState) => true; + @override + Description describe(Description description) => description.add('anything'); +} + +/// **DEPRECATED** Use [isA] instead. +/// +/// A matcher that matches if an object is an instance of [T] (or a subtype). +@Deprecated('Use `isA()` instead.') +// ignore: camel_case_types +class isInstanceOf extends TypeMatcher { + const isInstanceOf(); +} + +/// A matcher that matches a function call against no exception. +/// +/// The function will be called once. Any exceptions will be silently swallowed. +/// The value passed to expect() should be a reference to the function. +/// Note that the function cannot take arguments; to handle this +/// a wrapper will have to be created. +const Matcher returnsNormally = _ReturnsNormally(); + +class _ReturnsNormally extends FeatureMatcher { + const _ReturnsNormally(); + + @override + bool typedMatches(Function f, Map matchState) { + try { + f(); + return true; + } catch (e, s) { + addStateInfo(matchState, {'exception': e, 'stack': s}); + return false; + } + } + + @override + Description describe(Description description) => + description.add('return normally'); + + @override + Description describeTypedMismatch(Function item, + Description mismatchDescription, Map matchState, bool verbose) { + mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']); + if (verbose) { + mismatchDescription.add(' at ').add(matchState['stack'].toString()); + } + return mismatchDescription; + } +} + +/// A matcher for [Map]. +const isMap = TypeMatcher(); + +/// A matcher for [List]. +const isList = TypeMatcher(); + +/// Returns a matcher that matches if an object has a length property +/// that matches [matcher]. +Matcher hasLength(Object? matcher) => _HasLength(wrapMatcher(matcher)); + +class _HasLength extends Matcher { + final Matcher _matcher; + const _HasLength(this._matcher); + + @override + bool matches(Object? item, Map matchState) { + try { + final length = (item as dynamic).length; + return _matcher.matches(length, matchState); + } catch (e) { + return false; + } + } + + @override + Description describe(Description description) => + description.add('an object with length of ').addDescriptionOf(_matcher); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + try { + final length = (item as dynamic).length; + return mismatchDescription.add('has length of ').addDescriptionOf(length); + } catch (e) { + return mismatchDescription.add('has no length property'); + } + } +} + +/// Returns a matcher that matches if the match argument contains the expected +/// value. +/// +/// For [String]s this means substring matching; +/// for [Map]s it means the map has the key, and for [Iterable]s +/// it means the iterable has a matching element. In the case of iterables, +/// [expected] can itself be a matcher. +Matcher contains(Object? expected) => _Contains(expected); + +class _Contains extends Matcher { + final Object? _expected; + + const _Contains(this._expected); + + @override + bool matches(Object? item, Map matchState) { + var expected = _expected; + if (item is String) { + return expected is Pattern && item.contains(expected); + } else if (item is Iterable) { + if (expected is Matcher) { + return item.any((e) => expected.matches(e, matchState)); + } else { + return item.contains(_expected); + } + } else if (item is Map) { + return item.containsKey(_expected); + } + return false; + } + + @override + Description describe(Description description) => + description.add('contains ').addDescriptionOf(_expected); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (item is String || item is Iterable || item is Map) { + super.describeMismatch(item, mismatchDescription, matchState, verbose); + mismatchDescription.add('does not contain ').addDescriptionOf(_expected); + return mismatchDescription; + } else { + return mismatchDescription.add('is not a string, map or iterable'); + } + } +} + +/// Returns a matcher that matches if the match argument is in +/// the expected value. This is the converse of [contains]. +Matcher isIn(Object? expected) { + if (expected is Iterable) { + return _In(expected, expected.contains); + } else if (expected is String) { + return _In(expected, expected.contains); + } else if (expected is Map) { + return _In(expected, expected.containsKey); + } + + throw ArgumentError.value( + expected, 'expected', 'Only Iterable, Map, and String are supported.'); +} + +class _In extends FeatureMatcher { + final Object _source; + final bool Function(T) _containsFunction; + + const _In(this._source, this._containsFunction); + + @override + bool typedMatches(T item, Map matchState) => _containsFunction(item); + + @override + Description describe(Description description) => + description.add('is in ').addDescriptionOf(_source); +} + +/// Returns a matcher that uses an arbitrary function that returns whether the +/// value is considered a match. +/// +/// For example: +/// +/// expect(actual, predicate((v) => (v % 2) == 0, 'is even')); +/// +/// Use this method when a value is checked for one conceptual property +/// described by [description]. +/// +/// If the value can be rejected for more than one reason prefer using [isA] and +/// the [TypeMatcher.having] API to build up a matcher with output that can +/// distinquish between them. +/// +/// Using an explicit generict argument allows a passed function literal to have +/// an inferred argument type of [T], and values of the wrong type will be +/// rejected with an informative message. +Matcher predicate(bool Function(T) f, + [String description = 'satisfies function']) => + _Predicate(f, description); + +class _Predicate extends FeatureMatcher { + final bool Function(T) _matcher; + final String _description; + + _Predicate(this._matcher, this._description); + + @override + bool typedMatches(T item, Map matchState) => _matcher(item); + + @override + Description describe(Description description) => + description.add(_description); +} diff --git a/pkgs/matcher/lib/src/custom_matcher.dart b/pkgs/matcher/lib/src/custom_matcher.dart new file mode 100644 index 000000000..b0f2d6b6b --- /dev/null +++ b/pkgs/matcher/lib/src/custom_matcher.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2012, 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 'package:stack_trace/stack_trace.dart'; + +import 'description.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// A base class for [Matcher] instances that match based on some feature of the +/// value under test. +/// +/// Derived classes should call the base constructor with a feature name and +/// description, and an instance matcher, and should implement the +/// [featureValueOf] abstract method. +/// +/// The feature description will typically describe the item and the feature, +/// while the feature name will just name the feature. For example, we may +/// have a Widget class where each Widget has a price; we could make a +/// [CustomMatcher] that can make assertions about prices with: +/// +/// ```dart +/// class HasPrice extends CustomMatcher { +/// HasPrice(matcher) : super("Widget with price that is", "price", matcher); +/// featureValueOf(actual) => (actual as Widget).price; +/// } +/// ``` +/// +/// and then use this for example like: +/// +/// ```dart +/// expect(inventoryItem, HasPrice(greaterThan(0))); +/// ``` +class CustomMatcher extends Matcher { + final String _featureDescription; + final String _featureName; + final Matcher _matcher; + + CustomMatcher( + this._featureDescription, this._featureName, Object? valueOrMatcher) + : _matcher = wrapMatcher(valueOrMatcher); + + /// Override this to extract the interesting feature. + Object? featureValueOf(dynamic actual) => actual; + + @override + bool matches(Object? item, Map matchState) { + try { + var f = featureValueOf(item); + if (_matcher.matches(f, matchState)) return true; + addStateInfo(matchState, {'custom.feature': f}); + } catch (exception, stack) { + addStateInfo(matchState, { + 'custom.exception': exception.toString(), + 'custom.stack': Chain.forTrace(stack) + .foldFrames( + (frame) => + frame.package == 'test' || + frame.package == 'stream_channel' || + frame.package == 'matcher', + terse: true) + .toString() + }); + } + return false; + } + + @override + Description describe(Description description) => + description.add(_featureDescription).add(' ').addDescriptionOf(_matcher); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (matchState['custom.exception'] != null) { + mismatchDescription + .add('threw ') + .addDescriptionOf(matchState['custom.exception']) + .add('\n') + .add(matchState['custom.stack'].toString()); + return mismatchDescription; + } + + mismatchDescription + .add('has ') + .add(_featureName) + .add(' with value ') + .addDescriptionOf(matchState['custom.feature']); + var innerDescription = StringDescription(); + + _matcher.describeMismatch(matchState['custom.feature'], innerDescription, + matchState['state'] as Map, verbose); + + if (innerDescription.length > 0) { + mismatchDescription.add(' which ').add(innerDescription.toString()); + } + return mismatchDescription; + } +} diff --git a/pkgs/matcher/lib/src/description.dart b/pkgs/matcher/lib/src/description.dart new file mode 100644 index 000000000..090aada06 --- /dev/null +++ b/pkgs/matcher/lib/src/description.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2012, 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 'interfaces.dart'; +import 'pretty_print.dart'; + +/// The default implementation of [Description]. This should rarely need +/// substitution, although conceivably it is a place where other languages +/// could be supported. +class StringDescription implements Description { + final StringBuffer _out = StringBuffer(); + + /// Initialize the description with initial contents [init]. + StringDescription([String init = '']) { + _out.write(init); + } + + @override + int get length => _out.length; + + /// Get the description as a string. + @override + String toString() => _out.toString(); + + /// Append [text] to the description. + @override + Description add(String text) { + _out.write(text); + return this; + } + + /// Change the value of the description. + @override + Description replace(String text) { + _out.clear(); + return add(text); + } + + /// Appends a description of [value]. If it is an IMatcher use its + /// describe method; if it is a string use its literal value after + /// escaping any embedded control characters; otherwise use its + /// toString() value and wrap it in angular "quotes". + @override + Description addDescriptionOf(Object? value) { + if (value is Matcher) { + value.describe(this); + } else { + add(prettyPrint(value, maxLineLength: 80, maxItems: 25)); + } + return this; + } + + /// Append an [Iterable] [list] of objects to the description, using the + /// specified [separator] and framing the list with [start] + /// and [end]. + @override + Description addAll( + String start, String separator, String end, Iterable list) { + var separate = false; + add(start); + for (var item in list) { + if (separate) { + add(separator); + } + addDescriptionOf(item); + separate = true; + } + add(end); + return this; + } +} diff --git a/pkgs/matcher/lib/src/equals_matcher.dart b/pkgs/matcher/lib/src/equals_matcher.dart new file mode 100644 index 000000000..5c4f4c574 --- /dev/null +++ b/pkgs/matcher/lib/src/equals_matcher.dart @@ -0,0 +1,327 @@ +// Copyright (c) 2018, 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 'feature_matcher.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher that matches if the value is structurally equal to +/// [expected]. +/// +/// If [expected] is a [Matcher], then it matches using that. Otherwise it tests +/// for equality using `==` on the expected value. +/// +/// For [Iterable]s and [Map]s, this will recursively match the elements. To +/// handle cyclic structures a recursion depth [limit] can be provided. The +/// default limit is 100. [Set]s will be compared order-independently. +Matcher equals(Object? expected, [int limit = 100]) => expected is String + ? _StringEqualsMatcher(expected) + : _DeepMatcher(expected, limit); + +typedef _RecursiveMatcher = _Mismatch? Function(Object?, Object?, String, int); + +/// A special equality matcher for strings. +class _StringEqualsMatcher extends FeatureMatcher { + final String _value; + + _StringEqualsMatcher(this._value); + + @override + bool typedMatches(String item, Map matchState) => _value == item; + + @override + Description describe(Description description) => + description.addDescriptionOf(_value); + + @override + Description describeTypedMismatch(String item, + Description mismatchDescription, Map matchState, bool verbose) { + var buff = StringBuffer(); + buff.write('is different.'); + var escapedItem = escape(item); + var escapedValue = escape(_value); + var minLength = escapedItem.length < escapedValue.length + ? escapedItem.length + : escapedValue.length; + var start = 0; + for (; start < minLength; start++) { + if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) { + break; + } + } + if (start == minLength) { + if (escapedValue.length < escapedItem.length) { + buff.write(' Both strings start the same, but the actual value also' + ' has the following trailing characters: '); + _writeTrailing(buff, escapedItem, escapedValue.length); + } else { + buff.write(' Both strings start the same, but the actual value is' + ' missing the following trailing characters: '); + _writeTrailing(buff, escapedValue, escapedItem.length); + } + } else { + buff.write('\nExpected: '); + _writeLeading(buff, escapedValue, start); + _writeTrailing(buff, escapedValue, start); + buff.write('\n Actual: '); + _writeLeading(buff, escapedItem, start); + _writeTrailing(buff, escapedItem, start); + buff.write('\n '); + for (var i = start > 10 ? 14 : start; i > 0; i--) { + buff.write(' '); + } + buff.write('^\n Differ at offset $start'); + } + + return mismatchDescription.add(buff.toString()); + } + + static void _writeLeading(StringBuffer buff, String s, int start) { + if (start > 10) { + buff.write('... '); + buff.write(s.substring(start - 10, start)); + } else { + buff.write(s.substring(0, start)); + } + } + + static void _writeTrailing(StringBuffer buff, String s, int start) { + if (start + 10 > s.length) { + buff.write(s.substring(start)); + } else { + buff.write(s.substring(start, start + 10)); + buff.write(' ...'); + } + } +} + +class _DeepMatcher extends Matcher { + final Object? _expected; + final int _limit; + + _DeepMatcher(this._expected, [int limit = 1000]) : _limit = limit; + + _Mismatch? _compareIterables(Iterable expected, Object? actual, + _RecursiveMatcher matcher, int depth, String location) { + if (actual is Iterable) { + var expectedIterator = expected.iterator; + var actualIterator = actual.iterator; + for (var index = 0;; index++) { + // Advance in lockstep. + var expectedNext = expectedIterator.moveNext(); + var actualNext = actualIterator.moveNext(); + + // If we reached the end of both, we succeeded. + if (!expectedNext && !actualNext) return null; + + // Fail if their lengths are different. + var newLocation = '$location[$index]'; + if (!expectedNext) { + return _Mismatch.simple(newLocation, actual, 'longer than expected'); + } + if (!actualNext) { + return _Mismatch.simple(newLocation, actual, 'shorter than expected'); + } + + // Match the elements. + var rp = matcher(expectedIterator.current, actualIterator.current, + newLocation, depth); + if (rp != null) return rp; + } + } else { + return _Mismatch.simple(location, actual, 'is not Iterable'); + } + } + + _Mismatch? _compareSets(Set expected, Object? actual, + _RecursiveMatcher matcher, int depth, String location) { + if (actual is Iterable) { + var other = actual.toSet(); + + for (var expectedElement in expected) { + if (other.every((actualElement) => + matcher(expectedElement, actualElement, location, depth) != null)) { + return _Mismatch( + location, + actual, + (description, verbose) => description + .add('does not contain ') + .addDescriptionOf(expectedElement)); + } + } + + if (other.length > expected.length) { + return _Mismatch.simple(location, actual, 'larger than expected'); + } else if (other.length < expected.length) { + return _Mismatch.simple(location, actual, 'smaller than expected'); + } else { + return null; + } + } else { + return _Mismatch.simple(location, actual, 'is not Iterable'); + } + } + + _Mismatch? _recursiveMatch( + Object? expected, Object? actual, String location, int depth) { + // If the expected value is a matcher, try to match it. + if (expected is Matcher) { + var matchState = {}; + if (expected.matches(actual, matchState)) return null; + return _Mismatch(location, actual, (description, verbose) { + var oldLength = description.length; + expected.describeMismatch(actual, description, matchState, verbose); + if (depth > 0 && description.length == oldLength) { + description.add('does not match '); + expected.describe(description); + } + }); + } else { + // Otherwise, test for equality. + try { + if (expected == actual) return null; + } catch (e) { + // TODO(gram): Add a test for this case. + return _Mismatch( + location, + actual, + (description, verbose) => + description.add('== threw ').addDescriptionOf(e)); + } + } + + if (depth > _limit) { + return _Mismatch.simple( + location, actual, 'recursion depth limit exceeded'); + } + + // If _limit is 1 we can only recurse one level into object. + if (depth == 0 || _limit > 1) { + if (expected is Set) { + return _compareSets( + expected, actual, _recursiveMatch, depth + 1, location); + } else if (expected is Iterable) { + return _compareIterables( + expected, actual, _recursiveMatch, depth + 1, location); + } else if (expected is Map) { + if (actual is! Map) { + return _Mismatch.simple(location, actual, 'expected a map'); + } + var err = (expected.length == actual.length) + ? '' + : 'has different length and '; + for (var key in expected.keys) { + if (!actual.containsKey(key)) { + return _Mismatch( + location, + actual, + (description, verbose) => description + .add('${err}is missing map key ') + .addDescriptionOf(key)); + } + } + + for (var key in actual.keys) { + if (!expected.containsKey(key)) { + return _Mismatch( + location, + actual, + (description, verbose) => description + .add('${err}has extra map key ') + .addDescriptionOf(key)); + } + } + + for (var key in expected.keys) { + var rp = _recursiveMatch( + expected[key], actual[key], "$location['$key']", depth + 1); + if (rp != null) return rp; + } + + return null; + } + } + + // If we have recursed, show the expected value too; if not, expect() will + // show it for us. + if (depth > 0) { + return _Mismatch(location, actual, + (description, verbose) => description.addDescriptionOf(expected), + instead: true); + } else { + return _Mismatch(location, actual, null); + } + } + + @override + bool matches(Object? actual, Map matchState) { + var mismatch = _recursiveMatch(_expected, actual, '', 0); + if (mismatch == null) return true; + addStateInfo(matchState, {'mismatch': mismatch}); + return false; + } + + @override + Description describe(Description description) => + description.addDescriptionOf(_expected); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + var mismatch = matchState['mismatch'] as _Mismatch; + var describeProblem = mismatch.describeProblem; + if (mismatch.location.isNotEmpty) { + mismatchDescription + .add('at location ') + .add(mismatch.location) + .add(' is ') + .addDescriptionOf(mismatch.actual); + if (describeProblem != null) { + mismatchDescription + .add(' ${mismatch.instead ? 'instead of' : 'which'} '); + describeProblem(mismatchDescription, verbose); + } + } else { + // If we didn't get a good reason, that would normally be a + // simple 'is ' message. We only add that if the mismatch + // description is non empty (so we are supplementing the mismatch + // description). + if (describeProblem == null) { + if (mismatchDescription.length > 0) { + mismatchDescription.add('is ').addDescriptionOf(item); + } + } else { + describeProblem(mismatchDescription, verbose); + } + } + return mismatchDescription; + } +} + +class _Mismatch { + /// A human-readable description of the location within the collection where + /// the mismatch occurred. + final String location; + + /// The actual value found at [location]. + final Object? actual; + + /// Callback that can create a detailed description of the problem. + final void Function(Description, bool verbose)? describeProblem; + + /// If `true`, [describeProblem] describes the expected value, so when the + /// final mismatch description is pieced together, it will be preceded by + /// `instead of` (e.g. `at location [2] is <3> instead of <4>`). If `false`, + /// [describeProblem] is a problem description from a sub-matcher, so when the + /// final mismatch description is pieced together, it will be preceded by + /// `which` (e.g. `at location [2] is which has length of 3`). + final bool instead; + + _Mismatch(this.location, this.actual, this.describeProblem, + {this.instead = false}); + + _Mismatch.simple(this.location, this.actual, String problem) + : describeProblem = ((description, verbose) => description.add(problem)), + instead = false; +} diff --git a/pkgs/matcher/lib/src/error_matchers.dart b/pkgs/matcher/lib/src/error_matchers.dart new file mode 100644 index 000000000..5b6238d98 --- /dev/null +++ b/pkgs/matcher/lib/src/error_matchers.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2012, 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 'type_matcher.dart'; + +/// A matcher for [ArgumentError]. +const isArgumentError = TypeMatcher(); + +/// A matcher for [TypeError]. +@Deprecated('CastError has been deprecated in favor of TypeError. ') +const isCastError = TypeMatcher(); + +/// A matcher for [ConcurrentModificationError]. +const isConcurrentModificationError = + TypeMatcher(); + +/// A matcher for [Error]. +@Deprecated( + 'CyclicInitializationError is deprecated and will be removed in Dart 3. ' + 'Use `isA()` instead.') +const isCyclicInitializationError = TypeMatcher(); + +/// A matcher for [Exception]. +const isException = TypeMatcher(); + +/// A matcher for [FormatException]. +const isFormatException = TypeMatcher(); + +/// A matcher for [NoSuchMethodError]. +const isNoSuchMethodError = TypeMatcher(); + +/// A matcher for [TypeError]. +@Deprecated('NullThrownError is deprecated and will be removed in Dart 3. ' + 'Use `isA()` instead.') +const isNullThrownError = TypeMatcher(); + +/// A matcher for [RangeError]. +const isRangeError = TypeMatcher(); + +/// A matcher for [StateError]. +const isStateError = TypeMatcher(); + +/// A matcher for [UnimplementedError]. +const isUnimplementedError = TypeMatcher(); + +/// A matcher for [UnsupportedError]. +const isUnsupportedError = TypeMatcher(); diff --git a/pkgs/matcher/lib/src/expect/async_matcher.dart b/pkgs/matcher/lib/src/expect/async_matcher.dart new file mode 100644 index 000000000..854151d5f --- /dev/null +++ b/pkgs/matcher/lib/src/expect/async_matcher.dart @@ -0,0 +1,68 @@ +// Copyright (c) 2017, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart'; + +import '../description.dart'; +import '../equals_matcher.dart'; +import '../interfaces.dart'; +import '../operator_matchers.dart'; +import '../type_matcher.dart'; +import 'expect.dart'; + +/// A matcher that does asynchronous computation. +/// +/// Rather than implementing [matches], subclasses implement [matchAsync]. +/// [AsyncMatcher.matches] ensures that the test doesn't complete until the +/// returned future completes, and [expect] returns a future that completes when +/// the returned future completes so that tests can wait for it. +abstract class AsyncMatcher extends Matcher { + const AsyncMatcher(); + + /// Returns `null` if this matches [item], or a [String] description of the + /// failure if it doesn't match. + /// + /// This can return a [Future] or a synchronous value. If it returns a + /// [Future], neither [expect] nor the test will complete until that [Future] + /// completes. + /// + /// If this returns a [String] synchronously, [expect] will synchronously + /// throw a [TestFailure] and [matches] will synchronously return `false`. + dynamic /*FutureOr*/ matchAsync(dynamic item); + + @override + bool matches(dynamic item, Map matchState) { + final result = matchAsync(item); + expect( + result, + anyOf([ + equals(null), + const TypeMatcher(), + const TypeMatcher() + ]), + reason: 'matchAsync() may only return a String, a Future, or null.'); + + if (result is Future) { + final outstandingWork = TestHandle.current.markPending(); + result.then((realResult) { + if (realResult != null) { + fail(formatFailure(this, item, realResult as String)); + } + outstandingWork.complete(); + }); + } else if (result is String) { + matchState[this] = result; + return false; + } + + return true; + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) => + StringDescription(matchState[this] as String); +} diff --git a/pkgs/matcher/lib/src/expect/expect.dart b/pkgs/matcher/lib/src/expect/expect.dart new file mode 100644 index 000000000..8dd8cae4c --- /dev/null +++ b/pkgs/matcher/lib/src/expect/expect.dart @@ -0,0 +1,161 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart'; + +import '../description.dart'; +import '../equals_matcher.dart'; +import '../interfaces.dart'; +import '../operator_matchers.dart'; +import '../type_matcher.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'future_matchers.dart'; +import 'prints_matcher.dart'; +import 'throws_matcher.dart'; +import 'util/pretty_print.dart'; + +/// The type used for functions that can be used to build up error reports +/// upon failures in [expect]. +@Deprecated('Will be removed in 0.13.0.') +typedef ErrorFormatter = String Function(Object? actual, Matcher matcher, + String? reason, Map matchState, bool verbose); + +/// Assert that [actual] matches [matcher]. +/// +/// This is the main assertion function. [reason] is optional and is typically +/// not supplied, as a reason is generated from [matcher]; if [reason] +/// is included it is appended to the reason generated by the matcher. +/// +/// [matcher] can be a value in which case it will be wrapped in an +/// [equals] matcher. +/// +/// If the assertion fails a [TestFailure] is thrown. +/// +/// If [skip] is a String or `true`, the assertion is skipped. The arguments are +/// still evaluated, but [actual] is not verified to match [matcher]. If +/// [actual] is a [Future], the test won't complete until the future emits a +/// value. +/// +/// If [skip] is a string, it should explain why the assertion is skipped; this +/// reason will be printed when running the test. +/// +/// Certain matchers, like [completion] and [throwsA], either match or fail +/// asynchronously. When you use [expect] with these matchers, it ensures that +/// the test doesn't complete until the matcher has either matched or failed. If +/// you want to wait for the matcher to complete before continuing the test, you +/// can call [expectLater] instead and `await` the result. +void expect(dynamic actual, dynamic matcher, + {String? reason, + Object? /* String|bool */ skip, + @Deprecated('Will be removed in 0.13.0.') bool verbose = false, + @Deprecated('Will be removed in 0.13.0.') ErrorFormatter? formatter}) { + _expect(actual, matcher, + reason: reason, skip: skip, verbose: verbose, formatter: formatter); +} + +/// Just like [expect], but returns a [Future] that completes when the matcher +/// has finished matching. +/// +/// For the [completes] and [completion] matchers, as well as [throwsA] and +/// related matchers when they're matched against a [Future], the returned +/// future completes when the matched future completes. For the [prints] +/// matcher, it completes when the future returned by the callback completes. +/// Otherwise, it completes immediately. +/// +/// If the matcher fails asynchronously, that failure is piped to the returned +/// future where it can be handled by user code. +Future expectLater(dynamic actual, dynamic matcher, + {String? reason, Object? /* String|bool */ skip}) => + _expect(actual, matcher, reason: reason, skip: skip); + +/// The implementation of [expect] and [expectLater]. +Future _expect(Object? actual, Object? matcher, + {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) { + final test = TestHandle.current; + formatter ??= (actual, matcher, reason, matchState, verbose) { + var mismatchDescription = StringDescription(); + matcher.describeMismatch(actual, mismatchDescription, matchState, verbose); + + return formatFailure(matcher, actual, mismatchDescription.toString(), + reason: reason); + }; + + if (skip != null && skip is! bool && skip is! String) { + throw ArgumentError.value(skip, 'skip', 'must be a bool or a String'); + } + + matcher = wrapMatcher(matcher); + if (skip != null && skip != false) { + String message; + if (skip is String) { + message = 'Skip expect: $skip'; + } else if (reason != null) { + message = 'Skip expect ($reason).'; + } else { + var description = StringDescription().addDescriptionOf(matcher); + message = 'Skip expect ($description).'; + } + + test.markSkipped(message); + return Future.sync(() {}); + } + + if (matcher is AsyncMatcher) { + // Avoid async/await so that expect() throws synchronously when possible. + var result = matcher.matchAsync(actual); + expect( + result, + anyOf([ + equals(null), + const TypeMatcher(), + const TypeMatcher() + ]), + reason: 'matchAsync() may only return a String, a Future, or null.'); + + if (result is String) { + fail(formatFailure(matcher, actual, result, reason: reason)); + } else if (result is Future) { + final outstandingWork = test.markPending(); + return result.then((realResult) { + if (realResult == null) return; + fail(formatFailure(matcher as Matcher, actual, realResult as String, + reason: reason)); + }).whenComplete( + // Always remove this, in case the failure is caught and handled + // gracefully. + outstandingWork.complete); + } + + return Future.sync(() {}); + } + + var matchState = {}; + try { + if ((matcher as Matcher).matches(actual, matchState)) { + return Future.sync(() {}); + } + } catch (e, trace) { + reason ??= '$e at $trace'; + } + fail(formatter(actual, matcher as Matcher, reason, matchState, verbose)); +} + +/// Convenience method for throwing a new [TestFailure] with the provided +/// [message]. +Never fail(String message) => throw TestFailure(message); + +// The default error formatter. +@Deprecated('Will be removed in 0.13.0.') +String formatFailure(Matcher expected, Object? actual, String which, + {String? reason}) { + var buffer = StringBuffer(); + buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); + buffer.writeln(indent(prettyPrint(actual), first: ' Actual: ')); + if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: ')); + if (reason != null) buffer.writeln(reason); + return buffer.toString(); +} diff --git a/pkgs/matcher/lib/src/expect/expect_async.dart b/pkgs/matcher/lib/src/expect/expect_async.dart new file mode 100644 index 000000000..88cf6f261 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/expect_async.dart @@ -0,0 +1,586 @@ +// Copyright (c) 2015, 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 'package:test_api/hooks.dart'; + +import 'util/placeholder.dart'; + +// Function types returned by expectAsync# methods. + +typedef Func0 = T Function(); +typedef Func1 = T Function([A a]); +typedef Func2 = T Function([A a, B b]); +typedef Func3 = T Function([A a, B b, C c]); +typedef Func4 = T Function([A a, B b, C c, D d]); +typedef Func5 = T Function([A a, B b, C c, D d, E e]); +typedef Func6 = T Function([A a, B b, C c, D d, E e, F f]); + +/// A wrapper for a function that ensures that it's called the appropriate +/// number of times. +/// +/// The containing test won't be considered to have completed successfully until +/// this function has been called the appropriate number of times. +/// +/// The wrapper function is accessible via [func]. It supports up to six +/// optional and/or required positional arguments, but no named arguments. +class _ExpectedFunction { + /// The wrapped callback. + final Function _callback; + + /// The minimum number of calls that are expected to be made to the function. + /// + /// If fewer calls than this are made, the test will fail. + final int _minExpectedCalls; + + /// The maximum number of calls that are expected to be made to the function. + /// + /// If more calls than this are made, the test will fail. + final int _maxExpectedCalls; + + /// A callback that should return whether the function is not expected to have + /// any more calls. + /// + /// This will be called after every time the function is run. The test case + /// won't be allowed to terminate until it returns `true`. + /// + /// This may be `null`. If so, the function is considered to be done after + /// it's been run once. + final bool Function()? _isDone; + + /// A descriptive name for the function. + final String _id; + + /// An optional description of why the function is expected to be called. + /// + /// If not passed, this will be an empty string. + final String _reason; + + /// The number of times the function has been called. + int _actualCalls = 0; + + /// The test in which this function was wrapped. + late final TestHandle _test; + + /// Whether this function has been called the requisite number of times. + late bool _complete; + + OutstandingWork? _outstandingWork; + + /// Wraps [callback] in a function that asserts that it's called at least + /// [minExpected] times and no more than [maxExpected] times. + /// + /// If passed, [id] is used as a descriptive name fo the function and [reason] + /// as a reason it's expected to be called. If [isDone] is passed, the test + /// won't be allowed to complete until it returns `true`. + _ExpectedFunction(Function callback, int minExpected, int maxExpected, + {String? id, String? reason, bool Function()? isDone}) + : _callback = callback, + _minExpectedCalls = minExpected, + _maxExpectedCalls = + (maxExpected == 0 && minExpected > 0) ? minExpected : maxExpected, + _isDone = isDone, + _reason = reason == null ? '' : '\n$reason', + _id = _makeCallbackId(id, callback) { + try { + _test = TestHandle.current; + } on OutsideTestException { + throw StateError('`expectAsync` must be called within a test.'); + } + + if (maxExpected > 0 && minExpected > maxExpected) { + throw ArgumentError('max ($maxExpected) may not be less than count ' + '($minExpected).'); + } + + if (isDone != null || minExpected > 0) { + _outstandingWork = _test.markPending(); + _complete = false; + } else { + _complete = true; + } + } + + /// Tries to find a reasonable name for [callback]. + /// + /// If [id] is passed, uses that. Otherwise, tries to determine a name from + /// calling `toString`. If no name can be found, returns the empty string. + static String _makeCallbackId(String? id, Function callback) { + if (id != null) return '$id '; + + // If the callback is not an anonymous closure, try to get the + // name. + var toString = callback.toString(); + var prefix = "Function '"; + var start = toString.indexOf(prefix); + if (start == -1) return ''; + + start += prefix.length; + var end = toString.indexOf("'", start); + if (end == -1) return ''; + return '${toString.substring(start, end)} '; + } + + /// Returns a function that has the same number of positional arguments as the + /// wrapped function (up to a total of 6). + Function get func { + if (_callback is Function(Never, Never, Never, Never, Never, Never)) { + return max6; + } + if (_callback is Function(Never, Never, Never, Never, Never)) return max5; + if (_callback is Function(Never, Never, Never, Never)) return max4; + if (_callback is Function(Never, Never, Never)) return max3; + if (_callback is Function(Never, Never)) return max2; + if (_callback is Function(Never)) return max1; + if (_callback is Function()) return max0; + + _outstandingWork?.complete(); + throw ArgumentError( + 'The wrapped function has more than 6 required arguments'); + } + + // This indirection is critical. It ensures the returned function has an + // argument count of zero. + T max0() => max6(); + + T max1([Object? a0 = placeholder]) => max6(a0); + + T max2([Object? a0 = placeholder, Object? a1 = placeholder]) => max6(a0, a1); + + T max3( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder]) => + max6(a0, a1, a2); + + T max4( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder, + Object? a3 = placeholder]) => + max6(a0, a1, a2, a3); + + T max5( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder, + Object? a3 = placeholder, + Object? a4 = placeholder]) => + max6(a0, a1, a2, a3, a4); + + T max6( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder, + Object? a3 = placeholder, + Object? a4 = placeholder, + Object? a5 = placeholder]) => + _run([a0, a1, a2, a3, a4, a5].where((a) => a != placeholder)); + + /// Runs the wrapped function with [args] and returns its return value. + T _run(Iterable args) { + // Note that in the old test, this returned `null` if it encountered an + // error, where now it just re-throws that error because Zone machinery will + // pass it to the invoker anyway. + try { + _actualCalls++; + if (_test.shouldBeDone) { + throw TestFailure( + 'Callback ${_id}called ($_actualCalls) after test case ' + '${_test.name} had already completed.$_reason'); + } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) { + throw TestFailure('Callback ${_id}called more times than expected ' + '($_maxExpectedCalls).$_reason'); + } + + return Function.apply(_callback, args.toList()) as T; + } finally { + _afterRun(); + } + } + + /// After each time the function is run, check to see if it's complete. + void _afterRun() { + if (_complete) return; + if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return; + if (_isDone != null && !_isDone()) return; + + // Mark this callback as complete and remove it from the test case's + // outstanding callback count; if that hits zero the test is done. + _complete = true; + _outstandingWork?.complete(); + } +} + +/// This function is deprecated because it doesn't work well with strong mode. +/// Use [expectAsync0], [expectAsync1], +/// [expectAsync2], [expectAsync3], [expectAsync4], [expectAsync5], or +/// [expectAsync6] instead. +@Deprecated('Will be removed in 0.13.0') +Function expectAsync(Function callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).func; + +/// Informs the framework that the given [callback] of arity 0 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with zero arguments. See also +/// [expectAsync1], [expectAsync2], [expectAsync3], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func0 expectAsync0(T Function() callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max0; + +/// Informs the framework that the given [callback] of arity 1 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with one argument. See also +/// [expectAsync0], [expectAsync2], [expectAsync3], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func1 expectAsync1(T Function(A) callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max1; + +/// Informs the framework that the given [callback] of arity 2 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with two arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync3], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func2 expectAsync2(T Function(A, B) callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max2; + +/// Informs the framework that the given [callback] of arity 3 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with three arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func3 expectAsync3(T Function(A, B, C) callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max3; + +/// Informs the framework that the given [callback] of arity 4 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with four arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func4 expectAsync4( + T Function(A, B, C, D) callback, + {int count = 1, + int max = 0, + String? id, + String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max4; + +/// Informs the framework that the given [callback] of arity 5 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with five arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3], +/// [expectAsync4], and [expectAsync6] for callbacks with different arity. +Func5 expectAsync5( + T Function(A, B, C, D, E) callback, + {int count = 1, + int max = 0, + String? id, + String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max5; + +/// Informs the framework that the given [callback] of arity 6 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with six arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3], +/// [expectAsync4], and [expectAsync5] for callbacks with different arity. +Func6 expectAsync6( + T Function(A, B, C, D, E, F) callback, + {int count = 1, + int max = 0, + String? id, + String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).max6; + +/// This function is deprecated because it doesn't work well with strong mode. +/// Use [expectAsyncUntil0], [expectAsyncUntil1], +/// [expectAsyncUntil2], [expectAsyncUntil3], [expectAsyncUntil4], +/// [expectAsyncUntil5], or [expectAsyncUntil6] instead. +@Deprecated('Will be removed in 0.13.0') +Function expectAsyncUntil(Function callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, id: id, reason: reason, isDone: isDone) + .func; + +/// Informs the framework that the given [callback] of arity 0 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with zero arguments. See also +/// [expectAsyncUntil1], [expectAsyncUntil2], [expectAsyncUntil3], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func0 expectAsyncUntil0(T Function() callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max0; + +/// Informs the framework that the given [callback] of arity 1 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with one argument. See also +/// [expectAsyncUntil0], [expectAsyncUntil2], [expectAsyncUntil3], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func1 expectAsyncUntil1( + T Function(A) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max1; + +/// Informs the framework that the given [callback] of arity 2 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with two arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil3], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func2 expectAsyncUntil2( + T Function(A, B) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max2; + +/// Informs the framework that the given [callback] of arity 3 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with three arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func3 expectAsyncUntil3( + T Function(A, B, C) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max3; + +/// Informs the framework that the given [callback] of arity 4 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with four arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil3], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func4 expectAsyncUntil4( + T Function(A, B, C, D) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max4; + +/// Informs the framework that the given [callback] of arity 5 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with five arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func5 expectAsyncUntil5( + T Function(A, B, C, D, E) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max5; + +/// Informs the framework that the given [callback] of arity 6 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with six arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil5] for +/// callbacks with different arity. +Func6 expectAsyncUntil6( + T Function(A, B, C, D, E, F) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max6; diff --git a/pkgs/matcher/lib/src/expect/future_matchers.dart b/pkgs/matcher/lib/src/expect/future_matchers.dart new file mode 100644 index 000000000..407b9b898 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/future_matchers.dart @@ -0,0 +1,119 @@ +// Copyright (c) 2012, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart' show pumpEventQueue; + +import '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'expect.dart'; +import 'throws_matcher.dart'; +import 'util/pretty_print.dart'; + +/// Matches a [Future] that completes successfully with any value. +/// +/// This creates an asynchronous expectation. The call to [expect] will return +/// immediately and execution will continue. To wait for the future to +/// complete and the expectation to run use [expectLater] and wait on the +/// returned future. +/// +/// To test that a Future completes with an exception, you can use [throws] and +/// [throwsA]. +final Matcher completes = const _Completes(null); + +/// Matches a [Future] that completes successfully with a value that matches +/// [matcher]. +/// +/// This creates an asynchronous expectation. The call to [expect] will return +/// immediately and execution will continue. Later, when the future completes, +/// the expectation against [matcher] will run. To wait for the future to +/// complete and the expectation to run use [expectLater] and wait on the +/// returned future. +/// +/// To test that a Future completes with an exception, you can use [throws] and +/// [throwsA]. +Matcher completion(Object? matcher, + [@Deprecated('this parameter is ignored') String? description]) => + _Completes(wrapMatcher(matcher)); + +class _Completes extends AsyncMatcher { + final Matcher? _matcher; + + const _Completes(this._matcher); + + // Avoid async/await so we synchronously start listening to [item]. + @override + dynamic /*FutureOr*/ matchAsync(Object? item) { + if (item is! Future) return 'was not a Future'; + + return item.then((value) async { + if (_matcher == null) return null; + + String? result; + if (_matcher is AsyncMatcher) { + result = await _matcher.matchAsync(value) as String?; + if (result == null) return null; + } else { + var matchState = {}; + if (_matcher.matches(value, matchState)) return null; + result = _matcher + .describeMismatch(value, StringDescription(), matchState, false) + .toString(); + } + + var buffer = StringBuffer(); + buffer.writeln(indent(prettyPrint(value), first: 'emitted ')); + if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); + return buffer.toString().trimRight(); + }); + } + + @override + Description describe(Description description) { + if (_matcher == null) { + description.add('completes successfully'); + } else { + description.add('completes to a value that ').addDescriptionOf(_matcher); + } + return description; + } +} + +/// Matches a [Future] that does not complete. +/// +/// Note that this creates an asynchronous expectation. The call to +/// `expect()` that includes this will return immediately and execution will +/// continue. +final Matcher doesNotComplete = const _DoesNotComplete(); + +class _DoesNotComplete extends Matcher { + const _DoesNotComplete(); + + @override + Description describe(Description description) { + description.add('does not complete'); + return description; + } + + @override + bool matches(Object? item, Map matchState) { + if (item is! Future) return false; + item.then((value) { + fail('Future was not expected to complete but completed with a value of ' + '$value'); + }); + expect(pumpEventQueue(), completes); + return true; + } + + @override + Description describeMismatch( + Object? item, Description description, Map matchState, bool verbose) { + if (item is! Future) return description.add('$item is not a Future'); + return description; + } +} diff --git a/pkgs/matcher/lib/src/expect/never_called.dart b/pkgs/matcher/lib/src/expect/never_called.dart new file mode 100644 index 000000000..20b529971 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/never_called.dart @@ -0,0 +1,68 @@ +// Copyright (c) 2017, 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 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/hooks.dart'; + +import 'expect.dart'; +import 'future_matchers.dart'; +import 'util/placeholder.dart'; +import 'util/pretty_print.dart'; + +/// Returns a function that causes the test to fail if it's called. +/// +/// This can safely be passed in place of any callback that takes ten or fewer +/// positional parameters. For example: +/// +/// ```dart +/// // Asserts that the stream never emits an event. +/// stream.listen(neverCalled); +/// ``` +/// +/// This also ensures that the test doesn't complete until a call to +/// [pumpEventQueue] finishes, so that the callback has a chance to be called. +Null Function( + [Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?]) get neverCalled { + // Make sure the test stays alive long enough to call the function if it's + // going to. + expect(pumpEventQueue(), completes); + + var zone = Zone.current; + return ( + [a1 = placeholder, + a2 = placeholder, + a3 = placeholder, + a4 = placeholder, + a5 = placeholder, + a6 = placeholder, + a7 = placeholder, + a8 = placeholder, + a9 = placeholder, + a10 = placeholder]) { + var arguments = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10] + .where((argument) => argument != placeholder) + .toList(); + + var argsText = arguments.isEmpty + ? ' no arguments.' + : ':\n${bullet(arguments.map(prettyPrint))}'; + zone.handleUncaughtError( + TestFailure( + 'Callback should never have been called, but it was called with' + '$argsText'), + zone.run(Chain.current)); + return null; + }; +} diff --git a/pkgs/matcher/lib/src/expect/prints_matcher.dart b/pkgs/matcher/lib/src/expect/prints_matcher.dart new file mode 100644 index 000000000..57ae95e26 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/prints_matcher.dart @@ -0,0 +1,72 @@ +// 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 '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'expect.dart'; +import 'util/pretty_print.dart'; + +/// Matches a [Function] that prints text that matches [matcher]. +/// +/// [matcher] may be a String or a [Matcher]. +/// +/// If the function this runs against returns a [Future], all text printed by +/// the function (using [Zone] scoping) until that Future completes is matched. +/// +/// This only tracks text printed using the [print] function. +/// +/// This returns an [AsyncMatcher], so [expect] won't complete until the matched +/// function does. +Matcher prints(Object? matcher) => _Prints(wrapMatcher(matcher)); + +class _Prints extends AsyncMatcher { + final Matcher _matcher; + + _Prints(this._matcher); + + // Avoid async/await so we synchronously fail if the function is + // synchronous. + @override + dynamic /*FutureOr*/ matchAsync(Object? item) { + if (item is! Function()) return 'was not a unary Function'; + + var buffer = StringBuffer(); + var result = runZoned(item, + zoneSpecification: ZoneSpecification(print: (_, __, ____, line) { + buffer.writeln(line); + })); + + return result is Future + ? result.then((_) => _check(buffer.toString())) + : _check(buffer.toString()); + } + + @override + Description describe(Description description) => + description.add('prints ').addDescriptionOf(_matcher); + + /// Verifies that [actual] matches [_matcher] and returns a [String] + /// description of the failure if it doesn't. + String? _check(String actual) { + var matchState = {}; + if (_matcher.matches(actual, matchState)) return null; + + var result = _matcher + .describeMismatch(actual, StringDescription(), matchState, false) + .toString(); + + var buffer = StringBuffer(); + if (actual.isEmpty) { + buffer.writeln('printed nothing'); + } else { + buffer.writeln(indent(prettyPrint(actual), first: 'printed ')); + } + if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); + return buffer.toString().trimRight(); + } +} diff --git a/pkgs/matcher/lib/src/expect/stream_matcher.dart b/pkgs/matcher/lib/src/expect/stream_matcher.dart new file mode 100644 index 000000000..0c1d852d2 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/stream_matcher.dart @@ -0,0 +1,196 @@ +// Copyright (c) 2017, 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 'package:async/async.dart'; +import 'package:test_api/hooks.dart'; + +import '../interfaces.dart'; +import 'async_matcher.dart'; +import 'expect.dart'; +import 'util/pretty_print.dart'; + +/// A matcher that matches events from [Stream]s or [StreamQueue]s. +/// +/// Stream matchers are designed to make it straightforward to create complex +/// expectations for streams, and to interleave expectations with the rest of a +/// test. They can be used on a [Stream] to match all events it emits: +/// +/// ```dart +/// expect(stream, emitsInOrder([ +/// // Values match individual events. +/// "Ready.", +/// +/// // Matchers also run against individual events. +/// startsWith("Loading took"), +/// +/// // Stream matchers can be nested. This asserts that one of two events are +/// // emitted after the "Loading took" line. +/// emitsAnyOf(["Succeeded!", "Failed!"]), +/// +/// // By default, more events are allowed after the matcher finishes +/// // matching. This asserts instead that the stream emits a done event and +/// // nothing else. +/// emitsDone +/// ])); +/// ``` +/// +/// It can also match a [StreamQueue], in which case it consumes the matched +/// events. The call to [expect] returns a [Future] that completes when the +/// matcher is done matching. You can `await` this to consume different events +/// at different times: +/// +/// ```dart +/// var stdout = StreamQueue(stdoutLineStream); +/// +/// // Ignore lines from the process until it's about to emit the URL. +/// await expectLater(stdout, emitsThrough('WebSocket URL:')); +/// +/// // Parse the next line as a URL. +/// var url = Uri.parse(await stdout.next); +/// expect(url.host, equals('localhost')); +/// +/// // You can match against the same StreamQueue multiple times. +/// await expectLater(stdout, emits('Waiting for connection...')); +/// ``` +/// +/// Users can call [StreamMatcher] to create custom matchers. +abstract class StreamMatcher extends Matcher { + /// The description of this matcher. + /// + /// This is in the subjunctive mood, which means it can be used after the word + /// "should". For example, it might be "emit the right events". + String get description; + + /// Creates a new [StreamMatcher] described by [description] that matches + /// events with [matchQueue]. + /// + /// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue], + /// and should follow all the guarantees of that method. In particular: + /// + /// * If it matches successfully, it should return `null` and possibly consume + /// events. + /// * If it fails to match, consume no events and return a description of the + /// failure. + /// * The description should be in past tense. + /// * The description should be grammatically valid when used after "the + /// stream"—"emitted the wrong events", for example. + /// + /// The [matchQueue] callback may return the empty string to indicate a + /// failure if it has no information to add beyond the description of the + /// failure and the events actually emitted by the stream. + /// + /// The [description] should be in the subjunctive mood. This means that it + /// should be grammatically valid when used after the word "should". For + /// example, it might be "emit the right events". + factory StreamMatcher(Future Function(StreamQueue) matchQueue, + String description) = _StreamMatcher; + + /// Tries to match events emitted by [queue]. + /// + /// If this matches successfully, it consumes the matching events from [queue] + /// and returns `null`. + /// + /// If this fails to match, it doesn't consume any events and returns a + /// description of the failure. This description is in the past tense, and + /// could grammatically be used after "the stream". For example, it might + /// return "emitted the wrong events". + /// + /// The description string may also be empty, which indicates that the + /// matcher's description and the events actually emitted by the stream are + /// enough to understand the failure. + /// + /// If the queue emits an error, that error is re-thrown unless otherwise + /// indicated by the matcher. + Future matchQueue(StreamQueue queue); +} + +/// A concrete implementation of [StreamMatcher]. +/// +/// This is separate from the original type to hide the private [AsyncMatcher] +/// interface. +class _StreamMatcher extends AsyncMatcher implements StreamMatcher { + @override + final String description; + + /// The callback used to implement [matchQueue]. + final Future Function(StreamQueue) _matchQueue; + + _StreamMatcher(this._matchQueue, this.description); + + @override + Future matchQueue(StreamQueue queue) => _matchQueue(queue); + + @override + dynamic /*FutureOr*/ matchAsync(Object? item) { + StreamQueue queue; + var shouldCancelQueue = false; + if (item is StreamQueue) { + queue = item; + } else if (item is Stream) { + queue = StreamQueue(item); + shouldCancelQueue = true; + } else { + return 'was not a Stream or a StreamQueue'; + } + + // Avoid async/await in the outer method so that we synchronously error out + // for an invalid argument type. + var transaction = queue.startTransaction(); + var copy = transaction.newQueue(); + return matchQueue(copy).then((result) async { + // Accept the transaction if the result is null, indicating that the match + // succeeded. + if (result == null) { + transaction.commit(copy); + return null; + } + + // Get a list of events emitted by the stream so we can emit them as part + // of the error message. + var replay = transaction.newQueue(); + var events = []; + var subscription = Result.captureStreamTransformer + .bind(replay.rest.cast()) + .listen(events.add, onDone: () => events.add(null)); + + // Wait on a timer tick so all buffered events are emitted. + await Future.delayed(Duration.zero); + _unawaited(subscription.cancel()); + + var eventsString = events.map((event) { + if (event == null) { + return 'x Stream closed.'; + } else if (event.isValue) { + return addBullet(event.asValue!.value.toString()); + } else { + var error = event.asError!; + var chain = TestHandle.current.formatStackTrace(error.stackTrace); + var text = '${error.error}\n$chain'; + return indent(text, first: '! '); + } + }).join('\n'); + if (eventsString.isEmpty) eventsString = 'no events'; + + transaction.reject(); + + var buffer = StringBuffer(); + buffer.writeln(indent(eventsString, first: 'emitted ')); + if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); + return buffer.toString().trimRight(); + }, onError: (Object error) { + transaction.reject(); + // ignore: only_throw_errors + throw error; + }).then((result) { + if (shouldCancelQueue) queue.cancel(); + return result; + }); + } + + @override + Description describe(Description description) => + description.add('should ').add(this.description); +} + +void _unawaited(Future f) {} diff --git a/pkgs/matcher/lib/src/expect/stream_matchers.dart b/pkgs/matcher/lib/src/expect/stream_matchers.dart new file mode 100644 index 000000000..02efff34e --- /dev/null +++ b/pkgs/matcher/lib/src/expect/stream_matchers.dart @@ -0,0 +1,377 @@ +// Copyright (c) 2017, 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 'package:async/async.dart'; + +import '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'stream_matcher.dart'; +import 'throws_matcher.dart'; +import 'util/pretty_print.dart'; + +/// Returns a [StreamMatcher] that asserts that the stream emits a "done" event. +final emitsDone = StreamMatcher( + (queue) async => (await queue.hasNext) ? '' : null, 'be done'); + +/// Returns a [StreamMatcher] for [matcher]. +/// +/// If [matcher] is already a [StreamMatcher], it's returned as-is. If it's any +/// other [Matcher], this matches a single event that matches that matcher. If +/// it's any other Object, this matches a single event that's equal to that +/// object. +/// +/// This functions like [wrapMatcher] for [StreamMatcher]s: it can convert any +/// matcher-like value into a proper [StreamMatcher]. +StreamMatcher emits(Object? matcher) { + if (matcher is StreamMatcher) return matcher; + var wrapped = wrapMatcher(matcher); + + var matcherDescription = wrapped.describe(StringDescription()); + + return StreamMatcher((queue) async { + if (!await queue.hasNext) return ''; + + var matchState = {}; + var actual = await queue.next; + if (wrapped.matches(actual, matchState)) return null; + + var mismatchDescription = StringDescription(); + wrapped.describeMismatch(actual, mismatchDescription, matchState, false); + + if (mismatchDescription.length == 0) return ''; + return 'emitted an event that $mismatchDescription'; + }, + // TODO(nweiz): add "should" once matcher#42 is fixed. + 'emit an event that $matcherDescription'); +} + +/// Returns a [StreamMatcher] that matches a single error event that matches +/// [matcher]. +StreamMatcher emitsError(Object? matcher) { + var wrapped = wrapMatcher(matcher); + var matcherDescription = wrapped.describe(StringDescription()); + var throwsMatcher = throwsA(wrapped) as AsyncMatcher; + + return StreamMatcher( + (queue) => throwsMatcher.matchAsync(queue.next) as Future, + // TODO(nweiz): add "should" once matcher#42 is fixed. + 'emit an error that $matcherDescription'); +} + +/// Returns a [StreamMatcher] that allows (but doesn't require) [matcher] to +/// match the stream. +/// +/// This matcher always succeeds; if [matcher] doesn't match, this just consumes +/// no events. +StreamMatcher mayEmit(Object? matcher) { + var streamMatcher = emits(matcher); + return StreamMatcher((queue) async { + await queue.withTransaction( + (copy) async => (await streamMatcher.matchQueue(copy)) == null); + return null; + }, 'maybe ${streamMatcher.description}'); +} + +/// Returns a [StreamMatcher] that matches the stream if at least one of +/// [matchers] matches. +/// +/// If multiple matchers match the stream, this chooses the matcher that +/// consumes as many events as possible. +/// +/// If any matchers match the stream, no errors from other matchers are thrown. +/// If no matchers match and multiple matchers threw errors, the first error is +/// re-thrown. +StreamMatcher emitsAnyOf(Iterable matchers) { + var streamMatchers = matchers.map(emits).toList(); + if (streamMatchers.isEmpty) { + throw ArgumentError('matcher may not be empty'); + } + + if (streamMatchers.length == 1) return streamMatchers.first; + var description = 'do one of the following:\n' + '${bullet(streamMatchers.map((matcher) => matcher.description))}'; + + return StreamMatcher((queue) async { + var transaction = queue.startTransaction(); + + // Allocate the failures list ahead of time so that its order matches the + // order of [matchers], and thus the order the matchers will be listed in + // the description. + var failures = List.filled(matchers.length, null); + + // The first error thrown. If no matchers match and this exists, we rethrow + // it. + Object? firstError; + StackTrace? firstStackTrace; + + var futures = []; + StreamQueue? consumedMost; + for (var i = 0; i < matchers.length; i++) { + futures.add(() async { + var copy = transaction.newQueue(); + + String? result; + try { + result = await streamMatchers[i].matchQueue(copy); + } catch (error, stackTrace) { + if (firstError == null) { + firstError = error; + firstStackTrace = stackTrace; + } + return; + } + + if (result != null) { + failures[i] = result; + } else if (consumedMost == null || + consumedMost!.eventsDispatched < copy.eventsDispatched) { + consumedMost = copy; + } + }()); + } + + await Future.wait(futures); + + if (consumedMost == null) { + transaction.reject(); + if (firstError != null) { + await Future.error(firstError!, firstStackTrace); + } + + var failureMessages = []; + for (var i = 0; i < matchers.length; i++) { + var message = 'failed to ${streamMatchers[i].description}'; + if (failures[i]!.isNotEmpty) { + message += message.contains('\n') ? '\n' : ' '; + message += 'because it ${failures[i]}'; + } + + failureMessages.add(message); + } + + return 'failed all options:\n${bullet(failureMessages)}'; + } else { + transaction.commit(consumedMost!); + return null; + } + }, description); +} + +/// Returns a [StreamMatcher] that matches the stream if each matcher in +/// [matchers] matches, one after another. +/// +/// If any matcher fails to match, this fails and consumes no events. +StreamMatcher emitsInOrder(Iterable matchers) { + var streamMatchers = matchers.map(emits).toList(); + if (streamMatchers.length == 1) return streamMatchers.first; + + var description = 'do the following in order:\n' + '${bullet(streamMatchers.map((matcher) => matcher.description))}'; + + return StreamMatcher((queue) async { + for (var i = 0; i < streamMatchers.length; i++) { + var matcher = streamMatchers[i]; + var result = await matcher.matchQueue(queue); + if (result == null) continue; + + var newResult = "didn't ${matcher.description}"; + if (result.isNotEmpty) { + newResult += newResult.contains('\n') ? '\n' : ' '; + newResult += 'because it $result'; + } + return newResult; + } + return null; + }, description); +} + +/// Returns a [StreamMatcher] that matches any number of events followed by +/// events that match [matcher]. +/// +/// This consumes all events matched by [matcher], as well as all events before. +/// If the stream emits a done event without matching [matcher], this fails and +/// consumes no events. +StreamMatcher emitsThrough(Object? matcher) { + var streamMatcher = emits(matcher); + return StreamMatcher((queue) async { + var failures = []; + + Future tryHere() => queue.withTransaction((copy) async { + var result = await streamMatcher.matchQueue(copy); + if (result == null) return true; + failures.add(result); + return false; + }); + + while (await queue.hasNext) { + if (await tryHere()) return null; + await queue.next; + } + + // Try after the queue is done in case the matcher can match an empty + // stream. + if (await tryHere()) return null; + + var result = 'never did ${streamMatcher.description}'; + + var failureMessages = + bullet(failures.where((failure) => failure.isNotEmpty)); + if (failureMessages.isNotEmpty) { + result += result.contains('\n') ? '\n' : ' '; + result += 'because it:\n$failureMessages'; + } + + return result; + }, 'eventually ${streamMatcher.description}'); +} + +/// Returns a [StreamMatcher] that matches any number of events that match +/// [matcher]. +/// +/// This consumes events until [matcher] no longer matches. It always succeeds; +/// if [matcher] doesn't match, this just consumes no events. It never rethrows +/// errors. +StreamMatcher mayEmitMultiple(Object? matcher) { + var streamMatcher = emits(matcher); + + var description = streamMatcher.description; + description += description.contains('\n') ? '\n' : ' '; + description += 'zero or more times'; + + return StreamMatcher((queue) async { + while (await _tryMatch(queue, streamMatcher)) { + // Do nothing; the matcher presumably already consumed events. + } + return null; + }, description); +} + +/// Returns a [StreamMatcher] that matches a stream that never matches +/// [matcher]. +/// +/// This doesn't complete until the stream emits a done event. It never consumes +/// any events. It never re-throws errors. +StreamMatcher neverEmits(Object? matcher) { + var streamMatcher = emits(matcher); + return StreamMatcher((queue) async { + var events = 0; + var matched = false; + await queue.withTransaction((copy) async { + while (await copy.hasNext) { + matched = await _tryMatch(copy, streamMatcher); + if (matched) return false; + + events++; + + try { + await copy.next; + } catch (_) { + // Ignore errors events. + } + } + + matched = await _tryMatch(copy, streamMatcher); + return false; + }); + + if (!matched) return null; + return "after $events ${pluralize('event', events)} did " + '${streamMatcher.description}'; + }, 'never ${streamMatcher.description}'); +} + +/// Returns whether [matcher] matches [queue] at its current position. +/// +/// This treats errors as failures to match. +Future _tryMatch(StreamQueue queue, StreamMatcher matcher) { + return queue.withTransaction((copy) async { + try { + return (await matcher.matchQueue(copy)) == null; + } catch (_) { + return false; + } + }); +} + +/// Returns a [StreamMatcher] that matches the stream if each matcher in +/// [matchers] matches, in any order. +/// +/// If any matcher fails to match, this fails and consumes no events. If the +/// matchers match in multiple different possible orders, this chooses the order +/// that consumes as many events as possible. +/// +/// If any sequence of matchers matches the stream, no errors from other +/// sequences are thrown. If no sequences match and multiple sequences throw +/// errors, the first error is re-thrown. +/// +/// Note that checking every ordering of [matchers] is O(n!) in the worst case, +/// so this should only be called when there are very few [matchers]. +StreamMatcher emitsInAnyOrder(Iterable matchers) { + var streamMatchers = matchers.map(emits).toSet(); + if (streamMatchers.length == 1) return streamMatchers.first; + var description = 'do the following in any order:\n' + '${bullet(streamMatchers.map((matcher) => matcher.description))}'; + + return StreamMatcher( + (queue) async => await _tryInAnyOrder(queue, streamMatchers) ? null : '', + description); +} + +/// Returns whether [queue] matches [matchers] in any order. +Future _tryInAnyOrder( + StreamQueue queue, Set matchers) async { + if (matchers.length == 1) { + return await matchers.first.matchQueue(queue) == null; + } + + var transaction = queue.startTransaction(); + StreamQueue? consumedMost; + + // The first error thrown. If no matchers match and this exists, we rethrow + // it. + Object? firstError; + StackTrace? firstStackTrace; + + await Future.wait(matchers.map((matcher) async { + var copy = transaction.newQueue(); + try { + if (await matcher.matchQueue(copy) != null) return; + } catch (error, stackTrace) { + if (firstError == null) { + firstError = error; + firstStackTrace = stackTrace; + } + return; + } + + var rest = Set.from(matchers); + rest.remove(matcher); + + try { + if (!await _tryInAnyOrder(copy, rest)) return; + } catch (error, stackTrace) { + if (firstError == null) { + firstError = error; + firstStackTrace = stackTrace; + } + return; + } + + if (consumedMost == null || + consumedMost!.eventsDispatched < copy.eventsDispatched) { + consumedMost = copy; + } + })); + + if (consumedMost == null) { + transaction.reject(); + if (firstError != null) await Future.error(firstError!, firstStackTrace); + return false; + } else { + transaction.commit(consumedMost!); + return true; + } +} diff --git a/pkgs/matcher/lib/src/expect/throws_matcher.dart b/pkgs/matcher/lib/src/expect/throws_matcher.dart new file mode 100644 index 000000000..3a8182131 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/throws_matcher.dart @@ -0,0 +1,140 @@ +// 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart'; + +import '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'util/pretty_print.dart'; + +/// This function is deprecated. +/// +/// Use [throwsA] instead. We strongly recommend that you add assertions about +/// at least the type of the error, but you can write `throwsA(anything)` to +/// mimic the behavior of this matcher. +@Deprecated('Will be removed in 0.13.0') +const Matcher throws = Throws(); + +/// This can be used to match three kinds of objects: +/// +/// * A [Function] that throws an exception when called. The function cannot +/// take any arguments. If you want to test that a function expecting +/// arguments throws, wrap it in another zero-argument function that calls +/// the one you want to test. +/// +/// * A [Future] that completes with an exception. Note that this creates an +/// asynchronous expectation. The call to `expect()` that includes this will +/// return immediately and execution will continue. Later, when the future +/// completes, the actual expectation will run. +/// +/// * A [Function] that returns a [Future] that completes with an exception. +/// +/// In all three cases, when an exception is thrown, this will test that the +/// exception object matches [matcher]. If [matcher] is not an instance of +/// [Matcher], it will implicitly be treated as `equals(matcher)`. +/// +/// Examples: +/// ```dart +/// void functionThatThrows() => throw SomeException(); +/// +/// void functionWithArgument(bool shouldThrow) { +/// if (shouldThrow) { +/// throw SomeException(); +/// } +/// } +/// +/// Future asyncFunctionThatThrows() async => throw SomeException(); +/// +/// expect(functionThatThrows, throwsA(isA())); +/// +/// expect(() => functionWithArgument(true), throwsA(isA())); +/// +/// var future = asyncFunctionThatThrows(); +/// await expectLater(future, throwsA(isA())); +/// +/// await expectLater( +/// asyncFunctionThatThrows, throwsA(isA())); +/// ``` +Matcher throwsA(Object? matcher) => Throws(wrapMatcher(matcher)); + +/// Use the [throwsA] function instead. +@Deprecated('Will be removed in 0.13.0') +class Throws extends AsyncMatcher { + final Matcher? _matcher; + + const Throws([Matcher? matcher]) : _matcher = matcher; + + // Avoid async/await so we synchronously fail if we match a synchronous + // function. + @override + dynamic /*FutureOr*/ matchAsync(Object? item) { + if (item is! Function && item is! Future) { + return 'was not a Function or Future'; + } + + if (item is Future) { + return _matchFuture(item, 'emitted '); + } + + try { + item as Function; + var value = item(); + if (value is Future) { + return _matchFuture(value, 'returned a Future that emitted '); + } + + return indent(prettyPrint(value), first: 'returned '); + } catch (error, trace) { + return _check(error, trace); + } + } + + /// Matches [future], using try/catch since `onError` doesn't seem to work + /// properly in nnbd. + Future _matchFuture( + Future future, String messagePrefix) async { + try { + var value = await future; + return indent(prettyPrint(value), first: messagePrefix); + } catch (error, trace) { + return _check(error, trace); + } + } + + @override + Description describe(Description description) { + if (_matcher == null) { + return description.add('throws'); + } else { + return description.add('throws ').addDescriptionOf(_matcher); + } + } + + /// Verifies that [error] matches [_matcher] and returns a [String] + /// description of the failure if it doesn't. + String? _check(error, StackTrace? trace) { + if (_matcher == null) return null; + + var matchState = {}; + if (_matcher.matches(error, matchState)) return null; + + var result = _matcher + .describeMismatch(error, StringDescription(), matchState, false) + .toString(); + + var buffer = StringBuffer(); + buffer.writeln(indent(prettyPrint(error), first: 'threw ')); + if (trace != null) { + buffer.writeln(indent( + TestHandle.current.formatStackTrace(trace).toString(), + first: 'stack ')); + } + if (result.isNotEmpty) buffer.writeln(indent(result, first: 'which ')); + return buffer.toString().trimRight(); + } +} diff --git a/pkgs/matcher/lib/src/expect/throws_matchers.dart b/pkgs/matcher/lib/src/expect/throws_matchers.dart new file mode 100644 index 000000000..67d35b723 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/throws_matchers.dart @@ -0,0 +1,72 @@ +// 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import '../error_matchers.dart'; +import '../interfaces.dart'; +import '../type_matcher.dart'; +import 'throws_matcher.dart'; + +/// A matcher for functions that throw ArgumentError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsArgumentError = Throws(isArgumentError); + +/// A matcher for functions that throw ConcurrentModificationError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsConcurrentModificationError = + Throws(isConcurrentModificationError); + +/// A matcher for functions that throw CyclicInitializationError. +/// +/// See [throwsA] for objects that this can be matched against. +@Deprecated('throwsCyclicInitializationError has been deprecated, because ' + 'the type will longer exists in Dart 3.0. It will now catch any kind of ' + 'error, not only CyclicInitializationError.') +const Matcher throwsCyclicInitializationError = Throws(TypeMatcher()); + +/// A matcher for functions that throw Exception. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsException = Throws(isException); + +/// A matcher for functions that throw FormatException. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsFormatException = Throws(isFormatException); + +/// A matcher for functions that throw NoSuchMethodError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsNoSuchMethodError = Throws(isNoSuchMethodError); + +/// A matcher for functions that throw NullThrownError. +/// +/// See [throwsA] for objects that this can be matched against. +@Deprecated('throwsNullThrownError has been deprecated, because ' + 'NullThrownError has been replaced with TypeError. ' + 'Use `throwsA(isA())` instead.') +const Matcher throwsNullThrownError = Throws(TypeMatcher()); + +/// A matcher for functions that throw RangeError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsRangeError = Throws(isRangeError); + +/// A matcher for functions that throw StateError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsStateError = Throws(isStateError); + +/// A matcher for functions that throw Exception. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsUnimplementedError = Throws(isUnimplementedError); + +/// A matcher for functions that throw UnsupportedError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsUnsupportedError = Throws(isUnsupportedError); diff --git a/pkgs/matcher/lib/src/expect/util/placeholder.dart b/pkgs/matcher/lib/src/expect/util/placeholder.dart new file mode 100644 index 000000000..ee2dc70ac --- /dev/null +++ b/pkgs/matcher/lib/src/expect/util/placeholder.dart @@ -0,0 +1,16 @@ +// Copyright (c) 2017, 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. + +/// A class that's used as a default argument to detect whether an argument was +/// passed. +/// +/// We use a custom class for this rather than just `const Object()` so that +/// callers can't accidentally pass the placeholder value. +class _Placeholder { + const _Placeholder(); +} + +/// A placeholder to use as a default argument value to detect whether an +/// argument was passed. +const placeholder = _Placeholder(); diff --git a/pkgs/matcher/lib/src/expect/util/pretty_print.dart b/pkgs/matcher/lib/src/expect/util/pretty_print.dart new file mode 100644 index 000000000..de635bafb --- /dev/null +++ b/pkgs/matcher/lib/src/expect/util/pretty_print.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2021, 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 'package:term_glyph/term_glyph.dart' as glyph; + +import '../../description.dart'; + +/// Indent each line in [text] by [first] spaces. +/// +/// [first] is used in place of the first line's indentation. +String indent(String text, {required String first}) { + final prefix = ' ' * first.length; + var lines = text.split('\n'); + if (lines.length == 1) return '$first$text'; + + var buffer = StringBuffer('$first${lines.first}\n'); + + // Write out all but the first and last lines with [prefix]. + for (var line in lines.skip(1).take(lines.length - 2)) { + buffer.writeln('$prefix$line'); + } + buffer.write('$prefix${lines.last}'); + return buffer.toString(); +} + +/// Returns a pretty-printed representation of [value]. +/// +/// The matcher package doesn't expose its pretty-print function directly, but +/// we can use it through StringDescription. +String prettyPrint(Object? value) => + StringDescription().addDescriptionOf(value).toString(); + +/// Indents [text], and adds a bullet at the beginning. +String addBullet(String text) => indent(text, first: '${glyph.bullet} '); + +/// Converts [strings] to a bulleted list. +String bullet(Iterable strings) => strings.map(addBullet).join('\n'); + +/// Returns [name] if [number] is 1, or the plural of [name] otherwise. +/// +/// By default, this just adds "s" to the end of [name] to get the plural. If +/// [plural] is passed, that's used instead. +String pluralize(String name, int number, {String? plural}) { + if (number == 1) return name; + if (plural != null) return plural; + return '${name}s'; +} diff --git a/pkgs/matcher/lib/src/feature_matcher.dart b/pkgs/matcher/lib/src/feature_matcher.dart new file mode 100644 index 000000000..1dbe56c38 --- /dev/null +++ b/pkgs/matcher/lib/src/feature_matcher.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2018, 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 'interfaces.dart'; +import 'type_matcher.dart'; + +/// A package-private [TypeMatcher] implementation that makes it easy for +/// subclasses to validate aspects of specific types while providing consistent +/// type checking. +abstract class FeatureMatcher extends TypeMatcher { + const FeatureMatcher(); + + @override + bool matches(dynamic item, Map matchState) => + super.matches(item, matchState) && typedMatches(item as T, matchState); + + bool typedMatches(T item, Map matchState); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (item is T) { + return describeTypedMismatch( + item, mismatchDescription, matchState, verbose); + } + + return super.describe(mismatchDescription.add('not an ')); + } + + Description describeTypedMismatch(T item, Description mismatchDescription, + Map matchState, bool verbose) => + mismatchDescription; +} diff --git a/pkgs/matcher/lib/src/having_matcher.dart b/pkgs/matcher/lib/src/having_matcher.dart new file mode 100644 index 000000000..2d2dc3d4f --- /dev/null +++ b/pkgs/matcher/lib/src/having_matcher.dart @@ -0,0 +1,75 @@ +// Copyright (c) 2018, 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 'custom_matcher.dart'; +import 'interfaces.dart'; +import 'type_matcher.dart'; +import 'util.dart'; + +/// A package-private [TypeMatcher] implementation that handles is returned +/// by calls to [TypeMatcher.having]. +class HavingMatcher implements TypeMatcher { + final TypeMatcher _parent; + final List<_FunctionMatcher> _functionMatchers; + + HavingMatcher(this._parent, String description, Object? Function(T) feature, + dynamic matcher) + : _functionMatchers = [ + _FunctionMatcher(description, feature, matcher) + ]; + + HavingMatcher._fromExisting( + this._parent, + String description, + Object? Function(T) feature, + dynamic matcher, + Iterable<_FunctionMatcher>? existing) + : _functionMatchers = [ + ...?existing, + _FunctionMatcher(description, feature, matcher) + ]; + + @override + TypeMatcher having( + Object? Function(T) feature, String description, dynamic matcher) => + HavingMatcher._fromExisting( + _parent, description, feature, matcher, _functionMatchers); + + @override + bool matches(dynamic item, Map matchState) { + for (var matcher in [_parent].followedBy(_functionMatchers)) { + if (!matcher.matches(item, matchState)) { + addStateInfo(matchState, {'matcher': matcher}); + return false; + } + } + return true; + } + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + var matcher = matchState['matcher'] as Matcher; + matcher.describeMismatch( + item, mismatchDescription, matchState['state'] as Map, verbose); + return mismatchDescription; + } + + @override + Description describe(Description description) => description + .add('') + .addDescriptionOf(_parent) + .add(' with ') + .addAll('', ' and ', '', _functionMatchers); +} + +class _FunctionMatcher extends CustomMatcher { + final Object? Function(T value) _feature; + + _FunctionMatcher(String name, this._feature, Object? matcher) + : super('`$name`:', '`$name`', matcher); + + @override + Object? featureValueOf(covariant T actual) => _feature(actual); +} diff --git a/pkgs/matcher/lib/src/interfaces.dart b/pkgs/matcher/lib/src/interfaces.dart new file mode 100644 index 000000000..24527f7b1 --- /dev/null +++ b/pkgs/matcher/lib/src/interfaces.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2012, 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. + +/// Matchers build up their error messages by appending to Description objects. +/// +/// This interface is implemented by StringDescription. +/// +/// This interface is unlikely to need other implementations, but could be +/// useful to replace in some cases - e.g. language conversion. +abstract class Description { + int get length; + + /// Change the value of the description. + Description replace(String text); + + /// This is used to add arbitrary text to the description. + Description add(String text); + + /// This is used to add a meaningful description of a value. + Description addDescriptionOf(Object? value); + + /// This is used to add a description of an [Iterable] [list], + /// with appropriate [start] and [end] markers and inter-element [separator]. + Description addAll(String start, String separator, String end, Iterable list); +} + +/// The base class for all matchers. +/// +/// [matches] and [describe] must be implemented by subclasses. +/// +/// Subclasses can override [describeMismatch] if a more specific description is +/// required when the matcher fails. +abstract class Matcher { + const Matcher(); + + /// Does the matching of the actual vs expected values. + /// + /// [item] is the actual value. [matchState] can be supplied + /// and may be used to add details about the mismatch that are too + /// costly to determine in [describeMismatch]. + bool matches(dynamic item, Map matchState); + + /// Builds a textual description of the matcher. + Description describe(Description description); + + /// Builds a textual description of a specific mismatch. + /// + /// [item] is the value that was tested by [matches]; [matchState] is + /// the [Map] that was passed to and supplemented by [matches] + /// with additional information about the mismatch, and [mismatchDescription] + /// is the [Description] that is being built to describe the mismatch. + /// + /// A few matchers make use of the [verbose] flag to provide detailed + /// information that is not typically included but can be of help in + /// diagnosing failures, such as stack traces. + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) => + mismatchDescription; +} diff --git a/pkgs/matcher/lib/src/iterable_matchers.dart b/pkgs/matcher/lib/src/iterable_matchers.dart new file mode 100644 index 000000000..918650a26 --- /dev/null +++ b/pkgs/matcher/lib/src/iterable_matchers.dart @@ -0,0 +1,416 @@ +// Copyright (c) 2012, 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 'description.dart'; +import 'equals_matcher.dart'; +import 'feature_matcher.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher which matches [Iterable]s in which all elements +/// match the given [valueOrMatcher]. +Matcher everyElement(Object? valueOrMatcher) => + _EveryElement(wrapMatcher(valueOrMatcher)); + +class _EveryElement extends _IterableMatcher { + final Matcher _matcher; + + _EveryElement(this._matcher); + + @override + bool typedMatches(Iterable item, Map matchState) { + var i = 0; + for (var element in item) { + if (!_matcher.matches(element, matchState)) { + addStateInfo(matchState, {'index': i, 'element': element}); + return false; + } + ++i; + } + return true; + } + + @override + Description describe(Description description) => + description.add('every element(').addDescriptionOf(_matcher).add(')'); + + @override + Description describeTypedMismatch(dynamic item, + Description mismatchDescription, Map matchState, bool verbose) { + if (matchState['index'] != null) { + var index = matchState['index']; + var element = matchState['element']; + mismatchDescription + .add('has value ') + .addDescriptionOf(element) + .add(' which '); + var subDescription = StringDescription(); + _matcher.describeMismatch( + element, subDescription, matchState['state'] as Map, verbose); + if (subDescription.length > 0) { + mismatchDescription.add(subDescription.toString()); + } else { + mismatchDescription.add("doesn't match "); + _matcher.describe(mismatchDescription); + } + mismatchDescription.add(' at index $index'); + return mismatchDescription; + } + return super + .describeMismatch(item, mismatchDescription, matchState, verbose); + } +} + +/// Returns a matcher which matches [Iterable]s in which at least one +/// element matches the given [valueOrMatcher]. +Matcher anyElement(Object? valueOrMatcher) => + _AnyElement(wrapMatcher(valueOrMatcher)); + +class _AnyElement extends _IterableMatcher { + final Matcher _matcher; + + _AnyElement(this._matcher); + + @override + bool typedMatches(Iterable item, Map matchState) => + item.any((e) => _matcher.matches(e, matchState)); + + @override + Description describe(Description description) => + description.add('some element ').addDescriptionOf(_matcher); +} + +/// Returns a matcher which matches [Iterable]s that have the same +/// length and the same elements as [expected], in the same order. +/// +/// This is equivalent to [equals] but does not recurse. +Matcher orderedEquals(Iterable expected) => _OrderedEquals(expected); + +class _OrderedEquals extends _IterableMatcher { + final Iterable _expected; + final Matcher _matcher; + + _OrderedEquals(this._expected) : _matcher = equals(_expected, 1); + + @override + bool typedMatches(Iterable item, Map matchState) => + _matcher.matches(item, matchState); + + @override + Description describe(Description description) => + description.add('equals ').addDescriptionOf(_expected).add(' ordered'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + return _matcher.describeMismatch( + item, mismatchDescription, matchState, verbose); + } +} + +/// Returns a matcher which matches [Iterable]s that have the same length and +/// the same elements as [expected], but not necessarily in the same order. +/// +/// Note that this is worst case O(n^2) runtime and memory usage so it should +/// only be used on small iterables. +Matcher unorderedEquals(Iterable expected) => _UnorderedEquals(expected); + +class _UnorderedEquals extends _UnorderedMatches { + final List _expectedValues; + + _UnorderedEquals(Iterable expected) + : _expectedValues = expected.toList(), + super(expected.map(equals)); + + @override + Description describe(Description description) => description + .add('equals ') + .addDescriptionOf(_expectedValues) + .add(' unordered'); +} + +/// Iterable matchers match against [Iterable]s. We add this intermediate +/// class to give better mismatch error messages than the base Matcher class. +abstract class _IterableMatcher extends FeatureMatcher { + const _IterableMatcher(); +} + +/// Returns a matcher which matches [Iterable]s whose elements match the +/// matchers in [expected], but not necessarily in the same order. +/// +/// Note that this is worst case O(n^2) runtime and memory usage so it should +/// only be used on small iterables. +Matcher unorderedMatches(Iterable expected) => _UnorderedMatches(expected); + +class _UnorderedMatches extends _IterableMatcher { + final List _expected; + final bool _allowUnmatchedValues; + + _UnorderedMatches(Iterable expected, {bool allowUnmatchedValues = false}) + : _expected = expected.map(wrapMatcher).toList(), + _allowUnmatchedValues = allowUnmatchedValues; + + String? _test(List values) { + // Check the lengths are the same. + if (_expected.length > values.length) { + return 'has too few elements (${values.length} < ${_expected.length})'; + } else if (!_allowUnmatchedValues && _expected.length < values.length) { + return 'has too many elements (${values.length} > ${_expected.length})'; + } + + var edges = List.generate(values.length, (_) => [], growable: false); + for (var v = 0; v < values.length; v++) { + for (var m = 0; m < _expected.length; m++) { + if (_expected[m].matches(values[v], {})) { + edges[v].add(m); + } + } + } + // The index into `values` matched with each matcher or `null` if no value + // has been matched yet. + var matched = List.filled(_expected.length, null); + for (var valueIndex = 0; valueIndex < values.length; valueIndex++) { + _findPairing(edges, valueIndex, matched); + } + for (var matcherIndex = 0; + matcherIndex < _expected.length; + matcherIndex++) { + if (matched[matcherIndex] == null) { + final description = StringDescription() + .add('has no match for ') + .addDescriptionOf(_expected[matcherIndex]) + .add(' at index $matcherIndex'); + final remainingUnmatched = + matched.sublist(matcherIndex + 1).where((m) => m == null).length; + return remainingUnmatched == 0 + ? description.toString() + : description + .add(' along with $remainingUnmatched other unmatched') + .toString(); + } + } + return null; + } + + @override + bool typedMatches(Iterable item, Map mismatchState) => + _test(item.toList()) == null; + + @override + Description describe(Description description) => description + .add('matches ') + .addAll('[', ', ', ']', _expected) + .add(' unordered'); + + @override + Description describeTypedMismatch(dynamic item, + Description mismatchDescription, Map matchState, bool verbose) => + mismatchDescription.add(_test(item.toList())!); + + /// Returns `true` if the value at [valueIndex] can be paired with some + /// unmatched matcher and updates the state of [matched]. + /// + /// If there is a conflict where multiple values may match the same matcher + /// recursively looks for a new place to match the old value. + bool _findPairing( + List> edges, int valueIndex, List matched) => + _findPairingInner(edges, valueIndex, matched, {}); + + /// Implementation of [_findPairing], tracks [reserved] which are the + /// matchers that have been used _during_ this search. + bool _findPairingInner(List> edges, int valueIndex, + List matched, Set reserved) { + final possiblePairings = + edges[valueIndex].where((m) => !reserved.contains(m)); + for (final matcherIndex in possiblePairings) { + reserved.add(matcherIndex); + final previouslyMatched = matched[matcherIndex]; + if (previouslyMatched == null || + // If the matcher isn't already free, check whether the existing value + // occupying the matcher can be bumped to another one. + _findPairingInner(edges, matched[matcherIndex]!, matched, reserved)) { + matched[matcherIndex] = valueIndex; + return true; + } + } + return false; + } +} + +/// A pairwise matcher for [Iterable]s. +/// +/// The [comparator] function, taking an expected and an actual argument, and +/// returning whether they match, will be applied to each pair in order. +/// [description] should be a meaningful name for the comparator. +Matcher pairwiseCompare(Iterable expected, + bool Function(S, T) comparator, String description) => + _PairwiseCompare(expected, comparator, description); + +typedef _Comparator = bool Function(S a, T b); + +class _PairwiseCompare extends _IterableMatcher { + final Iterable _expected; + final _Comparator _comparator; + final String _description; + + _PairwiseCompare(this._expected, this._comparator, this._description); + + @override + bool typedMatches(Iterable item, Map matchState) { + if (item.length != _expected.length) return false; + var iterator = item.iterator; + var i = 0; + for (var e in _expected) { + iterator.moveNext(); + if (!_comparator(e, iterator.current as T)) { + addStateInfo(matchState, + {'index': i, 'expected': e, 'actual': iterator.current}); + return false; + } + i++; + } + return true; + } + + @override + Description describe(Description description) => + description.add('pairwise $_description ').addDescriptionOf(_expected); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + if (item.length != _expected.length) { + return mismatchDescription + .add('has length ${item.length} instead of ${_expected.length}'); + } else { + return mismatchDescription + .add('has ') + .addDescriptionOf(matchState['actual']) + .add(' which is not $_description ') + .addDescriptionOf(matchState['expected']) + .add(' at index ${matchState["index"]}'); + } + } +} + +/// Matches [Iterable]s which contain an element matching every value in +/// [expected] in any order, and may contain additional values. +/// +/// For example: `[0, 1, 0, 2, 0]` matches `containsAll([1, 2])` and +/// `containsAll([2, 1])` but not `containsAll([1, 2, 3])`. +/// +/// Will only match values which implement [Iterable]. +/// +/// Each element in the value will only be considered a match for a single +/// matcher in [expected] even if it could satisfy more than one. For instance +/// `containsAll([greaterThan(1), greaterThan(2)])` will not be satisfied by +/// `[3]`. To check that all matchers are satisfied within an iterable and allow +/// the same element to satisfy multiple matchers use +/// `allOf(matchers.map(contains))`. +/// +/// Note that this is worst case O(n^2) runtime and memory usage so it should +/// only be used on small iterables. +Matcher containsAll(Iterable expected) => _ContainsAll(expected); + +class _ContainsAll extends _UnorderedMatches { + final Iterable _unwrappedExpected; + + _ContainsAll(Iterable expected) + : _unwrappedExpected = expected, + super(expected.map(wrapMatcher), allowUnmatchedValues: true); + @override + Description describe(Description description) => + description.add('contains all of ').addDescriptionOf(_unwrappedExpected); +} + +/// Matches [Iterable]s which contain an element matching every value in +/// [expected] in the same order, but may contain additional values interleaved +/// throughout. +/// +/// For example: `[0, 1, 0, 2, 0]` matches `containsAllInOrder([1, 2])` but not +/// `containsAllInOrder([2, 1])` or `containsAllInOrder([1, 2, 3])`. +/// +/// Will only match values which implement [Iterable]. +Matcher containsAllInOrder(Iterable expected) => _ContainsAllInOrder(expected); + +class _ContainsAllInOrder extends _IterableMatcher { + final Iterable _expected; + + _ContainsAllInOrder(this._expected); + + String? _test(Iterable item, Map matchState) { + var matchers = _expected.map(wrapMatcher).toList(); + var matcherIndex = 0; + for (var value in item) { + if (matchers[matcherIndex].matches(value, matchState)) matcherIndex++; + if (matcherIndex == matchers.length) return null; + } + return StringDescription() + .add('did not find a value matching ') + .addDescriptionOf(matchers[matcherIndex]) + .add(' following expected prior values') + .toString(); + } + + @override + bool typedMatches(Iterable item, Map matchState) => + _test(item, matchState) == null; + + @override + Description describe(Description description) => description + .add('contains in order(') + .addDescriptionOf(_expected) + .add(')'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) => + mismatchDescription.add(_test(item, matchState)!); +} + +/// Matches [Iterable]s where exactly one element matches the expected +/// value, and all other elements don't match. +Matcher containsOnce(Object? expected) => _ContainsOnce(expected); + +class _ContainsOnce extends _IterableMatcher { + final Object? _expected; + + _ContainsOnce(this._expected); + + String? _test(Iterable item, Map matchState) { + var matcher = wrapMatcher(_expected); + var matches = [ + for (var value in item) + if (matcher.matches(value, matchState)) value, + ]; + if (matches.length == 1) { + return null; + } + if (matches.isEmpty) { + return StringDescription() + .add('did not find a value matching ') + .addDescriptionOf(matcher) + .toString(); + } + return StringDescription() + .add('expected only one value matching ') + .addDescriptionOf(matcher) + .add(' but found multiple: ') + .addAll('', ', ', '', matches) + .toString(); + } + + @override + bool typedMatches(Iterable item, Map matchState) => + _test(item, matchState) == null; + + @override + Description describe(Description description) => + description.add('contains once(').addDescriptionOf(_expected).add(')'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) => + mismatchDescription.add(_test(item, matchState)!); +} diff --git a/pkgs/matcher/lib/src/map_matchers.dart b/pkgs/matcher/lib/src/map_matchers.dart new file mode 100644 index 000000000..97be1ea7d --- /dev/null +++ b/pkgs/matcher/lib/src/map_matchers.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2012, 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 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher which matches maps containing the given [value]. +Matcher containsValue(Object? value) => _ContainsValue(value); + +class _ContainsValue extends Matcher { + final Object? _value; + + const _ContainsValue(this._value); + + @override + bool matches(Object? item, Map matchState) => + (item as dynamic).containsValue(_value); + @override + Description describe(Description description) => + description.add('contains value ').addDescriptionOf(_value); +} + +/// Returns a matcher which matches maps containing the key-value pair +/// with [key] => [valueOrMatcher]. +Matcher containsPair(Object? key, Object? valueOrMatcher) => + _ContainsMapping(key, wrapMatcher(valueOrMatcher)); + +class _ContainsMapping extends Matcher { + final Object? _key; + final Matcher _valueMatcher; + + const _ContainsMapping(this._key, this._valueMatcher); + + @override + bool matches(Object? item, Map matchState) => + (item as dynamic).containsKey(_key) && + _valueMatcher.matches(item[_key], matchState); + + @override + Description describe(Description description) { + return description + .add('contains pair ') + .addDescriptionOf(_key) + .add(' => ') + .addDescriptionOf(_valueMatcher); + } + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (!(item as dynamic).containsKey(_key)) { + return mismatchDescription + .add(" doesn't contain key ") + .addDescriptionOf(_key); + } else { + mismatchDescription + .add(' contains key ') + .addDescriptionOf(_key) + .add(' but with value '); + _valueMatcher.describeMismatch( + item[_key], mismatchDescription, matchState, verbose); + return mismatchDescription; + } + } +} diff --git a/pkgs/matcher/lib/src/numeric_matchers.dart b/pkgs/matcher/lib/src/numeric_matchers.dart new file mode 100644 index 000000000..5193d302e --- /dev/null +++ b/pkgs/matcher/lib/src/numeric_matchers.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2012, 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 'feature_matcher.dart'; +import 'interfaces.dart'; + +/// Returns a matcher which matches if the match argument is within [delta] +/// of some [value]. +/// +/// In other words, this matches if the match argument is greater than +/// than or equal [value]-[delta] and less than or equal to [value]+[delta]. +Matcher closeTo(num value, num delta) => _IsCloseTo(value, delta); + +class _IsCloseTo extends FeatureMatcher { + final num _value, _delta; + + const _IsCloseTo(this._value, this._delta); + + @override + bool typedMatches(dynamic item, Map matchState) { + var diff = item - _value; + if (diff < 0) diff = -diff; + return diff <= _delta; + } + + @override + Description describe(Description description) => description + .add('a numeric value within ') + .addDescriptionOf(_delta) + .add(' of ') + .addDescriptionOf(_value); + + @override + Description describeTypedMismatch(dynamic item, + Description mismatchDescription, Map matchState, bool verbose) { + var diff = item - _value; + if (diff < 0) diff = -diff; + return mismatchDescription.add(' differs by ').addDescriptionOf(diff); + } +} + +/// Returns a matcher which matches if the match argument is greater +/// than or equal to [low] and less than or equal to [high]. +Matcher inInclusiveRange(num low, num high) => _InRange(low, high, true, true); + +/// Returns a matcher which matches if the match argument is greater +/// than [low] and less than [high]. +Matcher inExclusiveRange(num low, num high) => + _InRange(low, high, false, false); + +/// Returns a matcher which matches if the match argument is greater +/// than [low] and less than or equal to [high]. +Matcher inOpenClosedRange(num low, num high) => + _InRange(low, high, false, true); + +/// Returns a matcher which matches if the match argument is greater +/// than or equal to a [low] and less than [high]. +Matcher inClosedOpenRange(num low, num high) => + _InRange(low, high, true, false); + +class _InRange extends FeatureMatcher { + final num _low, _high; + final bool _lowMatchValue, _highMatchValue; + + const _InRange( + this._low, this._high, this._lowMatchValue, this._highMatchValue); + + @override + bool typedMatches(dynamic value, Map matchState) { + if (value < _low || value > _high) { + return false; + } + if (value == _low) { + return _lowMatchValue; + } + if (value == _high) { + return _highMatchValue; + } + // Value may still be outside if range if it can't be compared. + return value > _low && value < _high; + } + + @override + Description describe(Description description) => + description.add('be in range from ' + "$_low (${_lowMatchValue ? 'inclusive' : 'exclusive'}) to " + "$_high (${_highMatchValue ? 'inclusive' : 'exclusive'})"); +} diff --git a/pkgs/matcher/lib/src/operator_matchers.dart b/pkgs/matcher/lib/src/operator_matchers.dart new file mode 100644 index 000000000..ced25fcd7 --- /dev/null +++ b/pkgs/matcher/lib/src/operator_matchers.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2012, 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 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher that inverts [valueOrMatcher] to its logical negation. +Matcher isNot(Object? valueOrMatcher) => _IsNot(wrapMatcher(valueOrMatcher)); + +class _IsNot extends Matcher { + final Matcher _matcher; + + const _IsNot(this._matcher); + + @override + bool matches(dynamic item, Map matchState) => + !_matcher.matches(item, matchState); + + @override + Description describe(Description description) => + description.add('not ').addDescriptionOf(_matcher); +} + +/// This returns a matcher that matches if all of the matchers passed as +/// arguments (up to 7) match. +/// +/// Instead of passing the matchers separately they can be passed as a single +/// List argument. Any argument that is not a matcher is implicitly wrapped in a +/// Matcher to check for equality. +Matcher allOf(Object? arg0, + [Object? arg1, + Object? arg2, + Object? arg3, + Object? arg4, + Object? arg5, + Object? arg6]) { + return _AllOf(_wrapArgs(arg0, arg1, arg2, arg3, arg4, arg5, arg6)); +} + +class _AllOf extends Matcher { + final List _matchers; + + const _AllOf(this._matchers); + + @override + bool matches(dynamic item, Map matchState) { + for (var matcher in _matchers) { + if (!matcher.matches(item, matchState)) { + addStateInfo(matchState, {'matcher': matcher}); + return false; + } + } + return true; + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + var matcher = matchState['matcher']; + matcher.describeMismatch( + item, mismatchDescription, matchState['state'], verbose); + return mismatchDescription; + } + + @override + Description describe(Description description) => + description.addAll('(', ' and ', ')', _matchers); +} + +/// Matches if any of the given matchers evaluate to true. +/// +/// The arguments can be a set of matchers as separate parameters +/// (up to 7), or a List of matchers. +/// +/// The matchers are evaluated from left to right using short-circuit +/// evaluation, so evaluation stops as soon as a matcher returns true. +/// +/// Any argument that is not a matcher is implicitly wrapped in a +/// Matcher to check for equality. +Matcher anyOf(Object? arg0, + [Object? arg1, + Object? arg2, + Object? arg3, + Object? arg4, + Object? arg5, + Object? arg6]) { + return _AnyOf(_wrapArgs(arg0, arg1, arg2, arg3, arg4, arg5, arg6)); +} + +class _AnyOf extends Matcher { + final List _matchers; + + const _AnyOf(this._matchers); + + @override + bool matches(dynamic item, Map matchState) { + for (var matcher in _matchers) { + if (matcher.matches(item, matchState)) { + return true; + } + } + return false; + } + + @override + Description describe(Description description) => + description.addAll('(', ' or ', ')', _matchers); +} + +List _wrapArgs(Object? arg0, Object? arg1, Object? arg2, Object? arg3, + Object? arg4, Object? arg5, Object? arg6) { + Iterable args; + if (arg0 is List) { + if (arg1 != null || + arg2 != null || + arg3 != null || + arg4 != null || + arg5 != null || + arg6 != null) { + throw ArgumentError('If arg0 is a List, all other arguments must be' + ' null.'); + } + + args = arg0; + } else { + args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6].where((e) => e != null); + } + + return args.map(wrapMatcher).toList(); +} diff --git a/pkgs/matcher/lib/src/order_matchers.dart b/pkgs/matcher/lib/src/order_matchers.dart new file mode 100644 index 000000000..1146f6ab2 --- /dev/null +++ b/pkgs/matcher/lib/src/order_matchers.dart @@ -0,0 +1,108 @@ +// Copyright (c) 2016, 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 'interfaces.dart'; + +/// Returns a matcher which matches if the match argument is greater +/// than the given [value]. +Matcher greaterThan(Object value) => + _OrderingMatcher(value, false, false, true, 'a value greater than'); + +/// Returns a matcher which matches if the match argument is greater +/// than or equal to the given [value]. +Matcher greaterThanOrEqualTo(Object value) => _OrderingMatcher( + value, true, false, true, 'a value greater than or equal to'); + +/// Returns a matcher which matches if the match argument is less +/// than the given [value]. +Matcher lessThan(Object value) => + _OrderingMatcher(value, false, true, false, 'a value less than'); + +/// Returns a matcher which matches if the match argument is less +/// than or equal to the given [value]. +Matcher lessThanOrEqualTo(Object value) => + _OrderingMatcher(value, true, true, false, 'a value less than or equal to'); + +/// A matcher which matches if the match argument is zero. +const Matcher isZero = + _OrderingMatcher(0, true, false, false, 'a value equal to'); + +/// A matcher which matches if the match argument is non-zero. +const Matcher isNonZero = + _OrderingMatcher(0, false, true, true, 'a value not equal to'); + +/// A matcher which matches if the match argument is positive. +const Matcher isPositive = + _OrderingMatcher(0, false, false, true, 'a positive value', false); + +/// A matcher which matches if the match argument is zero or negative. +const Matcher isNonPositive = + _OrderingMatcher(0, true, true, false, 'a non-positive value', false); + +/// A matcher which matches if the match argument is negative. +const Matcher isNegative = + _OrderingMatcher(0, false, true, false, 'a negative value', false); + +/// A matcher which matches if the match argument is zero or positive. +const Matcher isNonNegative = + _OrderingMatcher(0, true, false, true, 'a non-negative value', false); + +// TODO(kevmoo) Note that matchers that use _OrderingComparison only use +// `==` and `<` operators to evaluate the match. Or change the matcher. +class _OrderingMatcher extends Matcher { + /// Expected value. + final Object _value; + + /// What to return if actual == expected + final bool _equalValue; + + /// What to return if actual < expected + final bool _lessThanValue; + + /// What to return if actual > expected + final bool _greaterThanValue; + + /// Textual name of the inequality + final String _comparisonDescription; + + /// Whether to include the expected value in the description + final bool _valueInDescription; + + const _OrderingMatcher(this._value, this._equalValue, this._lessThanValue, + this._greaterThanValue, this._comparisonDescription, + [bool valueInDescription = true]) + : _valueInDescription = valueInDescription; + + @override + bool matches(Object? item, Map matchState) { + if (item == _value) { + return _equalValue; + } else if ((item as dynamic) < _value) { + return _lessThanValue; + } else if (item > _value) { + return _greaterThanValue; + } else { + return false; + } + } + + @override + Description describe(Description description) { + if (_valueInDescription) { + return description + .add(_comparisonDescription) + .add(' ') + .addDescriptionOf(_value); + } else { + return description.add(_comparisonDescription); + } + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + mismatchDescription.add('is not '); + return describe(mismatchDescription); + } +} diff --git a/pkgs/matcher/lib/src/pretty_print.dart b/pkgs/matcher/lib/src/pretty_print.dart new file mode 100644 index 000000000..d9eaaeccf --- /dev/null +++ b/pkgs/matcher/lib/src/pretty_print.dart @@ -0,0 +1,133 @@ +// Copyright (c) 2013, 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 'description.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// Returns a pretty-printed representation of [object]. +/// +/// If [maxLineLength] is passed, this will attempt to ensure that each line is +/// no longer than [maxLineLength] characters long. This isn't guaranteed, since +/// individual objects may have string representations that are too long, but +/// most lines will be less than [maxLineLength] long. +/// +/// If [maxItems] is passed, [Iterable]s and [Map]s will only print their first +/// [maxItems] members or key/value pairs, respectively. +String prettyPrint(Object? object, {int? maxLineLength, int? maxItems}) { + String prettyPrintImpl( + Object? object, int indent, Set seen, bool top) { + // If the object is a matcher, use its description. + if (object is Matcher) { + var description = StringDescription(); + object.describe(description); + return '<$description>'; + } + + // Avoid looping infinitely on recursively-nested data structures. + if (seen.contains(object)) return '(recursive)'; + seen = seen.union({object}); + String pp(Object? child) => prettyPrintImpl(child, indent + 2, seen, false); + + if (object is Iterable) { + // Print the type name for non-List iterables. + var type = object is List ? '' : '${_typeName(object)}:'; + + // Truncate the list of strings if it's longer than [maxItems]. + var strings = object.map(pp).toList(); + if (maxItems != null && strings.length > maxItems) { + strings.replaceRange(maxItems - 1, strings.length, ['...']); + } + + // If the printed string is short and doesn't contain a newline, print it + // as a single line. + var singleLine = "$type[${strings.join(', ')}]"; + if ((maxLineLength == null || + singleLine.length + indent <= maxLineLength) && + !singleLine.contains('\n')) { + return singleLine; + } + + // Otherwise, print each member on its own line. + return '$type[\n${strings.map((string) { + return _indent(indent + 2) + string; + }).join(',\n')}\n${_indent(indent)}]'; + } else if (object is Map) { + // Convert the contents of the map to string representations. + var strings = object.keys.map((key) { + return '${pp(key)}: ${pp(object[key])}'; + }).toList(); + + // Truncate the list of strings if it's longer than [maxItems]. + if (maxItems != null && strings.length > maxItems) { + strings.replaceRange(maxItems - 1, strings.length, ['...']); + } + + // If the printed string is short and doesn't contain a newline, print it + // as a single line. + var singleLine = '{${strings.join(", ")}}'; + if ((maxLineLength == null || + singleLine.length + indent <= maxLineLength) && + !singleLine.contains('\n')) { + return singleLine; + } + + // Otherwise, print each key/value pair on its own line. + return '{\n${strings.map((string) { + return _indent(indent + 2) + string; + }).join(',\n')}\n${_indent(indent)}}'; + } else if (object is String) { + // Escape strings and print each line on its own line. + var value = object + .split('\n') + .map(_escapeString) + .join("\\n'\n${_indent(indent + 2)}'"); + return "'$value'"; + } else { + var value = object.toString().replaceAll('\n', '${_indent(indent)}\n'); + var defaultToString = value.startsWith('Instance of '); + + // If this is the top-level call to [prettyPrint], wrap the value on angle + // brackets to set it apart visually. + if (top) value = '<$value>'; + + // Print the type of objects with custom [toString] methods. Primitive + // objects and objects that don't implement a custom [toString] don't need + // to have their types printed. + if (object is num || + object is bool || + object is Function || + object is RegExp || + object is MapEntry || + object is Expando || + object == null || + defaultToString) { + return value; + } else { + return '${_typeName(object)}:$value'; + } + } + } + + return prettyPrintImpl(object, 0, {}, true); +} + +String _indent(int length) => List.filled(length, ' ').join(''); + +/// Returns the name of the type of [x] with fallbacks for core types with +/// private implementations. +String _typeName(Object x) { + if (x is Type) return 'Type'; + if (x is Uri) return 'Uri'; + if (x is Set) return 'Set'; + if (x is BigInt) return 'BigInt'; + return '${x.runtimeType}'; +} + +/// Returns [source] with any control characters replaced by their escape +/// sequences. +/// +/// This doesn't add quotes to the string, but it does escape single quote +/// characters so that single quotes can be applied externally. +String _escapeString(String source) => escape(source).replaceAll("'", r"\'"); diff --git a/pkgs/matcher/lib/src/string_matchers.dart b/pkgs/matcher/lib/src/string_matchers.dart new file mode 100644 index 000000000..8b2f95ae1 --- /dev/null +++ b/pkgs/matcher/lib/src/string_matchers.dart @@ -0,0 +1,183 @@ +// Copyright (c) 2012, 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 'feature_matcher.dart'; +import 'interfaces.dart'; + +/// Returns a matcher which matches if the match argument is a string and +/// is equal to [value] when compared case-insensitively. +Matcher equalsIgnoringCase(String value) => _IsEqualIgnoringCase(value); + +class _IsEqualIgnoringCase extends FeatureMatcher { + final String _value; + final String _matchValue; + + _IsEqualIgnoringCase(String value) + : _value = value, + _matchValue = value.toLowerCase(); + + @override + bool typedMatches(String item, Map matchState) => + _matchValue == item.toLowerCase(); + + @override + Description describe(Description description) => + description.addDescriptionOf(_value).add(' ignoring case'); +} + +/// Returns a matcher which matches if the match argument is a string and +/// is equal to [value], ignoring whitespace. +/// +/// In this matcher, "ignoring whitespace" means comparing with all runs of +/// whitespace collapsed to single space characters and leading and trailing +/// whitespace removed. +/// +/// For example, the following will all match successfully: +/// +/// expect("hello world", equalsIgnoringWhitespace("hello world")); +/// expect(" hello world", equalsIgnoringWhitespace("hello world")); +/// expect("hello world ", equalsIgnoringWhitespace("hello world")); +/// +/// The following will not match: +/// +/// expect("helloworld", equalsIgnoringWhitespace("hello world")); +/// expect("he llo world", equalsIgnoringWhitespace("hello world")); +Matcher equalsIgnoringWhitespace(String value) => + _IsEqualIgnoringWhitespace(value); + +class _IsEqualIgnoringWhitespace extends FeatureMatcher { + final String _matchValue; + + _IsEqualIgnoringWhitespace(String value) + : _matchValue = collapseWhitespace(value); + + @override + bool typedMatches(String item, Map matchState) => + _matchValue == collapseWhitespace(item); + + @override + Description describe(Description description) => + description.addDescriptionOf(_matchValue).add(' ignoring whitespace'); + + @override + Description describeTypedMismatch(dynamic item, + Description mismatchDescription, Map matchState, bool verbose) { + return mismatchDescription + .add('is ') + .addDescriptionOf(collapseWhitespace(item)) + .add(' with whitespace compressed'); + } +} + +/// Returns a matcher that matches if the match argument is a string and +/// starts with [prefixString]. +Matcher startsWith(String prefixString) => _StringStartsWith(prefixString); + +class _StringStartsWith extends FeatureMatcher { + final String _prefix; + + const _StringStartsWith(this._prefix); + + @override + bool typedMatches(dynamic item, Map matchState) => item.startsWith(_prefix); + + @override + Description describe(Description description) => + description.add('a string starting with ').addDescriptionOf(_prefix); +} + +/// Returns a matcher that matches if the match argument is a string and +/// ends with [suffixString]. +Matcher endsWith(String suffixString) => _StringEndsWith(suffixString); + +class _StringEndsWith extends FeatureMatcher { + final String _suffix; + + const _StringEndsWith(this._suffix); + + @override + bool typedMatches(dynamic item, Map matchState) => item.endsWith(_suffix); + + @override + Description describe(Description description) => + description.add('a string ending with ').addDescriptionOf(_suffix); +} + +/// Returns a matcher that matches if the match argument is a string and +/// contains a given list of [substrings] in relative order. +/// +/// For example, `stringContainsInOrder(["a", "e", "i", "o", "u"])` will match +/// "abcdefghijklmnopqrstuvwxyz". + +Matcher stringContainsInOrder(List substrings) => + _StringContainsInOrder(substrings); + +class _StringContainsInOrder extends FeatureMatcher { + final List _substrings; + + const _StringContainsInOrder(this._substrings); + + @override + bool typedMatches(dynamic item, Map matchState) { + var fromIndex = 0; + for (var s in _substrings) { + var index = item.indexOf(s, fromIndex); + if (index < 0) return false; + fromIndex = index + s.length; + } + return true; + } + + @override + Description describe(Description description) => description.addAll( + 'a string containing ', ', ', ' in order', _substrings); +} + +/// Returns a matcher that matches if the match argument is a string and +/// matches the regular expression given by [re]. +/// +/// [re] can be a [RegExp] instance or a [String]; in the latter case it will be +/// used to create a RegExp instance. +Matcher matches(Pattern re) => _MatchesRegExp(re); + +class _MatchesRegExp extends FeatureMatcher { + final RegExp _regexp; + + _MatchesRegExp(Pattern re) + : _regexp = (re is String) + ? RegExp(re) + : (re is RegExp) + ? re + : throw ArgumentError('matches requires a regexp or string'); + + @override + bool typedMatches(dynamic item, Map matchState) => _regexp.hasMatch(item); + + @override + Description describe(Description description) => + description.add("match '${_regexp.pattern}'"); +} + +/// Utility function to collapse whitespace runs to single spaces +/// and strip leading/trailing whitespace. +String collapseWhitespace(String string) { + var result = StringBuffer(); + var skipSpace = true; + for (var i = 0; i < string.length; i++) { + var character = string[i]; + if (_isWhitespace(character)) { + if (!skipSpace) { + result.write(' '); + skipSpace = true; + } + } else { + result.write(character); + skipSpace = false; + } + } + return result.toString().trim(); +} + +bool _isWhitespace(String ch) => + ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; diff --git a/pkgs/matcher/lib/src/type_matcher.dart b/pkgs/matcher/lib/src/type_matcher.dart new file mode 100644 index 000000000..c78a3b2cd --- /dev/null +++ b/pkgs/matcher/lib/src/type_matcher.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2018, 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 'package:meta/meta.dart'; + +import 'having_matcher.dart'; +import 'interfaces.dart'; + +/// Returns a matcher that matches objects with type [T]. +/// +/// ```dart +/// expect(shouldBeDuration, isA()); +/// ``` +/// +/// Expectations can be chained on top of the type using the +/// [TypeMatcher.having] method to add additional constraints. +TypeMatcher isA() => TypeMatcher(); + +/// A [Matcher] subclass that supports validating the [Type] of the target +/// object. +/// +/// ```dart +/// expect(shouldBeDuration, TypeMatcher()); +/// ``` +/// +/// If you want to further validate attributes of the specified [Type], use the +/// [having] function. +/// +/// ```dart +/// void shouldThrowRangeError(int value) { +/// throw RangeError.range(value, 10, 20); +/// } +/// +/// expect( +/// () => shouldThrowRangeError(5), +/// throwsA(const TypeMatcher() +/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) +/// .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); +/// ``` +/// +/// Notice that you can chain multiple calls to [having] to verify multiple +/// aspects of an object. +/// +/// Note: All of the top-level `isType` matchers exposed by this package are +/// instances of [TypeMatcher], so you can use the [having] function without +/// creating your own instance. +/// +/// ```dart +/// expect( +/// () => shouldThrowRangeError(5), +/// throwsA(isRangeError +/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) +/// .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); +/// ``` +class TypeMatcher extends Matcher { + final String? _name; + + /// Create a matcher matches instances of type [T]. + /// + /// For a fluent API to create TypeMatchers see [isA]. + const TypeMatcher( + [@Deprecated('Provide a type argument to TypeMatcher and omit the name. ' + 'This argument will be removed in the next release.') + String? name]) + : _name = + // ignore: deprecated_member_use_from_same_package + name; + + /// Returns a new [TypeMatcher] that validates the existing type as well as + /// a specific [feature] of the object with the provided [matcher]. + /// + /// Provides a human-readable [description] of the [feature] to make debugging + /// failures easier. + /// + /// ```dart + /// /// Validates that the object is a [RangeError] with a message containing + /// /// the string 'details' and `start` and `end` properties that are `null`. + /// final _rangeMatcher = isRangeError + /// .having((e) => e.message, 'message', contains('details')) + /// .having((e) => e.start, 'start', isNull) + /// .having((e) => e.end, 'end', isNull); + /// ``` + @useResult + TypeMatcher having( + Object? Function(T) feature, String description, dynamic matcher) => + HavingMatcher(this, description, feature, matcher); + + @override + Description describe(Description description) { + var name = _name ?? _stripDynamic(T); + return description.add(""); + } + + @override + bool matches(Object? item, Map matchState) => item is T; + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + var name = _name ?? _stripDynamic(T); + return mismatchDescription.add("is not an instance of '$name'"); + } +} + +final _dart2DynamicArgs = RegExp(''); + +/// With this expression `{}.runtimeType.toString`, +/// Dart 1: " +/// Dart 2: ">" +/// +/// This functions returns the Dart 1 output, when Dart 2 runtime semantics +/// are enabled. +String _stripDynamic(Type type) => + type.toString().replaceAll(_dart2DynamicArgs, ''); diff --git a/pkgs/matcher/lib/src/util.dart b/pkgs/matcher/lib/src/util.dart new file mode 100644 index 000000000..af0ba2c64 --- /dev/null +++ b/pkgs/matcher/lib/src/util.dart @@ -0,0 +1,70 @@ +// 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 'core_matchers.dart'; +import 'equals_matcher.dart'; +import 'interfaces.dart'; + +/// A [Map] between whitespace characters and their escape sequences. +const _escapeMap = { + '\n': r'\n', + '\r': r'\r', + '\f': r'\f', + '\b': r'\b', + '\t': r'\t', + '\v': r'\v', + '\x7F': r'\x7F', // delete +}; + +/// A [RegExp] that matches whitespace characters that should be escaped. +final _escapeRegExp = RegExp( + '[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_getHexLiteral).join()}]'); + +/// Useful utility for nesting match states. +void addStateInfo(Map matchState, Map values) { + var innerState = Map.of(matchState); + matchState.clear(); + matchState['state'] = innerState; + matchState.addAll(values); +} + +/// Takes an argument and returns an equivalent [Matcher]. +/// +/// If the argument is already a matcher this does nothing, +/// else if the argument is a function, it generates a predicate +/// function matcher, else it generates an equals matcher. +Matcher wrapMatcher(Object? valueOrMatcher) { + if (valueOrMatcher is Matcher) { + return valueOrMatcher; + } else if (valueOrMatcher is bool Function(Object?)) { + // already a predicate that can handle anything + return predicate(valueOrMatcher); + } else if (valueOrMatcher is bool Function(Never)) { + // unary predicate, but expects a specific type + // so wrap it. + // ignore: unnecessary_lambdas + return predicate((a) => (valueOrMatcher as dynamic)(a)); + } else { + return equals(valueOrMatcher); + } +} + +/// Returns [str] with all whitespace characters represented as their escape +/// sequences. +/// +/// Backslash characters are escaped as `\\` +String escape(String str) { + str = str.replaceAll('\\', r'\\'); + return str.replaceAllMapped(_escapeRegExp, (match) { + var mapped = _escapeMap[match[0]]; + if (mapped != null) return mapped; + return _getHexLiteral(match[0]!); + }); +} + +/// Given single-character string, return the hex-escaped equivalent. +String _getHexLiteral(String input) { + var rune = input.runes.single; + return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0'); +} diff --git a/pkgs/matcher/pubspec.yaml b/pkgs/matcher/pubspec.yaml new file mode 100644 index 000000000..938a18ae8 --- /dev/null +++ b/pkgs/matcher/pubspec.yaml @@ -0,0 +1,25 @@ +name: matcher +version: 0.12.17-wip +description: >- + Support for specifying test expectations via an extensible Matcher class. + Also includes a number of built-in Matcher implementations for common cases. +repository: https://github.com/dart-lang/matcher + +environment: + sdk: ^3.4.0 + +dependencies: + async: ^2.10.0 + meta: ^1.8.0 + stack_trace: ^1.10.0 + term_glyph: ^1.2.0 + test_api: ">=0.5.0 <0.8.0" + +dev_dependencies: + fake_async: ^1.3.0 + lints: ^3.0.0 + test: ^1.23.0 + +dependency_overrides: + test: 1.25.0 + test_api: 0.7.3 diff --git a/pkgs/matcher/test/core_matchers_test.dart b/pkgs/matcher/test/core_matchers_test.dart new file mode 100644 index 000000000..04fc8b39d --- /dev/null +++ b/pkgs/matcher/test/core_matchers_test.dart @@ -0,0 +1,247 @@ +// Copyright (c) 2012, 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 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, group; + +import 'test_utils.dart'; + +void main() { + test('isTrue', () { + shouldPass(true, isTrue); + shouldFail(false, isTrue, 'Expected: true Actual: '); + }); + + test('isFalse', () { + shouldPass(false, isFalse); + shouldFail(10, isFalse, 'Expected: false Actual: <10>'); + shouldFail(true, isFalse, 'Expected: false Actual: '); + }); + + test('isNull', () { + shouldPass(null, isNull); + shouldFail(false, isNull, 'Expected: null Actual: '); + }); + + test('isNotNull', () { + shouldPass(false, isNotNull); + shouldFail(null, isNotNull, 'Expected: not null Actual: '); + }); + + test('isNaN', () { + shouldPass(double.nan, isNaN); + shouldFail(3.1, isNaN, 'Expected: NaN Actual: <3.1>'); + shouldFail('not a num', isNaN, endsWith('not an ')); + }); + + test('isNotNaN', () { + shouldPass(3.1, isNotNaN); + shouldFail(double.nan, isNotNaN, 'Expected: not NaN Actual: '); + shouldFail('not a num', isNotNaN, endsWith('not an ')); + }); + + test('same', () { + var a = {}; + var b = {}; + shouldPass(a, same(a)); + shouldFail(b, same(a), 'Expected: same instance as {} Actual: {}'); + }); + + test('equals', () { + var a = {}; + var b = {}; + shouldPass(a, equals(a)); + shouldPass(a, equals(b)); + }); + + test('equals with null', () { + Object? a; // null + var b = {}; + shouldPass(a, equals(a)); + shouldFail( + a, equals(b), 'Expected: {} Actual: Which: expected a map'); + shouldFail(b, equals(a), 'Expected: Actual: {}'); + }); + + test('equals with a set', () { + var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var set1 = numbers.toSet(); + numbers.shuffle(); + var set2 = numbers.toSet(); + + shouldPass(set2, equals(set1)); + shouldPass(numbers, equals(set1)); + shouldFail( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + equals(set1), + matches(r'Expected: .*:\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\]' + r' Actual: \[1, 2, 3, 4, 5, 6, 7, 8, 9\]' + r' Which: does not contain <10>')); + shouldFail( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + equals(set1), + matches(r'Expected: .*:\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\]' + r' Actual: \[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11\]' + r' Which: larger than expected')); + }); + + test('anything', () { + var a = {}; + shouldPass(0, anything); + shouldPass(null, anything); + shouldPass(a, anything); + shouldFail(a, isNot(anything), 'Expected: not anything Actual: {}'); + }); + + test('returnsNormally', () { + shouldPass(doesNotThrow, returnsNormally); + shouldFail( + doesThrow, + returnsNormally, + matches(r'Expected: return normally' + r' Actual: ' + r' Which: threw StateError:')); + shouldFail('not a function', returnsNormally, + contains('not an ')); + }); + + test('hasLength', () { + var a = {}; + var b = []; + shouldPass(a, hasLength(0)); + shouldPass(b, hasLength(0)); + shouldPass('a', hasLength(1)); + shouldFail( + 0, + hasLength(0), + 'Expected: an object with length of <0> ' + 'Actual: <0> ' + 'Which: has no length property'); + + b.add(0); + shouldPass(b, hasLength(1)); + shouldFail( + b, + hasLength(2), + 'Expected: an object with length of <2> ' + 'Actual: [0] ' + 'Which: has length of <1>'); + + b.add(0); + shouldFail( + b, + hasLength(1), + 'Expected: an object with length of <1> ' + 'Actual: [0, 0] ' + 'Which: has length of <2>'); + shouldPass(b, hasLength(2)); + }); + + test('scalar type mismatch', () { + shouldFail( + 'error', + equals(5.1), + 'Expected: <5.1> ' + "Actual: 'error'"); + }); + + test('nested type mismatch', () { + shouldFail( + ['error'], + equals([5.1]), + 'Expected: [5.1] ' + "Actual: ['error'] " + "Which: at location [0] is 'error' instead of <5.1>"); + }); + + test('doubly-nested type mismatch', () { + shouldFail( + [ + ['error'] + ], + equals([ + [5.1] + ]), + 'Expected: [[5.1]] ' + "Actual: [['error']] " + "Which: at location [0][0] is 'error' instead of <5.1>"); + }); + + test('doubly nested inequality', () { + var actual1 = [ + ['foo', 'bar'], + ['foo'], + 3, + [] + ]; + var expected1 = [ + ['foo', 'bar'], + ['foo'], + 4, + [] + ]; + var reason1 = "Expected: [['foo', 'bar'], ['foo'], 4, []] " + "Actual: [['foo', 'bar'], ['foo'], 3, []] " + 'Which: at location [2] is <3> instead of <4>'; + + var actual2 = [ + ['foo', 'barry'], + ['foo'], + 4, + [] + ]; + var expected2 = [ + ['foo', 'bar'], + ['foo'], + 4, + [] + ]; + var reason2 = "Expected: [['foo', 'bar'], ['foo'], 4, []] " + "Actual: [['foo', 'barry'], ['foo'], 4, []] " + "Which: at location [0][1] is 'barry' instead of 'bar'"; + + var actual3 = [ + ['foo', 'bar'], + ['foo'], + 4, + {'foo': 'bar'} + ]; + var expected3 = [ + ['foo', 'bar'], + ['foo'], + 4, + {'foo': 'barry'} + ]; + var reason3 = "Expected: [['foo', 'bar'], ['foo'], 4, {'foo': 'barry'}] " + "Actual: [['foo', 'bar'], ['foo'], 4, {'foo': 'bar'}] " + "Which: at location [3]['foo'] is 'bar' instead of 'barry'"; + + shouldFail(actual1, equals(expected1), reason1); + shouldFail(actual2, equals(expected2), reason2); + shouldFail(actual3, equals(expected3), reason3); + }); + + group('Predicate Matchers', () { + test('isInstanceOf', () { + shouldFail(0, predicate((x) => x is String, 'an instance of String'), + 'Expected: an instance of String Actual: <0>'); + shouldPass('cow', predicate((x) => x is String, 'an instance of String')); + + shouldFail(0, predicate((bool x) => x, 'bool value is true'), + endsWith("not an ")); + }); + }); + + group('wrapMatcher', () { + test('wraps a predicate which allows a nullable argument', () { + final matcher = wrapMatcher((_) => true); + shouldPass(null, matcher); + }); + + test('wraps a predicate which has a typed argument check', () { + final matcher = wrapMatcher((int _) => true); + shouldPass(1, matcher); + }); + }); +} diff --git a/pkgs/matcher/test/custom_matcher_test.dart b/pkgs/matcher/test/custom_matcher_test.dart new file mode 100644 index 000000000..d0a17c9ea --- /dev/null +++ b/pkgs/matcher/test/custom_matcher_test.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2012, 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 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test; + +import 'test_utils.dart'; + +class _BadCustomMatcher extends CustomMatcher { + _BadCustomMatcher() : super('feature', 'description', {1: 'a'}); + @override + Object? featureValueOf(dynamic actual) => throw Exception('bang'); +} + +class _HasPrice extends CustomMatcher { + _HasPrice(Object? matcher) + : super('Widget with a price that is', 'price', matcher); + @override + Object? featureValueOf(Object? actual) => (actual as Widget).price; +} + +void main() { + test('Feature Matcher', () { + var w = Widget(); + w.price = 10; + shouldPass(w, _HasPrice(10)); + shouldPass(w, _HasPrice(greaterThan(0))); + shouldFail( + w, + _HasPrice(greaterThan(10)), + 'Expected: Widget with a price that is a value greater than <10> ' + "Actual: " + 'Which: has price with value <10> which is not ' + 'a value greater than <10>'); + }); + + test('Custom Matcher Exception', () { + shouldFail( + 'a', + _BadCustomMatcher(), + allOf([ + contains("Expected: feature {1: 'a'} "), + contains("Actual: 'a' "), + contains("Which: threw 'Exception: bang' "), + ])); + }); +} diff --git a/pkgs/matcher/test/escape_test.dart b/pkgs/matcher/test/escape_test.dart new file mode 100644 index 000000000..c898054e5 --- /dev/null +++ b/pkgs/matcher/test/escape_test.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: missing_whitespace_between_adjacent_strings + +import 'package:test/test.dart'; + +void main() { + group('escaping should work with', () { + _testEscaping('no escaped chars', 'Hello, world!', 'Hello, world!'); + _testEscaping('newline', '\n', r'\n'); + _testEscaping('carriage return', '\r', r'\r'); + _testEscaping('form feed', '\f', r'\f'); + _testEscaping('backspace', '\b', r'\b'); + _testEscaping('tab', '\t', r'\t'); + _testEscaping('vertical tab', '\v', r'\v'); + _testEscaping('null byte', '\x00', r'\x00'); + _testEscaping('ASCII control character', '\x11', r'\x11'); + _testEscaping('delete', '\x7F', r'\x7F'); + _testEscaping('escape combos', r'\n', r'\\n'); + _testEscaping( + 'All characters', + 'A new line\nA charriage return\rA form feed\fA backspace\b' + 'A tab\tA vertical tab\vA slash\\A null byte\x00A control char\x1D' + 'A delete\x7F', + r'A new line\nA charriage return\rA form feed\fA backspace\b' + r'A tab\tA vertical tab\vA slash\\A null byte\x00A control char\x1D' + r'A delete\x7F'); + }); + + group('unequal strings remain unequal when escaped', () { + _testUnequalStrings('with a newline', '\n', r'\n'); + _testUnequalStrings('with slash literals', '\\', r'\\'); + }); +} + +/// Creates a [test] with name [name] that verifies [source] escapes to value +/// [target]. +void _testEscaping(String name, String source, String target) { + test(name, () { + var escaped = escape(source); + expect(escaped == target, isTrue, + reason: 'Expected escaped value: $target\n' + ' Actual escaped value: $escaped'); + }); +} + +/// Creates a [test] with name [name] that ensures two different [String] values +/// [s1] and [s2] remain unequal when escaped. +void _testUnequalStrings(String name, String s1, String s2) { + test(name, () { + // Explicitly not using the equals matcher + expect(s1 != s2, isTrue, reason: 'The source values should be unequal'); + + var escapedS1 = escape(s1); + var escapedS2 = escape(s2); + + // Explicitly not using the equals matcher + expect(escapedS1 != escapedS2, isTrue, + reason: 'Unequal strings, when escaped, should remain unequal.'); + }); +} diff --git a/pkgs/matcher/test/expect_async_test.dart b/pkgs/matcher/test/expect_async_test.dart new file mode 100644 index 000000000..dd564ea5c --- /dev/null +++ b/pkgs/matcher/test/expect_async_test.dart @@ -0,0 +1,393 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; + +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import 'utils_new.dart'; + +void main() { + group('supports a function with this many arguments:', () { + test('0', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync0(() { + callbackRun = true; + })(); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('1', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync1((int arg) { + expect(arg, equals(1)); + callbackRun = true; + })(1); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('2', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync2((arg1, arg2) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + callbackRun = true; + })(1, 2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('3', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync3((arg1, arg2, arg3) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + callbackRun = true; + })(1, 2, 3); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('4', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync4((arg1, arg2, arg3, arg4) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + expect(arg4, equals(4)); + callbackRun = true; + })(1, 2, 3, 4); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('5', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync5((arg1, arg2, arg3, arg4, arg5) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + expect(arg4, equals(4)); + expect(arg5, equals(5)); + callbackRun = true; + })(1, 2, 3, 4, 5); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('6', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync6((arg1, arg2, arg3, arg4, arg5, arg6) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + expect(arg4, equals(4)); + expect(arg5, equals(5)); + expect(arg6, equals(6)); + callbackRun = true; + })(1, 2, 3, 4, 5, 6); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + }); + + group('with optional arguments', () { + test('allows them to be passed', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync1(([arg = 1]) { + expect(arg, equals(2)); + callbackRun = true; + })(2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('allows them not to be passed', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync1(([arg = 1]) { + expect(arg, equals(1)); + callbackRun = true; + })(); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + }); + + group('by default', () { + test("won't allow the test to complete until it's called", () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + callback = expectAsync0(() {}); + }); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + await monitor.onDone; + + expectTestPassed(monitor); + }); + + test('may only be called once', () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}); + callback(); + callback(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (1).'); + }); + }); + + group('with count', () { + test( + "won't allow the test to complete until it's called at least that " + 'many times', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + callback = expectAsync0(() {}, count: 3); + }); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + + await monitor.onDone; + + expectTestPassed(monitor); + }); + + test("will throw an error if it's called more than that many times", + () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}, count: 3); + callback(); + callback(); + callback(); + callback(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (3).'); + }); + + group('0,', () { + test("won't block the test's completion", () { + expectAsync0(() {}, count: 0); + }); + + test("will throw an error if it's ever called", () async { + var monitor = await TestCaseMonitor.run(() { + expectAsync0(() {}, count: 0)(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (0).'); + }); + }); + }); + + group('with max', () { + test('will allow the callback to be called that many times', () { + var callback = expectAsync0(() {}, max: 3); + callback(); + callback(); + callback(); + }); + + test('will allow the callback to be called fewer than that many times', () { + var callback = expectAsync0(() {}, max: 3); + callback(); + }); + + test("will throw an error if it's called more than that many times", + () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}, max: 3); + callback(); + callback(); + callback(); + callback(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (3).'); + }); + + test('-1, will allow the callback to be called any number of times', () { + var callback = expectAsync0(() {}, max: -1); + for (var i = 0; i < 20; i++) { + callback(); + } + }); + }); + + test('will throw an error if max is less than count', () { + expect(() => expectAsync0(() {}, max: 1, count: 2), throwsArgumentError); + }); + + group('expectAsyncUntil()', () { + test("won't allow the test to complete until isDone returns true", + () async { + late TestCaseMonitor monitor; + late Future future; + monitor = TestCaseMonitor.start(() { + var done = false; + var callback = expectAsyncUntil0(() {}, () => done); + + future = () async { + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + done = true; + callback(); + }(); + }); + await monitor.onDone; + + expectTestPassed(monitor); + // Ensure that the outer test doesn't complete until the inner future + // completes. + await future; + }); + + test("doesn't call isDone until after the callback is called", () { + var callbackRun = false; + expectAsyncUntil0(() => callbackRun = true, () { + expect(callbackRun, isTrue); + return true; + })(); + }); + }); + + test('allows errors', () async { + var monitor = await TestCaseMonitor.run(() { + expect(expectAsync0(() => throw 'oh no'), throwsA('oh no')); + }); + + expectTestPassed(monitor); + }); + + test('may be called in a non-test zone', () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}); + Zone.root.run(callback); + }); + expectTestPassed(monitor); + }); + + test('may be called in a FakeAsync zone that does not run further', () async { + var monitor = await TestCaseMonitor.run(() { + FakeAsync().run((_) { + var callback = expectAsync0(() {}); + callback(); + }); + }); + expectTestPassed(monitor); + }); + + group('old-style expectAsync()', () { + test('works with no arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expectAsync(() { + callbackRun = true; + })(); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('works with dynamic arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expectAsync((arg1, arg2) { + callbackRun = true; + })(1, 2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('works with non-nullable arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expectAsync((int arg1, int arg2) { + callbackRun = true; + })(1, 2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('works with 6 arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expectAsync((arg1, arg2, arg3, arg4, arg5, arg6) { + callbackRun = true; + })(1, 2, 3, 4, 5, 6); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test("doesn't support a function with 7 arguments", () { + // ignore: deprecated_member_use_from_same_package + expect(() => expectAsync((a, b, c, d, e, f, g) {}), throwsArgumentError); + }); + }); +} diff --git a/pkgs/matcher/test/expect_test.dart b/pkgs/matcher/test/expect_test.dart new file mode 100644 index 000000000..e2ef497a0 --- /dev/null +++ b/pkgs/matcher/test/expect_test.dart @@ -0,0 +1,36 @@ +// Copyright (c) 2017, 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 'package:test/test.dart'; + +import 'utils_new.dart'; + +void main() { + group('returned Future from expectLater()', () { + test('completes immediately for a sync matcher', () { + expect(expectLater(true, isTrue), completes); + }); + + test('contains the expect failure', () { + expect(expectLater(Future.value(true), completion(isFalse)), + throwsA(isTestFailure(anything))); + }); + + test('contains an async error', () { + expect(expectLater(Future.error('oh no'), completion(isFalse)), + throwsA('oh no')); + }); + }); + + group('an async matcher that fails synchronously', () { + test('throws synchronously', () { + expect(() => expect(() {}, throwsA(anything)), + throwsA(isTestFailure(anything))); + }); + + test('can be used with synchronous operators', () { + expect(() {}, isNot(throwsA(anything))); + }); + }); +} diff --git a/pkgs/matcher/test/having_test.dart b/pkgs/matcher/test/having_test.dart new file mode 100644 index 000000000..ddada77e1 --- /dev/null +++ b/pkgs/matcher/test/having_test.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2018, 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. + +// ignore_for_file: lines_longer_than_80_chars + +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, expect, throwsA, group; + +import 'test_utils.dart'; + +void main() { + test('success', () { + shouldPass(RangeError('details'), _rangeMatcher); + }); + + test('failure', () { + shouldFail( + CustomRangeError.range(-1, 1, 10), + _rangeMatcher, + "Expected: with " + "`message`: contains 'details' and `start`: null and `end`: null " + 'Actual: CustomRangeError: ' + "Which: has `message` with value 'Invalid value' " + "which does not contain 'details'", + ); + }); + + // This code is used in the [TypeMatcher] doc comments. + test('integration and example', () { + void shouldThrowRangeError(int value) { + throw RangeError.range(value, 10, 20); + } + + expect( + () => shouldThrowRangeError(5), + throwsA(const TypeMatcher() + .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) + .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); + + expect( + () => shouldThrowRangeError(5), + throwsA(isRangeError + .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) + .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); + }); + + test('having inside deep matcher', () { + shouldFail( + [RangeError.range(-1, 1, 10)], + equals([_rangeMatcher]), + anyOf([ + equalsIgnoringWhitespace( + "Expected: [ < with " + "`message`: contains 'details' and `start`: null and `end`: null> ] " + 'Actual: [RangeError:RangeError: ' + 'Invalid value: Not in inclusive range 1..10: -1] ' + 'Which: at location [0] is RangeError: ' + "which has `message` with value 'Invalid value' " + "which does not contain 'details'"), + equalsIgnoringWhitespace(// Older SDKs + "Expected: [ < with " + "`message`: contains 'details' and `start`: null and `end`: null> ] " + 'Actual: [RangeError:RangeError: ' + 'Invalid value: Not in range 1..10, inclusive: -1] ' + 'Which: at location [0] is RangeError: ' + "which has `message` with value 'Invalid value' " + "which does not contain 'details'") + ])); + }); + + group('CustomMatcher copy', () { + test('Feature Matcher', () { + var w = Widget(); + w.price = 10; + shouldPass(w, _hasPrice(10)); + shouldPass(w, _hasPrice(greaterThan(0))); + shouldFail( + w, + _hasPrice(greaterThan(10)), + "Expected: with `price`: a value greater than <10> " + "Actual: " + 'Which: has `price` with value <10> which is not ' + 'a value greater than <10>'); + }); + + test('Custom Matcher Exception', () { + shouldFail( + 'a', + _badCustomMatcher(), + allOf([ + contains( + "Expected: with `feature`: {1: 'a'} "), + contains("Actual: 'a'"), + ])); + shouldFail( + Widget(), + _badCustomMatcher(), + allOf([ + contains( + "Expected: with `feature`: {1: 'a'} "), + contains("Actual: "), + contains("Which: threw 'Exception: bang' "), + ])); + }); + }); +} + +final _rangeMatcher = isRangeError + .having((e) => e.message, 'message', contains('details')) + .having((e) => e.start, 'start', isNull) + .having((e) => e.end, 'end', isNull); + +Matcher _hasPrice(Object matcher) => + const TypeMatcher().having((e) => e.price, 'price', matcher); + +Matcher _badCustomMatcher() => const TypeMatcher() + .having((e) => throw Exception('bang'), 'feature', {1: 'a'}); + +class CustomRangeError extends RangeError { + CustomRangeError.range( + super.invalidValue, int super.minValue, int super.maxValue) + : super.range(); + + @override + String toString() => 'RangeError: Invalid value: details'; +} diff --git a/pkgs/matcher/test/iterable_matchers_test.dart b/pkgs/matcher/test/iterable_matchers_test.dart new file mode 100644 index 000000000..7607d18dd --- /dev/null +++ b/pkgs/matcher/test/iterable_matchers_test.dart @@ -0,0 +1,395 @@ +// Copyright (c) 2012, 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 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + test('isEmpty', () { + shouldPass([], isEmpty); + shouldFail([1], isEmpty, 'Expected: empty Actual: [1]'); + }); + + test('isNotEmpty', () { + shouldFail([], isNotEmpty, 'Expected: non-empty Actual: []'); + shouldPass([1], isNotEmpty); + }); + + test('contains', () { + var d = [1, 2]; + shouldPass(d, contains(1)); + shouldFail( + d, + contains(0), + 'Expected: contains <0> ' + 'Actual: [1, 2] ' + 'Which: does not contain <0>'); + + shouldFail( + 'String', + contains(42), + "Expected: contains <42> Actual: 'String' " + 'Which: does not contain <42>'); + }); + + test('equals with matcher element', () { + var d = ['foo', 'bar']; + shouldPass(d, equals(['foo', startsWith('ba')])); + shouldFail( + d, + equals(['foo', endsWith('ba')]), + "Expected: ['foo', ] " + "Actual: ['foo', 'bar'] " + "Which: at location [1] is 'bar' which " + "does not match a string ending with 'ba'"); + }); + + test('isIn', () { + // Iterable + shouldPass(1, isIn([1, 2])); + shouldFail(0, isIn([1, 2]), 'Expected: is in [1, 2] Actual: <0>'); + + // Map + shouldPass(1, isIn({1: null})); + shouldFail(0, isIn({1: null}), 'Expected: is in {1: null} Actual: <0>'); + + // String + shouldPass('42', isIn('1421')); + shouldFail('42', isIn('41'), "Expected: is in '41' Actual: '42'"); + shouldFail( + 0, isIn('a string'), endsWith('not an ')); + + // Invalid arg + expect(() => isIn(42), throwsArgumentError); + }); + + test('everyElement', () { + var d = [1, 2]; + var e = [1, 1, 1]; + shouldFail( + d, + everyElement(1), + 'Expected: every element(<1>) ' + 'Actual: [1, 2] ' + "Which: has value <2> which doesn't match <1> at index 1"); + shouldPass(e, everyElement(1)); + shouldFail('not iterable', everyElement(1), + endsWith('not an ')); + }); + + test('nested everyElement', () { + var d = [ + ['foo', 'bar'], + ['foo'], + [] + ]; + var e = [ + ['foo', 'bar'], + ['foo'], + 3, + [] + ]; + shouldPass(d, everyElement(anyOf(isEmpty, contains('foo')))); + shouldFail( + d, + everyElement(everyElement(equals('foo'))), + "Expected: every element(every element('foo')) " + "Actual: [['foo', 'bar'], ['foo'], []] " + "Which: has value ['foo', 'bar'] which has value 'bar' " + 'which is different. Expected: foo Actual: bar ^ ' + 'Differ at offset 0 at index 1 at index 0'); + shouldFail( + d, + everyElement(allOf(hasLength(greaterThan(0)), contains('foo'))), + 'Expected: every element((an object with length of a value ' + "greater than <0> and contains 'foo')) " + "Actual: [['foo', 'bar'], ['foo'], []] " + 'Which: has value [] which has length of <0> at index 2'); + shouldFail( + d, + everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))), + "Expected: every element((contains 'foo' and " + 'an object with length of a value greater than <0>)) ' + "Actual: [['foo', 'bar'], ['foo'], []] " + "Which: has value [] which does not contain 'foo' at index 2"); + shouldFail( + e, + everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))), + "Expected: every element((contains 'foo' and an object with " + 'length of a value greater than <0>)) ' + "Actual: [['foo', 'bar'], ['foo'], 3, []] " + 'Which: has value <3> which is not a string, map or iterable ' + 'at index 2'); + }); + + test('anyElement', () { + var d = [1, 2]; + var e = [1, 1, 1]; + shouldPass(d, anyElement(2)); + shouldFail( + e, anyElement(2), 'Expected: some element <2> Actual: [1, 1, 1]'); + shouldFail('not an iterable', anyElement(2), + endsWith('not an ')); + }); + + test('orderedEquals', () { + shouldPass([null], orderedEquals([null])); + var d = [1, 2]; + shouldPass(d, orderedEquals([1, 2])); + shouldFail( + d, + orderedEquals([2, 1]), + 'Expected: equals [2, 1] ordered ' + 'Actual: [1, 2] ' + 'Which: at location [0] is <1> instead of <2>'); + shouldFail('not an iterable', orderedEquals([1]), + endsWith('not an ')); + }); + + test('unorderedEquals', () { + var d = [1, 2]; + shouldPass(d, unorderedEquals([2, 1])); + shouldFail( + d, + unorderedEquals([1]), + 'Expected: equals [1] unordered ' + 'Actual: [1, 2] ' + 'Which: has too many elements (2 > 1)'); + shouldFail( + d, + unorderedEquals([3, 2, 1]), + 'Expected: equals [3, 2, 1] unordered ' + 'Actual: [1, 2] ' + 'Which: has too few elements (2 < 3)'); + shouldFail( + d, + unorderedEquals([3, 1]), + 'Expected: equals [3, 1] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for <3> at index 0'); + shouldFail( + d, + unorderedEquals([3, 4]), + 'Expected: equals [3, 4] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for <3> at index 0' + ' along with 1 other unmatched'); + shouldFail('not an iterable', unorderedEquals([1]), + endsWith('not an ')); + }); + + test('unorderedMatches', () { + var d = [1, 2]; + shouldPass(d, unorderedMatches([2, 1])); + shouldPass(d, unorderedMatches([greaterThan(1), greaterThan(0)])); + shouldPass(d, unorderedMatches([greaterThan(0), greaterThan(1)])); + shouldPass([2, 1], unorderedMatches([greaterThan(1), greaterThan(0)])); + + shouldPass([2, 1], unorderedMatches([greaterThan(0), greaterThan(1)])); + // Excersize the case where pairings should get "bumped" multiple times + shouldPass( + [0, 1, 2, 3, 5, 6], + unorderedMatches([ + greaterThan(1), // 6 + equals(2), // 2 + allOf([lessThan(3), isNot(0)]), // 1 + equals(0), // 0 + predicate((int v) => v % 2 == 1), // 3 + equals(5), // 5 + ])); + shouldFail( + d, + unorderedMatches([greaterThan(0)]), + 'Expected: matches [a value greater than <0>] unordered ' + 'Actual: [1, 2] ' + 'Which: has too many elements (2 > 1)'); + shouldFail( + d, + unorderedMatches([3, 2, 1]), + 'Expected: matches [<3>, <2>, <1>] unordered ' + 'Actual: [1, 2] ' + 'Which: has too few elements (2 < 3)'); + shouldFail( + d, + unorderedMatches([3, 1]), + 'Expected: matches [<3>, <1>] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for <3> at index 0'); + shouldFail( + d, + unorderedMatches([greaterThan(3), greaterThan(0)]), + 'Expected: matches [a value greater than <3>, a value greater than ' + '<0>] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for a value greater than <3> at index 0'); + shouldFail('not an iterable', unorderedMatches([greaterThan(1)]), + endsWith('not an ')); + }); + + test('containsAll', () { + var d = [0, 1, 2]; + shouldPass(d, containsAll([1, 2])); + shouldPass(d, containsAll([2, 1])); + shouldPass(d, containsAll([greaterThan(0), greaterThan(1)])); + shouldPass([2, 1], containsAll([greaterThan(0), greaterThan(1)])); + shouldFail( + d, + containsAll([1, 2, 3]), + 'Expected: contains all of [1, 2, 3] ' + 'Actual: [0, 1, 2] ' + 'Which: has no match for <3> at index 2'); + shouldFail( + 1, + containsAll([1]), + 'Expected: contains all of [1] ' + 'Actual: <1> ' + "Which: not an "); + shouldFail( + [-1, 2], + containsAll([greaterThan(0), greaterThan(1)]), + 'Expected: contains all of [>, ' + '>] ' + 'Actual: [-1, 2] ' + 'Which: has no match for a value greater than <1> at index 1'); + shouldFail('not an iterable', containsAll([1, 2, 3]), + endsWith('not an ')); + }); + + test('containsAllInOrder', () { + var d = [0, 1, 0, 2]; + shouldPass(d, containsAllInOrder([1, 2])); + shouldPass(d, containsAllInOrder([greaterThan(0), greaterThan(1)])); + shouldFail( + d, + containsAllInOrder([2, 1]), + 'Expected: contains in order([2, 1]) ' + 'Actual: [0, 1, 0, 2] ' + 'Which: did not find a value matching <1> following expected prior ' + 'values'); + shouldFail( + d, + containsAllInOrder([greaterThan(1), greaterThan(0)]), + 'Expected: contains in order([>, ' + '>]) ' + 'Actual: [0, 1, 0, 2] ' + 'Which: did not find a value matching a value greater than <0> ' + 'following expected prior values'); + shouldFail( + d, + containsAllInOrder([1, 2, 3]), + 'Expected: contains in order([1, 2, 3]) ' + 'Actual: [0, 1, 0, 2] ' + 'Which: did not find a value matching <3> following expected prior ' + 'values'); + shouldFail( + 1, + containsAllInOrder([1]), + 'Expected: contains in order([1]) ' + 'Actual: <1> ' + "Which: not an "); + }); + + test('containsOnce', () { + shouldPass([1, 2, 3, 4], containsOnce(2)); + shouldPass([1, 2, 11, 3], containsOnce(greaterThan(10))); + shouldFail( + [1, 2, 3, 4], + containsOnce(10), + 'Expected: contains once(<10>) ' + 'Actual: [1, 2, 3, 4] ' + 'Which: did not find a value matching <10>'); + shouldFail( + [1, 2, 3, 4], + containsOnce(greaterThan(10)), + 'Expected: contains once(a value greater than <10>) ' + 'Actual: [1, 2, 3, 4] ' + 'Which: did not find a value matching a value greater than <10>'); + shouldFail( + [1, 2, 1, 2], + containsOnce(2), + 'Expected: contains once(<2>) ' + 'Actual: [1, 2, 1, 2] ' + 'Which: expected only one value matching <2> ' + 'but found multiple: <2>, <2>'); + shouldFail( + [1, 2, 10, 20], + containsOnce(greaterThan(5)), + 'Expected: contains once(a value greater than <5>) ' + 'Actual: [1, 2, 10, 20] ' + 'Which: expected only one value matching a value greater than <5> ' + 'but found multiple: <10>, <20>'); + }); + + test('pairwise compare', () { + var c = [1, 2]; + var d = [1, 2, 3]; + var e = [1, 4, 9]; + shouldFail( + 'x', + pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'), + 'Expected: pairwise less than or equal [1, 4, 9] ' + "Actual: 'x' " + "Which: not an "); + shouldFail( + c, + pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'), + 'Expected: pairwise less than or equal [1, 4, 9] ' + 'Actual: [1, 2] ' + 'Which: has length 2 instead of 3'); + shouldPass( + d, pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal')); + shouldFail( + d, + pairwiseCompare(e, (int e, int a) => a < e, 'less than'), + 'Expected: pairwise less than [1, 4, 9] ' + 'Actual: [1, 2, 3] ' + 'Which: has <1> which is not less than <1> at index 0'); + shouldPass( + d, pairwiseCompare(e, (int e, int a) => a * a == e, 'square root of')); + shouldFail( + d, + pairwiseCompare(e, (int e, int a) => a + a == e, 'double'), + 'Expected: pairwise double [1, 4, 9] ' + 'Actual: [1, 2, 3] ' + 'Which: has <1> which is not double <1> at index 0'); + shouldFail( + 'not an iterable', + pairwiseCompare(e, (int e, int a) => a + a == e, 'double'), + endsWith('not an ')); + }); + + test('isEmpty', () { + var d = SimpleIterable(0); + var e = SimpleIterable(1); + shouldPass(d, isEmpty); + shouldFail( + e, + isEmpty, + 'Expected: empty ' + 'Actual: SimpleIterable:[1]'); + }); + + test('isNotEmpty', () { + var d = SimpleIterable(0); + var e = SimpleIterable(1); + shouldPass(e, isNotEmpty); + shouldFail( + d, + isNotEmpty, + 'Expected: non-empty ' + 'Actual: SimpleIterable:[]'); + }); + + test('contains', () { + var d = SimpleIterable(3); + shouldPass(d, contains(2)); + shouldFail( + d, + contains(5), + 'Expected: contains <5> ' + 'Actual: SimpleIterable:[3, 2, 1] ' + 'Which: does not contain <5>'); + }); +} diff --git a/pkgs/matcher/test/map_matchers_test.dart b/pkgs/matcher/test/map_matchers_test.dart new file mode 100644 index 000000000..4c699ab88 --- /dev/null +++ b/pkgs/matcher/test/map_matchers_test.dart @@ -0,0 +1,100 @@ +import 'package:matcher/matcher.dart' + show contains, containsValue, containsPair; +import 'package:test/test.dart' show test; + +import 'test_utils.dart'; + +void main() { + test('contains', () { + shouldPass({'a': 1}, contains('a')); + shouldPass({null: 1}, contains(null)); + shouldFail( + {'a': 1}, + contains(2), + 'Expected: contains <2> ' + 'Actual: {\'a\': 1} ' + 'Which: does not contain <2>', + ); + shouldFail( + {'a': 1}, + contains(null), + 'Expected: contains ' + 'Actual: {\'a\': 1} ' + 'Which: does not contain ', + ); + }); + + test('containsValue', () { + shouldPass({'a': 1, 'null': null}, containsValue(1)); + shouldPass({'a': 1, 'null': null}, containsValue(null)); + shouldFail( + {'a': 1, 'null': null}, + containsValue(2), + 'Expected: contains value <2> ' + "Actual: {'a': 1, 'null': null}", + ); + }); + + test('containsPair', () { + shouldPass({'a': 1, 'null': null}, containsPair('a', 1)); + shouldPass({'a': 1, 'null': null}, containsPair('null', null)); + shouldPass({null: null}, containsPair(null, null)); + shouldFail( + {'a': 1, 'null': null}, + containsPair('a', 2), + "Expected: contains pair 'a' => <2> " + "Actual: {'a': 1, 'null': null} " + "Which: contains key 'a' but with value is <1>", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('b', 1), + "Expected: contains pair 'b' => <1> " + "Actual: {'a': 1, 'null': null} " + "Which: doesn't contain key 'b'", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('null', 2), + "Expected: contains pair 'null' => <2> " + "Actual: {'a': 1, 'null': null} " + "Which: contains key 'null' but with value is ", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('2', null), + "Expected: contains pair '2' => " + "Actual: {'a': 1, 'null': null} " + "Which: doesn't contain key '2'", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('2', 'b'), + "Expected: contains pair '2' => 'b' " + "Actual: {'a': 1, 'null': null} " + "Which: doesn't contain key '2'", + ); + shouldFail( + {null: null}, + containsPair('not null', null), + "Expected: contains pair 'not null' => " + 'Actual: {null: null} ' + "Which: doesn't contain key 'not null'", + ); + shouldFail( + {null: null}, + containsPair(null, 'not null'), + 'Expected: contains pair => \'not null\' ' + 'Actual: {null: null} ' + 'Which: contains key but with value not an ' + '', + ); + shouldFail( + {null: null}, + containsPair('not null', 'not null'), + 'Expected: contains pair \'not null\' => \'not null\' ' + 'Actual: {null: null} ' + 'Which: doesn\'t contain key \'not null\' ', + ); + }); +} diff --git a/pkgs/matcher/test/matcher/completion_test.dart b/pkgs/matcher/test/matcher/completion_test.dart new file mode 100644 index 000000000..9259cd187 --- /dev/null +++ b/pkgs/matcher/test/matcher/completion_test.dart @@ -0,0 +1,192 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('[doesNotComplete]', () { + test('fails when provided a non future', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, doesNotComplete); + }); + + expectTestFailed(monitor, contains('10 is not a Future')); + }); + + test('succeeds when a future does not complete', () { + var completer = Completer(); + expect(completer.future, doesNotComplete); + }); + + test('fails when a future does complete', () async { + var monitor = await TestCaseMonitor.run(() { + var completer = Completer(); + completer.complete(null); + expect(completer.future, doesNotComplete); + }); + + expectTestFailed( + monitor, + 'Future was not expected to complete but completed with a value of' + ' null'); + }); + + test('fails when a future completes after the expect', () async { + var monitor = await TestCaseMonitor.run(() { + var completer = Completer(); + expect(completer.future, doesNotComplete); + completer.complete(null); + }); + + expectTestFailed( + monitor, + 'Future was not expected to complete but completed with a value of' + ' null'); + }); + + test('fails when a future eventually completes', () async { + var monitor = await TestCaseMonitor.run(() { + var completer = Completer(); + expect(completer.future, doesNotComplete); + Future(() async { + await pumpEventQueue(times: 10); + }).then(completer.complete); + }); + + expectTestFailed( + monitor, + 'Future was not expected to complete but completed with a value of' + ' null'); + }); + }); + group('[completes]', () { + test('blocks the test until the Future completes', () async { + final completer = Completer(); + final monitor = TestCaseMonitor.start(() { + expect(completer.future, completes); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + completer.complete(); + await monitor.onDone; + expectTestPassed(monitor); + }); + + test('with an error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error('X'), completes); + }); + + expect(monitor.state, equals(State.failed)); + expect(monitor.errors, [isAsyncError(equals('X'))]); + }); + + test('with a failure', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error(TestFailure('oh no')), completes); + }); + + expectTestFailed(monitor, 'oh no'); + }); + + test('with a non-future', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, completes); + }); + + expectTestFailed( + monitor, + 'Expected: completes successfully\n' + ' Actual: <10>\n' + ' Which: was not a Future\n'); + }); + + test('with a successful future', () { + expect(Future.value('1'), completes); + }); + }); + + group('[completion]', () { + test('blocks the test until the Future completes', () async { + final completer = Completer(); + final monitor = TestCaseMonitor.start(() { + expect(completer.future, completion(isNull)); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + completer.complete(null); + await monitor.onDone; + expectTestPassed(monitor); + }); + + test('with an error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error('X'), completion(isNull)); + }); + + expect(monitor.state, equals(State.failed)); + expect(monitor.errors, [isAsyncError(equals('X'))]); + }); + + test('with a failure', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error(TestFailure('oh no')), completion(isNull)); + }); + + expectTestFailed(monitor, 'oh no'); + }); + + test('with a non-future', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, completion(equals(10))); + }); + + expectTestFailed( + monitor, + 'Expected: completes to a value that <10>\n' + ' Actual: <10>\n' + ' Which: was not a Future\n'); + }); + + test('with an incorrect value', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.value('a'), completion(equals('b'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: completes to a value that 'b'\n" + ' Actual: <'), + endsWith('>\n' + " Which: emitted 'a'\n" + ' which is different.\n' + ' Expected: b\n' + ' Actual: a\n' + ' ^\n' + ' Differ at offset 0\n') + ])); + }); + + test("blocks expectLater's Future", () async { + var completer = Completer(); + var fired = false; + unawaited(expectLater(completer.future, completion(equals(1))).then((_) { + fired = true; + })); + + await pumpEventQueue(); + expect(fired, isFalse); + + completer.complete(1); + await pumpEventQueue(); + expect(fired, isTrue); + }); + }); +} diff --git a/pkgs/matcher/test/matcher/prints_test.dart b/pkgs/matcher/test/matcher/prints_test.dart new file mode 100644 index 000000000..cbdb12a63 --- /dev/null +++ b/pkgs/matcher/test/matcher/prints_test.dart @@ -0,0 +1,208 @@ +// 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 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('synchronous', () { + test('passes with an expected print', () { + expect(() => print('Hello, world!'), prints('Hello, world!\n')); + }); + + test('combines multiple prints', () { + expect(() { + print('Hello'); + print('World!'); + }, prints('Hello\nWorld!\n')); + }); + + test('works with a Matcher', () { + expect(() => print('Hello, world!'), prints(contains('Hello'))); + }); + + test('describes a failure nicely', () async { + void local() => print('Hello, world!'); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints('Goodbye, world!\n')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints 'Goodbye, world!\\n'\n" + " ''\n" + ' Actual: <'), + endsWith('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''\n" + ' which is different.\n' + ' Expected: Goodbye, w ...\n' + ' Actual: Hello, wor ...\n' + ' ^\n' + ' Differ at offset 0\n') + ])); + }); + + test('describes a failure with a non-descriptive Matcher nicely', () async { + void local() => print('Hello, world!'); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + endsWith('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''\n" + ' which does not contain \'Goodbye\'\n') + ])); + }); + + test('describes a failure with no text nicely', () async { + void local() {} + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: printed nothing\n' + ' which does not contain \'Goodbye\'\n') + ])); + }); + + test('with a non-function', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + "Expected: prints contains 'Goodbye'\n" + ' Actual: <10>\n' + ' Which: was not a unary Function\n'); + }); + }); + + group('asynchronous', () { + test('passes with an expected print', () { + expect(() => Future(() => print('Hello, world!')), + prints('Hello, world!\n')); + }); + + test('combines multiple prints', () { + expect( + () => Future(() { + print('Hello'); + print('World!'); + }), + prints('Hello\nWorld!\n')); + }); + + test('works with a Matcher', () { + expect(() => Future(() => print('Hello, world!')), + prints(contains('Hello'))); + }); + + test('describes a failure nicely', () async { + void local() => Future(() => print('Hello, world!')); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints('Goodbye, world!\n')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints 'Goodbye, world!\\n'\n" + " ''\n" + ' Actual: <'), + contains('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''\n" + ' which is different.\n' + ' Expected: Goodbye, w ...\n' + ' Actual: Hello, wor ...\n' + ' ^\n' + ' Differ at offset 0') + ])); + }); + + test('describes a failure with a non-descriptive Matcher nicely', () async { + void local() => Future(() => print('Hello, world!')); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + contains('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''") + ])); + }); + + test('describes a failure with no text nicely', () async { + void local() => Future.value(); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + contains('>\n' + ' Which: printed nothing') + ])); + }); + + test("won't let the test end until the Future completes", () async { + final completer = Completer(); + final monitor = TestCaseMonitor.start(() { + expect(() => completer.future, prints(isEmpty)); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + completer.complete(); + await monitor.onDone; + expectTestPassed(monitor); + }); + + test("blocks expectLater's Future", () async { + var completer = Completer(); + var fired = false; + + unawaited(expectLater(() { + scheduleMicrotask(() => print('hello!')); + return completer.future; + }, prints('hello!\n')) + .then((_) { + fired = true; + })); + + await pumpEventQueue(); + expect(fired, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(fired, isTrue); + }); + }); +} diff --git a/pkgs/matcher/test/matcher/throws_test.dart b/pkgs/matcher/test/matcher/throws_test.dart new file mode 100644 index 000000000..25b93a98c --- /dev/null +++ b/pkgs/matcher/test/matcher/throws_test.dart @@ -0,0 +1,282 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('synchronous', () { + group('[throws]', () { + test('with a function that throws an error', () { + // ignore: deprecated_member_use_from_same_package + expect(() => throw 'oh no', throws); + }); + + test("with a function that doesn't throw", () async { + void local() {} + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(local, throws); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith('Expected: throws\n' + ' Actual: <'), + endsWith('>\n' + ' Which: returned \n') + ])); + }); + + test('with a non-function', () async { + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(10, throws); + }); + + expectTestFailed( + monitor, + 'Expected: throws\n' + ' Actual: <10>\n' + ' Which: was not a Function or Future\n'); + }); + }); + + group('[throwsA]', () { + test('with a function that throws an identical error', () { + expect(() => throw 'oh no', throwsA('oh no')); + }); + + test('with a function that throws a matching error', () { + expect(() => throw const FormatException('bad'), + throwsA(isFormatException)); + }); + + test("with a function that doesn't throw", () async { + void local() {} + var monitor = await TestCaseMonitor.run(() { + expect(local, throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: returned \n') + ])); + }); + + test('with a non-function', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, throwsA('oh no')); + }); + + expectTestFailed( + monitor, + "Expected: throws 'oh no'\n" + ' Actual: <10>\n' + ' Which: was not a Function or Future\n'); + }); + + test('with a function that throws the wrong error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(() => throw 'aw dang', throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + contains('>\n' + " Which: threw 'aw dang'\n" + ' stack'), + endsWith(' which is different.\n' + ' Expected: oh no\n' + ' Actual: aw dang\n' + ' ^\n' + ' Differ at offset 0\n') + ])); + }); + }); + }); + + group('asynchronous', () { + group('[throws]', () { + test('with a Future that throws an error', () { + // ignore: deprecated_member_use_from_same_package + expect(Future.error('oh no'), throws); + }); + + test("with a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(Future.value(), throws); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith('Expected: throws\n' + ' Actual: <'), + endsWith('>\n' + ' Which: emitted \n') + ])); + }); + + test('with a closure that returns a Future that throws an error', () { + // ignore: deprecated_member_use_from_same_package + expect(() => Future.error('oh no'), throws); + }); + + test("with a closure that returns a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(Future.value, throws); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith('Expected: throws\n' + ' Actual: <'), + endsWith('>\n' + ' Which: returned a Future that emitted \n') + ])); + }); + + test("won't let the test end until the Future completes", () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + final completer = Completer(); + // ignore: deprecated_member_use_from_same_package + expect(completer.future, throws); + callback = () => completer.completeError('oh no'); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + callback(); + await monitor.onDone; + expectTestPassed(monitor); + }); + }); + + group('[throwsA]', () { + test('with a Future that throws an identical error', () { + expect(Future.error('oh no'), throwsA('oh no')); + }); + + test('with a Future that throws a matching error', () { + expect(Future.error(const FormatException('bad')), + throwsA(isFormatException)); + }); + + test("with a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.value(), throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: emitted \n') + ])); + }); + + test('with a Future that throws the wrong error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error('aw dang'), throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + contains('>\n' + " Which: threw 'aw dang'\n") + ])); + }); + + test('with a closure that returns a Future that throws a matching error', + () { + expect(() => Future.error(const FormatException('bad')), + throwsA(isFormatException)); + }); + + test("with a closure that returns a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.value, throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: returned a Future that emitted \n') + ])); + }); + + test('with closure that returns a Future that throws the wrong error', + () async { + var monitor = await TestCaseMonitor.run(() { + expect(() => Future.error('aw dang'), throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + contains('>\n' + " Which: threw 'aw dang'\n") + ])); + }); + + test("won't let the test end until the Future completes", () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + final completer = Completer(); + expect(completer.future, throwsA('oh no')); + callback = () => completer.completeError('oh no'); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + callback(); + await monitor.onDone; + + expectTestPassed(monitor); + }); + + test("blocks expectLater's Future", () async { + var completer = Completer(); + var fired = false; + unawaited(expectLater(completer.future, throwsArgumentError).then((_) { + fired = true; + })); + + await pumpEventQueue(); + expect(fired, isFalse); + + completer.completeError(ArgumentError('oh no')); + await pumpEventQueue(); + expect(fired, isTrue); + }); + }); + }); +} diff --git a/pkgs/matcher/test/matcher/throws_type_test.dart b/pkgs/matcher/test/matcher/throws_type_test.dart new file mode 100644 index 000000000..f4c55b014 --- /dev/null +++ b/pkgs/matcher/test/matcher/throws_type_test.dart @@ -0,0 +1,176 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: only_throw_errors + +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('[throwsArgumentError]', () { + test('passes when a ArgumentError is thrown', () { + expect(() => throw ArgumentError(''), throwsArgumentError); + }); + + test('fails when a non-ArgumentError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsArgumentError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws ")); + }); + }); + + group('[throwsConcurrentModificationError]', () { + test('passes when a ConcurrentModificationError is thrown', () { + expect(() => throw ConcurrentModificationError(''), + throwsConcurrentModificationError); + }); + + test('fails when a non-ConcurrentModificationError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsConcurrentModificationError); + }); + + expectTestFailed( + liveTest, + startsWith( + "Expected: throws ")); + }); + }); + + group('[throwsCyclicInitializationError]', () { + test('passes when a CyclicInitializationError is thrown', () { + expect( + () => _CyclicInitializationFailure().x, + // ignore: deprecated_member_use_from_same_package + throwsCyclicInitializationError); + }); + + test('fails when a non-CyclicInitializationError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(() => throw Exception(), throwsCyclicInitializationError); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws ")); + }); + }); + + group('[throwsException]', () { + test('passes when a Exception is thrown', () { + expect(() => throw Exception(''), throwsException); + }); + + test('fails when a non-Exception is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw 'oh no', throwsException); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws ")); + }); + }); + + group('[throwsFormatException]', () { + test('passes when a FormatException is thrown', () { + expect(() => throw const FormatException(''), throwsFormatException); + }); + + test('fails when a non-FormatException is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsFormatException); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws ")); + }); + }); + + group('[throwsNoSuchMethodError]', () { + test('passes when a NoSuchMethodError is thrown', () { + expect(() { + (1 as dynamic).notAMethodOnInt(); + }, throwsNoSuchMethodError); + }); + + test('fails when a non-NoSuchMethodError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsNoSuchMethodError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws ")); + }); + }); + + group('[throwsRangeError]', () { + test('passes when a RangeError is thrown', () { + expect(() => throw RangeError(''), throwsRangeError); + }); + + test('fails when a non-RangeError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsRangeError); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws ")); + }); + }); + + group('[throwsStateError]', () { + test('passes when a StateError is thrown', () { + expect(() => throw StateError(''), throwsStateError); + }); + + test('fails when a non-StateError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsStateError); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws ")); + }); + }); + + group('[throwsUnimplementedError]', () { + test('passes when a UnimplementedError is thrown', () { + expect(() => throw UnimplementedError(''), throwsUnimplementedError); + }); + + test('fails when a non-UnimplementedError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsUnimplementedError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws ")); + }); + }); + + group('[throwsUnsupportedError]', () { + test('passes when a UnsupportedError is thrown', () { + expect(() => throw UnsupportedError(''), throwsUnsupportedError); + }); + + test('fails when a non-UnsupportedError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsUnsupportedError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws ")); + }); + }); +} + +class _CyclicInitializationFailure { + late int x = y; + late int y = x; +} diff --git a/pkgs/matcher/test/mirror_matchers_test.dart b/pkgs/matcher/test/mirror_matchers_test.dart new file mode 100644 index 000000000..b19fe3ea6 --- /dev/null +++ b/pkgs/matcher/test/mirror_matchers_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2012, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +@TestOn('vm') + +import 'package:matcher/mirror_matchers.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +class C { + int instanceField = 1; + int get instanceGetter => 2; + static int staticField = 3; + static int get staticGetter => 4; +} + +void main() { + test('hasProperty', () { + var foo = [3]; + shouldPass(foo, hasProperty('length', 1)); + shouldFail( + foo, + hasProperty('foo'), + 'Expected: has property "foo" ' + 'Actual: [3] ' + 'Which: has no property named "foo"'); + shouldFail( + foo, + hasProperty('length', 2), + 'Expected: has property "length" which matches <2> ' + 'Actual: [3] ' + 'Which: has property "length" with value <1>'); + var c = C(); + shouldPass(c, hasProperty('instanceField', 1)); + shouldPass(c, hasProperty('instanceGetter', 2)); + shouldFail( + c, + hasProperty('staticField'), + 'Expected: has property "staticField" ' + 'Actual: ' + 'Which: has a member named "staticField",' + ' but it is not an instance property'); + shouldFail( + c, + hasProperty('staticGetter'), + 'Expected: has property "staticGetter" ' + 'Actual: ' + 'Which: has a member named "staticGetter",' + ' but it is not an instance property'); + }); +} diff --git a/pkgs/matcher/test/never_called_test.dart b/pkgs/matcher/test/never_called_test.dart new file mode 100644 index 000000000..4c83e39db --- /dev/null +++ b/pkgs/matcher/test/never_called_test.dart @@ -0,0 +1,75 @@ +// Copyright (c) 2017, 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 'package:term_glyph/term_glyph.dart' as glyph; +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import 'utils_new.dart'; + +void main() { + setUpAll(() { + glyph.ascii = true; + }); + + test("doesn't throw if it isn't called", () async { + var monitor = await TestCaseMonitor.run(() { + const Stream.empty().listen(neverCalled); + }); + + expectTestPassed(monitor); + }); + + group("if it's called", () { + test('throws', () async { + var monitor = await TestCaseMonitor.run(() { + neverCalled(); + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with no ' + 'arguments.'); + }); + + test('pretty-prints arguments', () async { + var monitor = await TestCaseMonitor.run(() { + neverCalled(1, 'foo\nbar'); + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with:\n' + '* <1>\n' + "* 'foo\\n'\n" + " 'bar'"); + }); + + test('keeps the test alive', () async { + var monitor = await TestCaseMonitor.run(() { + pumpEventQueue(times: 10).then(neverCalled); + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with:\n' + '* '); + }); + + test("can't be caught", () async { + var monitor = await TestCaseMonitor.run(() { + try { + neverCalled(); + } catch (_) { + // Do nothing. + } + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with ' + 'no arguments.'); + }); + }); +} diff --git a/pkgs/matcher/test/numeric_matchers_test.dart b/pkgs/matcher/test/numeric_matchers_test.dart new file mode 100644 index 000000000..39195889f --- /dev/null +++ b/pkgs/matcher/test/numeric_matchers_test.dart @@ -0,0 +1,98 @@ +// Copyright (c) 2012, 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 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + test('closeTo', () { + shouldPass(0, closeTo(0, 1)); + shouldPass(-1, closeTo(0, 1)); + shouldPass(1, closeTo(0, 1)); + shouldFail( + 1.001, + closeTo(0, 1), + 'Expected: a numeric value within <1> of <0> ' + 'Actual: <1.001> ' + 'Which: differs by <1.001>'); + shouldFail( + -1.001, + closeTo(0, 1), + 'Expected: a numeric value within <1> of <0> ' + 'Actual: <-1.001> ' + 'Which: differs by <1.001>'); + shouldFail( + 'not a num', closeTo(0, 1), endsWith('not an ')); + }); + + test('inInclusiveRange', () { + shouldFail( + -1, + inInclusiveRange(0, 2), + 'Expected: be in range from 0 (inclusive) to 2 (inclusive) ' + 'Actual: <-1>'); + shouldPass(0, inInclusiveRange(0, 2)); + shouldPass(1, inInclusiveRange(0, 2)); + shouldPass(2, inInclusiveRange(0, 2)); + shouldFail( + 3, + inInclusiveRange(0, 2), + 'Expected: be in range from 0 (inclusive) to 2 (inclusive) ' + 'Actual: <3>'); + shouldFail('not a num', inInclusiveRange(0, 1), + endsWith('not an ')); + }); + + test('inExclusiveRange', () { + shouldFail( + 0, + inExclusiveRange(0, 2), + 'Expected: be in range from 0 (exclusive) to 2 (exclusive) ' + 'Actual: <0>'); + shouldPass(1, inExclusiveRange(0, 2)); + shouldFail( + 2, + inExclusiveRange(0, 2), + 'Expected: be in range from 0 (exclusive) to 2 (exclusive) ' + 'Actual: <2>'); + shouldFail('not a num', inExclusiveRange(0, 1), + endsWith('not an ')); + }); + + test('inOpenClosedRange', () { + shouldFail( + 0, + inOpenClosedRange(0, 2), + 'Expected: be in range from 0 (exclusive) to 2 (inclusive) ' + 'Actual: <0>'); + shouldPass(1, inOpenClosedRange(0, 2)); + shouldPass(2, inOpenClosedRange(0, 2)); + shouldFail('not a num', inOpenClosedRange(0, 1), + endsWith('not an ')); + }); + + test('inClosedOpenRange', () { + shouldPass(0, inClosedOpenRange(0, 2)); + shouldPass(1, inClosedOpenRange(0, 2)); + shouldFail( + 2, + inClosedOpenRange(0, 2), + 'Expected: be in range from 0 (inclusive) to 2 (exclusive) ' + 'Actual: <2>'); + shouldFail('not a num', inClosedOpenRange(0, 1), + endsWith('not an ')); + }); + + group('NaN', () { + test('inInclusiveRange', () { + shouldFail( + double.nan, + inExclusiveRange(double.negativeInfinity, double.infinity), + 'Expected: be in range from ' + '-Infinity (exclusive) to Infinity (exclusive) ' + 'Actual: '); + }); + }); +} diff --git a/pkgs/matcher/test/operator_matchers_test.dart b/pkgs/matcher/test/operator_matchers_test.dart new file mode 100644 index 000000000..f4b6d3a45 --- /dev/null +++ b/pkgs/matcher/test/operator_matchers_test.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2012, 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 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, expect, throwsArgumentError; + +import 'test_utils.dart'; + +void main() { + test('anyOf', () { + // with a list + shouldFail( + 0, anyOf([equals(1), equals(2)]), 'Expected: (<1> or <2>) Actual: <0>'); + shouldPass(1, anyOf([equals(1), equals(2)])); + + // with individual items + shouldFail( + 0, anyOf(equals(1), equals(2)), 'Expected: (<1> or <2>) Actual: <0>'); + shouldPass(1, anyOf(equals(1), equals(2))); + }); + + test('allOf', () { + // with a list + shouldPass(1, allOf([lessThan(10), greaterThan(0)])); + shouldFail( + -1, + allOf([lessThan(10), greaterThan(0)]), + 'Expected: (a value less than <10> and a value greater than <0>) ' + 'Actual: <-1> ' + 'Which: is not a value greater than <0>'); + + // with individual items + shouldPass(1, allOf(lessThan(10), greaterThan(0))); + shouldFail( + -1, + allOf(lessThan(10), greaterThan(0)), + 'Expected: (a value less than <10> and a value greater than <0>) ' + 'Actual: <-1> ' + 'Which: is not a value greater than <0>'); + + // with maximum items + shouldPass( + 1, + allOf(lessThan(10), lessThan(9), lessThan(8), lessThan(7), lessThan(6), + lessThan(5), lessThan(4))); + shouldFail( + 4, + allOf(lessThan(10), lessThan(9), lessThan(8), lessThan(7), lessThan(6), + lessThan(5), lessThan(4)), + 'Expected: (a value less than <10> and a value less than <9> and a ' + 'value less than <8> and a value less than <7> and a value less than ' + '<6> and a value less than <5> and a value less than <4>) ' + 'Actual: <4> ' + 'Which: is not a value less than <4>'); + }); + + test('If the first argument is a List, the rest must be null', () { + expect(() => allOf([], 5), throwsArgumentError); + expect( + () => anyOf([], null, null, null, null, null, 42), throwsArgumentError); + }); +} diff --git a/pkgs/matcher/test/order_matchers_test.dart b/pkgs/matcher/test/order_matchers_test.dart new file mode 100644 index 000000000..8a7c3df6c --- /dev/null +++ b/pkgs/matcher/test/order_matchers_test.dart @@ -0,0 +1,149 @@ +// Copyright (c) 2012, 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 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + test('greaterThan', () { + shouldPass(10, greaterThan(9)); + shouldFail( + 9, + greaterThan(10), + 'Expected: a value greater than <10> ' + 'Actual: <9> ' + 'Which: is not a value greater than <10>'); + }); + + test('greaterThanOrEqualTo', () { + shouldPass(10, greaterThanOrEqualTo(10)); + shouldFail( + 9, + greaterThanOrEqualTo(10), + 'Expected: a value greater than or equal to <10> ' + 'Actual: <9> ' + 'Which: is not a value greater than or equal to <10>'); + }); + + test('lessThan', () { + shouldFail( + 10, + lessThan(9), + 'Expected: a value less than <9> ' + 'Actual: <10> ' + 'Which: is not a value less than <9>'); + shouldPass(9, lessThan(10)); + }); + + test('lessThanOrEqualTo', () { + shouldPass(10, lessThanOrEqualTo(10)); + shouldFail( + 11, + lessThanOrEqualTo(10), + 'Expected: a value less than or equal to <10> ' + 'Actual: <11> ' + 'Which: is not a value less than or equal to <10>'); + }); + + test('isZero', () { + shouldPass(0, isZero); + shouldFail( + 1, + isZero, + 'Expected: a value equal to <0> ' + 'Actual: <1> ' + 'Which: is not a value equal to <0>'); + }); + + test('isNonZero', () { + shouldFail( + 0, + isNonZero, + 'Expected: a value not equal to <0> ' + 'Actual: <0> ' + 'Which: is not a value not equal to <0>'); + shouldPass(1, isNonZero); + }); + + test('isPositive', () { + shouldFail( + -1, + isPositive, + 'Expected: a positive value ' + 'Actual: <-1> ' + 'Which: is not a positive value'); + shouldFail( + 0, + isPositive, + 'Expected: a positive value ' + 'Actual: <0> ' + 'Which: is not a positive value'); + shouldPass(1, isPositive); + }); + + test('isNegative', () { + shouldPass(-1, isNegative); + shouldFail( + 0, + isNegative, + 'Expected: a negative value ' + 'Actual: <0> ' + 'Which: is not a negative value'); + }); + + test('isNonPositive', () { + shouldPass(-1, isNonPositive); + shouldPass(0, isNonPositive); + shouldFail( + 1, + isNonPositive, + 'Expected: a non-positive value ' + 'Actual: <1> ' + 'Which: is not a non-positive value'); + }); + + test('isNonNegative', () { + shouldPass(1, isNonNegative); + shouldPass(0, isNonNegative); + shouldFail( + -1, + isNonNegative, + 'Expected: a non-negative value ' + 'Actual: <-1> ' + 'Which: is not a non-negative value'); + }); + + group('NaN', () { + test('greaterThan', () { + shouldFail( + double.nan, + greaterThan(10), + 'Expected: a value greater than <10> ' + 'Actual: ' + 'Which: is not a value greater than <10>'); + shouldFail( + 10, + greaterThan(double.nan), + 'Expected: a value greater than ' + 'Actual: <10> ' + 'Which: is not a value greater than '); + }); + + test('lessThanOrEqualTo', () { + shouldFail( + double.nan, + lessThanOrEqualTo(10), + 'Expected: a value less than or equal to <10> ' + 'Actual: ' + 'Which: is not a value less than or equal to <10>'); + shouldFail( + 10, + lessThanOrEqualTo(double.nan), + 'Expected: a value less than or equal to ' + 'Actual: <10> ' + 'Which: is not a value less than or equal to '); + }); + }); +} diff --git a/pkgs/matcher/test/pretty_print_test.dart b/pkgs/matcher/test/pretty_print_test.dart new file mode 100644 index 000000000..184704b87 --- /dev/null +++ b/pkgs/matcher/test/pretty_print_test.dart @@ -0,0 +1,263 @@ +// Copyright (c) 2013, 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:collection'; + +import 'package:matcher/matcher.dart'; +import 'package:matcher/src/pretty_print.dart'; +import 'package:test/test.dart' show group, test, expect; + +class DefaultToString {} + +class CustomToString { + @override + String toString() => 'string representation'; +} + +class _PrivateName { + @override + String toString() => 'string representation'; +} + +class _PrivateNameIterable extends IterableMixin { + @override + Iterator get iterator => [1, 2, 3].iterator; +} + +void main() { + test('with primitive objects', () { + expect(prettyPrint(12), equals('<12>')); + expect(prettyPrint(12.13), equals('<12.13>')); + expect(prettyPrint(true), equals('')); + expect(prettyPrint(null), equals('')); + expect(prettyPrint(() => 12), matches(r'')); + }); + + group('with a string', () { + test('containing simple characters', () { + expect(prettyPrint('foo'), equals("'foo'")); + }); + + test('containing newlines', () { + expect( + prettyPrint('foo\nbar\nbaz'), + equals("'foo\\n'\n" + " 'bar\\n'\n" + " 'baz'")); + }); + + test('containing escapable characters', () { + expect(prettyPrint("foo\rbar\tbaz'qux\v"), + equals(r"'foo\rbar\tbaz\'qux\v'")); + }); + }); + + group('with an iterable', () { + test('containing primitive objects', () { + expect(prettyPrint([1, true, 'foo']), equals("[1, true, 'foo']")); + }); + + test('containing a multiline string', () { + expect( + prettyPrint(['foo', 'bar\nbaz\nbip', 'qux']), + equals('[\n' + " 'foo',\n" + " 'bar\\n'\n" + " 'baz\\n'\n" + " 'bip',\n" + " 'qux'\n" + ']')); + }); + + test('containing a matcher', () { + expect(prettyPrint(['foo', endsWith('qux')]), + equals("['foo', ]")); + }); + + test("that's under maxLineLength", () { + expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 30), + equals('[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]')); + }); + + test("that's over maxLineLength", () { + expect( + prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 29), + equals('[\n' + ' 0,\n' + ' 1,\n' + ' 2,\n' + ' 3,\n' + ' 4,\n' + ' 5,\n' + ' 6,\n' + ' 7,\n' + ' 8,\n' + ' 9\n' + ']')); + }); + + test('factors indentation into maxLineLength', () { + expect( + prettyPrint([ + 'foo\nbar', + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ], maxLineLength: 30), + equals('[\n' + " 'foo\\n'\n" + " 'bar',\n" + ' [\n' + ' 0,\n' + ' 1,\n' + ' 2,\n' + ' 3,\n' + ' 4,\n' + ' 5,\n' + ' 6,\n' + ' 7,\n' + ' 8,\n' + ' 9\n' + ' ]\n' + ']')); + }); + + test("that's under maxItems", () { + expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 10), + equals('[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]')); + }); + + test("that's over maxItems", () { + expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 9), + equals('[0, 1, 2, 3, 4, 5, 6, 7, ...]')); + }); + + test("that's recursive", () { + var list = [1, 2, 3]; + list.add(list); + expect(prettyPrint(list), equals('[1, 2, 3, (recursive)]')); + }); + }); + + group('with a map', () { + test('containing primitive objects', () { + expect(prettyPrint({'foo': 1, 'bar': true}), + equals("{'foo': 1, 'bar': true}")); + }); + + test('containing a multiline string key', () { + expect( + prettyPrint({'foo\nbar': 1, 'bar': true}), + equals('{\n' + " 'foo\\n'\n" + " 'bar': 1,\n" + " 'bar': true\n" + '}')); + }); + + test('containing a multiline string value', () { + expect( + prettyPrint({'foo': 'bar\nbaz', 'qux': true}), + equals('{\n' + " 'foo': 'bar\\n'\n" + " 'baz',\n" + " 'qux': true\n" + '}')); + }); + + test('containing a multiline string key/value pair', () { + expect( + prettyPrint({'foo\nbar': 'baz\nqux'}), + equals('{\n' + " 'foo\\n'\n" + " 'bar': 'baz\\n'\n" + " 'qux'\n" + '}')); + }); + + test('containing a matcher key', () { + expect(prettyPrint({endsWith('bar'): 'qux'}), + equals("{: 'qux'}")); + }); + + test('containing a matcher value', () { + expect(prettyPrint({'foo': endsWith('qux')}), + equals("{'foo': }")); + }); + + test("that's under maxLineLength", () { + expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 32), + equals("{'0': 1, '2': 3, '4': 5, '6': 7}")); + }); + + test("that's over maxLineLength", () { + expect( + prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 31), + equals('{\n' + " '0': 1,\n" + " '2': 3,\n" + " '4': 5,\n" + " '6': 7\n" + '}')); + }); + + test('factors indentation into maxLineLength', () { + expect( + prettyPrint([ + 'foo\nbar', + {'0': 1, '2': 3, '4': 5, '6': 7} + ], maxLineLength: 32), + equals('[\n' + " 'foo\\n'\n" + " 'bar',\n" + ' {\n' + " '0': 1,\n" + " '2': 3,\n" + " '4': 5,\n" + " '6': 7\n" + ' }\n' + ']')); + }); + + test("that's under maxItems", () { + expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 4), + equals("{'0': 1, '2': 3, '4': 5, '6': 7}")); + }); + + test("that's over maxItems", () { + expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 3), + equals("{'0': 1, '2': 3, ...}")); + }); + }); + group('with an object', () { + test('with a default [toString]', () { + expect(prettyPrint(DefaultToString()), + equals("")); + }); + + test('with a custom [toString]', () { + expect(prettyPrint(CustomToString()), + equals('CustomToString:')); + }); + + test('with a custom [toString] and a private name', () { + expect(prettyPrint(_PrivateName()), + equals('_PrivateName:')); + }); + }); + + group('with an iterable', () { + test("that's not a list", () { + expect(prettyPrint([1, 2, 3, 4].map((n) => n * 2)), + equals('MappedListIterable:[2, 4, 6, 8]')); + }); + + test("that's not a list and has a private name", () { + expect(prettyPrint(_PrivateNameIterable()), + equals('_PrivateNameIterable:[1, 2, 3]')); + }); + }); + + test('Type', () { + expect(prettyPrint(''.runtimeType), 'Type:'); + }); +} diff --git a/pkgs/matcher/test/stream_matcher_test.dart b/pkgs/matcher/test/stream_matcher_test.dart new file mode 100644 index 000000000..c4af6665c --- /dev/null +++ b/pkgs/matcher/test/stream_matcher_test.dart @@ -0,0 +1,358 @@ +// Copyright (c) 2017, 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 'package:async/async.dart'; +import 'package:term_glyph/term_glyph.dart' as glyph; +import 'package:test/test.dart'; + +import 'utils_new.dart'; + +void main() { + setUpAll(() { + glyph.ascii = true; + }); + + late Stream stream; + late StreamQueue queue; + late Stream errorStream; + late StreamQueue errorQueue; + setUp(() { + stream = Stream.fromIterable([1, 2, 3, 4, 5]); + queue = StreamQueue(Stream.fromIterable([1, 2, 3, 4, 5])); + errorStream = Stream.fromFuture(Future.error('oh no!', StackTrace.current)); + errorQueue = StreamQueue( + Stream.fromFuture(Future.error('oh no!', StackTrace.current))); + }); + + group('emits()', () { + test('matches the first event of a Stream', () { + expect(stream, emits(1)); + }); + + test('rejects the first event of a Stream', () { + expect( + expectLater(stream, emits(2)), + throwsTestFailure(allOf([ + startsWith('Expected: should emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + + test('matches and consumes the next event of a StreamQueue', () { + expect(queue, emits(1)); + expect(queue.next, completion(equals(2))); + expect(queue, emits(3)); + expect(queue.next, completion(equals(4))); + }); + + test('rejects and does not consume the first event of a StreamQueue', () { + expect( + expectLater(queue, emits(2)), + throwsTestFailure(allOf([ + startsWith('Expected: should emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + + expect(queue, emits(1)); + }); + + test('rejects an empty stream', () { + expect( + expectLater(const Stream.empty(), emits(1)), + throwsTestFailure(allOf([ + startsWith('Expected: should emit an event that <1>\n'), + endsWith(' Which: emitted x Stream closed.\n') + ]))); + }); + + test('forwards a stream error', () { + expect(expectLater(errorStream, emits(1)), throwsA('oh no!')); + }); + + test('wraps a normal matcher', () { + expect(queue, emits(lessThan(5))); + expect(expectLater(queue, emits(greaterThan(5))), + throwsTestFailure(anything)); + }); + + test('returns a StreamMatcher as-is', () { + expect(queue, emits(emitsThrough(4))); + expect(queue, emits(5)); + }); + }); + + group('emitsDone', () { + test('succeeds for an empty stream', () { + expect(const Stream.empty(), emitsDone); + }); + + test('fails for a stream with events', () { + expect( + expectLater(stream, emitsDone), + throwsTestFailure(allOf([ + startsWith('Expected: should be done\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + }); + + group('emitsError()', () { + test('consumes a matching error', () { + expect(errorQueue, emitsError('oh no!')); + expect(errorQueue.hasNext, completion(isFalse)); + }); + + test('fails for a non-matching error', () { + expect( + expectLater(errorStream, emitsError('oh heck')), + throwsTestFailure(allOf([ + startsWith("Expected: should emit an error that 'oh heck'\n"), + contains(' Which: emitted ! oh no!\n'), + contains(' x Stream closed.\n' + " which threw 'oh no!'\n" + ' stack '), + endsWith(' which is different.\n' + ' Expected: oh heck\n' + ' Actual: oh no!\n' + ' ^\n' + ' Differ at offset 3\n') + ]))); + }); + + test('fails for a stream with events', () { + expect( + expectLater(stream, emitsDone), + throwsTestFailure(allOf([ + startsWith('Expected: should be done\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + }); + + group('mayEmit()', () { + test('consumes a matching event', () { + expect(queue, mayEmit(1)); + expect(queue, emits(2)); + }); + + test('allows a non-matching event', () { + expect(queue, mayEmit('fish')); + expect(queue, emits(1)); + }); + }); + + group('emitsAnyOf()', () { + test('consumes an event that matches a matcher', () { + expect(queue, emitsAnyOf([2, 1, 3])); + expect(queue, emits(2)); + }); + + test('consumes as many events as possible', () { + expect( + queue, + emitsAnyOf([ + 1, + emitsInOrder([1, 2]), + emitsInOrder([1, 2, 3]) + ])); + + expect(queue, emits(4)); + }); + + test('fails if no matchers match', () { + expect( + expectLater(stream, emitsAnyOf([2, 3, 4])), + throwsTestFailure(allOf([ + startsWith('Expected: should do one of the following:\n' + ' * emit an event that <2>\n' + ' * emit an event that <3>\n' + ' * emit an event that <4>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + ' which failed all options:\n' + ' * failed to emit an event that <2>\n' + ' * failed to emit an event that <3>\n' + ' * failed to emit an event that <4>\n') + ]))); + }); + + test('allows an error if any matcher matches', () { + expect(errorStream, emitsAnyOf([1, 2, emitsError('oh no!')])); + }); + + test('rethrows an error if no matcher matches', () { + expect( + expectLater(errorStream, emitsAnyOf([1, 2, 3])), throwsA('oh no!')); + }); + }); + + group('emitsInOrder()', () { + test('consumes matching events', () { + expect(queue, emitsInOrder([1, 2, emitsThrough(4)])); + expect(queue, emits(5)); + }); + + test("fails if the matchers don't match in order", () { + expect( + expectLater(queue, emitsInOrder([1, 3, 2])), + throwsTestFailure(allOf([ + startsWith('Expected: should do the following in order:\n' + ' * emit an event that <1>\n' + ' * emit an event that <3>\n' + ' * emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + " which didn't emit an event that <3>\n") + ]))); + }); + }); + + group('emitsThrough()', () { + test('consumes events including those matching the matcher', () { + expect(queue, emitsThrough(emitsInOrder([3, 4]))); + expect(queue, emits(5)); + }); + + test('consumes the entire queue with emitsDone', () { + expect(queue, emitsThrough(emitsDone)); + expect(queue.hasNext, completion(isFalse)); + }); + + test('fails if the queue never matches the matcher', () { + expect( + expectLater(queue, emitsThrough(6)), + throwsTestFailure(allOf([ + startsWith('Expected: should eventually emit an event that <6>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + ' which never did emit an event that <6>\n') + ]))); + }); + }); + + group('mayEmitMultiple()', () { + test('consumes multiple instances of the given matcher', () { + expect(queue, mayEmitMultiple(lessThan(3))); + expect(queue, emits(3)); + }); + + test('consumes zero instances of the given matcher', () { + expect(queue, mayEmitMultiple(6)); + expect(queue, emits(1)); + }); + + test("doesn't rethrow errors", () { + expect(errorQueue, mayEmitMultiple(1)); + expect(errorQueue, emitsError('oh no!')); + }); + }); + + group('neverEmits()', () { + test('succeeds if the event never matches', () { + expect(queue, neverEmits(6)); + expect(queue, emits(1)); + }); + + test('fails if the event matches', () { + expect( + expectLater(stream, neverEmits(4)), + throwsTestFailure(allOf([ + startsWith('Expected: should never emit an event that <4>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + ' which after 3 events did emit an event that <4>\n') + ]))); + }); + + test('fails if emitsDone matches', () { + expect(expectLater(stream, neverEmits(emitsDone)), + throwsTestFailure(anything)); + }); + + test("doesn't rethrow errors", () { + expect(errorQueue, neverEmits(6)); + expect(errorQueue, emitsError('oh no!')); + }); + }); + + group('emitsInAnyOrder()', () { + test('consumes events that match in any order', () { + expect(queue, emitsInAnyOrder([3, 1, 2])); + expect(queue, emits(4)); + }); + + test("fails if the events don't match in any order", () { + expect( + expectLater(stream, emitsInAnyOrder([4, 1, 2])), + throwsTestFailure(allOf([ + startsWith('Expected: should do the following in any order:\n' + ' * emit an event that <4>\n' + ' * emit an event that <1>\n' + ' * emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + + test("doesn't rethrow if some ordering matches", () { + expect(errorQueue, emitsInAnyOrder([emitsDone, emitsError('oh no!')])); + }); + + test('rethrows if no ordering matches', () { + expect( + expectLater(errorQueue, emitsInAnyOrder([1, emitsError('oh no!')])), + throwsA('oh no!')); + }); + }); + + test('A custom StreamController doesn\'t hang on close', () async { + var controller = StreamController(); + var done = expectLater(controller.stream, emits(null)); + controller.add(null); + await done; + await controller.close(); + }); +} diff --git a/pkgs/matcher/test/string_matchers_test.dart b/pkgs/matcher/test/string_matchers_test.dart new file mode 100644 index 000000000..be9e768ce --- /dev/null +++ b/pkgs/matcher/test/string_matchers_test.dart @@ -0,0 +1,151 @@ +// Copyright (c) 2012, 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 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, expect; + +import 'test_utils.dart'; + +void main() { + test('Reports mismatches in whitespace and escape sequences', () { + shouldFail('before\nafter', equals('before\\nafter'), + contains('Differ at offset 7')); + }); + + test('Retains outer matcher mismatch text', () { + shouldFail( + {'word': 'thing'}, + containsPair('word', equals('notthing')), + allOf([ + contains("contains key 'word' but with value is different"), + contains('Differ at offset 0') + ])); + }); + + test('collapseWhitespace', () { + var source = '\t\r\n hello\t\r\n world\r\t \n'; + expect(collapseWhitespace(source), 'hello world'); + }); + + test('isEmpty', () { + shouldPass('', isEmpty); + shouldFail(null, isEmpty, startsWith('Expected: empty Actual: ')); + shouldFail(0, isEmpty, startsWith('Expected: empty Actual: <0>')); + shouldFail('a', isEmpty, startsWith("Expected: empty Actual: 'a'")); + }); + + // Regression test for: https://code.google.com/p/dart/issues/detail?id=21562 + test('isNot(isEmpty)', () { + shouldPass('a', isNot(isEmpty)); + shouldFail('', isNot(isEmpty), 'Expected: not empty Actual: \'\''); + shouldFail(null, isNot(isEmpty), + startsWith('Expected: not empty Actual: ')); + }); + + test('isNotEmpty', () { + shouldFail('', isNotEmpty, startsWith("Expected: non-empty Actual: ''")); + shouldFail( + null, isNotEmpty, startsWith('Expected: non-empty Actual: ')); + shouldFail(0, isNotEmpty, startsWith('Expected: non-empty Actual: <0>')); + shouldPass('a', isNotEmpty); + }); + + test('equalsIgnoringCase', () { + shouldPass('hello', equalsIgnoringCase('HELLO')); + shouldFail('hi', equalsIgnoringCase('HELLO'), + "Expected: 'HELLO' ignoring case Actual: 'hi'"); + shouldFail(42, equalsIgnoringCase('HELLO'), + endsWith('not an ')); + }); + + test('equalsIgnoringWhitespace', () { + shouldPass(' hello world ', equalsIgnoringWhitespace('hello world')); + shouldFail( + ' helloworld ', + equalsIgnoringWhitespace('hello world'), + "Expected: 'hello world' ignoring whitespace " + "Actual: ' helloworld ' " + "Which: is 'helloworld' with whitespace compressed"); + shouldFail(42, equalsIgnoringWhitespace('HELLO'), + endsWith('not an ')); + }); + + test('startsWith', () { + shouldPass('hello', startsWith('')); + shouldPass('hello', startsWith('hell')); + shouldPass('hello', startsWith('hello')); + shouldFail( + 'hello', + startsWith('hello '), + "Expected: a string starting with 'hello ' " + "Actual: 'hello'"); + shouldFail( + 42, startsWith('hello '), endsWith('not an ')); + }); + + test('endsWith', () { + shouldPass('hello', endsWith('')); + shouldPass('hello', endsWith('lo')); + shouldPass('hello', endsWith('hello')); + shouldFail( + 'hello', + endsWith(' hello'), + "Expected: a string ending with ' hello' " + "Actual: 'hello'"); + shouldFail( + 42, startsWith('hello '), endsWith('not an ')); + }); + + test('contains', () { + shouldPass('hello', contains('')); + shouldPass('hello', contains('h')); + shouldPass('hello', contains('o')); + shouldPass('hello', contains('hell')); + shouldPass('hello', contains('hello')); + shouldFail('hello', contains(' '), + "Expected: contains ' ' Actual: 'hello' Which: does not contain ' '"); + }); + + test('stringContainsInOrder', () { + shouldPass('goodbye cruel world', stringContainsInOrder([''])); + shouldPass('goodbye cruel world', stringContainsInOrder(['goodbye'])); + shouldPass('goodbye cruel world', stringContainsInOrder(['cruel'])); + shouldPass('goodbye cruel world', stringContainsInOrder(['world'])); + shouldPass( + 'goodbye cruel world', stringContainsInOrder(['good', 'bye', 'world'])); + shouldPass( + 'goodbye cruel world', stringContainsInOrder(['goodbye', 'cruel'])); + shouldPass( + 'goodbye cruel world', stringContainsInOrder(['cruel', 'world'])); + shouldPass('goodbye cruel world', + stringContainsInOrder(['goodbye', 'cruel', 'world'])); + shouldPass( + 'foo', stringContainsInOrder(['f', '', '', '', 'o', '', '', 'o'])); + + shouldFail( + 'abc', + stringContainsInOrder(['ab', 'bc']), + "Expected: a string containing 'ab', 'bc' in order " + "Actual: 'abc'"); + shouldFail( + 'hello', + stringContainsInOrder(['hello', 'hello']), + "Expected: a string containing 'hello', 'hello' in order " + "Actual: 'hello'"); + shouldFail( + 'goodbye cruel world', + stringContainsInOrder(['goo', 'cruel', 'bye']), + "Expected: a string containing 'goo', 'cruel', 'bye' in order " + "Actual: 'goodbye cruel world'"); + }); + + test('matches', () { + shouldPass('c0d', matches('[a-z][0-9][a-z]')); + shouldPass('c0d', matches(RegExp('[a-z][0-9][a-z]'))); + shouldFail('cOd', matches('[a-z][0-9][a-z]'), + "Expected: match '[a-z][0-9][a-z]' Actual: 'cOd'"); + shouldFail(42, matches('[a-z][0-9][a-z]'), + endsWith('not an ')); + }); +} diff --git a/pkgs/matcher/test/test_utils.dart b/pkgs/matcher/test/test_utils.dart new file mode 100644 index 000000000..67b61a12d --- /dev/null +++ b/pkgs/matcher/test/test_utils.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2012, 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 'package:test/test.dart'; + +void shouldFail(Object? value, Matcher matcher, Object? expected) { + var failed = false; + try { + expect(value, matcher); + } on TestFailure catch (err) { + failed = true; + + var errorString = err.message; + + if (expected is String) { + expect(errorString, equalsIgnoringWhitespace(expected)); + } else { + expect(errorString?.replaceAll('\n', ''), expected); + } + } + + expect(failed, isTrue, reason: 'Expected to fail.'); +} + +void shouldPass(Object? value, Matcher matcher) { + expect(value, matcher); +} + +void doesNotThrow() {} +void doesThrow() { + throw StateError('X'); +} + +class Widget { + int? price; +} + +class SimpleIterable extends Iterable { + final int count; + + SimpleIterable(this.count); + + @override + Iterator get iterator => _SimpleIterator(count); +} + +class _SimpleIterator implements Iterator { + int _count; + int _current; + + _SimpleIterator(this._count) : _current = -1; + + @override + bool moveNext() { + if (_count > 0) { + _current = _count; + _count--; + return true; + } + _current = -1; + return false; + } + + @override + int get current => _current; +} diff --git a/pkgs/matcher/test/type_matcher_test.dart b/pkgs/matcher/test/type_matcher_test.dart new file mode 100644 index 000000000..99d44596f --- /dev/null +++ b/pkgs/matcher/test/type_matcher_test.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2012, 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. + +// ignore_for_file: deprecated_member_use_from_same_package +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, group; + +import 'test_utils.dart'; + +void main() { + _test(isMap, {}, name: 'Map'); + _test(isList, [], name: 'List'); + _test(isArgumentError, ArgumentError()); + _test(isCastError, TypeError()); + _test(isException, const FormatException()); + _test(isFormatException, const FormatException()); + _test(isStateError, StateError('oops')); + _test(isRangeError, RangeError('oops')); + _test(isUnimplementedError, UnimplementedError('oops')); + _test(isUnsupportedError, UnsupportedError('oops')); + _test(isConcurrentModificationError, ConcurrentModificationError()); + _test(isCyclicInitializationError, Error()); + _test(isNoSuchMethodError, null, + name: 'NoSuchMethodError'); + _test(isNullThrownError, TypeError()); + + group('custom `TypeMatcher`', () { + _test(const isInstanceOf(), 'hello'); + _test(const _StringMatcher(), 'hello'); + _test(const TypeMatcher(), 'hello'); + _test(isA(), 'hello'); + }); +} + +void _test(Matcher typeMatcher, T matchingInstance, {String? name}) { + name ??= T.toString(); + group('for `$name`', () { + if (matchingInstance != null) { + test('succeeds', () { + shouldPass(matchingInstance, typeMatcher); + }); + } + + test('fails', () { + shouldFail( + const _TestType(), + typeMatcher, + "Expected: Actual: " + " Which: is not an instance of '$name'", + ); + }); + }); +} + +// Validate that existing implementations continue to work. +class _StringMatcher extends TypeMatcher { + const _StringMatcher() : super('String'); + + @override + bool matches(dynamic item, Map matchState) => item is String; +} + +class _TestType { + const _TestType(); +} diff --git a/pkgs/matcher/test/utils_new.dart b/pkgs/matcher/test/utils_new.dart new file mode 100644 index 000000000..7d85a0248 --- /dev/null +++ b/pkgs/matcher/test/utils_new.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2023, 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 'package:matcher/expect.dart'; +import 'package:test_api/hooks_testing.dart'; + +/// Asserts that [monitor] has completed and passed. +/// +/// If the test had any errors, they're surfaced nicely into the outer test. +void expectTestPassed(TestCaseMonitor monitor) { + // Since the test is expected to pass, we forward any current or future errors + // to the running test, because they're definitely unexpected and it is most + // useful for the error to point directly to the throw point. + for (var error in monitor.errors) { + Zone.current.handleUncaughtError(error.error, error.stackTrace); + } + monitor.onError.listen((error) { + Zone.current.handleUncaughtError(error.error, error.stackTrace); + }); + + expect(monitor.state, State.passed); +} + +/// Asserts that [monitor] failed with a single [TestFailure] whose message +/// matches [message]. +void expectTestFailed(TestCaseMonitor monitor, Object? message) { + expect(monitor.state, State.failed); + expect(monitor.errors, [isAsyncError(isTestFailure(message))]); +} + +/// Returns a matcher that matches a [AsyncError] with an `error` field matching +/// [errorMatcher]. +Matcher isAsyncError(Matcher errorMatcher) => + isA().having((e) => e.error, 'error', errorMatcher); + +/// Returns a matcher that matches a [TestFailure] with the given [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher isTestFailure(Object? message) => const TypeMatcher() + .having((e) => e.message, 'message', message); + +/// Returns a matcher that matches a callback or Future that throws a +/// [TestFailure] with the given [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher throwsTestFailure(Object? message) => throwsA(isTestFailure(message));